Skip to content

[JSC] Parser: accept arguments and super() in the parameters of a function in a class field initializer - #711

Open
robobun wants to merge 2 commits into
mainfrom
robobun/049928b5/arguments-in-function-parameters-in-class-field
Open

robobun wants to merge 2 commits into
mainfrom
robobun/049928b5/arguments-in-function-parameters-in-class-field

Conversation

@robobun

@robobun robobun commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • JavaScriptCore rejects valid code. class A { f = function (p = arguments.length) { return p; }; } throws SyntaxError: Unexpected identifier 'arguments'. Cannot reference 'arguments' in class field initializer. Node accepts it.
  • Methods, setters and constructors in the initializer fail the same way. super() in the parameters of a derived class constructor there fails: super call is not valid in class field initializer context.
  • Cause: parseClass() sets m_parserState.isParsingClassFieldInitializer for the initializer (parser/Parser.cpp:3370). Only parseFunctionBody() cleared it (Parser.cpp:2367), so the parameters still had it.

Fix

Background

  • ContainsArguments is the spec operation behind "no arguments in a class field initializer". It looks into an arrow function and stops at any other function, which has its own arguments.
  • JavaScriptCore parses a field initializer in place with this flag set. The checks for arguments, super() and yield read it.
  • parseFunctionInfo() parses the name, the parameters and the body of every function.
Notes

Repro (jsc, or bun with plain eval). node 26 accepts all but the last one. jsc of 63a807e88c accepts only the first and the sixth, with and without --useSourceProviderCache=false:

const cases = [
    "(class { f = function () { return arguments.length; } })",
    "(class { f = function (p = arguments.length) { return p; } })",
    "(class { f = function (p = () => arguments.length) { return p; } })",
    "(class { f = { m(p = arguments.length) { return p; } } })",
    "(class { f = new (class { constructor(p = arguments.length) { this.p = p; } })() })",
    "(class { static { (function (p = arguments.length) { return p; })(); } })",
    "(class { f = class extends Object { constructor(p = super()) { } } })",
    "(class { f = (p = arguments.length) => p })", // must stay a SyntaxError
];
for (const c of cases) {
    try { (0, eval)(c); print("accepted", c); } catch (e) { print(String(e), c); }
}

Why one place. The flag was cleared in parseFunctionBody(), which sees only the body. The rule is about the whole function, so the clear moves to parseFunctionInfo(), which parses the parameters and the body. A computed key and a class heritage are parsed before parseFunctionInfo(), so they stay part of the initializer, as ContainsArguments says (f = { [arguments]() { } } is still an error). For an arrow function the new condition is the old one (bodyType != StandardFunctionBodyBlock is true exactly for the two arrow function modes). The SourceProviderCache is not involved: the flag at a source position does not depend on which pass parses it, and --useSourceProviderCache=false gives the same results.

Messages that change for code that is still an error. super() in the parameters of a function that cannot call it, in a field initializer: super is not valid in this context. The old message was Unexpected token '('. super call is not valid in class field initializer context. The new one is what the same function gets outside a class field, and in its body. Example: (class extends Object { f = { m(p = super()) { } } }). No test in JSTests or LayoutTests/js has the old message.

yield, new.target, direct eval. No result changes. yield in these parameters fails on an earlier check (Cannot use yield expression within parameters, or out of generator). new.target is valid in these parameters through currentScope()->isFunction(). Code from a direct eval in these parameters is checked through closestScopeOwningArguments(), which already stops at the function. The test has all three.

Generated corpora. Each program goes to an indirect eval in jsc of 63a807e88c, in jsc with this change, and in node 26. A program is a class field context with an initializer made from 54 expression templates with a hole (functions, generators, async functions, arrow functions, methods, getters, setters, constructors, class heritage, computed keys, fields, static blocks, patterns) and 16 leaves (arguments, arguments.length, { arguments }, super(), super.x, new.target, yield, await, this, eval, ...).

Tests run. The local build is Release with assertions. The new test passes in all 17 modes of run-javascriptcore-tests and fails to parse with the old jsc. It also passes with #704 applied, and node accepts and rejects the same programs. run-javascriptcore-tests --filter 'class|arguments|super|field|static-block|arrow|syntax|parser|eval|new-target|newtarget|default-param|destructur|reparse|source-provider|method|getter|setter|constructor': 15,613 runs of 1,563 tests, no failure. test262: every test under test/language and test/annexB/language in the strict, default, module and raw scenarios, compared by exit code and output between the two builds: 45,388 runs, one test differs, grammar-private-environment-on-class-heritage-chained-usage.js. Its message names one of three undeclared private names, and which one changes from run to run in both builds.

