diff --git a/src/nunit.analyzers.tests/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzerTests.cs b/src/nunit.analyzers.tests/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzerTests.cs index 3fc1d600..1be4a7d7 100644 --- a/src/nunit.analyzers.tests/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzerTests.cs @@ -63,8 +63,9 @@ public void TearDownMethod() RoslynAssert.Valid(analyzer, testCode); } - [Test] - public void AnalyzeWhenFieldIsConditionallyDisposed() + [TestCase("IDisposable")] + [TestCase("System.IDisposable")] + public void AnalyzeWhenFieldIsConditionallyDisposedUsingIsIDisposable(string interfaceName) { var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@" private object field = new DummyDisposable(); @@ -72,7 +73,7 @@ public void AnalyzeWhenFieldIsConditionallyDisposed() [OneTimeTearDown] public void TearDownMethod() {{ - if (field is IDisposable disposable) + if (field is {interfaceName} disposable) disposable.Dispose(); }} @@ -82,6 +83,25 @@ public void TearDownMethod() RoslynAssert.Valid(analyzer, testCode); } + [TestCase("IDisposable")] + [TestCase("System.IDisposable")] + public void AnalyzeWhenFieldIsConditionallyDisposedUsingAsIDisposable(string interfaceName) + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@" + private object field = new DummyDisposable(); + + [OneTimeTearDown] + public void TearDownMethod() + {{ + (field as {interfaceName})?.Dispose(); + }} + + {DummyDisposable} + "); + + RoslynAssert.Valid(analyzer, testCode); + } + [Test] public void AnalyzeWhenFieldWithInitializerIsDisposedInOneTimeTearDownMethod() { diff --git a/src/nunit.analyzers/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzer.cs b/src/nunit.analyzers/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzer.cs index 74f2ce2c..aec9123b 100644 --- a/src/nunit.analyzers/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzer.cs +++ b/src/nunit.analyzers/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzer.cs @@ -480,8 +480,7 @@ private static void DisposedIn(Parameters parameters, HashSet disposals, // disposable.Dispose(); if (ifStatement.Condition is IsPatternExpressionSyntax isPatternExpression && isPatternExpression.Pattern is DeclarationPatternSyntax declarationPattern && - declarationPattern.Type is IdentifierNameSyntax identifierName && - identifierName.Identifier.Text.EndsWith("Disposable", StringComparison.Ordinal) && + IsDisposable(declarationPattern.Type) && declarationPattern.Designation is SingleVariableDesignationSyntax singleVariableDesignation) { string? member = GetIdentifier(isPatternExpression.Expression); @@ -555,19 +554,59 @@ private static void DisposedIn(Parameters parameters, HashSet disposals, return memberAccessExpression.Name.Identifier.Text; } - // considering cast to IDisposable, e.g. in case of explicit interface implementation of IDisposable.Dispose() - else if (expression is ParenthesizedExpressionSyntax parenthesizedExpression && - parenthesizedExpression.Expression is CastExpressionSyntax castExpression && - castExpression.Expression is IdentifierNameSyntax castIdentifierName && - castExpression.Type is IdentifierNameSyntax typeIdentifierName && - typeIdentifierName.Identifier.Text.Equals("IDisposable", StringComparison.Ordinal)) + // considering cast to I(Async)Disposable, e.g. in case of explicit interface implementation of IDisposable.Dispose() + // or in case of 'as IDisposable' or 'as IAsyncDisposable' + else if (expression is ParenthesizedExpressionSyntax parenthesizedExpression) { - return castIdentifierName.Identifier.Text; + IdentifierNameSyntax? memberIdentifierName = null; + ExpressionSyntax? typeExpression = null; + + if (parenthesizedExpression.Expression is CastExpressionSyntax castExpression) + { + memberIdentifierName = castExpression.Expression as IdentifierNameSyntax; + typeExpression = castExpression.Type; + } + else if (parenthesizedExpression.Expression is BinaryExpressionSyntax binaryExpression && + binaryExpression.IsKind(SyntaxKind.AsExpression)) + { + memberIdentifierName = binaryExpression.Left as IdentifierNameSyntax; + typeExpression = binaryExpression.Right; + } + + if (memberIdentifierName is not null && + typeExpression is not null && IsDisposable(typeExpression)) + { + return memberIdentifierName.Identifier.Text; + } } return null; } + private static bool IsDisposable(ExpressionSyntax typeExpression) + { + IdentifierNameSyntax? typeIdentifierName = null; + + if (typeExpression is QualifiedNameSyntax qualifiedNameSyntax && + qualifiedNameSyntax.Left is IdentifierNameSyntax systemName && + systemName.Identifier.Text is "System") + { + typeIdentifierName = qualifiedNameSyntax.Right as IdentifierNameSyntax; + } + else if (typeExpression is IdentifierNameSyntax identifierNameSyntax) + { + typeIdentifierName = identifierNameSyntax; + } + + if (typeIdentifierName is not null && + typeIdentifierName.Identifier.Text is "IDisposable" or "IAsyncDisposable") + { + return true; + } + + return false; + } + private sealed class Parameters { private readonly INamedTypeSymbol type;