File: Completion\Providers\AbstractContextVariableArgumentProvider.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.Completion;
 
/// <summary>
/// This <see cref="ArgumentProvider"/> attempts to locate a matching value in the context of a method invocation.
/// </summary>
internal abstract class AbstractContextVariableArgumentProvider : ArgumentProvider
{
    protected abstract string ThisOrMeKeyword { get; }
 
    protected abstract bool IsInstanceContext(SyntaxTree syntaxTree, SyntaxToken targetToken, SemanticModel semanticModel, CancellationToken cancellationToken);
 
    public override async Task ProvideArgumentAsync(ArgumentContext context)
    {
        if (context.PreviousValue is not null)
        {
            // This argument provider does not attempt to replace arguments already in code.
            return;
        }
 
        var requireExactType = context.Parameter.Type.IsSpecialType()
            || context.Parameter.RefKind != RefKind.None;
        var symbols = context.SemanticModel.LookupSymbols(context.Position);
 
        // First try to find a local variable
        ISymbol? bestSymbol = null;
        string? bestSymbolName = null;
        CommonConversion bestConversion = default;
        foreach (var symbol in symbols)
        {
            ISymbol candidate;
            if (symbol.IsKind(SymbolKind.Parameter, out IParameterSymbol? parameter))
                candidate = parameter;
            else if (symbol.IsKind(SymbolKind.Local, out ILocalSymbol? local))
                candidate = local;
            else
                continue;
 
            CheckCandidate(candidate);
        }
 
        if (bestSymbol is not null)
        {
            context.DefaultValue = bestSymbolName;
            return;
        }
 
        // Next try fields and properties of the current type
        foreach (var symbol in symbols)
        {
            ISymbol candidate;
            if (symbol.IsKind(SymbolKind.Field, out IFieldSymbol? field))
                candidate = field;
            else if (symbol.IsKind(SymbolKind.Property, out IPropertySymbol? property))
                candidate = property;
            else
                continue;
 
            // Require a name match for primitive types
            if (candidate.GetSymbolType().IsSpecialType()
                && !string.Equals(candidate.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }
 
            CheckCandidate(candidate);
        }
 
        if (bestSymbol is not null)
        {
            context.DefaultValue = bestSymbolName;
            return;
        }
 
        // Finally, if the invocation occurs in an instance context, check the current type ('this' or 'Me')
        var tree = context.SemanticModel.SyntaxTree;
        var targetToken = await tree.GetTouchingTokenAsync(context.Position, context.CancellationToken).ConfigureAwait(false);
        if (IsInstanceContext(tree, targetToken, context.SemanticModel, context.CancellationToken))
        {
            var enclosingSymbol = context.SemanticModel.GetEnclosingSymbol(targetToken.SpanStart, context.CancellationToken);
            while (enclosingSymbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction or MethodKind.AnonymousFunction })
            {
                // It is allowed to reference the instance (`this`) within a local function or anonymous function,
                // as long as the containing method allows it
                enclosingSymbol = enclosingSymbol.ContainingSymbol;
            }
 
            if (enclosingSymbol is IMethodSymbol { ContainingType: { } containingType })
            {
                CheckCandidate(containingType, ThisOrMeKeyword);
            }
        }
 
        if (bestSymbol is not null)
        {
            context.DefaultValue = bestSymbolName;
            return;
        }
 
        // Local functions
        void CheckCandidate(ISymbol candidate, string? overridingName = null)
        {
            if (candidate.GetSymbolType() is not { } symbolType)
            {
                return;
            }
 
            if (requireExactType && !SymbolEqualityComparer.Default.Equals(context.Parameter.Type, symbolType))
            {
                return;
            }
 
            var conversion = context.SemanticModel.Compilation.ClassifyCommonConversion(symbolType, context.Parameter.Type);
            if (!conversion.IsImplicit)
            {
                return;
            }
 
            if (bestSymbol is not null && !IsNewConversionSameOrBetter(conversion))
            {
                if (!IsNewConversionSameOrBetter(conversion))
                    return;
 
                if (!IsNewNameSameOrBetter(candidate))
                    return;
            }
 
            bestSymbol = candidate;
            bestSymbolName = overridingName ?? bestSymbol.Name;
            bestConversion = conversion;
        }
 
        bool IsNewConversionSameOrBetter(CommonConversion conversion)
        {
            if (bestConversion.IsIdentity && !conversion.IsIdentity)
                return false;
 
            if (bestConversion.IsImplicit && !conversion.IsImplicit)
                return false;
 
            return true;
        }
 
        bool IsNewNameSameOrBetter(ISymbol symbol)
        {
            if (string.Equals(bestSymbol.Name, context.Parameter.Name))
                return string.Equals(symbol.Name, context.Parameter.Name);
 
            if (string.Equals(bestSymbol.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase))
                return string.Equals(symbol.Name, context.Parameter.Name, StringComparison.OrdinalIgnoreCase);
 
            return true;
        }
    }
}