Skip to content

Parser: allow using declarations at the top level of a function nested in a switch case clause - #423

Open
robobun wants to merge 1 commit into
mainfrom
farm/0221bac2/using-in-function-inside-switch-case
Open

robobun wants to merge 1 commit into
mainfrom
farm/0221bac2/using-in-function-inside-switch-case

Conversation

@robobun

@robobun robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator

Symptom

A using or await using declaration at the top level of a function that is lexically inside a case: / default: clause is rejected while the enclosing code is parsed, although it is not directly in the clause:

switch (0) { case 0: (() => { using a = resource; })(); }
// SyntaxError: 'using' declaration is not allowed directly in a switch case or default clause.

async function f() { switch (0) { case 0: async function g() { await using a = resource; } } }
// SyntaxError: 'await using' declaration is not allowed directly in a switch case or default clause.

Both are valid (the restriction in the proposal applies to the CaseClause's own StatementList only), and V8 accepts them. Every kind of nested body is affected: function declarations and expressions, arrows, methods, getters/setters, generators, async functions, class methods, the function in a class field initializer and a function in a parameter default. Wrapping the declaration in a block inside the function is the only workaround. Bun hit this with await using inside an async arrow in a switch case (the sync form is hit just the same once the initializer is not a constant).

Cause

m_insideSwitchCaseBody is set by parseSwitchClauses() / parseSwitchDefaultClause() for the statements of the clause and is only cleared again by parseBlockStatement(). parseFunctionBody() parses the body with parseSourceElements() directly, never through parseBlockStatement(), so the flag is still set when the nested body's statements are parsed and the checks in parseStatementListItem() fire. The body is only checked this way during the enclosing parse (a later lazy reparse of the function starts with a fresh parser), which is why the error appears at load time of the enclosing code.

Change

parseFunctionBody() clears the flag with a SetForScope for the duration of the body, next to the m_statementDepth reset that exists for the same reason (a function body is a fresh statement list, whatever it is nested in). The flag is restored afterwards, so case 0: function f() {} using x = r; is still rejected. Class static blocks already go through parseBlockStatement() and were fine. Parser.cpp is identical to upstream main here, so the same fix applies upstream.

JSTests/stress/using-declaration-in-function-inside-switch-case.js evaluates every kind of nested body containing using and await using inside case and default clauses (all must parse), keeps the declarations directly in a clause rejected (including right after a nested function has been parsed, and in a switch nested in such a function), and checks that the declarations in the nested functions actually dispose their resources.

Related, not fixed here

Two more parser flags leak into nested function bodies the same way, independently of this one (both reproduce identically on the current pin and on this PR's preview; V8 accepts all of them). They are tracked separately and would each get the same kind of reset:

  • m_parserState.allowAwait: a non-async function nested in an async function's parameter defaults, async function f(x = function () { var await = 1; }) {}, is rejected with "Cannot use 'await' as a variable name in an async function" (the override at parseFunctionInfo only covers the nested function's own parameters, not its body).
  • m_allowsIn: a block-bodied arrow in a for-loop init, for (var f = () => { return 'a' in {}; }; false;) {}, is rejected at the in (function expressions are fine because parseUnaryExpression re-enables it; arrows never go through that path).

Verification

  • The new test against the jsc shell of the current autobuild-caad865e prebuilt (unfixed): fails on the first body; taken apart, all 75 nested-body cases it checks are rejected with the message above and all 19 should-still-throw cases throw.
  • The same 150 accept / 19 reject expectations evaluated under node 26 (V8): 150 accepted, 19 rejected, so the test encodes the behaviour the other engine has as well.
  • With the fix (this PR's preview build autobuild-preview-pr-423-23e89c28): the new test passes on its jsc, the 41 existing using / await using / DisposableStack stress tests still pass, and Bump WebKit: allow using declarations in a function nested in a switch case clause bun#38285 (which pins the build) passes the same test as a jsc-stress fixture plus the user-facing shapes run by Bun; both fail on the current pin.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — targeted parser fix that mirrors the existing parseBlockStatement() pattern.

What was reviewed:

  • Confirmed m_insideSwitchCaseBody is only read at the two semanticFailIfTrue sites in parseStatementListItem(), so clearing it in parseFunctionBody() affects nothing else.
  • Verified SetForScope restores the flag so a using after the nested function in the same clause (and in subsequent clauses via the while loop in parseSwitchClauses) is still rejected — both covered by the new tests.
  • Checked the reset sits alongside the m_statementDepth = 0 reset for the same fresh-statement-list reason, and that the empty-body early return above it needs no change.
Extended reasoning...

Overview

This PR fixes a false-positive SyntaxError in the JSC parser: using / await using declarations at the top level of a function body were incorrectly rejected when that function was lexically nested inside a switch case/default clause. The Explicit Resource Management proposal only forbids using directly in a CaseClause's StatementList, not inside nested function bodies.

The production change is 3 lines in Parser.cpp:2381-2383: a SetForScope switchCaseScope(m_insideSwitchCaseBody, false) in parseFunctionBody(), placed immediately after the existing DepthManager / m_statementDepth = 0 reset that exists for the identical reason (a function body is a fresh statement list regardless of what it's nested in). This is the same RAII idiom already used at Parser.cpp:2073 in parseBlockStatement().

The other file is a new 148-line stress test covering 25 kinds of function bodies × 6 wrapper contexts (all must parse), 19 negative cases confirming the flag is correctly restored (direct-in-clause, after-nested-function, cross-clause, nested-switch-in-nested-function), and two runtime checks that disposal actually happens.

Security risks

None. This change only relaxes an over-restrictive syntax error to match spec/V8 behavior. It doesn't touch codegen, memory, JIT, or any security-sensitive path — it's a boolean flag reset in the parser's semantic-error checking.

Level of scrutiny

Low-to-medium. The mechanism is trivial (one RAII boolean reset matching an existing pattern 300 lines up), but parser correctness matters. I traced every read and write of m_insideSwitchCaseBody: it's written in parseSwitchClauses (1940), parseSwitchDefaultClause (1971), parseBlockStatement (2073), and now parseFunctionBody (2383); it's read only at the two semanticFailIfTrue checks (810, 845). The SetForScope in parseSwitchClauses at 1940 lives across the while (match(CASE)) loop, so restoration after a nested function body correctly leaves the flag true for the remainder of the current clause and subsequent clauses — the test case 0: function f() { } case 1: using x = null; exercises exactly this.

Other factors

  • The PR description is exceptionally detailed, including cross-verification of all 169 test expectations against V8 (node 26).
  • The fix is placed after the empty-body early return (match(CLOSEBRACE)), which is fine — an empty body has no statements to check.
  • Placing the reset before the ArrowFunctionBodyExpression branch is harmless (expression bodies can't contain declarations) and keeps it adjacent to the m_statementDepth reset.
  • The author notes Parser.cpp matches upstream WebKit here, so this is upstreamable.
  • No prior reviews or comments on the timeline; bug hunting system found nothing.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Limit details: You’ve used all 5 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 825a10a5-6f79-40cb-8ea1-015f27d677de

📥 Commits

Reviewing files that changed from the base of the PR and between 0cbb4a1 and c9731a2.

📒 Files selected for processing (2)
  • JSTests/stress/using-declaration-in-function-inside-switch-case.js
  • Source/JavaScriptCore/parser/Parser.cpp

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3699b4fd-0c41-4feb-bb52-a7cedbfde938

📥 Commits

Reviewing files that changed from the base of the PR and between caad865 and 23e89c28f7f09075deb3be2dc8da33e5a44075bd.

📒 Files selected for processing (2)
  • JSTests/stress/using-declaration-in-function-inside-switch-case.js
  • Source/JavaScriptCore/parser/Parser.cpp

Walkthrough

The parser now permits top-level using and await using declarations inside functions nested in switch clauses. Stress tests cover syntax validity, invalid direct declarations, and synchronous or asynchronous disposal order.

Changes

Nested resource declarations

Layer / File(s) Summary
Parser context and syntax coverage
Source/JavaScriptCore/parser/Parser.cpp, JSTests/stress/using-declaration-in-function-inside-switch-case.js
parseFunctionBody clears switch-clause context for nested functions. Tests cover valid function forms and invalid direct switch-clause declarations.
Resource disposal validation
JSTests/stress/using-declaration-in-function-inside-switch-case.js
Tests verify synchronous and asynchronous disposal order, promise completion, microtask draining, and error propagation.

Suggested reviewers: geoffreygaren, sosukesuzuki

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the bug, cause, change, tests, and scope, but it omits the required Bugzilla link, review line, and formatted file change list. Add the Bugzilla bug URL, the “Reviewed by NOBODY (OOPS!).” line, and the required formatted list of changed paths and affected functions or classes.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the parser change that allows using declarations in functions nested in switch clauses.

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

Preview Builds

Commit Release Date
c9731a24 autobuild-preview-pr-423-c9731a24 2026-08-16 11:21:15 UTC
23e89c28 autobuild-preview-pr-423-23e89c28 2026-08-13 22:25:41 UTC

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Verification with this PR's preview build (autobuild-preview-pr-423-23e89c28):

  • JSTests/stress/using-declaration-in-function-inside-switch-case.js passes on the preview's jsc; on the current pin's jsc (caad865e) it fails on its first shape with 'using' declaration is not allowed directly in a switch case or default clause.
  • The 41 existing using-declaration-*, await-using-*, *-disposable-* and for-using-* stress tests pass on the preview (the two syntax-error tests keep the direct-in-clause rejection covered).
  • Bun side: Bump WebKit: allow using declarations in a function nested in a switch case clause bun#38285 pins this build. It adds this test as a jsc-stress fixture plus the user-facing shapes as programs run by bun; both fail against the current pin and pass against the preview, and Bun's other using suites pass on it as well.

Once this merges, the Bun PR moves its pin to the merge commit.

…d in a switch case clause

m_insideSwitchCaseBody is set while the statements of a case/default
clause are parsed and only cleared again by parseBlockStatement(). A
function body is parsed through parseFunctionBody(), which goes straight
to parseSourceElements(), so a `using` or `await using` declaration at
the top level of a function, arrow, method, getter, setter, generator or
class field initializer function that is lexically inside a case clause
was rejected with "'using' declaration is not allowed directly in a
switch case or default clause" while the enclosing code was parsed.

Clear the flag for the duration of parseFunctionBody(), next to the
m_statementDepth reset that exists for the same reason. The flag is
restored when the body has been parsed, so a declaration directly in
the clause after the nested function is still rejected.
@robobun
robobun force-pushed the farm/0221bac2/using-in-function-inside-switch-case branch from 23e89c2 to c9731a2 Compare August 16, 2026 10:43

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — a one-line SetForScope reset that mirrors the existing pattern in parseBlockStatement().

What was reviewed:

  • Confirmed m_insideSwitchCaseBody is only read at the two semanticFailIfTrue sites in parseStatementListItem(), and only otherwise reset in parseBlockStatement() — so scoping the reset to parseFunctionBody() is the right place with no side effects.
  • Checked that SetForScope restores the flag on exit, and the tests cover the after-function-in-clause and nested-switch-in-nested-function cases that would break if it didn't.
  • Verified the reset sits alongside the m_statementDepth = 0 reset for the same reason (fresh statement list); the arrow-expression-body branch is unaffected since it can't contain a statement.
Extended reasoning...

Overview

This PR fixes a false-positive syntax error in the JSC parser: a using / await using declaration at the top level of a function body was being rejected when that function was lexically nested inside a switch case: / default: clause. The fix is a single line in Source/JavaScriptCore/parser/Parser.cpp (parseFunctionBody()): a SetForScope switchCaseScope(m_insideSwitchCaseBody, false); next to the existing m_statementDepth = 0 reset. A new 148-line stress test in JSTests/stress/ covers 25 function-body forms × 6 wrapper contexts (150 accept cases), 19 must-still-reject cases, and two runtime disposal-order checks.

Security risks

None. This is a parse-time acceptance change: previously-rejected valid code is now accepted, matching the Explicit Resource Management spec (the CaseClause restriction applies only to the clause's own StatementList) and V8's behavior. It doesn't touch codegen, memory, or any privileged path — the flag it resets is read exclusively at two semanticFailIfTrue guard sites and nowhere else.

Level of scrutiny

Low-to-moderate. Parser.cpp is a critical file, but this change is one effective line using an established RAII idiom (SetForScope) applied to the same flag in exactly the way parseBlockStatement() already does at line ~2073. There is no new logic, no new state, no control-flow change. The placement is correct: after the empty-body early return (irrelevant — no statements) and before both the block-body and arrow-expression-body parse paths. The RAII restore guarantees the flag is back for statements that follow the function in the enclosing clause, and the test suite explicitly verifies that (e.g. case 0: function f() { using y = null; } using x = null; still throws).

Other factors

  • Grep of m_insideSwitchCaseBody confirms it is set in parseSwitchClauses() / parseSwitchDefaultClause(), cleared in parseBlockStatement(), and now cleared in parseFunctionBody() — those are the only writers; the only readers are the two using/await using semantic checks. The blast radius is fully understood.
  • Verification is thorough: the PR description and follow-up comment record that the new test fails on the current pin, passes on the preview build, the 41 existing using/await using/DisposableStack stress tests still pass, and the 150/19 expectations were cross-checked against V8 (node 26). A companion Bun PR pins the preview build and passes.
  • The test file is comprehensive and well-structured — it covers restoration after every function form, nested switches inside nested functions, and actual runtime dispose ordering, not just parse acceptance.
  • No outstanding reviewer requests; CodeRabbit had no findings; no prior claude[bot] review on this PR.
  • The inherited upstream CODEOWNERS lists @WebKit/jsc-reviewers for this path, but that file is explicitly for auto-assigning reviewers ("Contributors do not 'own' WebKit components"), and this fork's recent JSC/WTF merges (#432, #428, #424) follow the same flow.

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.

1 participant