File: src\Analyzers\CSharp\Analyzers\UseIndexOrRangeOperator\CSharpUseRangeOperatorDiagnosticAnalyzer.InfoCache.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.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator;
 
using static Helpers;
 
internal sealed partial class CSharpUseRangeOperatorDiagnosticAnalyzer
{
    /// <summary>
    /// Helper type to cache information about types while analyzing the compilation.
    /// </summary>
    public sealed class InfoCache
    {
        /// <summary>
        /// The <see cref="T:System.Range"/> type.  Needed so that we only fixup code if we see the type
        /// we're using has an indexer that takes a <see cref="T:System.Range"/>.
        /// </summary>
        [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")]
        public readonly INamedTypeSymbol RangeType;
        public readonly INamedTypeSymbol? ExpressionOfTType;
 
        private readonly ConcurrentDictionary<IMethodSymbol, MemberInfo> _methodToMemberInfo = new();
 
        private InfoCache(INamedTypeSymbol rangeType, INamedTypeSymbol stringType, INamedTypeSymbol? expressionOfTType)
        {
            RangeType = rangeType;
            ExpressionOfTType = expressionOfTType;
 
            // Always allow using System.Range indexers with System.String.Substring.  The compiler has hard-coded
            // knowledge on how to use this type, even if there is no this[Range] indexer declared on it directly.
            //
            // Ensure that we can actually get the 'string' type. We may fail if there is no
            // proper mscorlib reference (for example, while a project is loading).
            if (!stringType.IsErrorType())
            {
                var substringMethod = stringType
                    .GetMembers(nameof(string.Substring))
                    .OfType<IMethodSymbol>()
                    .FirstOrDefault(IsTwoArgumentSliceLikeMethod);
 
                if (substringMethod is not null)
                    _methodToMemberInfo[substringMethod] = ComputeMemberInfo(substringMethod, requireRangeMember: false);
 
                // We also support translating certain calls to .Remove to calls to the range member, if the user is
                // trimming off the start or end of a string.
                var removeMethod = stringType
                    .GetMembers(nameof(string.Remove))
                    .OfType<IMethodSymbol>()
                    .FirstOrDefault(IsTwoArgumentSliceLikeMethod);
 
                if (removeMethod is not null)
                    _methodToMemberInfo[removeMethod] = ComputeMemberInfo(removeMethod, requireRangeMember: false);
            }
        }
 
        public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out InfoCache? infoCache)
        {
            var rangeType = compilation.GetBestTypeByMetadataName(typeof(Range).FullName!);
            if (rangeType == null || !rangeType.IsAccessibleWithin(compilation.Assembly))
            {
                infoCache = null;
                return false;
            }
 
            var stringType = compilation.GetSpecialType(SpecialType.System_String);
            infoCache = new InfoCache(rangeType, stringType, compilation.ExpressionOfTType());
            return true;
        }
 
        private static IMethodSymbol? GetSliceLikeMethod(INamedTypeSymbol namedType)
            => namedType.GetMembers()
                        .OfType<IMethodSymbol>()
                        .Where(IsTwoArgumentSliceLikeMethod)
                        .FirstOrDefault();
 
        public bool TryGetMemberInfo(IMethodSymbol method, out MemberInfo memberInfo)
        {
            if (!IsTwoArgumentSliceLikeMethod(method))
            {
                memberInfo = default;
                return false;
            }
 
            memberInfo = _methodToMemberInfo.GetOrAdd(method, m => ComputeMemberInfo(m, requireRangeMember: true));
            return memberInfo.LengthLikeProperty != null;
        }
 
        public bool TryGetMemberInfoOneArgument(IMethodSymbol method, out MemberInfo memberInfo)
        {
            if (!IsOneArgumentSliceLikeMethod(method))
            {
                memberInfo = default;
                return false;
            }
 
            if (!_methodToMemberInfo.TryGetValue(method, out memberInfo))
            {
                // Find overload of our method that is a slice-like method with two arguments. Computing member info for
                // this method will also check that the containing type has an int32 'Length' or 'Count' property, and
                // has a suitable indexer, so we don't have to.
                var overloadWithTwoArguments = method.ContainingType
                    .GetMembers(method.Name)
                    .OfType<IMethodSymbol>()
                    .FirstOrDefault(IsTwoArgumentSliceLikeMethod);
                if (overloadWithTwoArguments is null)
                {
                    memberInfo = default;
                    return false;
                }
 
                // Since the search is expensive, we keep both the original one-argument and two-arguments overload as
                // keys in the cache, pointing to the same member information object.
                var newMemberInfo = _methodToMemberInfo.GetOrAdd(overloadWithTwoArguments, _ => ComputeMemberInfo(overloadWithTwoArguments, requireRangeMember: true));
                _methodToMemberInfo.GetOrAdd(method, _ => newMemberInfo);
                memberInfo = newMemberInfo;
            }
 
            return memberInfo.LengthLikeProperty != null;
        }
 
        private MemberInfo ComputeMemberInfo(IMethodSymbol sliceLikeMethod, bool requireRangeMember)
        {
            Debug.Assert(IsTwoArgumentSliceLikeMethod(sliceLikeMethod) || IsOneArgumentSliceLikeMethod(sliceLikeMethod));
 
            // Check that the type has an int32 'Length' or 'Count' property. If not, we don't
            // consider it something indexable.
            var containingType = sliceLikeMethod.ContainingType;
            var lengthLikeProperty = TryGetLengthOrCountProperty(containingType);
            if (lengthLikeProperty == null)
                return default;
 
            if (!requireRangeMember)
                return new MemberInfo(lengthLikeProperty, overloadedMethod: null);
 
            // A Slice method can either be paired with an Range-taking indexer on the type, or
            // an Range-taking overload, or an explicit method called .Slice that takes two ints:
            //
            // https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support
            if (sliceLikeMethod.ReturnType.Equals(containingType))
            {
                // it's a method like:  MyType MyType.Get(int start, int length).  Look for an
                // indexer like  `MyType MyType.this[Range range]`.
                var indexer = GetIndexer(containingType, RangeType, containingType);
                if (indexer != null)
                {
                    return new MemberInfo(lengthLikeProperty, overloadedMethod: null);
                }
 
                // Also, look to see if the type has a `.Slice(int start, int length)` method.
                // This is also a method the compiler knows to look for when a user writes `x[a..b]`
                var actualSliceMethod =
                    sliceLikeMethod.ContainingType.GetMembers(nameof(Span<>.Slice))
                                                  .OfType<IMethodSymbol>()
                                                  .FirstOrDefault(s => IsTwoArgumentSliceLikeMethod(s));
                if (actualSliceMethod != null)
                {
                    return new MemberInfo(lengthLikeProperty, overloadedMethod: null);
                }
            }
 
            // it's a method like:   `SomeType MyType.Get(int start, int length)`.  Look 
            // for an overload like: `SomeType MyType.Get(Range)`
            var overloadedRangeMethod = GetOverload(sliceLikeMethod, RangeType);
            if (overloadedRangeMethod != null)
                return new MemberInfo(lengthLikeProperty, overloadedRangeMethod);
 
            // A slice-like method that we can't convert.
            return default;
        }
    }
}