Skip to content

Commit

Permalink
[JSC] Disallow yield/await expressions in class field initializers
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=278589
rdar://132338331

Reviewed by NOBODY (OOPS!).

The language spec doesn't explictly disallow yield and await expressions
in class field initializers, however it implicitly does given that
the expression is effectively evaluated as if it's inside a method.
Additionally, the consensus seems to be that these expressions
should not be allowed, see:

tc39/ecma262#3333

The yield and await expressions are now handled differently, but
similar to how they are each handled in other contexts. This also
seems to be the least disruptive way to handle existing scripts,
as well as consistent with other engines:

yield: raise syntax error
await: parse as an identifier

Also adding a new test that generates and verifies scripts containing
a class with a field initializer containing yield and await, where the
class is either not nested or nested inside generator or async functions
to verify the behavior of various combinations.

* JSTests/stress/yield-await-class-field-initializer-expr.js: Added.
(genTestCases):
(genTestScript.append):
(genTestScript):
(expected):
(testNegativeCases):
(testPositiveCases.assertEq):
(testPositiveCases.yieldFn0):
* Source/JavaScriptCore/parser/Parser.cpp:
(JSC::Parser<LexerType>::parseFunctionBody):
(JSC::Parser<LexerType>::parseClass):
(JSC::Parser<LexerType>::parseYieldExpression):
(JSC::Parser<LexerType>::parseAwaitExpression):
(JSC::Parser<LexerType>::parsePrimaryExpression):
(JSC::Parser<LexerType>::parseUnaryExpression):
* Source/JavaScriptCore/parser/Parser.h:
  • Loading branch information
dhecht committed Aug 26, 2024
1 parent 4fb2fee commit c5a62f3
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 2 deletions.
117 changes: 117 additions & 0 deletions JSTests/stress/yield-await-class-field-initializer-expr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Tests for 'yield' and 'await' inside class field initializers, where the class is declared inside
// async and generator functions.
const verbose = false;

const wrappers = ['none', 'generator', 'async'];
const fieldModifiers = ['', 'static'];
const fieldInitExprs = ['yield', 'yield 42', 'function() { yield 21; }', 'await', 'await 3', '() => await 7', 'function() { await 9; }'];

function genTestCases() {
let cases = [];
for (const wrapper of wrappers) {
for (const fieldModifier of fieldModifiers) {
for (const fieldInitExpr of fieldInitExprs) {
cases.push({ wrapper, fieldModifier, fieldInitExpr });
}
}
}
return cases;
}

function genTestScript(c) {
let tabs = 0;
let script = "";

function append(line) {
for (t = 0; t < tabs; t++)
script += ' ';
script += line + '\n';
}

switch (c.wrapper) {
case 'generator':
append('function * g() {');
break;
case 'async':
append('async function f() {');
break;
case 'none':
break;
}
tabs++;
append('class C {');
tabs++;
append(`${c.fieldModifier} f = ${c.fieldInitExpr};`);
tabs--;
append('}');
tabs--;
if (c.wrapper !== 'none') append('}');
return script;
}

function expected(c, result, error) {
if (c.fieldInitExpr === 'await') {
// 'await' will parse as an identifier.
if (c.wrapper === 'none' && c.fieldModifier === 'static') {
// In this case, 'await' as identifier produces a ReferenceError.
return result === null && error instanceof ReferenceError;
}
// In these cases, 'await' as identifier has value 'undefined').
return result === undefined && error === null;
}
// All other cases should result in a SyntaxError.
return result === null && error instanceof SyntaxError;
}

// Verify that 'await' and 'yield' do not parse as keywords (directly) inside class field initializers.
function testNegativeCases() {
cases = genTestCases();

for (const c of cases) {
let script = genTestScript(c);
let result = null;
let error = null;
try {
result = eval(script);
} catch (e) {
error = e;
}

if (verbose || !expected(c, result, error)) {
print(`Case: ${c.wrapper}:${c.fieldModifier}:${c.fieldInitExpr}`);
print(`Script:\n${script}`);
print(`Result: ${result}`);
print(`Error: ${error}\n`);
}
}
}

// Verify that 'await' and 'yield' work inside anonymous async / generator functions
// used as class field initializers.
function testPositiveCases() {
function assertEq(got, expected) {
if (got !== expected) {
throw Error(`Got: ${got}, Expected: ${expected}`);
}
}

class C {
asyncFn0 = async y => await y;
asyncFn1 = async () => { return await 5 };
asyncFn2 = async function(x) { return await x; };

yieldFn0 = function* () { yield 9; };
yieldFn1 = async function* () { yield await 11; };
};
let c = new C();

c.asyncFn0(3).then(x => assertEq(x, 3));
c.asyncFn1().then(x => assertEq(x, 5));
c.asyncFn2(7).then(x => assertEq(x, 7));

assertEq(c.yieldFn0().next().value, 9);
c.yieldFn1().next().then(x => assertEq(x.value, 11));
}

