diff --git a/src/nunit.analyzers.tests/DiagnosticSuppressors/NonNullableFieldOrPropertyIsUninitializedSuppressorTests.cs b/src/nunit.analyzers.tests/DiagnosticSuppressors/NonNullableFieldOrPropertyIsUninitializedSuppressorTests.cs index 22703c88..f6c43527 100644 --- a/src/nunit.analyzers.tests/DiagnosticSuppressors/NonNullableFieldOrPropertyIsUninitializedSuppressorTests.cs +++ b/src/nunit.analyzers.tests/DiagnosticSuppressors/NonNullableFieldOrPropertyIsUninitializedSuppressorTests.cs @@ -246,6 +246,39 @@ private void SetFields() RoslynAssert.Suppressed(suppressor, testCode); } + [TestCase("async Task", "await ", "", "")] + [TestCase("async Task", "await ", "this.", "")] + [TestCase("async Task", "await ", "", ".ConfigureAwait(false)")] + [TestCase("async Task", "await ", "this.", ".ConfigureAwait(false)")] + [TestCase("void", "", "", ".Wait()")] + [TestCase("void", "", "this.", ".Wait()")] + public void FieldAssignedInAsyncCalledMethod(string returnType, string awaitKeyWord, string accessor, string suffix) + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings(@$" + private string ↓field; + + [SetUp] + public {returnType} Initialize() + {{ + {awaitKeyWord}{accessor}SetFields(){suffix}; + }} + + [Test] + public void Test() + {{ + Assert.That({accessor}field, Is.Not.Null); + }} + + private Task SetFields() + {{ + {accessor}field = string.Empty; + return Task.CompletedTask; + }} + "); + + RoslynAssert.Suppressed(suppressor, testCode); + } + [TestCase("SetUp", "")] [TestCase("OneTimeSetUp", "this.")] public void FieldAssignedInCalledMethods(string attribute, string prefix) diff --git a/src/nunit.analyzers.tests/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzerTests.cs b/src/nunit.analyzers.tests/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzerTests.cs index 78da5790..3ee6728b 100644 --- a/src/nunit.analyzers.tests/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzerTests.cs +++ b/src/nunit.analyzers.tests/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzerTests.cs @@ -225,6 +225,40 @@ public void TearDownMethod() RoslynAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); } + [Test] + public void FieldConditionallyAssignedInCalledLocalMethod() + { + var testCode = TestUtility.WrapMethodInClassNamespaceAndAddUsings($@" + private object? ↓field; + private bool initializeField; + + public TestClass(bool initializeField) => this.initializeField = initializeField; + + [SetUp] + public async Task SetUp() + {{ + if (initializeField) + await InitializeField().ConfigureAwait(false); + }} + + [TearDown] + public void TearDownMethod() + {{ + field = null; + }} + + private Task InitializeField() + {{ + field = new DummyDisposable(); + return Task.CompletedTask; + }} + + {DummyDisposable} + "); + + RoslynAssert.Diagnostics(analyzer, expectedDiagnostic, testCode); + } + [Test] public void AnalyzeWhenPropertyBackingFieldIsDisposed( [Values("field", "Property")] string fieldOrProperty) diff --git a/src/nunit.analyzers/DiagnosticSuppressors/NonNullableFieldOrPropertyIsUninitializedSuppressor.cs b/src/nunit.analyzers/DiagnosticSuppressors/NonNullableFieldOrPropertyIsUninitializedSuppressor.cs index 0c67bbae..9e82444b 100644 --- a/src/nunit.analyzers/DiagnosticSuppressors/NonNullableFieldOrPropertyIsUninitializedSuppressor.cs +++ b/src/nunit.analyzers/DiagnosticSuppressors/NonNullableFieldOrPropertyIsUninitializedSuppressor.cs @@ -202,6 +202,17 @@ private static bool IsAssignedIn( ExpressionSyntax? expressionStatement, string fieldOrPropertyName) { + if (expressionStatement is AwaitExpressionSyntax awaitExpression) + { + expressionStatement = awaitExpression.Expression; + if (expressionStatement is InvocationExpressionSyntax awaitInvocationExpression && + awaitInvocationExpression.Expression is MemberAccessExpressionSyntax awaitMemberAccessExpression && + awaitMemberAccessExpression.Name.Identifier.Text == "ConfigureAwait") + { + expressionStatement = awaitMemberAccessExpression.Expression; + } + } + if (expressionStatement is AssignmentExpressionSyntax assignmentExpression) { if (assignmentExpression.Left is TupleExpressionSyntax tupleExpression) @@ -224,6 +235,13 @@ private static bool IsAssignedIn( } else if (expressionStatement is InvocationExpressionSyntax invocationExpression) { + if (invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression && + memberAccessExpression.Expression is InvocationExpressionSyntax awaitedInvocationExpression && + memberAccessExpression.Name.Identifier.Text == "Wait") + { + invocationExpression = awaitedInvocationExpression; + } + string? identifier = GetIdentifier(invocationExpression.Expression); if (!string.IsNullOrEmpty(identifier) && diff --git a/src/nunit.analyzers/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzer.cs b/src/nunit.analyzers/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzer.cs index 59ffe7c0..d0bad819 100644 --- a/src/nunit.analyzers/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzer.cs +++ b/src/nunit.analyzers/DisposeFieldsAndPropertiesInTearDown/DisposeFieldsAndPropertiesInTearDownAnalyzer.cs @@ -227,6 +227,17 @@ private static void AssignedIn(Parameters parameters, HashSet assignment private static void AssignedIn(Parameters parameters, HashSet assignments, ExpressionSyntax expression) { + if (expression is AwaitExpressionSyntax awaitExpression) + { + expression = awaitExpression.Expression; + if (expression is InvocationExpressionSyntax awaitInvocationExpression && + awaitInvocationExpression.Expression is MemberAccessExpressionSyntax awaitMemberAccessExpression && + awaitMemberAccessExpression.Name.Identifier.Text == "ConfigureAwait") + { + expression = awaitMemberAccessExpression.Expression; + } + } + if (expression is AssignmentExpressionSyntax assignmentExpression) { // We only deal with simple assignments, not tuple or deconstruct @@ -241,6 +252,13 @@ private static void AssignedIn(Parameters parameters, HashSet assignment } else if (expression is InvocationExpressionSyntax invocationExpression) { + if (invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression && + memberAccessExpression.Expression is InvocationExpressionSyntax awaitedInvocationExpression && + memberAccessExpression.Name.Identifier.Text == "Wait") + { + invocationExpression = awaitedInvocationExpression; + } + string? method = GetIdentifier(invocationExpression.Expression); if (method is not null) {