RefactorMCP catalog

Feature Flag Wrapping

Replaces an if on a feature flag with strategies: each branch moves into the Apply method of its own class, and a property named after the flag checks it and returns the strategy to apply. The flag then decides between two objects in one place instead of guarding code inline, and the old path can later be deleted by deleting a class.

This is a generator: it adds an interface, two classes and a property. The after/ fixtures pin one design, described below, rather than the only correct answer.

Arguments

ArgumentRequiredMeaning
flagyesThe flag's name, as passed to IsEnabled, such as NewCheckout

The target is the file holding the check: "target": { "file": "Checkout.cs" }.

Precondition

  • The file has exactly one if whose condition is x.IsEnabled("Flag"), with the flag as a string literal.
  • x is a field, property or type the class can read, such as a _flags field or a static Features class, not a local or parameter.
  • The branches do not leave the method (return, break, continue, goto, yield), do not use members of the class or its base classes, nor local functions, and do not assign a local or parameter declared outside them.
  • No type named I<Flag>Strategy, <Flag>Strategy or No<Flag>Strategy is in scope, and the class has no member named <Flag>.
  • The result compiles.

Transformation

  • internal interface I<Flag>Strategy declares void Apply(...).
  • internal sealed class <Flag>Strategy implements it with the if branch; internal sealed class No<Flag>Strategy with the else branch, or an empty body when there is none. An else if chain moves whole into the disabled strategy.
  • The three types follow the outermost type declaring the check, in the same namespace, in that order.
  • Apply takes the parameters and locals the branches read, in the order the method declares them, typed with the nullability they have at the check: a string? known not to be null there is passed as string.
  • The class gains private I<Flag>Strategy <Flag> => x.IsEnabled("Flag") ? new <Flag>Strategy() : new No<Flag>Strategy();, static when the method holding the check is, placed after that method.
  • The if becomes <Flag>.Apply(...);, keeping the comments above it. A comment trailing a branch moves with the branch.

Preserved

  • When the flag is checked: each call still checks it once, at the same point, so turning the flag on or off takes effect as before.
  • What each branch does.

Behaviour changes

  • Each call allocates a strategy object.

Limitations

  • Negated checks (!x.IsEnabled(...)), flags named by a constant, and checks combined with other conditions are not recognised.
  • Branches that use the class's members are refused rather than given access to them through a parameter.
  • Branches that await would need an asynchronous Apply and are refused as not compiling.

Error codes

CodeMeaning
flag-not-foundthe file has no if checking the flag
ambiguous-flag-checkthe flag is checked by more than one if in the file
flag-source-not-memberthe flags are read from a local or parameter
branch-leaves-earlya branch returns, breaks, continues, jumps or yields out
branch-uses-membersa branch uses a member of the class or a local function
branch-assigns-outer-variablea branch assigns a variable declared outside it
type-already-existsa strategy type's name is taken
member-existsthe class already has a member named after the flag

Cases

·

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

#branch-assigns-outer-variable

Refuses a branch that assigns a local declared outside it, which Apply would change only in its own copy

refusal
TargetCheckout.cs
Arguments
flag"Discounts"
Refusesbranch-assigns-outer-variable mentioning “total”; every file is left unchanged
Checkout.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface IFeatureFlags
6 {
7 bool IsEnabled(string flag);
8 }
9
10 public class Checkout
11 {
12 private readonly IFeatureFlags _flags;
13
14 public Checkout(IFeatureFlags flags)
15 {
16 _flags = flags;
17 }
18
19 public decimal Total(decimal price)
20 {
21 var total = price;
22 if (_flags.IsEnabled("Discounts"))
23 {
24 total = total * 0.9m;
25 }
26
27 return total;
28 }
29 }
30 }

#branch-leaves-early

Refuses a branch that returns from the method, which Apply cannot do on its caller's behalf

refusal
TargetCheckout.cs
Arguments
flag"NewCheckout"
Refusesbranch-leaves-early; every file is left unchanged
Checkout.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface IFeatureFlags
6 {
7 bool IsEnabled(string flag);
8 }
9
10 public class Checkout
11 {
12 private readonly IFeatureFlags _flags;
13
14 public Checkout(IFeatureFlags flags)
15 {
16 _flags = flags;
17 }
18
19 public void Pay()
20 {
21 if (_flags.IsEnabled("NewCheckout"))
22 {
23 Console.WriteLine("New checkout");
24 return;
25 }
26
27 Console.WriteLine("Old checkout");
28 }
29 }
30 }

