Skip to content

Commit 0e7469c

Browse files
authored
AK2002 - Add multi Context.Materializer() invocation detection (#118)
* AK2002 - Add multi `Context.Materializer()` invocation detection * Add more test cases
1 parent 8660808 commit 0e7469c

File tree

9 files changed

+555
-1
lines changed

9 files changed

+555
-1
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
// -----------------------------------------------------------------------
2+
// <copyright file="ShouldNotInvokeContextMaterializerMultipleTimesAnalyzerSpecs.cs" company="Akka.NET Project">
3+
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
4+
// </copyright>
5+
// -----------------------------------------------------------------------
6+
7+
using Microsoft.CodeAnalysis.Testing;
8+
using Verify = Akka.Analyzers.Tests.Utility.AkkaVerifier<Akka.Analyzers.ShouldNotInvokeContextMaterializerMultipleTimesAnalyzer>;
9+
10+
namespace Akka.Analyzers.Tests.Analyzers.AK2000;
11+
12+
public class ShouldNotInvokeContextMaterializerMultipleTimesAnalyzerSpecs
13+
{
14+
public static readonly TheoryData<string> SuccessCases = new()
15+
{
16+
// Using ActorSystem.Materializer from a non-actor class should not trigger any warnings
17+
"""
18+
using System.Linq;
19+
using Akka.Actor;
20+
using Akka.Streams;
21+
using Akka.Streams.Dsl;
22+
23+
public class MyClass
24+
{
25+
public MyClass(ActorSystem system)
26+
{
27+
var mat1 = system.Materializer();
28+
var mat2 = system.Materializer();
29+
30+
var source1 = Source.From(Enumerable.Range(0, 100))
31+
.RunWith(Sink.Ignore<int>(), system.Materializer());
32+
var source2 = Source.From(Enumerable.Range(0, 100))
33+
.RunWith(Sink.Ignore<int>(), system.Materializer());
34+
}
35+
}
36+
""",
37+
// Using ActorSystem.Materializer from an actor class should not trigger any warnings
38+
"""
39+
using System.Linq;
40+
using Akka.Actor;
41+
using Akka.Streams;
42+
using Akka.Streams.Dsl;
43+
44+
public class MyActor: ReceiveActor
45+
{
46+
public MyActor()
47+
{
48+
var mat1 = Context.System.Materializer();
49+
var mat2 = Context.System.Materializer();
50+
51+
var source1 = Source.From(Enumerable.Range(0, 100))
52+
.RunWith(Sink.Ignore<int>(), Context.System.Materializer());
53+
var source2 = Source.From(Enumerable.Range(0, 100))
54+
.RunWith(Sink.Ignore<int>(), Context.System.Materializer());
55+
}
56+
}
57+
""",
58+
// Using ActorSystem.Materializer from an actor class should not trigger any warnings
59+
"""
60+
using System.Linq;
61+
using Akka.Actor;
62+
using Akka.Streams;
63+
using Akka.Streams.Dsl;
64+
65+
public class MyActor: ReceiveActor
66+
{
67+
public MyActor()
68+
{
69+
var system = Context.System;
70+
var mat1 = system.Materializer();
71+
var mat2 = system.Materializer();
72+
73+
var source1 = Source.From(Enumerable.Range(0, 100))
74+
.RunWith(Sink.Ignore<int>(), system.Materializer());
75+
var source2 = Source.From(Enumerable.Range(0, 100))
76+
.RunWith(Sink.Ignore<int>(), system.Materializer());
77+
}
78+
}
79+
""",
80+
// Using ActorMaterializerExtensions.Materializer() with ActorSystem argument from an actor class should not trigger any warnings
81+
"""
82+
using System.Linq;
83+
using Akka.Actor;
84+
using Akka.Streams;
85+
using Akka.Streams.Dsl;
86+
87+
public class MyActor: ReceiveActor
88+
{
89+
public MyActor()
90+
{
91+
var system = Context.System;
92+
var mat1 = ActorMaterializerExtensions.Materializer(system);
93+
var mat2 = ActorMaterializerExtensions.Materializer(system);
94+
95+
var source1 = Source.From(Enumerable.Range(0, 100))
96+
.RunWith(Sink.Ignore<int>(), ActorMaterializerExtensions.Materializer(system));
97+
var source2 = Source.From(Enumerable.Range(0, 100))
98+
.RunWith(Sink.Ignore<int>(), ActorMaterializerExtensions.Materializer(system));
99+
}
100+
}
101+
""",
102+
// Cached Context.Materializer should not trigger any warnings
103+
"""
104+
using System.Linq;
105+
using Akka.Actor;
106+
using Akka.Streams;
107+
using Akka.Streams.Dsl;
108+
109+
public class MyActor: ReceiveActor
110+
{
111+
public MyActor()
112+
{
113+
var mat = Context.Materializer();
114+
115+
var source1 = Source.From(Enumerable.Range(0, 100))
116+
.RunWith(Sink.Ignore<int>(), mat);
117+
var source2 = Source.From(Enumerable.Range(0, 100))
118+
.RunWith(Sink.Ignore<int>(), mat);
119+
}
120+
}
121+
""",
122+
// Cached Context.Materializer should not trigger any warnings
123+
"""
124+
using System.Linq;
125+
using Akka.Actor;
126+
using Akka.Streams;
127+
using Akka.Streams.Dsl;
128+
129+
public class MyActor: ReceiveActor
130+
{
131+
private readonly ActorMaterializer _materializer = Context.Materializer();
132+
public MyActor()
133+
{
134+
var source1 = Source.From(Enumerable.Range(0, 100))
135+
.RunWith(Sink.Ignore<int>(), _materializer);
136+
var source2 = Source.From(Enumerable.Range(0, 100))
137+
.RunWith(Sink.Ignore<int>(), _materializer);
138+
}
139+
}
140+
""",
141+
// Cached Context.Materializer should not trigger any warnings
142+
"""
143+
using System.Linq;
144+
using Akka.Actor;
145+
using Akka.Streams;
146+
using Akka.Streams.Dsl;
147+
148+
public class MyActor: ReceiveActor
149+
{
150+
private ActorMaterializer Materializer { get; } = Context.Materializer();
151+
public MyActor()
152+
{
153+
var source1 = Source.From(Enumerable.Range(0, 100))
154+
.RunWith(Sink.Ignore<int>(), Materializer);
155+
var source2 = Source.From(Enumerable.Range(0, 100))
156+
.RunWith(Sink.Ignore<int>(), Materializer);
157+
}
158+
}
159+
""",
160+
};
161+
162+
[Theory]
163+
[MemberData(nameof(SuccessCases))]
164+
public Task SuccessCase(string code)
165+
{
166+
return Verify.VerifyAnalyzer(code);
167+
}
168+
169+
public static readonly
170+
TheoryData<(string testData, (int startLine, int startColumn, int endLine, int endColumn)[] spanData)>
171+
FailureCases = new()
172+
{
173+
(
174+
// Context.Materializer invoked multiple times
175+
"""
176+
// 01
177+
using System.Linq;
178+
using Akka.Actor;
179+
using Akka.Streams;
180+
using Akka.Streams.Dsl;
181+
182+
public class MyActor: ReceiveActor
183+
{
184+
public MyActor()
185+
{
186+
var source1 = Source.From(Enumerable.Range(0, 100))
187+
.RunWith(Sink.Ignore<int>(), Context.Materializer());
188+
var source2 = Source.From(Enumerable.Range(0, 100))
189+
.RunWith(Sink.Ignore<int>(), Context.Materializer());
190+
var source3 = Source.From(Enumerable.Range(0, 100))
191+
.RunWith(Sink.Ignore<int>(), Context.Materializer());
192+
}
193+
}
194+
""", new[]{(14, 42, 14, 64), (16, 42, 16, 64)}),
195+
(
196+
// Context.Materializer invoked multiple times
197+
"""
198+
// 02
199+
using System.Linq;
200+
using Akka.Actor;
201+
using Akka.Streams;
202+
using Akka.Streams.Dsl;
203+
204+
public class MyActor: ReceiveActor
205+
{
206+
private ActorMaterializer Materializer { get; } = Context.Materializer();
207+
public MyActor()
208+
{
209+
var source1 = Source.From(Enumerable.Range(0, 100))
210+
.RunWith(Sink.Ignore<int>(), Materializer);
211+
var source2 = Source.From(Enumerable.Range(0, 100))
212+
.RunWith(Sink.Ignore<int>(), Context.Materializer());
213+
var source3 = Source.From(Enumerable.Range(0, 100))
214+
.RunWith(Sink.Ignore<int>(), Context.Materializer());
215+
}
216+
}
217+
""", new[]{(15, 42, 15, 64), (17, 42, 17, 64)}),
218+
(
219+
// Context.Materializer invoked multiple times, mixed with ActorSystem.Materializer()
220+
// Should not emit warning on Context.System.Materializer()
221+
"""
222+
// 03
223+
using System.Linq;
224+
using Akka.Actor;
225+
using Akka.Streams;
226+
using Akka.Streams.Dsl;
227+
228+
public class MyActor: ReceiveActor
229+
{
230+
public MyActor()
231+
{
232+
var source1 = Source.From(Enumerable.Range(0, 100))
233+
.RunWith(Sink.Ignore<int>(), Context.Materializer());
234+
var source2 = Source.From(Enumerable.Range(0, 100))
235+
.RunWith(Sink.Ignore<int>(), Context.System.Materializer());
236+
var source3 = Source.From(Enumerable.Range(0, 100))
237+
.RunWith(Sink.Ignore<int>(), Context.Materializer());
238+
}
239+
}
240+
""", new[]{(16, 42, 16, 64)}),
241+
(
242+
// ActorMaterializerExtensions.Materializer() invoked multiple times
243+
"""
244+
// 04
245+
using System.Linq;
246+
using Akka.Actor;
247+
using Akka.Streams;
248+
using Akka.Streams.Dsl;
249+
250+
public class MyActor: ReceiveActor
251+
{
252+
public MyActor()
253+
{
254+
var source1 = Source.From(Enumerable.Range(0, 100))
255+
.RunWith(Sink.Ignore<int>(), ActorMaterializerExtensions.Materializer(Context));
256+
var source2 = Source.From(Enumerable.Range(0, 100))
257+
.RunWith(Sink.Ignore<int>(), Context.Materializer());
258+
var source3 = Source.From(Enumerable.Range(0, 100))
259+
.RunWith(Sink.Ignore<int>(), ActorMaterializerExtensions.Materializer(Context));
260+
}
261+
}
262+
""", new[]{(14, 42, 14, 64), (16, 42, 16, 91)}),
263+
};
264+
265+
[Theory]
266+
[MemberData(nameof(FailureCases))]
267+
public async Task FailureCase((string testData, (int startLine, int startColumn, int endLine, int endColumn)[] spanData) d)
268+
{
269+
var (testData, spanData) = d;
270+
var expectedDiagnostics = new DiagnosticResult[spanData.Length];
271+
var currentDiagnosticIndex = 0;
272+
273+
// there can be multiple violations per test case
274+
foreach (var (startLine, startColumn, endLine, endColumn) in spanData)
275+
{
276+
expectedDiagnostics[currentDiagnosticIndex++] = Verify.Diagnostic().WithSpan(startLine, startColumn, endLine, endColumn);
277+
}
278+
279+
await Verify.VerifyAnalyzer(testData, expectedDiagnostics).ConfigureAwait(true);
280+
}
281+
}

src/Akka.Analyzers.Tests/Utility/ReferenceAssembliesHelper.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ static ReferenceAssembliesHelper()
3434

3535
// TODO: does this bring all other transitive dependencies?
3636
CurrentAkka = defaultAssemblies.AddPackages(
37-
ImmutableArray<PackageIdentity>.Empty.Add(new PackageIdentity("Akka.Cluster.Sharding", "1.5.15"))
37+
ImmutableArray<PackageIdentity>.Empty
38+
.Add(new PackageIdentity("Akka.Cluster.Sharding", "1.5.15"))
39+
.Add(new PackageIdentity("Akka.Streams", "1.5.15"))
3840
);
3941
}
4042
}

0 commit comments

Comments
 (0)