RefactorMCP catalog

Change Signature

Adds, removes and reorders the parameters of a method or constructor, and updates every call in the solution to match. Overrides, overridden methods, interface members and their implementations change together, since they must keep the same parameters.

Many composites build on this primitive: Introduce Parameter Object, Preserve Whole Object, Parameterise Method and Constructor Injection all change a signature as one of their steps.

Arguments

parameters is the complete new parameter list, in order. An existing parameter is named; a new one also gives its type, and either the value every existing call passes for it or a default, or both. A parameter of the old signature that is not listed is removed.

"arguments": {
  "parameters": [
    { "name": "b" },
    { "name": "a" },
    { "name": "scale", "type": "int", "value": "1" },
    { "name": "label", "type": "string?", "default": "null" }
  ]
}
FieldMeaning
namean existing parameter's name, or the new parameter's name
typethe type of a new parameter; given only for new parameters
valuea C# expression each existing call passes for a new parameter
defaulta default value declared for a new parameter; with no value, existing calls leave it out

replacements is optional. For a removed parameter the body still uses, it gives the expression that takes the place of each use, usually one reading the parameter's value from a new parameter:

"arguments": {
  "parameters": [ { "name": "point", "type": "Point", "value": "new Point(3, 4)" } ],
  "replacements": { "x": "point.X", "y": "point.Y" }
}

The target is the method or constructor, by symbol. Targeting an override or an implementation changes the whole family.

Precondition

  • Every listed name is either an existing parameter or comes with a type.
  • A new parameter has a value for existing calls, a default, or both.
  • No removed parameter is read or written in the body of any member of the family, unless replacements gives an expression for it. A replaced parameter is only read, never assigned, and only removed parameters are replaced.
  • A params array stays last, and no required parameter follows an optional one.
  • The this parameter of an extension method stays first.
  • Every member of the family is declared in the solution; a method that implements or overrides a member from a referenced assembly cannot change.
  • The method is only called, never converted to a delegate: a method group would no longer match its delegate type.
  • The result compiles.

Transformation

  • Each declaration in the family gets the new parameter list. New parameters are declared with the given type and default.
  • Each use of a replaced parameter, in every body of the family, becomes its expression, in parentheses unless it is a name or member access.
  • Each call's arguments are rearranged into the new order, with the given value for each new parameter and nothing for a removed one. This covers invocations, base calls, object creation, target-typed new, and this() and base() constructor initializers.
  • Named arguments keep their names. Once a call leaves an optional parameter to its default, every argument after it is named.
  • The arguments a call passes to a params array in expanded form stay together, in order, at the end.
  • A call to an extension method in reduced form keeps its receiver.
  • References in nameof and documentation comments are left alone.

Preserved

  • Every call passes the same values to the same parameters, so the method sees what it saw before, apart from new parameters, which receive the given value.
  • Layout: each position in a parameter or argument list keeps its line breaks and indentation, so a list laid out one per line stays that way.
  • Comments travel with the parameter or argument they belong to, including a line comment after its comma.
  • Nullable annotations on new and existing parameters.

Limitations

  • Arguments are evaluated in the new parameter order, so reordering calls whose arguments have side effects that depend on each other changes the order those side effects happen in.
  • A removed parameter's argument is dropped even if evaluating it had side effects. Remove Unused Parameter refuses in that case.
  • A replacement is taken on trust: behaviour is preserved only when, at every call, the expression gives the value the call passed for the parameter.
  • <param> elements in documentation comments are not reordered, added or removed, and cref references with parameter lists are not updated.
  • Delegates, indexers, operators, attribute constructors and primary constructors are not covered.
  • A call that passes several values to a params array after an argument it leaves out cannot be named, and is refused.

Error codes

CodeMeaning
removed-parameter-in-usea parameter left out of the new list is used in a body
replaced-parameter-assigneda parameter given a replacement is assigned in a body
replaced-parameter-kepta parameter given a replacement is still in the new list
unknown-parametera listed name is not a parameter and has no type to add it
duplicate-parametera new parameter has the name of an existing one
missing-valuea new parameter has neither a value for calls nor a default
optional-before-requireda required parameter would follow an optional one
params-not-lasta params array would not be last
extension-this-movedthe this parameter of an extension method would move
method-group-referencethe method is used as a method group
external-memberthe method overrides or implements a member outside the solution

Cases

·

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

#add-parameter-across-files

Adds a parameter and passes the given value at calls in another file

