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:
@@ -21,19 +21,27 @@ namespace ExtractMethod.Tooling;
|
||||
/// the precise v1 rule.</param>
|
||||
public sealed record ParamSuggestion(string Name, string Type, bool ByRef);
|
||||
|
||||
/// <summary>One return candidate of the extracted method (v1: plain strings).</summary>
|
||||
/// <param name="Name">Variable name as it appears in the source.</param>
|
||||
/// <param name="Type">Type as a display string — the signature line of em 03
|
||||
/// prints it as the extracted method's return type.</param>
|
||||
public sealed record ReturnSuggestion(string Name, string Type);
|
||||
|
||||
/// <summary>The classified buckets of one selection (v1: symbol names as plain strings).</summary>
|
||||
/// <param name="Params">Variables the selection READS and the new method therefore
|
||||
/// receives: read locals + the enclosing method's parameters (parent decision #6).
|
||||
/// A variable may also appear in <see cref="Locals"/> and/or <see cref="Returns"/> —
|
||||
/// v1 reports per-bucket and em 03 dedupes into a coherent signature.</param>
|
||||
/// v1 reports per-bucket; em 03's signature builder dedupes into a coherent
|
||||
/// signature (a variable DECLARED inside the selection cannot be a parameter).</param>
|
||||
/// <param name="Returns">Variables written inside AND read after the selection
|
||||
/// (tail data flow), plus the value of a trailing <c>return X;</c> when X is a
|
||||
/// simple name — they must flow out of the extraction.</param>
|
||||
/// simple name — they must flow out of the extraction. Typed since em 03: the
|
||||
/// signature line needs the return type, not just the name.</param>
|
||||
/// <param name="Locals">Variables written inside and never read after: scratch
|
||||
/// locals of the new method.</param>
|
||||
public sealed record ExtractionSuggestion(
|
||||
IReadOnlyList<ParamSuggestion> Params,
|
||||
IReadOnlyList<string> Returns,
|
||||
IReadOnlyList<ReturnSuggestion> Returns,
|
||||
IReadOnlyList<string> Locals);
|
||||
|
||||
/// <summary>
|
||||
@@ -146,20 +154,28 @@ public static class DataFlowClassifier
|
||||
// ReadInside is ImmutableArray (not a set): fine for Contains lookups.
|
||||
var readAfter = AnalyzeTailFlow(model, selection)?.ReadInside ?? ImmutableArray<ISymbol>.Empty;
|
||||
|
||||
var trailingReturn = TrailingReturnName(selection);
|
||||
|
||||
// ---- returns: written inside ∧ read after, plus trailing return X ----
|
||||
var returnNames = writtenInside
|
||||
// Typed since em 03 (the signature line needs the return type). The
|
||||
// symbols come straight from the flow sets, so types are exact; the
|
||||
// trailing-return candidate resolves its own identifier below.
|
||||
var returnSymbols = writtenInside
|
||||
.Where(v => readAfter.Contains(v))
|
||||
.Select(v => v.Name)
|
||||
.Concat(trailingReturn is { } name ? new[] { name } : Array.Empty<string>())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(n => n, StringComparer.Ordinal)
|
||||
.ToDictionary(v => v.Name, VariableTypeString, StringComparer.Ordinal);
|
||||
|
||||
var trailingReturn = TrailingReturnSuggestion(model, selection);
|
||||
if (trailingReturn is { } candidate)
|
||||
{
|
||||
returnSymbols.TryAdd(candidate.Name, candidate.Type);
|
||||
}
|
||||
|
||||
var returnSuggestions = returnSymbols
|
||||
.Select(kv => new ReturnSuggestion(kv.Key, kv.Value))
|
||||
.OrderBy(r => r.Name, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
// ---- locals: written inside, never read after -> scratch locals ----
|
||||
var localNames = writtenInside
|
||||
.Where(v => !returnNames.Contains(v.Name, StringComparer.Ordinal))
|
||||
.Where(v => !returnSymbols.ContainsKey(v.Name))
|
||||
.Select(v => v.Name)
|
||||
.OrderBy(n => n, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
@@ -171,22 +187,22 @@ public static class DataFlowClassifier
|
||||
.OrderBy(v => v.Name, StringComparer.Ordinal)
|
||||
.Select(v => new ParamSuggestion(
|
||||
v.Name,
|
||||
ParamTypeString(v),
|
||||
VariableTypeString(v),
|
||||
IsReassignedInside(model, v, selection.Statements)))
|
||||
.ToList();
|
||||
|
||||
return new ExtractionSuggestion(paramSuggestions, returnNames, localNames);
|
||||
return new ExtractionSuggestion(paramSuggestions, returnSuggestions, localNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display string of the variable's type. ISymbol has no Type member;
|
||||
/// only locals and parameters carry one (the params bucket is restricted
|
||||
/// to exactly those kinds, hence the match).
|
||||
/// Display string of a variable's type. ISymbol has no Type member; only
|
||||
/// locals and parameters carry one. Used for the params bucket AND (since
|
||||
/// em 03) the returns bucket — the signature line prints it.
|
||||
/// </summary>
|
||||
private static string ParamTypeString(ISymbol variable) => variable switch
|
||||
private static string VariableTypeString(ISymbol variable) => variable switch
|
||||
{
|
||||
ILocalSymbol local => local.Type?.ToDisplayString() ?? "unknown",
|
||||
IParameterSymbol parameter => parameter.Type?.ToDisplayString() ?? "unknown",
|
||||
ILocalSymbol local => local.Type?.ToDisplayString(Displays.TypeFormat) ?? "unknown",
|
||||
IParameterSymbol parameter => parameter.Type?.ToDisplayString(Displays.TypeFormat) ?? "unknown",
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
@@ -264,14 +280,37 @@ public static class DataFlowClassifier
|
||||
|
||||
/// <summary>
|
||||
/// Parent spec: "if the selection ends with `return X;`, X is the candidate."
|
||||
/// Returns the simple-name candidate, or null when the returned expression
|
||||
/// is not a simple name — an expression like `score + bonus` is not nameable
|
||||
/// as a v1 string (composite return expressions are extract-first shape for
|
||||
/// em 03). Empty return ('return;') has no candidate.
|
||||
/// Returns the simple-name candidate with its type, or null when the
|
||||
/// returned expression is not a simple name — an expression like
|
||||
/// `score + bonus` is not nameable as a v1 string (composite return
|
||||
/// expressions are extract-first shape for em 03: the report suggests
|
||||
/// extracting it into a local first). Empty return ('return;') has no
|
||||
/// candidate.
|
||||
/// </summary>
|
||||
private static string? TrailingReturnName(SelectionReport selection)
|
||||
private static ReturnSuggestion? TrailingReturnSuggestion(SemanticModel model, SelectionReport selection)
|
||||
{
|
||||
var last = selection.Statements[^1];
|
||||
return last is ReturnStatementSyntax { Expression: IdentifierNameSyntax name } ? name.Identifier.ValueText : null;
|
||||
if (last is not ReturnStatementSyntax { Expression: IdentifierNameSyntax name })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var type = model.GetSymbolInfo(name).Symbol switch
|
||||
{
|
||||
ILocalSymbol local => local.Type?.ToDisplayString(Displays.TypeFormat) ?? "unknown",
|
||||
IParameterSymbol parameter => parameter.Type?.ToDisplayString(Displays.TypeFormat) ?? "unknown",
|
||||
_ => "unknown",
|
||||
};
|
||||
return new ReturnSuggestion(name.Identifier.ValueText, type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The trailing return statement when its expression is NOT a simple
|
||||
/// name (and not absent) — the composite-return shape the em 03 report
|
||||
/// flags as "extract a local first" (see <see cref="TrailingReturnSuggestion"/>).
|
||||
/// </summary>
|
||||
public static ReturnStatementSyntax? CompositeTrailingReturn(SelectionReport selection)
|
||||
=> selection.Statements[^1] is ReturnStatementSyntax { Expression: not null and not IdentifierNameSyntax } ret
|
||||
? ret
|
||||
: null;
|
||||
}
|
||||
Reference in New Issue
Block a user