RefactorMCP catalog

Convert Local Function to Method

Moves a local function out of its member to a private method of the containing type. Variables it captures become parameters. The reverse of Convert Method to Local Function.

Target

The local function, by a caret on its name or on a call to it. In a later step of a composite, the containing member's symbol with arguments.function naming the local function. arguments.name names the method, and defaults to the local function's name.

Precondition

  • The local function calls no other local function of its member, which the method could not reach.
  • When it captures variables or type parameters of its member, every use of it is a call. A method group would have nothing to pass the captured values with.
  • An async or iterator local function does not assign a captured variable, which would have to be passed by ref.
  • The type has no member with the method's name, inherited or declared.

Transformation

  • The method is declared after the member that held the local function, after a blank line, with the local function's signature, modifiers and body. It is private, and static when the member is static or the local function reaches no instance member, through this, base or a simple name.
  • Each local or parameter of the member that the local function reads becomes a parameter of the method, in declaration order, after its own parameters, and is passed by name at every call. A variable the local function assigns is a ref parameter, passed with ref.
  • Each type parameter of the enclosing method that the local function uses, directly or through a captured variable's type, becomes a type parameter of the method, after its own, with its constraint. Calls pass type arguments explicitly only when one of them cannot be inferred from the parameters.
  • Every call, including recursive calls, uses the method's name.
  • In a nullable context, a captured parameter keeps its annotation. A local declared with var is passed as not nullable where its value is known not to be null.

Preserved

  • Behaviour: the method sees the values the captured variables hold at each call, and assignments to them are made through ref.
  • Comments above and inside the local function move with it.

Limitations

  • Another local of the member with the method's name would hide the method at the calls; this is not checked.

Error codes

CodeMeaning
not-a-local-functionthe target is not a local function or a call to one
name-conflictthe type already has a member with the method's name
used-as-delegatethe local function captures variables and is used without being called
calls-local-functionthe local function calls another local function of its member
ref-in-asyncan async or iterator local function assigns a captured variable

Cases

·

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

#async-writes-captured-rejected

Refuses an async function that assigns a captured variable, which an async method cannot take by ref

refusal
TargetSample.cs, the /*^*/ caret
Refusesref-in-async; every file is left unchanged
Sample.csinput
1 using System.Threading.Tasks;
2
3 public class Sample
4 {
5 public async Task<int> CountAsync(int times)
6 {
7 var count = 0;
8 for (var i = 0; i < times; i++)
9 await StepAsync();
10 return count;
11
12 async Task /*^*/StepAsync()
13 {
14 await Task.Yield();
15 count++;
16 }
17 }
18 }

#by-symbol

In a later step of a composite, the containing member's symbol and the function's name pick the local function

success
Targetsymbol M:Sample.Total(System.Int32[])
Arguments
function"WithTax"
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+ }
910
10− static int WithTax(int price) => price * 120 / 100;
11− }
11+ private static int WithTax(int price) => price * 120 / 100;
1212 }

#calls-local-function-rejected

Refuses a function that calls another local function, which the method could not reach

refusal
TargetSample.cs, the /*^*/ caret
Refusescalls-local-function; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Total(int[] prices)
4 {
5 var sum = 0;
6 foreach (var price in prices)
7 sum += WithTax(price);
8 return sum;
9
10 int /*^*/WithTax(int price) => Round(price * 120) / 100;
11
12 int Round(int value) => value - value % 10;
13 }
14 }

#captured-variables

A parameter and a local the function reads become parameters of the method, passed at each call

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public int Total(int[] prices, int rate)
44 {
55 var bonus = prices.Length > 3 ? 1 : 0;
66 var sum = 0;
77 foreach (var price in prices)
8− sum += /*^*/WithTax(price);
8+ sum += WithTax(price, rate, bonus);
99 return sum;
10+ }
1011
11− int WithTax(int price)
12− {
13− return price * (100 + rate) / 100 + bonus;
14− }
12+ private static int WithTax(int price, int rate, int bonus)
13+ {
14+ return price * (100 + rate) / 100 + bonus;
1515 }
1616 }

#generic-enclosing-method

A type parameter of the enclosing method that the function uses becomes a type parameter of the method, with its constraint

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 public class Sample
55 {
66 public List<T> Twice<T>(T item) where T : IComparable<T>
77 {
8− return Pair();
8+ return Pair(item);
9+ }
910
10− List<T> /*^*/Pair() => new List<T> { item, item };
11− }
11+ private static List<T> Pair<T>(T item) where T : IComparable<T> => new List<T> { item, item };
1212 }

#keeps-comments

Comments above and inside the local function move with it