success
Targetsymbol M:Shop.Order.Total(System.Decimal)
Arguments
parameters[{"name": "subtotal"}, {"name": "shipping", "type": "decimal", "value": "0m"}]
Checkout.csmodified
11 namespace Shop;
22
33 public class Checkout
44 {
55 public decimal Pay(Order order)
66 {
7− return order.Total(100m) + order.Total(20m);
7+ return order.Total(100m, 0m) + order.Total(20m, 0m);
88 }
99 }
Order.csmodified
11 namespace Shop;
22
33 public class Order
44 {
5− public decimal Total(decimal subtotal)
5+ public decimal Total(decimal subtotal, decimal shipping)
66 {
77 return subtotal;
88 }
99 }

#constructor

Adds a parameter to a constructor, updating object creation, target-typed new, this() and base() calls

success
Targetsymbol M:Shop.Account.#ctor(System.String)
Arguments
parameters[{"name": "owner"}, {"name": "limit", "type": "decimal", "value": "0m"}]
Account.csmodified
11 namespace Shop;
22
33 public class Account
44 {
55 private readonly string _owner;
66
7− public Account(string owner)
7+ public Account(string owner, decimal limit)
88 {
99 _owner = owner;
1010 }
1111
12− public Account() : this("guest")
12+ public Account() : this("guest", 0m)
1313 {
1414 }
1515
1616 public string Owner => _owner;
1717 }
1818
1919 public class Savings : Account
2020 {
21− public Savings(string owner) : base(owner)
21+ public Savings(string owner) : base(owner, 0m)
2222 {
2323 }
2424 }
Bank.csmodified
11 namespace Shop;
22
33 public class Bank
44 {
55 public Account Open(string name)
66 {
7− Account first = new Account(name);
8− Account second = new("joint");
7+ Account first = new Account(name, 0m);
8+ Account second = new("joint", 0m);
99 return first.Owner == name ? first : second;
1010 }
1111 }

#duplicate-parameter-rejected

Refuses to add a parameter with the name of one the method already has

refusal
Targetsymbol M:Shop.Pricing.Discount(System.Decimal,System.Int32)
Arguments
parameters[{"name": "price"}, {"name": "percent"}, {"name": "price", "type": "decimal", "value": "0m"}]
Refusesduplicate-parameter mentioning “price”; every file is left unchanged
Pricing.csinput
1 namespace Shop;
2
3 public class Pricing
4 {
5 public decimal Discount(decimal price, int percent)
6 {
7 return price * percent / 100m;
8 }
9 }

#extension-method

Adds a parameter to an extension method, called both as an extension and as a static method

success
Targetsymbol M:Shop.TextExtensions.Truncate(System.String,System.Int32)
Arguments
parameters[{"name": "text"}, {"name": "length"}, {"name": "ellipsis", "type": "string", "value": "\"...\""}]
TextExtensions.csmodified
11 namespace Shop;
22
33 public static class TextExtensions
44 {
5− public static string Truncate(this string text, int length)
5+ public static string Truncate(this string text, int length, string ellipsis)
66 {
77 return text.Length <= length ? text : text.Substring(0, length);
88 }
99 }
1010
1111 public class Labels
1212 {
1313 public string Short(string name)
1414 {
15− return name.Truncate(10) + TextExtensions.Truncate(name, 3);
15+ return name.Truncate(10, "...") + TextExtensions.Truncate(name, 3, "...");
1616 }
1717 }

#extension-this-moved-rejected

Refuses to move the this parameter of an extension method away from the front

refusal
Targetsymbol M:Shop.TextExtensions.Truncate(System.String,System.Int32)
Arguments
parameters[{"name": "length"}, {"name": "text"}]
Refusesextension-this-moved; every file is left unchanged
TextExtensions.csinput
1 namespace Shop;
2
3 public static class TextExtensions
4 {
5 public static string Truncate(this string text, int length)
6 {
7 return text.Length <= length ? text : text.Substring(0, length);
8 }
9 }

#external-member-rejected

Refuses to change a method that implements an interface declared outside the solution

refusal
Targetsymbol M:Shop.Item.CompareTo(Shop.Item)
Arguments
parameters[{"name": "other"}, {"name": "descending", "type": "bool", "value": "false"}]
Refusesexternal-member mentioning “IComparable”; every file is left unchanged
Item.csinput
1 using System;
2
3 namespace Shop;
4
5 public class Item : IComparable<Item>
6 {
7 public int Rank { get; set; }
8
9 public int CompareTo(Item other)
10 {
11 return Rank.CompareTo(other.Rank);
12 }
13 }

#generic-method

Reorders a generic method's parameters at calls with inferred and explicit type arguments

