RefactorMCP catalog

Convert Method to Local Function

Moves a private method used by only one member into that member as a local function. The reverse of Convert Local Function to Method.

Target

The method, by symbol. Overloads are told apart by their signature.

Precondition

  • The method is private, and not an extension, extern or partial method.
  • It is used, and every use outside its own body is in one member: one method, constructor, operator or accessor. Uses inside the method, such as recursive calls, do not count.
  • Every use is inside that member's block body, where a local function can be declared. A member with an expression body can be converted to a block body first.
  • Every call is on the member's own instance: unqualified, through this, or through the type for a static method.
  • The caller uses no other overload of the method, which the local function would hide.
  • No local, parameter, local function or method type parameter of the caller has the method's name, the name of one of its type parameters, or the name of anything the method refers to from outside itself, such as a field it reads.

Transformation

  • The method becomes a local function at the end of the caller's body, after a blank line, with the same signature, type parameters, constraints and body. Its accessibility modifier is dropped; a static method becomes a static local function.
  • this.M() and Type.M() become M().
  • The method is removed from its type. When the caller is in another file of a partial type, the local function goes there.

Preserved

  • Behaviour: every call reaches the same code with the same this.
  • Comments above and inside the method move with it.
  • Uses as a method group and in nameof.

Limitations

  • A caller with an expression body is refused rather than converted to a block body.
  • Uses in a constructor initializer or a field initializer are outside any block and are refused.

Error codes

CodeMeaning
not-privatethe method is not private, so code outside its type may use it
unsupported-methodthe method is an extension, extern or partial method
never-callednothing uses the method, so there is no member to move it into
called-from-several-membersthe method is used from more than one member
caller-expression-bodieda use is outside a block body, such as in an expression-bodied member
called-on-another-instancea call is on an instance other than the caller's own
overloaded-methodthe caller also uses another overload of the method
name-conflicta name in the caller would hide the method or something it refers to

Cases

·

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

#called-from-accessor

A method called from a property's get accessor becomes a local function in the accessor

success
Targetsymbol M:Sample.Sum
Sample.csmodified
11 public class Sample
22 {
33 private int[] _values = { 1, 2 };
44
55 public int Total
66 {
77 get
88 {
99 return Sum();
10+
11+ int Sum()
12+ {
13+ var sum = 0;
14+ foreach (var value in _values)
15+ sum += value;
16+ return sum;
17+ }
1018 }
1119 }
12−
13− private int Sum()
14− {
15− var sum = 0;
16− foreach (var value in _values)
17− sum += value;
18− return sum;
19− }
2020 }

#caller-in-another-file

A method of a partial class called from a member in another file moves into that member

success
Targetsymbol M:Shop.Order.Discount(System.Decimal)
Order.Pricing.csmodified
11 namespace Shop;
22
33 public partial class Order
44 {
55 public decimal Price()
66 {
77 return _total - Discount(0.1m);
8+
9+ decimal Discount(decimal rate) => _total * rate;
810 }
911 }
Order.csmodified
11 namespace Shop;
22
33 public partial class Order
44 {
55 private decimal _total = 100m;
6−
7− private decimal Discount(decimal rate) => _total * rate;
86 }

#expression-bodied-caller-rejected

Refuses a caller with an expression body, which has no block to hold a local function

refusal
Targetsymbol M:Sample.WithTax(System.Int32)
Refusescaller-expression-bodied; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Total(int price) => WithTax(price);
4
5 private static int WithTax(int price) => price * 120 / 100;
6 }

#extension-method-rejected

Refuses an extension method, which a local function cannot be

refusal
Targetsymbol M:Text.Shout(System.String)
Refusesunsupported-method; every file is left unchanged
Text.csinput
1 public static class Text
2 {
3 public static string Greet(string name)
4 {
5 return name.Shout();
6 }
7
8 private static string Shout(this string text) => text.ToUpperInvariant() + "!";
9 }

#generic-method

A generic method keeps its type parameters and constraint as a local function

success
Targetsymbol M:Sample.Larger``1(``0,``0)
Sample.csmodified
11 using System;
22
33 public class Sample
44 {
55 public string Compare(string a, string b, int x, int y)
66 {
77 return Larger(a, b) + Larger(x, y);
8− }
98
10− private static T Larger<T>(T a, T b) where T : IComparable<T>
11− {
12− return a.CompareTo(b) >= 0 ? a : b;
9+ static T Larger<T>(T a, T b) where T : IComparable<T>
10+ {
11+ return a.CompareTo(b) >= 0 ? a : b;
12+ }
1313 }
1414 }

#instance-method

A private instance method using a field becomes a local function that is not static

success
Targetsymbol M:Sample.WithTax(System.Int32)
Sample.csmodified
11 public class Sample
22 {
33 private int _rate = 20;
44
55 public int Total(int[] prices)
66 {
77 var sum = 0;
88 foreach (var price in prices)
99 sum += WithTax(price);
1010 return sum;
11+
12+ int WithTax(int price) => price * (100 + _rate) / 100;
1113 }
12−
13− private int WithTax(int price) => price * (100 + _rate) / 100;
1414 }

#keeps-comments

The comments above the method and inside it move with it

