RefactorMCP catalog

Introduce Generic Type Parameter

Replaces a concrete type used in a class or method with a new type parameter, constrained as the uses need, and passes the old type wherever the class or method is used, so every existing use means what it did.

Arguments

ArgumentRequiredMeaning
typeyesThe concrete type to replace, as written in the class's file
nameyesThe new type parameter's name

The target is the class or the method, by symbol: "target": { "symbol": "T:Shop.Box" } or "target": { "symbol": "M:Shop.Ledger.Load" }.

Precondition

  • type names a type, and the class or method uses it.
  • name is an identifier nothing in scope already uses: not an existing type parameter, type or member.
  • The class or method does not reach a static member through type, and does not create it with constructor arguments; a type parameter can do neither.
  • Some constraint makes the result compile with every use reaching the same members as before.

Transformation

  • Every name of type in the class or method becomes the type parameter: in declarations, casts, typeof, type arguments and nullable annotations. var stays var, and nameof(Invoice) is left alone because its text is its value.
  • The constraint is the loosest that works, tried in order: none, each interface the type implements, then the type itself when it is a class that is not sealed. new() is added when the code creates instances.
  • A class gains the parameter after any it already has; its references to itself take the new parameter, and every other reference to it takes the old type: Box becomes Box<Invoice>, Index<int> becomes Index<int, Invoice>, and Box.Empty() becomes Box<Invoice>.Empty().
  • A method gains the parameter; a call that cannot infer it, because no parameter's type mentions the old type, passes the old type explicitly, spelling out any type arguments it used to infer.
  • The constraint clause goes at the end of the header, before the body.

Preserved

  • Every use of the class or method, which instantiates it with the old type.
  • What each use of a value of the old type reaches: another overload or a different member is refused, so the constraint is tightened or the refactoring declines.

Limitations

  • Calls between the class's own members are not rechecked for overloads, since their signatures change together.
  • References in documentation comments are left as they are.
  • Only a class's or method's own declaration is changed; partial classes with several declarations are not covered.

Error codes

CodeMeaning
type-not-foundtype names no type the file can see
type-not-usedthe class or method does not use type
invalid-namename is not an identifier
name-conflictname is already a type parameter, type or member in scope
uses-static-membersthe code reaches a static member through type
no-valid-constraintno constraint keeps the code compiling and binding as before, or instances are created with arguments

Cases

·

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

#already-generic-class

Adds a second type parameter to a generic class and to every use of it