Self-review. Two reviewers asked for the same thing: the first version of the test did not check the flag after the function. A parser that clears the flag and never restores it passed that test, and so did one that does not restore it when the function comes from the SourceProviderCache. The second commit adds [function (p = arguments) { }, arguments], (a = function (p = arguments) { }, b = arguments) => a, [class extends Object { constructor(p = super()) { } }, super()] and ten more, and both of those parsers fail it. The two concerns that I rejected are about programs that have a second, separate error that jsc does not see, with and without this change: the shorthand { arguments } (#704), static f = await, and [super.x] below. The old parser rejected them only because of the function next to it. With p = 1 in place of p = arguments, the old parser accepts every one of them.

Found during this work, not fixed here.

  • function f(p) { for (arguments.length of [5]); return arguments.length; } throws ReferenceError: arguments is not defined when it is called. node returns 5. The arguments.length fast path of the parser does not count the head of a for-of or for-in as a write. No class is needed.
  • (async function () { class C { f = (p = await) => p; } }) is a SyntaxError (Cannot use 'await' within a parameter default expression.). node accepts it. Both accept f = await there, where await is an identifier.
  • (function () { class C { [super.x]() { } } }) is accepted. node rejects it: the function has no home object.
  • (class { f = new.target; }) in global code throws ReferenceError: Can't find private variable: PrivateSymbol.newTargetLocal when the class is evaluated. [JSC] new.target in a class field initializer or a class static block throws a ReferenceError when the class is in an arrow function #647 changes the code for new.target in a field initializer. I did not test this program with it.

With other open PRs. This change applies on top of #704 without a conflict, and both tests pass with the two together. #430 adds a line next to the one this change removes from parseFunctionBody(), so the second one to land needs a rebase.

… function in a class field initializer

A class field initializer has two early errors: "ContainsArguments of Initializer is true" and "Initializer Contains SuperCall is true". Both rules look into an arrow function and stop at any other function, because such a function has its own `arguments` and its own constructor kind. The parser keeps m_parserState.isParsingClassFieldInitializer for these checks. Only parseFunctionBody() cleared the flag, so it was still set while parseFunctionInfo() parsed the parameters. `class A { f = function (p = arguments.length) { return p; }; }` was a SyntaxError ("Cannot reference 'arguments' in class field initializer"). So was `super()` in the parameters of the constructor of a derived class in the initializer ("super call is not valid in class field initializer context").

parseFunctionInfo() now clears the flag for the parameters and the body of every function that is not an arrow function. An arrow function keeps the flag, as before.
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Preview build of 0b6d919: autobuild-preview-pr-711-0b6d9196

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 80d3cf15-9be0-4ae5-b1f8-f88da5f2d46b

📥 Commits

Reviewing files that changed from the base of the PR and between 57818ea and 0b6d919.

📒 Files selected for processing (1)
  • JSTests/stress/class-field-initializer-nested-function-parameters.js

Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review.


Walkthrough

The parser preserves class-field initializer state only for nested arrow functions. A stress test covers valid parsing, expected syntax errors, parameter defaults, and repeated construction across base and derived classes.

Changes

Class-field initializer parsing

Layer / File(s) Summary
Parser state handling
Source/JavaScriptCore/parser/Parser.cpp
parseFunctionBody no longer overrides class-field initializer state. parseFunctionInfo preserves the state for nested arrow functions and clears it for other function types.
Syntax regression coverage
JSTests/stress/class-field-initializer-nested-function-parameters.js
Adds assertion helpers and valid and invalid parser cases for arguments, super(), yield, and await, including exact SyntaxError checks.
Runtime regression coverage
JSTests/stress/class-field-initializer-nested-function-parameters.js
Checks parameter defaults across fields, methods, generators, async functions, private fields, eval, new.target, nested classes, and repeated base and derived construction.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 0b6d9

The parser accepts the intended nested-function parameter forms while preserving class-field restrictions, with regression coverage described for valid and invalid cases.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides detailed problem, cause, fix, testing, and compatibility information. It does not include the required Bugzilla bug title and link, the Reviewed by NOBODY line, or the templat… Add the associated Bugzilla URL and bug title, include the required review-status line, and list the changed paths with relevant functions or classes using the repository template format.
✅ 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 identifies the parser change to accept arguments and super() in parameters of functions nested in class-field initializers.
Full details: Description check

Explanation

The description provides detailed problem, cause, fix, testing, and compatibility information. It does not include the required Bugzilla bug title and link, the Reviewed by NOBODY line, or the template-style changed-file entries.

  • Fix all pre-merge checks with AI

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


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

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread Source/JavaScriptCore/parser/Parser.cpp
robobun added a commit to oven-sh/bun that referenced this pull request Sep 21, 2026
WEBKIT_VERSION is autobuild-preview-pr-711-57818ea1: the current pin (63a807e88c) plus the one commit of oven-sh/WebKit#711. The test comment names that PR. Before this merges, the tag must become the merge commit of oven-sh/WebKit#711.
After a function with `arguments` or `super()` in its parameters, the rest of the initializer has the checks of the initializer again: `[function (p = arguments) { }, arguments]` is still a SyntaxError, and `new.target` in an arrow function after the function is still valid in global code.

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

Code review found no issues

No high-confidence issues detected in this change.

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