|
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.InlineHints;
internal abstract class AbstractInlineParameterNameHintsService : IInlineParameterNameHintsService
{
protected enum HintKind
{
Literal,
ObjectCreation,
Other
}
protected abstract void AddAllParameterNameHintLocations(
SemanticModel semanticModel,
ISyntaxFactsService syntaxFacts,
SyntaxNode node,
ArrayBuilder<(int position, SyntaxNode argument, IParameterSymbol? parameter, HintKind kind)> buffer,
CancellationToken cancellationToken);
protected abstract bool IsIndexer(SyntaxNode node, IParameterSymbol parameter);
protected abstract string GetReplacementText(string parameterName);
public async Task<ImmutableArray<InlineHint>> GetInlineHintsAsync(
Document document,
TextSpan textSpan,
InlineParameterHintsOptions options,
SymbolDescriptionOptions displayOptions,
bool displayAllOverride,
CancellationToken cancellationToken)
{
var enabledForParameters = displayAllOverride || options.EnabledForParameters;
if (!enabledForParameters)
return [];
var literalParameters = displayAllOverride || options.ForLiteralParameters;
var objectCreationParameters = displayAllOverride || options.ForObjectCreationParameters;
var otherParameters = displayAllOverride || options.ForOtherParameters;
if (!literalParameters && !objectCreationParameters && !otherParameters)
return [];
var indexerParameters = displayAllOverride || options.ForIndexerParameters;
var suppressForParametersThatDifferOnlyBySuffix = !displayAllOverride && options.SuppressForParametersThatDifferOnlyBySuffix;
var suppressForParametersThatMatchMethodIntent = !displayAllOverride && options.SuppressForParametersThatMatchMethodIntent;
var suppressForParametersThatMatchArgumentName = !displayAllOverride && options.SuppressForParametersThatMatchArgumentName;
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
using var _1 = ArrayBuilder<InlineHint>.GetInstance(out var result);
using var _2 = ArrayBuilder<(int position, SyntaxNode argument, IParameterSymbol? parameter, HintKind kind)>.GetInstance(out var buffer);
foreach (var node in root.DescendantNodes(textSpan, n => n.Span.IntersectsWith(textSpan)))
{
cancellationToken.ThrowIfCancellationRequested();
AddAllParameterNameHintLocations(semanticModel, syntaxFacts, node, buffer, cancellationToken);
if (buffer.Count > 0)
{
AddHintsIfAppropriate(node);
buffer.Clear();
}
}
return result.ToImmutableAndClear();
void AddHintsIfAppropriate(SyntaxNode node)
{
if (suppressForParametersThatDifferOnlyBySuffix && ParametersDifferOnlyBySuffix(buffer))
return;
foreach (var (position, argument, parameter, kind) in buffer)
{
// We get hints on *nodes* that intersect the passed in text span. However, while the full node may
// intersect the span, the positions of the all the sub-nodes in it that we make hints for (like the
// positions of the arguments in an invocation) may not. So, filter out any hints that aren't actually
// in the span we care about here.
if (!textSpan.IntersectsWith(position))
continue;
if (string.IsNullOrEmpty(parameter?.Name))
continue;
if (suppressForParametersThatMatchMethodIntent && MatchesMethodIntent(parameter))
continue;
if (suppressForParametersThatMatchArgumentName && ParameterMatchesArgumentName(argument, parameter, syntaxFacts))
continue;
if (!indexerParameters && IsIndexer(node, parameter))
continue;
if (HintMatches(kind, literalParameters, objectCreationParameters, otherParameters))
{
var inlineHintText = GetReplacementText(parameter.Name);
var textSpan = new TextSpan(position, 0);
TextChange? replacementTextChange = null;
if (!parameter.IsParams)
{
replacementTextChange = new TextChange(textSpan, inlineHintText);
}
result.Add(new InlineHint(
textSpan,
[new TaggedText(TextTags.Text, parameter.Name + ": ")],
replacementTextChange,
ranking: InlineHintsConstants.ParameterRanking,
InlineHintHelpers.GetDescriptionFunction(position, parameter.GetSymbolKey(cancellationToken: cancellationToken), displayOptions)));
}
}
}
}
private static bool ParametersDifferOnlyBySuffix(
ArrayBuilder<(int position, SyntaxNode argument, IParameterSymbol? parameter, HintKind kind)> parameterHints)
{
// Only relevant if we have two or more parameters.
if (parameterHints.Count <= 1)
return false;
return ParametersDifferOnlyByAlphaSuffix(parameterHints) ||
ParametersDifferOnlyByNumericSuffix(parameterHints);
static bool ParametersDifferOnlyByAlphaSuffix(
ArrayBuilder<(int position, SyntaxNode argument, IParameterSymbol? parameter, HintKind kind)> parameterHints)
{
if (!HasAlphaSuffix(parameterHints[0].parameter, out var firstPrefix))
return false;
for (var i = 1; i < parameterHints.Count; i++)
{
if (!HasAlphaSuffix(parameterHints[i].parameter, out var nextPrefix))
return false;
if (!firstPrefix.Span.Equals(nextPrefix.Span, StringComparison.Ordinal))
return false;
}
return true;
}
static bool ParametersDifferOnlyByNumericSuffix(
ArrayBuilder<(int position, SyntaxNode argument, IParameterSymbol? parameter, HintKind kind)> parameterHints)
{
if (!HasNumericSuffix(parameterHints[0].parameter, out var firstPrefix))
return false;
for (var i = 1; i < parameterHints.Count; i++)
{
if (!HasNumericSuffix(parameterHints[i].parameter, out var nextPrefix))
return false;
if (!firstPrefix.Span.Equals(nextPrefix.Span, StringComparison.Ordinal))
return false;
}
return true;
}
static bool HasAlphaSuffix(IParameterSymbol? parameter, out ReadOnlyMemory<char> prefix)
{
var name = parameter?.Name;
// Has to end with A-Z
// That A-Z can't be following another A-Z (that's just a capitalized word).
if (name?.Length >= 2 &&
IsUpperAlpha(name[^1]) &&
!IsUpperAlpha(name[^2]))
{
prefix = name.AsMemory()[..^1];
return true;
}
prefix = default;
return false;
}
static bool HasNumericSuffix(IParameterSymbol? parameter, out ReadOnlyMemory<char> prefix)
{
var name = parameter?.Name;
// Has to end with 0-9. only handles single-digit numeric suffix for now for simplicity
if (name?.Length >= 2 &&
IsNumeric(name[^1]))
{
prefix = name.AsMemory()[..^1];
return true;
}
prefix = default;
return false;
}
static bool IsUpperAlpha(char c)
=> c is >= 'A' and <= 'Z';
static bool IsNumeric(char c)
=> c is >= '0' and <= '9';
}
private static bool HintMatches(HintKind kind, bool literalParameters, bool objectCreationParameters, bool otherParameters)
{
return kind switch
{
HintKind.Literal => literalParameters,
HintKind.ObjectCreation => objectCreationParameters,
HintKind.Other => otherParameters,
_ => throw ExceptionUtilities.UnexpectedValue(kind),
};
}
protected static bool MatchesMethodIntent(IParameterSymbol? parameter)
{
// Methods like `SetColor(color: "y")` `FromResult(result: "x")` `Enable/DisablePolling(bool)` don't need
// parameter names to improve clarity. The parameter is clear from the context of the method name.
// First, this only applies to methods/local functions (as we're looking at the method name itself) so filter down to those.
if (parameter is not { ContainingSymbol: IMethodSymbol { MethodKind: MethodKind.Ordinary or MethodKind.LocalFunction } method })
return false;
// We only care when dealing with the first parameter. Note: we don't have to worry parameter reordering
// due to named-parameter use. That's because this entire feature only works when we don't use
// named-parameters. So, by definition, the parameter/arg must be in the right location.
if (method.Parameters[0] != parameter)
return false;
var methodName = method.Name;
// Check for something like `EnableLogging(true)`
if (TryGetSuffix("Enable", methodName, out _) ||
TryGetSuffix("Disable", methodName, out _))
{
return parameter.Type.SpecialType == SpecialType.System_Boolean;
}
// More names can be added here if we find other patterns like this.
if (TryGetSuffix("Set", methodName, out var suffix) ||
TryGetSuffix("From", methodName, out suffix))
{
return SuffixMatchesParameterName(suffix, parameter.Name);
}
return false;
static bool TryGetSuffix(string prefix, string nameValue, out ReadOnlyMemory<char> suffix)
{
if (nameValue.Length > prefix.Length &&
nameValue.StartsWith(prefix) &&
char.IsUpper(nameValue[prefix.Length]))
{
suffix = nameValue.AsMemory()[prefix.Length..];
return true;
}
suffix = default;
return false;
}
static bool SuffixMatchesParameterName(ReadOnlyMemory<char> suffix, string parameterName)
{
// Method's name will be something like 'FromResult', so 'suffix' will be 'Result' and parameterName
// will be 'result'. So we check if the first letters differ on case and the rest of the method
// matches.
return char.ToLower(suffix.Span[0]) == parameterName[0] &&
suffix.Span[1..].Equals(parameterName.AsSpan()[1..], StringComparison.Ordinal);
}
}
private static bool ParameterMatchesArgumentName(SyntaxNode argument, IParameterSymbol parameter, ISyntaxFactsService syntaxFacts)
{
var argumentName = GetIdentifierNameFromArgument(argument, syntaxFacts);
return syntaxFacts.StringComparer.Compare(parameter.Name, argumentName) == 0;
}
protected static string GetIdentifierNameFromArgument(SyntaxNode argument, ISyntaxFactsService syntaxFacts)
{
var identifierNameSyntax =
syntaxFacts.IsArgument(argument) ? syntaxFacts.GetExpressionOfArgument(argument) :
syntaxFacts.IsAttributeArgument(argument) ? syntaxFacts.GetExpressionOfAttributeArgument(argument) : null;
if (syntaxFacts.IsMemberAccessExpression(identifierNameSyntax))
{
identifierNameSyntax = syntaxFacts.GetNameOfMemberAccessExpression(identifierNameSyntax);
}
if (!syntaxFacts.IsIdentifierName(identifierNameSyntax))
return string.Empty;
var identifier = syntaxFacts.GetIdentifierOfIdentifierName(identifierNameSyntax);
return identifier.ValueText;
}
}
|