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;
}
}