success
Targetsymbol M:Shop.Choice.Pick``1(System.Boolean,``0,``0)
Arguments
parameters[{"name": "first"}, {"name": "second"}, {"name": "preferFirst"}]
Choice.csmodified
11 namespace Shop;
22
33 public static class Choice
44 {
5− public static T Pick<T>(bool preferFirst, T first, T second)
5+ public static T Pick<T>(T first, T second, bool preferFirst)
66 {
77 return preferFirst ? first : second;
88 }
99 }
1010
1111 public class Menu
1212 {
1313 public string Main()
1414 {
15− return Choice.Pick(true, "soup", "salad") + Choice.Pick<int>(false, 1, 2);
15+ return Choice.Pick("soup", "salad", true) + Choice.Pick<int>(1, 2, false);
1616 }
1717 }

#interface-and-implementations

Reorders an interface method's parameters in every implementation, explicit ones included, and at calls through the interface and through a class

success
Targetsymbol M:Shop.IShipping.Cost(System.Decimal,System.String)
Arguments
parameters[{"name": "region"}, {"name": "weight"}]
Quote.csmodified
11 namespace Shop;
22
33 public class Quote
44 {
55 public decimal Price(IShipping shipping, Courier courier)
66 {
7− return shipping.Cost(1.5m, "EU") + courier.Cost(2m, "US");
7+ return shipping.Cost("EU", 1.5m) + courier.Cost("US", 2m);
88 }
99 }
Shipping.csmodified
11 namespace Shop;
22
33 public interface IShipping
44 {
5− decimal Cost(decimal weight, string region);
5+ decimal Cost(string region, decimal weight);
66 }
77
88 public class Courier : IShipping
99 {
10− public decimal Cost(decimal weight, string region)
10+ public decimal Cost(string region, decimal weight)
1111 {
1212 return weight * 2m;
1313 }
1414 }
1515
1616 public class Post : IShipping
1717 {
18− decimal IShipping.Cost(decimal weight, string region)
18+ decimal IShipping.Cost(string region, decimal weight)
1919 {
2020 return region == "EU" ? weight : weight * 3m;
2121 }
2222 }

#method-group-rejected

Refuses when the method is converted to a delegate, which would no longer match

refusal
Targetsymbol M:Shop.Calculator.Add(System.Int32,System.Int32)
Arguments
parameters[{"name": "b"}, {"name": "a"}]
Refusesmethod-group-reference mentioning “Add”; every file is left unchanged
Calculator.csinput
1 using System;
2
3 namespace Shop;
4
5 public class Calculator
6 {
7 public int Add(int a, int b)
8 {
9 return a + b;
10 }
11
12 public Func<int, int, int> Operation()
13 {
14 return Add;
15 }
16 }

#missing-value-rejected

Refuses to add a parameter with neither a value for existing calls nor a default

refusal
Targetsymbol M:Shop.Pricing.Discount(System.Decimal,System.Int32)
Arguments
parameters[{"name": "price"}, {"name": "percent"}, {"name": "rounding", "type": "int"}]
Refusesmissing-value mentioning “rounding”; every file is left unchanged
Pricing.csinput
1 namespace Shop;
2
3 public class Pricing
4 {
5 public decimal Discount(decimal price, int percent)
6 {
7 return price * percent / 100m;
8 }
9 }

#named-and-omitted-arguments

Reorders and adds an optional parameter; named arguments keep their names and an argument after an omitted one becomes named

success
Targetsymbol M:Shop.Mailer.Compose(System.String,System.String,System.Boolean,System.String)
Arguments
parameters[{"name": "subject"}, {"name": "to"}, {"name": "priority", "type": "int", "default": "0"}, {"name": "urgent"}, {"name": "footer"}]
Mailer.csmodified
11 namespace Shop;
22
33 public class Mailer
44 {
5− public string Compose(string to, string subject, bool urgent = false, string footer = "")
5+ public string Compose(string subject, string to, int priority = 0, bool urgent = false, string footer = "")
66 {
77 return to + subject + urgent + footer;
88 }
99
1010 public void Send()
1111 {
12− Compose("a@example.com", "Hello");
12+ Compose("Hello", "a@example.com");
1313 Compose(subject: "Hi", to: "b@example.com");
14− Compose("c@example.com", "Report", footer: "Thanks");
15− Compose("d@example.com", "Memo", true);
14+ Compose("Report", "c@example.com", footer: "Thanks");
15+ Compose("Memo", "d@example.com", urgent: true);
1616 }
1717 }

#nullable-parameter

Adds a nullable reference parameter in a nullable context, passing null

