From 57818ea177105156692d5f1715d516c8430f816a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:02:03 +0000 Subject: [PATCH 1/2] [JSC] Parser: accept `arguments` and `super()` in the parameters of a 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. --- ...-initializer-nested-function-parameters.js | 283 ++++++++++++++++++ Source/JavaScriptCore/parser/Parser.cpp | 4 +- 2 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 JSTests/stress/class-field-initializer-nested-function-parameters.js diff --git a/JSTests/stress/class-field-initializer-nested-function-parameters.js b/JSTests/stress/class-field-initializer-nested-function-parameters.js new file mode 100644 index 0000000000000..dfa0c3ef20dbb --- /dev/null +++ b/JSTests/stress/class-field-initializer-nested-function-parameters.js @@ -0,0 +1,283 @@ +// FieldDefinition: it is a Syntax Error if ContainsArguments of Initializer is true, or if Initializer Contains SuperCall. +// Both rules look into an arrow function and stop at any other function. The parser used to stop only at the body of +// such a function, so `arguments` and `super()` in its parameters were a SyntaxError. + +function shouldBe(actual, expected) { + if (actual !== expected) + throw new Error(`bad value: ${String(actual)}, expected: ${String(expected)}`); +} + +function shouldThrowSyntaxError(script, message) { + let error; + try { + (0, eval)(script); + } catch (e) { + error = e; + } + if (!(error instanceof SyntaxError)) + throw new Error(`Expected SyntaxError for: ${script}` + (error ? `, but got ${error}` : ", but nothing was thrown")); + if (error.message !== message) + throw new Error(`Expected "${message}" for: ${script}, but got "${error.message}"`); +} + +function shouldNotThrowSyntaxError(script) { + try { + (0, eval)(script); + } catch (e) { + if (e instanceof SyntaxError) + throw new Error(`Unexpected SyntaxError for: ${script}: ${e.message}`); + throw e; + } +} + +// [text before the expression in the field initializer, text after it] +const fields = [ + ["(class { f = (", "); })"], + ["(class { static f = (", "); })"], + ["(class { #f = (", "); })"], + ["(class { static #f = (", "); })"], + ["(class { ['f'] = (", "); })"], + ["(class extends Object { f = (", "); })"], + // A class field initializer in another one. + ["(class { g = class { f = (", "); }; })"], + // The expression is in an arrow function in the initializer. + ["(class { f = () => (", "); })"], + ["(class { f = (a = (", ")) => a; })"], + ["(class { f = async () => { await (", "); }; })"], +]; +// Only for valid code: `arguments` in an initializer in a class static block gets the error of the static block. +const fieldInStaticBlock = ["(class { static { class C { f = (", "); } } })"]; + +// `arguments` in the parameters of a function that is not an arrow function. +const functionsWithOwnArguments = [ + "function (p = arguments) { }", + "function (p = arguments.length) { }", + "function (p = arguments[0]) { }", + "function (p = () => arguments) { }", + "function (p = () => () => arguments.length) { }", + "function ({ p = arguments }) { }", + "function ([p = arguments]) { }", + "function ({ [arguments.length]: p }) { }", + "function (...[p = arguments]) { }", + "function (a, b = a, c = arguments.length) { }", + "function (p = `${arguments.length}`) { }", + "function (p = { [arguments.length]: 1 }) { }", + "function (p = typeof arguments) { }", + "function named(p = arguments) { }", + "function* (p = arguments) { }", + "async function (p = arguments) { }", + "async function* (p = arguments) { }", + "{ m(p = arguments) { } }", + "{ *m(p = arguments) { } }", + "{ async m(p = arguments) { } }", + "{ async *m(p = arguments) { } }", + "{ set m(p = arguments) { } }", + "{ set m({ p = arguments }) { } }", + "{ ['m'](p = arguments) { } }", + "class { constructor(p = arguments) { } }", + "class extends Object { constructor(p = arguments) { super(); } }", + "class { m(p = arguments) { } }", + "class { static m(p = arguments) { } }", + "class { #m(p = arguments) { } }", + "class { static #m(p = arguments) { } }", + "class { *m(p = arguments) { } }", + "class { async m(p = arguments) { } }", + "class { set m(p = arguments) { } }", + "class { static set m(p = arguments) { } }", + // A function in the parameters of another function. + "function (p = function (q = arguments) { }) { }", + "function (p = { m(q = arguments) { } }) { }", + "function (p = () => function (q = arguments) { }) { }", + // The parameters belong to the function, and so does a class heritage or a computed key in them. + "function (p = class extends arguments { }) { }", + "function (p = class { [arguments]() { } }) { }", + "function (p = class { [arguments] = 1; }) { }", + "function (p = class { static [arguments.length] = 1; }) { }", + // The body was valid before too. + "function () { arguments; }", + "function (p = arguments) { arguments; () => arguments; }", +]; + +for (const [before, after] of [...fields, fieldInStaticBlock]) { + for (const expression of functionsWithOwnArguments) + shouldNotThrowSyntaxError(before + expression + after); +} + +// `super()` in the parameters of the constructor of a derived class. +for (const [before, after] of [...fields, fieldInStaticBlock]) { + for (const expression of [ + "class extends Object { constructor(p = super()) { } }", + "class extends Object { constructor(p = () => super()) { p(); } }", + "class extends Object { constructor(p = super(), q = () => super()) { } }", + "class extends Object { constructor({ p = super() }) { } }", + "class extends Object { constructor(p = class extends super() { }) { } }", + "class extends Object { constructor(p = class { [super()]() { } }) { } }", + "class extends Object { constructor(p = { [super()]: 1 }) { } }", + "class extends Object { constructor(p = super()) { } static m(q = arguments) { } }", + ]) + shouldNotThrowSyntaxError(before + expression + after); +} + +// Still a SyntaxError: the initializer itself, an arrow function, a computed key and a class heritage are part of the initializer. +const argumentsMessage = "Unexpected identifier 'arguments'. Cannot reference 'arguments' in class field initializer."; +const superCallMessage = "Unexpected token '('. super call is not valid in class field initializer context."; +const superMessage = "super is not valid in this context."; + +for (const [before, after] of fields) { + for (const expression of [ + "arguments", + "arguments.length", + "() => arguments", + "(p = arguments) => p", + "(p = arguments.length) => p", + "([p = arguments]) => p", + "(p = () => arguments) => p", + "async (p = arguments) => p", + "async () => arguments", + "{ [arguments]() { } }", + "{ [arguments.length]: function (p) { } }", + "{ get [arguments]() { return 1; } }", + "class { [arguments]() { } }", + "class { static [arguments.length](p) { } }", + "class extends arguments { }", + "class extends (() => arguments) { }", + // A class field initializer in the parameters is an initializer again. + "function (p = class { g = arguments; }) { }", + "function (p = class { g = () => arguments; }) { }", + "function (p = class { static g = arguments.length; }) { }", + "function (p = class { g = (q = arguments) => q; }) { }", + "{ m(p = class { g = arguments; }) { } }", + "function () { class C { g = arguments; } }", + ]) + shouldThrowSyntaxError(before + expression + after, argumentsMessage); + + shouldThrowSyntaxError(before + "function (p = class { static { arguments; } }) { }" + after, "Cannot use 'arguments' as an identifier in static block."); + // The parser reads "({ p = arguments })" as an expression first, and that fails before it gets to `arguments`. + shouldThrowSyntaxError(before + "({ p = arguments }) => p" + after, "Unexpected token '='. Expected a ':' following the property name 'p'."); +} + +for (const [before, after] of [ + ["(class extends Object { f = (", "); })"], + ["(class extends Object { static f = (", "); })"], + ["(class extends Object { constructor() { super(); } f = (", "); })"], + ["(class extends Object { constructor(p = class extends Object { f = (", "); }) { super(); } })"], +]) { + for (const expression of [ + "super()", + "() => super()", + "(p = super()) => p", + "{ [super()]: 1 }", + "class extends super() { }", + "class { [super()]() { } }", + "class extends Object { constructor(p = class { g = super(); }) { } }", + "class extends Object { constructor(p = class { g = () => super(); }) { } }", + ]) + shouldThrowSyntaxError(before + expression + after, superCallMessage); + + // Only the constructor of a derived class can call super(). + for (const expression of [ + "function (p = super()) { }", + "function (p = () => super()) { }", + "{ m(p = super()) { } }", + "{ set m(p = super()) { } }", + "class { constructor(p = super()) { } }", + "class extends Object { m(p = super()) { } }", + "class extends Object { static m(p = super()) { } }", + "class extends Object { set m(p = super()) { } }", + "class extends Object { constructor(p = function (q = super()) { }) { } }", + "class extends Object { constructor(p = function () { super(); }) { } }", + ]) + shouldThrowSyntaxError(before + expression + after, superMessage); +} + +// `yield` and `await` in these parameters are what they are without the class field. +shouldThrowSyntaxError("(function* () { class C { f = function (p = yield) { }; } })", "Unexpected keyword 'yield'. Cannot use yield expression out of generator."); +shouldThrowSyntaxError("(function* () { class C { f = function* (p = yield) { }; } })", "Unexpected keyword 'yield'. Cannot use yield expression within parameters."); +shouldThrowSyntaxError("(function* () { class C { f = { *m(p = yield) { } }; } })", "Unexpected keyword 'yield'. Cannot use yield expression within parameters."); +shouldThrowSyntaxError("(function* () { class C { f = (p = yield) => p; } })", "Unexpected keyword 'yield'. Cannot use yield expression inside class field initializer expression."); +shouldThrowSyntaxError("(async function () { class C { f = async function (p = await 1) { }; } })", "Cannot use 'await' within a parameter default expression."); +shouldNotThrowSyntaxError("(async function () { class C { f = function (p = await) { }; } })"); + +// The values. +class A { + length = function (p = arguments.length) { return p; }; + object = function (p = arguments) { return p === arguments; }; + arrow = function (p = () => arguments.length) { return p(); }; + pattern = function ({ p = arguments.length }, q) { return p; }; + rest = function (...[p = arguments.length]) { return p; }; + method = { m(p = arguments.length) { return p; } }; + setter = { set m(p = arguments.length) { this.value = p; } }; + static staticField = function (p = arguments[1]) { return p; }; + #privateField = function (p = arguments.length) { return p; }; + callPrivateField() { return this.#privateField(undefined, 1, 2, 3); } + generator = function* (p = arguments.length) { yield p; }; + asyncFunction = async function (p = arguments.length) { return p; }; + inArrow = () => function (p = arguments.length) { return p; }; + nested = new (class { constructor(p = arguments.length) { this.p = p; } })(undefined, 1); + nestedFunction = function (p = function (q = arguments.length) { return q; }) { return p(undefined, 1, 2) + arguments.length; }; + newTarget = function (p = new.target, q = () => new.target) { return [p, q()]; }; + evalInParameters = function (p = eval("arguments.length"), q = () => eval("arguments.length")) { return p + q(); }; + evalInInitializer = eval("(function (p = arguments.length) { return p; })"); +} + +let asyncResult; +for (let i = 0; i < testLoopCount; ++i) { + const a = new A(); + shouldBe(a.length(), 0); + shouldBe(a.length(undefined, 2), 2); + shouldBe(a.length(7, 2), 7); + shouldBe(a.object(), true); + shouldBe(a.arrow(undefined, 1, 2), 3); + shouldBe(a.pattern({}, 1), 2); + shouldBe(a.pattern({ p: 5 }), 5); + shouldBe(a.rest(), 0); + shouldBe(a.rest(undefined, 1), 2); + shouldBe(a.method.m(undefined, 1, 2, 3), 4); + a.setter.m = undefined; + shouldBe(a.setter.value, 1); + shouldBe(A.staticField(undefined, "second"), "second"); + shouldBe(a.callPrivateField(), 4); + shouldBe(a.generator(undefined, 1).next().value, 2); + shouldBe(a.inArrow()(undefined, 1, 2), 3); + shouldBe(a.nested.p, 2); + shouldBe(a.nestedFunction(undefined, 1), 5); + shouldBe(a.newTarget()[0], undefined); + shouldBe(a.newTarget()[1], undefined); + shouldBe(new a.newTarget()[0], a.newTarget); + shouldBe(new a.newTarget()[1], a.newTarget); + shouldBe(a.evalInParameters(undefined, undefined, 1), 6); + shouldBe(a.evalInInitializer(undefined, 1, 2), 3); + if (!i) + a.asyncFunction(undefined, 1, 2).then(value => { asyncResult = value; }); +} +drainMicrotasks(); +shouldBe(asyncResult, 3); + +class Base { + constructor(value) { this.value = value; } +} +class B { + Derived = class extends Base { + constructor(p = super("parameters"), q = this.value) { + shouldBe(p, this); + this.q = q; + } + }; + DerivedWithArrow = class extends Base { + constructor(p = () => super("arrow")) { + shouldBe(p(), this); + } + }; + static Derived = class extends Base { + constructor(p = arguments.length, q = super(p)) { } + }; +} + +for (let i = 0; i < testLoopCount; ++i) { + const b = new B(); + const derived = new b.Derived(); + shouldBe(derived.value, "parameters"); + shouldBe(derived.q, "parameters"); + shouldBe(new b.DerivedWithArrow().value, "arrow"); + shouldBe(new B.Derived(undefined, undefined, 2).value, 3); +} diff --git a/Source/JavaScriptCore/parser/Parser.cpp b/Source/JavaScriptCore/parser/Parser.cpp index d3f28f7d139f3..4749744293203 100644 --- a/Source/JavaScriptCore/parser/Parser.cpp +++ b/Source/JavaScriptCore/parser/Parser.cpp @@ -2364,7 +2364,6 @@ template TreeFunctionBody Parser::parseFunctionBo TreeBuilder& context, SyntaxChecker& syntaxChecker, const JSTokenLocation& startLocation, int startColumn, unsigned functionStart, int functionNameStart, int parametersStart, ConstructorKind constructorKind, SuperBinding superBinding, FunctionBodyType bodyType, unsigned parameterCount) { - SetForScope overrideParsingClassFieldInitializer(m_parserState.isParsingClassFieldInitializer, bodyType != StandardFunctionBodyBlock && m_parserState.isParsingClassFieldInitializer); SetForScope maybeUnmaskAsync(m_parserState.classFieldInitMasksAsync, !isAsyncFunctionParseMode(m_parseMode) && m_parserState.classFieldInitMasksAsync); bool isArrowFunctionBodyExpression = bodyType == ArrowFunctionBodyExpression; if (!isArrowFunctionBodyExpression) { @@ -2599,6 +2598,9 @@ template bool Parser::parseFunctionInfo(TreeBuild functionScope->setConstructorKind(constructorKind); SetForScope functionParsePhasePoisoner(m_parserState.functionParsePhase, FunctionParsePhase::Body); + // The early errors of a class field initializer (ContainsArguments, Contains SuperCall) look into an arrow function and + // stop at any other function: at its parameters as well as its body. + SetForScope overrideParsingClassFieldInitializer(m_parserState.isParsingClassFieldInitializer, isArrowFunctionParseMode(mode) && m_parserState.isParsingClassFieldInitializer); int functionNameStart = m_token.m_startPosition.offset; const Identifier* lastFunctionName = m_parserState.lastFunctionName; m_parserState.lastFunctionName = nullptr; From 0b6d919695ba924b7a0fae996478ea801ff83c0f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 22 Sep 2026 05:47:17 +0000 Subject: [PATCH 2/2] [JSC] Test what follows a nested function in a class field initializer 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. --- ...-initializer-nested-function-parameters.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/JSTests/stress/class-field-initializer-nested-function-parameters.js b/JSTests/stress/class-field-initializer-nested-function-parameters.js index dfa0c3ef20dbb..d1be2644a90cc 100644 --- a/JSTests/stress/class-field-initializer-nested-function-parameters.js +++ b/JSTests/stress/class-field-initializer-nested-function-parameters.js @@ -148,9 +148,26 @@ for (const [before, after] of fields) { "function (p = class { g = (q = arguments) => q; }) { }", "{ m(p = class { g = arguments; }) { } }", "function () { class C { g = arguments; } }", + // After the function, the rest of the initializer is checked as before. + "[function (p = arguments) { }, arguments]", + "[{ m(p = arguments) { } }, () => arguments]", + "function (p = arguments) { }(arguments)", + "(a = function (p = arguments) { }, b = arguments) => a", + "async (a = function (p = arguments) { }) => arguments", + "{ m(p = arguments) { }, [arguments]: 1 }", + "class { m(p = arguments) { } [arguments]() { } }", + "() => { (function (p = arguments) { }); return arguments; }", ]) shouldThrowSyntaxError(before + expression + after, argumentsMessage); + // And what is valid only in the initializer is still valid after the function: in global code, `new.target` in an arrow function. + for (const expression of [ + "[function (p = arguments) { }, () => new.target]", + "[() => 1, function (p = arguments) { }]", + "(a = function (p = arguments) { }) => function (q = arguments) { }", + ]) + shouldNotThrowSyntaxError(before + expression + after); + shouldThrowSyntaxError(before + "function (p = class { static { arguments; } }) { }" + after, "Cannot use 'arguments' as an identifier in static block."); // The parser reads "({ p = arguments })" as an expression first, and that fails before it gets to `arguments`. shouldThrowSyntaxError(before + "({ p = arguments }) => p" + after, "Unexpected token '='. Expected a ':' following the property name 'p'."); @@ -171,6 +188,9 @@ for (const [before, after] of [ "class { [super()]() { } }", "class extends Object { constructor(p = class { g = super(); }) { } }", "class extends Object { constructor(p = class { g = () => super(); }) { } }", + // After the constructor, the rest of the initializer is checked as before. + "[class extends Object { constructor(p = super()) { } }, super()]", + "[class extends Object { constructor(p = super()) { } }, () => super()]", ]) shouldThrowSyntaxError(before + expression + after, superCallMessage);