File: src\Analyzers\CSharp\CodeFixes\UseIndexOrRangeOperator\CSharpUseRangeOperatorCodeFixProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator;
 
using static CodeFixHelpers;
using static CSharpSyntaxTokens;
using static CSharpUseRangeOperatorDiagnosticAnalyzer;
using static Helpers;
using static SyntaxFactory;
 
[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseRangeOperator), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class CSharpUseRangeOperatorCodeFixProvider() : SyntaxEditorBasedCodeFixProvider
{
    public override ImmutableArray<string> FixableDiagnosticIds { get; } =
        [IDEDiagnosticIds.UseRangeOperatorDiagnosticId];
 
    public override Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        RegisterCodeFix(context, CSharpAnalyzersResources.Use_range_operator, nameof(CSharpAnalyzersResources.Use_range_operator));
        return Task.CompletedTask;
    }
 
    protected override async Task FixAllAsync(
        Document document, ImmutableArray<Diagnostic> diagnostics,
        SyntaxEditor editor, CancellationToken cancellationToken)
    {
        var invocationNodes = diagnostics
            .Select(d => GetInvocationExpression(d, cancellationToken))
            .OrderByDescending(i => i.SpanStart)
            .ToImmutableArray();
        var syntaxGenerator = SyntaxGenerator.GetGenerator(document);
 
        await editor.ApplyExpressionLevelSemanticEditsAsync(
            document, invocationNodes,
            canReplace: (_1, _2) => true,
            (semanticModel, currentRoot, currentInvocation) =>
                UpdateInvocation(semanticModel, currentRoot, currentInvocation, syntaxGenerator, cancellationToken),
            cancellationToken).ConfigureAwait(false);
    }
 
    private static SyntaxNode UpdateInvocation(
        SemanticModel semanticModel, SyntaxNode currentRoot,
        InvocationExpressionSyntax currentInvocation,
        SyntaxGenerator generator,
        CancellationToken cancellationToken)
    {
        if (semanticModel.GetOperation(currentInvocation, cancellationToken) is IInvocationOperation invocation &&
            InfoCache.TryCreate(semanticModel.Compilation, out var infoCache) &&
            AnalyzeInvocation(invocation, infoCache) is { } result)
        {
            var updatedNode = FixOne(result, generator);
            if (updatedNode != null)
                return currentRoot.ReplaceNode(result.Invocation, updatedNode);
        }
 
        return currentRoot;
    }
 
    private static InvocationExpressionSyntax GetInvocationExpression(Diagnostic d, CancellationToken cancellationToken)
        => (InvocationExpressionSyntax)d.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken);
 
    private static ExpressionSyntax FixOne(Result result, SyntaxGenerator generator)
    {
        var invocation = result.Invocation;
 
        var rangeExpression = CreateRangeExpression(result, generator);
        var argument = Argument(rangeExpression).WithAdditionalAnnotations(Formatter.Annotation);
        var arguments = SingletonSeparatedList(argument);
 
        if (result.MemberInfo.OverloadedMethod == null)
        {
            var argList = invocation.ArgumentList;
            var argumentList = BracketedArgumentList(
                    OpenBracketToken.WithTriviaFrom(argList.OpenParenToken),
                    arguments,
                    CloseBracketToken.WithTriviaFrom(argList.CloseParenToken));
            if (invocation.Expression is MemberBindingExpressionSyntax)
            {
                // x?.Substring(...) -> x?[...]
                return ElementBindingExpression(argumentList);
            }
 
            if (invocation.Expression is IdentifierNameSyntax)
            {
                // Substring(...) -> this[...]
                return ElementAccessExpression(ThisExpression(), argumentList);
            }
 
            var expression = invocation.Expression is MemberAccessExpressionSyntax memberAccess
                ? memberAccess.Expression // x.Substring(...) -> x[...]
                : invocation.Expression;
            return ElementAccessExpression(expression, argumentList);
        }
        else
        {
            return invocation.ReplaceNode(
                invocation.ArgumentList,
                invocation.ArgumentList.WithArguments(arguments));
        }
    }
 
    private static RangeExpressionSyntax CreateRangeExpression(Result result, SyntaxGenerator generator)
        => result.Kind switch
        {
            ResultKind.Computed => CreateComputedRange(result),
            ResultKind.Constant => CreateConstantRange(result, generator),
            _ => throw ExceptionUtilities.Unreachable(),
        };
 
    private static RangeExpressionSyntax CreateComputedRange(Result result)
    {
        var instance = result.InvocationOperation.Instance;
        var lengthLikeProperty = result.MemberInfo.LengthLikeProperty;
 
        Contract.ThrowIfNull(instance);
 
        var (startExpr, startFromEnd, endExpr, endFromEnd) = GetComputedRangeData();
 
        // expressions that the iops point to may be skip certain expressions actually in source (like checked
        // exprs).  Walk upwards so we grab all of that when producing the final range expression.
        startExpr = WalkUpCheckedExpressions(startExpr);
        endExpr = WalkUpCheckedExpressions(endExpr);
 
        return RangeExpression(
            startExpr != null && startFromEnd ? IndexExpression(startExpr) : startExpr?.Parenthesize(),
            endExpr != null && endFromEnd ? IndexExpression(endExpr) : endExpr?.Parenthesize());
 
        (ExpressionSyntax? startExpr, bool startFromEnd, ExpressionSyntax? endExpr, bool endFromEnd) GetComputedRangeData()
        {
            if (IsStringRemoveMethod(result.InvocationOperation.TargetMethod))
            {
                if (result.Invocation.ArgumentList.Arguments.Count == 1)
                {
                    if (IsSubtraction(result.InvocationOperation.Arguments[0].Value, out var subtraction) &&
                        IsInstanceLengthCheck(lengthLikeProperty, instance, subtraction.LeftOperand))
                    {
                        // `string.Remove(string.Length - x)` becomes `string[..^x]`
                        return (startExpr: null, startFromEnd: false, (ExpressionSyntax)subtraction.RightOperand.Syntax, endFromEnd: true);
                    }
                    else
                    {
                        // `string.Remove(x)` becomes `string[..x]`
                        return (startExpr: null, startFromEnd: false, (ExpressionSyntax)result.InvocationOperation.Arguments[0].Value.Syntax, endFromEnd: false);
                    }
                }
                else
                {
                    if (IsSubtraction(result.InvocationOperation.Arguments[1].Value, out var subtraction) &&
                        IsInstanceLengthCheck(lengthLikeProperty, instance, subtraction.LeftOperand))
                    {
                        // `string.Remove(0, string.Length - x)` becomes `string[^x..]`
                        return ((ExpressionSyntax)subtraction.RightOperand.Syntax, startFromEnd: true, endExpr: null, endFromEnd: false);
                    }
                    else
                    {
                        // `string.Remove(0, x)` becomes `string[x..]`
                        return ((ExpressionSyntax)result.InvocationOperation.Arguments[1].Value.Syntax, startFromEnd: false, endExpr: null, endFromEnd: false);
                    }
                }
            }
 
            // We have enough information now to generate `start..end`.  However, this will often
            // not be what the user wants.  For example, generating `start..expr.Length` is not as
            // desirable as `start..`.  Similarly, `start..(expr.Length - 1)` is not as desirable as
            // `start..^1`.  
 
            var startOperation = result.Op1;
            var endOperation = result.Op2;
 
            // If our start-op is actually equivalent to `expr.Length - val`, then just change our
            // start-op to be `val` and record that we should emit it as `^val`.
            var startFromEnd = IsFromEnd(lengthLikeProperty, instance, ref startOperation);
            var startExpr = (ExpressionSyntax)startOperation.Syntax;
 
            var endFromEnd = false;
            ExpressionSyntax? endExpr = null;
 
            if (endOperation is not null)
            {
                // We need to do the same for the second argument, since it's present.
                // Similarly, if our end-op is actually equivalent to `expr.Length - val`, then just
                // change our end-op to be `val` and record that we should emit it as `^val`.
                endFromEnd = IsFromEnd(lengthLikeProperty, instance, ref endOperation);
 
                // Check if the range goes to 'expr.Length'; if it does, we leave off
                // the end part of the range, i.e. `start..`.
                if (!IsInstanceLengthCheck(lengthLikeProperty, instance, endOperation))
                    endExpr = (ExpressionSyntax)endOperation.Syntax;
            }
 
            // If we're starting the range operation from 0, then we can just leave off the start of
            // the range. i.e. `..end`
            if (startOperation.ConstantValue.HasValue &&
                startOperation.ConstantValue.Value is 0)
            {
                startExpr = null;
            }
 
            return (startExpr, startFromEnd, endExpr, endFromEnd);
        }
    }
 
    [return: NotNullIfNotNull(nameof(expr))]
    private static ExpressionSyntax? WalkUpCheckedExpressions(ExpressionSyntax? expr)
    {
        while (expr?.Parent is CheckedExpressionSyntax parent)
            expr = parent;
 
        return expr;
    }
 
    private static RangeExpressionSyntax CreateConstantRange(Result result, SyntaxGenerator generator)
    {
        if (IsStringRemoveMethod(result.InvocationOperation.TargetMethod))
        {
            if (result.Op1 == result.InvocationOperation.Arguments[0].Value)
            {
                // `string.Remove(x, string.Length - x)` becomes `string[..x]`
                return RangeExpression(leftOperand: null, WalkUpCheckedExpressions((ExpressionSyntax)result.Op1.Syntax));
            }
            else
            {
                // `string.Remove(string.Length - x, x)` becomes `string[..^x]`
                return RangeExpression(leftOperand: null, IndexExpression(WalkUpCheckedExpressions((ExpressionSyntax)result.Op1.Syntax)));
            }
        }
 
        Contract.ThrowIfNull(result.Op2);
 
        // the form is s.Slice(constant1, s.Length - constant2).  Want to generate
        // s[constant1..(constant2-constant1)]
        var constant1 = GetInt32Value(result.Op1);
        var constant2 = GetInt32Value(result.Op2);
 
        return RangeExpression(
            // If we're starting the range operation from 0, then we can just leave off the start of
            // the range. i.e. `..end`
            constant1 == 0 ? null : WalkUpCheckedExpressions((ExpressionSyntax)result.Op1.Syntax),
            IndexExpression((ExpressionSyntax)generator.LiteralExpression(constant2 - constant1)));
    }
 
    private static int GetInt32Value(IOperation operation)
        => (int)operation.ConstantValue.Value!; // Safe as we already confirmed this was an int when making the result.
 
    /// <summary>
    /// check if its the form: `expr.Length - value`.  If so, update rangeOperation to then
    /// point to 'value' so that we can generate '^value'.
    /// </summary>
    private static bool IsFromEnd(
        IPropertySymbol lengthLikeProperty, IOperation instance, ref IOperation rangeOperation)
    {
        if (IsSubtraction(rangeOperation, out var subtraction) &&
            IsInstanceLengthCheck(lengthLikeProperty, instance, subtraction.LeftOperand))
        {
            rangeOperation = subtraction.RightOperand;
            return true;
        }
 
        return false;
    }
}