success
Targetsymbol M:Shop.Greeter.Greet(System.String)
Arguments
parameters[{"name": "name"}, {"name": "title", "type": "string?", "value": "null"}]
Projectnullable enable
Greeter.csmodified
11 namespace Shop;
22
33 public class Greeter
44 {
5− public string Greet(string name)
5+ public string Greet(string name, string? title)
66 {
77 return "Hello " + name;
88 }
99
1010 public string Welcome(string? nickname)
1111 {
12− return Greet(nickname ?? "friend");
12+ return Greet(nickname ?? "friend", null);
1313 }
1414 }

#optional-before-required-rejected

Refuses an order that puts an optional parameter before a required one

refusal
Targetsymbol M:Shop.Mailer.Compose(System.String,System.String)
Arguments
parameters[{"name": "subject"}, {"name": "to"}]
Refusesoptional-before-required mentioning “subject”; every file is left unchanged
Mailer.csinput
1 namespace Shop;
2
3 public class Mailer
4 {
5 public string Compose(string to, string subject = "")
6 {
7 return to + subject;
8 }
9 }

#overloads

Changes one overload and its calls, leaving the other overload and its calls alone

success
Targetsymbol M:Shop.Geometry.Area(System.Int32,System.Int32)
Arguments
parameters[{"name": "height"}, {"name": "width"}]
Geometry.csmodified
11 namespace Shop;
22
33 public class Geometry
44 {
55 public int Area(int side)
66 {
77 return Area(side, side);
88 }
99
10− public int Area(int width, int height)
10+ public int Area(int height, int width)
1111 {
1212 return width * height;
1313 }
1414
1515 public int Total()
1616 {
17− return Area(3) + Area(4, 5);
17+ return Area(3) + Area(5, 4);
1818 }
1919 }

#override-hierarchy

Targeting an override changes the virtual method it overrides too, and the base call inside it

success
Targetsymbol M:Shop.LoudNotifier.Format(System.String,System.Int32)
Arguments
parameters[{"name": "message"}, {"name": "level"}, {"name": "channel", "type": "string", "value": "\"email\""}]
Notifier.csmodified
11 namespace Shop;
22
33 public class Notifier
44 {
5− public virtual string Format(string message, int level)
5+ public virtual string Format(string message, int level, string channel)
66 {
77 return level + ": " + message;
88 }
99 }
1010
1111 public class LoudNotifier : Notifier
1212 {
13− public override string Format(string message, int level)
13+ public override string Format(string message, int level, string channel)
1414 {
15− return base.Format(message.ToUpperInvariant(), level) + "!";
15+ return base.Format(message.ToUpperInvariant(), level, "email") + "!";
1616 }
1717 }
1818
1919 public class Alerts
2020 {
2121 public string Raise(Notifier notifier)
2222 {
23− return notifier.Format("disk full", 2);
23+ return notifier.Format("disk full", 2, "email");
2424 }
2525 }

#params-array

Adds a parameter before a params array; expanded arguments stay together at the end

success
Targetsymbol M:Shop.Logger.Log(System.String,System.Object[])
Arguments
parameters[{"name": "level", "type": "int", "value": "0"}, {"name": "format"}, {"name": "values"}]
Logger.csmodified
11 namespace Shop;
22
33 public class Logger
44 {
5− public string Log(string format, params object[] values)
5+ public string Log(int level, string format, params object[] values)
66 {
77 return string.Format(format, values);
88 }
99
1010 public string Run()
1111 {
12− return Log("{0} {1}", 1, 2) + Log("none");
12+ return Log(0, "{0} {1}", 1, 2) + Log(0, "none");
1313 }
1414 }

#params-not-last-rejected

Refuses an order that moves a params array away from the end

refusal
Targetsymbol M:Shop.Logger.Log(System.String,System.Object[])
Arguments
parameters[{"name": "values"}, {"name": "format"}]
Refusesparams-not-last mentioning “values”; every file is left unchanged
Logger.csinput
1 namespace Shop;
2
3 public class Logger
4 {
5 public string Log(string format, params object[] values)
6 {
7 return string.Format(format, values);
8 }
9 }

#preserves-trivia

Reorders parameters and arguments laid out one per line; comments travel with their parameter or argument and the layout stays

