RefactorMCP catalog

Replace Array with Object

Replaces an array whose elements mean different things, such as a name at index 0 and a score at index 1, with a new class that has a named property for each index.

This is a generator: it adds a type, so the fixtures pin one chosen design rather than the only correct answer.

Arguments

ArgumentRequiredMeaning
nameyesThe new class's name
membersyesThe property names, one per index in order, as an array

The target is a field, by symbol ("symbol": "F:Shop.Result.Score"), or a field or local, by caret on its name in its declaration or a use.

Precondition

  • The variable is a single-dimensional array whose element type does not use a type parameter.
  • The class name and every member name are valid identifiers, and the member names are distinct.
  • The variable's namespace has no type with the class's name, and its folder no file with that name.
  • Every use of the variable is an element access with a literal index, or an assignment of a new array.
  • Every literal index has a member, and every array created for the variable has as many elements as there are members.
  • The variable is declared on its own, not alongside other variables.
  • The result compiles.

Transformation

  • The class is a public class in a new file named after it, beside the file declaring the variable, in that file's namespace and namespace style.
  • It has one public auto-property with get and set per member, in index order, typed as the array's element type, with no blank lines between them. When nullable analysis is enabled and the element type is a non-nullable reference type, each property is initialised to null!, since an array's elements start null too.
  • The variable's declared type becomes the class; var stays var.
  • new T[2] becomes new C().
  • new T[] { a, b }, new[] { a, b }, { a, b } and [a, b] become new C { First = a, Second = b }, keeping the initialiser's line breaks and comments.
  • x[1] becomes x.Second.
  • The new file imports what the element type needs.

Behaviour changes

  • The value is a reference to an object rather than an array: code that copied the array reference still shares it, as before, but nothing can index it or ask for its length.

Preserved

  • Every value stored and read, now through the named properties.
  • Comments and layout of the initialisers and element accesses.

Limitations

  • An index must be an integer literal; a constant from elsewhere or a variable is refused.
  • Uses as an array (Length, foreach, passing it to a method, returning it) are refused rather than adapted.
  • Every property has the array's element type; narrowing them is left to Change Type.
  • A parameter or property holding the array cannot be the target.

Error codes

CodeMeaning
not-an-arraythe variable is not a single-dimensional array field or local
generic-element-typethe element type uses a type parameter
invalid-namethe class or a member name is not a valid identifier, or the names repeat
type-already-existsthe class's name is already taken
unsupported-usethe variable is used as an array, or assigned something other than a new array
index-out-of-rangea literal index has no member
wrong-member-countan array created for the variable has a different number of elements than there are members
breaks-compilationthe result would not compile

Cases

·

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

#collection-expression

Replaces collection expressions and an explicitly sized creation with an initialiser, in a file-scoped namespace

success
Targetsymbol F:Shop.Order._range
Arguments
name"PriceRange"
members["Low", "High"]
Order.csmodified
11 namespace Shop;
22
33 public class Order
44 {
5− private decimal[] _range = [0m, 100m];
5+ private PriceRange _range = new PriceRange { Low = 0m, High = 100m };
66
77 public void Widen()
88 {
9− _range = new decimal[2] { _range[0] - 10m, _range[1] + 10m };
9+ _range = new PriceRange { Low = _range.Low - 10m, High = _range.High + 10m };
1010 }
1111
12− public void Reset() => _range = [0m, 0m];
12+ public void Reset() => _range = new PriceRange { Low = 0m, High = 0m };
1313
14− public bool Contains(decimal price) => price >= _range[0] && price <= _range[1];
14+ public bool Contains(decimal price) => price >= _range.Low && price <= _range.High;
1515 }
PriceRange.csadded
1+namespace Shop;
2+
3+public class PriceRange
4+{
5+ public decimal Low { get; set; }
6+ public decimal High { get; set; }
7+}

#comments-in-initialiser

Keeps the layout and comments of a multi-line initialiser and of the element uses

