RefactorMCP catalog

Pull Up Field

Moves a field from a class into its base class. The other subclasses that declare the same field lose their copy, since they now inherit it.

Arguments

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

Precondition

  • The class has a base class, declared in the solution.
  • The field's type and initializer use nothing the base class cannot see: no other member or nested type of the subclass, and no type parameter of the subclass that the base class does not share.
  • Neither the base class nor any class above it has a member of that name.
  • Every other subclass of the base that declares a member of that name declares the same field: a direct subclass, with the same type, initializer and static-ness. Anything else would hide the pulled-up field.
  • The result compiles.

Transformation

  • The field goes after the last field of the base class, or first when it has none, keeping its documentation comment, initializer and other modifiers.
  • A private field becomes protected, so the subclass can still use it.
  • A declaration of several fields is split, and only the target moves.
  • The subclass's type parameters are written as the base class's: a field List<T> in Repository<T> : Store<T> becomes List<TItem> in Store<TItem>.
  • The base class's file gains the usings the field's type and initializer need.
  • Identical fields in the other subclasses are removed.

Preserved

  • Every use of the field, in the subclass and elsewhere, which now binds to the inherited field.
  • Comments around the field that belong to its neighbours.

Limitations

  • A field that other code reaches through a sibling's differently-accessible copy is refused by the compile check rather than reconciled.
  • Usings the subclass's file no longer needs are left in place.

Error codes

CodeMeaning
no-base-classthe class has no base class other than object
base-not-in-sourcethe base class is not declared in the solution
member-exists-in-basethe base class or a class above it has a member of that name
conflicts-with-siblinganother subclass declares a different member of that name
uses-subclass-membersthe field's type or initializer uses something only the subclass has
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.

#base-not-in-source

Refuses when the base class is not declared in the solution

refusal
Targetsymbol F:Shop.OrderException.OrderId
Refusesbase-not-in-source mentioning “System.Exception”; every file is left unchanged
OrderException.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class OrderException : Exception
6 {
7 public int OrderId;
8 }
9 }

#conflicts-with-sibling

Refuses when another subclass declares a different member of that name, which would hide the moved field

refusal
Targetsymbol F:Shop.Manager.Rank
Refusesconflicts-with-sibling mentioning “Engineer”; every file is left unchanged
People.csinput
1 namespace Shop
2 {
3 public class Employee
4 {
5 }
6
7 public class Manager : Employee
8 {
9 public int Rank;
10 }
11
12 public class Engineer : Employee
13 {
14 public string Rank;
15 }
16 }

#generic-base

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

success
Targetsymbol F:Shop.Repository`1._items
Repository.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Repository<T> : Store<T>
66 {
7− private readonly List<T> _items = new List<T>();
8−
97 public override int Count => _items.Count;
108 }
119 }
Store.csmodified
1+using System.Collections.Generic;
2+
13 namespace Shop
24 {
35 public abstract class Store<TItem>
46 {
7+ protected readonly List<TItem> _items = new List<TItem>();
8+
59 public abstract int Count { get; }
610 }
711 }

#keeps-comments-and-initializer

Moves the field's documentation comment and initializer with it, and leaves the comments around it in place

success
Targetsymbol F:Shop.Manager.Holidays
People.csmodified
11 namespace Shop
22 {
33 public class Employee
44 {
55 // Pay is monthly.
66 protected decimal Salary;
7+ /// <summary>Days of leave a year.</summary>
8+ protected int Holidays = 25;
79 }
810
911 public class Manager : Employee
1012 {
1113 // Managers are senior.
1214 public int Grade;
1315
14− /// <summary>Days of leave a year.</summary>
15− protected int Holidays = 25;
16−
1716 // Approvals follow.
1817 public bool CanApprove() => Grade > 2 && Holidays > 0;
1918 }
2019 }

#member-exists-in-base

Refuses when the base class already has a member of that name

refusal
Targetsymbol F:Shop.Manager.Name
Refusesmember-exists-in-base mentioning “Name”; every file is left unchanged
People.csinput
1 namespace Shop
2 {
3 public class Employee
4 {
5 public string Name() => "employee";
6 }
7
8 public class Manager : Employee
9 {
10 public new string Name;
11 }
12 }

#no-base-class

Refuses when the class has no base class to move the field into

refusal
Targetsymbol F:Shop.Manager._name
Refusesno-base-class; every file is left unchanged
Manager.csinput
1 namespace Shop
2 {
3 public class Manager
4 {
5 private string _name;
6
7 public string Describe() => _name;
8 }
9 }

#nullable-annotations

Keeps nullable annotations on the moved field

success
Targetsymbol F:Shop.Manager.Nickname
Projectnullable enable
People.csmodified
11 namespace Shop
22 {
33 public class Employee
44 {
55 public string Name = "";
6+ public string? Nickname;
67 }
78
89 public class Manager : Employee
910 {
10− public string? Nickname;
11−
1211 public string Greeting() => Nickname ?? Name;
1312 }
1413 }

#one-of-several-declarators

Splits a declaration of several fields and moves only the target

success
Targetsymbol F:Shop.Manager._reports
People.csmodified
11 namespace Shop
22 {
33 public class Employee
44 {
5+ protected int _reports;
56 }
67
78 public class Manager : Employee
89 {
9− protected int _grade, _reports;
10+ protected int _grade;
1011
1112 public int Load() => _grade * _reports;
1213 }
1314 }

#private-field-becomes-protected

Moves a field into the base class in another file, widening private to protected so the subclass still sees it

success
Targetsymbol F:Shop.Manager._name
Employee.csmodified
11 namespace Shop
22 {
33 public class Employee
44 {
55 public decimal Salary;
6+ protected string _name;
67
78 public decimal Monthly() => Salary / 12;
89 }
910 }
Manager.csmodified
11 namespace Shop
22 {
33 public class Manager : Employee
44 {
5− private string _name;
6−
75 public string Describe() => "Manager " + _name;
86 }
97 }

#removes-identical-sibling-fields

Removes the same field from the other subclasses, which now inherit it

success
Targetsymbol F:Shop.Engineer.Name
Employee.csmodified
11 namespace Shop
22 {
33 public abstract class Employee
44 {
5+ public string Name;
56 }
67 }
Engineer.csmodified
11 namespace Shop
22 {
33 public class Engineer : Employee
44 {
5− public string Name;
65 public int Level;
76 }
87 }
Salesman.csmodified
11 namespace Shop
22 {
33 public class Salesman : Employee
44 {
55 public int Quota;
6−
7− public string Name;
86 }
97 }
Directory.csunchanged
11 namespace Shop
22 {
33 public static class Directory
44 {
55 public static string Describe(Engineer engineer, Salesman salesman) => engineer.Name + " and " + salesman.Name;
66 }
77 }

#uses-subclass-members

Refuses when the field's initializer reads a member only the subclass has

refusal
Targetsymbol F:Shop.Manager._bonus
Refusesuses-subclass-members mentioning “Grade”; every file is left unchanged
People.csinput
1 namespace Shop
2 {
3 public class Employee
4 {
5 }
6
7 public class Manager : Employee
8 {
9 private const int Grade = 3;
10 private int _bonus = Grade * 100;
11
12 public int Bonus() => _bonus;
13 }
14 }