success
Targetsymbol M:Shop.Invoice.Line(System.String,System.Int32,System.Decimal)
Arguments
parameters[{"name": "quantity"}, {"name": "product"}, {"name": "price"}]
Invoice.csmodified
11 namespace Shop;
22
33 public class Invoice
44 {
55 /// <summary>Builds a line.</summary>
66 public string Line(
7+ int quantity,
78 string product, // what was sold
8− int quantity,
99 decimal price)
1010 {
1111 return product + quantity + price;
1212 }
1313
1414 public string Print()
1515 {
1616 // two line items
1717 return Line(
18+ 2,
1819 "pen", // product
19− 2,
20− 1.5m) + Line("cup", /* count */ 1, 2m);
20+ 1.5m) + Line(/* count */ 1, "cup", 2m);
2121 }
2222 }

#remove-parameter

Removes a parameter the body does not read, and its argument

success
Targetsymbol M:Shop.Report.Title(System.String,System.Int32)
Arguments
parameters[{"name": "name"}]
Report.csmodified
11 namespace Shop;
22
33 public class Report
44 {
5− public string Title(string name, int width)
5+ public string Title(string name)
66 {
77 return name.ToUpperInvariant();
88 }
99
1010 public string Header()
1111 {
12− return Title("sales", 80);
12+ return Title("sales");
1313 }
1414 }

#removed-parameter-in-use-rejected

Refuses to remove a parameter the body reads

refusal
Targetsymbol M:Shop.Pricing.Discount(System.Decimal,System.Int32)
Arguments
parameters[{"name": "percent"}]
Refusesremoved-parameter-in-use mentioning “price”; every file is left unchanged
Pricing.csinput
1 namespace Shop;
2
3 public class Pricing
4 {
5 public decimal Discount(decimal price, int percent)
6 {
7 return price * percent / 100m;
8 }
9 }

#reorder-parameters

Swaps two parameters and the arguments of a call in the same class

success
Targetsymbol M:Shop.Pricing.Discount(System.Decimal,System.Int32)
Arguments
parameters[{"name": "percent"}, {"name": "price"}]
Pricing.csmodified
11 namespace Shop;
22
33 public class Pricing
44 {
5− public decimal Discount(decimal price, int percent)
5+ public decimal Discount(int percent, decimal price)
66 {
77 return price * percent / 100m;
88 }
99
1010 public decimal Sale(decimal price)
1111 {
12− return price - Discount(price, 10);
12+ return price - Discount(10, price);
1313 }
1414 }

#replace-removed-parameter-uses

Removes parameters the body uses by replacing each use with an expression over the new parameter

success
Targetsymbol M:Geometry.Grid.DistanceFromOrigin(System.Int32,System.Int32)
Arguments
parameters[{"name": "point", "type": "Point", "value": "new Point(3, 4)"}]
replacements{"x": "point.X", "y": "point.Y"}
Grid.csmodified
11 using System;
22
33 namespace Geometry
44 {
55 public record Point(int X, int Y);
66
77 public class Grid
88 {
9− public double DistanceFromOrigin(int x, int y)
9+ public double DistanceFromOrigin(Point point)
1010 {
11− return Math.Sqrt(x * x + y * y);
11+ return Math.Sqrt(point.X * point.X + point.Y * point.Y);
1212 }
1313
1414 public double Sample()
1515 {
16− return DistanceFromOrigin(3, 4);
16+ return DistanceFromOrigin(new Point(3, 4));
1717 }
1818 }
1919 }

#replaced-parameter-assigned-rejected

Refuses to replace the uses of a parameter the body assigns, since an expression cannot be assigned in its place

refusal
Targetsymbol M:Geometry.Grid.Clamp(System.Int32,System.Int32)
Arguments
parameters[{"name": "max"}, {"name": "limits", "type": "Limits", "value": "new Limits(0)"}]
replacements{"value": "limits.Min"}
Refusesreplaced-parameter-assigned mentioning “'value'”; every file is left unchanged
Grid.csinput
1 namespace Geometry
2 {
3 public record Limits(int Min);
4
5 public class Grid
6 {
7 public int Clamp(int value, int max)
8 {
9 if (value > max)
10 value = max;
11 return value;
12 }
13
14 public int Sample()
15 {
16 return Clamp(12, 10);
17 }
18 }
19 }

#unknown-parameter-rejected

Refuses a parameter that neither exists nor has a type to add it with

refusal
Targetsymbol M:Shop.Pricing.Discount(System.Decimal,System.Int32)
Arguments
parameters[{"name": "price"}, {"name": "rate"}]
Refusesunknown-parameter mentioning “rate”; every file is left unchanged
Pricing.csinput
1 namespace Shop;
2
3 public class Pricing
4 {
5 public decimal Discount(decimal price, int percent)
6 {
7 return price * percent / 100m;
8 }
9 }