File: Microsoft.NetCore.Analyzers\Performance\UseStartsWithInsteadOfIndexOfComparisonWithZero.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.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.NetAnalyzers;

namespace Microsoft.NetCore.Analyzers.Performance
{
    public abstract class UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFix : SyntaxEditorBasedCodeFixProvider
    {
        public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(UseStartsWithInsteadOfIndexOfComparisonWithZero.RuleId);

        public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            RegisterCodeFix(
                context,
                MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFixTitle,
                nameof(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFixTitle));

            return Task.CompletedTask;
        }

        protected sealed override Task ApplyFixAsync(Document document, Diagnostic diagnostic, SyntaxEditor editor, CancellationToken cancellationToken)
        {
            SyntaxNode node = editor.OriginalRoot.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);

            bool shouldNegate = diagnostic.Properties.ContainsKey(UseStartsWithInsteadOfIndexOfComparisonWithZero.ShouldNegateKey);
            bool compilationHasStartsWithCharOverload = diagnostic.Properties.ContainsKey(UseStartsWithInsteadOfIndexOfComparisonWithZero.CompilationHasStartsWithCharOverloadKey);
            _ = diagnostic.Properties.TryGetValue(UseStartsWithInsteadOfIndexOfComparisonWithZero.ExistingOverloadKey, out string? overloadValue);

            // The replacement re-emits the instance and the arguments, and `IndexOf(...) == 0` comparisons nest,
            // so those are read off the node as an inner fix has already rewritten it rather than off the
            // original tree.
            editor.ReplaceNode(node, (currentNode, generator) =>
            {
                if (GetIndexOfInvocation(currentNode) is not SyntaxNode invocation)
                {
                    return currentNode;
                }

                SyntaxNode instance = GetInstance(invocation);
                SyntaxNode[] arguments = GetArguments(invocation);

                switch (overloadValue)
                {
                    // For 'IndexOf(string)' and 'IndexOf(string, stringComparison)', we replace with StartsWith(same arguments)
                    case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString:
                    case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString_StringComparison:
                        return CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate);

                    // For 'a.IndexOf(ch, stringComparison)':
                    // C#: Use 'a.AsSpan().StartsWith(stackalloc char[1] { ch }, stringComparison)'
                    // https://learn.microsoft.com/dotnet/api/system.memoryextensions.startswith?view=net-7.0#system-memoryextensions-startswith(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-stringcomparison)
                    // VB: Use a.StartsWith(c.ToString(), stringComparison)
                    case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar_StringComparison:
                        return HandleCharStringComparisonOverload(generator, instance, arguments, shouldNegate);

                    // If 'StartsWith(char)' is available, use it. Otherwise check '.Length > 0 && [0] == ch'
                    // For negation, we use '.Length == 0 || [0] != ch'
                    case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadChar:
                        if (compilationHasStartsWithCharOverload)
                        {
                            return CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate);
                        }

                        SyntaxNode lengthAccess = generator.MemberAccessExpression(instance, "Length");
                        SyntaxNode zeroLiteral = generator.LiteralExpression(0);

                        SyntaxNode indexed = generator.ElementAccessExpression(instance, zeroLiteral);
                        SyntaxNode ch = GetArgumentExpression(arguments[0]);

                        SyntaxNode replacement = shouldNegate
                            ? generator.LogicalOrExpression(
                                generator.ValueEqualsExpression(lengthAccess, zeroLiteral),
                                generator.ValueNotEqualsExpression(indexed, ch))
                            : generator.LogicalAndExpression(
                                generator.GreaterThanExpression(lengthAccess, zeroLiteral),
                                generator.ValueEqualsExpression(indexed, ch));

                        return AppendElasticMarker(replacement);

                    default:
                        Debug.Fail("This should never happen.");

                        return currentNode;
                }
            });

            return Task.CompletedTask;
        }

        /// <summary>
        /// Returns the <c>IndexOf</c> invocation <paramref name="comparison"/> compares with zero, or
        /// <see langword="null"/> when it is not a shape the fix handles.
        /// </summary>
        protected abstract SyntaxNode? GetIndexOfInvocation(SyntaxNode comparison);

        /// <summary>
        /// Returns the instance <paramref name="invocation"/> is called on.
        /// </summary>
        protected abstract SyntaxNode GetInstance(SyntaxNode invocation);

        /// <summary>
        /// Returns <paramref name="invocation"/>'s arguments, in source order.
        /// </summary>
        protected abstract SyntaxNode[] GetArguments(SyntaxNode invocation);

        /// <summary>
        /// Returns the expression <paramref name="argument"/> passes, without any name prefix.
        /// </summary>
        protected abstract SyntaxNode GetArgumentExpression(SyntaxNode argument);

        protected abstract SyntaxNode HandleCharStringComparisonOverload(SyntaxGenerator generator, SyntaxNode instance, SyntaxNode[] arguments, bool shouldNegate);
        protected abstract SyntaxNode AppendElasticMarker(SyntaxNode replacement);

        protected static SyntaxNode CreateStartsWithInvocationFromArguments(SyntaxGenerator generator, SyntaxNode instance, SyntaxNode[] arguments, bool shouldNegate)
        {
            var expression = generator.InvocationExpression(generator.MemberAccessExpression(instance, "StartsWith"), arguments);
            return shouldNegate ? generator.LogicalNotExpression(expression) : expression;
        }
    }
}