Skip to content

Commit da1f7ae

Browse files
committed
WIP add fix for DisposeAsync
1 parent 4c2b7e8 commit da1f7ae

4 files changed

Lines changed: 220 additions & 50 deletions

File tree

IDisposableAnalyzers/Analyzers/FieldAndPropertyDeclarationAnalyzer.cs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,41 @@ private static void HandleFieldOrProperty(SyntaxNodeAnalysisContext context, Fie
6060
}
6161
else if (TestFixture.IsAssignedInInitialize(member, context.SemanticModel, context.CancellationToken, out _, out var setupAttribute))
6262
{
63-
if (!DisposedInTearDown())
63+
switch (TestFixture.FindTearDown(setupAttribute!, context.SemanticModel, context.CancellationToken))
6464
{
65-
context.ReportDiagnostic(Diagnostic.Create(Descriptors.IDISP002DisposeMember, context.Node.GetLocation()));
66-
}
67-
68-
bool DisposedInTearDown()
69-
{
70-
return TestFixture.FindTearDown(setupAttribute!, context.SemanticModel, context.CancellationToken) is { } tearDown &&
71-
DisposableMember.IsDisposed(member.FieldOrProperty, tearDown, context.SemanticModel, context.CancellationToken);
65+
case { } tearDown
66+
when !DisposableMember.IsDisposed(member.FieldOrProperty, tearDown, context.SemanticModel, context.CancellationToken):
67+
context.ReportDiagnostic(
68+
Diagnostic.Create(
69+
Descriptors.IDISP002DisposeMember,
70+
context.Node.GetLocation(),
71+
additionalLocations: new[] { tearDown.GetLocation() }));
72+
break;
73+
case null:
74+
context.ReportDiagnostic(Diagnostic.Create(Descriptors.IDISP002DisposeMember, context.Node.GetLocation()));
75+
break;
7276
}
7377
}
74-
else if (DisposableMember.IsDisposed(member, context.SemanticModel, context.CancellationToken).IsEither(Result.No, Result.AssumeNo))
78+
else
7579
{
76-
context.ReportDiagnostic(Diagnostic.Create(Descriptors.IDISP002DisposeMember, context.Node.GetLocation()));
80+
if (DisposeMethod.FindDisposeAsync(member.FieldOrProperty.ContainingType, context.Compilation, Search.TopLevel) is { } disposeAsync &&
81+
!DisposableMember.IsDisposed(member.FieldOrProperty, disposeAsync, context.SemanticModel, context.CancellationToken))
82+
{
83+
context.ReportDiagnostic(
84+
Diagnostic.Create(
85+
Descriptors.IDISP002DisposeMember,
86+
context.Node.GetLocation(),
87+
additionalLocations: disposeAsync.Locations));
88+
}
7789

78-
if (DisposeMethod.FindFirst(member.FieldOrProperty.ContainingType, context.Compilation, Search.TopLevel) is null)
90+
if (DisposableMember.IsDisposed(member, context.SemanticModel, context.CancellationToken).IsEither(Result.No, Result.AssumeNo))
7991
{
80-
context.ReportDiagnostic(Diagnostic.Create(Descriptors.IDISP006ImplementIDisposable, member.Declaration.GetLocation()));
92+
context.ReportDiagnostic(Diagnostic.Create(Descriptors.IDISP002DisposeMember, context.Node.GetLocation()));
93+
94+
if (DisposeMethod.FindFirst(member.FieldOrProperty.ContainingType, context.Compilation, Search.TopLevel) is null)
95+
{
96+
context.ReportDiagnostic(Diagnostic.Create(Descriptors.IDISP006ImplementIDisposable, member.Declaration.GetLocation()));
97+
}
8198
}
8299
}
83100
}

