Files
db-subclass-to-dto/tools/ExtractMethod/Program.cs
T
mostalive cdadafe676 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').
2026-09-12 21:15:28 +01:00

98 lines
3.6 KiB
C#

using ExtractMethod.Tooling;
using Microsoft.CodeAnalysis;
// CLIs are boring on purpose: argument parsing and printing live here, all
// Roslyn logic lives in Tooling/ so the tests can drive it directly.
string usage = "usage: ExtractMethod <file.cs> <startLine> <endLine> (1-based, inclusive)";
if (args.Length != 3)
{
Console.Error.WriteLine(usage);
return 2;
}
string file = Path.GetFullPath(args[0]);
if (!File.Exists(file))
{
Console.Error.WriteLine($"error: file not found: {file}");
Console.Error.WriteLine(usage);
return 2;
}
if (!int.TryParse(args[1], out int startLine) || !int.TryParse(args[2], out int endLine))
{
Console.Error.WriteLine($"error: line numbers must be integers");
Console.Error.WriteLine(usage);
return 2;
}
// 1. parse the file
var tree = CompilationLoader.ParseFile(file);
// 2. build the scratch compilation (refs from TRUSTED_PLATFORM_ASSEMBLIES)
var compilation = CompilationLoader.CreateCompilation(tree, Path.GetFileNameWithoutExtension(file));
if (compilation.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error))
{
Console.Error.WriteLine("warning: the file does not compile cleanly under a plain Roslyn compilation; reporting syntax-level resolution only");
}
// 3. snap the range to whole statements, report cleanly otherwise
var resolved = SelectionResolver.Resolve(tree, startLine, endLine);
if (!resolved.Succeeded)
{
Console.Error.WriteLine($"error: {resolved.Error}");
return SelectionResolver.ExitError;
}
// 4. compose the full suggestion (em 02 buckets + em 03 extract-first and
// signature), generate the refactoring (em 04), verify it and print the
// diff. Any failure is a clean error: message on stderr, same exit code
// as the resolver, never a stack trace.
var model = compilation.GetSemanticModel(tree);
try
{
var extraction = ExtractionReporter.Compose(model, resolved);
Console.Write(ReportFormatter.Format(extraction));
// 5. generate the refactoring from the suggestion (em 04 codegen): new
// method + rewritten call site + transformed tree.
var refactoring = RefactoringGenerator.Generate(model, resolved, extraction.Signature);
// 6. round-trip check: the generated code must compile with zero
// diagnostics (same scratch-compilation path as the input file).
var transformedCompilation = CompilationLoader.CreateCompilation(
refactoring.TransformedTree, Path.GetFileNameWithoutExtension(file));
var diagnostics = transformedCompilation.GetDiagnostics().ToList();
if (diagnostics.Count > 0)
{
Console.Error.WriteLine(
$"error: round-trip check failed — the generated code has {diagnostics.Count} diagnostic(s):");
foreach (var diagnostic in diagnostics)
{
Console.Error.WriteLine($" {diagnostic}");
}
Console.Error.WriteLine("---- generated code ----");
Console.Error.Write(refactoring.TransformedText);
return SelectionResolver.ExitError;
}
// 7. v1 decision (em 04): print a unified diff to stdout — PREVIEW, never
// an in-place file rewrite. Applying the diff (or a future --write
// flag with backup semantics) is a deliberate next step, because
// mutating the caller's file is a different safety policy than
// generating code.
Console.Write(UnifiedDiff.Diff(
tree.ToString(),
refactoring.TransformedText,
Path.GetFileName(file),
Path.GetFileName(file) + " (generated)"));
}
catch (Exception e) when (e is InvalidOperationException or ArgumentException)
{
Console.Error.WriteLine($"error: {e.Message}");
return SelectionResolver.ExitError;
}
return 0;