#branch-uses-members

Refuses a branch that uses members of the class, which a strategy class cannot reach

refusal
TargetCheckout.cs
Arguments
flag"NewCheckout"
Refusesbranch-uses-members mentioning “_payments”; every file is left unchanged
Checkout.csinput
1 using System;
2 using System.Collections.Generic;
3
4 namespace Shop
5 {
6 public interface IFeatureFlags
7 {
8 bool IsEnabled(string flag);
9 }
10
11 public class Checkout
12 {
13 private readonly IFeatureFlags _flags;
14 private readonly List<decimal> _payments = new List<decimal>();
15
16 public Checkout(IFeatureFlags flags)
17 {
18 _flags = flags;
19 }
20
21 public void Pay(decimal amount)
22 {
23 if (_flags.IsEnabled("NewCheckout"))
24 {
25 _payments.Add(amount);
26 }
27 }
28 }
29 }

#checked-twice

Refuses a flag checked in more than one place, whose branches would need separate strategies

refusal
TargetCheckout.cs
Arguments
flag"NewCheckout"
Refusesambiguous-flag-check; every file is left unchanged
Checkout.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface IFeatureFlags
6 {
7 bool IsEnabled(string flag);
8 }
9
10 public class Checkout
11 {
12 private readonly IFeatureFlags _flags;
13
14 public Checkout(IFeatureFlags flags)
15 {
16 _flags = flags;
17 }
18
19 public void Pay()
20 {
21 if (_flags.IsEnabled("NewCheckout"))
22 {
23 Console.WriteLine("New checkout");
24 }
25 }
26
27 public void Refund()
28 {
29 if (_flags.IsEnabled("NewCheckout"))
30 {
31 Console.WriteLine("New refund");
32 }
33 }
34 }
35 }

#flag-not-found

Refuses a flag the file never checks

refusal
TargetCheckout.cs
Arguments
flag"Missing"
Refusesflag-not-found; every file is left unchanged
Checkout.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface IFeatureFlags
6 {
7 bool IsEnabled(string flag);
8 }
9
10 public class Checkout
11 {
12 private readonly IFeatureFlags _flags;
13
14 public Checkout(IFeatureFlags flags)
15 {
16 _flags = flags;
17 }
18
19 public void Pay()
20 {
21 if (_flags.IsEnabled("NewCheckout"))
22 {
23 Console.WriteLine("New checkout");
24 }
25 }
26 }
27 }

#flag-source-not-member

Refuses a check on flags passed as a parameter, which a property of the class cannot read

refusal
TargetCheckout.cs
Arguments
flag"NewCheckout"
Refusesflag-source-not-member; every file is left unchanged
Checkout.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface IFeatureFlags
6 {
7 bool IsEnabled(string flag);
8 }
9
10 public class Checkout
11 {
12 public void Pay(IFeatureFlags flags)
13 {
14 if (flags.IsEnabled("NewCheckout"))
15 {
16 Console.WriteLine("New checkout");
17 }
18 }
19 }
20 }

#if-else

Moves each branch of a flag check into a strategy class and selects the strategy through a private property that checks the flag

success
TargetCheckout.cs
Arguments
flag"NewCheckout"
Checkout.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public interface IFeatureFlags
66 {
77 bool IsEnabled(string flag);
88 }
99
1010 public class Checkout
1111 {
1212 private readonly IFeatureFlags _flags;
1313
1414 public Checkout(IFeatureFlags flags)
1515 {
1616 _flags = flags;
1717 }
1818
1919 public void Pay()
2020 {
2121 Console.WriteLine("Paying");
22− if (_flags.IsEnabled("NewCheckout"))
23− {
24− Console.WriteLine("New checkout");
25− }
26− else
27− {
28− Console.WriteLine("Old checkout");
29− }
22+ NewCheckout.Apply();
23+ }
24+
25+ private INewCheckoutStrategy NewCheckout => _flags.IsEnabled("NewCheckout") ? new NewCheckoutStrategy() : new NoNewCheckoutStrategy();
26+ }
27+
28+ internal interface INewCheckoutStrategy
29+ {
30+ void Apply();
31+ }
32+
33+ internal sealed class NewCheckoutStrategy : INewCheckoutStrategy
34+ {
35+ public void Apply()
36+ {
37+ Console.WriteLine("New checkout");
38+ }
39+ }
40+
41+ internal sealed class NoNewCheckoutStrategy : INewCheckoutStrategy
42+ {
43+ public void Apply()
44+ {
45+ Console.WriteLine("Old checkout");
3046 }
3147 }
3248 }

