RefactorMCP catalog

Introduce Constant

Replaces a selected literal or constant expression with a named constant of the containing type, and optionally every other occurrence of the same value in that type. The catalog's answer to a magic number.

Precondition

  • The selection is exactly one expression whose value is known at compile time, other than null.
  • The expression does not use a local constant, which a member of the type could not see.
  • No member of the containing type already has the name.

Transformation

  • A private const of the expression's type is declared with the expression as its value, with redundant outer parentheses removed. It is placed after the type's existing fields and constants, or first in the type, followed by a blank line, when it has none.
  • The selected expression is replaced by the constant's name.
  • With "replaceAll": true, every expression in the containing type that is written the same way and has the same type is replaced too. 60 matches 60 but not 60.0, 600 or the text inside "60 per minute".

Preserved

  • Every value the code computes, since the constant has exactly the value of the expression it replaces.
  • Comments around the replaced expression.

Limitations

  • Occurrences are matched by how they are written, so 60 does not match 0x3C or 30 * 2.
  • Only the type declaration containing the selection is searched; other parts of a partial type are not.

Error codes

CodeMeaning
not-an-expressionthe selection is not exactly one expression
not-constantthe expression's value is not known at compile time
references-localthe expression uses a local constant
name-conflictthe type already has a member with the name

Cases

·

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

#constant-expression

A constant expression made of several literals becomes one constant, and the comments around it stay put

success
TargetSchedule.cs, the /*[*/ … /*]*/ selection
Arguments
name"MinutesPerDay"
Schedule.csmodified
11 namespace Shop
22 {
33 public static class Schedule
44 {
5+ private const int MinutesPerDay = 24 * 60;
6+
57 // Days are counted from midnight.
68 public static int MinutesUntil(int day)
79 {
810 // Whole days first.
9− return day * /*[*/(24 * 60)/*]*/; // no daylight saving
11+ return day * MinutesPerDay; // no daylight saving
1012 }
1113 }
1214 }

#literal

A selected literal becomes a private constant of the containing type

success
TargetSession.cs, the /*[*/ … /*]*/ selection
Arguments
name"SecondsPerMinute"
Session.csmodified
11 namespace Shop
22 {
33 public class Session
44 {
5+ private const int SecondsPerMinute = 60;
6+
57 public int TimeoutSeconds(int minutes)
68 {
7− return minutes * /*[*/60/*]*/;
9+ return minutes * SecondsPerMinute;
810 }
911 }
1012 }

#name-conflict-rejected

Refuses a name another member of the type already has

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Limit"
Refusesname-conflict mentioning “'Limit'”; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public int Limit() => 5;
6
7 public bool IsLarge(int x)
8 {
9 return x > /*[*/100/*]*/;
10 }
11 }
12 }

#not-constant-rejected

Refuses an expression whose value is not known at compile time

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Doubled"
Refusesnot-constant; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public int Run(int x)
6 {
7 return /*[*/x * 2/*]*/;
8 }
9 }
10 }

#references-local-rejected

Refuses a constant expression that uses a local constant, which the new constant could not see

refusal
TargetSample.cs, the /*[*/ … /*]*/ selection
Arguments
name"Limit"
Refusesreferences-local; every file is left unchanged
Sample.csinput
1 namespace Shop
2 {
3 public class Sample
4 {
5 public bool IsLarge(int x)
6 {
7 const int step = 10;
8 return x > /*[*/step * 3/*]*/;
9 }
10 }
11 }

#replace-all-occurrences

With replaceAll, every occurrence of the same value of the same type in the type is replaced, and different values are left alone

success
TargetClock.cs, the /*[*/ … /*]*/ selection
Arguments
name"SecondsPerMinute"
replaceAlltrue
Clock.csmodified
11 namespace Shop
22 {
33 public class Clock
44 {
55 private int _offset;
6+ private const int SecondsPerMinute = 60;
67
78 public int ToSeconds(int minutes)
89 {
9− return minutes * /*[*/60/*]*/ + _offset;
10+ return minutes * SecondsPerMinute + _offset;
1011 }
1112
1213 public double ToMinutes(int seconds)
1314 {
14− return seconds / 60 + seconds % 60 / 60.0;
15+ return seconds / SecondsPerMinute + seconds % SecondsPerMinute / 60.0;
1516 }
1617
1718 public string Label() => "60 per minute, 600 per ten";
1819 }
1920 }

#string-in-generic-type

A string literal in a generic type becomes a string constant of that type

success
TargetRepository.cs, the /*[*/ … /*]*/ selection
Arguments
name"Separator"
Repository.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Repository<T>
66 {
77 private readonly List<T> _items = new List<T>();
8+ private const string Separator = ", ";
89
910 public string Describe()
1011 {
11− return string.Join(/*[*/", "/*]*/, _items);
12+ return string.Join(Separator, _items);
1213 }
1314 }
1415 }