Skip to content

RavenDB-26506 - allow variable argument in ConfigureAwait#7

Merged
ppekrol merged 4 commits into
ravendb:masterfrom
grisha-kotler:RavenDB-26506
May 19, 2026
Merged

RavenDB-26506 - allow variable argument in ConfigureAwait#7
ppekrol merged 4 commits into
ravendb:masterfrom
grisha-kotler:RavenDB-26506

Conversation

@grisha-kotler
Copy link
Copy Markdown
Member

@grisha-kotler grisha-kotler commented May 16, 2026

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 explicit true". 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) ✓ ← new

Robustness

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 an InvalidOperationException on 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 is
no need for the analyzer to add further noise.

Still warned

  • .ConfigureAwait(true)
  • missing .ConfigureAwait(...) entirely ✗

Test

Added TestMethod6_VariableArgument covering the field-variable case.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 explicit ConfigureAwait(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.

Comment on lines +31 to +36
if (argument != null)
{
var configureAwaitIsExplicitTrue = argument.IsKind(SyntaxKind.TrueLiteralExpression);
if (configureAwaitIsExplicitTrue == false)
return;
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +29 to +37
// 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;
}
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +119 to +138
[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()
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Member

@ppekrol ppekrol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 early return when argument == null or update the description.
  • Tests: robustness fix (FirstOrDefault) is untested — a test for zero-arg / incomplete ConfigureAwait(...) would lock it in.

Nothing structural; happy to approve once the above are addressed.

Comment on lines +29 to +37
// 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;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
// 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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 it

If the intent is just to stop crashing, update the description so it doesn't claim suppression that isn't there.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +33 to +35
var configureAwaitIsExplicitTrue = argument.IsKind(SyntaxKind.TrueLiteralExpression);
if (configureAwaitIsExplicitTrue == false)
return;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (configureAwaitIsFalse)
return;
}
// safely get the first argument, or null if the user hasn't typed it yet
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +119 to +135
[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);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation: the new test method body uses 8-space indent (and 4-space for the attribute), but TestMethod1TestMethod5 above all use tabs. Please re-indent to match the rest of the file.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +136 to +137


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
[TestMethod]
public void TestMethod6_VariableArgument()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Member

@ppekrol ppekrol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember to bump version: d21cfb5

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment on lines 27 to 32
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;
Comment on lines 23 to 28
var awaitExpressionSyntax = (AwaitExpressionSyntax)context.Node;
var invocationExpressionSyntax = awaitExpressionSyntax.Expression as InvocationExpressionSyntax;
var awaitedExpression = invocationExpressionSyntax?.Expression as MemberAccessExpressionSyntax;

if (awaitedExpression?.Name.Identifier.Text == "ConfigureAwait")
{
@ppekrol ppekrol merged commit e9265f5 into ravendb:master May 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants