RefactorMCP catalog

Convert String Concatenation to Interpolation

Turns a chain of + string concatenations into one interpolated string: "Hello, " + name + "!" becomes $"Hello, {name}!".

Target

The concatenation, by a caret anywhere in it. The whole chain the caret is in is converted, through parentheses around nested concatenations.

Precondition

  • The expression is a + that concatenates strings.
  • No operand of the chain is itself a numeric addition, as in a + b + " total", where a + b adds numbers before the concatenation.
  • There are no comments between the operands.

Transformation

  • String literals become text, keeping their escapes as written. Braces are doubled so they stay text.
  • The result is a verbatim $@"..." when every string literal is verbatim. Otherwise it is a regular $"...", and verbatim literals are escaped.
  • Character literals become text.
  • An interpolated string operand is merged in.
  • Any other operand becomes an interpolation {operand}. Parentheses around it are dropped, except around a conditional, whose colon would start a format.
  • value.ToString("F2") on a formattable value type becomes {value:F2}.
  • The result is written on one line, in place of the chain.

Preserved

  • The resulting string: interpolation formats each value with the current culture, as concatenation does.
  • Comments before and after the expression.

Limitations

  • A value of a type whose IFormattable.ToString(null, provider) differs from its ToString() is formatted by the former after conversion.
  • Raw string literals are interpolated as values rather than merged as text.
  • An expression split over several lines becomes one line, however long.

Error codes

CodeMeaning
not-a-concatenationthe expression at the caret is not a string concatenation
numeric-additionan operand of the chain adds numbers before the concatenation
contains-commentsthere are comments between the operands, which the string could not keep

Cases

·

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

#char-operand

A character literal becomes text

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return name + /*^*/':' + ' ' + count;
5+ return $"{name}: {count}";
66 }
77 }

#comments-rejected

Refuses a concatenation with comments between its operands, which the string could not keep

refusal
TargetSample.cs, the /*^*/ caret
Refusescontains-comments; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public string Describe(string name, int count)
4 {
5 return /*^*/"Name: " + name + // who
6 ", count: " + count;
7 }
8 }

#conditional-operand

A conditional operand keeps its parentheses, whose colon would otherwise start a format

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return "Status: " + /*^*/(ok ? "on" : "off");
5+ return $"Status: {(ok ? "on" : "off")}";
66 }
77 }

#escapes-braces

Braces in the literals are doubled so they stay text

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return /*^*/"{" + name + "}";
5+ return $"{{{name}}}";
66 }
77 }

#format-string

A value formatted with ToString and a format string becomes a format specifier

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return /*^*/"Price: " + price.ToString("F2") + " each";
5+ return $"Price: {price:F2} each";
66 }
77 }

#generic

A value of a type parameter is interpolated

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe<T>(T value)
44 {
5− return /*^*/"Value: " + value;
5+ return $"Value: {value}";
66 }
77 }

#interpolated-operand

An interpolated string operand is merged into the result

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return $"{count} x " + /*^*/name;
5+ return $"{count} x {name}";
66 }
77 }

#keeps-escape-sequences

Escape sequences in the literals are kept as written

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return /*^*/"Name:\t\"" + name + "\"\n";
5+ return $"Name:\t\"{name}\"\n";
66 }
77 }

#mixed-verbatim

A verbatim literal among regular ones has its text escaped for a regular interpolated string

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return /*^*/@"C:\Temp\" + name + ".txt\n";
5+ return $"C:\\Temp\\{name}.txt\n";
66 }
77 }

#multi-line

A concatenation split over several lines becomes one line, keeping the statement's comments

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count)
44 {
55 // The summary line.
6− var text = /*^*/"Name: " +
7− name +
8− ", count: " +
9− count; // for the log
6+ var text = $"Name: {name}, count: {count}"; // for the log
107 return text;
118 }
129 }

#non-string-operand

A number is interpolated as concatenation would format it

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return "Total: " /*^*/+ count + " items";
5+ return $"Total: {count} items";
66 }
77 }

#not-a-concatenation-rejected

Refuses an addition that does not produce a string

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-a-concatenation; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
4 {
5 return (a /*^*/+ b).ToString();
6 }
7 }

#nullable-context

In a nullable context a nullable operand is interpolated as it was concatenated

success
TargetSample.cs, the /*^*/ caret
Projectnullable enable
Sample.csmodified
11 public class Sample
22 {
33 public string Greet(string? name)
44 {
5− return /*^*/"Hello " + name;
5+ return $"Hello {name}";
66 }
77 }

#numeric-addition-rejected

Refuses a chain whose leading operands are added as numbers before the concatenation

refusal
TargetSample.cs, the /*^*/ caret
Refusesnumeric-addition; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
4 {
5 return a + b + /*^*/" total";
6 }
7 }

#parenthesised-addition

A parenthesised numeric addition is interpolated without its parentheses

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return "Sum: " + /*^*/(a + b);
5+ return $"Sum: {a + b}";
66 }
77 }

#simple

String literals and a string operand become one interpolated string

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return "Hello, " + /*^*/name + "!";
5+ return $"Hello, {name}!";
66 }
77 }

#verbatim

Verbatim literals make a verbatim interpolated string

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 public class Sample
22 {
33 public string Describe(string name, int count, bool ok, decimal price, int a, int b)
44 {
5− return /*^*/@"C:\Temp\" + name + @".txt";
5+ return $@"C:\Temp\{name}.txt";
66 }
77 }