Files
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

151 lines
5.7 KiB
C#

// Demo fixture for the extract-method micro-tool (tools/ExtractMethod).
//
// This file is DATA, not project source: the test project excludes it from
// compilation (see BeforeAfter.Tests.csproj) and runs Roslyn over its raw
// text, so the tool sees exactly what is checked in here. It uses explicit
// usings because the scratch compilation has no implicit usings.
//
// Bucket map (the classification yak 02 works from):
// bucket where example
// local, read-only (param) ScoreReads seed, score
// written + read-later (return) Summarize, Heaviest total, message, best
// scratch local (local) Summarize for-loop i
// enclosing-method param (param) ScoreReads, Summarize bonus, limit
// field access (extract-first) ScoreReads _seed (optional)
// property access (extract-first) Summarize Scale (optional)
// indexer access (extract-first) Heaviest widgets[i]
// method invocation (extract-first, flagged) Heaviest best.Bigger(...)
// multi-statement range incl. for Summarize total + for-loop
//
// Tests pin EXACT line numbers of the statements they select. Keep the
// formatting stable; when a line must change, update DemoFixtureTests too.
using System.Collections.Generic;
namespace BeforeAfter.Tests.ExtractMethod.Fixtures;
/// <summary>
/// Small first-class citizen of the extract-method tool: every method below
/// shows one or more of the classification "buckets" the tool must report.
/// </summary>
public class Demo
{
// Own-class state: reading it from a selection is "extract-first optional"
// (a same-class extracted method can still see these fields). Initialized
// here so the fixture compiles with zero diagnostics (CS0649 otherwise).
private int _seed = 5;
// Own-class property: like a field, accessible from a same-class method,
// so hoisting it is optional in the report.
public int Scale { get; set; } = 1;
/// <summary>Small local type so the scratch compilation binds.</summary>
public sealed record Widget(int Weight, int Count, string Label)
{
/// <summary>Instance method used to exercise the invocation bucket.</summary>
public Widget Bigger(Widget other) =>
Weight >= other.Weight ? this : other;
}
/// <summary>
/// Bucket: enclosing-method parameter read (bonus =&gt; param), local
/// read-only (seed, score =&gt; param), own-class field access
/// (_seed =&gt; extract-first, optional). Ends with a return expression.
/// </summary>
public int ScoreReads(int bonus)
{
int seed = _seed;
int score = seed * 2;
return score + bonus;
}
/// <summary>
/// Bucket: written + read-later locals (total, message =&gt; return),
/// scratch local (i =&gt; local), own-class property access (Scale =&gt;
/// extract-first optional), multi-statement range incl. a for-loop.
/// </summary>
public string Summarize(int limit)
{
int total = 0;
for (int i = 0; i < limit; i++)
{
total += i;
}
int scaled = total * Scale;
string message = "sum=" + scaled;
return message;
}
/// <summary>
/// Bucket: indexer access (widgets[i] on List&lt;T&gt; =&gt; extract-first),
/// method invocation (Bigger =&gt; extract-first, flagged because hoisting
/// changes how often it is evaluated), local written + read-later
/// (best =&gt; return).
/// </summary>
public Widget Heaviest(List<Widget> widgets, int count)
{
Widget best = widgets[0];
for (int i = 1; i < count; i++)
{
best = best.Bigger(widgets[i]);
}
return best;
}
/// <summary>
/// Bucket (em 03): the field read <c>_seed</c> appears TWICE — the
/// extract-first scan must dedupe it to one entry with an occurrence
/// count of 2. The field WRITE below (<c>_seed = a;</c>) is a
/// non-variable assignment left-hand side: the reads-only scan skips it
/// (decision 5) and the report notes the limitation.
/// </summary>
public int RepeatReads(int n)
{
int a = _seed + n;
int b = _seed * n;
_seed = a;
return a + b;
}
/// <summary>Bucket (em 03): a cast is an extract-first candidate too
/// (parent spec lists casts); its bound symbol is null — conversions are
/// not symbols — so the entry keys on its text alone.</summary>
public double Casts(int total)
{
return (double)total / _seed;
}
/// <summary>Bucket (em 04 codegen): a local DECLARED BEFORE the selection
/// that the selection writes and the tail reads — the promoted signature
/// keeps it as a <c>ref</c> parameter (the write-back already flows the
/// value out, so the result is not re-returned; void) and the call site
/// is a plain call, no assignment: <c>Extract(ref acc, n);</c>.</summary>
public int Accumulate(int n)
{
int acc = 0;
for (int i = 0; i < n; i++)
{
acc += i;
}
return acc * 2;
}
/// <summary>Bucket (em 04 codegen): TWO locals declared inside, both
/// written inside and read after — the promoted signature returns a
/// tuple <c>(int, int)</c> and the call site deconstructs it:
/// <c>var (x, y) = Extract(n);</c>.</summary>
public (int, int) Pairwise(int n)
{
int x = 0;
int y = 1;
for (int i = 0; i < n; i++)
{
x += i;
y += i * 2;
}
return (x + y, x - y);
}
}