RefactorMCP catalog

Push Down Field

Moves a field from a class into the direct subclasses that need it: those whose code uses it, or whose callers reach it through a reference of the subclass's type. When nothing uses the field, every direct subclass gets a copy, so the declaration is never lost.

Arguments

None. The target is the field, by symbol: "target": { "symbol": "F:Shop.Employee.Quota" }.

Precondition

  • The class has at least one subclass declared in the solution.
  • The class does not use the field itself.
  • No code reaches the field through a reference of the class's type, or of a type above it, since such a reference would no longer have the field.
  • The field's type and initializer use nothing private to the class.
  • No receiving subclass already has a member of that name.
  • The result compiles.

Transformation

  • Each receiving subclass gets the field after its last field, or first when it has none, with its documentation comment, initializer and modifiers.
  • A use by a class further down counts as a use by the direct subclass it derives from.
  • In a sealed subclass a protected field becomes private.
  • The class's type parameters are written as the subclass's type arguments: List<TItem> in Store<TItem> becomes List<Order> in OrderStore : Store<Order>. Each subclass's file gains the usings the field needs.
  • A declaration of several fields is split, and only the target moves.

Preserved

  • Every use of the field, which now binds to the subclass's copy.
  • Comments around the field that belong to its neighbours.

Limitations

  • The field only moves one level: a field that a class further down uses goes to the direct subclass above that class, not to the class itself.
  • Usings the class's file no longer needs are left in place.

Error codes

CodeMeaning
no-subclassesno class in the solution derives from the field's class
used-by-basethe field's class uses the field itself
used-through-basecode reaches the field through a reference of the field's class
uses-base-private-membersthe field's type or initializer uses something private to its class
member-exists-in-subclassa receiving subclass already has a member of that name
breaks-compilationthe result would not compile

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#generic-base

Writes the base class's type parameter as the subclass's type argument and imports what the field needs

