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