RavenDB-26506 - allow variable argument in ConfigureAwait#7
Conversation
There was a problem hiding this comment.
Pull request overview
Updates the ConfigureAwait analyzer to accept non-literal (variable/field/expression) arguments and to be more resilient during live-editing/incomplete syntax, reducing unnecessary diagnostics and IDE analyzer noise.
Changes:
- Loosen
ConfigureAwait(...)validation to only flag explicitConfigureAwait(true). - Replace
.Single()with a safer first-argument lookup to avoid exceptions on incomplete argument lists. - Add a unit test covering variable/field argument usage.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| Raven.CodeAnalysis/ConfigureAwait/ConfigureAwaitAnalyzer.cs | Adjusts argument inspection logic for ConfigureAwait and attempts to improve robustness for incomplete code. |
| Raven.CodeAnalysis.Test/ConfigureAwaitTests.cs | Adds a new test ensuring variable/field arguments do not trigger the diagnostic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (argument != null) | ||
| { | ||
| var configureAwaitIsExplicitTrue = argument.IsKind(SyntaxKind.TrueLiteralExpression); | ||
| if (configureAwaitIsExplicitTrue == false) | ||
| return; | ||
| } |
| // safely get the first argument, or null if the user hasn't typed it yet | ||
| var argument = invocationExpressionSyntax.ArgumentList.Arguments.FirstOrDefault()?.Expression; | ||
| if (argument != null) | ||
| { | ||
| var configureAwaitIsExplicitTrue = argument.IsKind(SyntaxKind.TrueLiteralExpression); | ||
| if (configureAwaitIsExplicitTrue == false) | ||
| return; | ||
| } | ||
| } |
| [TestMethod] | ||
| public void TestMethod6_VariableArgument() | ||
| { | ||
| const string input = @" | ||
| class C | ||
| { | ||
| private Task SthAsync() { return null; } | ||
| private bool _continueOnCapturedContext; | ||
|
|
||
| async Task M() | ||
| { | ||
| await SthAsync().ConfigureAwait(_continueOnCapturedContext); | ||
| } | ||
| }"; | ||
|
|
||
| VerifyCSharpDiagnostic(input); | ||
| } | ||
|
|
||
|
|
||
| protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() |
ppekrol
left a comment
There was a problem hiding this comment.
Small, well-scoped change with sound intent — allowing non-literal ConfigureAwait arguments matches the ticket, and swapping .Single() for .FirstOrDefault() avoids a crash on incomplete syntax during typing. A few things to address before merge (inline). Highlights:
- Indentation: all new lines use spaces; both files use tabs everywhere else. Most visible issue and likely an editor-config slip.
- Style:
if (configureAwaitIsExplicitTrue == false)→if (!...). The intermediate variable can be inlined. - Description vs. behaviour: the PR says incomplete-code (empty argument list) silently returns, but the code falls through to
ReportDiagnostic. Either add an earlyreturnwhenargument == nullor update the description. - Tests: robustness fix (
FirstOrDefault) is untested — a test for zero-arg / incompleteConfigureAwait(...)would lock it in.
Nothing structural; happy to approve once the above are addressed.
| // safely get the first argument, or null if the user hasn't typed it yet | ||
| var argument = invocationExpressionSyntax.ArgumentList.Arguments.FirstOrDefault()?.Expression; | ||
| if (argument != null) | ||
| { | ||
| var configureAwaitIsExplicitTrue = argument.IsKind(SyntaxKind.TrueLiteralExpression); | ||
| if (configureAwaitIsExplicitTrue == false) | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
Indentation: these 9 lines use spaces (4/8/12), but the rest of the file uses tabs (you can see it in cat -A: surrounding lines start with ^I^I^I, new lines start with spaces). Please re-indent to tabs so the file stays consistent — otherwise mixed whitespace will bite the next person who runs a formatter.
| } | ||
| // safely get the first argument, or null if the user hasn't typed it yet | ||
| var argument = invocationExpressionSyntax.ArgumentList.Arguments.FirstOrDefault()?.Expression; | ||
| if (argument != null) |
There was a problem hiding this comment.
Behaviour doesn't match the PR description. The description says: "if no argument exists yet, the analyzer silently returns without reporting a diagnostic."
But when Arguments is empty (incomplete typing), argument is null, this if is skipped, and control falls through to ReportDiagnostic at line 39 — so the diagnostic still fires on top of the compiler's red squiggle.
If the intent is genuinely to suppress on incomplete code, add an early return:
if (argument == null)
return; // incomplete code — let the compiler handle itIf the intent is just to stop crashing, update the description so it doesn't claim suppression that isn't there.
| var configureAwaitIsExplicitTrue = argument.IsKind(SyntaxKind.TrueLiteralExpression); | ||
| if (configureAwaitIsExplicitTrue == false) | ||
| return; |
There was a problem hiding this comment.
== false is non-idiomatic in C# and the temp variable is used exactly once. Suggest collapsing to:
if (!argument.IsKind(SyntaxKind.TrueLiteralExpression))
return;Matches the terseness of the surrounding analyzer code.
| if (configureAwaitIsFalse) | ||
| return; | ||
| } | ||
| // safely get the first argument, or null if the user hasn't typed it yet |
There was a problem hiding this comment.
Nit: "safely" is filler — the value of this comment is the why (incomplete syntax during typing would have made .Single() throw). Something like // argument list may be empty during typing — Single() would throw is closer to the load-bearing context. Optional.
| [TestMethod] | ||
| public void TestMethod6_VariableArgument() | ||
| { | ||
| const string input = @" | ||
| class C | ||
| { | ||
| private Task SthAsync() { return null; } | ||
| private bool _continueOnCapturedContext; | ||
|
|
||
| async Task M() | ||
| { | ||
| await SthAsync().ConfigureAwait(_continueOnCapturedContext); | ||
| } | ||
| }"; | ||
|
|
||
| VerifyCSharpDiagnostic(input); | ||
| } |
There was a problem hiding this comment.
Indentation: the new test method body uses 8-space indent (and 4-space for the attribute), but TestMethod1–TestMethod5 above all use tabs. Please re-indent to match the rest of the file.
|
|
||
|
|
There was a problem hiding this comment.
Two blank lines between the new test and the protected override below — drop one to match the single-blank-line spacing used elsewhere in the file.
|
|
||
| protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() | ||
| [TestMethod] | ||
| public void TestMethod6_VariableArgument() |
There was a problem hiding this comment.
Test coverage gap: the PR description enumerates field / variable / property / expression as newly-allowed arguments, but only the field case is exercised. The analyzer treats them identically, so one test is technically sufficient — however, the robustness fix (.FirstOrDefault() replacing .Single()) has no test at all. A case like await x.ConfigureAwait() (zero args) or an incomplete invocation would lock in the no-crash guarantee and prevent a future revert to .Single() from silently regressing. Worth adding.
| if (awaitedExpression?.Name.Identifier.Text == "ConfigureAwait") | ||
| { | ||
| var configureAwaitIsFalse = invocationExpressionSyntax.ArgumentList.Arguments.Single().Expression.IsKind(SyntaxKind.FalseLiteralExpression); | ||
| if (configureAwaitIsFalse) | ||
| // argument list may be empty during typing — FirstOrDefault avoids a throw from Single() | ||
| var argument = invocationExpressionSyntax.ArgumentList.Arguments.FirstOrDefault()?.Expression; | ||
| if (argument == null || !argument.IsKind(SyntaxKind.TrueLiteralExpression)) | ||
| return; |
| var awaitExpressionSyntax = (AwaitExpressionSyntax)context.Node; | ||
| var invocationExpressionSyntax = awaitExpressionSyntax.Expression as InvocationExpressionSyntax; | ||
| var awaitedExpression = invocationExpressionSyntax?.Expression as MemberAccessExpressionSyntax; | ||
|
|
||
| if (awaitedExpression?.Name.Identifier.Text == "ConfigureAwait") | ||
| { |
Previously the analyzer only accepted
ConfigureAwait(false)— any other argument, including a field or variable, would trigger RDB0002. This forced callers with intentionally dynamic behavior to suppress the warning via#pragma warning disable.Change
Flip the check from "argument must be
false" to "argument must not be explicittrue". Any non-literal argument (field, variable, property, expression) is now trusted and passes without a warning.Allowed after this change
.ConfigureAwait(false)✓.ConfigureAwait(continueOnCapturedContext: false)✓.ConfigureAwait(_continueOnCapturedContext)✓ ← newRobustness
Roslyn analyzers run continuously on incomplete syntax trees while the developer is typing. If a developer pauses mid-expression after typing
await myTask.ConfigureAwait(, the argument list is temporarily empty.The original
.Single()call would throw anInvalidOperationExceptionon an empty collection. Roslyn catches analyzer crashes gracefully, but it creates unnecessary noise in the IDE error logs.Changed to
.FirstOrDefault()with a null guard — if no argument exists yet, the analyzer silently returns without reporting a diagnostic. The compiler already shows a red squiggle for incomplete code, so there isno need for the analyzer to add further noise.
Still warned
.ConfigureAwait(true)✗.ConfigureAwait(...)entirely ✗Test
Added
TestMethod6_VariableArgumentcovering the field-variable case.