success
Targetsymbol M:Sample.WithTax(System.Int32)
Sample.csmodified
11 public class Sample
22 {
33 public int Total(int[] prices)
44 {
55 var sum = 0;
66 foreach (var price in prices)
77 sum += WithTax(price);
88
99 // Rounded down.
1010 return sum;
11− }
1211
13− // Twenty percent, in whole units.
14− private static int WithTax(int price)
15− {
16− // Multiply first to keep precision.
17− return price * 120 / 100;
12+ // Twenty percent, in whole units.
13+ static int WithTax(int price)
14+ {
15+ // Multiply first to keep precision.
16+ return price * 120 / 100;
17+ }
1818 }
1919
2020 public int Count(int[] prices) => prices.Length;
2121 }

#method-group

A method used as a method group in its one caller becomes a local function used the same way

success
Targetsymbol M:Sample.Format(System.Int32)
Sample.csmodified
11 using System.Collections.Generic;
22 using System.Linq;
33
44 public class Sample
55 {
66 public List<string> Describe(int[] numbers)
77 {
88 return numbers.Select(Format).ToList();
9+
10+ static string Format(int number) => "#" + number;
911 }
10−
11− private static string Format(int number) => "#" + number;
1212 }

#name-conflict-rejected

Refuses when a local of the caller would hide a field the method reads

refusal
Targetsymbol M:Sample.WithTax(System.Int32)
Refusesname-conflict; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 private int rate = 20;
4
5 public int Total(int price)
6 {
7 var rate = 5;
8 return WithTax(price) + rate;
9 }
10
11 private int WithTax(int price) => price * (100 + rate) / 100;
12 }

#never-called-rejected

Refuses a method that nothing uses, which has no member to move into

refusal
Targetsymbol M:Sample.WithTax(System.Int32)
Refusesnever-called; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Total(int price) => price;
4
5 private int WithTax(int price) => price * 120 / 100;
6 }

#nullable-context

A method with nullable annotations keeps them as a local function

success
Targetsymbol M:Sample.Clean(System.String)
Projectnullable enable
Sample.csmodified
11 public class Sample
22 {
33 public string Label(string? name)
44 {
55 return Clean(name) ?? "none";
6+
7+ static string? Clean(string? name) => name?.Trim();
68 }
7−
8− private static string? Clean(string? name) => name?.Trim();
99 }

#other-instance-rejected

Refuses a call on another instance, which a local function could not be called on

refusal
Targetsymbol M:Shop.Order.Amount
Refusescalled-on-another-instance; every file is left unchanged
Order.csinput
1 namespace Shop;
2
3 public class Order
4 {
5 private decimal _amount;
6
7 public decimal Combined(Order other)
8 {
9 return _amount + other.Amount();
10 }
11
12 private decimal Amount() => _amount;
13 }

#overload-rejected

Refuses when the caller also calls another overload, which the local function would hide

refusal
Targetsymbol M:Sample.Format(System.Int32)
Refusesoverloaded-method; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public string Describe(int number, string text)
4 {
5 return Format(number) + Format(text);
6 }
7
8 private static string Format(int number) => "#" + number;
9
10 private static string Format(string text) => text.Trim();
11 }

#public-method-rejected

Refuses a method that is not private, which code elsewhere could call

refusal
Targetsymbol M:Sample.WithTax(System.Int32)
Refusesnot-private; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Total(int price) => WithTax(price);
4
5 public int WithTax(int price) => price * 120 / 100;
6 }

#several-calls

Every call in the one member, including one qualified with this and one inside a lambda, calls the local function

success
Targetsymbol M:Sample.Describe(System.Int32)
Sample.csmodified
11 using System.Collections.Generic;
22 using System.Linq;
33
44 public class Sample
55 {
66 private string _prefix = "#";
77
88 public List<string> Report(int first, int[] rest)
99 {
10− var lines = new List<string> { this.Describe(first) };
10+ var lines = new List<string> { Describe(first) };
1111 lines.AddRange(rest.Select(n => Describe(n)));
1212 return lines;
13− }
1413
15− private string Describe(int number)
16− {
17− return _prefix + number;
14+ string Describe(int number)
15+ {
16+ return _prefix + number;
17+ }
1818 }
1919 }

#several-members-rejected

Refuses a method called from more than one member

refusal
Targetsymbol M:Sample.WithTax(System.Int32)
Refusescalled-from-several-members; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Total(int price)
4 {
5 return WithTax(price);
6 }
7
8 public int Double(int price)
9 {
10 return WithTax(price) * 2;
11 }
12
13 private static int WithTax(int price) => price * 120 / 100;
14 }

#static-method

A private static method called from one method becomes a static local function at the end of it

success
Targetsymbol M:Sample.WithTax(System.Int32)
Sample.csmodified
11 public class Sample
22 {
33 public int Total(int[] prices)
44 {
55 var sum = 0;
66 foreach (var price in prices)
77 sum += WithTax(price);
88 return sum;
9− }
109
11− private static int WithTax(int price)
12− {
13− return price * 120 / 100;
10+ static int WithTax(int price)
11+ {
12+ return price * 120 / 100;
13+ }
1414 }
1515 }