success
TargetFixture.cs, the /*^*/ caret
Arguments
name"Teams"
members["Home", "Away"]
Fixture.csmodified
11 namespace Shop
22 {
33 public class Fixture
44 {
55 public string Title()
66 {
7− var /*^*/teams = new[]
7+ var teams = new Teams
88 {
9− "Arsenal", // at home
10− "Chelsea" // visiting
9+ Home = "Arsenal", // at home
10+ Away = "Chelsea" // visiting
1111 };
1212
1313 // Home side first.
14− return teams[0] /* home */ + " v " + teams[1];
14+ return teams.Home /* home */ + " v " + teams.Away;
1515 }
1616 }
1717 }
Teams.csadded
1+namespace Shop
2+{
3+ public class Teams
4+ {
5+ public string Home { get; set; }
6+ public string Away { get; set; }
7+ }
8+}

#element-passed-by-reference

Refuses when the result would not compile, here because an element is passed as an out argument, which a property cannot be

refusal
TargetParser.cs, the /*^*/ caret
Arguments
name"Dimensions"
members["Width", "Height"]
Refusesbreaks-compilation mentioning “Parser.cs”; every file is left unchanged
Parser.csinput
1 namespace Shop
2 {
3 public class Parser
4 {
5 public int Area(string width, string height)
6 {
7 var /*^*/size = new int[2];
8 int.TryParse(width, out size[0]);
9 int.TryParse(height, out size[1]);
10 return size[0] * size[1];
11 }
12 }
13 }

#element-type-needs-using

Imports the element type's namespace into the new class's file

success
Targetsymbol F:Shop.Booking._stay
Arguments
name"Stay"
members["Arrival", "Departure"]
Booking.csmodified
11 using System;
22
33 namespace Shop
44 {
55 public class Booking
66 {
7− private readonly DateTime[] _stay;
7+ private readonly Stay _stay;
88
99 public Booking(DateTime arrival, int nights)
1010 {
11− _stay = new DateTime[] { arrival, arrival.AddDays(nights) };
11+ _stay = new Stay { Arrival = arrival, Departure = arrival.AddDays(nights) };
1212 }
1313
14− public TimeSpan Length => _stay[1] - _stay[0];
14+ public TimeSpan Length => _stay.Departure - _stay.Arrival;
1515 }
1616 }
Stay.csadded
1+using System;
2+
3+namespace Shop
4+{
5+ public class Stay
6+ {
7+ public DateTime Arrival { get; set; }
8+ public DateTime Departure { get; set; }
9+ }
10+}

#field-used-in-another-file

Replaces a public array field, its initialiser and its uses in another namespace, including an assigned implicitly typed array

success
Targetsymbol F:Shop.Result.Score
Arguments
name"FinalScore"
members["Home", "Away"]
FinalScore.csadded
1+namespace Shop
2+{
3+ public class FinalScore
4+ {
5+ public int Home { get; set; }
6+ public int Away { get; set; }
7+ }
8+}
Reports/Scoreline.csmodified
11 namespace Shop.Reports
22 {
33 public class Scoreline
44 {
55 public string Describe(Result result)
66 {
7− return result.Score[0] + "-" + result.Score[1];
7+ return result.Score.Home + "-" + result.Score.Away;
88 }
99
1010 public void Reset(Result result)
1111 {
12− result.Score = new[] { 0, 0 };
12+ result.Score = new FinalScore { Home = 0, Away = 0 };
1313 }
1414 }
1515 }
Result.csmodified
11 namespace Shop
22 {
33 public class Result
44 {
5− public int[] Score = { 2, 1 };
5+ public FinalScore Score = new FinalScore { Home = 2, Away = 1 };
66 }
77 }

#generic-element-type

Refuses an array whose element type uses a type parameter, which the new class would have to declare

