File: Microsoft.NetCore.Analyzers\Performance\PreferLengthCountIsEmptyOverAnyAnalyzer.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;
using System.Collections;
using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using static Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources;

namespace Microsoft.NetCore.Analyzers.Performance
{
    /// <summary>
    /// Prefer using 'IsEmpty' or comparing 'Count' / 'Length' property to 0 rather than using 'Any()', both for clarity and for performance.
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class PreferLengthCountIsEmptyOverAnyAnalyzer : DiagnosticAnalyzer
    {
        private const string AnyText = nameof(Enumerable.Any);

        internal const string IsEmptyText = nameof(ImmutableArray<dynamic>.IsEmpty);
        internal const string LengthText = nameof(Array.Length);
        internal const string CountText = nameof(ICollection.Count);

        internal const string RuleId = "CA1860";
        internal const string DiagnosticPropertyKey = nameof(DiagnosticPropertyKey);

        internal static readonly DiagnosticDescriptor IsEmptyDescriptor = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(PreferLengthCountIsEmptyOverAnyTitle)),
            CreateLocalizableResourceString(nameof(PreferIsEmptyOverAnyMessage)),
            DiagnosticCategory.Performance,
            RuleLevel.IdeSuggestion,
            CreateLocalizableResourceString(nameof(PreferLengthCountIsEmptyOverAnyDescription)),
            isPortedFxCopRule: false,
            isDataflowRule: false
        );

        internal static readonly DiagnosticDescriptor LengthDescriptor = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(PreferLengthCountIsEmptyOverAnyTitle)),
            CreateLocalizableResourceString(nameof(PreferLengthOverAnyMessage)),
            DiagnosticCategory.Performance,
            RuleLevel.IdeSuggestion,
            CreateLocalizableResourceString(nameof(PreferLengthCountIsEmptyOverAnyDescription)),
            isPortedFxCopRule: false,
            isDataflowRule: false
        );

        internal static readonly DiagnosticDescriptor CountDescriptor = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(PreferLengthCountIsEmptyOverAnyTitle)),
            CreateLocalizableResourceString(nameof(PreferCountOverAnyMessage)),
            DiagnosticCategory.Performance,
            RuleLevel.IdeSuggestion,
            CreateLocalizableResourceString(nameof(PreferLengthCountIsEmptyOverAnyDescription)),
            isPortedFxCopRule: false,
            isDataflowRule: false
        );

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
            LengthDescriptor,
            CountDescriptor,
            IsEmptyDescriptor
        );

        public override void Initialize(AnalysisContext context)
        {
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.EnableConcurrentExecution();
            context.RegisterCompilationStartAction(ctx =>
            {
                var typeProvider = WellKnownTypeProvider.GetOrCreate(ctx.Compilation);
                var iEnumerable = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsIEnumerable);
                var iEnumerableOfT = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIEnumerable1);
                var linqExpressionType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqExpressionsExpression1);
                var anyMethod = typeProvider
                    .GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqEnumerable)
                    ?.GetMembers(AnyText)
                    .OfType<IMethodSymbol>()
                    .FirstOrDefault(m => m.IsExtensionMethod && m.Parameters.Length == 1);
                if (iEnumerable is not null && iEnumerableOfT is not null && anyMethod is not null)
                {
                    ctx.RegisterOperationAction(c => OnInvocationAnalysis(c, iEnumerable, iEnumerableOfT, anyMethod, linqExpressionType), OperationKind.Invocation);
                }
            });
        }

        private static void OnInvocationAnalysis(OperationAnalysisContext context, INamedTypeSymbol iEnumerable, INamedTypeSymbol iEnumerableOfT, IMethodSymbol anyMethod, INamedTypeSymbol? linqExpressionType)
        {
            var invocation = (IInvocationOperation)context.Operation;
            if (invocation.IsWithinExpressionTree(linqExpressionType))
            {
                return;
            }

            var originalMethod = invocation.TargetMethod.OriginalDefinition;
            if (originalMethod.MethodKind == MethodKind.ReducedExtension)
            {
                originalMethod = originalMethod.ReducedFrom!;
            }

            if (originalMethod.Equals(anyMethod, SymbolEqualityComparer.Default))
            {
                var type = invocation.GetReceiverType(context.Compilation, beforeConversion: true, context.CancellationToken);
                if (type is null || (!type.AllInterfaces.Contains(iEnumerable, SymbolEqualityComparer.Default) && !type.AllInterfaces.Contains(iEnumerableOfT)))
                {
                    return;
                }

                if (HasEligibleIsEmptyProperty(type))
                {
                    var properties = ImmutableDictionary.CreateBuilder<string, string?>();
                    properties.Add(DiagnosticPropertyKey, IsEmptyText);
                    context.ReportDiagnostic(invocation.CreateDiagnostic(IsEmptyDescriptor, properties: properties.ToImmutable()));
                }
                else if (HasEligibleLengthProperty(type))
                {
                    var properties = ImmutableDictionary.CreateBuilder<string, string?>();
                    properties.Add(DiagnosticPropertyKey, LengthText);
                    context.ReportDiagnostic(invocation.CreateDiagnostic(LengthDescriptor, properties: properties.ToImmutable()));
                }

                else if (HasEligibleCountProperty(type))
                {
                    var properties = ImmutableDictionary.CreateBuilder<string, string?>();
                    properties.Add(DiagnosticPropertyKey, CountText);
                    context.ReportDiagnostic(invocation.CreateDiagnostic(CountDescriptor, properties: properties.ToImmutable()));
                }
            }
        }

        private static bool HasEligibleIsEmptyProperty(ITypeSymbol typeSymbol)
        {
            return typeSymbol.GetMembers(IsEmptyText)
                .OfType<IPropertySymbol>()
                .Any(property => property.Type.SpecialType == SpecialType.System_Boolean);
        }

        private static bool HasEligibleLengthProperty(ITypeSymbol typeSymbol)
        {
            if (typeSymbol is IArrayTypeSymbol)
            {
                return true;
            }

            return typeSymbol.GetMembers(LengthText)
                .OfType<IPropertySymbol>()
                .Any(property => property.Type.SpecialType is SpecialType.System_Int32 or SpecialType.System_UInt32);
        }

        private static bool HasEligibleCountProperty(ITypeSymbol typeSymbol)
        {
            return typeSymbol.GetMembers(CountText)
                // Include implemented interface members.
                .Concat(typeSymbol.AllInterfaces.SelectMany(s => s.GetMembers(CountText)))
                .OfType<IPropertySymbol>()
                .Any(property => property.Type.SpecialType is SpecialType.System_Int32 or SpecialType.System_UInt32);
        }
    }
}