Skip to content

Comments

Improve error recovery around 'scoped' modifier parsing#81636

Merged
CyrusNajmabadi merged 32 commits intodotnet:mainfrom
CyrusNajmabadi:scopedParsing
Dec 30, 2025
Merged

Improve error recovery around 'scoped' modifier parsing#81636
CyrusNajmabadi merged 32 commits intodotnet:mainfrom
CyrusNajmabadi:scopedParsing

Conversation

@CyrusNajmabadi
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Dec 10, 2025

We now allow a misplacced 'scoped' keyword to be treated as a normal modifier, and move teh reporting of issues to the binding phase. Today we get entirely thrown off and enter very bad error recovery that often gets thrown entirely off, creating tons of cascading errors.

Note: the net effect of this is much fewer errors, and much better error recovery. The added length in this PR is the addition of new tests, or updating existing tests to report binding errors to demonstrate that errors are still produced semantically where they would have previously been syntactic.

@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner December 10, 2025 13:26
@CyrusNajmabadi CyrusNajmabadi marked this pull request as draft December 10, 2025 13:27
return true;

using var beforeScopedResetPoint = this.GetDisposableResetPoint(resetOnDispose: false);
using var beforeScopedResetPoint = this.GetDisposableResetPoint(resetOnDispose: true);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

no need to for dual reset points. and we can always trivially clean up. we just let the caller then eat the token if this function says it is a keyword.

Diagnostic(ErrorCode.ERR_IdentifierExpected, "readonly").WithLocation(3, 37),
// (3,37): error CS1003: Syntax error, ',' expected
// public static void M(ref scoped readonly int p) => throw null;
Diagnostic(ErrorCode.ERR_SyntaxError, "readonly").WithArguments(",").WithLocation(3, 37),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

lots of the PR will just be syntax errors going away, and better errors about misplaced 'scopes' modifiers.

Diagnostic(ErrorCode.ERR_RefReadOnlyWrongOrdering, "readonly").WithLocation(3, 26),
// (3,35): error CS0246: The type or namespace name 'scoped' could not be found (are you missing a using directive or an assembly reference?)
// public static void M(readonly scoped ref int p) => throw null;
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "scoped").WithArguments("scoped").WithLocation(3, 35),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

or removing errors where we considered it a type name, but now realie it should be treated as a modifier.

Diagnostic(ErrorCode.ERR_ScopedAfterInOutRefReadonly, "scoped").WithLocation(1, 29),
// (1,47): error CS8936: Feature 'ref fields' is not available in C# 10.0. Please use language version 11.0 or greater.
// void F(ref scoped int b, in scoped int c, out scoped int d) { }
Diagnostic(ErrorCode.ERR_ScopedAfterInOutRefReadonly, "scoped").WithLocation(1, 47)]);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the difference between these two is just the errors about a feature not being available. is tehre a helper to filter those out appropriately to make the test cleaner?

// (1,54): error CS1003: Syntax error, ',' expected
// void F(ref scoped int b, in scoped int c, out scoped int d) { }
Diagnostic(ErrorCode.ERR_SyntaxError, "int").WithArguments(",").WithLocation(1, 54)
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

in some tests all syntax diagnostics went away. as such, i added semantic diagnostics to show we still report problems with the scoped modifier.

M(SyntaxKind.CommaToken);
N(SyntaxKind.Parameter);
{
N(SyntaxKind.ScopedKeyword);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

these tests demonstrate better understanding, with 'scoped' properly being treated as a modifier.

Diagnostic(ErrorCode.ERR_SemicolonExpected, "").WithLocation(1, 26)]);

N(SyntaxKind.ParenthesizedExpression);
N(SyntaxKind.ParenthesizedLambdaExpression);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we didn't even realize this was a lambda before...

@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review December 10, 2025 14:43
@CyrusNajmabadi
Copy link
Contributor Author

@dotnet/roslyn-compiler this is ready for review.