testNegativeCases();
testPositiveCases();
10 changes: 8 additions & 2 deletions Source/JavaScriptCore/parser/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2239,6 +2239,7 @@ template <class TreeBuilder> TreeFunctionBody Parser<LexerType>::parseFunctionBo
ConstructorKind constructorKind, SuperBinding superBinding, FunctionBodyType bodyType, unsigned parameterCount)
{
SetForScope overrideParsingClassFieldInitializer(m_parserState.isParsingClassFieldInitializer, bodyType == StandardFunctionBodyBlock ? false : m_parserState.isParsingClassFieldInitializer);
SetForScope maybeUnmaskAsync(m_parserState.classFieldInitMasksAsync, isAsyncFunctionParseMode(m_parseMode) ? false : m_parserState.classFieldInitMasksAsync);
bool isArrowFunctionBodyExpression = bodyType == ArrowFunctionBodyExpression;
if (!isArrowFunctionBodyExpression) {
next();
Expand Down Expand Up @@ -3224,6 +3225,7 @@ template <class TreeBuilder> TreeClassExpression Parser<LexerType>::parseClass(T
size_t usedVariablesSize = currentScope()->currentUsedVariablesSize();
currentScope()->pushUsedVariableSet();
SetForScope overrideParsingClassFieldInitializer(m_parserState.isParsingClassFieldInitializer, true);
SetForScope maskAsync(m_parserState.classFieldInitMasksAsync, true);
classScope->setExpectedSuperBinding(SuperBinding::Needed);
initializer = parseAssignmentExpression(context);
classScope->setExpectedSuperBinding(SuperBinding::NotNeeded);
Expand Down Expand Up @@ -4331,6 +4333,9 @@ template <class TreeBuilder> TreeExpression Parser<LexerType>::parseYieldExpress
// http://ecma-international.org/ecma-262/6.0/#sec-generator-function-definitions-static-semantics-early-errors
failIfTrue(m_parserState.functionParsePhase == FunctionParsePhase::Parameters, "Cannot use yield expression within parameters");

// https://github.com/tc39/ecma262/issues/3333
failIfTrue(m_parserState.isParsingClassFieldInitializer, "Cannot use yield expression inside class field initializer expression");

JSTokenLocation location(tokenLocation());
JSTextPosition divotStart = tokenStartPosition();
ASSERT(match(YIELD));
Expand All @@ -4357,6 +4362,7 @@ template <class TreeBuilder> TreeExpression Parser<LexerType>::parseAwaitExpress
ASSERT(currentScope()->isAsyncFunction() || isModuleParseMode(sourceParseMode()));
ASSERT(isAsyncFunctionParseMode(sourceParseMode()) || isModuleParseMode(sourceParseMode()));
ASSERT(m_parserState.functionParsePhase != FunctionParsePhase::Parameters);
ASSERT(!m_parserState.classFieldInitMasksAsync);
JSTokenLocation location(tokenLocation());
JSTextPosition divotStart = tokenStartPosition();
next();
Expand Down Expand Up @@ -5091,7 +5097,7 @@ template <class TreeBuilder> TreeExpression Parser<LexerType>::parsePrimaryExpre
semanticFailIfTrue(currentScope()->isStaticBlock(), "The 'await' keyword is disallowed in the IdentifierReference position within static block");
if (m_parserState.functionParsePhase == FunctionParsePhase::Parameters)
semanticFailIfFalse(m_parserState.allowAwait, "Cannot use 'await' within a parameter default expression");
else if (currentFunctionScope()->isAsyncFunctionBoundary() || isModuleParseMode(sourceParseMode()))
else if (!m_parserState.classFieldInitMasksAsync && (currentFunctionScope()->isAsyncFunctionBoundary() || isModuleParseMode(sourceParseMode())))
return parseAwaitExpression(context);

goto identifierExpression;
Expand Down Expand Up @@ -5588,7 +5594,7 @@ template <class TreeBuilder> TreeExpression Parser<LexerType>::parseUnaryExpress
bool hasPrefixUpdateOp = false;
unsigned lastOperator = 0;

if (UNLIKELY(match(AWAIT) && (currentFunctionScope()->isAsyncFunctionBoundary() || isModuleParseMode(sourceParseMode())))) {
if (UNLIKELY(match(AWAIT) && !m_parserState.classFieldInitMasksAsync && (currentFunctionScope()->isAsyncFunctionBoundary() || isModuleParseMode(sourceParseMode())))) {
semanticFailIfTrue(currentScope()->isStaticBlock(), "Cannot use 'await' within static block");
return parseAwaitExpression(context);
}
Expand Down
1 change: 1 addition & 0 deletions Source/JavaScriptCore/parser/Parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -2018,6 +2018,7 @@ class Parser {
const Identifier* lastPrivateName { nullptr };
bool allowAwait { true };
bool isParsingClassFieldInitializer { false };
bool classFieldInitMasksAsync { false };
};

// If you're using this directly, you probably should be using
Expand Down

0 comments on commit c5a62f3

Please sign in to comment.