File: CSharpUseIndexOperatorDiagnosticAnalyzer.cs
Web Access
Project: ..\..\..\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.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator
{
    using static Helpers;
 
    /// <summary>
    /// <para>Analyzer that looks for code like:</para>
    ///
    /// <list type="number">
    /// <item><description><c>s[s.Length - n]</c> and offers to change that to <c>s[^n]</c></description></item>
    /// <item><description></description><c>s.Get(s.Length - n)</c> and offers to change that to <c>s.Get(^n)</c></item>
    /// </list>
    ///
    /// <para>In order to do convert between indexers, the type must look 'indexable'.  Meaning, it must
    /// have an <see cref="int"/>-returning property called <c>Length</c> or <c>Count</c>, and it must have both an
    /// <see cref="int"/>-indexer, and a <see cref="T:System.Index"/>-indexer.  In order to convert between methods, the type
    /// must have identical overloads except that one takes an <see cref="int"/>, and the other a <see cref="T:System.Index"/>.</para>
    ///
    /// <para>It is assumed that if the type follows this shape that it is well behaved and that this
    /// transformation will preserve semantics.  If this assumption is not good in practice, we
    /// could always limit the feature to only work on an allow list of known safe types.</para>
    ///
    /// <para>Note that this feature only works if the code literally has <c>expr1.Length - expr2</c>. If
    /// code has this, and is calling into a method that takes either an <see cref="int"/> or a <see cref="T:System.Index"/>,
    /// it feels very safe to assume this is well behaved and switching to <c>^expr2</c> is going to
    /// preserve semantics.</para>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")]
    internal sealed partial class CSharpUseIndexOperatorDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
    {
        public CSharpUseIndexOperatorDiagnosticAnalyzer()
            : base(IDEDiagnosticIds.UseIndexOperatorDiagnosticId,
                   EnforceOnBuildValues.UseIndexOperator,
                   CSharpCodeStyleOptions.PreferIndexOperator,
                   new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_index_operator), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
                   new LocalizableResourceString(nameof(CSharpAnalyzersResources.Indexing_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
        {
        }
 
        public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
        protected override void InitializeWorker(AnalysisContext context)
        {
            context.RegisterCompilationStartAction(context =>
            {
                var compilation = (CSharpCompilation)context.Compilation;
 
                // Only supported on C# 8 and above.
                if (compilation.LanguageVersion < LanguageVersion.CSharp8)
                    return;
 
                // We're going to be checking every property-reference and invocation in the
                // compilation. Cache information we compute in this object so we don't have to
                // continually recompute it.
                if (!InfoCache.TryCreate(compilation, out var infoCache))
                    return;
 
                // Register to hear property references, so we can hear about calls to indexers
                // like: s[s.Length - n]
                context.RegisterOperationAction(
                    c => AnalyzePropertyReference(c, infoCache),
                    OperationKind.PropertyReference);
 
                // Register to hear about methods for: s.Get(s.Length - n)
                context.RegisterOperationAction(
                    c => AnalyzeInvocation(c, infoCache),
                    OperationKind.Invocation);
 
                var arrayType = compilation.GetSpecialType(SpecialType.System_Array);
                var arrayLengthProperty = TryGetNoArgInt32Property(arrayType, nameof(Array.Length));
 
                if (arrayLengthProperty != null)
                {
                    // Array indexing is represented with a different operation kind.  Register
                    // specifically for that.
                    context.RegisterOperationAction(
                        c => AnalyzeArrayElementReference(c, infoCache, arrayLengthProperty),
                        OperationKind.ArrayElementReference);
                }
            });
        }
 
        private void AnalyzeInvocation(
            OperationAnalysisContext context, InfoCache infoCache)
        {
            var cancellationToken = context.CancellationToken;
            var invocationOperation = (IInvocationOperation)context.Operation;
 
            if (invocationOperation.Arguments.Length != 1)
                return;
 
            AnalyzeInvokedMember(
                context, infoCache,
                invocationOperation.Instance,
                invocationOperation.TargetMethod,
                invocationOperation.Arguments[0].Value,
                lengthLikeProperty: null,
                cancellationToken);
        }
 
        private void AnalyzePropertyReference(
            OperationAnalysisContext context, InfoCache infoCache)
        {
            var cancellationToken = context.CancellationToken;
            var propertyReference = (IPropertyReferenceOperation)context.Operation;
 
            // Only analyze indexer calls.
            if (!propertyReference.Property.IsIndexer)
                return;
 
            if (propertyReference.Arguments.Length != 1)
                return;
 
            AnalyzeInvokedMember(
                context, infoCache,
                propertyReference.Instance,
                propertyReference.Property.GetMethod,
                propertyReference.Arguments[0].Value,
                lengthLikeProperty: null,
                cancellationToken);
        }
 
        private void AnalyzeArrayElementReference(
            OperationAnalysisContext context, InfoCache infoCache, IPropertySymbol arrayLengthProperty)
        {
            var cancellationToken = context.CancellationToken;
            var arrayElementReference = (IArrayElementReferenceOperation)context.Operation;
 
            // Has to be a single-dimensional element access.
            if (arrayElementReference.Indices.Length != 1)
                return;
 
            AnalyzeInvokedMember(
                context, infoCache,
                arrayElementReference.ArrayReference,
                targetMethod: null,
                arrayElementReference.Indices[0],
                lengthLikeProperty: arrayLengthProperty,
                cancellationToken);
        }
 
        private void AnalyzeInvokedMember(
            OperationAnalysisContext context,
            InfoCache infoCache,
            IOperation? instance,
            IMethodSymbol? targetMethod,
            IOperation argumentValue,
            IPropertySymbol? lengthLikeProperty,
            CancellationToken cancellationToken)
        {
            // look for `s[s.Length - value]` or `s.Get(s.Length- value)`.
 
            // Needs to have the one arg for `s.Length - value`, and that arg needs to be
            // a subtraction.
            if (instance is null ||
                !IsSubtraction(argumentValue, out var subtraction))
            {
                return;
            }
 
            if (subtraction.Syntax is not BinaryExpressionSyntax binaryExpression)
                return;
 
            // Don't bother analyzing if the user doesn't like using Index/Range operators.
            var option = context.GetCSharpAnalyzerOptions().PreferIndexOperator;
            if (!option.Value)
                return;
 
            // Ok, looks promising.  We're indexing in with some subtraction expression. Examine the
            // type this indexer is in to see if there's another member that takes a System.Index
            // that we can convert to.
            //
            // Also ensure that the left side of the subtraction : `s.Length - value` is actually
            // getting the length off the same instance we're indexing into.
 
            lengthLikeProperty ??= TryGetLengthLikeProperty(infoCache, targetMethod);
            if (lengthLikeProperty == null ||
                !IsInstanceLengthCheck(lengthLikeProperty, instance, subtraction.LeftOperand))
            {
                return;
            }
 
            var semanticModel = instance.SemanticModel;
            Contract.ThrowIfNull(semanticModel);
 
            if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, instance.Syntax, infoCache.ExpressionOfTType, cancellationToken))
                return;
 
            // Everything looks good.  We can update this to use the System.Index member instead.
            context.ReportDiagnostic(
                DiagnosticHelper.Create(
                    Descriptor,
                    binaryExpression.GetLocation(),
                    option.Notification.Severity,
                    ImmutableArray<Location>.Empty,
                    ImmutableDictionary<string, string?>.Empty));
        }
 
        private static IPropertySymbol? TryGetLengthLikeProperty(InfoCache infoCache, IMethodSymbol? targetMethod)
            => targetMethod != null && infoCache.TryGetMemberInfo(targetMethod, out var memberInfo)
                ? memberInfo.LengthLikeProperty
                : null;
    }
}