File: Microsoft.CodeQuality.Analyzers\ApiDesignGuidelines\CollectionsShouldImplementGenericInterface.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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.PooledObjects;
using Analyzer.Utilities.PooledObjects.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
    using static MicrosoftCodeQualityAnalyzersResources;

    /// <summary>
    /// CA1010: <inheritdoc cref="CollectionsShouldImplementGenericInterfaceTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class CollectionsShouldImplementGenericInterfaceAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1010";

        private static readonly LocalizableString s_localizableTitle =
            CreateLocalizableResourceString(nameof(CollectionsShouldImplementGenericInterfaceTitle));

        private static readonly LocalizableString s_localizableDescription =
            CreateLocalizableResourceString(nameof(CollectionsShouldImplementGenericInterfaceDescription));

        internal static readonly DiagnosticDescriptor Rule =
            DiagnosticDescriptorHelper.Create(
                RuleId,
                s_localizableTitle,
                CreateLocalizableResourceString(nameof(CollectionsShouldImplementGenericInterfaceMessage)),
                DiagnosticCategory.Design,
                RuleLevel.IdeHidden_BulkConfigurable,
                description: s_localizableDescription,
                isPortedFxCopRule: true,
                isDataflowRule: false);

        internal static readonly DiagnosticDescriptor RuleMultiple =
            DiagnosticDescriptorHelper.Create(
                RuleId,
                s_localizableTitle,
                CreateLocalizableResourceString(nameof(CollectionsShouldImplementGenericInterfaceMultipleMessage)),
                DiagnosticCategory.Design,
                RuleLevel.IdeHidden_BulkConfigurable,
                description: s_localizableDescription,
                isPortedFxCopRule: true,
                isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterCompilationStartAction(
               context =>
               {
                   var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(context.Compilation);

                   // Orders inside the array matters as we report only on the first missing implemented tuple.
                   var interfaceToGenericInterfaceMapBuilder = ImmutableArray.CreateBuilder<KeyValuePair<INamedTypeSymbol, ImmutableArray<INamedTypeSymbol>>>();

                   if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsIList, out var iListType))
                   {
                       var builder = ArrayBuilder<INamedTypeSymbol>.GetInstance();
                       builder.AddIfNotNull(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIList1));
                       builder.AddIfNotNull(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIReadOnlyList1));
                       if (builder.Count > 0)
                       {
                           interfaceToGenericInterfaceMapBuilder.Add(new(iListType, builder.ToImmutable()));
                       }
                   }

                   if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsICollection, out var iCollectionType))
                   {
                       var builder = ArrayBuilder<INamedTypeSymbol>.GetInstance();
                       builder.AddIfNotNull(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericICollection1));
                       builder.AddIfNotNull(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIReadOnlyCollection1));
                       if (builder.Count > 0)
                       {
                           interfaceToGenericInterfaceMapBuilder.Add(new(iCollectionType, builder.ToImmutable()));
                       }
                   }

                   if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsIEnumerable, out var iEnumerableType) &&
                       wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIEnumerable1, out var genericIEnumerableType))
                   {
                       interfaceToGenericInterfaceMapBuilder.Add(new(iEnumerableType, ImmutableArray.Create(genericIEnumerableType)));
                   }

                   context.RegisterSymbolAction(c => AnalyzeSymbol(c, interfaceToGenericInterfaceMapBuilder.ToImmutable()), SymbolKind.NamedType);
               });
        }

        private static void AnalyzeSymbol(SymbolAnalysisContext context, ImmutableArray<KeyValuePair<INamedTypeSymbol, ImmutableArray<INamedTypeSymbol>>> interfacePairs)
        {
            Debug.Assert(interfacePairs.All(kvp => kvp.Key.TypeKind == TypeKind.Interface && kvp.Value.All(x => x.TypeKind == TypeKind.Interface)));

            var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;

            // FxCop compat: only fire on externally visible types by default.
            if (!context.Options.MatchesConfiguredVisibility(Rule, namedTypeSymbol, context.Compilation))
            {
                return;
            }

            using var allInterfaces = PooledHashSet<INamedTypeSymbol>.GetInstance();
            foreach (var @interface in namedTypeSymbol.AllInterfaces.Select(i => i.OriginalDefinition))
            {
                allInterfaces.Add(@interface);
            }

            // First we need to try to match all types from the user definition...
            var userMap = context.Options.GetAdditionalRequiredGenericInterfaces(Rule, context.Symbol, context.Compilation);
            if (!userMap.IsEmpty)
            {
                foreach (var @interface in allInterfaces)
                {
                    if (!@interface.IsGenericType &&
                        userMap.TryGetValue(@interface, out var genericInterface) &&
                        genericInterface?.IsGenericType == true)
                    {
                        ReportDiagnostic(@interface, genericInterface);
                        return;
                    }
                }
            }

            // ...Then we can proceed with the hardcoded ones keeping the declaration order
            for (int i = 0; i < interfacePairs.Length; i++)
            {
                var kvp = interfacePairs[i];

                if (allInterfaces.Contains(kvp.Key) && !allInterfaces.Intersect(kvp.Value, SymbolEqualityComparer.Default).Any())
                {
                    if (kvp.Value.Length > 1)
                    {
                        ReportDiagnosticAlt(kvp.Key, kvp.Value);
                    }
                    else
                    {
                        ReportDiagnostic(kvp.Key, kvp.Value[0]);
                    }

                    return;
                }
            }

            return;

            void ReportDiagnostic(INamedTypeSymbol @interface, INamedTypeSymbol genericInterface)
            {
                context.ReportDiagnostic(namedTypeSymbol.CreateDiagnostic(Rule, namedTypeSymbol.Name,
                    @interface.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
                    genericInterface.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)));
            }

            void ReportDiagnosticAlt(INamedTypeSymbol @interface, IEnumerable<INamedTypeSymbol> genericInterfaces)
            {
                context.ReportDiagnostic(namedTypeSymbol.CreateDiagnostic(RuleMultiple, namedTypeSymbol.Name,
                    @interface.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
                    string.Join("', '", genericInterfaces.Select(x => x.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)))));
            }
        }
    }
}