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