diff --git a/docs/Rules/MA0032.md b/docs/Rules/MA0032.md index 9db3b895..8657073b 100644 --- a/docs/Rules/MA0032.md +++ b/docs/Rules/MA0032.md @@ -16,6 +16,22 @@ class Test } ```` +## Configuration + +```` +MA0032.allowOverloadsWithOptionalParameters = false +```` + +````c# +Foo.Bar(); // report when MA0032.allowOverloadsWithOptionalParameters is true + +class Foo +{ + public static void Bar() => throw null; + public static void Bar(CancellationToken cancellationToken, bool dummy = false) => throw null; +} +```` + ## Additional resources - [Enforcing asynchronous code good practices using a Roslyn analyzer](https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm) diff --git a/docs/Rules/MA0040.md b/docs/Rules/MA0040.md index 80e45ec8..525c865a 100644 --- a/docs/Rules/MA0040.md +++ b/docs/Rules/MA0040.md @@ -18,6 +18,22 @@ class Test This rule only reports a diagnostic when a `CancellationToken` is available in the scope. [MA0032](MA0032.md) detects the same cases, but reports them even if applying a fix would require you to change the calling method's signature. +## Configuration + +```` +MA0032.allowOverloadsWithOptionalParameters = false +```` + +````c# +Foo.Bar(); // report when MA0032.allowOverloadsWithOptionalParameters is true + +class Foo +{ + public static void Bar() => throw null; + public static void Bar(CancellationToken cancellationToken, bool dummy = false) => throw null; +} +```` + ## Additional resources - [Enforcing asynchronous code good practices using a Roslyn analyzer](https://www.meziantou.net/enforcing-asynchronous-code-good-practices-using-a-roslyn-analyzer.htm) diff --git a/src/Meziantou.Analyzer/Internals/OverloadFinder.cs b/src/Meziantou.Analyzer/Internals/OverloadFinder.cs index 9c74bad2..4116ee94 100644 --- a/src/Meziantou.Analyzer/Internals/OverloadFinder.cs +++ b/src/Meziantou.Analyzer/Internals/OverloadFinder.cs @@ -22,40 +22,43 @@ public bool HasOverloadWithAdditionalParameterOfType( if (currentOperation.SemanticModel is null) return false; - return FindOverloadWithAdditionalParameterOfType(methodSymbol, syntaxNode: currentOperation.Syntax, includeObsoleteMethods: false, additionalParameterTypes) is not null; + return FindOverloadWithAdditionalParameterOfType(methodSymbol, syntaxNode: currentOperation.Syntax, includeObsoleteMethods: false, allowOptionalParameters: false, additionalParameterTypes) is not null; } private IMethodSymbol? FindOverloadWithAdditionalParameterOfType( IMethodSymbol methodSymbol, params ITypeSymbol[] additionalParameterTypes) { - return FindOverloadWithAdditionalParameterOfType(methodSymbol, includeObsoleteMethods: false, additionalParameterTypes); + return FindOverloadWithAdditionalParameterOfType(methodSymbol, includeObsoleteMethods: false, allowOptionalParameters: false, additionalParameterTypes); } public IMethodSymbol? FindOverloadWithAdditionalParameterOfType( IMethodSymbol methodSymbol, bool includeObsoleteMethods, + bool allowOptionalParameters, params ITypeSymbol[] additionalParameterTypes) { - return FindOverloadWithAdditionalParameterOfType(methodSymbol, syntaxNode: null, includeObsoleteMethods, additionalParameterTypes); + return FindOverloadWithAdditionalParameterOfType(methodSymbol, syntaxNode: null, includeObsoleteMethods, allowOptionalParameters, additionalParameterTypes); } public IMethodSymbol? FindOverloadWithAdditionalParameterOfType( IMethodSymbol methodSymbol, IOperation operation, bool includeObsoleteMethods, + bool allowOptionalParameters, params ITypeSymbol[] additionalParameterTypes) { if (operation.SemanticModel is null) return null; - return FindOverloadWithAdditionalParameterOfType(methodSymbol, operation.Syntax, includeObsoleteMethods, additionalParameterTypes); + return FindOverloadWithAdditionalParameterOfType(methodSymbol, operation.Syntax, includeObsoleteMethods, allowOptionalParameters, additionalParameterTypes); } public IMethodSymbol? FindOverloadWithAdditionalParameterOfType( IMethodSymbol methodSymbol, SyntaxNode? syntaxNode, bool includeObsoleteMethods, + bool allowOptionalParameters, params ITypeSymbol[] additionalParameterTypes) { if (additionalParameterTypes is null) @@ -83,7 +86,7 @@ public bool HasOverloadWithAdditionalParameterOfType( if (!includeObsoleteMethods && IsObsolete(method)) continue; - if (HasSimilarParameters(methodSymbol, method, additionalParameterTypes)) + if (HasSimilarParameters(methodSymbol, method, allowOptionalParameters, additionalParameterTypes)) return method; } } @@ -91,12 +94,12 @@ public bool HasOverloadWithAdditionalParameterOfType( return null; } - public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, params ITypeSymbol[] additionalParameterTypes) + public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, bool allowOptionalParameters, params ITypeSymbol[] additionalParameterTypes) { if (method.IsEqualTo(otherMethod)) return false; - if (otherMethod.Parameters.Length - method.Parameters.Length != additionalParameterTypes.Length) + if (!allowOptionalParameters && otherMethod.Parameters.Length - method.Parameters.Length != additionalParameterTypes.Length) return false; // Most of the time, an overload has the same order for the parameters @@ -139,6 +142,7 @@ public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol othe } // Slower search, allows to find overload with different parameter order + // Also, handle allow optional parameters { var otherMethodParameters = otherMethod.Parameters; @@ -166,7 +170,16 @@ public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol othe } } - return otherMethodParameters.Length == 0; + if (otherMethodParameters.Length == 0) + return true; + + if (allowOptionalParameters) + { + if (otherMethodParameters.All(p => p.IsOptional)) + return true; + } + + return false; } } diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index fd66fd9e..f9990e4e 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -299,10 +299,10 @@ private bool IsPotentialMember(IInvocationOperation operation, IMethodSymbol met if (methodSymbol.HasAttribute(ObsoleteAttributeSymbol)) return false; - if (OverloadFinder.HasSimilarParameters(method, methodSymbol)) + if (OverloadFinder.HasSimilarParameters(method, methodSymbol, allowOptionalParameters: false)) return true; - if (CancellationTokenSymbol is not null && OverloadFinder.HasSimilarParameters(method, methodSymbol, CancellationTokenSymbol)) + if (CancellationTokenSymbol is not null && OverloadFinder.HasSimilarParameters(method, methodSymbol, allowOptionalParameters: false, CancellationTokenSymbol)) return true; } diff --git a/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasCancellationTokenAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasCancellationTokenAnalyzer.cs index f1892f50..df6407af 100644 --- a/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasCancellationTokenAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseAnOverloadThatHasCancellationTokenAnalyzer.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Threading; +using Meziantou.Analyzer.Configurations; using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -105,7 +106,7 @@ private bool HasExplicitCancellationTokenArgument(IInvocationOperation operation private sealed record AdditionalParameterInfo(int ParameterIndex, string? Name, bool HasEnumeratorCancellationAttribute); - private bool HasAnOverloadWithCancellationToken(IInvocationOperation operation, [NotNullWhen(true)] out AdditionalParameterInfo? parameterInfo) + private bool HasAnOverloadWithCancellationToken(OperationAnalysisContext context, IInvocationOperation operation, [NotNullWhen(true)] out AdditionalParameterInfo? parameterInfo) { parameterInfo = default; var method = operation.TargetMethod; @@ -115,7 +116,8 @@ private bool HasAnOverloadWithCancellationToken(IInvocationOperation operation, if (IsArgumentImplicitlyDeclared(operation, CancellationTokenSymbol, out parameterInfo)) return true; - var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, operation, includeObsoleteMethods: false, CancellationTokenSymbol); + var allowOptionalParameters = context.Options.GetConfigurationValue(operation, "MA0032.allowOverloadsWithOptionalParameters", defaultValue: false); + var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, operation, includeObsoleteMethods: false, allowOptionalParameters, CancellationTokenSymbol); if (overload is not null) { for (var i = 0; i < overload.Parameters.Length; i++) @@ -163,7 +165,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) if (HasExplicitCancellationTokenArgument(operation)) return; - if (!HasAnOverloadWithCancellationToken(operation, out var parameterInfo)) + if (!HasAnOverloadWithCancellationToken(context, operation, out var parameterInfo)) return; var availableCancellationTokens = FindCancellationTokens(operation, context.CancellationToken); @@ -208,7 +210,7 @@ public void AnalyzeLoop(OperationAnalysisContext context) return; // Already handled by AnalyzeInvocation - if (HasAnOverloadWithCancellationToken(invocation, out _)) + if (HasAnOverloadWithCancellationToken(context, invocation, out _)) return; collection = invocation.GetChildOperations().FirstOrDefault(); diff --git a/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs index d1490efd..a68f3aa0 100644 --- a/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseIFormatProviderAnalyzer.cs @@ -79,7 +79,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) return; } - var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, operation, includeObsoleteMethods: false, _cultureSensitiveContext.FormatProviderSymbol); + var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, operation, includeObsoleteMethods: false, allowOptionalParameters: false, _cultureSensitiveContext.FormatProviderSymbol); if (overload is not null) { context.ReportDiagnostic(Rule, operation, operation.TargetMethod.Name, _cultureSensitiveContext.FormatProviderSymbol.ToDisplayString()); @@ -106,7 +106,7 @@ public void AnalyzeInvocation(OperationAnalysisContext context) if (_cultureSensitiveContext.CultureInfoSymbol is not null && !operation.HasArgumentOfType(_cultureSensitiveContext.CultureInfoSymbol)) { - var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, includeObsoleteMethods: false, _cultureSensitiveContext.CultureInfoSymbol); + var overload = _overloadFinder.FindOverloadWithAdditionalParameterOfType(operation.TargetMethod, includeObsoleteMethods: false, allowOptionalParameters: false, _cultureSensitiveContext.CultureInfoSymbol); if (overload is not null) { context.ReportDiagnostic(Rule, operation, operation.TargetMethod.Name, _cultureSensitiveContext.CultureInfoSymbol.ToDisplayString()); diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs index 82015361..136f7efe 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseAnOverloadThatHasCancellationTokenAnalyzerTests.cs @@ -1187,4 +1187,45 @@ await CreateProjectBuilder() .ShouldFixCodeWith(Fix) .ValidateAsync(); } + + [Fact] + public async Task SuggestOverloadWithOptionalParameters_AllowOptionalParameters_True() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + [|Sample.Repro()|]; + + class Sample + { + public static void Repro() => throw null; + public static void Repro(CancellationToken cancellationToken, bool dummy = false) => throw null; + } + """) + .AddAnalyzerConfiguration("MA0032.allowOverloadsWithOptionalParameters", "true") + .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication) + .ValidateAsync(); + } + + [Fact] + public async Task SuggestOverloadWithOptionalParameters_AllowOptionalParameters_False() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + Sample.Repro(); + + class Sample + { + public static void Repro() => throw null; + public static void Repro(CancellationToken cancellationToken, bool dummy = false) => throw null; + } + """) + .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication) + .ValidateAsync(); + } }