Skip to content

Commit 5dee0c1

Browse files
Recognize "(field as IDisposable)?.Dispose()"
1 parent 3e3b1c3 commit 5dee0c1

File tree

2 files changed

+71
-12
lines changed

2 files changed

+71
-12
lines changed

src/nunit.analyzers.tests/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzerTests.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,17 @@ public void TearDownMethod()
6363
RoslynAssert.Valid(analyzer, testCode);
6464
}
6565

66-
[Test]
67-
public void AnalyzeWhenFieldIsConditionallyDisposed()
66+
[TestCase("IDisposable")]
67+
[TestCase("System.IDisposable")]
68+
public void AnalyzeWhenFieldIsConditionallyDisposedUsingIsIDisposable(string interfaceName)
6869
{
6970
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
7071
private object field = new DummyDisposable();
7172
7273
[OneTimeTearDown]
7374
public void TearDownMethod()
7475
{{
75-
if (field is IDisposable disposable)
76+
if (field is {interfaceName} disposable)
7677
disposable.Dispose();
7778
}}
7879
@@ -82,6 +83,25 @@ public void TearDownMethod()
8283
RoslynAssert.Valid(analyzer, testCode);
8384
}
8485

86+
[TestCase("IDisposable")]
87+
[TestCase("System.IDisposable")]
88+
public void AnalyzeWhenFieldIsConditionallyDisposedUsingAsIDisposable(string interfaceName)
89+
{
90+
var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@"
91+
private object field = new DummyDisposable();
92+
93+
[OneTimeTearDown]
94+
public void TearDownMethod()
95+
{{
96+
(field as {interfaceName})?.Dispose();
97+
}}
98+
99+
{DummyDisposable}
100+
");
101+
102+
RoslynAssert.Valid(analyzer, testCode);
103+
}
104+
85105
[Test]
86106
public void AnalyzeWhenFieldWithInitializerIsDisposedInOneTimeTearDownMethod()
87107
{

src/nunit.analyzers/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzer.cs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,7 @@ private static void DisposedIn(Parameters parameters, HashSet<string> disposals,
480480
// disposable.Dispose();
481481
if (ifStatement.Condition is IsPatternExpressionSyntax isPatternExpression &&
482482
isPatternExpression.Pattern is DeclarationPatternSyntax declarationPattern &&
483-
declarationPattern.Type is IdentifierNameSyntax identifierName &&
484-
identifierName.Identifier.Text.EndsWith("Disposable", StringComparison.Ordinal) &&
483+
IsDisposable(declarationPattern.Type) &&
485484
declarationPattern.Designation is SingleVariableDesignationSyntax singleVariableDesignation)
486485
{
487486
string? member = GetIdentifier(isPatternExpression.Expression);
@@ -555,19 +554,59 @@ private static void DisposedIn(Parameters parameters, HashSet<string> disposals,
555554
return memberAccessExpression.Name.Identifier.Text;
556555
}
557556

558-
// considering cast to IDisposable, e.g. in case of explicit interface implementation of IDisposable.Dispose()
559-
else if (expression is ParenthesizedExpressionSyntax parenthesizedExpression &&
560-
parenthesizedExpression.Expression is CastExpressionSyntax castExpression &&
561-
castExpression.Expression is IdentifierNameSyntax castIdentifierName &&
562-
castExpression.Type is IdentifierNameSyntax typeIdentifierName &&
563-
typeIdentifierName.Identifier.Text.Equals("IDisposable", StringComparison.Ordinal))
557+
// considering cast to I(Async)Disposable, e.g. in case of explicit interface implementation of IDisposable.Dispose()
558+
// or in case of 'as IDisposable' or 'as IAsyncDisposable'
559+
else if (expression is ParenthesizedExpressionSyntax parenthesizedExpression)
564560
{
565-
return castIdentifierName.Identifier.Text;
561+
IdentifierNameSyntax? memberIdentifierName = null;
562+
ExpressionSyntax? typeExpression = null;
563+
564+
if (parenthesizedExpression.Expression is CastExpressionSyntax castExpression)
565+
{
566+
memberIdentifierName = castExpression.Expression as IdentifierNameSyntax;
567+
typeExpression = castExpression.Type;
568+
}
569+
else if (parenthesizedExpression.Expression is BinaryExpressionSyntax binaryExpression &&
570+
binaryExpression.IsKind(SyntaxKind.AsExpression))
571+
{
572+
memberIdentifierName = binaryExpression.Left as IdentifierNameSyntax;
573+
typeExpression = binaryExpression.Right;
574+
}
575+
576+
if (memberIdentifierName is not null &&
577+
typeExpression is not null && IsDisposable(typeExpression))
578+
{
579+
return memberIdentifierName.Identifier.Text;
580+
}
566581
}
567582

568583
return null;
569584
}
570585

586+
private static bool IsDisposable(ExpressionSyntax typeExpression)
587+
{
588+
IdentifierNameSyntax? typeIdentifierName = null;
589+
590+
if (typeExpression is QualifiedNameSyntax qualifiedNameSyntax &&
591+
qualifiedNameSyntax.Left is IdentifierNameSyntax systemName &&
592+
systemName.Identifier.Text is "System")
593+
{
594+
typeIdentifierName = qualifiedNameSyntax.Right as IdentifierNameSyntax;
595+
}
596+
else if (typeExpression is IdentifierNameSyntax identifierNameSyntax)
597+
{
598+
typeIdentifierName = identifierNameSyntax;
599+
}
600+
601+
if (typeIdentifierName is not null &&
602+
typeIdentifierName.Identifier.Text is "IDisposable" or "IAsyncDisposable")
603+
{
604+
return true;
605+
}
606+
607+
return false;
608+
}
609+
571610
private sealed class Parameters
572611
{
573612
private readonly INamedTypeSymbol type;

0 commit comments

Comments
 (0)