#member-exists

Refuses when the class already has a member named after the flag, which the selecting property needs

refusal
TargetCheckout.cs
Arguments
flag"NewCheckout"
Refusesmember-exists; every file is left unchanged
Checkout.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface IFeatureFlags
6 {
7 bool IsEnabled(string flag);
8 }
9
10 public class Checkout
11 {
12 private readonly IFeatureFlags _flags;
13
14 public Checkout(IFeatureFlags flags)
15 {
16 _flags = flags;
17 }
18
19 public bool NewCheckout => _flags.IsEnabled("NewCheckout");
20
21 public void Pay()
22 {
23 if (_flags.IsEnabled("NewCheckout"))
24 {
25 Console.WriteLine("New checkout");
26 }
27 }
28 }
29 }

#nested-class

Rewrites only the nested class holding the flag check; the class around it keeps its members and constructors

success
TargetCheckout.cs
Arguments
flag"NewCheckout"
Checkout.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public interface IFeatureFlags
66 {
77 bool IsEnabled(string flag);
88 }
99
1010 public class Checkout
1111 {
1212 public void Start()
1313 {
1414 Console.WriteLine("Starting");
1515 }
1616
1717 public class Payment
1818 {
1919 private readonly IFeatureFlags _flags;
2020
2121 public Payment(IFeatureFlags flags)
2222 {
2323 _flags = flags;
2424 }
2525
2626 public void Pay()
2727 {
28− if (_flags.IsEnabled("NewCheckout"))
29− {
30− Console.WriteLine("New checkout");
31− }
32− else
33− {
34− Console.WriteLine("Old checkout");
35− }
28+ NewCheckout.Apply();
3629 }
30+
31+ private INewCheckoutStrategy NewCheckout => _flags.IsEnabled("NewCheckout") ? new NewCheckoutStrategy() : new NoNewCheckoutStrategy();
32+ }
33+ }
34+
35+ internal interface INewCheckoutStrategy
36+ {
37+ void Apply();
38+ }
39+
40+ internal sealed class NewCheckoutStrategy : INewCheckoutStrategy
41+ {
42+ public void Apply()
43+ {
44+ Console.WriteLine("New checkout");
45+ }
46+ }
47+
48+ internal sealed class NoNewCheckoutStrategy : INewCheckoutStrategy
49+ {
50+ public void Apply()
51+ {
52+ Console.WriteLine("Old checkout");
3753 }
3854 }
3955 }

#nullable-flow

In a nullable context a parameter the branch reads is passed with the nullability it has there

success
TargetGreeter.cs
Arguments
flag"Nicknames"
Projectnullable enable
Greeter.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public interface IFeatureFlags
66 {
77 bool IsEnabled(string flag);
88 }
99
1010 public class Greeter
1111 {
1212 private readonly IFeatureFlags _flags;
1313
1414 public Greeter(IFeatureFlags flags)
1515 {
1616 _flags = flags;
1717 }
1818
1919 public void Greet(string? nickname, string name)
2020 {
2121 if (nickname is null)
2222 return;
2323
24− if (_flags.IsEnabled("Nicknames"))
25− {
26− Console.WriteLine("Hello " + nickname.ToUpperInvariant());
27− }
28− else
29− {
30− Console.WriteLine("Hello " + name);
31− }
24+ Nicknames.Apply(nickname, name);
25+ }
26+
27+ private INicknamesStrategy Nicknames => _flags.IsEnabled("Nicknames") ? new NicknamesStrategy() : new NoNicknamesStrategy();
28+ }
29+
30+ internal interface INicknamesStrategy
31+ {
32+ void Apply(string nickname, string name);
33+ }
34+
35+ internal sealed class NicknamesStrategy : INicknamesStrategy
36+ {
37+ public void Apply(string nickname, string name)
38+ {
39+ Console.WriteLine("Hello " + nickname.ToUpperInvariant());
40+ }
41+ }
42+
43+ internal sealed class NoNicknamesStrategy : INicknamesStrategy
44+ {
45+ public void Apply(string nickname, string name)
46+ {
47+ Console.WriteLine("Hello " + name);
3248 }
3349 }
3450 }

