RefactorMCP catalog

Extract Method

Moves a run of statements, or a single expression, out of a method body into a new private method, and replaces them with a call to it.

Precondition

  • The selection lies inside a block-bodied method and covers at least one whole statement, or exactly one expression that is not a statement of its own. Statements the selection touches are extracted whole.
  • An expression has a value whose type can be written, and is not assigned to or passed by ref or out.
  • No local declared inside the selection is used after it. Such a local would be left without a declaration.
  • No local or parameter assigned inside the selection is read after it, or before it in an enclosing loop. The assignment would change a copy in the new method and be lost.
  • In a void method, the statements return only at their end.
  • The class has no member with the name, or has a method of that name whose body is the selected code (see below).

Transformation

  • The new method is placed after the containing method, with the given name, and is private. It is static when the containing method is.
  • Statements are taken from the innermost block or switch section that holds the whole selection, so a selection inside the block of an if or loop extracts statements of that block. A statement an if, else or loop runs without braces is extracted on its own and replaced by the call.
  • An expression becomes a method returning it, with the expression's type, and the call takes its place. An expression over several lines keeps the indentation of its later lines relative to the statement it is in.
  • A return;, break; or continue; ending the selection stays at the call site, after the call.
  • When the name is that of a method the class already has, whose body is the selected code with each of its parameters standing for an expression in it, no method is created: the selection becomes a call of that method, passing those expressions. The method is not generic, takes its parameters by value, returns the selected expression's type or, for statements, nothing, and is static when the containing method is. The expressions have no side effects, and every other name means the same in both.
  • Parameters and locals the statements read are passed as parameters, in the order they are first used. A parameter keeps its nullable annotation; a var local that flow analysis knows is not null is passed as non-nullable.
  • Type parameters of the containing method that the new method needs are declared on it with their constraints. The call site names them only when the arguments cannot infer them.
  • When the statements return from the containing method on some paths, the new method returns a nullable result and the call site returns it when present, otherwise execution continues after the call.

Preserved

  • The behaviour of the containing method on every path.
  • Comments and blank lines inside the extracted statements or expression.
  • Comments and blank lines outside the selection: a comment above the first selected statement stays at the call site, and a blank line after the selection stays after the call.

Limitations

  • Expression-bodied methods are refused rather than converted to a block body.
  • Locals assigned inside the selection and read afterwards are not returned as out values; the extraction is refused instead.
  • A selection that spans several blocks extracts whole statements of the innermost block holding it. Statements inside a lambda or local function are not extracted on their own.
  • A void method's statements that return before their end are refused rather than turned into a method whose result says whether to return.
  • Only the selected occurrence of an expression is replaced, not other equal expressions.
  • Extraction from constructors, accessors and local functions is refused.
  • The new method is added to a class; structs and records are not covered.

Error codes

CodeMeaning
expression-bodied-memberthe selection is in an expression-bodied method
declared-local-used-afterthe selection declares a local used after it
assigned-local-used-afterthe selection assigns a local or parameter read after it
not-in-methodthe selection is not inside a method
no-statements-selectedthe selection covers no whole statement
returns-earlythe statements of a void method return before their end
name-conflictthe class already has a member with the name, and no method of that name has the selected code as its body
no-valuethe selected expression has no value a method could return
assigned-expressionthe selected expression is assigned to

Cases

·

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

#assigned-local-used-after-rejected

Refuses when the block assigns a local declared before it that the rest of the method reads

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"AddAll"
Refusesassigned-local-used-after mentioning “'total'”; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Sum(int[] values)
4 {
5 var total = 0;
6 /*[*/foreach (var value in values)
7 {
8 total += value;
9 }/*]*/
10 return total;
11 }
12 }

#awaits-become-async

Statements that await produce an async method returning Task, and the call is awaited

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Pause"
Sample.csmodified
11 using System;
22 using System.Threading.Tasks;
33
44 public class Sample
55 {
66 public async Task Run(int delay)
77 {
8− /*[*/await Task.Delay(delay);
9− Console.WriteLine("Waited");/*]*/
8+ await Pause(delay);
109 Console.WriteLine("Done");
1110 }
11+
12+ private async Task Pause(int delay)
13+ {
14+ await Task.Delay(delay);
15+ Console.WriteLine("Waited");
16+ }
1217 }

#declared-local-used-after-rejected