IDisposableAnalyzers/CodeFixes/DisposeMemberFix.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace IDisposableAnalyzers
22
{
3+
using System;
34
using System.Collections.Immutable;
45
using System.Composition;
56
using System.Diagnostics.CodeAnalysis;
@@ -33,7 +34,65 @@ protected override async Task RegisterCodeFixesAsync(DocumentEditorCodeFixContex
3334
semanticModel.TryGetSymbol(member, context.CancellationToken, out ISymbol? symbol) &&
3435
FieldOrProperty.TryCreate(symbol, out var disposable))
3536
{
36-
if (DisposeMethod.FindVirtual(symbol.ContainingType, semanticModel.Compilation, Search.TopLevel) is { } virtualDispose &&
37+
if (diagnostic.AdditionalLocations.TrySingle(out var additionalLocation) &&
38+
syntaxRoot.TryFindNodeOrAncestor(additionalLocation, out MethodDeclarationSyntax? method))
39+
{
40+
switch (method)
41+
{
42+
case { Identifier: { ValueText: "DisposeAsync" } }:
43+
context.RegisterCodeFix(
44+
$"{symbol.Name}.DisposeAsync() in {method}",
45+
(editor, cancellationToken) => editor.ReplaceNode(
46+
method,
47+
x => DisposeAsync(x, editor, cancellationToken)),
48+
"DisposeAsync",
49+
diagnostic);
50+
51+
MethodDeclarationSyntax DisposeAsync(MethodDeclarationSyntax old, DocumentEditor editor, CancellationToken cancellationToken)
52+
{
53+
return old switch
54+
{
55+
{ ExpressionBody: { Expression: { } expression } }
56+
=> old.AsBlockBody(
57+
SyntaxFactory.ExpressionStatement(expression),
58+
IDisposableFactory.DisposeAsyncStatement(disposable, editor.SemanticModel, cancellationToken)),
59+
{ Body: { } body }
60+
=> old.WithBody(
61+
body.AddStatements(IDisposableFactory.DisposeAsyncStatement(disposable, editor.SemanticModel, cancellationToken))),
62+
_ => throw new InvalidOperationException("Error generating DisposeAsync"),
63+
};
64+
}
65+
66+
break;
67+
68+
default:
69+
context.RegisterCodeFix(
70+
$"{symbol.Name}.Dispose() in {method}",
71+
(editor, cancellationToken) => editor.ReplaceNode(
72+
method,
73+
x => Dispose(x, editor, cancellationToken)),
74+
"TearDown",
75+
diagnostic);
76+
77+
MethodDeclarationSyntax Dispose(MethodDeclarationSyntax old, DocumentEditor editor, CancellationToken cancellationToken)
78+
{
79+
return old switch
80+
{
81+
{ ExpressionBody: { Expression: { } expression } }
82+
=> old.AsBlockBody(
83+
SyntaxFactory.ExpressionStatement(expression),
84+
IDisposableFactory.DisposeStatement(disposable, editor.SemanticModel, cancellationToken)),
85+
{ Body: { } body }
86+
=> old.WithBody(body.AddStatements(
87+
IDisposableFactory.DisposeStatement(disposable, editor.SemanticModel, cancellationToken))),
88+
_ => throw new InvalidOperationException("Error generating Dispose"),
89+
};
90+
}
91+
92+
break;
93+
}
94+
}
95+
else if (DisposeMethod.FindVirtual(symbol.ContainingType, semanticModel.Compilation, Search.TopLevel) is { } virtualDispose &&
3796
virtualDispose.TrySingleDeclaration(context.CancellationToken, out MethodDeclarationSyntax? disposeDeclaration))
3897
{
3998
if (disposeDeclaration is { ParameterList: { Parameters: { Count: 1 } parameters }, Body: { } block })

IDisposableAnalyzers/CodeFixes/Helpers/IDisposableFactory.cs

Lines changed: 107 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ internal static class IDisposableFactory
1919
SyntaxFactory.QualifiedName(SyntaxFactory.IdentifierName("System"), SyntaxFactory.IdentifierName("IDisposable"))
2020
.WithAdditionalAnnotations(Simplifier.Annotation);
2121

22+
internal static readonly TypeSyntax SystemIAsyncDisposable =
23+
SyntaxFactory.QualifiedName(SyntaxFactory.IdentifierName("System"), SyntaxFactory.IdentifierName("IAsyncDisposable"))
24+
.WithAdditionalAnnotations(Simplifier.Annotation);
25+
2226
internal static readonly StatementSyntax GcSuppressFinalizeThis =
2327
SyntaxFactory.ExpressionStatement(
2428
SyntaxFactory.InvocationExpression(
@@ -105,58 +109,71 @@ ExpressionSyntax Normalize(ExpressionSyntax e)
105109
}
106110
}
107111

108-
internal static ExpressionStatementSyntax DisposeStatement(FieldOrProperty disposable, SemanticModel semanticModel, CancellationToken cancellationToken)
112+
internal static ExpressionStatementSyntax DisposeAsyncStatement(FieldOrProperty disposable, SemanticModel semanticModel, CancellationToken cancellationToken)
109113
{
110-
using (var walker = MutationWalker.For(disposable, semanticModel, cancellationToken))
114+
switch (MemberAccessContext.Create(disposable, semanticModel, cancellationToken))
111115
{
112-
if (IsNeverNull(out var neverNull))
113-
{
114-
if (disposable.Type.IsAssignableTo(KnownSymbol.IDisposable, semanticModel.Compilation) &&
115-
DisposeMethod.Find(disposable.Type, semanticModel.Compilation, Search.Recursive) is { ExplicitInterfaceImplementations: { IsEmpty: true } })
116+
case { NeverNull: { } neverNull }:
117+
if (disposable.Type.IsAssignableTo(KnownSymbol.IAsyncDisposable, semanticModel.Compilation) &&
118+
DisposeMethod.FindDisposeAsync(disposable.Type, semanticModel.Compilation, Search.Recursive) is { ExplicitInterfaceImplementations: { IsEmpty: true } })
116119
{
117-
return DisposeStatement(neverNull.WithoutTrivia()).WithLeadingElasticLineFeed();
120+
return AsyncDisposeStatement(neverNull.WithoutTrivia()).WithLeadingElasticLineFeed();
118121
}
119122

120-
return DisposeStatement(
121-
SyntaxFactory.CastExpression(
122-
SystemIDisposable,
123-
neverNull.WithoutTrivia()))
123+
return AsyncDisposeStatement(
124+
SyntaxFactory.CastExpression(
125+
SystemIAsyncDisposable,
126+
neverNull.WithoutTrivia()))
124127
.WithLeadingElasticLineFeed();
125-
}
126128

127-
bool IsNeverNull(out ExpressionSyntax memberAccess)
128-
{
129-
if (walker.TrySingle(out var mutation) &&
130-
mutation is AssignmentExpressionSyntax { Left: { } single, Right: ObjectCreationExpressionSyntax _, Parent: ExpressionStatementSyntax { Parent: BlockSyntax { Parent: ConstructorDeclarationSyntax _ } } } &&
131-
disposable.Symbol.ContainingType.Constructors.Length == 1)
129+
static ExpressionStatementSyntax AsyncDisposeStatement(ExpressionSyntax expression)
132130
{
133-
memberAccess = single;
134-
return true;
131+
return SyntaxFactory.ExpressionStatement(
132+
SyntaxFactory.AwaitExpression(
133+
expression: SyntaxFactory.InvocationExpression(
134+
expression: SyntaxFactory.MemberAccessExpression(
135+
kind: SyntaxKind.SimpleMemberAccessExpression,
136+
expression: expression,
137+
name: SyntaxFactory.IdentifierName("DisposeAsync")),
138+
argumentList: SyntaxFactory.ArgumentList())));
135139
}
136140

137-
if (walker.IsEmpty &&
138-
disposable.Initializer(cancellationToken) is { Value: ObjectCreationExpressionSyntax _ })
141+
default:
142+
throw new InvalidOperationException("Error generating DisposeAsyncStatement.");
143+
}
144+
}
145+
146+
internal static ExpressionStatementSyntax DisposeStatement(FieldOrProperty disposable, SemanticModel semanticModel, CancellationToken cancellationToken)
147+
{
148+
switch (MemberAccessContext.Create(disposable, semanticModel, cancellationToken))
149+
{
150+
case { NeverNull: { } neverNull }:
151+
if (disposable.Type.IsAssignableTo(KnownSymbol.IDisposable, semanticModel.Compilation) &&
152+
DisposeMethod.Find(disposable.Type, semanticModel.Compilation, Search.Recursive) is { ExplicitInterfaceImplementations: { IsEmpty: true } })
139153
{
140-
memberAccess = MemberAccess(disposable, semanticModel, cancellationToken);
141-
return true;
154+
return DisposeStatement(neverNull.WithoutTrivia()).WithLeadingElasticLineFeed();
142155
}
143156

144-
memberAccess = null!;
145-
return false;
146-
}
147-
}
157+
return DisposeStatement(
158+
SyntaxFactory.CastExpression(
159+
SystemIDisposable,
160+
neverNull.WithoutTrivia()))
161+
.WithLeadingElasticLineFeed();
162+
case { MaybeNull: { } maybeNull }:
163+
if (DisposeMethod.IsAccessibleOn(disposable.Type, semanticModel.Compilation))
164+
{
165+
return ConditionalDisposeStatement(maybeNull).WithLeadingElasticLineFeed();
166+
}
148167

149-
if (DisposeMethod.IsAccessibleOn(disposable.Type, semanticModel.Compilation))
150-
{
151-
return ConditionalDisposeStatement(MemberAccess(disposable, semanticModel, cancellationToken)).WithLeadingElasticLineFeed();
168+
return ConditionalDisposeStatement(
169+
SyntaxFactory.BinaryExpression(
170+
SyntaxKind.AsExpression,
171+
maybeNull,
172+
SystemIDisposable))
173+
.WithLeadingElasticLineFeed();
174+
default:
175+
throw new InvalidOperationException("Error generating DisposeStatement.");
152176
}
153-
154-
return ConditionalDisposeStatement(
155-
SyntaxFactory.BinaryExpression(
156-
SyntaxKind.AsExpression,
157-
MemberAccess(disposable, semanticModel, cancellationToken),
158-
SystemIDisposable))
159-
.WithLeadingElasticLineFeed();
160177
}
161178

162179
internal static ExpressionSyntax MemberAccess(SyntaxToken memberIdentifier, SemanticModel semanticModel, CancellationToken cancellationToken)
@@ -260,5 +277,58 @@ internal static ArgumentListSyntax Arguments(ExpressionSyntax expression)
260277
{
261278
return SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(expression)));
262279
}
280+
281+
private struct MemberAccessContext
282+
{
283+
internal readonly ExpressionSyntax? NeverNull;
284+
internal readonly ExpressionSyntax? MaybeNull;
285+
286+
private MemberAccessContext(ExpressionSyntax? neverNull, ExpressionSyntax? maybeNull)
287+
{
288+
this.NeverNull = neverNull;
289+
this.MaybeNull = maybeNull;
290+
}
291+
292+
internal static MemberAccessContext Create(FieldOrProperty disposable, SemanticModel semanticModel, CancellationToken cancellationToken)
293+
{
294+
using (var walker = MutationWalker.For(disposable, semanticModel, cancellationToken))
295+
{
296+
if (IsNeverNull(out var neverNull))
297+
{
298+
return new MemberAccessContext(neverNull.WithoutTrivia(), null);
299+
}
300+
301+
if (walker.Assignments.TryFirst(out var first))
302+
{
303+
return new MemberAccessContext(null, first.Left.WithoutTrivia());
304+
}
305+
306+
bool IsNeverNull(out ExpressionSyntax memberAccess)
307+
{
308+
if (walker.TrySingle(out var mutation) &&
309+
mutation is AssignmentExpressionSyntax { Left: { } single, Right: ObjectCreationExpressionSyntax _, Parent: ExpressionStatementSyntax { Parent: BlockSyntax { Parent: ConstructorDeclarationSyntax _ } } } &&
310+
disposable.Symbol.ContainingType.Constructors.Length == 1)
311+
{
312+
memberAccess = single;
313+
return true;
314+
}
315+
316+
if (walker.IsEmpty &&
317+
disposable.Initializer(cancellationToken) is { Value: ObjectCreationExpressionSyntax _ })
318+
{
319+
memberAccess = MemberAccess(disposable, semanticModel, cancellationToken);
320+
return true;
321+
}
322+
323+
memberAccess = null!;
324+
return false;
325+
}
326+
}
327+
328+
return new MemberAccessContext(
329+
null,
330+
MemberAccess(disposable, semanticModel, cancellationToken));
331+
}
332+
}
263333
}
264334
}

