File: Microsoft.NetCore.Analyzers\Runtime\DoNotUseEnumerableMethodsOnIndexableCollectionsInsteadUseTheCollectionDirectly.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.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    /// <summary>
    /// CA1826: Use property instead of Linq Enumerable method
    /// </summary>
    public abstract class DoNotUseEnumerableMethodsOnIndexableCollectionsInsteadUseTheCollectionDirectlyFixer : CodeFixProvider
    {
        private const string FirstPropertyName = "First";
        private const string LastPropertyName = "Last";
        private const string CountPropertyName = "Count";

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

        public sealed override FixAllProvider GetFixAllProvider()
        {
            // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
            return WellKnownFixAllProviders.BatchFixer;
        }

        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var diagnostic = context.Diagnostics[0];

            var methodPropertyKey = DoNotUseEnumerableMethodsOnIndexableCollectionsInsteadUseTheCollectionDirectlyAnalyzer.MethodPropertyKey;
            // The fixer is only implemented for "Enumerable.First", "Enumerable.Last" and "Enumerable.Count"
            if (!diagnostic.Properties.TryGetValue(methodPropertyKey, out var method)
                || (method != FirstPropertyName && method != LastPropertyName && method != CountPropertyName))
            {
                return;
            }

            var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
            var invocationNode = root.FindNode(context.Span, getInnermostNodeForTie: true);
            if (invocationNode == null)
            {
                return;
            }

            var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
            if (semanticModel.GetOperation(invocationNode, context.CancellationToken) is not IInvocationOperation invocationOperation)
            {
                return;
            }

            var collectionSyntax = invocationOperation.GetInstanceSyntax();
            if (collectionSyntax == null)
            {
                return;
            }

            // Last and Count code fix need the Count property so we want to ensure it exists before registration
            if (method is LastPropertyName or CountPropertyName)
            {
                var typeSymbol = semanticModel.GetTypeInfo(collectionSyntax).Type;
                if (!typeSymbol.HasAnyCollectionCountProperty(WellKnownTypeProvider.GetOrCreate(semanticModel.Compilation)))
                {
                    return;
                }
            }

            var title = MicrosoftNetCoreAnalyzersResources.UseIndexer;

            context.RegisterCodeFix(CodeAction.Create(title,
                                        ct => UseCollectionDirectlyAsync(context.Document, root, invocationNode, collectionSyntax, method),
                                        equivalenceKey: title),
                                    diagnostic);
        }

        private Task<Document> UseCollectionDirectlyAsync(Document document, SyntaxNode root, SyntaxNode invocationNode, SyntaxNode collectionSyntax, string methodName)
        {
            var generator = SyntaxGenerator.GetGenerator(document);

            var elementAccessNode = GetReplacementNode(methodName, generator, collectionSyntax);
            if (elementAccessNode == null)
            {
                return Task.FromResult(document);
            }

            var newRoot = root.ReplaceNode(invocationNode, elementAccessNode.WithTrailingTrivia(invocationNode.GetTrailingTrivia()));
            return Task.FromResult(document.WithSyntaxRoot(newRoot));
        }

        private SyntaxNode? GetReplacementNode(string methodName, SyntaxGenerator generator, SyntaxNode collectionSyntax)
        {
            var adjustedCollectionSyntax = AdjustSyntaxNode(collectionSyntax);
            var adjustedCollectionSyntaxNoTrailingTrivia = adjustedCollectionSyntax.WithoutTrailingTrivia();

            if (methodName == FirstPropertyName)
            {
                var zeroLiteral = generator.LiteralExpression(0);
                return generator.ElementAccessExpression(adjustedCollectionSyntaxNoTrailingTrivia, zeroLiteral);
            }

            if (methodName == LastPropertyName)
            {
                // TODO: Handle C# 8 index expression (and vb.net equivalent if any)

                // TODO: Handle cases were `collectionSyntax` is an invocation. We would need to create some intermediate variable.
                var countMemberAccess = generator.MemberAccessExpression(collectionSyntax.WithoutTrailingTrivia(), CountPropertyName);
                var oneLiteral = generator.LiteralExpression(1);

                // The SubstractExpression method will wrap left and right in parenthesis but those will be automatically removed later on
                var substraction = generator.SubtractExpression(countMemberAccess, oneLiteral);
                return generator.ElementAccessExpression(adjustedCollectionSyntaxNoTrailingTrivia, substraction);
            }

            if (methodName == CountPropertyName)
            {
                return generator.MemberAccessExpression(adjustedCollectionSyntaxNoTrailingTrivia, CountPropertyName);
            }

            Debug.Fail($"Unexpected method name '{methodName}' for {DoNotUseEnumerableMethodsOnIndexableCollectionsInsteadUseTheCollectionDirectlyAnalyzer.RuleId} code fix.");
            return null;
        }

        [return: NotNullIfNotNull(nameof(syntaxNode))]
        private protected abstract SyntaxNode? AdjustSyntaxNode(SyntaxNode? syntaxNode);
    }
}