Files
db-subclass-to-dto/tests/BeforeAfter.Tests/ExtractMethod/ReportTests.cs
T
mostalive 49a5633a7d em 03: extract-first scan + full suggestion report
- ExtractFirstScanner: reads-only scan of non-local/param read expressions
  (member/field/property access, element access, invocation, cast), deduped
  by symbol+text with occurrence counts; invocations flagged (hoisting
  changes eval count); own-class field/property reads marked optional.
  Assignment LHS skipped whole (decision 5) — surfaced as a report note.
- SignatureBuilder: promotes bucket overlaps into one coherent signature
  (declared-inside vars drop from params; declared-inside return candidates
  become the return; outside candidates already covered by their ref
  write-back stay ref params). Trailing composite returns stay void with a
  note suggesting an extract-variable first.
- ExtractionReporter + ReportFormatter: single composition shared by CLI and
  tests; report = header, params (ref), returns, locals, extract-first
  (count + flags), notes, suggested-signature line last.
- DataFlowClassifier: Returns now carry types (ReturnSuggestion) — the
  signature line needs them.
- Displays: compact symbol/type formatting for the report.
- Fixture: RepeatReads (dedupe ×2 + skipped field write), Casts (cast
  candidate) appended at the end so all pinned line numbers stay put.
2026-09-12 18:26:20 +01:00

107 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ExtractMethod.Tooling;
using Microsoft.CodeAnalysis;
namespace BeforeAfter.Tests.ExtractMethod;
/// <summary>
/// Tests for yak em 03: the signature promotion (SignatureBuilder) and the
/// formatted report (ReportFormatter) over the checked-in Demo.cs fixture.
/// Line anchors are the exact fixture lines pinned by em 01/02: ScoreReads
/// body 56..58, Summarize 68..76 (partial selection 68..72), Heaviest 87..93.
/// </summary>
public class ReportTests
{
// ---------------------------------------------------------------------
// Signature promotion, the case em 02's comment promises: in Summarize
// lines 68..72, `total` sits in params[ref] AND returns (declared inside,
// reassigned inside, read after), and `i` sits in params AND locals (the
// for-header declares it inside). Promotion: both drop out of the params,
// total becomes the plain return value -> `int Extract(int limit)`.
// ---------------------------------------------------------------------
[Fact]
public void Signature_for_Summarize_selection_promotes_declared_inside_variables()
{
var (report, formatted) = Compose(68, 72);
Assert.Equal(new[] { "limit" }, report.Signature.Params.Select(p => p.Name));
Assert.Equal("int", report.Signature.ReturnType);
Assert.Contains("suggested signature: int Extract(int limit)", formatted);
}
// ---------------------------------------------------------------------
// Heaviest full body: `best` (declared inside, ref in the raw params,
// trailing return candidate) promotes to the return; the for-var `i`
// drops from the params; widgets/count stay as plain in-params.
// ---------------------------------------------------------------------
[Fact]
public void Signature_for_Heaviest_returns_the_trailing_candidate_and_drops_declared_inside_params()
{
var (_, formatted) = Compose(87, 93);
// Parameter order follows the classifier's ordinal-by-name order —
// the same order the params bucket above prints (em 02 determinism).
Assert.Contains("suggested signature: Widget Extract(int count, List<Widget> widgets)", formatted);
}
// ---------------------------------------------------------------------
// No nameable return -> void (ScoreReads ends with `return score + bonus;`,
// a composite expression v1 cannot name — the report notes this); locals
// declared inside (seed, score) drop from the params; only the enclosing
// method's own parameter survives.
// ---------------------------------------------------------------------
[Fact]
public void Signature_for_ScoreReads_is_void_and_carries_only_the_enclosing_parameter()
{
var (report, formatted) = Compose(56, 58);
Assert.Equal(new[] { "bonus" }, report.Signature.Params.Select(p => p.Name));
Assert.Equal("void", report.Signature.ReturnType);
Assert.Contains("suggested signature: void Extract(int bonus)", formatted);
// The void-ness has an explanation in the report: the composite
// trailing return is not nameable in v1.
Assert.Contains(report.Notes, n => n.Contains("composite return expression (score + bonus)", StringComparison.Ordinal));
}
// ---------------------------------------------------------------------
// The report the CLI prints, pinned EXACTLY for one fixture selection
// (acceptance: "CLI report on the fixture matches the tests"). Heaviest
// full body is the richest case: raw buckets (params include the
// declared-inside best/i — the promotion happens only in the signature),
// extract-first with the invocation flag, no notes, signature last.
// ---------------------------------------------------------------------
[Fact]
public void Formatted_report_for_Heaviest_matches_the_CLI_output_exactly()
{
var (_, formatted) = Compose(87, 93);
var expected =
"3 statement(s) selected, lines 87..93 in Heaviest(): LocalDeclarationStatement, ForStatement, ReturnStatement\n" +
"params: best (Widget) [ref]\n" +
"params: count (int) [in]\n" +
"params: i (int) [ref]\n" +
"params: widgets (List<Widget>) [in]\n" +
"returns: best (Widget)\n" +
"locals: i\n" +
"extract-first:\n" +
" - widgets[0] — List<Widget>.this[int] ×1\n" +
" - best.Bigger(widgets[i]) — Widget.Bigger(Widget) ×1 [hoisting changes eval count]\n" +
" - widgets[i] — List<Widget>.this[int] ×1\n" +
"suggested signature: Widget Extract(int count, List<Widget> widgets)\n";
Assert.Equal(expected, formatted);
}
private static (ExtractionReport Report, string Formatted) Compose(int startLine, int endLine)
{
var (tree, compilation) = DemoFixture.Load();
var model = compilation.GetSemanticModel(tree);
var resolved = SelectionResolver.Resolve(tree, startLine, endLine);
Assert.True(resolved.Succeeded, resolved.Error);
var report = ExtractionReporter.Compose(model, resolved);
return (report, ReportFormatter.Format(report));
}
}