IDisposableAnalyzers/Helpers/DisposeMethod.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,30 @@ static bool IsMatch(IMethodSymbol candidate)
7979
return null;
8080
}
8181

82+
internal static IMethodSymbol? FindDisposeAsync(ITypeSymbol type, Compilation compilation, Search search)
83+
{
84+
if (!type.IsAssignableTo(KnownSymbol.IAsyncDisposable, compilation))
85+
{
86+
return null;
87+
}
88+
89+
if (search == Search.TopLevel)
90+
{
91+
return type.TryFindFirstMethod("DisposeAsync", x => IsMatch(x), out var topLevel)
92+
? topLevel
93+
: null;
94+
}
95+
96+
return type.TryFindFirstMethodRecursive("DisposeAsync", x => IsMatch(x), out var recursive)
97+
? recursive
98+
: null;
99+
100+
static bool IsMatch(IMethodSymbol candidate)
101+
{
102+
return candidate is { DeclaredAccessibility: Accessibility.Public, ReturnsVoid: false, Name: "DisposeAsync", Parameters: { Length: 0 } };
103+
}
104+
}
105+
82106
internal static bool IsAccessibleOn(ITypeSymbol type, Compilation compilation)
83107
{
84108
if (type.TypeKind == TypeKind.Interface)

0 commit comments

Comments
 (0)