RefactorMCP catalog

Merge Sibling Ifs

Joins an if with the if after it when both have the same body, into one if on both conditions: if (a) { S } else if (b) { S } and if (a) { return; } if (b) { return; } become if (a || b) { S } and if (a || b) { return; }. The counterpart of Merge Nested If, which joins with &&.

Target

The first if, by caret: "target": { "file": "Disability.cs", "caret": "marker" }. The merged if stays where the first one was, so repeating the step on the same caret joins a longer run one condition at a time.

Precondition

  • The caret is on an if statement, between if and the closing parenthesis of its condition.
  • The if is followed by one of these, with the same body:
  • an else if, when the if has an else;
  • an if statement directly after it in the same block or switch section, when the if has no else. That if may have an else of its own.
  • Bodies are the same when they are the same code; comments and layout do not count.
  • For an if statement after it, the shared body always jumps away (returns, throws, breaks or continues). 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.
  • Neither condition declares a pattern or out variable, which would be unassigned when the other condition holds.

Transformation

  • The conditions are joined with ||, first then second, parenthesised only where precedence requires it: an operand joined by && needs none, a conditional or assignment does.
  • The first body is kept. The second if is removed; its else, if any, becomes the merged if's else, so an else if chain continues after the merged branch.

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, and comments above the second if, which follow them above the merged if. Comments in the first body.

Limitations

  • Only the next if is merged; a longer run takes one step per condition.
  • Comments inside the second body are dropped with it.

Error codes

CodeMeaning
not-an-ifthe caret is not on an if statement
no-sibling-ifthe if is not followed by an else if or if statement with the same body
body-falls-throughthe if statement after it shares a body that can fall through
declares-variablea condition declares a pattern or out variable

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

Refuses consecutive ifs whose shared body can fall through, since it runs twice when both conditions hold

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 cpu, int memory)
8 {
9 /*^*/if (cpu > 90)
10 {
11 Console.WriteLine("alert");
12 }
13
14 if (memory > 90)
15 {
16 Console.WriteLine("alert");
17 }
18 }
19 }
20 }

#comments-kept

The comment above the absorbed if moves above the merged if, and the first body is kept with its comments

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 // Too new to qualify.
12− /*^*/if (_seniority < 2)
12+ // Part-time staff are not covered.
13+ if (_seniority < 2 || _isPartTime)
1314 {
1415 return 0; // nothing
15− }
16−
17− // Part-time staff are not covered.
18− if (_isPartTime)
19− {
20− return 0;
2116 }
2217
2318 return _baseAmount * 0.1m;
2419 }
2520 }
2621 }

#consecutive-ifs

An if followed by an if with the same body, which always returns, becomes one if on both 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 decimal _baseAmount;
88
99 public decimal Amount()
1010 {
11− /*^*/if (_seniority < 2)
12− {
13− return 0;
14− }
15−
16− if (_monthsDisabled > 12)
11+ if (_seniority < 2 || _monthsDisabled > 12)
1712 {
1813 return 0;
1914 }
2015
2116 return _baseAmount * 0.1m;
2217 }
2318 }
2419 }

#declares-variable-rejected

Refuses conditions that declare a pattern variable, which would be unassigned when the other condition holds

refusal
TargetShapes.cs, the /*^*/ caret
Refusesdeclares-variable; every file is left unchanged
Shapes.csinput
1 namespace Geometry
2 {
3 public class Shapes
4 {
5 public int Sides(object shape)
6 {
7 /*^*/if (shape is string name && name.Length == 0)
8 {
9 return 0;
10 }
11
12 if (shape is null)
13 {
14 return 0;
15 }
16
17 return 1;
18 }
19 }
20 }

#else-if

An else if with the same body joins the if with ||, and the chain continues with its else

success
TargetTicket.cs, the /*^*/ caret
Ticket.csmodified
11 using System;
22
33 namespace Support
44 {
55 public class Ticket
66 {
77 public void Route(int priority, bool vip)
88 {
9− /*^*/if (priority > 3)
10− {
11− Console.WriteLine("urgent");
12− }
13− else if (vip)
9+ if (priority > 3 || vip)
1410 {
1511 Console.WriteLine("urgent");
1612 }
1713 else
1814 {
1915 Console.WriteLine("queued");
2016 }
2117 }
2218 }
2319 }

#following-if-with-else

The if after it may have an else, which the merged if keeps: it only ran when neither condition held

success
TargetBatch.cs, the /*^*/ caret
Batch.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Shop
55 {
66 public class Batch
77 {
88 public void Process(List<int> quantities)
99 {
1010 foreach (var quantity in quantities)
1111 {
12− /*^*/if (quantity == 0)
13− {
14− continue;
15− }
16− if (quantity > 100)
12+ if (quantity == 0 || quantity > 100)
1713 {
1814 continue;
1915 }
2016 else
2117 {
2218 Console.WriteLine(quantity);
2319 }
2420 }
2521 }
2622 }
2723 }

#no-sibling-if-rejected

Refuses an if whose next if has a different body

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

#not-an-if-rejected

Refuses a caret that is not on an if statement

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-an-if; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public int Twice(int value)
6 {
7 /*^*/return value * 2;
8 }
9 }
10 }

#nullable-context

With nullable annotations, a null test short-circuits the condition after it as the separate ifs did

success
TargetNames.cs, the /*^*/ caret
Projectnullable enable
Names.csmodified
11 namespace People
22 {
33 public class Names
44 {
55 public string Initial(string? name)
66 {
7− /*^*/if (name is null)
8− {
9− return "?";
10− }
11−
12− if (name.Length == 0)
7+ if (name is null || name.Length == 0)
138 {
149 return "?";
1510 }
1611
1712 return name.Substring(0, 1);
1813 }
1914 }
2015 }

#precedence

A condition that binds more loosely than || is parenthesised, and one joined by && is not

success
TargetAccess.cs, the /*^*/ caret
Access.csmodified
11 namespace Security
22 {
33 public class Access
44 {
55 public bool Allowed(bool admin, bool owner, bool locked, bool strict)
66 {
7− /*^*/if (locked && !admin)
8− return false;
9− if (strict ? !owner : !owner && !admin)
7+ if (locked && !admin || (strict ? !owner : !owner && !admin))
108 return false;
119 return true;
1210 }
1311 }
1412 }