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

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

    /// <summary>
    /// CA1710: <inheritdoc cref="IdentifiersShouldHaveCorrectSuffixTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class IdentifiersShouldHaveCorrectSuffixAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1710";
        private const string CollectionSuffix = "Collection";
        private const string DictionarySuffix = "Dictionary";
        private const string SetSuffix = "Set";
        private const string QueueSuffix = "Queue";
        private const string StackSuffix = "Stack";
        private const string EventHandlerString = "EventHandler";

        private static readonly ImmutableArray<string> s_setCollectionSuffixes = ImmutableArray.Create(SetSuffix, CollectionSuffix);
        private static readonly ImmutableArray<string> s_queueCollectionSuffixes = ImmutableArray.Create(QueueSuffix, CollectionSuffix);
        private static readonly ImmutableArray<string> s_stackCollectionSuffixes = ImmutableArray.Create(StackSuffix, CollectionSuffix);
        private static readonly ImmutableArray<string> s_dictionaryCollectionSuffixes = ImmutableArray.Create(DictionarySuffix, CollectionSuffix);
        private static readonly ImmutableArray<string> s_collectionDictionarySetStackQueueSuffixes = ImmutableArray.Create(CollectionSuffix, DictionarySuffix, SetSuffix, StackSuffix, QueueSuffix);

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(IdentifiersShouldHaveCorrectSuffixTitle));
        private static readonly LocalizableString s_localizableMessageDefault = CreateLocalizableResourceString(nameof(IdentifiersShouldHaveCorrectSuffixMessageDefault));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(IdentifiersShouldHaveCorrectSuffixDescription));

        internal static readonly DiagnosticDescriptor OneSuffixRule = DiagnosticDescriptorHelper.Create(RuleId,
            s_localizableTitle,
            s_localizableMessageDefault,
            DiagnosticCategory.Naming,
            RuleLevel.IdeHidden_BulkConfigurable,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);
        internal static readonly DiagnosticDescriptor MultipleSuffixesRule = DiagnosticDescriptorHelper.Create(RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(IdentifiersShouldHaveCorrectSuffixMessageMultiple)),
            DiagnosticCategory.Naming,
            RuleLevel.IdeHidden_BulkConfigurable,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);
        internal static readonly DiagnosticDescriptor UserSuffixRule = DiagnosticDescriptorHelper.Create(RuleId,
            s_localizableTitle,
            s_localizableMessageDefault,
            DiagnosticCategory.Naming,
            RuleLevel.IdeHidden_BulkConfigurable,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(OneSuffixRule, MultipleSuffixesRule, UserSuffixRule);

        // Define for each interface/type the collection of allowed suffixes (first item is the preferred suffix),
        // note that the order matters as the algorithm works on a first match basis.
        private static readonly ImmutableArray<(string typeName, ImmutableArray<string> suffixes)> s_baseTypesAndTheirSuffix =
            ImmutableArray.Create(
                // types
                (WellKnownTypeNames.SystemAttribute, ImmutableArray.Create("Attribute")),
                (WellKnownTypeNames.SystemEventArgs, ImmutableArray.Create("EventArgs")),
                (WellKnownTypeNames.SystemException, ImmutableArray.Create("Exception")),
                (WellKnownTypeNames.SystemCollectionsQueue, s_queueCollectionSuffixes),
                (WellKnownTypeNames.SystemCollectionsStack, s_stackCollectionSuffixes),
                (WellKnownTypeNames.SystemCollectionsGenericQueue1, s_queueCollectionSuffixes),
                (WellKnownTypeNames.SystemCollectionsGenericStack1, s_stackCollectionSuffixes),
                (WellKnownTypeNames.SystemDataDataSet, ImmutableArray.Create("DataSet")),
                (WellKnownTypeNames.SystemDataDataTable, ImmutableArray.Create("DataTable", CollectionSuffix)),
                (WellKnownTypeNames.SystemIOStream, ImmutableArray.Create("Stream")),
                // interfaces
                (WellKnownTypeNames.SystemCollectionsGenericIDictionary2, s_dictionaryCollectionSuffixes),
                (WellKnownTypeNames.SystemCollectionsGenericIReadOnlyDictionary2, s_dictionaryCollectionSuffixes),
                (WellKnownTypeNames.SystemCollectionsGenericISet1, s_setCollectionSuffixes),
                (WellKnownTypeNames.SystemCollectionsGenericIReadOnlySet1, s_setCollectionSuffixes),
                (WellKnownTypeNames.SystemCollectionsGenericICollection1, s_collectionDictionarySetStackQueueSuffixes),
                (WellKnownTypeNames.SystemCollectionsGenericIReadOnlyCollection1, s_collectionDictionarySetStackQueueSuffixes),
                (WellKnownTypeNames.SystemCollectionsIDictionary, s_dictionaryCollectionSuffixes),
                (WellKnownTypeNames.SystemCollectionsICollection, s_collectionDictionarySetStackQueueSuffixes),
                (WellKnownTypeNames.SystemCollectionsIEnumerable, s_collectionDictionarySetStackQueueSuffixes),
                (WellKnownTypeNames.SystemSecurityIPermission, ImmutableArray.Create("Permission")),
                (WellKnownTypeNames.SystemSecurityPolicyIMembershipCondition, ImmutableArray.Create("Condition"))
            );

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

            context.RegisterCompilationStartAction(AnalyzeCompilationStart);
        }

        private static void AnalyzeCompilationStart(CompilationStartAnalysisContext context)
        {
            var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(context.Compilation);

            var baseTypeSuffixMapBuilder = ImmutableDictionary.CreateBuilder<INamedTypeSymbol, ImmutableArray<string>>(SymbolEqualityComparer.Default);
            var interfaceTypeSuffixMapBuilder = ImmutableDictionary.CreateBuilder<INamedTypeSymbol, ImmutableArray<string>>(SymbolEqualityComparer.Default);

            foreach (var (typeName, suffixes) in s_baseTypesAndTheirSuffix)
            {
                if (!wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(typeName, out var wellKnownNamedType)
                    || wellKnownNamedType.OriginalDefinition == null)
                {
                    continue;
                }

                // If the type is interface
                if (wellKnownNamedType.OriginalDefinition.TypeKind == TypeKind.Interface)
                {
                    interfaceTypeSuffixMapBuilder.Add(wellKnownNamedType.OriginalDefinition, suffixes);
                }
                else
                {
                    baseTypeSuffixMapBuilder.Add(wellKnownNamedType.OriginalDefinition, suffixes);
                }
            }

            var baseTypeSuffixMap = baseTypeSuffixMapBuilder.ToImmutable();
            var interfaceTypeSuffixMap = interfaceTypeSuffixMapBuilder.ToImmutable();

            context.RegisterSymbolAction(context =>
            {
                var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
                if (!context.Options.MatchesConfiguredVisibility(OneSuffixRule, namedTypeSymbol, context.Compilation))
                {
                    Debug.Assert(!context.Options.MatchesConfiguredVisibility(MultipleSuffixesRule, namedTypeSymbol, context.Compilation));
                    Debug.Assert(!context.Options.MatchesConfiguredVisibility(UserSuffixRule, namedTypeSymbol, context.Compilation));
                    return;
                }

                Debug.Assert(context.Options.MatchesConfiguredVisibility(MultipleSuffixesRule, namedTypeSymbol, context.Compilation));
                Debug.Assert(context.Options.MatchesConfiguredVisibility(UserSuffixRule, namedTypeSymbol, context.Compilation));

                var excludeIndirectBaseTypes = context.Options.GetBoolOptionValue(EditorConfigOptionNames.ExcludeIndirectBaseTypes, OneSuffixRule,
                   namedTypeSymbol, context.Compilation, defaultValue: true);

                var baseTypes = excludeIndirectBaseTypes
                    ? namedTypeSymbol.BaseType != null ? ImmutableArray.Create(namedTypeSymbol.BaseType) : ImmutableArray<INamedTypeSymbol>.Empty
                    : namedTypeSymbol.GetBaseTypes();

                var userTypeSuffixMap = context.Options.GetAdditionalRequiredSuffixesOption(UserSuffixRule, context.Symbol, context.Compilation);

                if (TryGetTypeSuffix(baseTypes, baseTypeSuffixMap, userTypeSuffixMap, out var typesSuffixes, out var rule))
                {
                    if (!typesSuffixes.Any(suffix => namedTypeSymbol.Name.EndsWith(suffix, StringComparison.Ordinal)))
                    {
                        context.ReportDiagnostic(namedTypeSymbol.CreateDiagnostic(rule, namedTypeSymbol.ToDisplayString(), typesSuffixes[0],
                            string.Join("', '", typesSuffixes.Skip(1))));
                    }
                }
                else
                {
                    var interfaces = excludeIndirectBaseTypes
                        ? namedTypeSymbol.Interfaces
                        : namedTypeSymbol.AllInterfaces;

                    if (TryGetTypeSuffix(interfaces, interfaceTypeSuffixMap, userTypeSuffixMap, out var interfaceSuffixes, out rule) &&
                        !interfaceSuffixes.Any(suffix => namedTypeSymbol.Name.EndsWith(suffix, StringComparison.Ordinal)))
                    {
                        context.ReportDiagnostic(namedTypeSymbol.CreateDiagnostic(rule, namedTypeSymbol.ToDisplayString(), interfaceSuffixes[0],
                            string.Join("', '", interfaceSuffixes.Skip(1))));
                    }
                }
            }
            , SymbolKind.NamedType);

            var eventArgsType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemEventArgs);
            if (eventArgsType == null)
            {
                return;
            }

            context.RegisterSymbolAction(context =>
            {
                var eventSymbol = (IEventSymbol)context.Symbol;
                if (!eventSymbol.Type.Name.EndsWith(EventHandlerString, StringComparison.Ordinal) &&
                    eventSymbol.Type.IsInSource() &&
                    eventSymbol.Type.TypeKind == TypeKind.Delegate &&
                    ((INamedTypeSymbol)eventSymbol.Type).DelegateInvokeMethod?.HasEventHandlerSignature(eventArgsType) == true)
                {
                    context.ReportDiagnostic(eventSymbol.CreateDiagnostic(OneSuffixRule, eventSymbol.Type.Name, EventHandlerString));
                }
            },
            SymbolKind.Event);
        }

        private static bool TryGetTypeSuffix(IEnumerable<INamedTypeSymbol> typeSymbols, ImmutableDictionary<INamedTypeSymbol, ImmutableArray<string>> hardcodedMap,
            SymbolNamesWithValueOption<string?> userMap, out ImmutableArray<string> suffixes, [NotNullWhen(true)] out DiagnosticDescriptor? rule)
        {
            foreach (var type in typeSymbols)
            {
                // User specific mapping has higher priority than hardcoded one
                if (userMap.TryGetValue(type.OriginalDefinition, out var suffix))
                {
                    if (!RoslynString.IsNullOrWhiteSpace(suffix))
                    {
                        suffixes = ImmutableArray.Create(suffix);
                        rule = UserSuffixRule;
                        return true;
                    }
                }
                else if (hardcodedMap.TryGetValue(type.OriginalDefinition, out suffixes))
                {
                    rule = suffixes.Length == 1 ? OneSuffixRule : MultipleSuffixesRule;
                    return true;
                }
            }

            suffixes = ImmutableArray<string>.Empty;
            rule = null;
            return false;
        }
    }
}