// 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
}
}