RefactorMCP catalog

Convert If to Switch Expression

Turns an if / else if chain that compares one value with constants or patterns, and whose branches each produce a value, into a switch expression that is returned or assigned.

Recipe

  1. Convert If Chain to Switch Statement, with the caret on the first if: { "refactoring": "convert-if-chain-to-switch", "target": { "file": "Shipping.cs", "caret": "marker" } }.
  2. Convert Switch Statement to Switch Expression, with the caret on the switch keyword, which starts where the if did: { "refactoring": "convert-switch-statement-to-expression", "target": { "file": "Shipping.cs", "range": "7:13-7:13" } }.

A later step cannot use a marker, so it gives the caret as a range whose start is the caret.

Target

The first if of the chain, by caret: "target": { "file": "Shipping.cs", "caret": "marker" }.

Precondition

Both steps' preconditions hold for the chain:

  • The chain has at least two cases, counting a final else, and every condition tests the same value, without side effects, as a constant comparison, a pattern test, an || of those, or one of those followed by && condition.
  • No branch contains a break of an enclosing loop.
  • Every branch is one of return value;, variable = value; (the same variable in every such branch) or throw exception;, and the branches either all return or all assign, apart from those that throw.
  • A chain without a final else returns in its branches and is followed directly by a return or throw, which becomes the discard arm. Otherwise the switch expression would throw for a value no branch matches, where the chain does nothing.

Transformation

  • The chain becomes return x switch { ... }; or variable = x switch { ... };.
  • Each branch becomes an arm, in order: x == c becomes c, x is T t becomes T t, a || b becomes a or b, x is T t && condition becomes T t when condition, and throw e; becomes throw e.
  • The final else becomes the _ arm, as does a return or throw after a chain without one, which is removed.
  • Arms are written one per line, with a trailing comma after the last.

Preserved

  • Behaviour: patterns are tried in the order of the chain, and the _ arm only applies when no condition held.
  • A comment before the chain stays before the statement. Comments inside a branch are written above its arm, and a comment at the end of a branch's statement stays at the end of the arm.

Limitations

  • A declaration immediately before an assigning chain is not merged into the assignment; combine them with Join Declaration and Assignment.
  • Chains of separate if statements that each return are not collected; only else if chains are.

Error codes

CodeMeaning
not-an-ifthe caret is not on an if statement
too-few-casesthe chain has fewer than two cases
different-valuesthe conditions do not all test the same value
unsupported-conditiona condition is not a constant comparison or a pattern test that an arm can express
side-effectsthe tested value has side effects
contains-breaka branch contains a break of an enclosing loop
unsupported-sectiona branch does something other than return a value, assign one variable or throw
different-targetsthe branches assign different variables
no-defaultthere is no final else and no return after the chain to become the discard arm

Cases

·

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

#assignments

A chain whose branches assign one variable becomes an assignment of a switch expression, with || alternatives joined by or

success
TargetTicket.cs, the /*^*/ caret
Ticket.csmodified
11 namespace Support
22 {
33 public class Ticket
44 {
55 public string Describe(int code)
66 {
77 string label;
8− /*^*/if (code == 1)
8+ label = code switch
99 {
10− label = "new";
11− }
12− else if (code == 2 || code == 3)
13− {
14− label = "open";
15− }
16− else
17− {
18− label = "closed";
19− }
10+ 1 => "new",
11+ 2 or 3 => "open",
12+ _ => "closed",
13+ };
2014
2115 return label;
2216 }
2317 }
2418 }

#branch-does-more-rejected

A branch that does more than produce a value cannot become an arm; the chain is left as it was, not half converted to a switch statement

refusal
TargetShipping.cs, the /*^*/ caret
Refusesunsupported-section; every file is left unchanged
Shipping.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Shipping
6 {
7 public decimal Rate(string zone)
8 {
9 /*^*/if (zone == "local")
10 {
11 Console.WriteLine("local rate");
12 return 2.5m;
13 }
14 else
15 {
16 return 12m;
17 }
18 }
19 }
20 }

#comments-kept

A comment above the chain stays above the statement, and comments in the branches go with their arms

success
TargetShipping.cs, the /*^*/ caret
Shipping.csmodified
11 namespace Shop
22 {
33 public class Shipping
44 {
55 public decimal Rate(string zone)
66 {
77 // Zones are priced by distance.
8− /*^*/if (zone == "local")
8+ return zone switch
99 {
1010 // Delivered the same day.
11− return 2.5m;
12− }
13− else
14− {
15− return 12m; // by courier
16− }
11+ "local" => 2.5m,
12+ _ => 12m, // by courier
13+ };
1714 }
1815 }
1916 }