success
Targetsymbol T:Shop.Index`1
Arguments
type"Invoice"
name"TValue"
Catalog.csmodified
11 namespace Shop
22 {
33 public static class Catalog
44 {
5− public static Index<int> ByNumber(Invoice invoice) => new Index<int>().With(1, invoice);
5+ public static Index<int, Invoice> ByNumber(Invoice invoice) => new Index<int, Invoice>().With(1, invoice);
66 }
77 }
Index.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
5− public class Index<TKey>
5+ public class Index<TKey, TValue>
66 {
7− private readonly Dictionary<TKey, Invoice> _entries = new Dictionary<TKey, Invoice>();
7+ private readonly Dictionary<TKey, TValue> _entries = new Dictionary<TKey, TValue>();
88
9− public void Add(TKey key, Invoice value) => _entries[key] = value;
9+ public void Add(TKey key, TValue value) => _entries[key] = value;
1010
11− public Index<TKey> With(TKey key, Invoice value)
11+ public Index<TKey, TValue> With(TKey key, TValue value)
1212 {
1313 Add(key, value);
1414 return this;
1515 }
1616 }
1717 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public class Invoice
44 {
55 public decimal Total;
66 }
77 }

#class-without-member-use

Makes a class generic over a type it only stores, and passes the old type at every use of the class

success
Targetsymbol T:Shop.Box
Arguments
type"Invoice"
name"T"
Box.csmodified
11 namespace Shop
22 {
3− public class Box
3+ public class Box<T>
44 {
5− private Invoice _item;
5+ private T _item;
66
7− public void Put(Invoice item) => _item = item;
7+ public void Put(T item) => _item = item;
88
9− public Invoice Take() => _item;
9+ public T Take() => _item;
1010
11− public static Box Empty() => new Box();
11+ public static Box<T> Empty() => new Box<T>();
1212 }
1313 }
Shelf.csmodified
11 namespace Shop
22 {
33 public static class Shelf
44 {
55 public static decimal Roundtrip(Invoice invoice)
66 {
7− Box box = Box.Empty();
7+ Box<Invoice> box = Box<Invoice>.Empty();
88 box.Put(invoice);
99 return box.Take().Total;
1010 }
1111 }
1212 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public class Invoice
44 {
55 public decimal Total;
66 }
77 }

#constrained-by-member-use

Constrains the type parameter to the old type when the class uses its members

success
Targetsymbol T:Shop.Box
Arguments
type"Invoice"
name"TItem"
Box.csmodified
11 namespace Shop
22 {
3− public class Box
3+ public class Box<TItem> where TItem : Invoice
44 {
5− private Invoice _item;
5+ private TItem _item;
66
7− public void Put(Invoice item) => _item = item;
7+ public void Put(TItem item) => _item = item;
88
99 public decimal Value() => _item.Total;
1010 }
1111 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public class Invoice
44 {
55 public decimal Total;
66 }
77 }

#interface-constraint-for-sealed-type

Constrains to the interface the old type implements when the old type is sealed and the uses need only that interface

success
Targetsymbol T:Shop.Box
Arguments
type"Invoice"
name"T"
Box.csmodified
11 namespace Shop
22 {
3− public class Box
3+ public class Box<T> where T : IPriced
44 {
5− private Invoice _item;
5+ private T _item;
66
7− public void Put(Invoice item) => _item = item;
7+ public void Put(T item) => _item = item;
88
99 public decimal Value() => _item.Total;
1010 }
1111 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public interface IPriced
44 {
55 decimal Total { get; }
66 }
77
88 public sealed class Invoice : IPriced
99 {
1010 public decimal Total { get; set; }
1111 }
1212 }

#invalid-name

Refuses a name that is not an identifier

refusal
Targetsymbol T:Shop.Box
Arguments
type"Invoice"
name"T-Item"
Refusesinvalid-name; every file is left unchanged
Box.csinput
1 namespace Shop
2 {
3 public class Box
4 {
5 private Invoice _item;
6
7 public void Put(Invoice item) => _item = item;
8
9 public Invoice Take() => _item;
10
11 public static Box Empty() => new Box();
12 }
13 }
Invoice.csinput
1 namespace Shop
2 {
3 public class Invoice
4 {
5 public decimal Total;
6 }
7 }

#method-calls-need-type-argument

Passes the old type explicitly where the compiler could not infer the new type parameter

success
Targetsymbol M:Shop.Ledger.Load
Arguments
type"Invoice"
name"T"
Ledger.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Ledger
66 {
7− public List<Invoice> Load() => new List<Invoice>();
7+ public List<T> Load<T>() => new List<T>();
88
9− public int Count() => Load().Count;
9+ public int Count() => Load<Invoice>().Count;
1010 }
1111 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public class Invoice
44 {
55 public decimal Total;
66 }
77 }

#method-with-inferred-calls

Makes a method generic, leaving calls whose arguments let the compiler infer the type

success
Targetsymbol M:Shop.Ledger.Sum(System.Collections.Generic.List{Shop.Invoice})
Arguments
type"Invoice"
name"T"
Ledger.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public class Ledger
66 {
7− public decimal Sum(List<Invoice> items)
7+ public decimal Sum<T>(List<T> items) where T : Invoice
88 {
99 decimal sum = 0;
1010 foreach (var item in items)
1111 sum += item.Total;
1212 return sum;
1313 }
1414 }
1515 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public class Invoice
44 {
55 public decimal Total;
66 }
77 }
Report.csunchanged
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 public static class Report
66 {
77 public static decimal Total(Ledger ledger, List<Invoice> invoices) => ledger.Sum(invoices);
88 }
99 }

#name-conflict

Refuses a type parameter name the class already uses

refusal
Targetsymbol T:Shop.Index`1
Arguments
type"Invoice"
name"TKey"
Refusesname-conflict mentioning “TKey”; every file is left unchanged
Index.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Index<TKey>
6 {
7 private readonly Dictionary<TKey, Invoice> _entries = new Dictionary<TKey, Invoice>();
8
9 public void Add(TKey key, Invoice value) => _entries[key] = value;
10
11 public Index<TKey> With(TKey key, Invoice value)
12 {
13 Add(key, value);
14 return this;
15 }
16 }
17 }
Invoice.csinput
1 namespace Shop
2 {
3 public class Invoice
4 {
5 public decimal Total;
6 }
7 }