private bool IsPossibleScopedKeyword(bool isFunctionPointerParameter)
{
using var _ = this.GetDisposableResetPoint(resetOnDispose: true);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

for those keeping count, this made for a triple nested reset point. We now just have one reset point.

// trivial case. scoped ref/out/in/this is definitely the scoped keyword. Note: the only actual legal
// cases are `scoped ref`, `scoped out`, and `scoped in`. But we detect and allow `scoped this`, `scoped
// params` and `scoped readonly` as well. These will be reported as errors later in binding.
if (IsParameterModifierExcludingScoped(this.CurrentToken))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we intentionally allow a broader set than before. so you can say things like scoped this. This is caught later in the binder.

return false;

// In C# 14 we decided that within a lambda 'scoped' would *always* be a keyword.
// In C# 14 we decided that within a lambda 'scoped' would *always* be a modifier, not a type.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

refining this comment. It wasn't 100% accurate. It's not always a keyword. But it is a modifier if it previous would have been allowed as a type.

@CyrusNajmabadi
Copy link
Contributor Author

@jcouv @jjonescz for another pair of eyes.

@CyrusNajmabadi
Copy link
Contributor Author

@jcouv ptal :-)

}
else if (seenIn || seenOut || seenRef || seenReadonly)
{
// Matches original parsing logic that disallowed parsing out 'scoped' once in/out/ref/readonly had been seen.
Copy link
Member

@jcouv jcouv Dec 29, 2025

Choose a reason for hiding this comment

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

Matches original parsing logic

nit: "original" is relative. "Disallow parsing ..." would be enough.
Same comment applies below (line 791) #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated both comments.

else if (i < n - 1)
{
// Matches original parsing logic that only allowed 'scoped' to be followed by ref/out/in to
// actually be considered the modifier.
Copy link
Member

@jcouv jcouv Dec 29, 2025

Choose a reason for hiding this comment

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

I didn't understand this comment. The logic below merely reports a diagnostic, how does it make it not considered a modifier? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

instead of treating it as a modifier (no error), it errors. meaning we don't accept this as a modifier in this order.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated comment to now say: Only allow 'scoped' followed by ref/out/in to actually be considered a valid modifier.

// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Linq;
Copy link
Member

Choose a reason for hiding this comment

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

nit: We have another open issue related to scoped ordering: #78556
Could you include a test for scoped in this int and this scoped in int to show if the PR affects those scenarios?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure.

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks (commit 30)

@CyrusNajmabadi CyrusNajmabadi enabled auto-merge (squash) December 29, 2025 23:21
@CyrusNajmabadi CyrusNajmabadi enabled auto-merge (squash) December 30, 2025 01:59
@CyrusNajmabadi CyrusNajmabadi merged commit 96c60b5 into dotnet:main Dec 30, 2025
24 of 25 checks passed
@CyrusNajmabadi CyrusNajmabadi deleted the scopedParsing branch December 30, 2025 17:57
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Dec 30, 2025
333fred added a commit to 333fred/roslyn that referenced this pull request Jan 5, 2026
* upstream/main: (1373 commits)
  Add docs
  Use new test plan for dictionary expressions (dotnet#81861)
  Update dartlab pipeline setup (dotnet#81807)
  [main] Update dependencies from dotnet/roslyn (dotnet#81649)
  Fix
  LSP, everything but handlers
  Reapply "Update methods to be `async`." (dotnet#81808)
  Properly await calls to ensure exception handling works for sync and async scenarios
  Let Razor fill in project information on diagnostics (dotnet#81822)
  only analyzers
  Reapply "Update methods to be `async`." (dotnet#81808)
  only editorfeatures
  Reapply "Update methods to be `async`." (dotnet#81808)
  Enable GetTypeInfo for type of object creation syntax (dotnet#81802)
  Improve error recovery around 'scoped' modifier parsing (dotnet#81636)
  Only VisualStudio
  Only features
  Only analyzers
  Only editor features
  Reapply "Update methods to be `async`." (dotnet#81808)
  ...
@davidwengier davidwengier modified the milestones: Next, 18.3 Jan 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants