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

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

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

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(IdentifiersShouldNotHaveIncorrectSuffixTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(IdentifiersShouldNotHaveIncorrectSuffixDescription));

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

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

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

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

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
            TypeNoAlternateRule,
            MemberNewerVersionRule,
            TypeNewerVersionRule,
            MemberWithAlternateRule);

        internal const string AttributeSuffix = "Attribute";
        internal const string CollectionSuffix = "Collection";
        internal const string DictionarySuffix = "Dictionary";
        internal const string EventArgsSuffix = "EventArgs";
        internal const string EventHandlerSuffix = "EventHandler";
        internal const string ExSuffix = "Ex";
        internal const string ExceptionSuffix = "Exception";
        internal const string NewSuffix = "New";
        internal const string PermissionSuffix = "Permission";
        internal const string StreamSuffix = "Stream";
        internal const string DelegateSuffix = "Delegate";
        internal const string EnumSuffix = "Enum";
        internal const string ImplSuffix = "Impl";
        internal const string CoreSuffix = "Core";
        internal const string QueueSuffix = "Queue";
        internal const string StackSuffix = "Stack";
        internal const string FlagSuffix = "Flag";
        internal const string FlagsSuffix = "Flags";

        // Dictionary that maps from a type name suffix to the set of base types from which
        // a type with that suffix is permitted to derive.
        private static readonly ImmutableDictionary<string, ImmutableArray<string>> s_suffixToBaseTypeNamesDictionary = ImmutableDictionary.CreateRange(
            new Dictionary<string, ImmutableArray<string>>
            {
                [AttributeSuffix] = ImmutableArray.CreateRange(new[] { "System.Attribute" }),
                [CollectionSuffix] = ImmutableArray.CreateRange(new[] { "System.Collections.IEnumerable" }),
                [DictionarySuffix] = ImmutableArray.CreateRange(new[] { "System.Collections.IDictionary", "System.Collections.Generic.IDictionary`2", "System.Collections.Generic.IReadOnlyDictionary`2" }),
                [EventArgsSuffix] = ImmutableArray.CreateRange(new[] { "System.EventArgs" }),
                [ExceptionSuffix] = ImmutableArray.CreateRange(new[] { "System.Exception" }),
                [PermissionSuffix] = ImmutableArray.CreateRange(new[] { "System.Security.IPermission" }),
                [StreamSuffix] = ImmutableArray.CreateRange(new[] { "System.IO.Stream" }),
                [QueueSuffix] = ImmutableArray.CreateRange(new[] { "System.Collections.Queue", "System.Collections.Generic.Queue`1" }),
                [StackSuffix] = ImmutableArray.CreateRange(new[] { "System.Collections.Stack", "System.Collections.Generic.Stack`1" })
            });

        // Dictionary from type name suffix to an array containing the only types that are
        // allowed to have that suffix.
        private static readonly ImmutableDictionary<string, ImmutableArray<string>> s_suffixToAllowedTypesDictionary = ImmutableDictionary.CreateRange(
            new Dictionary<string, ImmutableArray<string>>
            {
                [DelegateSuffix] = ImmutableArray.CreateRange(new[] { "System.Delegate", "System.MulticastDelegate" }),
                [EventHandlerSuffix] = ImmutableArray.CreateRange(new[] { "System.EventHandler" }),
                [EnumSuffix] = ImmutableArray.CreateRange(new[] { "System.Enum" })
            });

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

            // Analyze type names.
            context.RegisterCompilationStartAction(
                compilationStartAnalysisContext =>
                {
                    var suffixToBaseTypeDictionaryBuilder = ImmutableDictionary.CreateBuilder<string, ImmutableArray<INamedTypeSymbol>>();

                    foreach (string suffix in s_suffixToBaseTypeNamesDictionary.Keys)
                    {
                        ImmutableArray<string> typeNames = s_suffixToBaseTypeNamesDictionary[suffix];

                        ImmutableArray<INamedTypeSymbol> namedTypeSymbolArray = ImmutableArray.CreateRange(
                            typeNames.Select(typeName => compilationStartAnalysisContext.Compilation.GetOrCreateTypeByMetadataName(typeName)?.OriginalDefinition).WhereNotNull());

                        suffixToBaseTypeDictionaryBuilder.Add(suffix, namedTypeSymbolArray);
                    }

                    var suffixToBaseTypeDictionary = suffixToBaseTypeDictionaryBuilder.ToImmutableDictionary();

                    compilationStartAnalysisContext.RegisterSymbolAction(
                        (SymbolAnalysisContext symbolAnalysisContext) =>
                        {
                            var namedTypeSymbol = (INamedTypeSymbol)symbolAnalysisContext.Symbol;

                            // Note all the descriptors/rules for this analyzer have the same ID and category and hence
                            // will always have identical configured visibility.
                            if (!symbolAnalysisContext.Options.MatchesConfiguredVisibility(TypeNoAlternateRule, namedTypeSymbol, symbolAnalysisContext.Compilation))
                            {
                                return;
                            }

                            var allowedSuffixes = symbolAnalysisContext.Options.GetStringOptionValue(EditorConfigOptionNames.AllowedSuffixes, TypeNoAlternateRule,
                                    namedTypeSymbol.Locations[0].SourceTree!, symbolAnalysisContext.Compilation)
                                .Split('|')
                                .ToImmutableHashSet();

                            string name = namedTypeSymbol.Name;
                            Compilation compilation = symbolAnalysisContext.Compilation;

                            foreach (string suffix in s_suffixToBaseTypeNamesDictionary.Keys)
                            {
                                if (IsNotChildOfAnyButHasSuffix(namedTypeSymbol, suffixToBaseTypeDictionary[suffix], suffix, allowedSuffixes))
                                {
                                    symbolAnalysisContext.ReportDiagnostic(
                                        namedTypeSymbol.CreateDiagnostic(TypeNoAlternateRule, name, suffix));
                                    return;
                                }
                            }

                            foreach (string suffix in s_suffixToAllowedTypesDictionary.Keys)
                            {
                                if (IsInvalidSuffix(name, suffix, allowedSuffixes)
                                    && !s_suffixToAllowedTypesDictionary[suffix].Contains(name))
                                {
                                    symbolAnalysisContext.ReportDiagnostic(
                                        namedTypeSymbol.CreateDiagnostic(TypeNoAlternateRule, name, suffix));
                                    return;
                                }
                            }

                            if (IsInvalidSuffix(name, ImplSuffix, allowedSuffixes))
                            {
                                symbolAnalysisContext.ReportDiagnostic(
                                    namedTypeSymbol.CreateDiagnostic(MemberWithAlternateRule, ImplSuffix, name, CoreSuffix));
                                return;
                            }

                            // FxCop performed the length check for "Ex", but not for any of the other
                            // suffixes, because alone among the suffixes, "Ex" is the only one that
                            // isn't itself a known type or a language keyword.
                            if (IsInvalidSuffix(name, ExSuffix, allowedSuffixes) && name.Length > ExSuffix.Length)
                            {
                                symbolAnalysisContext.ReportDiagnostic(
                                    namedTypeSymbol.CreateDiagnostic(TypeNewerVersionRule, ExSuffix, name));
                                return;
                            }

                            if (IsInvalidSuffix(name, NewSuffix, allowedSuffixes))
                            {
                                symbolAnalysisContext.ReportDiagnostic(
                                    namedTypeSymbol.CreateDiagnostic(TypeNewerVersionRule, NewSuffix, name));
                                return;
                            }

                            if (namedTypeSymbol.TypeKind == TypeKind.Enum)
                            {
                                if (IsInvalidSuffix(name, FlagSuffix, allowedSuffixes))
                                {
                                    symbolAnalysisContext.ReportDiagnostic(
                                        namedTypeSymbol.CreateDiagnostic(TypeNoAlternateRule, name, FlagSuffix));
                                    return;
                                }

                                if (IsInvalidSuffix(name, FlagsSuffix, allowedSuffixes))
                                {
                                    symbolAnalysisContext.ReportDiagnostic(
                                        namedTypeSymbol.CreateDiagnostic(TypeNoAlternateRule, name, FlagsSuffix));
                                    return;
                                }
                            }
                        }, SymbolKind.NamedType);
                });

            // Analyze method names.
            context.RegisterSymbolAction(
                (SymbolAnalysisContext context) =>
                {
                    var memberSymbol = context.Symbol;

                    // Note all the descriptors/rules for this analyzer have the same ID and category and hence
                    // will always have identical configured visibility.
                    if (!context.Options.MatchesConfiguredVisibility(TypeNoAlternateRule, memberSymbol, context.Compilation))
                    {
                        return;
                    }

                    if (memberSymbol.IsOverride || memberSymbol.IsImplementationOfAnyInterfaceMember())
                    {
                        return;
                    }

                    // If this is a method, and it's actually the getter or setter of a property,
                    // then don't complain. We'll complain about the property itself.
                    if (memberSymbol is IMethodSymbol methodSymbol && methodSymbol.IsPropertyAccessor())
                    {
                        return;
                    }

                    string name = memberSymbol.Name;

                    var allowedSuffixes = context.Options.GetStringOptionValue(EditorConfigOptionNames.AllowedSuffixes, TypeNoAlternateRule,
                            memberSymbol.Locations[0].SourceTree!, context.Compilation)
                        .Split('|')
                        .ToImmutableHashSet();

                    if (IsInvalidSuffix(name, ExSuffix, allowedSuffixes))
                    {
                        context.ReportDiagnostic(
                            memberSymbol.CreateDiagnostic(MemberNewerVersionRule, ExSuffix, name));
                        return;
                    }

                    // We only fire on member suffix "New" if the type already defines
                    // another member minus the suffix, e.g., we only fire on "MemberNew" if
                    // "Member" already exists. For some reason FxCop did not apply the
                    // same logic to the "Ex" suffix, and we follow FxCop's implementation.
                    if (IsInvalidSuffix(name, NewSuffix, allowedSuffixes))
                    {
                        string nameWithoutSuffix = name.WithoutSuffix(NewSuffix);
                        INamedTypeSymbol containingType = memberSymbol.ContainingType;

                        if (MemberNameExistsInHierarchy(nameWithoutSuffix, containingType, memberSymbol.Kind))
                        {
                            context.ReportDiagnostic(
                                memberSymbol.CreateDiagnostic(MemberNewerVersionRule, NewSuffix, name));
                            return;
                        }
                    }

                    if (IsInvalidSuffix(name, ImplSuffix, allowedSuffixes))
                    {
                        context.ReportDiagnostic(
                            memberSymbol.CreateDiagnostic(MemberWithAlternateRule, ImplSuffix, name, CoreSuffix));
                    }
                }, SymbolKind.Event, SymbolKind.Field, SymbolKind.Method, SymbolKind.Property);
        }

        private static bool MemberNameExistsInHierarchy(string memberName, INamedTypeSymbol containingType, SymbolKind kind)
        {
            for (INamedTypeSymbol? baseType = containingType; baseType != null; baseType = baseType.BaseType)
            {
                if (baseType.GetMembers(memberName).Any(member => member.Kind == kind))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool IsNotChildOfAnyButHasSuffix(INamedTypeSymbol namedTypeSymbol, ImmutableArray<INamedTypeSymbol> parentTypes, string suffix, ImmutableHashSet<string> allowedSuffixes)
        {
            if (parentTypes.IsEmpty)
            {
                // Bail out if we cannot find any well-known types with the suffix in the compilation.
                return false;
            }

            return IsInvalidSuffix(namedTypeSymbol.Name, suffix, allowedSuffixes)
                && !parentTypes.Any(namedTypeSymbol.DerivesFromOrImplementsAnyConstructionOf);
        }

        private static bool IsInvalidSuffix(string name, string suffix, ImmutableHashSet<string> allowedSuffixes)
            => !allowedSuffixes.Contains(suffix) && name.HasSuffix(suffix);
    }
}