diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseConfigureAwaitFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseConfigureAwaitFixer.cs index afd45be14..0922708fc 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseConfigureAwaitFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseConfigureAwaitFixer.cs @@ -1,3 +1,5 @@ +using Microsoft.CodeAnalysis.Formatting; + namespace Meziantou.Analyzer.Rules; [ExportCodeFixProvider(LanguageNames.CSharp), Shared] @@ -123,7 +125,8 @@ private static async Task AddConfigureAwait(CodeFixContext context, Sy { var newBlock = SyntaxFactory.Block(variablesStatement, newUsingBlock.WithoutLeadingTrivia()) .WithLeadingTrivia(usingBlock.GetLeadingTrivia()) - .WithTrailingTrivia(usingBlock.GetTrailingTrivia()); + .WithTrailingTrivia(usingBlock.GetTrailingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation); editor.ReplaceNode(usingBlock, newBlock); } @@ -205,21 +208,16 @@ private static async Task AddConfigureAwait(CodeFixContext context, Sy bool TryInsertVariableStatementBeforeUsing(LocalDeclarationStatementSyntax variableStatement, UsingStatementSyntax usingStatement) { - var insertionTarget = usingStatement; - while (insertionTarget.Parent is UsingStatementSyntax parentUsing && - parentUsing.Statement == insertionTarget && - parentUsing.Declaration is null) - { - insertionTarget = parentUsing; - } - - if (insertionTarget.Parent is BlockSyntax or SwitchSectionSyntax) + // The declaration can only be extracted where a statement can be inserted just before the using statement. + // Moving it before an enclosing statement would evaluate the initializer before that statement, + // so the caller wraps the declaration and the using statement in a block instead. + if (usingStatement.Parent is BlockSyntax or SwitchSectionSyntax) { - editor.InsertBefore(insertionTarget, variableStatement); + editor.InsertBefore(usingStatement, variableStatement); return true; } - if (insertionTarget.Parent is GlobalStatementSyntax { Parent: CompilationUnitSyntax } globalStatement) + if (usingStatement.Parent is GlobalStatementSyntax { Parent: CompilationUnitSyntax } globalStatement) { editor.InsertBefore(globalStatement, SyntaxFactory.GlobalStatement(variableStatement)); return true; diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseConfigureAwaitAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseConfigureAwaitAnalyzerTests.cs index 757d0e866..c7a31e04e 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseConfigureAwaitAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseConfigureAwaitAnalyzerTests.cs @@ -790,11 +790,13 @@ class ClassTest async Task Test() { Stream stream = OpenWrite(); - var streamWriter = new StreamWriter(stream); await using (stream.ConfigureAwait(false)) - await using (streamWriter.ConfigureAwait(false)) { - await streamWriter.WriteAsync("test-data").ConfigureAwait(false); + var streamWriter = new StreamWriter(stream); + await using (streamWriter.ConfigureAwait(false)) + { + await streamWriter.WriteAsync("test-data").ConfigureAwait(false); + } } } @@ -805,6 +807,59 @@ async Task Test() return test.RunAsync(); } + [Fact] + public Task AwaitUsing_UnderUsingWithSideEffect_KeepsEvaluationOrder() + { + var test = CreateTest(); + test.TestCode = """ + using System; + using System.Threading.Tasks; + class ClassTest + { + async Task Test() + { + using (Create("outer")) + await using (var {|MA0004:resource = Create("inner")|}) + { + } + } + + static Resource Create(string text) => throw null; + } + class Resource : IDisposable, IAsyncDisposable + { + public void Dispose() => throw null; + public ValueTask DisposeAsync() => throw null; + } + """; + test.FixedCode = """ + using System; + using System.Threading.Tasks; + class ClassTest + { + async Task Test() + { + using (Create("outer")) + { + var resource = Create("inner"); + await using (resource.ConfigureAwait(false)) + { + } + } + } + + static Resource Create(string text) => throw null; + } + class Resource : IDisposable, IAsyncDisposable + { + public void Dispose() => throw null; + public ValueTask DisposeAsync() => throw null; + } + """; + + return test.RunAsync(); + } + [Fact] public Task AwaitUsingVar_InsideConfiguredUsing_ShouldNotThrow() {