refusal
Targetsymbol F:Shop.Pair`1._items
Arguments
name"Items"
members["Left", "Right"]
Refusesgeneric-element-type mentioning “T”; every file is left unchanged
Pair.csinput
1 namespace Shop
2 {
3 public class Pair<T>
4 {
5 private readonly T[] _items = new T[2];
6
7 public T Left => _items[0];
8
9 public T Right => _items[1];
10 }
11 }

#index-out-of-range

Refuses a constant index with no member to name it

refusal
TargetReport.cs, the /*^*/ caret
Arguments
name"Performance"
members["Club", "Wins"]
Refusesindex-out-of-range mentioning “row[2]”; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Report
4 {
5 public string Describe()
6 {
7 var /*^*/row = new string[3];
8 row[0] = "Liverpool";
9 row[2] = "15";
10 return row[0];
11 }
12 }
13 }

#invalid-name

Refuses a member name that is not a valid identifier

refusal
TargetReport.cs, the /*^*/ caret
Arguments
name"Performance"
members["Club", "2nd"]
Refusesinvalid-name mentioning “2nd”; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Report
4 {
5 public string Describe()
6 {
7 string[] /*^*/row = { "Liverpool", "15" };
8 return row[0] + row[1];
9 }
10 }
11 }

#local-array

Replaces a local array whose elements mean different things with a class that names them

success
TargetReport.cs, the /*^*/ caret
Arguments
name"Performance"
members["Club", "Wins"]
Performance.csadded
1+namespace Shop
2+{
3+ public class Performance
4+ {
5+ public string Club { get; set; }
6+ public string Wins { get; set; }
7+ }
8+}
Report.csmodified
11 namespace Shop
22 {
33 public class Report
44 {
55 public string Describe()
66 {
7− string[] /*^*/row = new string[2];
8− row[0] = "Liverpool";
9− row[1] = "15";
10− return row[0] + ": " + row[1];
7+ Performance row = new Performance();
8+ row.Club = "Liverpool";
9+ row.Wins = "15";
10+ return row.Club + ": " + row.Wins;
1111 }
1212 }
1313 }

#not-an-array

Refuses a field that is not a single-dimensional array

refusal
Targetsymbol F:Shop.Grid._cells
Arguments
name"Cells"
members["First", "Second"]
Refusesnot-an-array mentioning “_cells”; every file is left unchanged
Grid.csinput
1 namespace Shop
2 {
3 public class Grid
4 {
5 private readonly int[,] _cells = new int[2, 2];
6
7 public int Corner() => _cells[0, 0];
8 }
9 }

#nullable-annotations

With nullable analysis enabled, marks a member of a non-nullable reference type as starting null, as an array element does

success
Targetsymbol F:Shop.Contact._name
Arguments
name"PersonName"
members["First", "Last"]
Projectnullable enable
Contact.csmodified
11 namespace Shop
22 {
33 public class Contact
44 {
5− private readonly string[] _name = new string[2];
5+ private readonly PersonName _name = new PersonName();
66
77 public Contact(string first, string last)
88 {
9− _name[0] = first;
10− _name[1] = last;
9+ _name.First = first;
10+ _name.Last = last;
1111 }
1212
13− public string Greeting() => "Dear " + _name[0] + " " + _name[1];
13+ public string Greeting() => "Dear " + _name.First + " " + _name.Last;
1414 }
1515 }
PersonName.csadded
1+namespace Shop
2+{
3+ public class PersonName
4+ {
5+ public string First { get; set; } = null!;
6+ public string Last { get; set; } = null!;
7+ }
8+}

#type-already-exists

Refuses a class name the namespace already uses

refusal
TargetReport.cs, the /*^*/ caret
Arguments
name"Performance"
members["Club", "Wins"]
Refusestype-already-exists mentioning “Performance”; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Performance
4 {
5 }
6
7 public class Report
8 {
9 public string Describe()
10 {
11 string[] /*^*/row = { "Liverpool", "15" };
12 return row[0] + row[1];
13 }
14 }
15 }

#unsupported-use

Refuses when the array is used as an array, here by reading its Length

refusal
TargetReport.cs, the /*^*/ caret
Arguments
name"Performance"
members["Club", "Wins"]
Refusesunsupported-use mentioning “Report.cs(9”; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Report
4 {
5 public string Describe()
6 {
7 string[] /*^*/row = { "Liverpool", "15" };
8 var text = row[0];
9 return text + row.Length;
10 }
11 }
12 }

#wrong-member-count

Refuses an array created with more elements than there are member names

refusal
TargetReport.cs, the /*^*/ caret
Arguments
name"Performance"
members["Club", "Wins"]
Refuseswrong-member-count mentioning “3”; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Report
4 {
5 public string Describe()
6 {
7 string[] /*^*/row = { "Liverpool", "15", "3" };
8 return row[0] + row[1];
9 }
10 }
11 }