1+ // -----------------------------------------------------------------------
2+ // <copyright file="MustNotUseVoidAsyncDelegateInReceiveFixer.cs" company="Akka.NET Project">
3+ // Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
4+ // </copyright>
5+ // -----------------------------------------------------------------------
6+
7+ using System . Composition ;
8+ using Microsoft . CodeAnalysis ;
9+ using Microsoft . CodeAnalysis . CodeActions ;
10+ using Microsoft . CodeAnalysis . CodeFixes ;
11+ using Microsoft . CodeAnalysis . CSharp ;
12+ using Microsoft . CodeAnalysis . CSharp . Syntax ;
13+
14+ namespace Akka . Analyzers . Fixes ;
15+
16+ [ ExportCodeFixProvider ( LanguageNames . CSharp ) ]
17+ [ Shared ]
18+ public class MustNotUseVoidAsyncDelegateInReceivePersistentActorCommandFixer ( )
19+ : BatchedCodeFixProvider ( RuleDescriptors . Ak2005MustNotUseVoidAsyncDelegateInReceivePersistentActorCommand . Id )
20+ {
21+ public const string Key_FixCommandWithVoidAsyncDelegate = "AK2005_FixCommandWithVoidAsyncDelegate" ;
22+
23+ public override async Task RegisterCodeFixesAsync ( CodeFixContext context )
24+ {
25+ // 1) Get the syntax root
26+ var root = await context . Document . GetSyntaxRootAsync ( context . CancellationToken ) . ConfigureAwait ( false ) ;
27+ if ( root is null )
28+ return ;
29+
30+ // 2) Find the node at the diagnostic's span
31+ var diagnostic = context . Diagnostics . FirstOrDefault ( ) ;
32+ if ( diagnostic is null )
33+ return ;
34+ var diagnosticSpan = diagnostic . Location . SourceSpan ;
35+ var node = root . FindNode ( diagnosticSpan ) ;
36+
37+ // 3) Find the enclosing InvocationExpressionSyntax
38+ var invocation = node . FirstAncestorOrSelf < InvocationExpressionSyntax > ( ) ;
39+ if ( invocation == null )
40+ return ;
41+
42+ // 4) Check if any argument is an async lambda. If not, skip registering a fix.
43+ var hasAsyncLambda = invocation . ArgumentList . Arguments
44+ . Any ( a => a . Expression is LambdaExpressionSyntax lambda && lambda . AsyncKeyword . IsKind ( SyntaxKind . AsyncKeyword ) ) ;
45+ if ( ! hasAsyncLambda )
46+ return ;
47+
48+ context . RegisterCodeFix (
49+ CodeAction . Create (
50+ title : "Replace Command with CommandAsync" ,
51+ createChangedDocument : c => ReplaceCommandWithReceiveAsync ( context . Document , invocation , c ) ,
52+ equivalenceKey : Key_FixCommandWithVoidAsyncDelegate ) ,
53+ diagnostic ) ;
54+ }
55+
56+ private static async Task < Document > ReplaceCommandWithReceiveAsync ( Document document ,
57+ InvocationExpressionSyntax invocation , CancellationToken cancellationToken )
58+ {
59+ // We want to keep the same type arguments (e.g. <int>) but rename "Command" → "CommandAsync".
60+ // invocation.Expression can be:
61+ // • GenericNameSyntax (e.g. Command<int>)
62+ // • MemberAccessExpressionSyntax whose .Name is a GenericNameSyntax (e.g. this.Command<int>)
63+ // • (rarely) a bare IdentifierNameSyntax
64+
65+ var expression = invocation . Expression ;
66+
67+ var oldNameNode = expression switch
68+ {
69+ GenericNameSyntax genericName => ( SimpleNameSyntax ) genericName ,
70+ MemberAccessExpressionSyntax { Name : GenericNameSyntax memberGeneric } => memberGeneric ,
71+ IdentifierNameSyntax identifierName => identifierName ,
72+ _ => null
73+ } ;
74+
75+ if ( oldNameNode is null )
76+ {
77+ // If we don’t find a recognizable simple name, bail out.
78+ return document ;
79+ }
80+
81+ // Build a new SimpleNameSyntax with identifier "CommandAsync", preserving any type arguments.
82+ SimpleNameSyntax newNameNode ;
83+ if ( oldNameNode is GenericNameSyntax oldGeneric )
84+ {
85+ newNameNode = SyntaxFactory . GenericName ( SyntaxFactory . Identifier ( "CommandAsync" ) , oldGeneric . TypeArgumentList ) ;
86+ }
87+ else
88+ {
89+ newNameNode = SyntaxFactory . IdentifierName ( "CommandAsync" ) ;
90+ }
91+
92+ // If the old expression was a member‐access (e.g. this.Command<int>), replace only the Name:
93+ SyntaxNode replacedExpression ;
94+ if ( expression is MemberAccessExpressionSyntax oldMemberAccess )
95+ {
96+ replacedExpression = oldMemberAccess . WithName ( newNameNode ) ;
97+ }
98+ else
99+ {
100+ // Otherwise, replace the entire expression (Command<int> → CommandAsync<int>)
101+ replacedExpression = newNameNode ;
102+ }
103+
104+ // Create a new InvocationExpressionSyntax with the replaced expression
105+ var newInvocation = invocation . WithExpression ( ( ExpressionSyntax ) replacedExpression ) ;
106+
107+ // Replace invocation in the syntax tree
108+ var oldRoot = await document . GetSyntaxRootAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
109+ var newRoot = oldRoot ! . ReplaceNode ( invocation , newInvocation ) ;
110+
111+ return document . WithSyntaxRoot ( newRoot ) ;
112+ }
113+ }
0 commit comments