Refuses when the block declares a local the rest of the method still reads

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"WriteAdjusted"
Refusesdeclared-local-used-after mentioning “'adjusted'”; every file is left unchanged
Sample.csinput
1 using System;
2
3 public class Sample
4 {
5 public int Calc(int a)
6 {
7 /*[*/var adjusted = a + 1;
8 Console.WriteLine(adjusted);/*]*/
9 return adjusted;
10 }
11 }

#early-return-rejected

Refuses statements of a void method that return before their end, which the new method would only return from itself

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Check"
Refusesreturns-early; every file is left unchanged
Sample.csinput
1 using System;
2
3 public class Sample
4 {
5 public void Accept(int amount)
6 {
7 /*[*/if (amount < 0)
8 return;
9 Console.WriteLine("Checked");/*]*/
10 Console.WriteLine("Accepted");
11 }
12 }

#expression

A selection covering exactly one expression becomes a method returning its value, called in its place

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"IsStandardParcel"
Sample.csmodified
11 public class Sample
22 {
33 public string Service(int weight, bool express)
44 {
5− if (/*[*/weight < 1000 && !express/*]*/)
5+ if (IsStandardParcel(weight, express))
66 return "standard";
77 return "courier";
8+ }
9+
10+ private bool IsStandardParcel(int weight, bool express)
11+ {
12+ return weight < 1000 && !express;
813 }
914
1015 public int Limit()
1116 {
1217 return 1000;
1318 }
1419 }

#expression-assigned-local-used-after-rejected

Refuses an expression that assigns a local read after it, which the extracted method would only change a copy of

refusal
TargetRetry.cs, the /*[*/ … /*]*/ selection
Arguments
name"TooMany"
Refusesassigned-local-used-after mentioning “attempts”; every file is left unchanged
Retry.csinput
1 namespace Net
2 {
3 public class Retry
4 {
5 public bool Next(int attempts)
6 {
7 if (/*[*/++attempts > 3/*]*/)
8 {
9 return false;
10 }
11
12 return attempts % 2 == 0;
13 }
14 }
15 }

#expression-bodied-rejected

Refuses to extract from an expression-bodied method, which has no statements to move

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Extracted"
Refusesexpression-bodied-member; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Calc(int a, int b) => /*[*/a + b/*]*/;
4 }

#expression-comments

An expression over several lines moves with the comments inside it

success
TargetDispatch.cs, the /*[*/ … /*]*/ selection
Arguments
name"IsPriority"
Dispatch.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Order
66 {
77 public decimal Total { get; set; }
88
99 public bool IsPaid { get; set; }
1010 }
1111
1212 public class Dispatch
1313 {
1414 public void Send(Order order)
1515 {
1616 // Priority orders go first.
17− if (/*[*/order.Total > 100m // large orders
18− && order.IsPaid/*]*/)
17+ if (IsPriority(order))
1918 {
2019 Console.WriteLine("priority");
2120 }
2221 }
22+
23+ private bool IsPriority(Order order)
24+ {
25+ return order.Total > 100m // large orders
26+ && order.IsPaid;
27+ }
2328 }
2429 }

#expression-condition

A selected condition becomes a private method returning its value, after the containing method, taking the parameters it reads

success
TargetTickets.cs, the /*[*/ … /*]*/ selection
Arguments
name"IsExempt"
Tickets.csmodified
11 namespace Venue
22 {
33 public class Tickets
44 {
55 private decimal _price = 10m;
66
77 public decimal Fee(int age, bool member)
88 {
9− if (/*[*/age >= 65 || member/*]*/)
9+ if (IsExempt(age, member))
1010 {
1111 return 0m;
1212 }
1313
1414 return _price;
1515 }
1616
17+ private bool IsExempt(int age, bool member)
18+ {
19+ return age >= 65 || member;
20+ }
21+
1722 public decimal Deposit() => _price / 2;
1823 }
1924 }

#expression-from-static-method

An expression in a static method is extracted into a static method; a constant it reads stays reachable and is not passed

success
TargetTax.cs, the /*[*/ … /*]*/ selection
Arguments
name"Levy"
Tax.csmodified
11 namespace Billing
22 {
33 public static class Tax
44 {
55 private const decimal Rate = 0.2m;
66
77 public static decimal Total(decimal amount)
88 {
9− return amount + /*[*/amount * Rate/*]*/;
9+ return amount + Levy(amount);
10+ }
11+
12+ private static decimal Levy(decimal amount)
13+ {
14+ return amount * Rate;
1015 }
1116 }
1217 }

