File: Microsoft.NetCore.Analyzers\Runtime\ForwardCancellationTokenToInvocations.Fixer.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.NetAnalyzers;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    public abstract class ForwardCancellationTokenToInvocationsFixer<TArgumentSyntax> : SyntaxEditorBasedCodeFixProvider
        where TArgumentSyntax : SyntaxNode
    {
        // Attempts to retrieve the invocation from the current operation.
        protected abstract bool TryGetInvocation(
            SemanticModel model,
            SyntaxNode node,
            CancellationToken ct,
            [NotNullWhen(returnValue: true)] out IInvocationOperation? invocation);

        // Retrieves the invocation expression node and the invocation argument list
        protected abstract bool TryGetExpressionAndArguments(
            SyntaxNode invocationNode,
            [NotNullWhen(returnValue: true)] out SyntaxNode? expression,
            out ImmutableArray<TArgumentSyntax> arguments);

        // Verifies if the specified argument was passed with an explicit name.
        protected abstract bool IsArgumentNamed(IArgumentOperation argumentOperation);

        // Retrieves the invocation expression for a conditional operation, which consists of the dot and the method name.
        protected abstract SyntaxNode GetConditionalOperationInvocationExpression(SyntaxNode invocationNode);

        protected abstract SyntaxNode GetTypeSyntaxForArray(IArrayTypeSymbol type);
        protected abstract IEnumerable<SyntaxNode> GetExpressions(ImmutableArray<TArgumentSyntax> newArguments);
        protected abstract SyntaxNode GetArrayCreationExpression(SyntaxGenerator generator, SyntaxNode typeSyntax, IEnumerable<SyntaxNode> expressions);

        public override ImmutableArray<string> FixableDiagnosticIds { get; } =
            ImmutableArray.Create(ForwardCancellationTokenToInvocationsAnalyzer.RuleId);

        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            Document doc = context.Document;
            CancellationToken ct = context.CancellationToken;
            SyntaxNode root = await doc.GetRequiredSyntaxRootAsync(ct).ConfigureAwait(false);
            SemanticModel model = await doc.GetRequiredSemanticModelAsync(ct).ConfigureAwait(false);

            if (!TryGetFix(model, root, context.Diagnostics[0], ct, out _))
            {
                return;
            }

            RegisterCodeFix(context,
                MicrosoftNetCoreAnalyzersResources.ForwardCancellationTokenToInvocationsTitle,
                nameof(MicrosoftNetCoreAnalyzersResources.ForwardCancellationTokenToInvocationsTitle));
        }

        protected sealed override async Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, CancellationToken cancellationToken)
        {
            SemanticModel model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

            if (!TryGetFix(model, editor.OriginalRoot, diagnostic, cancellationToken, out Fix fix))
            {
                return;
            }

            editor.TrackNode(fix.Expression);
            foreach (TArgumentSyntax argument in fix.Arguments)
            {
                editor.TrackNode(argument);
            }

            // An argument can itself be a diagnosed invocation, so the invocation is rebuilt from the
            // arguments as the inner fixes left them rather than from the original tree.
            editor.ReplaceNode(fix.Invocation.Syntax, (currentNode, generator) =>
            {
                SyntaxNode expression = currentNode.GetCurrentNode(fix.Expression) ?? fix.Expression;

                ImmutableArray<TArgumentSyntax>.Builder currentArguments = ImmutableArray.CreateBuilder<TArgumentSyntax>(fix.Arguments.Length);
                foreach (TArgumentSyntax argument in fix.Arguments)
                {
                    currentArguments.Add((TArgumentSyntax)(currentNode.GetCurrentNode(argument) ?? argument));
                }

                return GenerateInvocation(generator, fix, expression, currentArguments.MoveToImmutable()).WithTriviaFrom(currentNode);
            });
        }

        private bool TryGetFix(SemanticModel model, SyntaxNode root, Diagnostic diagnostic, CancellationToken cancellationToken, out Fix fix)
        {
            fix = default;

            if (root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not SyntaxNode node)
            {
                return false;
            }

            // The analyzer created the diagnostic on the IdentifierNameSyntax, and the parent is the actual invocation
            if (!TryGetInvocation(model, node, cancellationToken, out IInvocationOperation? invocation))
            {
                return false;
            }

            ImmutableDictionary<string, string?> properties = diagnostic.Properties;

            if (!properties.TryGetValue(ForwardCancellationTokenToInvocationsAnalyzer.ShouldFix, out var shouldFix) ||
                string.IsNullOrEmpty(shouldFix) ||
                shouldFix!.Equals("0", StringComparison.InvariantCultureIgnoreCase))
            {
                return false;
            }

            // The name that identifies the object that is to be passed
            if (!properties.TryGetValue(ForwardCancellationTokenToInvocationsAnalyzer.ArgumentName, out var argumentName) ||
                string.IsNullOrEmpty(argumentName))
            {
                return false;
            }

            // If the invocation requires the token to be passed with a name, use this
            if (!properties.TryGetValue(ForwardCancellationTokenToInvocationsAnalyzer.ParameterName, out var parameterName))
            {
                return false;
            }

            if (!TryGetExpressionAndArguments(invocation.Syntax, out SyntaxNode? expression, out ImmutableArray<TArgumentSyntax> arguments))
            {
                return false;
            }

            var paramsArrayType = invocation.Arguments.SingleOrDefault(a => a.ArgumentKind == ArgumentKind.ParamArray)?.Value.Type as IArrayTypeSymbol;

            fix = new Fix(invocation, expression, arguments, argumentName!, parameterName!, paramsArrayType);
            return true;
        }

        private SyntaxNode GenerateInvocation(SyntaxGenerator generator, in Fix fix, SyntaxNode expression, ImmutableArray<TArgumentSyntax> currentArguments)
        {
            ImmutableArray<SyntaxNode> newArguments;
            if (fix.ParamsArrayType is not null)
            {
                // current callsite is a params array, we need to wrap all these arguments to preserve semantics
                var typeSyntax = GetTypeSyntaxForArray(fix.ParamsArrayType);
                var expressions = GetExpressions(currentArguments);
                newArguments = ImmutableArray.Create(GetArrayCreationExpression(generator, typeSyntax, expressions));
            }
            else
            {
                // not a params array just pass the existing arguments along
                newArguments = currentArguments.CastArray<SyntaxNode>();
            }

            SyntaxNode identifier = generator.IdentifierName(fix.ArgumentName);
            SyntaxNode cancellationTokenArgument;
            if (!string.IsNullOrEmpty(fix.ParameterName))
            {
                cancellationTokenArgument = generator.Argument(fix.ParameterName, RefKind.None, identifier);
            }
            else
            {
                cancellationTokenArgument = generator.Argument(identifier);
            }

            newArguments = newArguments.Add(cancellationTokenArgument);

            return generator.InvocationExpression(expression, newArguments);
        }

        private readonly struct Fix
        {
            public Fix(IInvocationOperation invocation, SyntaxNode expression, ImmutableArray<TArgumentSyntax> arguments,
                string argumentName, string parameterName, IArrayTypeSymbol? paramsArrayType)
            {
                Invocation = invocation;
                Expression = expression;
                Arguments = arguments;
                ArgumentName = argumentName;
                ParameterName = parameterName;
                ParamsArrayType = paramsArrayType;
            }

            public IInvocationOperation Invocation { get; }

            public SyntaxNode Expression { get; }

            public ImmutableArray<TArgumentSyntax> Arguments { get; }

            /// <summary>The name of the token to forward.</summary>
            public string ArgumentName { get; }

            /// <summary>The parameter to name the forwarded token after, or empty to pass it positionally.</summary>
            public string ParameterName { get; }

            public IArrayTypeSymbol? ParamsArrayType { get; }
        }
    }
}