- 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.
81 lines
3.0 KiB
C#
81 lines
3.0 KiB
C#
using System.Text;
|
||
|
||
namespace ExtractMethod.Tooling;
|
||
|
||
/// <summary>
|
||
/// Renders an <see cref="ExtractionReport"/> as the human-readable extract-
|
||
/// method suggestion report (parent spec): params (with ref), return
|
||
/// candidates, locals, extract-first candidates (deduped, with occurrence
|
||
/// counts and flags), the limitation notes, and the suggested signature line
|
||
/// LAST. Pure string shaping — no Roslyn — so tests can pin the exact output
|
||
/// the CLI prints.
|
||
/// </summary>
|
||
public static class ReportFormatter
|
||
{
|
||
public static string Format(ExtractionReport report)
|
||
{
|
||
var sb = new StringBuilder();
|
||
var selection = report.Selection;
|
||
var suggestion = report.Suggestion;
|
||
|
||
// Header (same wording the CLI printed since em 01).
|
||
sb.AppendLine(
|
||
$"{selection.Count} statement(s) selected, lines {selection.StartLine}..{selection.EndLine} " +
|
||
$"in {selection.Method?.Identifier.ValueText}(): " +
|
||
string.Join(", ", selection.Kinds));
|
||
|
||
AppendBucket(sb, "params", suggestion.Params.Select(p =>
|
||
$"{p.Name} ({p.Type}){(p.ByRef ? " [ref]" : " [in]")}"));
|
||
AppendBucket(sb, "returns", suggestion.Returns.Select(r => $"{r.Name} ({r.Type})"));
|
||
AppendBucket(sb, "locals", suggestion.Locals);
|
||
|
||
if (report.ExtractFirst.Count == 0)
|
||
{
|
||
sb.AppendLine("extract-first: (none)");
|
||
}
|
||
else
|
||
{
|
||
sb.AppendLine("extract-first:");
|
||
foreach (var entry in report.ExtractFirst)
|
||
{
|
||
// Flags accumulate; occurrence count is always shown because
|
||
// "count occurrences" is the point of the dedupe.
|
||
var flags = new List<string>();
|
||
if (entry.IsInvocation)
|
||
{
|
||
flags.Add("hoisting changes eval count");
|
||
}
|
||
|
||
if (entry.Optional)
|
||
{
|
||
flags.Add($"optional: own-class {entry.OwnMemberKind}");
|
||
}
|
||
|
||
var flagText = flags.Count > 0 ? $" [{string.Join("; ", flags)}]" : string.Empty;
|
||
sb.AppendLine($" - {entry.Text} — {entry.SymbolDisplay} ×{entry.Occurrences}{flagText}");
|
||
}
|
||
}
|
||
|
||
foreach (var note in report.Notes)
|
||
{
|
||
sb.AppendLine($"note: {note}");
|
||
}
|
||
|
||
var signature = report.Signature;
|
||
var parameterList = string.Join(", ", signature.Params.Select(p => $"{(p.ByRef ? "ref " : "")}{p.Type} {p.Name}"));
|
||
sb.AppendLine($"suggested signature: {signature.ReturnType} {signature.MethodName}({parameterList})");
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
private static void AppendBucket(StringBuilder sb, string name, IEnumerable<string> items)
|
||
{
|
||
var enumerated = items.ToList();
|
||
sb.AppendLine(enumerated.Count == 0 ? $"{name}: (none)" : $"{name}: {enumerated[0]}");
|
||
for (var i = 1; i < enumerated.Count; i++)
|
||
{
|
||
sb.AppendLine($"{name}: {enumerated[i]}");
|
||
}
|
||
}
|
||
}
|