#expression-generic-method

An expression using a type parameter of the containing method is extracted into a generic method with its constraint, called without type arguments the arguments infer

success
TargetFinder.cs, the /*[*/ … /*]*/ selection
Arguments
name"Matches"
Finder.csmodified
11 using System.Collections.Generic;
22
33 namespace Search
44 {
55 public class Finder
66 {
77 public int IndexOf<T>(List<T> items, T wanted) where T : class
88 {
99 for (var i = 0; i < items.Count; i++)
1010 {
11− if (/*[*/EqualityComparer<T>.Default.Equals(items[i], wanted)/*]*/)
11+ if (Matches(items, i, wanted))
1212 {
1313 return i;
1414 }
1515 }
1616
1717 return -1;
1818 }
19+
20+ private bool Matches<T>(List<T> items, int i, T wanted) where T : class
21+ {
22+ return EqualityComparer<T>.Default.Equals(items[i], wanted);
23+ }
1924 }
2025 }

#expression-no-value-rejected

Refuses an expression that has no value, such as a call returning void

refusal
TargetLogger.cs, the /*[*/ … /*]*/ selection
Arguments
name"Write"
Refusesno-value; every file is left unchanged
Logger.csinput
1 using System;
2
3 namespace Diagnostics
4 {
5 public class Logger
6 {
7 public Action For(string message)
8 {
9 return () => /*[*/Console.WriteLine(message)/*]*/;
10 }
11 }
12 }

#expression-nullable

With nullable annotations, a parameter keeps its annotation and the method returns a non-nullable type when the value cannot be null

success
TargetGreeter.cs, the /*[*/ … /*]*/ selection
Arguments
name"NameOrGuest"
Projectnullable enable
Greeter.csmodified
11 namespace People
22 {
33 public class Greeter
44 {
55 public string Greet(string? name)
66 {
7− var shown = /*[*/name ?? "guest"/*]*/;
7+ var shown = NameOrGuest(name);
88 return "Hello " + shown;
9+ }
10+
11+ private string NameOrGuest(string? name)
12+ {
13+ return name ?? "guest";
914 }
1015 }
1116 }

#expression-pattern-variable-used-after-rejected

Refuses an expression declaring a pattern variable that is used after it

refusal
TargetShapes.cs, the /*[*/ … /*]*/ selection
Arguments
name"IsCircle"
Refusesassigned-local-used-after mentioning “circle”; every file is left unchanged
Shapes.csinput
1 namespace Geometry
2 {
3 public class Circle
4 {
5 public double Radius { get; set; }
6 }
7
8 public class Shapes
9 {
10 public double Radius(object shape)
11 {
12 if (/*[*/shape is Circle circle/*]*/)
13 {
14 return circle.Radius;
15 }
16
17 return 0;
18 }
19 }
20 }

#from-static-method

Extracting from a static method creates a static method, callable without an instance

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Report"
Sample.csmodified
11 using System;
22
33 public static class Sample
44 {
55 public static int Twice(int value)
66 {
77 var doubled = value * 2;
8− /*[*/Console.WriteLine(doubled);/*]*/
8+ Report(doubled);
99 return doubled;
1010 }
11+
12+ private static void Report(int doubled)
13+ {
14+ Console.WriteLine(doubled);
15+ }
1116 }

#generic-method

Statements that use a method type parameter produce a generic method with the same constraint

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Report"
Sample.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 public class Sample
55 {
66 public T Largest<T>(List<T> items) where T : IComparable<T>
77 {
88 var largest = items[0];
99 foreach (var item in items)
1010 {
1111 if (item.CompareTo(largest) > 0)
1212 largest = item;
1313 }
1414
15− /*[*/Console.WriteLine($"{largest} of {items.Count}");/*]*/
15+ Report(largest, items);
1616 return largest;
1717 }
18+
19+ private void Report<T>(T largest, List<T> items) where T : IComparable<T>
20+ {
21+ Console.WriteLine($"{largest} of {items.Count}");
22+ }
1823 }

#name-conflict-rejected

Refuses a name the class already gives a member

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Report"
Refusesname-conflict; every file is left unchanged
Sample.csinput
1 using System;
2
3 public class Sample
4 {
5 public string Report { get; set; } = "";
6
7 public void Print(int value)
8 {
9 /*[*/Console.WriteLine(value);/*]*/
10 }
11 }

