File: src\Analyzers\Core\CodeFixes\AddExplicitCast\Fixer.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.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CodeFixes.AddExplicitCast;
 
internal abstract partial class AbstractAddExplicitCastCodeFixProvider<TExpressionSyntax>
{
    protected abstract class Fixer<TArgumentSyntax, TArgumentListSyntax, TInvocationSyntax>
        where TArgumentSyntax : SyntaxNode
        where TArgumentListSyntax : SyntaxNode
        where TInvocationSyntax : SyntaxNode
    {
        protected abstract TExpressionSyntax GetExpressionOfArgument(TArgumentSyntax argument);
        protected abstract TArgumentSyntax GenerateNewArgument(TArgumentSyntax oldArgument, ITypeSymbol conversionType);
        protected abstract TArgumentListSyntax GenerateNewArgumentList(TArgumentListSyntax oldArgumentList, ArrayBuilder<TArgumentSyntax> newArguments);
        protected abstract SeparatedSyntaxList<TArgumentSyntax> GetArgumentsOfArgumentList(TArgumentListSyntax argumentList);
        protected abstract SymbolInfo GetSpeculativeSymbolInfo(SemanticModel semanticModel, TArgumentListSyntax newArgumentList);
 
        /// <summary>
        /// Collect all the available cast pairs, format is (target argument expression, potential conversion type)
        /// </summary>
        /// <param name="targetArgument"> The argument that need to be cast</param>
        /// <param name="argumentList"> The argument list that contains the target argument to be cast </param>
        /// <param name="invocationNode"> The invocation node that is the parent of "argumentList"</param>
        /// <returns>
        /// Return all the available cast pairs, format is (target argument expression, potential conversion type)
        /// </returns>
        public ImmutableArray<(TExpressionSyntax, ITypeSymbol)> GetPotentialConversionTypes(
            Document document,
            SemanticModel semanticModel,
            SyntaxNode root,
            TArgumentSyntax targetArgument,
            TArgumentListSyntax argumentList,
            TInvocationSyntax invocationNode,
            CancellationToken cancellationToken)
        {
            // Implicit downcast appears on the argument of invocation node,
            // get all candidate functions and extract potential conversion types 
            var symbolInfo = semanticModel.GetSymbolInfo(invocationNode, cancellationToken);
            using var _ = ArrayBuilder<ISymbol>.GetInstance(out var candidateSymbols);
            if (symbolInfo.Symbol != null) // BC42016: the only candidate symbol is symbolInfo.Symbol
            {
                candidateSymbols.Add(symbolInfo.Symbol);
            }
            else
            {
                candidateSymbols.AddRange(symbolInfo.CandidateSymbols);
            }
 
            using var __ = ArrayBuilder<(TExpressionSyntax, ITypeSymbol)>.GetInstance(out var mutablePotentialConversionTypes);
            foreach (var candidateSymbol in candidateSymbols.OfType<IMethodSymbol>())
            {
                if (CanArgumentTypesBeConvertedToParameterTypes(
                        document, semanticModel, root, argumentList, candidateSymbol.Parameters,
                        targetArgument, cancellationToken, out var targetArgumentConversionType)
                    && GetExpressionOfArgument(targetArgument) is TExpressionSyntax argumentExpression)
                {
                    mutablePotentialConversionTypes.Add((argumentExpression, targetArgumentConversionType));
                }
            }
 
            // Sort the potential conversion types by inheritance distance, so that
            // operations are in order and user can choose least specific types(more accurate)
            mutablePotentialConversionTypes.Sort(new InheritanceDistanceComparer<TExpressionSyntax>(semanticModel));
 
            return mutablePotentialConversionTypes.ToImmutableAndClear();
        }
 
        /// <summary>
        /// Test if all argument types can be converted to corresponding parameter types.
        /// </summary>
        /// For example:
        /// class Base { }
        /// class Derived1 : Base { }
        /// class Derived2 : Base { }
        /// class Derived3 : Base { }
        /// void DoSomething(int i, Derived1 d) { }
        /// void DoSomething(string s, Derived2 d) { }
        /// void DoSomething(int i, Derived3 d) { }
        /// 
        /// Base b;
        /// DoSomething(1, [||]b);
        ///
        /// *void DoSomething(string s, Derived2 d) { }* is not the perfect match candidate function for
        /// *DoSomething(1, [||]b)* because int and string are not ancestor-descendant relationship. Thus,
        /// Derived2 is not a potential conversion type.
        /// 
        /// <param name="argumentList"> The argument list of invocation expression</param>
        /// <param name="parameters"> The parameters of function</param>
        /// <param name="targetArgument">The argument need to be cast.</param>
        /// <param name="targetArgumentConversionType"> Output the corresponding parameter type of
        /// "targetArgument" if function returns true</param>
        /// <returns>
        /// True, if arguments and parameters match perfectly.
        /// "targetArgumentConversionType" outputs the corresponding parameter type of "targetArgument"
        /// False, otherwise.
        /// </returns>
        public bool CanArgumentTypesBeConvertedToParameterTypes(
            Document document,
            SemanticModel semanticModel,
            SyntaxNode root,
            TArgumentListSyntax argumentList,
            ImmutableArray<IParameterSymbol> parameters,
            TArgumentSyntax targetArgument,
            CancellationToken cancellationToken,
            [NotNullWhen(true)] out ITypeSymbol? targetArgumentConversionType)
        {
            targetArgumentConversionType = null;
 
            // No conversion happens under this case
            if (parameters.Length == 0)
                return false;
 
            var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
            var semanticFacts = document.GetRequiredLanguageService<ISemanticFactsService>();
 
            var arguments = GetArgumentsOfArgumentList(argumentList);
            using var _ = ArrayBuilder<TArgumentSyntax>.GetInstance(out var newArguments);
 
            for (var i = 0; i < arguments.Count; i++)
            {
                // Parameter index cannot out of its range, #arguments is larger than #parameter only if 
                // the last parameter with keyword params
                var parameterIndex = Math.Min(i, parameters.Length - 1);
 
                // If the argument has a name, get the corresponding parameter index
                if (syntaxFacts.GetNameForArgument(arguments[i]) is string name
                    && name != string.Empty
                    && !FindCorrespondingParameterByName(name, parameters, ref parameterIndex))
                {
                    return false;
                }
 
                // The argument is either in order with parameters, or have a matched name with parameters.
                var argumentExpression = GetExpressionOfArgument(arguments[i]);
                if (argumentExpression == null)
                {
                    // argumentExpression is null when it is an omitted argument in VB .NET
                    newArguments.Add(arguments[i]);
                    continue;
                }
 
                var parameterType = parameters[parameterIndex].Type;
                if (parameters[parameterIndex].IsParams
                    && parameterType is IArrayTypeSymbol paramsType
                    && semanticFacts.ClassifyConversion(semanticModel, argumentExpression, paramsType.ElementType).Exists)
                {
                    newArguments.Add(GenerateNewArgument(arguments[i], paramsType.ElementType));
                    if (arguments[i].Equals(targetArgument))
                        targetArgumentConversionType = paramsType.ElementType;
                }
                else if (semanticFacts.ClassifyConversion(semanticModel, argumentExpression, parameterType).Exists)
                {
                    newArguments.Add(GenerateNewArgument(arguments[i], parameterType));
                    if (arguments[i].Equals(targetArgument))
                        targetArgumentConversionType = parameterType;
                }
                else if (syntaxFacts.IsDeclarationExpression(argumentExpression)
                    && semanticModel.GetTypeInfo(argumentExpression, cancellationToken).Type is ITypeSymbol argumentType
                    && semanticModel.Compilation.ClassifyCommonConversion(argumentType, parameterType).IsIdentity)
                {
                    // Direct conversion from a declaration expression to a type is unspecified, thus we classify the
                    // conversion from the type of declaration expression to the parameter type
                    // An example for this case:
                    // void Goo(out int i) { i = 1; }
                    // Goo([|out var i|]);
                    // "var i" is a declaration expression
                    // 
                    // In addition, since this case is with keyword "out", the type of declaration expression and the
                    // parameter type must be identical in order to match.
                    newArguments.Add(arguments[i]);
                }
                else
                {
                    return false;
                }
            }
 
            return targetArgumentConversionType != null
                && IsInvocationExpressionWithNewArgumentsApplicable(
                    semanticModel, root, argumentList, newArguments, targetArgument);
        }
 
        /// <summary>
        /// Check whether the invocation expression with new arguments is applicable.
        /// </summary>
        /// <param name="oldArgumentList" >old argumentList node</param>
        /// <param name="newArguments"> new arguments that are cast by corresponding parameter types</param>
        /// <param name="targetNode"> The node needs to be cast.</param>
        /// <returns>
        /// Return true if the invocation expression with new arguments is applicable.
        /// Otherwise, return false
        /// </returns>
        private bool IsInvocationExpressionWithNewArgumentsApplicable(SemanticModel semanticModel,
            SyntaxNode root,
            TArgumentListSyntax oldArgumentList,
            ArrayBuilder<TArgumentSyntax> newArguments,
            SyntaxNode targetNode)
        {
            var newRoot = root.ReplaceNode(oldArgumentList, GenerateNewArgumentList(oldArgumentList, newArguments));
            if (newRoot.FindNode(targetNode.Span).GetAncestorOrThis<TArgumentListSyntax>() is TArgumentListSyntax newArgumentList)
            {
                var symbolInfo = GetSpeculativeSymbolInfo(semanticModel, newArgumentList);
                return symbolInfo.Symbol != null;
            }
 
            return false;
        }
    }
}