#different-values-rejected

Every condition must test the same value

refusal
TargetShipping.cs, the /*^*/ caret
Refusesdifferent-values; every file is left unchanged
Shipping.csinput
1 namespace Shop
2 {
3 public class Shipping
4 {
5 public decimal Rate(string zone, string carrier)
6 {
7 /*^*/if (zone == "local")
8 {
9 return 2.5m;
10 }
11 else if (carrier == "courier")
12 {
13 return 20m;
14 }
15 else
16 {
17 return 12m;
18 }
19 }
20 }
21 }

#no-default-rejected

A chain with no else that assigns leaves the variable alone for an unmatched value, where a switch expression would throw

refusal
TargetTicket.cs, the /*^*/ caret
Refusesno-default; every file is left unchanged
Ticket.csinput
1 namespace Support
2 {
3 public class Ticket
4 {
5 public string Describe(int code)
6 {
7 var label = "unknown";
8 /*^*/if (code == 1)
9 {
10 label = "new";
11 }
12 else if (code == 2)
13 {
14 label = "open";
15 }
16
17 return label;
18 }
19 }
20 }

#not-an-if-rejected

The caret must be on the first if of the chain

refusal
TargetShipping.cs, the /*^*/ caret
Refusesnot-an-if; every file is left unchanged
Shipping.csinput
1 namespace Shop
2 {
3 public class Shipping
4 {
5 public decimal Rate(string zone)
6 {
7 var surcharge = 1m;
8 if (zone == "local")
9 {
10 return 2.5m * surcharge;
11 }
12 else
13 {
14 /*^*/return 12m * surcharge;
15 }
16 }
17 }
18 }

#patterns-and-following-return

Type patterns with conditions become arms with when clauses, a throwing branch becomes a throw expression, and the return after a chain with no else becomes the discard arm

success
TargetGeometry.cs, the /*^*/ caret
Geometry.csmodified
11 using System;
22
33 namespace Drawing
44 {
55 public abstract class Shape
66 {
77 }
88
99 public class Circle : Shape
1010 {
1111 public double Radius { get; set; }
1212 }
1313
1414 public class Square : Shape
1515 {
1616 public double Side { get; set; }
1717 }
1818
1919 public class Geometry
2020 {
2121 public double Area(Shape shape)
2222 {
23− /*^*/if (shape is Circle c && c.Radius > 0)
23+ return shape switch
2424 {
25− return Math.PI * c.Radius * c.Radius;
26− }
27− else if (shape is Square s)
28− {
29− return s.Side * s.Side;
30− }
31− else if (shape is null)
32− {
33− throw new ArgumentNullException(nameof(shape));
34− }
35−
36− return 0;
25+ Circle c when c.Radius > 0 => Math.PI * c.Radius * c.Radius,
26+ Square s => s.Side * s.Side,
27+ null => throw new ArgumentNullException(nameof(shape)),
28+ _ => 0,
29+ };
3730 }
3831 }
3932 }

#recipe

The recipe: the chain becomes a switch statement, which becomes a returned switch expression

successrecipe
Recipe
  1. convert-if-chain-to-switch on Shipping.cs, the /*^*/ caret
  2. convert-switch-statement-to-expression on Shipping.cs, range 7:13-7:13
Shipping.csmodified
11 namespace Shop
22 {
33 public class Shipping
44 {
55 public decimal Rate(string zone)
66 {
7− /*^*/if (zone == "local")
7+ return zone switch
88 {
9− return 2.5m;
10− }
11− else if (zone == "national")
12− {
13− return 5m;
14− }
15− else
16− {
17− return 12m;
18− }
9+ "local" => 2.5m,
10+ "national" => 5m,
11+ _ => 12m,
12+ };
1913 }
2014 }
2115 }

#returns

A chain whose branches each return a value becomes a returned switch expression

success
TargetShipping.cs, the /*^*/ caret
Shipping.csmodified
11 namespace Shop
22 {
33 public class Shipping
44 {
55 public decimal Rate(string zone)
66 {
7− /*^*/if (zone == "local")
7+ return zone switch
88 {
9− return 2.5m;
10− }
11− else if (zone == "national")
12− {
13− return 5m;
14− }
15− else
16− {
17− return 12m;
18− }
9+ "local" => 2.5m,
10+ "national" => 5m,
11+ _ => 12m,
12+ };
1913 }
2014 }
2115 }