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

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

        public override FixAllProvider GetFixAllProvider()
            => WellKnownFixAllProviders.BatchFixer;

        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var document = context.Document;
            var diagnostic = context.Diagnostics[0];
            var root = await document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
            var node = root.FindNode(context.Span, getInnermostNodeForTie: true);

            context.RegisterCodeFix(
                CodeAction.Create(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFixTitle,
                createChangedDocument: cancellationToken =>
                {
                    var instance = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan);
                    var arguments = new SyntaxNode[diagnostic.AdditionalLocations.Count - 1];
                    for (int i = 1; i < diagnostic.AdditionalLocations.Count; i++)
                    {
                        arguments[i - 1] = root.FindNode(diagnostic.AdditionalLocations[i].SourceSpan);
                    }

                    var generator = SyntaxGenerator.GetGenerator(document);
                    var shouldNegate = diagnostic.Properties.TryGetValue(UseStartsWithInsteadOfIndexOfComparisonWithZero.ShouldNegateKey, out _);
                    var compilationHasStartsWithCharOverload = diagnostic.Properties.TryGetKey(UseStartsWithInsteadOfIndexOfComparisonWithZero.CompilationHasStartsWithCharOverloadKey, out _);
                    _ = diagnostic.Properties.TryGetValue(UseStartsWithInsteadOfIndexOfComparisonWithZero.ExistingOverloadKey, out var overloadValue);
                    switch (overloadValue)
                    {
                        // For 'IndexOf(string)' and 'IndexOf(string, stringComparison)', we replace with StartsWith(same arguments)
                        case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString:
                        case UseStartsWithInsteadOfIndexOfComparisonWithZero.OverloadString_StringComparison:
                            return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, 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 Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, 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 Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, CreateStartsWithInvocationFromArguments(generator, instance, arguments, shouldNegate))));
                            }

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

                            var indexed = generator.ElementAccessExpression(instance, zeroLiteral);
                            var ch = root.FindNode(arguments[0].Span, getInnermostNodeForTie: true);

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

                            return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(node, AppendElasticMarker(replacement))));

                        default:
                            Debug.Fail("This should never happen.");
                            return Task.FromResult(document);
                    }
                },
                equivalenceKey: nameof(MicrosoftNetCoreAnalyzersResources.UseStartsWithInsteadOfIndexOfComparisonWithZeroCodeFixTitle)),
                context.Diagnostics);
        }

        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;
        }
    }
}