RefactorMCP catalog

Consolidate Conditional Expression

Combines a run of conditions that lead to the same code into one condition, and optionally gives it a name by extracting it into a method.

Recipe

  1. Merge the conditions that share a body into one if, one condition per step, on the same if, which stays where it was:
  2. Merge Sibling Ifs for if statements or else if branches that follow it, joining with ||: { "refactoring": "merge-sibling-ifs", "target": { "file": "Disability.cs", "caret": "marker" } }
  3. Merge Nested If for an if nested in it, joining with &&: { "refactoring": "merge-nested-if", "target": { "file": "Shipping.cs", "caret": "marker" } }
  4. With name, Extract Method on the combined condition, selected exactly, which extracts it into a method returning its value: { "refactoring": "extract-method", "target": { "file": "Disability.cs", "range": "12:17-12:70" }, "arguments": { "name": "IsNotEligible" } }

A later step cannot use a marker, so it gives the caret as a range whose start is the caret, such as "range": "12:13-12:13", and the condition as the range it covers once the conditions are merged.

Target and arguments

The first if, by caret: "target": { "file": "Disability.cs", "caret": "marker" }.

ArgumentRequiredMeaning
namenothe name of a private method to extract the combined condition into

Precondition

  • The caret is on an if statement that has one of these to combine with:
  • A nested if: its body is another if and nothing else, and neither has an else. Deeper nesting of the same shape is combined too.
  • else if branches directly after it with the same body.
  • if statements directly after it, with no else, with the same body. That body must always jump away (return, throw, break or continue): the second if then only runs when the first condition was false, exactly as || evaluates it. A body that can fall through would run once for each condition that holds.
  • Conditions joined with || declare no pattern or out variable, which would be unassigned when an earlier condition holds.
  • With name: the name is a valid identifier that no member of the class has, and no condition declares or assigns a variable.

Transformation

  • Nested conditions are joined with &&, outer first; the innermost body becomes the body.
  • else if branches with the same body are joined with || in order, and the chain continues with the else of the last one.
  • Consecutive if statements with the same body are joined with || in order and the later ones are removed.
  • Operands are parenthesised only where precedence requires it.
  • With name, the combined condition becomes the body of private bool Name(...) { return condition; }, placed after the member holding the if, and the if calls it. The method takes the locals and parameters the conditions read, in the order they are first read, with their types, and is static when the member holding the if is.

Preserved

  • Behaviour: the conditions are evaluated in the same order and short-circuit as before, and the body runs exactly when it did.
  • Comments above the first if stay above it; comments above the if statements it absorbs follow them.
  • Pattern variables declared by an outer condition stay in scope in the conditions after it and in the body.

Limitations

  • Only a run that starts at the caret is combined; a run that starts at a later if needs the caret there.
  • Bodies must be the same code; bodies that differ only in comments or layout count as the same, and the first one is kept.
  • The extracted method is not generic, so a condition that uses a type parameter of the containing method is refused by the compile check.

Error codes

CodeMeaning
not-an-ifthe caret is not on an if statement
nothing-to-consolidatethere is no nested if, else if or following if with the same body
body-falls-throughthe following if statements share a body that can fall through
declares-variablea condition joined with ``, or extracted, declares or assigns a variable
invalid-namename is not a valid identifier
name-conflictthe class already has a member named name

Cases

·

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

#body-falls-through-rejected

Consecutive ifs with the same body that does not jump away both run it when both hold, where one combined if would run it once

refusal
TargetAlerts.cs, the /*^*/ caret
Refusesbody-falls-through; every file is left unchanged
Alerts.csinput
1 using System;
2
3 namespace Monitoring
4 {
5 public class Alerts
6 {
7 public void Check(int load, int errors)
8 {
9 /*^*/if (load > 90)
10 {
11 Console.WriteLine("alert");
12 }
13
14 if (errors > 0)
15 {
16 Console.WriteLine("alert");
17 }
18 }
19 }
20 }

#comments-kept

Comments above each merged if are kept above the combined one, and a blank line after the last stays

success
TargetDisability.cs, the /*^*/ caret
Disability.csmodified
11 namespace Staff
22 {
33 public class Disability
44 {
55 private int _seniority;
66 private bool _isPartTime;
77 private decimal _baseAmount;
88
99 public decimal Amount()
1010 {
1111 // New starters are not covered.
12− /*^*/if (_seniority < 2)
13− {
14− return 0;
15− }
16−
1712 // Nor are part-timers.
18− if (_isPartTime)
13+ if (_seniority < 2 || _isPartTime)
1914 {
2015 return 0;
2116 }
2217
2318 return _baseAmount * 0.1m;
2419 }
2520 }
2621 }

#declares-variable-rejected

A condition that declares a pattern variable cannot be joined with ||, which would leave the variable unassigned

