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:
@@ -0,0 +1,58 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace ExtractMethod.Tooling;
|
||||
|
||||
/// <summary>Everything the extract-method report is built from: the resolved
|
||||
/// selection, the raw classification buckets (em 02), the extract-first
|
||||
/// candidates (em 03), the promoted signature line (em 03) and the v1
|
||||
/// limitation notes. One record so the CLI and the tests compose EXACTLY the
|
||||
/// same thing — the report cannot drift between them.</summary>
|
||||
public sealed record ExtractionReport(
|
||||
SelectionReport Selection,
|
||||
ExtractionSuggestion Suggestion,
|
||||
IReadOnlyList<ExtractFirstEntry> ExtractFirst,
|
||||
SignatureSuggestion Signature,
|
||||
IReadOnlyList<string> Notes);
|
||||
|
||||
/// <summary>
|
||||
/// Composes the full extraction suggestion for a resolved selection: the em 02
|
||||
/// data-flow buckets, the em 03 extract-first scan, the promoted signature and
|
||||
/// the limitation notes. Throws the same clean exceptions as
|
||||
/// <see cref="DataFlowClassifier.Classify"/> when analysis cannot bind.
|
||||
/// </summary>
|
||||
public static class ExtractionReporter
|
||||
{
|
||||
public static ExtractionReport Compose(SemanticModel model, SelectionReport selection)
|
||||
{
|
||||
var suggestion = DataFlowClassifier.Classify(model, selection);
|
||||
var extractFirst = ExtractFirstScanner.Scan(model, selection);
|
||||
var signature = SignatureBuilder.Build(model, selection, suggestion);
|
||||
return new ExtractionReport(selection, suggestion, extractFirst, signature, BuildNotes(model, selection));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The v1 limitation notes (parent decision #5 and the em 02 contract):
|
||||
/// each note is printed only when its limitation actually applies to the
|
||||
/// selection, so a clean selection gets a clean report.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<string> BuildNotes(SemanticModel model, SelectionReport selection)
|
||||
{
|
||||
var notes = new List<string>();
|
||||
|
||||
if (ExtractFirstScanner.HasNonVariableAssignmentLeftSide(model, selection))
|
||||
{
|
||||
notes.Add(
|
||||
"reads-only scan (decision 5): assignment left-hand sides are skipped, " +
|
||||
"so writes to fields/indexers/properties are invisible to this report");
|
||||
}
|
||||
|
||||
if (DataFlowClassifier.CompositeTrailingReturn(selection) is { } trailing)
|
||||
{
|
||||
notes.Add(
|
||||
$"the selection ends with a composite return expression ({trailing.Expression!.ToFullString().Trim()}) — " +
|
||||
"extract it into a local first to make the return value nameable");
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user