#static-flags-file-scoped

A check in a static method on a static flag source gives a static property, in a file-scoped namespace

success
TargetBanner.cs
Arguments
flag"Sale"
Banner.csmodified
11 using System;
22
33 namespace Shop;
44
55 public static class Features
66 {
77 public static bool IsEnabled(string flag) => flag == "Sale";
88 }
99
1010 public static class Banner
1111 {
1212 public static void Show()
1313 {
14− if (Features.IsEnabled("Sale"))
15− {
16− Console.WriteLine("Sale on now");
17− }
18− else
19− {
20− Console.WriteLine("Welcome");
21− }
14+ Sale.Apply();
15+ }
16+
17+ private static ISaleStrategy Sale => Features.IsEnabled("Sale") ? new SaleStrategy() : new NoSaleStrategy();
18+}
19+
20+internal interface ISaleStrategy
21+{
22+ void Apply();
23+}
24+
25+internal sealed class SaleStrategy : ISaleStrategy
26+{
27+ public void Apply()
28+ {
29+ Console.WriteLine("Sale on now");
2230 }
2331 }
32+
33+internal sealed class NoSaleStrategy : ISaleStrategy
34+{
35+ public void Apply()
36+ {
37+ Console.WriteLine("Welcome");
38+ }
39+}

#type-already-exists

Refuses when a strategy type's name is already taken

refusal
TargetCheckout.cs
Arguments
flag"NewCheckout"
Refusestype-already-exists mentioning “NewCheckoutStrategy”; every file is left unchanged
Checkout.csinput
1 using System;
2
3 namespace Shop
4 {
5 public interface IFeatureFlags
6 {
7 bool IsEnabled(string flag);
8 }
9
10 public class NewCheckoutStrategy
11 {
12 }
13
14 public class Checkout
15 {
16 private readonly IFeatureFlags _flags;
17
18 public Checkout(IFeatureFlags flags)
19 {
20 _flags = flags;
21 }
22
23 public void Pay()
24 {
25 if (_flags.IsEnabled("NewCheckout"))
26 {
27 Console.WriteLine("New checkout");
28 }
29 }
30 }
31 }

#variables-and-comments

Passes the parameters and locals a branch reads to Apply, keeps comments inside and above the check, and gives the disabled strategy an empty body when there is no else

success
TargetMailer.cs
Arguments
flag"Receipts"
Mailer.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public interface IFeatureFlags
66 {
77 bool IsEnabled(string flag);
88 }
99
1010 public class Mailer
1111 {
1212 private readonly IFeatureFlags _flags;
1313
1414 public Mailer(IFeatureFlags flags)
1515 {
1616 _flags = flags;
1717 }
1818
1919 public void Send(string to, decimal total)
2020 {
2121 var subject = "Order for " + to;
2222
2323 // Receipts are being trialled
24− if (_flags.IsEnabled("Receipts"))
25− Console.WriteLine(subject + ": " + total); // print the receipt
24+ Receipts.Apply(total, subject);
25+ }
26+
27+ private IReceiptsStrategy Receipts => _flags.IsEnabled("Receipts") ? new ReceiptsStrategy() : new NoReceiptsStrategy();
28+ }
29+
30+ internal interface IReceiptsStrategy
31+ {
32+ void Apply(decimal total, string subject);
33+ }
34+
35+ internal sealed class ReceiptsStrategy : IReceiptsStrategy
36+ {
37+ public void Apply(decimal total, string subject)
38+ {
39+ Console.WriteLine(subject + ": " + total); // print the receipt
40+ }
41+ }
42+
43+ internal sealed class NoReceiptsStrategy : IReceiptsStrategy
44+ {
45+ public void Apply(decimal total, string subject)
46+ {
2647 }
2748 }
2849 }