| File: Microsoft.NetCore.Analyzers\Runtime\PreferDictionaryContainsMethods.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.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; using static Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources; namespace Microsoft.NetCore.Analyzers.Runtime { /// <summary> /// CA1841: <inheritdoc cref="PreferDictionaryContainsMethodsTitle"/> /// </summary> public abstract class PreferDictionaryContainsMethods : DiagnosticAnalyzer { internal const string RuleId = "CA1841"; private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(PreferDictionaryContainsMethodsTitle)); internal static readonly DiagnosticDescriptor ContainsKeyRule = DiagnosticDescriptorHelper.Create( RuleId, s_localizableTitle, CreateLocalizableResourceString(nameof(PreferDictionaryContainsKeyMessage)), DiagnosticCategory.Performance, RuleLevel.IdeSuggestion, CreateLocalizableResourceString(nameof(PreferDictionaryContainsKeyDescription)), isPortedFxCopRule: false, isDataflowRule: false); internal static readonly DiagnosticDescriptor ContainsValueRule = DiagnosticDescriptorHelper.Create( RuleId, s_localizableTitle, CreateLocalizableResourceString(nameof(PreferDictionaryContainsValueMessage)), DiagnosticCategory.Performance, RuleLevel.IdeSuggestion, CreateLocalizableResourceString(nameof(PreferDictionaryContainsValueDescription)), isPortedFxCopRule: false, isDataflowRule: false); private const string ContainsMethodName = "Contains"; internal const string ContainsKeyMethodName = "ContainsKey"; internal const string ContainsValueMethodName = "ContainsValue"; internal const string KeysPropertyName = "Keys"; internal const string ValuesPropertyName = "Values"; public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(ContainsKeyRule, ContainsValueRule); public sealed override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(OnCompilationStart); } private void OnCompilationStart(CompilationStartAnalysisContext compilationContext) { var compilation = compilationContext.Compilation; if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericICollection1, out var icollectionType)) return; if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIDictionary2, out var idictionaryType)) return; if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIEnumerable1, out var ienumerableType)) return; var linqExpressionType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqExpressionsExpression1); compilationContext.RegisterOperationAction(OnOperationAction, OperationKind.Invocation); return; // Local functions. void OnOperationAction(OperationAnalysisContext context) { // We report a diagnostic if we find an invocation of an applicable Contains method, // and the contains method is being invoked on an applicable property. // A property is applicable if: // 1) It belongs to a type that implements IDictionary`2 // 2) It's name is either "Keys" or "Values" // A Contains method is applicable if: // 1) It has a boolean return type. // 2) It has one argument, not counting the first argument of an extension method. // 3) The argument must match the applicable type argument. // - If the property's name is "Keys", it must match the type substituted for TKey in the IDictionary`2 instance. // - If the property's name is "Values", it must match the type substituted for TValue in the IDictionary`2 instance. // Once we have an applicable Contains method and an applicable property reference, we search for an applicable ContainsXXX // method on the IDictionary`2 receiver. // A ContainsXXX method is applicable if: // 1) It has a boolean return type // 2) It is publically accessible // 3) Its name is "ContainsKey" if property is "Keys", or "ContainsValue" if property is "Values". // 4) It has exactly one parameter of the correct type: // - If the method is a "ContainsKey" method, its type must be TKey. // - If the method is a "ContainsValue" method, its type must be TValue. var invocation = (IInvocationOperation)context.Operation; IMethodSymbol containsMethod = invocation.TargetMethod; // Check if we are in a Expression<Func<T...>> context, in which case it is possible // that the underlying call doesn't have the comparison option so we want to bail-out. if (invocation.IsWithinExpressionTree(linqExpressionType)) { return; } if (containsMethod.Name != ContainsMethodName || containsMethod.ReturnType.SpecialType != SpecialType.System_Boolean || !TryGetPropertyReferenceOperation(invocation, out IPropertySymbol? property) || !TryGetConstructedDictionaryType(property.ContainingType, out INamedTypeSymbol? constructedDictionaryType)) { return; } // At this point, we know that the method being invoked is a method called "Contains" that has a boolean return type. // We also know that the method is being invoked on a property belonging to a type that implements IDictionary`2. We will // compare the types used to construct IDictionary`2 to the parameter type of the Contains method. ITypeSymbol keyType = constructedDictionaryType.TypeArguments[0]; ITypeSymbol valueType = constructedDictionaryType.TypeArguments[1]; // We use Parameters.Last() because the first argument could be the key/value collection, depending // on whether the method is an extension method and whether the language is C# or Visual Basic. ITypeSymbol containsParameterType = containsMethod.Parameters[^1].Type; if (property.Name == KeysPropertyName) { if (!containsParameterType.Equals(keyType, SymbolEqualityComparer.Default)) return; // Now we search for an accessible ContainsKey method that returns boolean and accepts a single // parameter of the type that was substituted for TKey. IMethodSymbol? containsKeyMethod = property.ContainingType.GetMembers(ContainsKeyMethodName) .OfType<IMethodSymbol>() .FirstOrDefault(x => x.ReturnType.SpecialType == SpecialType.System_Boolean && x.IsPublic() && x.Parameters.Length == 1 && x.Parameters[0].Type.Equals(keyType, SymbolEqualityComparer.Default)); if (containsKeyMethod is null) return; var diagnostic = invocation.CreateDiagnostic(ContainsKeyRule, property.ContainingType.Name); context.ReportDiagnostic(diagnostic); } else if (property.Name == ValuesPropertyName) { if (!containsParameterType.Equals(valueType, SymbolEqualityComparer.Default)) return; // Now we search for an accessible ContainsValue method that returns boolean and accepts a single // parameter of the type that was substituted for TValue. IMethodSymbol? containsValueMethod = property.ContainingType.GetMembers(ContainsValueMethodName) .OfType<IMethodSymbol>() .FirstOrDefault(x => x.ReturnType.SpecialType == SpecialType.System_Boolean && x.IsPublic() && x.Parameters.Length == 1 && x.Parameters[0].Type.Equals(valueType, SymbolEqualityComparer.Default)); if (containsValueMethod is null) return; var diagnostic = invocation.CreateDiagnostic(ContainsValueRule, property.ContainingType.Name); context.ReportDiagnostic(diagnostic); } } bool TryGetConstructedDictionaryType(INamedTypeSymbol derived, [NotNullWhen(true)] out INamedTypeSymbol? constructedDictionaryType) { constructedDictionaryType = derived.GetBaseTypesAndThis() .FirstOrDefault(x => x.OriginalDefinition.Equals(idictionaryType, SymbolEqualityComparer.Default)); constructedDictionaryType ??= derived.AllInterfaces .FirstOrDefault(x => x.OriginalDefinition.Equals(idictionaryType, SymbolEqualityComparer.Default)); return constructedDictionaryType is not null; } } private protected abstract bool TryGetPropertyReferenceOperation(IInvocationOperation containsInvocation, [NotNullWhen(true)] out IPropertySymbol? propertySymbol); } }