Skip to content

Commit ced84c3

Browse files
committed
Initial IAsyncDisposable
#199
1 parent 455d7e3 commit ced84c3

11 files changed

Lines changed: 213 additions & 10 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
namespace IDisposableAnalyzers.NetCoreTests.Helpers
2+
{
3+
using System.Threading;
4+
using Gu.Roslyn.AnalyzerExtensions;
5+
using Gu.Roslyn.Asserts;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using NUnit.Framework;
8+
9+
public static class DisposableMemberTests
10+
{
11+
[Test]
12+
public static void SimpleFieldIAsyncDisposable()
13+
{
14+
var syntaxTree = CSharpSyntaxTree.ParseText(@"
15+
namespace N
16+
{
17+
using System;
18+
using System.IO;
19+
using System.Threading.Tasks;
20+
21+
public class C : IAsyncDisposable
22+
{
23+
private readonly IAsyncDisposable disposable = File.OpenRead(string.Empty);
24+
25+
public async ValueTask DisposeAsync()
26+
{
27+
await this.disposable.DisposeAsync();
28+
}
29+
}
30+
}");
31+
var compilation = CSharpCompilation.Create("test", new[] { syntaxTree }, MetadataReferences.FromAttributes());
32+
var semanticModel = compilation.GetSemanticModel(syntaxTree);
33+
var declaration = syntaxTree.FindFieldDeclaration("disposable");
34+
var symbol = semanticModel.GetDeclaredSymbolSafe(declaration, CancellationToken.None);
35+
Assert.AreEqual(Result.Yes, DisposableMember.IsDisposed(new FieldOrPropertyAndDeclaration(symbol, declaration), semanticModel, CancellationToken.None));
36+
}
37+
}
38+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace IDisposableAnalyzers.NetCoreTests.IDISP002DisposeMemberTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using NUnit.Framework;
6+
7+
public static class Valid
8+
{
9+
private static readonly DiagnosticAnalyzer Analyzer = new FieldAndPropertyDeclarationAnalyzer();
10+
11+
[Test]
12+
public static void FieldDisposeAsyncInDisposeAsync()
13+
{
14+
var code = @"
15+
namespace N
16+
{
17+
using System;
18+
using System.IO;
19+
using System.Threading.Tasks;
20+
21+
public class C : IAsyncDisposable
22+
{
23+
private readonly IAsyncDisposable disposable = File.OpenRead(string.Empty);
24+
25+
public async ValueTask DisposeAsync()
26+
{
27+
await this.disposable.DisposeAsync();
28+
}
29+
}
30+
}";
31+
RoslynAssert.Valid(Analyzer, code);
32+
}
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace IDisposableAnalyzers.NetCoreTests.IDISP006ImplementIDisposableTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using NUnit.Framework;
6+
7+
public static class Valid
8+
{
9+
private static readonly DiagnosticAnalyzer Analyzer = new FieldAndPropertyDeclarationAnalyzer();
10+
11+
[Test]
12+
public static void FieldDisposeAsyncInDisposeAsync()
13+
{
14+
var code = @"
15+
namespace N
16+
{
17+
using System;
18+
using System.IO;
19+
using System.Threading.Tasks;
20+
21+
public class C : IAsyncDisposable
22+
{
23+
private readonly IAsyncDisposable disposable = File.OpenRead(string.Empty);
24+
25+
public async ValueTask DisposeAsync()
26+
{
27+
await this.disposable.DisposeAsync();
28+
}
29+
}
30+
}";
31+
RoslynAssert.Valid(Analyzer, code);
32+
}
33+
}
34+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// ReSharper disable InconsistentNaming
2+
#pragma warning disable GURA04, GURA06 // Name of class should match asserts.
3+
namespace IDisposableAnalyzers.NetCoreTests
4+
{
5+
using System;
6+
using System.Collections.Immutable;
7+
using System.Linq;
8+
using Gu.Roslyn.AnalyzerExtensions;
9+
using Gu.Roslyn.Asserts;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.Diagnostics;
12+
using NUnit.Framework;
13+
14+
public static class ValidWithAllAnalyzers
15+
{
16+
private static readonly ImmutableArray<DiagnosticAnalyzer> AllAnalyzers = typeof(KnownSymbol)
17+
.Assembly
18+
.GetTypes()
19+
.Where(typeof(DiagnosticAnalyzer).IsAssignableFrom)
20+
.Select(t => (DiagnosticAnalyzer)Activator.CreateInstance(t))
21+
.ToImmutableArray();
22+
23+
// ReSharper disable once InconsistentNaming
24+
private static readonly Solution ValidCodeProjectSln = CodeFactory.CreateSolution(
25+
ProjectFile.Find("ValidCode.NetCore.csproj"),
26+
AllAnalyzers,
27+
MetadataReferences.FromAttributes());
28+
29+
private static IDisposable cacheTransaction;
30+
31+
[OneTimeSetUp]
32+
public static void OneTimeSetUp()
33+
{
34+
// The cache will be enabled when running in VS.
35+
// It speeds up the tests and makes them more realistic
36+
cacheTransaction = SyntaxTreeCache<SemanticModel>.Begin(null);
37+
}
38+
39+
[OneTimeTearDown]
40+
public static void OneTimeTearDown()
41+
{
42+
cacheTransaction.Dispose();
43+
}
44+
45+
[Test]
46+
public static void NotEmpty()
47+
{
48+
CollectionAssert.IsNotEmpty(AllAnalyzers);
49+
}
50+
51+
[TestCaseSource(nameof(AllAnalyzers))]
52+
public static void ValidCodeProject(DiagnosticAnalyzer analyzer)
53+
{
54+
RoslynAssert.Valid(analyzer, ValidCodeProjectSln);
55+
}
56+
}
57+
}

IDisposableAnalyzers.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IDisposableAnalyzers.NetCor
5656
EndProject
5757
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValidCode", "ValidCode\ValidCode.csproj", "{C6A235B1-A780-43D5-BA1F-E31861C019F5}"
5858
EndProject
59+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ValidCode.NetCore", "ValidCode.NetCore\ValidCode.NetCore.csproj", "{2E2A331D-D45F-49E9-A4B7-FDF64367E2A5}"
60+
EndProject
5961
Global
6062
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6163
Debug|Any CPU = Debug|Any CPU
@@ -86,6 +88,10 @@ Global
8688
{C6A235B1-A780-43D5-BA1F-E31861C019F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
8789
{C6A235B1-A780-43D5-BA1F-E31861C019F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
8890
{C6A235B1-A780-43D5-BA1F-E31861C019F5}.Release|Any CPU.Build.0 = Release|Any CPU
91+
{2E2A331D-D45F-49E9-A4B7-FDF64367E2A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
92+
{2E2A331D-D45F-49E9-A4B7-FDF64367E2A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
93+
{2E2A331D-D45F-49E9-A4B7-FDF64367E2A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
94+
{2E2A331D-D45F-49E9-A4B7-FDF64367E2A5}.Release|Any CPU.Build.0 = Release|Any CPU
8995
EndGlobalSection
9096
GlobalSection(SolutionProperties) = preSolution
9197
HideSolutionNode = FALSE

IDisposableAnalyzers/Analyzers/DisposeCallAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ private static void Handle(SyntaxNodeAnalysisContext context)
2929
{
3030
if (!context.IsExcludedFromAnalysis() &&
3131
context.Node is InvocationExpressionSyntax invocation &&
32-
DisposeCall.IsIDisposableDispose(invocation, context.SemanticModel, context.CancellationToken) &&
32+
DisposeCall.IsMatch(invocation, context.SemanticModel, context.CancellationToken) &&
3333
!invocation.TryFirstAncestorOrSelf<AnonymousFunctionExpressionSyntax>(out _) &&
3434
DisposeCall.TryGetDisposedRootMember(invocation, context.SemanticModel, context.CancellationToken, out var root))
3535
{

IDisposableAnalyzers/Analyzers/FieldAndPropertyDeclarationAnalyzer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ private static void HandleFieldOrProperty(SyntaxNodeAnalysisContext context, Fie
6363
{
6464
context.ReportDiagnostic(Diagnostic.Create(Descriptors.IDISP002DisposeMember, context.Node.GetLocation()));
6565

66-
if (!DisposeMethod.TryFindFirst(member.FieldOrProperty.ContainingType, context.Compilation, Search.TopLevel, out _) &&
67-
!TestFixture.IsAssignedInInitialize(member, context.SemanticModel, context.CancellationToken, out _, out _))
66+
if (!TestFixture.IsAssignedInInitialize(member, context.SemanticModel, context.CancellationToken, out _, out _) &&
67+
!DisposeMethod.TryFindFirst(member.FieldOrProperty.ContainingType, context.Compilation, Search.TopLevel, out _))
6868
{
69-
context.ReportDiagnostic(Diagnostic.Create(Descriptors.IDISP006ImplementIDisposable, context.Node.GetLocation()));
69+
context.ReportDiagnostic(Diagnostic.Create(Descriptors.IDISP006ImplementIDisposable, member.Declaration.GetLocation()));
7070
}
7171
}
7272
}

IDisposableAnalyzers/Helpers/DisposeCall.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ internal static class DisposeCall
1111
internal static bool TryGetDisposed(InvocationExpressionSyntax disposeCall, SemanticModel semanticModel, CancellationToken cancellationToken, [NotNullWhen(true)] out ISymbol? disposed)
1212
{
1313
disposed = null;
14-
return IsIDisposableDispose(disposeCall, semanticModel, cancellationToken) &&
14+
return IsMatch(disposeCall, semanticModel, cancellationToken) &&
1515
MemberPath.TrySingle(disposeCall, out var expression) &&
1616
semanticModel.TryGetSymbol(expression, cancellationToken, out disposed);
1717
}
@@ -71,13 +71,11 @@ internal static bool IsDisposing(InvocationExpressionSyntax disposeCall, ISymbol
7171
return false;
7272
}
7373

74-
internal static bool IsIDisposableDispose(InvocationExpressionSyntax candidate, SemanticModel semanticModel, CancellationToken cancellationToken)
74+
internal static bool IsMatch(InvocationExpressionSyntax candidate, SemanticModel semanticModel, CancellationToken cancellationToken)
7575
{
7676
return candidate.ArgumentList is { Arguments: { Count: 0 } } &&
77-
candidate.TryGetMethodName(out var name) &&
78-
name == "Dispose" &&
79-
semanticModel.TryGetSymbol(candidate, cancellationToken, out var method) &&
80-
method.ContainingType.IsAssignableTo(KnownSymbol.IDisposable, semanticModel.Compilation);
77+
(candidate.IsSymbol(KnownSymbol.IDisposable.Dispose, semanticModel, cancellationToken) ||
78+
candidate.IsSymbol(KnownSymbol.IAsyncDisposable.DisposeAsync, semanticModel, cancellationToken));
8179
}
8280
}
8381
}

IDisposableAnalyzers/Helpers/Walkers/DisposeWalker.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node)
2929
{
3030
this.invocations.Add(node);
3131
}
32+
33+
if (this.SemanticModel.TryGetSymbol(node, KnownSymbol.IAsyncDisposable.DisposeAsync, this.CancellationToken, out var disposeAsync) &&
34+
disposeAsync.Parameters.Length == 0)
35+
{
36+
this.invocations.Add(node);
37+
}
3238
}
3339

3440
public override void VisitIdentifierName(IdentifierNameSyntax node)
@@ -46,6 +52,13 @@ internal static DisposeWalker Borrow(INamedTypeSymbol type, SemanticModel semant
4652
return BorrowAndVisit(declaration, SearchScope.Instance, type, semanticModel, () => new DisposeWalker(), cancellationToken);
4753
}
4854

55+
if (type.IsAssignableTo(KnownSymbol.IAsyncDisposable, semanticModel.Compilation) &&
56+
type.TryFindFirstMethod(x => x is { Parameters: { Length: 0 } } && x == KnownSymbol.IAsyncDisposable.DisposeAsync, out disposeMethod) &&
57+
disposeMethod.TrySingleDeclaration(cancellationToken, out declaration))
58+
{
59+
return BorrowAndVisit(declaration, SearchScope.Instance, type, semanticModel, () => new DisposeWalker(), cancellationToken);
60+
}
61+
4962
return Borrow(() => new DisposeWalker());
5063
}
5164

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace ValidCode.NetCore.AsyncDisposable
2+
{
3+
using System;
4+
using System.IO;
5+
using System.Threading.Tasks;
6+
7+
public class Impl : IAsyncDisposable
8+
{
9+
private readonly IAsyncDisposable disposable = File.OpenRead(string.Empty);
10+
11+
public async ValueTask DisposeAsync()
12+
{
13+
await this.disposable.DisposeAsync();
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)