diff --git a/README.md b/README.md index b1354adee..f38a84eac 100755 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0134](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0134.md)|Usage|Observe result of async calls|⚠️|✔️|❌| |[MA0135](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0135.md)|Design|The log parameter has no configured type|⚠️|❌|❌| |[MA0136](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0136.md)|Usage|Raw String contains an implicit end of line character|👻|✔️|❌| -|[MA0137](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0137.md)|Design|Use 'Async' suffix when a method returns an awaitable type|⚠️|❌|❌| -|[MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md)|Design|Do not use 'Async' suffix when a method does not return an awaitable type|⚠️|❌|❌| +|[MA0137](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0137.md)|Design|Use 'Async' suffix when a method returns an awaitable type|⚠️|❌|✔️| +|[MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md)|Design|Do not use 'Async' suffix when a method does not return an awaitable type|⚠️|❌|✔️| |[MA0139](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0139.md)|Design|Log parameter type is not valid|⚠️|✔️|❌| |[MA0140](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0140.md)|Design|Both if and else branch have identical code|⚠️|✔️|❌| |[MA0141](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0141.md)|Usage|Use pattern matching instead of inequality operators for null check|ℹ️|❌|✔️| diff --git a/docs/README.md b/docs/README.md index 15873ba1c..c014ab89c 100755 --- a/docs/README.md +++ b/docs/README.md @@ -136,8 +136,8 @@ |[MA0134](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0134.md)|Usage|Observe result of async calls|⚠️|✔️|❌| |[MA0135](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0135.md)|Design|The log parameter has no configured type|⚠️|❌|❌| |[MA0136](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0136.md)|Usage|Raw String contains an implicit end of line character|👻|✔️|❌| -|[MA0137](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0137.md)|Design|Use 'Async' suffix when a method returns an awaitable type|⚠️|❌|❌| -|[MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md)|Design|Do not use 'Async' suffix when a method does not return an awaitable type|⚠️|❌|❌| +|[MA0137](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0137.md)|Design|Use 'Async' suffix when a method returns an awaitable type|⚠️|❌|✔️| +|[MA0138](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0138.md)|Design|Do not use 'Async' suffix when a method does not return an awaitable type|⚠️|❌|✔️| |[MA0139](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0139.md)|Design|Log parameter type is not valid|⚠️|✔️|❌| |[MA0140](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0140.md)|Design|Both if and else branch have identical code|⚠️|✔️|❌| |[MA0141](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0141.md)|Usage|Use pattern matching instead of inequality operators for null check|ℹ️|❌|✔️| diff --git a/docs/Rules/MA0137.md b/docs/Rules/MA0137.md index 90df90a4d..2412c6acd 100644 --- a/docs/Rules/MA0137.md +++ b/docs/Rules/MA0137.md @@ -1,6 +1,6 @@ # MA0137 - Use 'Async' suffix when a method returns an awaitable type -Source: [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs) +Sources: [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs), [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs) Methods that return awaitable types such as `Task` or `ValueTask` should have an Async suffix. diff --git a/docs/Rules/MA0138.md b/docs/Rules/MA0138.md index 337a2d579..556cf82e0 100644 --- a/docs/Rules/MA0138.md +++ b/docs/Rules/MA0138.md @@ -1,6 +1,6 @@ # MA0138 - Do not use 'Async' suffix when a method does not return an awaitable type -Source: [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs) +Sources: [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs), [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs) Methods that does not return an awaitable type such as `Task` or `ValueTask` should not have an 'Async' suffix. diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs new file mode 100644 index 000000000..e5d6e5c49 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs @@ -0,0 +1,83 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Rename; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create( + RuleIdentifiers.MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffix, + RuleIdentifiers.MethodsNotReturningAnAwaitableTypeMustNotHaveTheAsyncSuffix); + + public override FixAllProvider? GetFixAllProvider() => null; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + return; + + var nodeToFix = root.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel is null) + return; + + IMethodSymbol? methodSymbol = null; + + // Try to get the method symbol: either from a method declaration or a local function + var declarationNode = nodeToFix.AncestorsAndSelf().FirstOrDefault(n => n is MethodDeclarationSyntax or LocalFunctionStatementSyntax); + if (declarationNode is MethodDeclarationSyntax methodDeclaration) + { + methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken) as IMethodSymbol; + } + else if (declarationNode is LocalFunctionStatementSyntax localFunctionStatement) + { + methodSymbol = semanticModel.GetDeclaredSymbol(localFunctionStatement, context.CancellationToken) as IMethodSymbol; + } + + if (methodSymbol is null) + return; + + foreach (var diagnostic in context.Diagnostics) + { + string newName; + string title; + if (diagnostic.Id == RuleIdentifiers.MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffix) + { + newName = methodSymbol.Name + "Async"; + title = $"Rename to '{newName}'"; + } + else + { + if (!methodSymbol.Name.EndsWith("Async", StringComparison.Ordinal)) + continue; + + newName = methodSymbol.Name[..^"Async".Length]; + title = $"Rename to '{newName}'"; + } + + context.RegisterCodeFix( + CodeAction.Create( + title, + ct => RenameMethodAsync(context.Document, methodSymbol, newName, ct), + equivalenceKey: title), + diagnostic); + } + } + + private static async Task RenameMethodAsync(Document document, IMethodSymbol methodSymbol, string newName, CancellationToken cancellationToken) + { + var solution = document.Project.Solution; + return await Renamer.RenameSymbolAsync(solution, methodSymbol, new SymbolRenameOptions(), newName, cancellationToken).ConfigureAwait(false); + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs index f2e2cb3f7..ce2ede6cc 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs @@ -8,6 +8,7 @@ public sealed class MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyze private static ProjectBuilder CreateProjectBuilder() => new ProjectBuilder() .WithAnalyzer() + .WithCodeFixProvider() .WithTargetFramework(TargetFramework.Net8_0) .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview); @@ -171,6 +172,69 @@ class TypeName .AddXUnitApi() .ValidateAsync(); + [Fact] + public Task AsyncMethodWithoutSuffix_CodeFix_AddsAsyncSuffix() + => CreateProjectBuilder() + .WithSourceCode(""" + class TypeName + { + System.Threading.Tasks.Task {|MA0137:Test|}() => throw null; + void Caller() { _ = Test(); } + } + """) + .ShouldFixCodeWith(""" + class TypeName + { + System.Threading.Tasks.Task TestAsync() => throw null; + void Caller() { _ = TestAsync(); } + } + """) + .ValidateAsync(); + + [Fact] + public Task MethodNotReturningAwaitableTypeWithSuffix_CodeFix_RemovesAsyncSuffix() + => CreateProjectBuilder() + .WithSourceCode(""" + class TypeName + { + void {|MA0138:TestAsync|}() => throw null; + void Caller() { TestAsync(); } + } + """) + .ShouldFixCodeWith(""" + class TypeName + { + void Test() => throw null; + void Caller() { Test(); } + } + """) + .ValidateAsync(); + + [Fact] + public Task VoidLocalFunctionWithSuffix_CodeFix_RemovesAsyncSuffix() + => CreateProjectBuilder() + .WithSourceCode(""" + class TypeName + { + void Test() + { + void {|MA0138:FooAsync|}() => throw null; + FooAsync(); + } + } + """) + .ShouldFixCodeWith(""" + class TypeName + { + void Test() + { + void Foo() => throw null; + Foo(); + } + } + """) + .ValidateAsync(); + [Fact] public Task IgnoreTestMethods_ExcludeTestMethodsTrue() => CreateProjectBuilder()