Skip to content

Commit 00327bf

Browse files
authored
PH_S035: Blocking Async Method Invocation in Constructor (#102)
* Implemented analyzer for blocking async access in constructors * Updated app version to v3.7.0 * Added PH_S035 to the release notes * Add documentation for PH_S035 * Fixed the analysis title
1 parent dcd1927 commit 00327bf

6 files changed

Lines changed: 211 additions & 4 deletions

File tree

doc/analyzers/PH_S035.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# PH_S034 - Blocking Async Method Invocation in Constructor
2+
3+
## Problem
4+
5+
Constructors cannot be asynchronous. Therefore, developers tend to call asynchronous APIs from the constructor blockingly, either using `Wait()` or `Result`.
6+
7+
```cs
8+
class FileResource {
9+
public string Content { get; }
10+
11+
public FileResource(string filePath) {
12+
Content = File.ReadAllTextAsync(filePath).Result;
13+
}
14+
}
15+
```
16+
17+
Invoking async methods to await their completion blockingly is generally discouraged, as stated by [PH_P005](PH_P005.md).
18+
19+
20+
## Solution
21+
22+
Refactor the constructor to an `async` factory method that the caller can await. Alternatively, the invocation of the blocking counterpart might be an acceptable solution here.
23+
24+
```cs
25+
// Solution #1 - Refactor to factory method (recommended)
26+
class FileResource {
27+
public string Content { get; }
28+
29+
private FileResource(string content) {
30+
Content = content;
31+
}
32+
33+
public static async Task<FileResource> CreateAsync(string filePath) {
34+
return new FileResource(
35+
await File.ReadAllTextAsync(filePath)
36+
);
37+
}
38+
}
39+
40+
// Solution #2 - Call the synchronous API
41+
class FileResource {
42+
public string Content { get; }
43+
44+
public FileResource(string filePath) {
45+
Content = File.ReadAllText(filePath);
46+
}
47+
}
48+
```

src/ParallelHelper.Plugin/ReleaseNotes.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
v3.6.0
1+
v3.7.0
2+
3+
- New Analyzer: PH_S035 - Blocking Async Method Invocation in Constructor
4+
5+
6+
------------------
7+
8+
v3.6.0
29

310
- New Analyzer: PH_S034 - Async Lambda Inferred to Async Void
411

src/ParallelHelper.Plugin/source.extension.vsixmanifest

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
33
<Metadata>
4-
<Identity Id="ParallelHelper.f833642b-cd24-49ac-addc-e5328ce8fe88" Version="3.6.0" Language="en-US" Publisher="Christoph Amrein" />
4+
<Identity Id="ParallelHelper.f833642b-cd24-49ac-addc-e5328ce8fe88" Version="3.7.0" Language="en-US" Publisher="Christoph Amrein" />
55
<DisplayName>ParallelHelper</DisplayName>
66
<Description xml:space="preserve">ParallelHelper is a static code analyzer that helps to identify concurrency related issues. Moreover, it provides hints to improve the robustness of code with concurrency in mind.</Description>
77
<MoreInfo>https://github.com/Concurrency-Lab/ParallelHelper</MoreInfo>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2+
using ParallelHelper.Analyzer.Smells;
3+
4+
namespace ParallelHelper.Test.Analyzer.Smells {
5+
[TestClass]
6+
public class BlockingAsyncMethodInvocationInConstructorAnalyzerTest : AnalyzerTestBase<BlockingAsyncMethodInvocationInConstructorAnalyzer> {
7+
[TestMethod]
8+
public void ReportsTaskRunWaitInvocationInConstructor() {
9+
const string source = @"
10+
using System.Threading.Tasks;
11+
12+
class Test {
13+
public Test() {
14+
Task.Run(() => {}).Wait();
15+
}
16+
}";
17+
VerifyDiagnostic(source, new DiagnosticResultLocation(5, 5));
18+
}
19+
20+
[TestMethod]
21+
public void ReportsWebClientDownloadResultAccessInConstructor() {
22+
const string source = @"
23+
using System;
24+
using System.Net;
25+
26+
class Test {
27+
private readonly string content;
28+
29+
public Test(Uri address) {
30+
using(var client = new WebClient()) {
31+
content = client.DownloadStringTaskAsync(address).Result;
32+
}
33+
}
34+
}";
35+
VerifyDiagnostic(source, new DiagnosticResultLocation(9, 17));
36+
}
37+
38+
[TestMethod]
39+
public void DoesNotReportTaskRunWaitInvocationInDifferentActivationFrameInConstructor() {
40+
const string source = @"
41+
using System;
42+
using System.Threading.Tasks;
43+
44+
class Test {
45+
public Test() {
46+
Action action = () => Task.Run(() => {}).Wait();
47+
}
48+
}";
49+
VerifyDiagnostic(source);
50+
}
51+
52+
[TestMethod]
53+
public void DoesNotReportWebClientDownloadResultAccessInMethod() {
54+
const string source = @"
55+
using System;
56+
using System.Net;
57+
58+
class Test {
59+
private string content;
60+
61+
public void Refresh(Uri address) {
62+
using(var client = new WebClient()) {
63+
content = client.DownloadStringTaskAsync(address).Result;
64+
}
65+
}
66+
}";
67+
VerifyDiagnostic(source);
68+
}
69+
}
70+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using ParallelHelper.Extensions;
6+
using ParallelHelper.Util;
7+
using System.Collections.Generic;
8+
using System.Collections.Immutable;
9+
using System.Linq;
10+
11+
namespace ParallelHelper.Analyzer.Smells {
12+
/// <summary>
13+
/// Analyzer that analyzes sources for blocking invocations of async methods in constructors.
14+
///
15+
/// <example>Illustrates a class with a method that invokes an async method within a constructor and blocks for the result.
16+
/// <code>
17+
/// class Sample {
18+
/// private readonly string content;
19+
///
20+
/// public Sample(string filePath) {
21+
/// content = File.ReadAllTextAsync(filePath).Result;
22+
/// }
23+
/// }
24+
/// </code>
25+
/// </example>
26+
/// </summary>
27+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
28+
public class BlockingAsyncMethodInvocationInConstructorAnalyzer : DiagnosticAnalyzer {
29+
public const string DiagnosticId = "PH_S035";
30+
31+
private const string Category = "Concurrency";
32+
33+
private static readonly LocalizableString Title = "Blocking Async Method Invocation in Constructor";
34+
private static readonly LocalizableString MessageFormat = "The blocking invocation of async methods within the constructor is discouraged since they cannot be awaited. Use a factory method instead.";
35+
private static readonly LocalizableString Description = "";
36+
37+
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
38+
DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning,
39+
isEnabledByDefault: true, description: Description, helpLinkUri: HelpLinkFactory.CreateUri(DiagnosticId)
40+
);
41+
42+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
43+
44+
public override void Initialize(AnalysisContext context) {
45+
context.EnableConcurrentExecution();
46+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
47+
context.RegisterSyntaxNodeAction(AnalyzeConstructor, SyntaxKind.ConstructorDeclaration);
48+
}
49+
50+
private static void AnalyzeConstructor(SyntaxNodeAnalysisContext context) {
51+
new Analyzer(context).Analyze();
52+
}
53+
54+
private class Analyzer : InternalAnalyzerBase<ConstructorDeclarationSyntax> {
55+
private readonly TaskAnalysis _taskAnalysis;
56+
57+
public Analyzer(SyntaxNodeAnalysisContext context) : base(new SyntaxNodeAnalysisContextWrapper(context)) {
58+
_taskAnalysis = new TaskAnalysis(context.SemanticModel, context.CancellationToken);
59+
}
60+
61+
public override void Analyze() {
62+
foreach(var blockingAccess in GetBlockingTaskAccesses()) {
63+
Context.ReportDiagnostic(Diagnostic.Create(Rule, blockingAccess.GetLocation()));
64+
}
65+
}
66+
67+
private IEnumerable<SyntaxNode> GetBlockingTaskAccesses() {
68+
return Root.DescendantNodesInSameActivationFrame()
69+
.WithCancellation(CancellationToken)
70+
.Where(IsBlockingTaskAccess);
71+
}
72+
73+
private bool IsBlockingTaskAccess(SyntaxNode node) {
74+
return node switch {
75+
InvocationExpressionSyntax invocation => _taskAnalysis.IsBlockingMethodInvocation(invocation),
76+
MemberAccessExpressionSyntax memberAccess => _taskAnalysis.IsBlockingPropertyAccess(memberAccess),
77+
_ => false
78+
};
79+
}
80+
}
81+
}
82+
}

src/ParallelHelper/ParallelHelper.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010

1111
<PropertyGroup>
1212
<PackageId>ParallelHelper</PackageId>
13-
<Version>3.6.0</Version>
13+
<Version>3.7.0</Version>
1414
<Authors>Christoph Amrein</Authors>
1515
<PackageProjectUrl>https://github.com/Concurrency-Lab/ParallelHelper</PackageProjectUrl>
1616
<RepositoryUrl>https://github.com/Concurrency-Lab/ParallelHelper</RepositoryUrl>
1717
<RepositoryType>git</RepositoryType>
1818
<Description>ParallelHelper is a static code analyzer that helps to identify concurrency related issues. Moreover, it provides hints to improve the robustness of code with concurrency in mind.</Description>
19-
<PackageReleaseNotes>- New Analyzer: PH_S034 - Async Lambda Inferred to Async Void</PackageReleaseNotes>
19+
<PackageReleaseNotes>- New Analyzer: PH_S035 - Blocking Async Method Invocation in Constructor</PackageReleaseNotes>
2020
<Copyright>Copyright (C) 2022 Christoph Amrein</Copyright>
2121
<PackageTags>C#, Parallel, Asynchronous, Concurrency, Bugs, Best Practices, TPL, Task, QC, Static Analysis, async, await</PackageTags>
2222
<NoPackageAnalysis>true</NoPackageAnalysis>

0 commit comments

Comments
 (0)