RefactorMCP catalog

Replace Nested Conditional with Guard Clauses

Flattens nested conditionals so that each special case is checked by a guard clause that leaves early, and the normal path runs unindented after them.

Recipe

Starting with the outermost if and repeating on the if that each step leaves last, one of these steps, chosen by the shape of the if:

  • It has an else, and its branch always jumps away: Remove Redundant Else, so the else's statements follow it. { "refactoring": "remove-redundant-else", "target": { "file": "Payroll.cs", "caret": "marker" } }
  • It has an else that always jumps away, and its branch does not: Invert If, which swaps the branches, then Remove Redundant Else on the same if. { "refactoring": "invert-if", "target": { "file": "Shipping.cs", "caret": "marker" } }, { "refactoring": "remove-redundant-else", "target": { "file": "Shipping.cs", "range": "9:13-9:13" } }
  • It has no else and ends a method or loop body: Invert If, which turns it into an early return or continue with the old branch after it. { "refactoring": "invert-if", "target": { "file": "Shipping.cs", "range": "22:13-22:13" } }

A later step cannot use a marker, so it gives the caret as a range whose start is the caret, on the if as the previous steps left it.

Target

The outermost if, by caret: "target": { "file": "Shipping.cs", "caret": "marker" }.

Precondition

  • The caret is on an if statement in a block.
  • The if can become a guard clause, in one of these ways:
  • It has an else, and its branch always jumps away (returns, throws, breaks or continues).
  • It has an else that always jumps away, and its branch does not.
  • It has no else, nothing follows it, and its block is the body of a method, function or accessor returning nothing, or of a loop.

Transformation

Starting with the if at the caret:

  • When its branch always jumps away, the else is removed and its statements follow the if: if (a) { return A(); } else { return B(); } becomes if (a) { return A(); } followed by return B();. An else if becomes an if of its own.
  • When only the else always jumps away, the condition is negated, the else becomes the branch, and the old branch follows the if.
  • When it has no else and ends a method or loop body, the condition is negated, the branch becomes return; or continue;, and the old branch follows the if, as Invert If does.

The statements that now follow the guard clause are set apart from it by a blank line. When the last of them is an if, it is flattened the same way, until no nesting that can become a guard clause is left.

Conditions are negated as Invert If negates them: comparisons flip where that is exact, !x becomes x, and && and || follow De Morgan's laws.

Preserved

  • The behaviour of every path through the statement.
  • Comments inside each branch move with that branch; a comment above a nested if stays above it.

Limitations

  • Only the last statement of what a guard clause guarded is flattened further.
  • Branches that assign a result variable returned after the conditional are not turned into returns; the nesting is kept.
  • A value-returning method whose nested if has no else has no implicit exit, so that if is left as it is.

Error codes

CodeMeaning
not-an-ifthe caret is not on an if statement
not-in-blockthe if is not a statement of a block, such as an else if
no-guard-clausethe if has no else after a branch that always jumps away, and does not end a method or loop body

Cases

·

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

#else-after-return

An else after a branch that always returns follows the if instead, repeatedly, so each special case is a guard clause

success
TargetPayroll.cs, the /*^*/ caret
Payroll.csmodified
11 namespace Staff
22 {
33 public class Payroll
44 {
55 private bool _isDead;
66 private bool _isSeparated;
77 private bool _isRetired;
88
99 public decimal PayAmount()
1010 {
11− /*^*/if (_isDead)
11+ if (_isDead)
1212 {
1313 return DeadAmount();
1414 }
15− else
15+
16+ if (_isSeparated)
1617 {
17− if (_isSeparated)
18− {
19− return SeparatedAmount();
20− }
21− else
22− {
23− if (_isRetired)
24− {
25− return RetiredAmount();
26− }
27− else
28− {
29− return NormalPayAmount();
30− }
31− }
18+ return SeparatedAmount();
3219 }
20+
21+ if (_isRetired)
22+ {
23+ return RetiredAmount();
24+ }
25+
26+ return NormalPayAmount();
3327 }
3428
3529 private decimal DeadAmount() => 0m;
3630
3731 private decimal SeparatedAmount() => 100m;
3832
3933 private decimal RetiredAmount() => 200m;
4034
4135 private decimal NormalPayAmount() => 300m;
4236 }
4337 }

