|
// 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 Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Shared.Utilities;
/// <summary>
/// Helper type that allows features (like signature-help or go-to-definition) to make better decisions about which
/// overload the user is likely choosing when the compiler itself bails out and gives a generic list of options.
/// </summary>
/// <param name="position">Location the user is at <em>within</em> the argument list. Used to determine which <see
/// cref="IParameterSymbol"/> corresponds to that location. This value is optional, and does not need to be provided if
/// the client of <see cref="LightweightOverloadResolution"/> only wants to determine which overload is most likely
/// given the arguments.</param>
internal readonly struct LightweightOverloadResolution(
ISemanticFacts semanticFacts,
SemanticModel semanticModel,
SeparatedSyntaxList<SyntaxNode> arguments,
int? position = null)
{
private ISyntaxFacts SyntaxFacts => semanticFacts.SyntaxFacts;
public IMethodSymbol? RefineOverload(SymbolInfo symbolInfo, ImmutableArray<IMethodSymbol> candidates)
=> RefineOverloadAndPickParameter(symbolInfo, candidates).method;
public (IMethodSymbol? method, int parameterIndex) RefineOverloadAndPickParameter(SymbolInfo symbolInfo, ImmutableArray<IMethodSymbol> candidates)
{
// If 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
return symbolInfo.Symbol is IMethodSymbol method
? TryFindParameterIndexIfCompatibleMethod(method)
: GuessCurrentSymbolAndParameter(candidates);
}
public int FindParameterIndexIfCompatibleMethod(IMethodSymbol method)
{
var (match, parameterIndex) = TryFindParameterIndexIfCompatibleMethod(method);
return match is null ? -1 : parameterIndex;
}
/// <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 (IMethodSymbol? symbol, int parameterIndex) GuessCurrentSymbolAndParameter(ImmutableArray<IMethodSymbol> methodGroup)
{
if (arguments.Count > 0)
{
foreach (var method in methodGroup)
{
var (candidateMethod, parameterIndex) = TryFindParameterIndexIfCompatibleMethod(method);
if (candidateMethod != null)
return (candidateMethod, 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>
private (IMethodSymbol? method, int parameterIndex) TryFindParameterIndexIfCompatibleMethod(IMethodSymbol method)
{
// map the arguments to their corresponding parameters
using var argumentToParameterMap = TemporaryArray<int>.Empty;
for (var i = 0; i < arguments.Count; i++)
argumentToParameterMap.Add(-1);
if (!TryPrepareArgumentToParameterMap(method, ref argumentToParameterMap.AsRef()))
return (null, -1);
// verify that the arguments are compatible with their corresponding parameters
var parameters = method.Parameters;
for (var argumentIndex = 0; argumentIndex < arguments.Count; argumentIndex++)
{
var parameterIndex = argumentToParameterMap[argumentIndex];
if (parameterIndex < 0)
continue;
var parameter = parameters[parameterIndex];
var argument = arguments[argumentIndex];
// We found a corresponding argument for this parameter. If it's not compatible (say, a string passed
// to an int parameter), then this is not a suitable overload.
if (!IsCompatibleArgument(argument, parameter))
return (null, -1);
}
if (position is null)
return (method, -1);
// find the parameter at the cursor position
var argumentIndexToSave = GetArgumentIndex(position.Value);
var foundParameterIndex = -1;
if (argumentIndexToSave >= 0)
{
foundParameterIndex = argumentToParameterMap[argumentIndexToSave];
if (foundParameterIndex < 0)
foundParameterIndex = FirstUnspecifiedParameter(ref argumentToParameterMap.AsRef());
}
Debug.Assert(foundParameterIndex < parameters.Length);
return (method, foundParameterIndex);
}
/// <summary>
/// Determines if the given argument is compatible with the given parameter
/// </summary>
private bool IsCompatibleArgument(SyntaxNode argument, IParameterSymbol parameter)
{
var parameterRefKind = parameter.RefKind;
if (parameterRefKind == RefKind.None)
{
var expression = this.SyntaxFacts.GetExpressionOfArgument(argument);
if (IsEmptyArgument(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
&& semanticFacts.ClassifyConversion(semanticModel, expression, arrayType.ElementType).IsImplicit)
{
return true;
}
return semanticFacts.ClassifyConversion(semanticModel, expression, type).IsImplicit;
}
var argumentRefKind = this.SyntaxFacts.GetRefKindOfArgument(argument);
if (parameterRefKind == argumentRefKind)
return true;
// A by-value argument matches an `in` parameter
if (parameterRefKind == RefKind.In && argumentRefKind == RefKind.None)
return true;
return false;
}
// If the cursor is pointing at an argument for which we did not find the corresponding
// parameter, we will highlight the first unspecified parameter.
private int FirstUnspecifiedParameter(ref TemporaryArray<int> argumentToParameterMap)
{
using var specified = TemporaryArray<bool>.Empty;
for (var i = 0; i < arguments.Count; i++)
specified.Add(false);
for (var i = 0; i < arguments.Count; i++)
{
var parameterIndex = argumentToParameterMap[i];
if (parameterIndex >= 0 && parameterIndex < arguments.Count)
specified.AsRef()[parameterIndex] = true;
}
for (var i = 0; i < specified.Count; i++)
{
if (!specified[i])
return i;
}
return 0;
}
/// <summary>
/// Find the parameter index corresponding to each argument provided
/// </summary>
private bool TryPrepareArgumentToParameterMap(IMethodSymbol method, ref TemporaryArray<int> argumentToParameterMap)
{
Contract.ThrowIfTrue(argumentToParameterMap.Count != arguments.Count);
var currentParameterIndex = 0;
var seenOutOfPositionArgument = false;
var inParams = false;
for (var argumentIndex = 0; argumentIndex < arguments.Count; argumentIndex++)
{
// Went past the number of parameters this method takes, and this is a non-params method. There's no
// way this could ever match.
if (argumentIndex >= method.Parameters.Length && !inParams)
return false;
var argument = arguments[argumentIndex];
var expression = this.SyntaxFacts.GetExpressionOfArgument(argument);
var argumentName = this.SyntaxFacts.GetNameForArgument(argument);
if (!string.IsNullOrEmpty(argumentName))
{
// If this was a named argument but the method has no parameter with that name, there's definitely
// no match. Note: this is C# only, so we don't need to worry about case matching.
var namedParameterIndex = method.Parameters.IndexOf(p => p.Name == argumentName);
if (namedParameterIndex < 0)
return false;
if (namedParameterIndex != currentParameterIndex)
seenOutOfPositionArgument = true;
AddArgumentToParameterMapping(argumentIndex, namedParameterIndex, ref argumentToParameterMap);
}
else if (IsEmptyArgument(expression))
{
// We count the empty argument as a used position
if (!seenOutOfPositionArgument)
AddArgumentToParameterMapping(argumentIndex, currentParameterIndex, ref argumentToParameterMap);
}
else if (seenOutOfPositionArgument)
{
// Unnamed arguments are not allowed after an out-of-position argument
return false;
}
else
{
// Normal argument.
AddArgumentToParameterMapping(argumentIndex, currentParameterIndex, ref argumentToParameterMap);
}
}
return true;
void AddArgumentToParameterMapping(int argumentIndex, int parameterIndex, ref TemporaryArray<int> argumentToParameterMap)
{
Debug.Assert(parameterIndex >= 0);
Debug.Assert(parameterIndex < method.Parameters.Length);
inParams |= method.Parameters[parameterIndex].IsParams;
argumentToParameterMap[argumentIndex] = parameterIndex;
// Increment our current parameter index if we're still processing parameters in sequential order.
if (!seenOutOfPositionArgument && !inParams)
currentParameterIndex++;
}
}
private static bool IsEmptyArgument(SyntaxNode 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 int GetArgumentIndex(int position)
{
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;
}
}
|