|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.SignatureHelp
{
internal abstract partial class AbstractCSharpSignatureHelpProvider
{
internal static class LightweightOverloadResolution
{
public static void RefineOverloadAndPickParameter(
Document document,
int position,
SemanticModel semanticModel,
ImmutableArray<IMethodSymbol> candidates,
SeparatedSyntaxList<ArgumentSyntax> arguments,
out IMethodSymbol? currentSymbol,
out int parameterIndex)
{
var semanticFactsService = document.GetRequiredLanguageService<ISemanticFactsService>();
if (candidates.Length == 1)
{
// The compiler told us the correct overload or we only have one choice, but we need to find out the parameter to highlight given cursor position
currentSymbol = candidates[0];
FindParameterIndexIfCompatibleMethod(arguments, currentSymbol, position, semanticModel, semanticFactsService, out parameterIndex);
}
else
{
(currentSymbol, parameterIndex) = GuessCurrentSymbolAndParameter(arguments, candidates, position, semanticModel, semanticFactsService);
}
}
/// <summary>
/// If the symbol could not be bound, we could be dealing with a partial invocation, we'll try to find a possible overload.
/// </summary>
private static (IMethodSymbol? symbol, int parameterIndex) GuessCurrentSymbolAndParameter(
SeparatedSyntaxList<ArgumentSyntax> arguments,
ImmutableArray<IMethodSymbol> methodGroup,
int position,
SemanticModel semanticModel,
ISemanticFactsService semanticFactsService)
{
if (arguments.Count != 0)
{
foreach (var method in methodGroup)
{
if (FindParameterIndexIfCompatibleMethod(arguments, method, position, semanticModel, semanticFactsService, out var parameterIndex))
return (method, parameterIndex);
}
}
// Note: Providing no recommendation if no arguments allows the model to keep the last implicit choice
return (null, -1);
}
/// <summary>
/// Simulates overload resolution with the arguments provided so far and determines if you might be calling this overload.
/// Returns true if an overload is acceptable. In that case, we output the parameter that should be highlighted given the cursor's
/// position in the partial invocation.
/// </summary>
internal static bool FindParameterIndexIfCompatibleMethod(
SeparatedSyntaxList<ArgumentSyntax> arguments,
IMethodSymbol method,
int position,
SemanticModel semanticModel,
ISemanticFactsService semanticFactsService,
out int foundParameterIndex)
{
// map the arguments to their corresponding parameters
var argumentCount = arguments.Count;
using var _ = ArrayBuilder<int>.GetInstance(argumentCount, fillWithValue: -1, out var argToParamMap);
if (!TryPrepareArgToParamMap(arguments, method, argToParamMap))
{
foundParameterIndex = -1;
return false;
}
// verify that the arguments are compatible with their corresponding parameters
var parameters = method.Parameters;
for (var argumentIndex = 0; argumentIndex < argumentCount; argumentIndex++)
{
var parameterIndex = argToParamMap[argumentIndex];
if (parameterIndex < 0)
continue;
var parameter = parameters[parameterIndex];
var argument = arguments[argumentIndex];
if (!IsCompatibleArgument(argument, parameter))
{
foundParameterIndex = -1;
return false;
}
}
// find the parameter at the cursor position
var argumentIndexToSave = TryGetArgumentIndex(arguments, position);
if (argumentIndexToSave >= 0)
{
var foundParam = argToParamMap[argumentIndexToSave];
if (foundParam >= 0)
{
foundParameterIndex = foundParam;
}
else
{
foundParameterIndex = FirstUnspecifiedParameter(argToParamMap, argumentCount);
}
}
else
{
foundParameterIndex = -1;
}
Debug.Assert(foundParameterIndex < parameters.Length);
return true;
// If the cursor is pointing at an argument for which we did not find the corresponding
// parameter, we will highlight the first unspecified parameter.
static int FirstUnspecifiedParameter(ArrayBuilder<int> argToParamMap, int argumentCount)
{
using var _ = ArrayBuilder<bool>.GetInstance(argumentCount, false, out var specified);
for (var i = 0; i < argumentCount; i++)
{
var parameterIndex = argToParamMap[i];
if (parameterIndex >= 0)
{
specified[parameterIndex] = true;
}
}
var first = specified.FindIndex(s => !s);
return first <= 0 ? 0 : first;
}
// Determines if the given argument is compatible with the given parameter
bool IsCompatibleArgument(ArgumentSyntax argument, IParameterSymbol parameter)
{
var parameterRefKind = parameter.RefKind;
if (parameterRefKind == RefKind.None)
{
if (IsEmptyArgument(argument.Expression))
{
// An argument left empty is considered to match any parameter
// M(1, $$)
// M(1, , 2$$)
return true;
}
var type = parameter.Type;
if (parameter.IsParams
&& type is IArrayTypeSymbol arrayType
&& HasImplicitConversion(argument.Expression, arrayType.ElementType))
{
return true;
}
return HasImplicitConversion(argument.Expression, type);
}
var argumentRefKind = argument.GetRefKind();
if (parameterRefKind == RefKind.In && argumentRefKind == RefKind.None)
{
// A by-value argument matches an `in` parameter
return true;
}
if (parameterRefKind == argumentRefKind)
{
return true;
}
return false;
}
bool HasImplicitConversion(SyntaxNode expression, ITypeSymbol destination)
{
var conversion = semanticFactsService.ClassifyConversion(semanticModel, expression, destination);
return conversion.IsImplicit;
}
}
/// <summary>
/// Find the parameter index corresponding to each argument provided
/// </summary>
private static bool TryPrepareArgToParamMap(SeparatedSyntaxList<ArgumentSyntax> arguments, IMethodSymbol method, ArrayBuilder<int> argToParamMap)
{
var parameters = method.Parameters;
var parameterCount = parameters.Length;
var currentParameterIndex = 0;
var seenOutOfPositionArgument = false;
var inParams = false;
for (var argumentIndex = 0; argumentIndex < arguments.Count; argumentIndex++)
{
if (argumentIndex >= parameterCount && !inParams)
{
return false;
}
var argument = arguments[argumentIndex];
if (HasName(argument, out var name))
{
var namedParameterIndex = parameters.IndexOf(p => p.Name == name);
if (namedParameterIndex < 0)
{
return false;
}
if (namedParameterIndex != currentParameterIndex)
{
seenOutOfPositionArgument = true;
}
AddArgToParamMapping(argumentIndex, namedParameterIndex);
IncrementParameterIndexIfNeeded();
}
else if (IsEmptyArgument(argument.Expression))
{
if (!seenOutOfPositionArgument)
{
// We count the empty argument as a used position
AddArgToParamMapping(argumentIndex, currentParameterIndex);
IncrementParameterIndexIfNeeded();
}
}
else if (seenOutOfPositionArgument)
{
// Unnamed arguments are not allowed after an out-of-position argument
return false;
}
else
{
AddArgToParamMapping(argumentIndex, currentParameterIndex);
IncrementParameterIndexIfNeeded();
}
}
return true;
void IncrementParameterIndexIfNeeded()
{
if (!seenOutOfPositionArgument && !inParams)
{
currentParameterIndex++;
}
}
void AddArgToParamMapping(int argumentIndex, int parameterIndex)
{
Debug.Assert(parameterIndex >= 0);
Debug.Assert(parameterIndex < parameterCount);
inParams |= parameters[parameterIndex].IsParams;
argToParamMap[argumentIndex] = parameterIndex;
}
}
private static bool IsEmptyArgument(ExpressionSyntax expression)
=> expression.Span.IsEmpty;
/// <summary>
/// Given the cursor position, find which argument is active.
/// This will be useful to later find which parameter should be highlighted.
/// </summary>
private static int TryGetArgumentIndex(SeparatedSyntaxList<ArgumentSyntax> arguments, int position)
{
if (arguments.Count == 0)
return -1;
for (var i = 0; i < arguments.Count - 1; i++)
{
// `$$,` points to the argument before the separator
// but `,$$` points to the argument following the separator
if (position <= arguments.GetSeparator(i).Span.Start)
return i;
}
return arguments.Count - 1;
}
private static bool HasName(ArgumentSyntax argument, [NotNullWhen(true)] out string? name)
{
name = argument.NameColon?.Name.Identifier.ValueText;
return name != null;
}
}
}
}
|