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