#nested-block

Statements inside the block of an if statement are extracted on their own, and a return ending the selection stays at the call site

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"LogRejection"
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public void Accept(int amount)
66 {
77 if (amount < 0)
88 {
9− /*[*/Console.WriteLine("Rejected");
10− Console.WriteLine(amount);
11− return;/*]*/
9+ LogRejection(amount);
10+ return;
1211 }
1312
1413 Console.WriteLine("Accepted");
1514 }
15+
16+ private void LogRejection(int amount)
17+ {
18+ Console.WriteLine("Rejected");
19+ Console.WriteLine(amount);
20+ }
1621 }

#no-statements-selected-rejected

Refuses a selection inside the method that covers no statement of its body

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Extracted"
Refusesno-statements-selected; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Calc(/*[*/int a/*]*/, int b)
4 {
5 return a + b;
6 }
7 }

#not-in-method-rejected

Refuses a selection inside a property accessor, which is not a method

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Compute"
Refusesnot-in-method; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 private int _count;
4
5 public int Doubled
6 {
7 get
8 {
9 /*[*/return _count * 2;/*]*/
10 }
11 }
12 }

#nullable-context

Parameters keep their nullable annotations, and a local known not to be null is passed as non-nullable

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Log"
Projectnullable enable
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public string Greet(string first, string? middle)
66 {
77 var greeting = "Hello " + first;
8− /*[*/Console.WriteLine(greeting.Length);
9− Console.WriteLine(middle ?? "no middle name");/*]*/
8+ Log(greeting, middle);
109 return greeting;
1110 }
11+
12+ private void Log(string greeting, string? middle)
13+ {
14+ Console.WriteLine(greeting.Length);
15+ Console.WriteLine(middle ?? "no middle name");
16+ }
1217 }

#onto-existing-method

Named after an existing method whose body is the selected code with a parameter standing for a literal, the selection becomes a call of that method

success
TargetEmployee.cs, the /*[*/ … /*]*/ selection
Arguments
name"Raise"
Employee.csmodified
11 public class Employee
22 {
33 private decimal _salary;
44
55 public void Raise(decimal factor)
66 {
77 _salary *= factor;
88 }
99
1010 public void Promote(string title)
1111 {
12− /*[*/_salary *= 1.2m;/*]*/
12+ Raise(1.2m);
1313 System.Console.WriteLine(title);
1414 }
1515 }

#preserves-comments

Comments inside and around the extracted statements travel with them

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Log"
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public void Run(string name)
66 {
77 // Greet first.
8− /*[*/Console.WriteLine("Hello");
9− // Then name the caller.
10− Console.WriteLine(name); // trailing note/*]*/
8+ Log(name);
119
1210 Console.WriteLine("Done");
1311 }
12+
13+ private void Log(string name)
14+ {
15+ Console.WriteLine("Hello");
16+ // Then name the caller.
17+ Console.WriteLine(name); // trailing note
18+ }
1419 }

#returns-value-from-branch

Extracts a block that returns on one branch; the caller returns only when the extracted method produced a value

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"ExplainPositive"
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public int Calc(int a, int b)
66 {
77 int limit = 10;
8− /*[*/if (a > limit)
8+ var explainPositiveResult = ExplainPositive(a, limit, b);
9+ if (explainPositiveResult != null)
10+ {
11+ return explainPositiveResult.Value;
12+ }
13+
14+ return 0;
15+ }
16+
17+ private int? ExplainPositive(int a, int limit, int b)
18+ {
19+ if (a > limit)
920 {
1021 return a + b;
11− }/*]*/
12− return 0;
22+ }
23+
24+ return default;
1325 }
1426 }

#simple-statements

Extracts a guard statement into a private method taking the parameters it reads

success
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"ValidateInputs"
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public int Calc(int a, int b)
66 {
7− /*[*/if (a < 0 || b < 0)
8− {
9− throw new ArgumentException();
10− }/*]*/
7+ ValidateInputs(a, b);
118 var result = a + b;
129 return result;
1310 }
11+
12+ private void ValidateInputs(int a, int b)
13+ {
14+ if (a < 0 || b < 0)
15+ {
16+ throw new ArgumentException();
17+ }
18+ }
1419 }