success
Targetsymbol F:Shop.Store`1.Cache
OrderStore.csmodified
1+using System.Collections.Generic;
2+
13 namespace Shop
24 {
35 public class Order
46 {
57 }
68
79 public class OrderStore : Store<Order>
810 {
11+ protected List<Order> Cache = new List<Order>();
12+
913 public override int Count() => Cache.Count;
1014 }
1115 }
Store.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public abstract class Store<TItem>
66 {
7− protected List<TItem> Cache = new List<TItem>();
8−
97 public abstract int Count();
108 }
119 }

#keeps-comments

Moves the field's documentation comment with it and leaves its neighbours' comments alone

success
Targetsymbol F:Shop.Employee.Territory
People.csmodified
11 namespace Shop
22 {
33 public class Employee
44 {
55 // Everyone has a name.
66 public string Name;
7−
8− /// <summary>Where a salesman sells.</summary>
9− public string Territory;
107
118 // Pay is monthly.
129 public decimal Salary;
1310 }
1411
1512 public class Salesman : Employee
1613 {
1714 // Sales so far this year.
1815 public decimal Sales;
16+ /// <summary>Where a salesman sells.</summary>
17+ public string Territory;
1918
2019 public string Region() => Territory.ToUpperInvariant();
2120 }
2221 }

#member-exists-in-subclass

Refuses when a subclass that would receive the field already has a member of that name

refusal
Targetsymbol F:Shop.Employee.Quota
Refusesmember-exists-in-subclass mentioning “Salesman”; every file is left unchanged
People.csinput
1 namespace Shop
2 {
3 public class Employee
4 {
5 public int Quota;
6 }
7
8 public class Salesman : Employee
9 {
10 public new string Quota = "high";
11 }
12 }

#no-subclasses

Refuses when no class derives from the field's class

refusal
Targetsymbol F:Shop.Employee.Quota
Refusesno-subclasses; every file is left unchanged
Employee.csinput
1 namespace Shop
2 {
3 public class Employee
4 {
5 public int Quota;
6 }
7 }

#nullable-annotations

Keeps nullable annotations on the moved field

success
Targetsymbol F:Shop.Employee.Territory
Projectnullable enable
People.csmodified
11 namespace Shop
22 {
33 public class Employee
44 {
5− public string? Territory;
65 }
76
87 public class Salesman : Employee
98 {
9+ public string? Territory;
10+
1011 public string Region() => Territory ?? "none";
1112 }
1213 }

#one-of-several-declarators

Splits a declaration of several fields and moves only the target

success
Targetsymbol F:Shop.Employee.Quota
People.csmodified
11 namespace Shop
22 {
33 public class Employee
44 {
5− protected int Grade, Quota;
5+ protected int Grade;
66
77 public int Level() => Grade;
88 }
99
1010 public class Salesman : Employee
1111 {
12+ protected int Quota;
13+
1214 public int Target() => Quota * Grade;
1315 }
1416 }

#sealed-subclass-becomes-private

Narrows a protected field to private in a sealed subclass, where protected means nothing

success
Targetsymbol F:Shop.Employee.Quota
People.csmodified
11 namespace Shop
22 {
33 public class Employee
44 {
5− protected int Quota;
65 }
76
87 public sealed class Salesman : Employee
98 {
9+ private int Quota;
10+
1011 public void Assign(int quota) => Quota = quota;
1112
1213 public bool Met(int sales) => sales >= Quota;
1314 }
1415 }

#to-every-subclass-when-unused

Copies a field no code uses into every direct subclass, so it is not lost

success
Targetsymbol F:Shop.Employee.Notes
People.csmodified
11 namespace Shop
22 {
33 public abstract class Employee
44 {
5− public string Notes = "";
6−
75 public abstract decimal Pay();
86 }
97
108 public class Salesman : Employee
119 {
10+ public string Notes = "";
11+
1212 public override decimal Pay() => 100;
1313 }
1414
1515 public class Engineer : Employee
1616 {
1717 public int Level;
18+ public string Notes = "";
1819
1920 public override decimal Pay() => 200;
2021 }
2122 }

#to-subclass-that-uses-it

Moves a field into the one subclass that uses it, including through a subclass-typed reference in another file

success
Targetsymbol F:Shop.Employee.Quota
Employee.csmodified
11 namespace Shop
22 {
33 public abstract class Employee
44 {
55 public string Name;
6− public int Quota;
76 }
87 }
Salesman.csmodified
11 namespace Shop
22 {
33 public class Salesman : Employee
44 {
55 public decimal Sales;
6+ public int Quota;
67
78 public bool MetQuota() => Sales >= Quota;
89 }
910 }
Engineer.csunchanged
11 namespace Shop
22 {
33 public class Engineer : Employee
44 {
55 public int Level;
66 }
77 }
Targets.csunchanged
11 namespace Shop
22 {
33 public static class Targets
44 {
55 public static void Raise(Salesman salesman) => salesman.Quota += 10;
66 }
77 }

#used-by-base

Refuses when the base class itself uses the field

refusal
Targetsymbol F:Shop.Employee.Quota
Refusesused-by-base mentioning “Employee”; every file is left unchanged
People.csinput
1 namespace Shop
2 {
3 public class Employee
4 {
5 protected int Quota;
6
7 public bool HasQuota() => Quota > 0;
8 }
9
10 public class Salesman : Employee
11 {
12 public int Target() => Quota;
13 }
14 }

#used-through-base

Refuses when code elsewhere reaches the field through a base-typed reference

refusal
Targetsymbol F:Shop.Employee.Quota
Refusesused-through-base mentioning “Payroll.cs”; every file is left unchanged
Payroll.csinput
1 namespace Shop
2 {
3 public static class Payroll
4 {
5 public static int QuotaOf(Employee employee) => employee.Quota;
6 }
7 }
People.csinput
1 namespace Shop
2 {
3 public class Employee
4 {
5 public int Quota;
6 }
7
8 public class Salesman : Employee
9 {
10 public int Target() => Quota;
11 }
12 }

#uses-base-private-members

Refuses when the field's initializer uses a member private to the base class

refusal
Targetsymbol F:Shop.Employee.Quota
Refusesuses-base-private-members mentioning “DefaultQuota”; every file is left unchanged
People.csinput
1 namespace Shop
2 {
3 public class Employee
4 {
5 private const int DefaultQuota = 10;
6 protected int Quota = DefaultQuota;
7 }
8
9 public class Salesman : Employee
10 {
11 public int Target() => Quota;
12 }
13 }