RefactorMCP catalog

Introduce Field

Stores the value of a selected expression in a new field of the containing type and uses the field in its place. A second form adds a field of a named type to a type, which composite recipes such as Extract Class use to hold the object that members move to.

From an expression

"target": { "file": ..., "selection": "marker" } with "arguments": { "name": "_field" }.

Precondition

  • The selection is exactly one expression, which produces a value and is not assigned to.
  • No member of the containing type already has the name.
  • The expression's type does not use a type parameter of the containing method, which a field could not name.
  • Unless the expression is constant, it is inside a statement in a block, and that statement evaluates it exactly once each time it runs: it is not on the right of &&, || or ??, in a branch of ?:, after ?., in a switch expression arm, a lambda or a loop condition.

Transformation

  • A constant expression becomes a private readonly field initialised where it is declared, static when the expression is in a static member.
  • Any other expression becomes a private field without an initialiser. An assignment of the expression to the field is inserted immediately before the statement that contained it, and takes over the comments above that statement.
  • Only the selected occurrence is replaced.
  • The field is placed after the type's existing fields, or first in the type, followed by a blank line, when it has none.
  • Its type is written as code at the expression would write it. With nullable reference types enabled, a reference-type field assigned in a member is declared nullable, because it is null until the member runs.

Preserved

  • The value the statement computes, and the order in which the statement evaluates its parts.
  • Comments above the statement and trailing comments on its line.

Adding a field of a type

"target": { "symbol": "T:Shop.Customer" } with "arguments": { "type": "Address", "name": "_address" }.

  • The field is added to the targeted type, after its existing fields.
  • When the named type is a non-abstract class with an accessible parameterless constructor, the field is private readonly and initialised with a new instance, so code moved behind it has an object to run against. Otherwise it is declared private without an initialiser, to be assigned by a later step.
  • The type name must resolve from inside the targeted type, and the field name must be free.

Limitations

  • Other occurrences of the same expression are not replaced.
  • A non-constant expression in an expression-bodied member is refused rather than converting the member to a block body.
  • A non-constant expression is re-evaluated each time the member runs and stored in the field; the refactoring does not move the evaluation into a constructor.

Error codes

CodeMeaning
not-an-expressionthe selection is not exactly one expression
no-valuethe expression produces no value, such as a call to a void method
assigned-expressionthe expression is assigned to or passed by ref or out
name-conflictthe type already has a member with the name
method-type-parameterthe expression's type uses a type parameter of the method
conditionally-evaluatedthe statement may evaluate the expression other than exactly once
expression-bodied-membera non-constant expression is in an expression-bodied member
not-in-blockthe statement containing the expression is not in a block
unknown-typethe type named for a new field does not resolve

Cases

·

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

#assigned-in-member

An expression reading parameters is assigned to the new field just before its statement, and only the selected occurrence is replaced

success
TargetOrder.cs, the /*[*/ … /*]*/ selection
Arguments
name"_subtotal"
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
55 private int _lines;
6+ private decimal _subtotal;
67
78 public decimal Total(decimal price, int quantity)
89 {
910 _lines++;
10− var discount = /*[*/price * quantity/*]*/ > 100 ? 5 : 0;
11+ _subtotal = price * quantity;
12+ var discount = _subtotal > 100 ? 5 : 0;
1113 return price * quantity - discount;
1214 }
1315
1416 public int Lines => _lines;
1517 }
1618 }

#conditionally-evaluated-rejected

Refuses an expression its statement may not evaluate, since assigning it first would evaluate it always

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"_large"
Refusesconditionally-evaluated; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public bool IsLarge(string text)
6 {
7 return text != null && /*[*/text.Length > 10/*]*/;
8 }
9 }
10 }

#constant-expression

A constant expression becomes a readonly field initialised where it is declared

success
TargetSession.cs, the /*[*/ … /*]*/ selection
Arguments
name"_secondsPerMinute"
Session.csmodified
11 namespace Shop
22 {
33 public class Session
44 {
5+ private readonly int _secondsPerMinute = 60;
6+
57 public int TimeoutSeconds(int minutes)
68 {
7− return minutes * /*[*/60/*]*/;
9+ return minutes * _secondsPerMinute;
810 }
911 }
1012 }

