em 04: codegen — generate the refactoring, round-trip check, unified-diff preview

RefactoringGenerator emits the new method (promoted signature, parameter
names = source symbol names) plus the rewritten call site for every
em 02/03 bucket shape (return slot declared inside/outside, tuple
deconstruction, ref write-back, return-call when the selection ends a
non-void method) and refuses loudly where v1 cannot be sound (composite
trailing return, return not ending the method).

The transformed tree must compile with zero diagnostics (the yak's
round-trip check, enforced in both the CLI and the tests).

Findings that shaped the implementation, all enforced by tests:
- NormalizeWhitespace on the WHOLE root corrupts doc-comment trivia so
  Roslyn's XML-doc writer fails with CS1569 ('count (-3) must be
  non-negative') — and it would reformat the entire file, killing the
  diff. Only generated nodes are normalized; original trivia is kept and
  indentation/end-of-line is grafted onto the inserted statements/method.
- ParseTypeName("void") yields a node rejected as a method return type
  (CS1547) — a void signature must be PredefinedType(Token(VoidKeyword)).
- NormalizeWhitespace drops the space after the contextual keyword 'var'
  before '(' — the parsed 'var (x, y) = ...' deconstruction skeleton is
  used verbatim; builder-made statements are normalized.
- The new method is normalized inside a throwaway class wrapper: the
  normalizer computes indentation from nesting depth, and a bare method
  has none.

v1 decision: print a unified-diff PREVIEW to stdout (hand-rolled LCS
unified diff), never an in-place rewrite. Also fixes the hunk header
off-by-one from the phantom trailing empty line of Split('\n').
This commit is contained in:
2026-09-12 21:15:28 +01:00
parent 49a5633a7d
commit cdadafe676
6 changed files with 892 additions and 6 deletions
@@ -10,10 +10,16 @@ namespace ExtractMethod.Tooling;
/// <param name="MethodName">v1 always suggests <c>Extract</c>.</param>
/// <param name="Params">The coherent parameter list (ref-ness preserved from
/// the classification).</param>
/// <param name="ReturnSlots">The individual return slot candidates (name +
/// type), name-ordered — the same order the tuple <see cref="ReturnType"/>
/// was built from, and the order em 04's codegen emits them in the appended
/// <c>return (...)</c> statement and the call-side declaration. Empty for a
/// void extraction.</param>
public sealed record SignatureSuggestion(
string ReturnType,
string MethodName,
IReadOnlyList<ParamSuggestion> Params);
IReadOnlyList<ParamSuggestion> Params,
IReadOnlyList<ReturnSuggestion> ReturnSlots);
/// <summary>
/// Turns the raw em 02 buckets into ONE coherent signature line. The buckets
@@ -66,7 +72,7 @@ public static class SignatureBuilder
_ => $"({string.Join(", ", returnSlots.Select(r => r.Type))})",
};
return new SignatureSuggestion(returnType, "Extract", parameters);
return new SignatureSuggestion(returnType, "Extract", parameters, returnSlots);
}
/// <summary>