#else-if-chain

An else if chain whose branches return becomes a run of separate ifs

success
TargetGrading.cs, the /*^*/ caret
Grading.csmodified
11 namespace School
22 {
33 public class Grading
44 {
55 public string Grade(int score)
66 {
7− /*^*/if (score >= 90)
7+ if (score >= 90)
88 {
99 return "A";
1010 }
11− else if (score >= 70)
11+
12+ if (score >= 70)
1213 {
1314 return "B";
1415 }
15− else
16− {
17− return "C";
18− }
16+
17+ return "C";
1918 }
2019 }
2120 }

#else-if-rejected

An else if is not a statement of a block, so nothing can follow it as a guard clause; start from the first if of the chain

refusal
TargetGrading.cs, the /*^*/ caret
Refusesnot-in-block; every file is left unchanged
Grading.csinput
1 namespace School
2 {
3 public class Grading
4 {
5 public string Grade(int score)
6 {
7 if (score >= 90)
8 {
9 return "A";
10 }
11 else /*^*/if (score >= 70)
12 {
13 return "B";
14 }
15 else
16 {
17 return "C";
18 }
19 }
20 }
21 }

#loop-body

Nested ifs ending a loop body become guard clauses that continue

success
TargetShipping.cs, the /*^*/ caret
Shipping.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop
55 {
66 public class Shipping
77 {
88 public void ShipAll(List<string> items, int stock)
99 {
1010 foreach (var item in items)
1111 {
12− /*^*/if (item != null)
12+ if (item == null)
1313 {
14− if (stock > 0 && item.Length > 0)
15− {
16− Console.WriteLine(item);
17− }
14+ continue;
1815 }
16+
17+ if (stock <= 0 || item.Length <= 0)
18+ {
19+ continue;
20+ }
21+
22+ Console.WriteLine(item);
1923 }
2024 }
2125 }
2226 }

#nested-ifs

Nested ifs ending a void method become a guard clause each, 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
1212 public class Shipping
1313 {
1414 public void Ship(Order order)
1515 {
16− /*^*/if (order != null)
16+ if (order == null)
1717 {
18− // Only paid orders are packed.
19− if (order.IsPaid)
20− {
21− Console.WriteLine("packing");
22− Console.WriteLine(order.Id);
23− }
18+ return;
2419 }
20+
21+ // Only paid orders are packed.
22+ if (!order.IsPaid)
23+ {
24+ return;
25+ }
26+
27+ Console.WriteLine("packing");
28+ Console.WriteLine(order.Id);
2529 }
2630 }
2731 }

#no-guard-clause-rejected

An if with no else followed by code it can fall through to has no guard clause to become

refusal
TargetShipping.cs, the /*^*/ caret
Refusesno-guard-clause; every file is left unchanged
Shipping.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Shipping
6 {
7 public void Ship(string order)
8 {
9 /*^*/if (order != null)
10 {
11 Console.WriteLine(order);
12 }
13
14 Console.WriteLine("done");
15 }
16 }
17 }

#not-an-if-rejected

The caret must be on an if statement

refusal
TargetShipping.cs, the /*^*/ caret
Refusesnot-an-if; every file is left unchanged
Shipping.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class Shipping
6 {
7 public void Ship(string order)
8 {
9 /*^*/Console.WriteLine("checking");
10 if (order != null)
11 {
12 Console.WriteLine(order);
13 }
14 }
15 }
16 }

#recipe

The recipe: Invert If on the outer if and then on the inner one, each becoming an early return

successrecipe
Recipe
  1. invert-if on Shipping.cs, the /*^*/ caret
  2. invert-if on Shipping.cs, range 22:13-22: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
