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.
This commit is contained in:
2026-09-12 18:26:20 +01:00
parent 5d02c1aabf
commit 49a5633a7d
11 changed files with 985 additions and 68 deletions
@@ -63,7 +63,8 @@ public class DataFlowClassificationTests
Assert.True(tailFlow.Succeeded, "two-argument AnalyzeDataFlow must succeed on the (multi-statement) tail region");
var suggestion = DataFlowClassifier.Classify(model, report);
Assert.Equal(new[] { "total" }, suggestion.Returns);
Assert.Equal(new[] { "total" }, suggestion.Returns.Select(r => r.Name));
Assert.Equal(new[] { "int" }, suggestion.Returns.Select(r => r.Type));
Assert.Equal(new[] { "i" }, suggestion.Locals);
// limit is read in the for-header (68..72) — a plain by-value in-param.
@@ -86,7 +87,7 @@ public class DataFlowClassificationTests
{
var suggestion = ResolveAndClassify(68, 76);
Assert.Equal(new[] { "message" }, suggestion.Returns);
Assert.Equal(new[] { "message" }, suggestion.Returns.Select(r => r.Name));
Assert.Equal(new[] { "i", "scaled", "total" }, suggestion.Locals);
var total = Assert.Single(suggestion.Params.Where(p => p.Name == "total"));
@@ -113,7 +114,7 @@ public class DataFlowClassificationTests
{
var suggestion = ResolveAndClassify(87, 93);
Assert.Equal(new[] { "best" }, suggestion.Returns);
Assert.Equal(new[] { "best" }, suggestion.Returns.Select(r => r.Name));
Assert.Equal(new[] { "i" }, suggestion.Locals);
Assert.Equal(new[] { "best", "count", "i", "widgets" }, suggestion.Params.Select(p => p.Name));
Assert.Equal(new[] { true, false, true, false }, suggestion.Params.Select(p => p.ByRef));