Skip to content

Commit 96ddcd4

Browse files
authored
Merge pull request #65 from Concurrency-Lab/ph-s019-exclude-methods
PH_S019: Add support for method exclusions
2 parents b4a6de1 + 8dc024a commit 96ddcd4

6 files changed

Lines changed: 148 additions & 3 deletions

File tree

doc/analyzers/PH_S019.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@ An asynchronous method makes use of a blocking method, although there exists an
77
## Solution
88

99
Replace the blocking method with its asynchronous counterpart.
10+
11+
## Additional Note
12+
13+
The methods `AddAsync` and `AddRangeAsync` of entity framework's `DbSet` and `DbContext` are explicitely excluded from this analysis by default. The [documentation](https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.addasync?view=efcore-5.0) recommends using the non-async versions instead.
14+
15+
## Options
16+
17+
```
18+
# A white-space separated list of methods to exclude when searching for async counterparts
19+
# Format: <type-specifier>:<method1>,<method2>
20+
dotnet_diagnostic.PH_P019.exclusions = Microsoft.EntityFrameworkCore.DbContext:Add,AddRange Microsoft.EntityFrameworkCore.DbSet`1:Add,AddRange
21+
```

reportall.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dotnet_diagnostic.PH_S016.severity = warning
3636
dotnet_diagnostic.PH_S017.severity = warning
3737
dotnet_diagnostic.PH_S018.severity = warning
3838
dotnet_diagnostic.PH_S019.severity = warning
39+
dotnet_diagnostic.PH_S019.exclusions =
3940
dotnet_diagnostic.PH_S020.severity = warning
4041
dotnet_diagnostic.PH_S021.severity = warning
4142
dotnet_diagnostic.PH_S022.severity = warning

src/ParallelHelper.Plugin/ReleaseNotes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- New Analyzer: PH_B015 - Disposed Task Instead of Value
44
- Improved Analyzer: PH_S007 - Now respects activation frames
55
- Improved Analyzer: PH_P007 - Now reports default expressions where a token is available
6+
- Improved Analyzer: PH_S019 - Now ignores EF Core's Add and AddRange of DbSet and DbContext by default
67
- Improved Analyzer: PH_S025 - Now supports lambda expressions and respects activation frames
78

89

src/ParallelHelper.Test/Analyzer/Smells/BlockingMethodWithAsyncCounterpartInAsyncMethodAnalyzerTest.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ public Task DoItAsync() {
108108
VerifyDiagnostic(source, new DiagnosticResultLocation(7, 5));
109109
}
110110

111+
[TestMethod]
112+
public void ReportsReadOfTypeWithReadAsyncInAsyncMethodIfExclusionEntryIsInvalid() {
113+
const string source = @"
114+
using System.IO;
115+
using System.Net.Sockets;
116+
using System.Threading.Tasks;
117+
118+
class Test {
119+
public async Task DoWorkAsync() {
120+
using(var client = new TcpClient())
121+
using(var reader = new StreamReader(client.GetStream())) {
122+
var buffer = new char[1024];
123+
reader.Read(buffer, 0, buffer.Length);
124+
}
125+
}
126+
}";
127+
CreateAnalyzerCompilationBuilder()
128+
.AddSourceTexts(source)
129+
.AddAnalyzerOption("dotnet_diagnostic.PH_S019.exclusions", "System.IO.StreamReader;ReadLine,Read,ReadBlock")
130+
.VerifyDiagnostic(new DiagnosticResultLocation(10, 7));
131+
}
132+
111133
[TestMethod]
112134
public void DoesNotReportReadOfTypeWithReadAsyncInNonAsyncMethod() {
113135
const string source = @"
@@ -278,5 +300,74 @@ public Task DoItAsync() {
278300
}";
279301
VerifyDiagnostic(source);
280302
}
303+
304+
[TestMethod]
305+
public void DoesNotReportReadOfTypeWithReadAsyncInAsyncMethodIfExcludedAsSingleTypeAndMethodEntry() {
306+
const string source = @"
307+
using System.IO;
308+
using System.Net.Sockets;
309+
using System.Threading.Tasks;
310+
311+
class Test {
312+
public async Task DoWorkAsync() {
313+
using(var client = new TcpClient())
314+
using(var reader = new StreamReader(client.GetStream())) {
315+
var buffer = new char[1024];
316+
reader.Read(buffer, 0, buffer.Length);
317+
}
318+
}
319+
}";
320+
CreateAnalyzerCompilationBuilder()
321+
.AddSourceTexts(source)
322+
.AddAnalyzerOption("dotnet_diagnostic.PH_S019.exclusions", "System.IO.StreamReader:Read")
323+
.VerifyDiagnostic();
324+
}
325+
326+
[TestMethod]
327+
public void DoesNotReportReadOfTypeWithReadAsyncInAsyncMethodIfExcludedAsSingleTypeEntry() {
328+
const string source = @"
329+
using System.IO;
330+
using System.Net.Sockets;
331+
using System.Threading.Tasks;
332+
333+
class Test {
334+
public async Task DoWorkAsync() {
335+
using(var client = new TcpClient())
336+
using(var reader = new StreamReader(client.GetStream())) {
337+
var buffer = new char[1024];
338+
reader.Read(buffer, 0, buffer.Length);
339+
}
340+
}
341+
}";
342+
CreateAnalyzerCompilationBuilder()
343+
.AddSourceTexts(source)
344+
.AddAnalyzerOption("dotnet_diagnostic.PH_S019.exclusions", "System.IO.StreamReader:ReadLine,Read,ReadBlock")
345+
.VerifyDiagnostic();
346+
}
347+
348+
[TestMethod]
349+
public void DoesNotReportReadOfTypeWithReadAsyncInAsyncMethodIfExcludedAsMultipleTypeEntry() {
350+
const string source = @"
351+
using System.IO;
352+
using System.Net.Sockets;
353+
using System.Threading.Tasks;
354+
355+
class Test {
356+
public async Task DoWorkAsync() {
357+
using(var client = new TcpClient())
358+
using(var reader = new StreamReader(client.GetStream())) {
359+
var buffer = new char[1024];
360+
reader.Read(buffer, 0, buffer.Length);
361+
}
362+
}
363+
}";
364+
const string exclusions = @"Microsoft.EntityFrameworkCore.DbContext:Add,AddRange
365+
Microsoft.EntityFrameworkCore.DbSet`1:Add,AddRange
366+
System.IO.StreamReader:ReadLine,Read,ReadBlock";
367+
CreateAnalyzerCompilationBuilder()
368+
.AddSourceTexts(source)
369+
.AddAnalyzerOption("dotnet_diagnostic.PH_S019.exclusions", exclusions)
370+
.VerifyDiagnostic();
371+
}
281372
}
282373
}

