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.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    public abstract class ForwardCancellationTokenToInvocationsFixer<TArgumentSyntax> : CodeFixProvider
        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 FixAllProvider GetFixAllProvider() =>
            WellKnownFixAllProviders.BatchFixer;

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

            if (root.FindNode(context.Span, getInnermostNodeForTie: true) is not SyntaxNode node)
            {
                return;
            }

            SemanticModel model = await doc.GetRequiredSemanticModelAsync(ct).ConfigureAwait(false);

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

            ImmutableDictionary<string, string?>? properties = context.Diagnostics[0].Properties;

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

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

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

            string title = MicrosoftNetCoreAnalyzersResources.ForwardCancellationTokenToInvocationsTitle;

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

            var paramsArrayType = invocation.Arguments.SingleOrDefault(a => a.ArgumentKind == ArgumentKind.ParamArray)?.Value.Type as IArrayTypeSymbol;
            Task<Document> CreateChangedDocumentAsync(CancellationToken _)
            {
                SyntaxNode newRoot = TryGenerateNewDocumentRoot(doc, root, invocation, argumentName!, parameterName!, expression, newArguments, paramsArrayType);
                Document newDocument = doc.WithSyntaxRoot(newRoot);
                return Task.FromResult(newDocument);
            }

            context.RegisterCodeFix(
                CodeAction.Create(
                    title: title,
                    CreateChangedDocumentAsync,
                    equivalenceKey: title),
                context.Diagnostics);
        }

        private SyntaxNode TryGenerateNewDocumentRoot(
            Document doc,
            SyntaxNode root,
            IInvocationOperation invocation,
            string invocationTokenArgumentName,
            string ancestorTokenParameterName,
            SyntaxNode expression,
            ImmutableArray<TArgumentSyntax> currentArguments,
            IArrayTypeSymbol? paramsArrayType)
        {
            SyntaxGenerator generator = SyntaxGenerator.GetGenerator(doc);

            ImmutableArray<SyntaxNode> newArguments;
            if (paramsArrayType is not null)
            {
                // current callsite is a params array, we need to wrap all these arguments to preserve semantics
                var typeSyntax = GetTypeSyntaxForArray(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(invocationTokenArgumentName);
            SyntaxNode cancellationTokenArgument;
            if (!string.IsNullOrEmpty(ancestorTokenParameterName))
            {
                cancellationTokenArgument = generator.Argument(ancestorTokenParameterName, RefKind.None, identifier);
            }
            else
            {
                cancellationTokenArgument = generator.Argument(identifier);
            }

            newArguments = newArguments.Add(cancellationTokenArgument);

            // Insert the new arguments to the new invocation
            SyntaxNode newInvocationWithArguments = generator.InvocationExpression(expression, newArguments).WithTriviaFrom(invocation.Syntax);

            return generator.ReplaceNode(root, invocation.Syntax, newInvocationWithArguments);
        }
    }
}