#new-constraint

Adds a new() constraint when the class creates instances of the old type

success
Targetsymbol T:Shop.Factory
Arguments
type"Invoice"
name"T"
Factory.csmodified
11 namespace Shop
22 {
3− public class Factory
3+ public class Factory<T> where T : new()
44 {
5− public Invoice Create() => new Invoice();
5+ public T Create() => new T();
66 }
77 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public class Invoice
44 {
55 public decimal Total;
66 }
77 }

#no-valid-constraint

Refuses when the old type is sealed and no interface it implements offers the members used

refusal
Targetsymbol T:Shop.Box
Arguments
type"Invoice"
name"T"
Refusesno-valid-constraint mentioning “Invoice”; every file is left unchanged
Box.csinput
1 namespace Shop
2 {
3 public class Box
4 {
5 private Invoice _item;
6
7 public void Put(Invoice item) => _item = item;
8
9 public decimal Value() => _item.Total;
10 }
11 }
Invoice.csinput
1 namespace Shop
2 {
3 public sealed class Invoice
4 {
5 public decimal Total;
6 }
7 }

#nullable-annotations

Keeps nullable annotations, now on the type parameter

success
Targetsymbol T:Shop.Box
Arguments
type"Invoice"
name"T"
Projectnullable enable
Box.csmodified
11 namespace Shop
22 {
3− public class Box
3+ public class Box<T> where T : Invoice
44 {
5− private Invoice? _item;
5+ private T? _item;
66
7− public void Put(Invoice? item) => _item = item;
7+ public void Put(T? item) => _item = item;
88
99 public decimal Value() => _item?.Total ?? 0;
1010 }
1111 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public class Invoice
44 {
55 public decimal Total;
66 }
77 }

#type-not-found

Refuses a type that does not exist

refusal
Targetsymbol T:Shop.Box
Arguments
type"Bill"
name"T"
Refusestype-not-found mentioning “Bill”; every file is left unchanged
Box.csinput
1 namespace Shop
2 {
3 public class Box
4 {
5 private Invoice _item;
6
7 public void Put(Invoice item) => _item = item;
8
9 public Invoice Take() => _item;
10
11 public static Box Empty() => new Box();
12 }
13 }
Invoice.csinput
1 namespace Shop
2 {
3 public class Invoice
4 {
5 public decimal Total;
6 }
7 }

#type-not-used

Refuses a type the class does not use

refusal
Targetsymbol T:Shop.Box
Arguments
type"Receipt"
name"T"
Refusestype-not-used mentioning “Receipt”; every file is left unchanged
Box.csinput
1 namespace Shop
2 {
3 public class Box
4 {
5 private Invoice _item;
6
7 public void Put(Invoice item) => _item = item;
8
9 public Invoice Take() => _item;
10
11 public static Box Empty() => new Box();
12 }
13 }
Invoice.csinput
1 namespace Shop
2 {
3 public class Invoice
4 {
5 public decimal Total;
6 }
7
8 public class Receipt
9 {
10 }
11 }

#uses-static-members

Refuses when the class reaches a static member through the old type, which a type parameter cannot do

refusal
Targetsymbol T:Shop.Box
Arguments
type"Invoice"
name"T"
Refusesuses-static-members mentioning “Blank”; every file is left unchanged
Box.csinput
1 namespace Shop
2 {
3 public class Box
4 {
5 private Invoice _item = Invoice.Blank();
6
7 public Invoice Take() => _item;
8 }
9 }
Invoice.csinput
1 namespace Shop
2 {
3 public class Invoice
4 {
5 public static Invoice Blank() => new Invoice();
6 }
7 }