using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace ExtractMethod.Tooling; /// The suggested signature line of the extraction (parent spec: /// "ends with a suggested signature: int Extract(int i, Order order)"). /// Return type, void when nothing must flow /// out, or a tuple type when several declared-inside variables must. /// v1 always suggests Extract. /// The coherent parameter list (ref-ness preserved from /// the classification). /// The individual return slot candidates (name + /// type), name-ordered — the same order the tuple /// was built from, and the order em 04's codegen emits them in the appended /// return (...) statement and the call-side declaration. Empty for a /// void extraction. public sealed record SignatureSuggestion( string ReturnType, string MethodName, IReadOnlyList Params, IReadOnlyList ReturnSlots); /// /// 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. /// 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); } /// /// 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. /// public static HashSet DeclaredInsideNames(SemanticModel model, SelectionReport selection) { var names = new HashSet(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; } }