src/ParallelHelper/Analyzer/Smells/BlockingMethodWithAsyncCounterpartInAsyncMethodAnalyzer.cs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ public class BlockingMethodWithAsyncCounterpartInAsyncMethodAnalyzer : Diagnosti
4747

4848
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
4949

50+
private const string DefaultExcludedMethods = @"Microsoft.EntityFrameworkCore.DbContext:Add,AddRange
51+
Microsoft.EntityFrameworkCore.DbSet`1:Add,AddRange";
52+
5053
public override void Initialize(AnalysisContext context) {
5154
context.EnableConcurrentExecution();
5255
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
@@ -74,20 +77,36 @@ public override void Analyze() {
7477
if(!IsAsyncMethod && !IsAsyncAnonymousFunction) {
7578
return;
7679
}
80+
var excludedMethods = GetExcludedMethods().ToArray();
7781
foreach(var invocation in GetAllInvocations()) {
78-
AnalyzeInvocation(invocation);
82+
AnalyzeInvocation(invocation, excludedMethods);
7983
}
8084
}
8185

86+
private IEnumerable<MethodDescriptor> GetExcludedMethods() {
87+
return Context.Options.GetConfig(Rule, "exclusions", DefaultExcludedMethods).Split()
88+
.WithCancellation(CancellationToken)
89+
.Select(ToMethodDescriptor)
90+
.IsNotNull();
91+
}
92+
93+
private static MethodDescriptor? ToMethodDescriptor(string config) {
94+
var splitByTypeAndMethods = config.Split(':');
95+
return splitByTypeAndMethods.Length != 2
96+
? null
97+
: new MethodDescriptor(splitByTypeAndMethods[0], splitByTypeAndMethods[1].Split(','));
98+
}
99+
82100
private IEnumerable<InvocationExpressionSyntax> GetAllInvocations() {
83101
return Root.DescendantNodesInSameActivationFrame()
84102
.WithCancellation(CancellationToken)
85103
.OfType<InvocationExpressionSyntax>();
86104
}
87105

88-
private void AnalyzeInvocation(InvocationExpressionSyntax invocation) {
106+
private void AnalyzeInvocation(InvocationExpressionSyntax invocation, IReadOnlyCollection<MethodDescriptor> excludedMethods) {
89107
if(SemanticModel.GetSymbolInfo(invocation, CancellationToken).Symbol is IMethodSymbol method
90-
&& !IsPotentiallyAsyncMethod(method) && TryGetAsyncCounterpart(method, out var asyncName)) {
108+
&& !IsPotentiallyAsyncMethod(method) && TryGetAsyncCounterpart(method, out var asyncName)
109+
&& !IsExcludedMethod(method, excludedMethods)) {
91110
Context.ReportDiagnostic(Diagnostic.Create(Rule, invocation.GetLocation(), method.Name, asyncName));
92111
}
93112
}
@@ -105,6 +124,26 @@ private static bool TryGetAsyncCounterpart(IMethodSymbol method, out string asyn
105124
asyncName = "";
106125
return false;
107126
}
127+
128+
private bool IsExcludedMethod(IMethodSymbol method, IReadOnlyCollection<MethodDescriptor> excludedMethods) {
129+
return excludedMethods.WithCancellation(CancellationToken)
130+
.Any(excludedMethod => IsAnyMethodOf(method, excludedMethod));
131+
}
132+
133+
private bool IsAnyMethodOf(IMethodSymbol method, MethodDescriptor descriptor) {
134+
return SemanticModel.IsEqualType(method.ContainingType, descriptor.Type)
135+
&& descriptor.Methods.Contains(method.Name);
136+
}
137+
}
138+
139+
private class MethodDescriptor {
140+
public string Type { get; }
141+
public IImmutableSet<string> Methods { get; }
142+
143+
public MethodDescriptor(string type, params string[] methods) {
144+
Type = type;
145+
Methods = methods.ToImmutableHashSet();
146+
}
108147
}
109148
}
110149
}

src/ParallelHelper/ParallelHelper.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<PackageReleaseNotes>- New Analyzer: PH_B015 - Disposed Task Instead of Value
2121
- Improved Analyzer: PH_S007 - Now respects activation frames
2222
- Improved Analyzer: PH_P007 - Now reports default expressions where a token is available
23+
- Improved Analyzer: PH_S019 - Now ignores EF Core's Add and AddRange of DbSet and DbContext by default
2324
- Improved Analyzer: PH_S025 - Now supports lambda expressions and respects activation frames</PackageReleaseNotes>
2425
<Copyright>Copyright (C) 2019 - 2021 Christoph Amrein, Concurrency Lab, Eastern Switzerland University of Applied Sciences, Switzerland</Copyright>
2526
<PackageTags>C#, Parallel, Asynchronous, Concurrency, Bugs, Best Practices, TPL, Task, QC, Static Analysis, async, await</PackageTags>

0 commit comments

Comments
 (0)