#expression-bodied-rejected

Refuses a non-constant expression in an expression-bodied member, which has no statement to assign the field before

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"_doubled"
Refusesexpression-bodied-member; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public int Twice(int x) => /*[*/x * 2/*]*/ + 1;
6 }
7 }

#field-of-type

Given a type and no selection, adds a readonly field of that type holding a new instance, after the existing fields

success
Targetsymbol T:Shop.Customer
Arguments
type"Address"
name"_address"
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
55 public string Name = "";
6+ private readonly Address _address = new Address();
67
78 public string Greeting() => "Dear " + Name;
89 }
910 }
Address.csunchanged
11 namespace Shop
22 {
33 public class Address
44 {
55 public string Street = "";
66 }
77 }

#generic-type

An expression whose type uses the class's type parameter gives a field of that type

success
TargetCache.cs, the /*[*/ … /*]*/ selection
Arguments
name"_snapshot"
Cache.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Cache<T>
66 {
7+ private List<T> _snapshot;
8+
79 public int Count(IEnumerable<T> items)
810 {
9− return /*[*/new List<T>(items)/*]*/.Count;
11+ _snapshot = new List<T>(items);
12+ return _snapshot.Count;
1013 }
1114 }
1215 }

#method-type-parameter-rejected

Refuses an expression whose type uses a type parameter of the method, which a field cannot name

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"_items"
Refusesmethod-type-parameter; every file is left unchanged
Sample.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Sample
6 {
7 public int Wrap<T>(T item)
8 {
9 return /*[*/new List<T> { item }/*]*/.Count;
10 }
11 }
12 }

#name-conflict-rejected

Refuses a name another member of the type already has

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"_total"
Refusesname-conflict mentioning “'_total'”; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 private int _total;
6
7 public int Run(int x)
8 {
9 _total += x;
10 return /*[*/x * 2/*]*/ + _total;
11 }
12 }
13 }

#no-value-rejected

Refuses an expression that produces no value

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"_written"
Refusesno-value; every file is left unchanged
Sample.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Sample
6 {
7 public void Run()
8 {
9 /*[*/Console.WriteLine("Hello")/*]*/;
10 }
11 }
12 }

#not-an-expression-rejected

Refuses a selection that is a statement rather than an expression

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"_value"
Refusesnot-an-expression; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public int Run(int x)
6 {
7 /*[*/var y = x + 1;/*]*/
8 return y;
9 }
10 }
11 }

#nullable-reference

With nullable reference types enabled, a reference-type field assigned in a member is declared nullable, since no constructor assigns it

success
TargetGreeter.cs, the /*[*/ … /*]*/ selection
Arguments
name"_trimmed"
Projectnullable enable
Greeter.csmodified
11 namespace Shop
22 {
33 public class Greeter
44 {
5+ private string? _trimmed;
6+
57 public int Length(string name)
68 {
7− return /*[*/name.Trim()/*]*/.Length;
9+ _trimmed = name.Trim();
10+ return _trimmed.Length;
811 }
912 }
1013 }

#preserves-comments

Comments above the statement stay above it, now above the assignment that opens it, and a trailing comment stays on its line

success
TargetConverter.cs, the /*[*/ … /*]*/ selection
Arguments
name"_converted"
Converter.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Converter
66 {
77 // The last rate used.
88 private decimal _rate = 1.1m;
9+ private decimal _converted;
910
1011 public decimal Convert(decimal amount)
1112 {
1213 var rate = _rate;
1314
1415 // Round to whole cents.
15− return Math.Round(/*[*/amount * rate/*]*/, 2); // banker's rounding
16+ _converted = amount * rate;
17+ return Math.Round(_converted, 2); // banker's rounding
1618 }
1719 }
1820 }

#static-member

An expression in a static method becomes a static field

success
TargetLabels.cs, the /*[*/ … /*]*/ selection
Arguments
name"_upperName"
Labels.csmodified
11 namespace Shop
22 {
33 public static class Labels
44 {
5+ private static string _upperName;
6+
57 public static string Shout(string name)
68 {
7− return /*[*/name.ToUpperInvariant()/*]*/ + "!";
9+ _upperName = name.ToUpperInvariant();
10+ return _upperName + "!";
811 }
912 }
1013 }