File: Microsoft.NetCore.Analyzers\Performance\DoNotUseCountWhenAnyCanBeUsed.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.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.Formatting;
using Microsoft.CodeAnalysis.NetAnalyzers;

namespace Microsoft.NetCore.Analyzers.Performance
{
    /// <summary>
    /// CA1827: Do not use Count()/LongCount() when Any() can be used.
    /// CA1828: Do not use CountAsync()/LongCountAsync() when AnyAsync() can be used.
    /// </summary>
    public abstract class DoNotUseCountWhenAnyCanBeUsedFixer : CodeFixProvider
    {
        private const string AsyncMethodName = "AnyAsync";
        private const string SyncMethodName = "Any";

        /// <summary>
        /// A list of diagnostic IDs that this provider can provider fixes for.
        /// </summary>
        /// <value>The fixable diagnostic ids.</value>
        public override ImmutableArray<string> FixableDiagnosticIds { get; } =
            ImmutableArray.Create(
                UseCountProperlyAnalyzer.CA1827,
                UseCountProperlyAnalyzer.CA1828);

        /// <summary>
        /// Gets an optional <see cref="FixAllProvider" /> that can fix all/multiple occurrences of diagnostics fixed by this code fix provider.
        /// </summary>
        /// <returns>FixAllProvider.</returns>
        /// <remarks>
        /// The synchronous and asynchronous fixes carry different equivalence keys, so this filters on the key
        /// itself -- <see cref="SyntaxEditorFixAllProvider"/> does not.
        /// </remarks>
        public sealed override FixAllProvider GetFixAllProvider()
            => SyntaxEditorFixAllProvider.Create<string?>(context => context.CodeActionEquivalenceKey, ApplyFixAsync);

        /// <summary>
        /// Computes one or more fixes for the specified <see cref="CodeFixContext" />.
        /// </summary>
        /// <param name="context">A <see cref="CodeFixContext" /> containing context information about the diagnostics to fix.
        /// The context must only contain diagnostics with a <see cref="Diagnostic.Id" /> included in the <see cref="CodeFixProvider.FixableDiagnosticIds" /> 
        /// for the current provider.</param>
        /// <returns>A <see cref="Task" /> that represents the asynchronous operation.</returns>
        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
            var node = root.FindNode(context.Span);
            var diagnostic = context.Diagnostics[0];
            var isAsync = IsAsync(diagnostic);

            if (node is object &&
                diagnostic.Properties.TryGetValue(UseCountProperlyAnalyzer.OperationKey, out var operation) &&
                this.TryGetFixer(node, operation!, isAsync, out _, out _))
            {
                var document = context.Document;
                var diagnostics = context.Diagnostics;
                var title = GetTitle(isAsync);

                context.RegisterCodeFix(
                    CodeAction.Create(
                        title,
                        ct => SyntaxEditorFixAllProvider.ApplyFixesAsync(document, diagnostics, (doc, diag, editor, token) => ApplyFixAsync(doc, diag, editor, title, token), ct),
                        title),
                    diagnostics);
            }
        }

        private Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, string? equivalenceKey, CancellationToken cancellationToken)
        {
            var isAsync = IsAsync(diagnostic);

            if (equivalenceKey is not null && equivalenceKey != GetTitle(isAsync))
            {
                return Task.CompletedTask;
            }

            var pattern = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan);

            if (pattern is null ||
                !diagnostic.Properties.TryGetValue(UseCountProperlyAnalyzer.OperationKey, out var operation) ||
                !this.TryGetFixer(pattern, operation!, isAsync, out var expression, out var arguments))
            {
                return Task.CompletedTask;
            }

            var shouldNegate = diagnostic.Properties.ContainsKey(UseCountProperlyAnalyzer.ShouldNegateKey);
            var carriedOver = new List<SyntaxNode>(arguments) { expression };

            //  The replacement is built out of the reported node's own descendants, so track them: a nested
            //  violation may already have been rewritten by the time this fix runs.
            foreach (var node in carriedOver)
            {
                editor.TrackNode(node);
            }

            editor.ReplaceNode(pattern, (currentNode, generator) =>
            {
                SyntaxNode Current(SyntaxNode original) => currentNode.GetCurrentNode(original) ?? original;

                var memberAccess = generator.MemberAccessExpression(Current(expression).WithoutTrailingTrivia(), isAsync ? AsyncMethodName : SyncMethodName);
                var replacementSyntax = generator.InvocationExpression(memberAccess, arguments.Select(Current));

                if (isAsync)
                {
                    replacementSyntax = generator.AwaitExpression(replacementSyntax);
                }

                if (shouldNegate)
                {
                    replacementSyntax = generator.LogicalNotExpression(replacementSyntax);
                }

                return replacementSyntax
                    .WithAdditionalAnnotations(Formatter.Annotation)
                    .WithTriviaFrom(currentNode);
            });

            return Task.CompletedTask;
        }

        private static bool IsAsync(Diagnostic diagnostic)
            => diagnostic.Properties.ContainsKey(UseCountProperlyAnalyzer.IsAsyncKey) ||
               diagnostic.Id == UseCountProperlyAnalyzer.CA1828;

        private static string GetTitle(bool isAsync)
            => isAsync ?
                MicrosoftNetCoreAnalyzersResources.DoNotUseCountAsyncWhenAnyAsyncCanBeUsedTitle :
                MicrosoftNetCoreAnalyzersResources.DoNotUseCountWhenAnyCanBeUsedTitle;

        /// <summary>
        /// Tries to get a fixer for the specified <paramref name="node" />.
        /// </summary>
        /// <param name="node">The node to get a fixer for.</param>
        /// <param name="operation">The operation to get the fixer from.</param>
        /// <param name="isAsync"><see langword="true" /> if it's an asynchronous method; <see langword="false" /> otherwise.</param>
        /// <param name="expression">If this method returns <see langword="true" />, contains the expression to be used to invoke <c>Any</c>.</param>
        /// <param name="arguments">If this method returns <see langword="true" />, contains the arguments from <c>Any</c> to be used on <c>Count</c>.</param>
        /// <returns><see langword="true" /> if a fixer was found., <see langword="false" /> otherwise.</returns>
        protected abstract bool TryGetFixer(
            SyntaxNode node,
            string operation,
            bool isAsync,
            [NotNullWhen(returnValue: true)] out SyntaxNode? expression,
            [NotNullWhen(returnValue: true)] out IEnumerable<SyntaxNode>? arguments);
    }
}