diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/InvocationResolvesToOverrideWithParams.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/InvocationResolvesToOverrideWithParams.cs index ff445bdddc6..c3b67d04a04 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/InvocationResolvesToOverrideWithParams.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/InvocationResolvesToOverrideWithParams.cs @@ -25,28 +25,21 @@ public sealed class InvocationResolvesToOverrideWithParams : SonarDiagnosticAnal { private const string DiagnosticId = "S3220"; private const string MessageFormat = "Review this call, which partially matches an overload without 'params'. The partial match is '{0}'."; - private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat); - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); - protected override void Initialize(SonarAnalysisContext context) - { - context.RegisterNodeAction( - c => - { - var invocation = (InvocationExpressionSyntax)c.Node; - CheckCall(c, invocation, invocation.ArgumentList); - }, - SyntaxKind.InvocationExpression); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + protected override void Initialize(SonarAnalysisContext context) => context.RegisterNodeAction( c => { - var objectCreation = (ObjectCreationExpressionSyntax)c.Node; - CheckCall(c, objectCreation, objectCreation.ArgumentList); + var node = c.Node; + var argumentList = (node as InvocationExpressionSyntax)?.ArgumentList + ?? ((ObjectCreationExpressionSyntax)node).ArgumentList; + CheckCall(c, node, argumentList); }, + SyntaxKind.InvocationExpression, SyntaxKind.ObjectCreationExpression); - } private static void CheckCall(SonarSyntaxNodeReportingContext context, SyntaxNode node, ArgumentListSyntax argumentList) { @@ -55,14 +48,16 @@ private static void CheckCall(SonarSyntaxNodeReportingContext context, SyntaxNod && method.Parameters.LastOrDefault() is { IsParams: true } && !IsInvocationWithExplicitArray(argumentList, method, context.SemanticModel) && ArgumentTypes(context, argumentList) is var argumentTypes - && argumentTypes.All(x => x is not IErrorTypeSymbol) - && OtherOverloadsOf(method).FirstOrDefault(IsPossibleMatch) is { } otherMethod) + && Array.TrueForAll(argumentTypes, x => x is not IErrorTypeSymbol) + && OtherOverloadsOf(method).FirstOrDefault(IsPossibleMatch) is { } otherMethod + && method.IsGenericMethod == otherMethod.IsGenericMethod) { context.ReportIssue(Rule, node, otherMethod.ToMinimalDisplayString(context.SemanticModel, node.SpanStart)); } bool IsPossibleMatch(IMethodSymbol method) => - ArgumentsMatchParameters(argumentList, argumentTypes, method, context.SemanticModel) && MethodAccessibleWithinType(method, context.ContainingSymbol.ContainingType); + ArgumentsMatchParameters(argumentList, argumentTypes, method, context.SemanticModel) + && MethodAccessibleWithinType(method, context.ContainingSymbol.ContainingType); } private static ITypeSymbol[] ArgumentTypes(SonarSyntaxNodeReportingContext context, ArgumentListSyntax argumentList) => @@ -81,7 +76,7 @@ private static bool IsInvocationWithExplicitArray(ArgumentListSyntax argumentLis { var lookup = new CSharpMethodParameterLookup(argumentList, invokedMethodSymbol); var parameters = argumentList.Arguments.Select(Valid).ToArray(); - return parameters.All(x => x is not null) && parameters.Count(x => x.IsParams) == 1; + return Array.TrueForAll(parameters, x => x is not null) && parameters.Count(x => x.IsParams) == 1; IParameterSymbol Valid(ArgumentSyntax argument) => lookup.TryGetSymbol(argument, out var parameter) @@ -94,7 +89,7 @@ private static bool ArgumentsMatchParameters(ArgumentListSyntax argumentList, IT { var lookup = new CSharpMethodParameterLookup(argumentList, possibleOtherMethod); var parameters = argumentList.Arguments.Select((argument, index) => Valid(argument, argumentTypes[index])).ToArray(); - return parameters.All(x => x is not null) && possibleOtherMethod.Parameters.Except(parameters).All(x => x.HasExplicitDefaultValue); + return Array.TrueForAll(parameters, x => x is not null) && possibleOtherMethod.Parameters.Except(parameters).All(x => x.HasExplicitDefaultValue); IParameterSymbol Valid(ArgumentSyntax argument, ITypeSymbol type) => lookup.TryGetSymbol(argument, out var parameter) @@ -105,19 +100,18 @@ IParameterSymbol Valid(ArgumentSyntax argument, ITypeSymbol type) => } private static bool MethodAccessibleWithinType(IMethodSymbol method, ITypeSymbol type) => - IsInTypeOrNested(method, type) - || method.DeclaredAccessibility switch - { - Accessibility.Private => false, - // ProtectedAndInternal corresponds to `private protected`. - Accessibility.ProtectedAndInternal => type.DerivesFrom(method.ContainingType) && method.IsInSameAssembly(type), - // ProtectedOrInternal corresponds to `protected internal`. - Accessibility.ProtectedOrInternal => type.DerivesFrom(method.ContainingType) || method.IsInSameAssembly(type), - Accessibility.Protected => type.DerivesFrom(method.ContainingType), - Accessibility.Internal => method.IsInSameAssembly(type), - Accessibility.Public => true, - _ => false, - }; + IsInTypeOrNested(method, type) || method.DeclaredAccessibility switch + { + Accessibility.Private => false, + // ProtectedAndInternal corresponds to `private protected`. + Accessibility.ProtectedAndInternal => type.DerivesFrom(method.ContainingType) && method.IsInSameAssembly(type), + // ProtectedOrInternal corresponds to `protected internal`. + Accessibility.ProtectedOrInternal => type.DerivesFrom(method.ContainingType) || method.IsInSameAssembly(type), + Accessibility.Protected => type.DerivesFrom(method.ContainingType), + Accessibility.Internal => method.IsInSameAssembly(type), + Accessibility.Public => true, + _ => false, + }; private static bool IsInTypeOrNested(IMethodSymbol method, ITypeSymbol type) => type is not null && (method.IsInType(type) || IsInTypeOrNested(method, type.ContainingType)); diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/InvocationResolvesToOverrideWithParams.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/InvocationResolvesToOverrideWithParams.cs index 3a9cef6757a..184a0873722 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/InvocationResolvesToOverrideWithParams.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/InvocationResolvesToOverrideWithParams.cs @@ -414,11 +414,18 @@ public void AccessibilityAcrossAssemblies() class Repro_8522 { T Get(params string[] key) => default; - string Get(string key) => default; + T GetBothHaveGenerics(params int[] ints) => default; + T GetBothHaveGenerics(int anInt) => default; + + T GenericsWhereOneHasObjectParam(params int[] ints) => default; + T GenericsWhereOneHasObjectParam(object anInt) => default; + void Test() { - Get("text"); // Noncompliant FP + Get("text"); // Compliant + GetBothHaveGenerics(1); // Compliant, when both methods are generic it seems to resolve correctly to the T GetBothHaveGenerics(int anInt). + GenericsWhereOneHasObjectParam(1); // Noncompliant } }