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

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

    /// <summary>
    /// CA1708: <inheritdoc cref="IdentifiersShouldDifferByMoreThanCaseTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class IdentifiersShouldDifferByMoreThanCaseAnalyzer : DiagnosticAnalyzer
    {
        public const string RuleId = "CA1708";
        public const string Namespace = "Namespaces";
        public const string Type = "Types";
        public const string Member = "Members";
        public const string Parameter = "Parameters of";

        internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(IdentifiersShouldDifferByMoreThanCaseTitle)),
            CreateLocalizableResourceString(nameof(IdentifiersShouldDifferByMoreThanCaseMessage)),
            DiagnosticCategory.Naming,
            RuleLevel.IdeHidden_BulkConfigurable,
            description: CreateLocalizableResourceString(nameof(IdentifiersShouldDifferByMoreThanCaseDescription)),
            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.RegisterCompilationAction(AnalyzeCompilation);
            context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
        }

        private static void AnalyzeCompilation(CompilationAnalysisContext context)
        {
            IEnumerable<INamespaceSymbol> globalNamespaces = context.Compilation.GlobalNamespace.GetNamespaceMembers()
                .Where(item => Equals(item.ContainingAssembly, context.Compilation.Assembly));

            IEnumerable<INamedTypeSymbol> globalTypes = context.Compilation.GlobalNamespace.GetTypeMembers().Where(item =>
                    Equals(item.ContainingAssembly, context.Compilation.Assembly) &&
                    MatchesConfiguredVisibility(item, context.Options, context.Compilation) &&
                    !item.IsFileLocal());

            CheckTypeNames(globalTypes, context);
            CheckNamespaceMembers(globalNamespaces, context);
        }

        private static void AnalyzeSymbol(SymbolAnalysisContext context)
        {
            var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;

            // Do not descent into non-publicly visible types by default
            // Note: This is the behavior of FxCop, it might be more correct to descend into internal but not private
            // types because "InternalsVisibleTo" could be set. But it might be bad for users to start seeing warnings
            // where they previously did not from FxCop.
            // Note that end user can now override this default behavior via options.
            if (!context.Options.MatchesConfiguredVisibility(Rule, namedTypeSymbol, context.Compilation))
            {
                return;
            }

            // Get externally visible members in the given type
            IEnumerable<ISymbol> members = namedTypeSymbol.GetMembers()
                                                          .Where(item => !item.IsAccessorMethod() &&
                                                                         MatchesConfiguredVisibility(item, context.Options, context.Compilation));

            if (members.Any())
            {
                // Check parameters names of externally visible members with parameters
                CheckParameterMembers(members, context.ReportDiagnostic);

                // Check names of externally visible type members and their members
                CheckTypeMembers(members, context.ReportDiagnostic);
            }
        }

        private static void CheckNamespaceMembers(IEnumerable<INamespaceSymbol> namespaces, CompilationAnalysisContext context)
        {
            HashSet<INamespaceSymbol> excludedNamespaces = new HashSet<INamespaceSymbol>();
            foreach (INamespaceSymbol @namespace in namespaces)
            {
                // Get all the potentially externally visible types in the namespace
                IEnumerable<INamedTypeSymbol> typeMembers = @namespace.GetTypeMembers().Where(item =>
                    Equals(item.ContainingAssembly, context.Compilation.Assembly) &&
                    MatchesConfiguredVisibility(item, context.Options, context.Compilation) &&
                    !item.IsFileLocal());

                if (typeMembers.Any())
                {
                    CheckTypeNames(typeMembers, context);
                }
                else
                {
                    // If the namespace does not contain any externally visible types then exclude it from name check
                    excludedNamespaces.Add(@namespace);
                }

                IEnumerable<INamespaceSymbol> namespaceMembers = @namespace.GetNamespaceMembers();
                if (namespaceMembers.Any())
                {
                    CheckNamespaceMembers(namespaceMembers, context);

                    // If there is a child namespace that has externally visible types, then remove the parent namespace from exclusion list
                    if (namespaceMembers.Any(item => !excludedNamespaces.Contains(item)))
                    {
                        excludedNamespaces.Remove(@namespace);
                    }
                }
            }

            // Before name check, remove all namespaces that don't contain externally visible types in current scope
            namespaces = namespaces.Where(item => !excludedNamespaces.Contains(item));

            CheckNamespaceNames(namespaces, context);
        }

        private static void CheckTypeMembers(IEnumerable<ISymbol> members, Action<Diagnostic> addDiagnostic)
        {
            // If there is only one member, then return
            if (!members.Skip(1).Any())
            {
                return;
            }

            using var overloadsToSkip = PooledHashSet<ISymbol>.GetInstance();
            using var membersByName = PooledDictionary<string, PooledHashSet<ISymbol>>.GetInstance(StringComparer.OrdinalIgnoreCase);
            foreach (var member in members)
            {
                // Ignore constructors, indexers, operators, destructors and extension blocks for name check
                if (member.IsConstructor() ||
                    member.IsDestructor() ||
                    member.IsIndexer() ||
                    member.IsUserDefinedOperator() ||
                    member.IsExtension() ||
                    overloadsToSkip.Contains(member))
                {
                    continue;
                }

                var name = DiagnosticHelpers.GetMemberName(member);
                if (!membersByName.TryGetValue(name, out var membersWithName))
                {
                    membersWithName = PooledHashSet<ISymbol>.GetInstance();
                    membersByName[name] = membersWithName;
                }

                membersWithName.Add(member);

                if (member is IMethodSymbol method)
                {
                    foreach (var overload in method.GetOverloads())
                    {
                        overloadsToSkip.Add(overload);
                    }
                }
            }

            foreach (var (name, membersWithName) in membersByName)
            {
                if (membersWithName.Count > 1 &&
                    !membersWithName.All(m => m.IsOverride))
                {
                    ISymbol symbol = membersWithName.First().ContainingSymbol;
                    addDiagnostic(symbol.CreateDiagnostic(Rule, Member, GetSymbolDisplayString(membersWithName)));
                }

                membersWithName.Dispose();
            }
        }

        private static void CheckParameterMembers(IEnumerable<ISymbol> members, Action<Diagnostic> addDiagnostic)
        {
            foreach (var member in members)
            {
                if (IsViolatingMember(member) || IsViolatingDelegate(member))
                {
                    addDiagnostic(member.CreateDiagnostic(Rule, Parameter, member.ToDisplayString()));
                }
            }

            return;

            // Local functions
            static bool IsViolatingMember(ISymbol member)
                => member.ContainingType.DelegateInvokeMethod == null &&
                   HasViolatingParameters(member);

            static bool IsViolatingDelegate(ISymbol member)
                => member is INamedTypeSymbol typeSymbol &&
                   typeSymbol.DelegateInvokeMethod != null &&
                   HasViolatingParameters(typeSymbol.DelegateInvokeMethod);
        }

        #region NameCheck Methods

        private static bool HasViolatingParameters(ISymbol symbol)
        {
            var parameters = symbol.GetParameters();

            // We only analyze symbols with more then one parameter.
            if (parameters.Length <= 1)
            {
                return false;
            }

            using var uniqueNames = PooledHashSet<string>.GetInstance(StringComparer.OrdinalIgnoreCase);
            foreach (var parameter in parameters)
            {
                if (!uniqueNames.Add(parameter.Name))
                {
                    return true;
                }
            }

            return false;
        }

        private static void CheckTypeNames(IEnumerable<INamedTypeSymbol> types, CompilationAnalysisContext context)
        {
            // If there is only one type, then return
            if (!types.Skip(1).Any())
            {
                return;
            }

            using var typesByName = PooledDictionary<string, PooledHashSet<ISymbol>>.GetInstance(StringComparer.OrdinalIgnoreCase);
            foreach (var type in types)
            {
                var name = DiagnosticHelpers.GetMemberName(type);
                if (!typesByName.TryGetValue(name, out var typesWithName))
                {
                    typesWithName = PooledHashSet<ISymbol>.GetInstance();
                    typesByName[name] = typesWithName;
                }

                typesWithName.Add(type);
            }

            foreach (var (_, typesWithName) in typesByName)
            {
                if (typesWithName.Count > 1)
                {
                    context.ReportNoLocationDiagnostic(Rule, Type, GetSymbolDisplayString(typesWithName));
                }

                typesWithName.Dispose();
            }
        }

        private static void CheckNamespaceNames(IEnumerable<INamespaceSymbol> namespaces, CompilationAnalysisContext context)
        {
            // If there is only one namespace, then return
            if (!namespaces.Skip(1).Any())
            {
                return;
            }

            using var namespacesByName = PooledDictionary<string, PooledHashSet<ISymbol>>.GetInstance(StringComparer.OrdinalIgnoreCase);
            foreach (var namespaceSym in namespaces)
            {
                var name = namespaceSym.ToDisplayString();
                if (!namespacesByName.TryGetValue(name, out var namespacesWithName))
                {
                    namespacesWithName = PooledHashSet<ISymbol>.GetInstance();
                    namespacesByName[name] = namespacesWithName;
                }

                namespacesWithName.Add(namespaceSym);
            }

            foreach (var (_, namespacesWithName) in namespacesByName)
            {
                if (namespacesWithName.Count > 1)
                {
                    context.ReportNoLocationDiagnostic(Rule, Namespace, GetSymbolDisplayString(namespacesWithName));
                }

                namespacesWithName.Dispose();
            }
        }

        #endregion

        #region Helper Methods

        private static string GetSymbolDisplayString(PooledHashSet<ISymbol> group)
        {
            return string.Join(", ", group.Select(s => s.ToDisplayString()).OrderBy(k => k, StringComparer.Ordinal));
        }

        public static bool MatchesConfiguredVisibility(ISymbol symbol, AnalyzerOptions options, Compilation compilation)
        {
            var defaultAllowedVisibilties = SymbolVisibilityGroup.Public | SymbolVisibilityGroup.Internal;
            var allowedVisibilities = options.GetSymbolVisibilityGroupOption(Rule, symbol, compilation, defaultAllowedVisibilties);
            return allowedVisibilities.Contains(symbol.GetResultantVisibility());
        }

        #endregion
    }
}