1212 public class Shipping
1313 {
1414 public void Ship(Order order)
1515 {
16− /*^*/if (order != null)
16+ if (order == null)
1717 {
18− // Only paid orders are packed.
19− if (order.IsPaid)
20− {
21− Console.WriteLine("packing");
22− Console.WriteLine(order.Id);
23− }
18+ return;
2419 }
20+
21+ // Only paid orders are packed.
22+ if (!order.IsPaid)
23+ {
24+ return;
25+ }
26+
27+ Console.WriteLine("packing");
28+ Console.WriteLine(order.Id);
2529 }
2630 }
2731 }

#recipe-else-after-return

The recipe for an else after a branch that returns: Remove Redundant Else on the outer if, then on each if it leaves behind

successrecipe
Recipe
  1. remove-redundant-else on Payroll.cs, the /*^*/ caret
  2. remove-redundant-else on Payroll.cs, range 16:13-16:13
  3. remove-redundant-else on Payroll.cs, range 21:13-21:13
Payroll.csmodified
11 namespace Staff
22 {
33 public class Payroll
44 {
55 private bool _isDead;
66 private bool _isSeparated;
77 private bool _isRetired;
88
99 public decimal PayAmount()
1010 {
11− /*^*/if (_isDead)
11+ if (_isDead)
1212 {
1313 return DeadAmount();
1414 }
15− else
15+
16+ if (_isSeparated)
1617 {
17− if (_isSeparated)
18− {
19− return SeparatedAmount();
20− }
21− else
22− {
23− if (_isRetired)
24− {
25− return RetiredAmount();
26− }
27− else
28− {
29− return NormalPayAmount();
30− }
31− }
18+ return SeparatedAmount();
3219 }
20+
21+ if (_isRetired)
22+ {
23+ return RetiredAmount();
24+ }
25+
26+ return NormalPayAmount();
3327 }
3428
3529 private decimal DeadAmount() => 0m;
3630
3731 private decimal SeparatedAmount() => 100m;
3832
3933 private decimal RetiredAmount() => 200m;
4034
4135 private decimal NormalPayAmount() => 300m;
4236 }
4337 }

#recipe-else-if-chain

The recipe for an else if chain: Remove Redundant Else on the first if, which makes the else if an if of its own, then on that if

successrecipe
Recipe
  1. remove-redundant-else on Grading.cs, the /*^*/ caret
  2. remove-redundant-else on Grading.cs, range 12:13-12:13
Grading.csmodified
11 namespace School
22 {
33 public class Grading
44 {
55 public string Grade(int score)
66 {
7− /*^*/if (score >= 90)
7+ if (score >= 90)
88 {
99 return "A";
1010 }
11− else if (score >= 70)
11+
12+ if (score >= 70)
1213 {
1314 return "B";
1415 }
15− else
16− {
17− return "C";
18− }
16+
17+ return "C";
1918 }
2019 }
2120 }

#recipe-reversed-condition

The recipe when only the else jumps away: Invert If swaps the branches, then Remove Redundant Else lets the normal path follow the guard clause

successrecipe
Recipe
  1. invert-if on Shipping.cs, the /*^*/ caret
  2. remove-redundant-else on Shipping.cs, range 9:13-9:13
Shipping.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Shipping
66 {
77 public void Ship(bool paid, int id)
88 {
9− /*^*/if (paid)
10− {
11− Console.WriteLine("packing");
12− }
13− else
9+ if (!paid)
1410 {
1511 throw new InvalidOperationException("The order is not paid");
1612 }
13+
14+ Console.WriteLine("packing");
1715
1816 Console.WriteLine(id);
1917 }
2018 }
2119 }

#reversed-condition

When only the else always jumps away, the condition is negated so the else becomes the guard clause and the normal path follows it

success
TargetShipping.cs, the /*^*/ caret
Shipping.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Shipping
66 {
77 public void Ship(bool paid, int id)
88 {
9− /*^*/if (paid)
10− {
11− Console.WriteLine("packing");
12− }
13− else
9+ if (!paid)
1410 {
1511 throw new InvalidOperationException("The order is not paid");
1612 }
13+
14+ Console.WriteLine("packing");
1715
1816 Console.WriteLine(id);
1917 }
2018 }
2119 }