Skip to content

Commit 58c1293

Browse files
authored
Add AK2003, AK2004 - Must not use void async delegate in Receive message handler (#122)
* Add AK2003 - Must not use void async delegate in `Receive` message handler * Add code fixer
1 parent aa3822b commit 58c1293

16 files changed

+1432
-0
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<PackageVersion Include="Akka.Hosting" Version="1.5.6.1" />
88
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.8.0" />
99
<PackageVersion Include="NuGet.Frameworks" Version="6.9.1" />
10+
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
1011
</ItemGroup>
1112
<!-- Testing Utilities -->
1213
<ItemGroup>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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

Comments
 (0)