| File: Microsoft.NetCore.Analyzers\Usage\PreferGenericOverloads.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 System.Threading; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.NetCore.Analyzers.Usage { using static MicrosoftNetCoreAnalyzersResources; /// <summary> /// CA2263: <inheritdoc cref="PreferGenericOverloadsTitle"/> /// </summary> public abstract class PreferGenericOverloadsAnalyzer : DiagnosticAnalyzer { internal const string RuleId = "CA2263"; internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(PreferGenericOverloadsTitle)), CreateLocalizableResourceString(nameof(PreferGenericOverloadsMessage)), DiagnosticCategory.Usage, RuleLevel.IdeSuggestion, CreateLocalizableResourceString(nameof(PreferGenericOverloadsDescription)), isPortedFxCopRule: false, isDataflowRule: false); public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterCompilationStartAction(context => context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation)); } private void AnalyzeInvocation(OperationAnalysisContext context) { if (!RuntimeTypeInvocationContext.TryGetContext((IInvocationOperation)context.Operation, out var invocationContext)) { return; } // Get all methods on the containing type with the same name as the original invocation that are applicable generic overloads. var genericInvocation = invocationContext.Method.ContainingType .GetMembers(invocationContext.Method.Name) .OfType<IMethodSymbol>() .Where(IsApplicableGenericOverload) .FirstOrDefault(); if (genericInvocation is not null) { context.ReportDiagnostic(invocationContext.Invocation.CreateDiagnostic( Rule, genericInvocation.ToDisplayString(), invocationContext.Method.ToDisplayString())); } // A generic overload is applicable iff: // 1. The arity is the same as the type parameters of the original invocation // 2. The parameters count is the same as the other arguments of the original invocation // 3. It is not the same as the containing symbol containing the original invocation // This is to prevent cases where the generic method forwards to a non generic one, e.g. Foo<T>() calls Foo(typeof(T)). // Without this condition we would create an infinite loop as we would replace Foo(typeof(T)) with Foo<T>(). // 4. No nullability generic constraint is violated. // We must explicitly check for this, and not rely on the speculative binding later, since it will still succeed even if a notnull constraint is violated. // 5. The return type is assignable to the original return type. We do not check the return type for expression statements. // 6. All other arguments of the original invocation are assignable to the parameters of the method. // 7. Speculative binding of the new invocation succeeds; this is to check if any type parameter constraints are violated. bool IsApplicableGenericOverload(IMethodSymbol method) { // Reduce method if original method was reduced. if (invocationContext.Method.ReducedFrom is not null) { method = invocationContext.ReduceExtensionMethodOrOriginal(method, context.Compilation, context.CancellationToken); } if (method.Arity != invocationContext.TypeArguments.Length || method.Parameters.Length != invocationContext.OtherArguments.Length || SymbolEqualityComparer.Default.Equals(method, context.ContainingSymbol)) { return false; } var genericMethod = method.Construct(invocationContext.TypeArguments.ToArray()); if (AreNullabilityConstraintsViolated(method) || !invocationContext.IsReturnTypeCompatible(genericMethod, context.Compilation) || !invocationContext.AreOtherArgumentsCompatible(genericMethod, context.Compilation) || !TryGetModifiedInvocationSyntax(invocationContext, out var modifiedInvocationSyntax)) { return false; } var speculativeSymbolInfo = invocationContext.SemanticModel?.GetSpeculativeSymbolInfo( invocationContext.Syntax.SpanStart, modifiedInvocationSyntax, SpeculativeBindingOption.BindAsExpression); // Check if the expression was bound successfully. if (speculativeSymbolInfo?.Symbol is not IMethodSymbol boundMethod) { return false; } // Reduce the constructed method if the bound method is reduced to be able to compare them. if (boundMethod.ReducedFrom is not null) { genericMethod = invocationContext.ReduceExtensionMethodOrOriginal(genericMethod, context.Compilation, context.CancellationToken); } // Check if the speculative symbol was bound to the same method. // This prevents cases where we bind to a overload that was ruled out before (e.g. it is the same as the containing symbol). return SymbolEqualityComparer.Default.Equals(boundMethod, genericMethod) && SymbolEqualityComparer.Default.Equals(boundMethod.ReturnType, genericMethod.ReturnType); static bool AreNullabilityConstraintsViolated(IMethodSymbol method) { for (int i = 0; i < method.TypeParameters.Length; i++) { if (method.TypeParameters[i].HasNotNullConstraint && method.TypeArguments[i].CanHoldNullValue()) { return true; } } return false; } } } // Make the context internal to be also usable in the fixer. protected internal sealed class RuntimeTypeInvocationContext { private RuntimeTypeInvocationContext( IInvocationOperation invocation, ImmutableArray<ITypeSymbol> typeArguments, ImmutableArray<IArgumentOperation> otherArguments) { Invocation = invocation; TypeArguments = typeArguments; OtherArguments = otherArguments; } public static bool TryGetContext(IInvocationOperation invocation, [NotNullWhen(true)] out RuntimeTypeInvocationContext? invocationContext) { invocationContext = default; var argumentsInParameterOrder = invocation.Arguments.GetArgumentsInParameterOrder(); var typeOfArguments = argumentsInParameterOrder.WhereAsArray(a => a.Value is ITypeOfOperation); // Bail out if there is no argument using the typeof operator. if (typeOfArguments.Length == 0) { return false; } // Split arguments into type arguments and other arguments passed to a potential generic overload. var typeArguments = typeOfArguments .Select(a => a.Value) .OfType<ITypeOfOperation>() .Select(t => t.TypeOperand) .Where(t => t is not INamedTypeSymbol { IsUnboundGenericType: true }) .ToImmutableArray(); // Bail out if there are no type arguments left after filtering out unbound generic types. if (typeArguments.Length == 0) { return false; } var otherArguments = argumentsInParameterOrder.RemoveRange(typeOfArguments); invocationContext = new RuntimeTypeInvocationContext(invocation, typeArguments, otherArguments); return true; } public IInvocationOperation Invocation { get; } public ImmutableArray<ITypeSymbol> TypeArguments { get; } public ImmutableArray<IArgumentOperation> OtherArguments { get; } public SemanticModel? SemanticModel => Invocation.SemanticModel; public IMethodSymbol Method => Invocation.TargetMethod; public SyntaxNode Syntax => Invocation.Syntax; public IOperation? Parent => Invocation.Parent; public IMethodSymbol ReduceExtensionMethodOrOriginal(IMethodSymbol method, Compilation compilation, CancellationToken cancellationToken) { if (method.IsExtensionMethod) { var receiverType = Invocation.GetReceiverType(compilation, false, cancellationToken); if (receiverType is not null) { return method.ReduceExtensionMethod(receiverType) ?? method; } } return method; } public bool IsReturnTypeCompatible(IMethodSymbol method, Compilation compilation) { // We do not care if we change the return type if it is an expression statement. if (Parent is IExpressionStatementOperation) { return true; } return method.ReturnType.IsAssignableTo(Method.ReturnType, compilation); } public bool AreOtherArgumentsCompatible(IMethodSymbol method, Compilation compilation) { for (int i = 0; i < OtherArguments.Length; i++) { if (!OtherArguments[i].Value.WalkDownConversion().Type.IsAssignableTo(method.Parameters[i].Type, compilation)) { return false; } } return true; } } protected abstract bool TryGetModifiedInvocationSyntax(RuntimeTypeInvocationContext invocationContext, [NotNullWhen(true)] out SyntaxNode? modifiedInvocationSyntax); } }