Skip to content

Commit 328d172

Browse files
committed
Add AsyncVoidReturningRelayCommandMethodCodeFixer
1 parent 8e88f30 commit 328d172

File tree

5 files changed

+110
-8
lines changed

5 files changed

+110
-8
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Immutable;
6+
using System.Composition;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using CommunityToolkit.Mvvm.SourceGenerators;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.CodeActions;
12+
using Microsoft.CodeAnalysis.CodeFixes;
13+
using Microsoft.CodeAnalysis.CSharp.Syntax;
14+
using Microsoft.CodeAnalysis.Editing;
15+
using Microsoft.CodeAnalysis.Simplification;
16+
using Microsoft.CodeAnalysis.Text;
17+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
18+
19+
namespace CommunityToolkit.Mvvm.CodeFixers;
20+
21+
/// <summary>
22+
/// A code fixer that automatically updates the return type of <see langword="async"/> <see cref="void"/> methods using <c>[RelayCommand]</c> to return a <see cref="Task"/> instead.
23+
/// </summary>
24+
[ExportCodeFixProvider(LanguageNames.CSharp)]
25+
[Shared]
26+
public sealed class AsyncVoidReturningRelayCommandMethodCodeFixer : CodeFixProvider
27+
{
28+
/// <inheritdoc/>
29+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(AsyncVoidReturningRelayCommandMethodId);
30+
31+
/// <inheritdoc/>
32+
public override FixAllProvider? GetFixAllProvider()
33+
{
34+
return WellKnownFixAllProviders.BatchFixer;
35+
}
36+
37+
/// <inheritdoc/>
38+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
39+
{
40+
Diagnostic diagnostic = context.Diagnostics[0];
41+
TextSpan diagnosticSpan = context.Span;
42+
43+
// Retrieve the property passed by the analyzer
44+
if (diagnostic.Properties[AsyncVoidReturningRelayCommandMethodAnalyzer.MethodNameKey] is not string methodName)
45+
{
46+
return;
47+
}
48+
49+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
50+
51+
// Get the method declaration from the target diagnostic
52+
if (root!.FindNode(diagnosticSpan) is MethodDeclarationSyntax { Identifier.Text: string identifierName } methodDeclaration &&
53+
identifierName == methodName)
54+
{
55+
// Register the code fix to update the return type to be Task instead
56+
context.RegisterCodeFix(
57+
CodeAction.Create(
58+
title: "Change return type to Task",
59+
createChangedDocument: token => ChangeReturnType(context.Document, root, methodDeclaration, token),
60+
equivalenceKey: "Change return type to Task"),
61+
diagnostic);
62+
}
63+
}
64+
65+
/// <summary>
66+
/// Applies the code fix to a target method declaration and returns an updated document.
67+
/// </summary>
68+
/// <param name="document">The original document being fixed.</param>
69+
/// <param name="root">The original tree root belonging to the current document.</param>
70+
/// <param name="methodDeclaration">The <see cref="MethodDeclarationSyntax"/> to update.</param>
71+
/// <param name="cancellationToken">The cancellation token for the operation.</param>
72+
/// <returns>An updated document with the applied code fix, and the return type of the method being <see cref="Task"/>.</returns>
73+
private static async Task<Document> ChangeReturnType(Document document, SyntaxNode root, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken)
74+
{
75+
// Get the semantic model (bail if it's not available)
76+
if (await document.GetSemanticModelAsync(cancellationToken) is not SemanticModel semanticModel)
77+
{
78+
return document;
79+
}
80+
81+
// Also bail if we can't resolve the Task symbol (this should really never happen)
82+
if (semanticModel.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task") is not INamedTypeSymbol taskSymbol)
83+
{
84+
return document;
85+
}
86+
87+
// Create the new syntax node for the return, and configure it to automatically add "using System.Threading.Tasks;" if needed
88+
SyntaxNode typeSyntax = SyntaxGenerator.GetGenerator(document).TypeExpression(taskSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation);
89+
90+
// Replace the void return type with the newly created Task type expression
91+
return document.WithSyntaxRoot(root.ReplaceNode(methodDeclaration.ReturnType, typeSyntax));
92+
}
93+
}

src/CommunityToolkit.Mvvm.CodeFixers/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ public sealed class ClassUsingAttributeInsteadOfInheritanceCodeFixer : CodeFixPr
4040
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
4141
{
4242
Diagnostic diagnostic = context.Diagnostics[0];
43-
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
43+
TextSpan diagnosticSpan = context.Span;
4444

45-
// Retrieve the property passed by the analyzer
45+
// Retrieve the properties passed by the analyzer
4646
if (diagnostic.Properties[ClassUsingAttributeInsteadOfInheritanceAnalyzer.TypeNameKey] is not string typeName ||
4747
diagnostic.Properties[ClassUsingAttributeInsteadOfInheritanceAnalyzer.AttributeTypeNameKey] is not string attributeTypeName)
4848
{
@@ -59,11 +59,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
5959
context.RegisterCodeFix(
6060
CodeAction.Create(
6161
title: "Inherit from ObservableObject",
62-
createChangedDocument: token => UpdateReference(context.Document, root, classDeclaration, attributeTypeName),
62+
createChangedDocument: token => RemoveAttribute(context.Document, root, classDeclaration, attributeTypeName),
6363
equivalenceKey: "Inherit from ObservableObject"),
6464
diagnostic);
65-
66-
return;
6765
}
6866
}
6967

@@ -75,7 +73,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
7573
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> to update.</param>
7674
/// <param name="attributeTypeName">The name of the attribute that should be removed.</param>
7775
/// <returns>An updated document with the applied code fix, and <paramref name="classDeclaration"/> inheriting from <c>ObservableObject</c>.</returns>
78-
private static Task<Document> UpdateReference(Document document, SyntaxNode root, ClassDeclarationSyntax classDeclaration, string attributeTypeName)
76+
private static Task<Document> RemoveAttribute(Document document, SyntaxNode root, ClassDeclarationSyntax classDeclaration, string attributeTypeName)
7977
{
8078
// Insert ObservableObject always in first position in the base list. The type might have
8179
// some interfaces in the base list, so we just copy them back after ObservableObject.

src/CommunityToolkit.Mvvm.CodeFixers/FieldReferenceForObservablePropertyFieldCodeFixer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public sealed class FieldReferenceForObservablePropertyFieldCodeFixer : CodeFixP
3737
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
3838
{
3939
Diagnostic diagnostic = context.Diagnostics[0];
40-
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
40+
TextSpan diagnosticSpan = context.Span;
4141

4242
// Retrieve the properties passed by the analyzer
4343
if (diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey] is not string fieldName ||

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
1717
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1818
public sealed class AsyncVoidReturningRelayCommandMethodAnalyzer : DiagnosticAnalyzer
1919
{
20+
/// <summary>
21+
/// The key for the name of the target method to update.
22+
/// </summary>
23+
internal const string MethodNameKey = "MethodName";
24+
2025
/// <inheritdoc/>
2126
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(AsyncVoidReturningRelayCommandMethod);
2227

@@ -52,6 +57,7 @@ public override void Initialize(AnalysisContext context)
5257
context.ReportDiagnostic(Diagnostic.Create(
5358
AsyncVoidReturningRelayCommandMethod,
5459
context.Symbol.Locations.FirstOrDefault(),
60+
ImmutableDictionary.Create<string, string?>().Add(MethodNameKey, methodSymbol.Name),
5561
context.Symbol));
5662
}, SymbolKind.Method);
5763
});

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ internal static class DiagnosticDescriptors
2929
/// </summary>
3030
public const string FieldReferenceForObservablePropertyFieldId = "MVVMTK0034";
3131

32+
/// <summary>
33+
/// The diagnostic id for <see cref="AsyncVoidReturningRelayCommandMethod"/>.
34+
/// </summary>
35+
public const string AsyncVoidReturningRelayCommandMethodId = "MVVMTK0039";
36+
3237
/// <summary>
3338
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a duplicate declaration of <see cref="INotifyPropertyChanged"/> would happen.
3439
/// <para>
@@ -645,7 +650,7 @@ internal static class DiagnosticDescriptors
645650
/// </para>
646651
/// </summary>
647652
public static readonly DiagnosticDescriptor AsyncVoidReturningRelayCommandMethod = new DiagnosticDescriptor(
648-
id: "MVVMTK0039",
653+
id: AsyncVoidReturningRelayCommandMethodId,
649654
title: "Async void returning method annotated with RelayCommand",
650655
messageFormat: "The method {0} annotated with [RelayCommand] is async void (make sure to return a Task type instead)",
651656
category: typeof(RelayCommandGenerator).FullName,

0 commit comments

Comments
 (0)