refusal
TargetShapes.cs, the /*^*/ caret
Refusesdeclares-variable; every file is left unchanged
Shapes.csinput
1 namespace Drawing
2 {
3 public class Circle
4 {
5 public double Radius { get; set; }
6 }
7
8 public class Shapes
9 {
10 public double Size(object shape, bool empty)
11 {
12 /*^*/if (shape is Circle c)
13 {
14 return 0;
15 }
16
17 if (empty)
18 {
19 return 0;
20 }
21
22 return 1;
23 }
24 }
25 }

#else-if-chain

Branches of an else if chain with the same body become one branch on their conditions joined by ||, whether or not the body jumps away

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)
9− {
10− label = "open";
11− }
12− else if (code == 2)
8+ if (code == 1 || code == 2)
139 {
1410 label = "open";
1511 }
1612 else
1713 {
1814 label = "closed";
1915 }
2016
2117 return label;
2218 }
2319 }
2420 }

#extract-condition

With a name, the combined condition is extracted into a private method after the containing one

success
TargetDisability.cs, the /*^*/ caret
Arguments
name"IsNotEligible"
Disability.csmodified
11 namespace Staff
22 {
33 public class Disability
44 {
55 private int _seniority;
66 private int _monthsDisabled;
77 private bool _isPartTime;
88 private decimal _baseAmount;
99
1010 public decimal Amount()
1111 {
12− /*^*/if (_seniority < 2)
13− {
14− return 0;
15− }
16−
17− if (_monthsDisabled > 12)
18− {
19− return 0;
20− }
21−
22− if (_isPartTime)
12+ if (IsNotEligible())
2313 {
2414 return 0;
2515 }
2616
2717 return _baseAmount * 0.1m;
2818 }
19+
20+ private bool IsNotEligible()
21+ {
22+ return _seniority < 2 || _monthsDisabled > 12 || _isPartTime;
23+ }
2924 }
3025 }

#extract-with-parameters

The extracted condition takes the locals and parameters it reads, in the order it first reads them, and is static when the containing method is

success
TargetTickets.cs, the /*^*/ caret
Arguments
name"IsExempt"
Tickets.csmodified
11 namespace Venue
22 {
33 public static class Tickets
44 {
55 public static decimal Fee(int age, bool member)
66 {
77 var senior = age >= 65;
8− /*^*/if (senior)
9− {
10− return 0m;
11− }
12−
13− if (member && age < 18)
8+ if (IsExempt(senior, member, age))
149 {
1510 return 0m;
1611 }
1712
1813 return 10m;
1914 }
15+
16+ private static bool IsExempt(bool senior, bool member, int age)
17+ {
18+ return senior || member && age < 18;
19+ }
2020 }
2121 }

#invalid-name-rejected

The name for the extracted condition must be a valid identifier

refusal
TargetDisability.cs, the /*^*/ caret
Arguments
name"not eligible"
Refusesinvalid-name; every file is left unchanged
Disability.csinput
1 namespace Staff
2 {
3 public class Disability
4 {
5 private int _seniority;
6 private bool _isPartTime;
7
8 public decimal Amount()
9 {
10 /*^*/if (_seniority < 2)
11 {
12 return 0;
13 }
14
15 if (_isPartTime)
16 {
17 return 0;
18 }
19
20 return 10;
21 }
22 }
23 }

#name-conflict-rejected

The name for the extracted condition must not already name a member of the class

refusal
TargetDisability.cs, the /*^*/ caret
Arguments
name"IsNotEligible"
Refusesname-conflict; every file is left unchanged
Disability.csinput
1 namespace Staff
2 {
3 public class Disability
4 {
5 private int _seniority;
6 private bool _isPartTime;
7
8 public bool IsNotEligible { get; set; }
9
10 public decimal Amount()
11 {
12 /*^*/if (_seniority < 2)
13 {
14 return 0;
15 }
16
17 if (_isPartTime)
18 {
19 return 0;
20 }
21
22 return 10;
23 }
24 }
25 }

#nested-ifs

Nested ifs without else become one if on their conditions joined by &&, in one step

success
TargetShipping.cs, the /*^*/ caret
Shipping.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Order
66 {
77 public int Id { get; set; }
88
99 public bool IsPaid { get; set; }
1010
1111 public int Items { get; set; }
1212 }
1313
1414 public class Shipping
1515 {
1616 public void Ship(Order order)
1717 {
18− /*^*/if (order != null)
18+ if (order != null && order.IsPaid && order.Items > 0)
1919 {
20− if (order.IsPaid)
21− {
22− if (order.Items > 0)
23− {
24− Console.WriteLine(order.Id);
25− }
26− }
20+ Console.WriteLine(order.Id);
2721 }
2822 }
2923 }
3024 }

#not-an-if-rejected

The caret must be on an if statement

refusal
TargetDisability.cs, the /*^*/ caret
Refusesnot-an-if; every file is left unchanged
Disability.csinput
1 namespace Staff
2 {
3 public class Disability
4 {
5 private int _seniority;
6
7 public decimal Amount()
8 {
9 if (_seniority < 2)
10 {
11 return 0;
12 }
13
14 /*^*/return 1;
15 }
16 }
17 }

#nothing-to-consolidate-rejected

