Rewriters
WrapGod.Migration.Engine ships eleven concrete IRuleRewriter implementations
organized in two levels. All rewriters use syntax-only analysis (no SemanticModel
required) and can safely operate on code that does not compile.
Rewriter Catalogue
A-Level (Syntactic)
| Class | Kind |
Rule type | What it transforms |
|---|---|---|---|
RenameTypeRewriter |
renameType |
RenameTypeRule |
Identifier nodes, qualified names |
RenameNamespaceRewriter |
renameNamespace |
RenameNamespaceRule |
using directives, qualified names |
RenameMemberRewriter |
renameMember |
RenameMemberRule |
Member-access expressions |
ChangeParameterRewriter |
changeParameter |
ChangeParameterRule |
Named-argument labels in invocations |
RemoveMemberRewriter |
removeMember |
RemoveMemberRule |
Call-site expression statements |
AddRequiredParameterRewriter |
addRequiredParameter |
AddRequiredParameterRule |
Argument lists |
ChangeTypeReferenceRewriter |
changeTypeReference |
ChangeTypeReferenceRule |
Type-position syntax nodes |
B-Level (Structural)
| Class | Kind |
Rule type | What it transforms |
|---|---|---|---|
SplitMethodRewriter |
splitMethod |
SplitMethodRule |
One call → N sequential statements with MIGRATION comment |
ExtractParameterObjectRewriter |
extractParameterObject |
ExtractParameterObjectRule |
Named args → new OptionsType { … } argument |
PropertyToMethodRewriter |
propertyToMethod |
PropertyToMethodRule |
Property reads/writes → method invocations |
MoveMemberRewriter |
moveMember |
MoveMemberRule |
Static-style OldType.Member → NewType.Member |
Universal Contracts
Every rewriter guarantees:
- Trivia preserved. Every replacement node copies trivia from the original via
WithTriviaFromorTriviaPreservation.WithReplacedToken. Whitespace and comments are never corrupted. - Return
nullon no-match. When nothing in the tree matches the rule, the rewriter returnsnullso the orchestrator can skip file-write. - Ambiguous →
SkippedRewrite. When a match is syntactically uncertain, aSkippedRewriteentry is recorded on theRewriteContextand the node is left unchanged. Wrong rewrites are never applied. - No
SemanticModel. All rewriters work on the raw syntax tree, so they handle broken or partially-compiled code.
RenameTypeRewriter
Renames a type by its short name (unqualified identifier) wherever it appears in identifier positions and as fully-qualified names.
// Rule:
// OldName = "Foo.OldWidget"
// NewName = "Foo.NewWidget"
// Before
Foo.OldWidget w = new Foo.OldWidget();
// After
Foo.NewWidget w = new Foo.NewWidget();
Ambiguity: Generic arguments and identifiers that are part of the right-hand side of a member access are NOT rewritten by this rewriter (the parent qualified name handles them).
RenameNamespaceRewriter
Rewrites using directives and qualified names whose namespace starts with
OldNamespace. Only exact prefix matches (followed by . or end-of-name) are
updated; unrelated identifiers that share a prefix are left alone.
// Rule:
// OldNamespace = "Acme.Legacy"
// NewNamespace = "Acme.Modern"
// Before
using Acme.Legacy.Widgets;
// After
using Acme.Modern.Widgets;
RenameMemberRewriter
Renames a method or property on a specific declaring type at every member.Access
expression. The receiver type is inferred via syntax-only heuristics (local variable
declarations and parameter types visible in the enclosing method body). When the
receiver type cannot be determined, a SkippedRewrite is recorded.
// Rule:
// TypeName = "MyService"
// OldMemberName = "Execute"
// NewMemberName = "Run"
// Before
MyService svc = new MyService();
svc.Execute();
// After
MyService svc = new MyService();
svc.Run();
Receiver inference scope: searches in order — (1) local variables and parameters in
the enclosing method/constructor/property body, (2) field and property declarations on
the enclosing type, (3) the receiver identifier itself as a type-of-name reference (for
static class member calls). Falls back to SkippedRewrite when the receiver is a
chained-call result or any expression whose type cannot be inferred syntactically.
ChangeParameterRewriter
Rewrites the named-argument label of a parameter that was renamed. Also performs
receiver-type disambiguation — when the receiver type can be inferred (via
RewriterHelpers.TryInferReceiverTypeName) and does not match the rule's declaring
type, the invocation is skipped to avoid rewriting unrelated methods that share a name.
For parameter type changes on positional arguments, a SkippedRewrite with reason
"type change requires semantic conversion" is recorded because a syntax-only
rewriter cannot safely convert argument values.
// Rule:
// TypeName = "Builder"
// MethodName = "Build"
// OldParameterName = "size"
// NewParameterName = "buttonSize"
// Before
builder.Build(size: 42);
// After
builder.Build(buttonSize: 42);
RemoveMemberRewriter
When a member has been completely removed from the API, every call site is removed
entirely and its location is annotated with a comment that preserves the original text
for manual inspection. The comment is attached as trivia to the next sibling statement
(or the closing brace if the removed call was the last statement), so no orphan ;
is left in the output. Each removal is recorded as an AppliedRewrite.
// Rule:
// MemberName = "Deprecated"
// Note = "Use NewApi.Process() instead."
// Before
void M(OldApi obj)
{
obj.Deprecated();
var x = 1;
}
// After
void M(OldApi obj)
{
// MIGRATION: DEL-001 removed — Use NewApi.Process() instead.: obj.Deprecated();
var x = 1;
}
AddRequiredParameterRewriter
Inserts a placeholder expression at the specified zero-based position in every matching
invocation's argument list, annotated with a TODO MIGRATION comment so developers
know to supply a real value. The placeholder is null when the parameter type is
syntactically a reference type (nullable annotation, array, string/object, or an
interface following the I + UpperCamelCase convention) and default otherwise. The
insertion is recorded as an AppliedRewrite.
// Rule:
// MethodName = "Apply"
// ParameterName = "theme"
// ParameterType = "MudTheme"
// Position = 0
// Before
provider.Apply();
// After
provider.Apply( default /* TODO MIGRATION: ARP-001 required arg 'theme' (MudTheme) added */);
ChangeTypeReferenceRewriter
Replaces a type reference (by short name or fully-qualified name) wherever it appears
in a syntactic type position: field declarations, method return types, parameter
types, cast expressions, typeof(), generic type arguments, base lists, and object
creation expressions. The rewriter also handles generic type names (e.g., IList<T>).
// Rule:
// OldType = "IList"
// NewType = "IReadOnlyList"
// Before
IList<string> items;
var t = typeof(IList<int>);
// After
IReadOnlyList<string> items;
var t = typeof(IReadOnlyList<int>);
B-Level Structural Rewriters
B-level rewriters handle structural breaking changes that go beyond simple identifier replacement. They share the same contracts as A-level rewriters but reason about the surrounding statement or expression context.
SplitMethodRewriter
One call site is replaced with multiple sequential calls, with the original call
commented-out as a MIGRATION annotation.
Statement context only. Calls whose return value is consumed (e.g., var x = Foo())
or chained invocations (e.g., Foo().Bar()) are skipped with a descriptive reason.
// Rule:
// TypeName = "Panel"
// OldMethodName = "Refresh"
// NewMethodNames = ["RefreshLayout", "RefreshContent"]
// Before
panel.Refresh();
// After
// MIGRATION: SM-001 split-method — original: panel.Refresh();
panel.RefreshLayout();
panel.RefreshContent();
Skipped cases:
// Return value consumed → SkippedRewrite
var html = panel.Refresh();
// Chained call → SkippedRewrite
panel.Refresh().ToString();
ExtractParameterObjectRewriter
Collapses a set of individually-named arguments into a new parameter object literal.
Named arguments matching the rule's ExtractedParameters list are extracted into an
object initializer. Non-extracted arguments are preserved in the argument list alongside
the new object. Positional-only calls where arguments cannot be unambiguously mapped are
skipped.
// Rule:
// TypeName = "Dialog"
// MethodName = "Show"
// ParameterObjectType = "ShowOptions"
// ExtractedParameters = ["title", "message"]
// Before
dlg.Show(title: "Hello", message: "World");
// After
dlg.Show(new ShowOptions { Title = "Hello", Message = "World" });
Extra arguments not in ExtractedParameters are preserved:
// Before
dlg.Show(title: "Hello", message: "World", callback: cb);
// After
dlg.Show(callback: cb, new ShowOptions { Title = "Hello", Message = "World" });
Skipped cases:
// More positional args than extracted params → SkippedRewrite
dlg.Show("Hello", "World", cb);
PropertyToMethodRewriter
Property reads become no-argument method calls; property writes become single-argument
method calls. The NewMethodName in the rule is used verbatim for both contexts.
// Rule:
// TypeName = "Button"
// OldPropertyName = "Disabled"
// NewMethodName = "SetDisabled"
// Write context: Before
btn.Disabled = true;
// Write context: After
btn.SetDisabled(true);
// Read context (with NewMethodName = "GetDisabled")
// Before
var b = btn.Disabled;
// After
var b = btn.GetDisabled();
Skipped cases:
// Compound-assignment → SkippedRewrite
btn.Disabled++;
btn.Disabled += 1;
MoveMemberRewriter
Redirects a static-style member access from one type to another. The receiver must match the old type's short name syntactically (or the fully-qualified old type name). Instance calls where the receiver is inferred to be a variable of a different type are skipped.
// Rule:
// OldTypeName = "LegacyHelper"
// NewTypeName = "ModernHelper"
// MemberName = "ComputeHash"
// Before
var h = LegacyHelper.ComputeHash();
// After
var h = ModernHelper.ComputeHash();
Skipped cases:
// Receiver inferred as a different type → SkippedRewrite
SomeOtherType legacyHelper = ...;
legacyHelper.ComputeHash(); // skipped: instance call cannot be moved syntactically
// Lowercase unresolvable identifier → SkippedRewrite (assumed instance variable)
legacyHelper.ComputeHash(); // skipped
See Also
- Migration Engine — scaffold contracts (
IRuleRewriter,RewriteContext,MigrationResult) - Migration Schema — rule kinds and JSON schema reference