success
TargetSample.cs, the /*^*/ caret
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+ }
1112
12− // Twenty percent, in whole units.
13− int /*^*/WithTax(int price)
14− {
15− // Multiply first to keep precision.
16− return price * 120 / 100;
17− }
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;
1818 }
1919 }

#name-conflict-rejected

Refuses when the type already has a member with the method's name

refusal
TargetSample.cs, the /*^*/ caret
Refusesname-conflict; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Total(int[] prices)
4 {
5 var sum = 0;
6 foreach (var price in prices)
7 sum += WithTax(price);
8 return sum;
9
10 static int /*^*/WithTax(int price) => price * 120 / 100;
11 }
12
13 public bool WithTax { get; set; }
14 }

#no-captures

A local function using nothing of its method or instance becomes a private static method after the method

success
TargetSample.cs, the /*^*/ caret
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+ }
910
10− int /*^*/WithTax(int price) => price * 120 / 100;
11− }
11+ private static int WithTax(int price) => price * 120 / 100;
1212
1313 public int Count(int[] prices) => prices.Length;
1414 }

#not-a-local-function-rejected

Refuses a caret on something other than a local function

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-a-local-function; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Total(int[] prices)
4 {
5 var /*^*/sum = 0;
6 foreach (var price in prices)
7 sum += price;
8 return sum;
9 }
10 }

#nullable-context

A captured nullable parameter keeps its annotation as a parameter of the method

success
TargetSample.cs, the /*^*/ caret
Projectnullable enable
Sample.csmodified
11 public class Sample
22 {
33 public string Label(string? name, string? fallback)
44 {
5− return Clean(name) ?? "none";
5+ return Clean(name, fallback) ?? "none";
6+ }
67
7− string? /*^*/Clean(string? text) => text?.Trim() ?? fallback;
8− }
8+ private static string? Clean(string? text, string? fallback) => text?.Trim() ?? fallback;
99 }

#recursive

A recursive call inside the function passes the captured variables on too

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public int SumTo(int limit, int step)
44 {
5− return /*^*/Sum(0);
5+ return Sum(0, limit, step);
6+ }
67
7− int Sum(int from) => from > limit ? 0 : from + Sum(from + step);
8− }
8+ private static int Sum(int from, int limit, int step) => from > limit ? 0 : from + Sum(from + step, limit, step);
99 }

#renamed

The method takes the name given, and every call uses it

success
TargetSample.cs, the /*^*/ caret
Arguments
name"ApplyTax"
Sample.csmodified
11 public class Sample
22 {
33 public int Total(int[] prices)
44 {
55 var sum = 0;
66 foreach (var price in prices)
7− sum += WithTax(price);
7+ sum += ApplyTax(price);
88 return sum;
9+ }
910
10− static int /*^*/WithTax(int price) => price * 120 / 100;
11− }
11+ private static int ApplyTax(int price) => price * 120 / 100;
1212 }

#static-local-function

A static local function in a static method becomes a private static method

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public static class Sample
22 {
33 public static string Label(string name)
44 {
55 return Clean(name) + ":";
6+ }
67
7− static string /*^*/Clean(string text) => text.Trim();
8− }
8+ private static string Clean(string text) => text.Trim();
99 }

#type-arguments-not-inferred

A type parameter the calls cannot infer is passed explicitly

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe<T>()
44 {
5− return Name() + "!";
5+ return Name<T>() + "!";
6+ }
67
7− string /*^*/Name() => typeof(T).Name;
8− }
8+ private static string Name<T>() => typeof(T).Name;
99 }

#used-as-delegate-rejected

Refuses a function that captures a variable and is used as a delegate, which the method could not be passed as

refusal
TargetSample.cs, the /*^*/ caret
Refusesused-as-delegate; every file is left unchanged
Sample.csinput
1 using System.Collections.Generic;
2 using System.Linq;
3
4 public class Sample
5 {
6 public List<int> WithTax(int[] prices, int rate)
7 {
8 return prices.Select(Apply).ToList();
9
10 int /*^*/Apply(int price) => price * (100 + rate) / 100;
11 }
12 }

#uses-instance-member

A local function reading a field becomes an instance method

success
TargetSample.cs, the /*^*/ caret
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+ }
1112
12− int /*^*/WithTax(int price) => price * (100 + _rate) / 100;
13− }
13+ private int WithTax(int price) => price * (100 + _rate) / 100;
1414 }

#written-variable

A local the function assigns becomes a ref parameter, passed by ref

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public int Total(int[] prices)
44 {
55 var sum = 0;
66 foreach (var price in prices)
7− Add(price);
7+ Add(price, ref sum);
88 return sum;
9+ }
910
10− void /*^*/Add(int price)
11− {
12− sum += price;
13− }
11+ private static void Add(int price, ref int sum)
12+ {
13+ sum += price;
1414 }
1515 }