An if with no nested if, no else if and no following if with the same body has nothing to combine with

refusal
TargetDisability.cs, the /*^*/ caret
Refusesnothing-to-consolidate; every file is left unchanged
Disability.csinput
1 namespace Staff
2 {
3 public class Disability
4 {
5 private int _seniority;
6 private bool _isPartTime;
7
8 public decimal Amount()
9 {
10 /*^*/if (_seniority < 2)
11 {
12 return 0;
13 }
14
15 if (_isPartTime)
16 {
17 return 5;
18 }
19
20 return 10;
21 }
22 }
23 }

#recipe

The recipe: Merge Sibling Ifs, repeated on the same if, joins three conditions with ||, then Extract Method gives the combined condition a name

successrecipe
Recipe
  1. merge-sibling-ifs on Disability.cs, the /*^*/ caret
  2. merge-sibling-ifs on Disability.cs, range 12:13-12:13
  3. extract-method on Disability.cs, range 12:17-12:70
    name"IsNotEligible"
Disability.csmodified
11 namespace Staff
22 {
33 public class Disability
44 {
55 private int _seniority;
66 private int _monthsDisabled;
77 private bool _isPartTime;
88 private decimal _baseAmount;
99
1010 public decimal Amount()
1111 {
12− /*^*/if (_seniority < 2)
13− {
14− return 0;
15− }
16−
17− if (_monthsDisabled > 12)
18− {
19− return 0;
20− }
21−
22− if (_isPartTime)
12+ if (IsNotEligible())
2313 {
2414 return 0;
2515 }
2616
2717 return _baseAmount * 0.1m;
2818 }
19+
20+ private bool IsNotEligible()
21+ {
22+ return _seniority < 2 || _monthsDisabled > 12 || _isPartTime;
23+ }
2924 }
3025 }

#recipe-else-if-chain

The recipe for an else if chain: Merge Sibling Ifs joins the branches with the same body, and the chain continues with the else

successrecipe
Recipe
  1. merge-sibling-ifs on Ticket.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)
9− {
10− label = "open";
11− }
12− else if (code == 2)
8+ if (code == 1 || code == 2)
139 {
1410 label = "open";
1511 }
1612 else
1713 {
1814 label = "closed";
1915 }
2016
2117 return label;
2218 }
2319 }
2420 }

#recipe-nested-ifs

The recipe for nested conditions: Merge Nested If, repeated on the same if, joins three conditions with &&

successrecipe
Recipe
  1. merge-nested-if on Shipping.cs, the /*^*/ caret
  2. merge-nested-if on Shipping.cs, range 18:13-18:13
Shipping.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Order
66 {
77 public int Id { get; set; }
88
99 public bool IsPaid { get; set; }
1010
1111 public int Items { get; set; }
1212 }
1313
1414 public class Shipping
1515 {
1616 public void Ship(Order order)
1717 {
18− /*^*/if (order != null)
18+ if (order != null && order.IsPaid && order.Items > 0)
1919 {
20− if (order.IsPaid)
21− {
22− if (order.Items > 0)
23− {
24− Console.WriteLine(order.Id);
25− }
26− }
20+ Console.WriteLine(order.Id);
2721 }
2822 }
2923 }
3024 }

#recipe-with-parameters

The recipe with locals and parameters: Merge Sibling Ifs joins the conditions, and Extract Method passes what they read to a static method

successrecipe
Recipe
  1. merge-sibling-ifs on Tickets.cs, the /*^*/ caret
  2. extract-method on Tickets.cs, range 8:17-8:45
    name"IsExempt"
Tickets.csmodified
11 namespace Venue
22 {
33 public static class Tickets
44 {
55 public static decimal Fee(int age, bool member)
66 {
77 var senior = age >= 65;
8− /*^*/if (senior)
9− {
10− return 0m;
11− }
12−
13− if (member && age < 18)
8+ if (IsExempt(senior, member, age))
149 {
1510 return 0m;
1611 }
1712
1813 return 10m;
1914 }
15+
16+ private static bool IsExempt(bool senior, bool member, int age)
17+ {
18+ return senior || member && age < 18;
19+ }
2020 }
2121 }

#sequential-ifs

Consecutive ifs with the same body, which always returns, become one if on their conditions joined by ||

success
TargetDisability.cs, the /*^*/ caret
Disability.csmodified
11 namespace Staff
22 {
33 public class Disability
44 {
55 private int _seniority;
66 private int _monthsDisabled;
77 private bool _isPartTime;
88 private decimal _baseAmount;
99
1010 public decimal Amount()
1111 {
12− /*^*/if (_seniority < 2)
13− {
14− return 0;
15− }
16−
17− if (_monthsDisabled > 12)
18− {
19− return 0;
20− }
21−
22− if (_isPartTime)
12+ if (_seniority < 2 || _monthsDisabled > 12 || _isPartTime)
2313 {
2414 return 0;
2515 }
2616
2717 return _baseAmount * 0.1m;
2818 }
2919 }
3020 }