RefactoringGenerator emits the new method (promoted signature, parameter
names = source symbol names) plus the rewritten call site for every
em 02/03 bucket shape (return slot declared inside/outside, tuple
deconstruction, ref write-back, return-call when the selection ends a
non-void method) and refuses loudly where v1 cannot be sound (composite
trailing return, return not ending the method).
The transformed tree must compile with zero diagnostics (the yak's
round-trip check, enforced in both the CLI and the tests).
Findings that shaped the implementation, all enforced by tests:
- NormalizeWhitespace on the WHOLE root corrupts doc-comment trivia so
Roslyn's XML-doc writer fails with CS1569 ('count (-3) must be
non-negative') — and it would reformat the entire file, killing the
diff. Only generated nodes are normalized; original trivia is kept and
indentation/end-of-line is grafted onto the inserted statements/method.
- ParseTypeName("void") yields a node rejected as a method return type
(CS1547) — a void signature must be PredefinedType(Token(VoidKeyword)).
- NormalizeWhitespace drops the space after the contextual keyword 'var'
before '(' — the parsed 'var (x, y) = ...' deconstruction skeleton is
used verbatim; builder-made statements are normalized.
- The new method is normalized inside a throwaway class wrapper: the
normalizer computes indentation from nesting depth, and a bare method
has none.
v1 decision: print a unified-diff PREVIEW to stdout (hand-rolled LCS
unified diff), never an in-place rewrite. Also fixes the hunk header
off-by-one from the phantom trailing empty line of Split('\n').
135 lines
6.0 KiB
C#
135 lines
6.0 KiB
C#
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
|
|
namespace ExtractMethod.Tooling;
|
|
|
|
/// <summary>The suggested signature line of the extraction (parent spec:
|
|
/// "ends with a suggested signature: <c>int Extract(int i, Order order)</c>").</summary>
|
|
/// <param name="ReturnType">Return type, <c>void</c> when nothing must flow
|
|
/// out, or a tuple type when several declared-inside variables must.</param>
|
|
/// <param name="MethodName">v1 always suggests <c>Extract</c>.</param>
|
|
/// <param name="Params">The coherent parameter list (ref-ness preserved from
|
|
/// the classification).</param>
|
|
/// <param name="ReturnSlots">The individual return slot candidates (name +
|
|
/// type), name-ordered — the same order the tuple <see cref="ReturnType"/>
|
|
/// was built from, and the order em 04's codegen emits them in the appended
|
|
/// <c>return (...)</c> statement and the call-side declaration. Empty for a
|
|
/// void extraction.</param>
|
|
public sealed record SignatureSuggestion(
|
|
string ReturnType,
|
|
string MethodName,
|
|
IReadOnlyList<ParamSuggestion> Params,
|
|
IReadOnlyList<ReturnSuggestion> ReturnSlots);
|
|
|
|
/// <summary>
|
|
/// Turns the raw em 02 buckets into ONE coherent signature line. The buckets
|
|
/// over-report on purpose (parent spec: a variable can sit in several
|
|
/// buckets); building a signature is where the overlaps are resolved — the
|
|
/// "promotion" em 02's comment promises.
|
|
///
|
|
/// RULES (v1):
|
|
/// - A variable DECLARED inside the selection cannot be a parameter of the
|
|
/// extracted method (it does not exist before the selection): drop it from
|
|
/// the params. If it is also a return candidate, it becomes the RETURN —
|
|
/// e.g. `total` (declared, reassigned, read after) in Summarize lands in
|
|
/// params[ref] ∧ returns raw, and promotes to the plain return value.
|
|
/// - A return candidate declared OUTSIDE the selection that the extraction
|
|
/// already receives by-ref (read inside ∧ written inside) keeps its ref
|
|
/// parameter: the write-back already flows the value out, and also
|
|
/// returning it would hand the caller the same value twice.
|
|
/// - A return candidate declared OUTSIDE that is NOT a param (written
|
|
/// inside, never read inside) becomes a return slot too: the caller
|
|
/// reassigns it from the extracted method's result.
|
|
/// - Trailing composite returns (`return a + b;`) are not nameable in v1 —
|
|
/// no return slot; the report's note suggests extracting a local first.
|
|
///
|
|
/// Determinism: parameters keep the classifier's ordinal-by-name order and
|
|
/// return slots are ordered by name, so the line is stable for tests.
|
|
/// </summary>
|
|
public static class SignatureBuilder
|
|
{
|
|
public static SignatureSuggestion Build(SemanticModel model, SelectionReport selection, ExtractionSuggestion suggestion)
|
|
{
|
|
var declaredInside = DeclaredInsideNames(model, selection);
|
|
var paramNames = suggestion.Params.Select(p => p.Name).ToHashSet(StringComparer.Ordinal);
|
|
|
|
var parameters = suggestion.Params
|
|
.Where(p => !declaredInside.Contains(p.Name))
|
|
.ToList();
|
|
|
|
var returnSlots = suggestion.Returns
|
|
.Where(r => declaredInside.Contains(r.Name) || !paramNames.Contains(r.Name))
|
|
.OrderBy(r => r.Name, StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
var returnType = returnSlots.Count switch
|
|
{
|
|
0 => "void",
|
|
1 => returnSlots[0].Type,
|
|
// Branch-insensitive over-approximation can over-report returns
|
|
// (decision #4); the tuple keeps every candidate flowing rather
|
|
// than guessing which is real.
|
|
_ => $"({string.Join(", ", returnSlots.Select(r => r.Type))})",
|
|
};
|
|
|
|
return new SignatureSuggestion(returnType, "Extract", parameters, returnSlots);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Names of the locals DECLARED inside the selection. v1 covers the
|
|
/// declaration shapes the fixture uses — local declarations, for-loop
|
|
/// headers (`for (int i = ...)`) and foreach headers; other declaring
|
|
/// shapes (pattern declarations, using/fixed statements, local functions)
|
|
/// are not selected by the v1 resolver and would need their own cases.
|
|
///
|
|
/// Names, not symbols, are matched against the bucket entries — sound
|
|
/// here because C# forbids a local from shadowing another local or a
|
|
/// parameter of the same method, so names are unique per method.
|
|
/// </summary>
|
|
public static HashSet<string> DeclaredInsideNames(SemanticModel model, SelectionReport selection)
|
|
{
|
|
var names = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
foreach (var statement in selection.Statements)
|
|
{
|
|
foreach (var node in statement.DescendantNodesAndSelf())
|
|
{
|
|
switch (node)
|
|
{
|
|
case LocalDeclarationStatementSyntax localDeclaration:
|
|
foreach (var variable in localDeclaration.Declaration.Variables)
|
|
{
|
|
if (model.GetDeclaredSymbol(variable) is ILocalSymbol)
|
|
{
|
|
names.Add(variable.Identifier.ValueText);
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case ForStatementSyntax forStatement when forStatement.Declaration is not null:
|
|
foreach (var variable in forStatement.Declaration.Variables)
|
|
{
|
|
if (model.GetDeclaredSymbol(variable) is ILocalSymbol)
|
|
{
|
|
names.Add(variable.Identifier.ValueText);
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case ForEachStatementSyntax forEach:
|
|
if (model.GetDeclaredSymbol(forEach) is ILocalSymbol)
|
|
{
|
|
names.Add(forEach.Identifier.ValueText);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return names;
|
|
}
|
|
}
|