diff --git a/JSTests/stress/new-target-in-class-field-initializer-and-static-block.js b/JSTests/stress/new-target-in-class-field-initializer-and-static-block.js new file mode 100644 index 0000000000000..9a25f4f21a9f3 --- /dev/null +++ b/JSTests/stress/new-target-in-class-field-initializer-and-static-block.js @@ -0,0 +1,146 @@ +// new.target in a class field initializer or in a class static block is the new.target of that class element. +// It is undefined. It must not make the code around the class a user of new.target. + +function shouldBe(actual, expected, message) { + if (actual !== expected) + throw new Error(message + ": expected " + String(expected) + " but got " + String(actual)); +} + +function shouldBeAsync(promise, expected, message) { + let result; + let error; + promise.then(value => { result = value; }, e => { error = e; }); + drainMicrotasks(); + if (error) + throw error; + shouldBe(result, expected, message); +} + +// The class is in an arrow function in global code. +{ + const instanceField = () => { class A { x = typeof new.target; } return new A().x; }; + const staticField = () => { class A { static x = typeof new.target; } return A.x; }; + const staticBlock = () => { let r; class A { static { r = typeof new.target; } } return r; }; + const privateField = () => { class A { #x = typeof new.target; get x() { return this.#x; } } return new A().x; }; + const computedField = () => { class A { ["x"] = typeof new.target; } return new A().x; }; + const classExpression = () => new (class { x = typeof new.target; })().x; + const derivedClass = () => { class B { } class A extends B { x = typeof new.target; } return new A().x; }; + const classWithConstructor = () => { class A { x = typeof new.target; constructor() { this.y = new.target; } } let a = new A(); return a.x + " " + (a.y === A); }; + const arrowInArrow = () => (() => { class A { x = typeof new.target; } return new A().x; })(); + const arrowInField = () => { class A { x = (() => typeof new.target)(); } return new A().x; }; + const arrowInStaticBlock = () => { let r; class A { static { r = (() => typeof new.target)(); } } return r; }; + const blockInStaticBlock = () => { let r; class A { static { if (true) { r = typeof new.target; } } } return r; }; + const keyOfClassInField = () => { class A { x = class { static [typeof new.target] = 1; }; } return Object.keys(new A().x)[0]; }; + const fieldOfClassInField = () => { class A { x = class { static y = typeof new.target; }; } return new A().x.y; }; + const fieldOfClassInStaticBlock = () => { let r; class A { static { class B { y = typeof new.target; } r = new B().y; } } return r; }; + const parameterOfArrowInField = () => { class A { x = ((y = typeof new.target) => y)(); } return new A().x; }; + const evalInField = () => { class A { x = eval("typeof new.target"); } return new A().x; }; + const evalInStaticBlock = () => { let r; class A { static { r = eval("typeof new.target"); } } return r; }; + + shouldBe(instanceField(), "undefined", "instance field"); + shouldBe(staticField(), "undefined", "static field"); + shouldBe(staticBlock(), "undefined", "static block"); + shouldBe(privateField(), "undefined", "private field"); + shouldBe(computedField(), "undefined", "computed field"); + shouldBe(classExpression(), "undefined", "class expression"); + shouldBe(derivedClass(), "undefined", "derived class"); + shouldBe(classWithConstructor(), "undefined true", "class with constructor"); + shouldBe(arrowInArrow(), "undefined", "arrow function in arrow function"); + shouldBe(arrowInField(), "undefined", "arrow function in field"); + shouldBe(arrowInStaticBlock(), "undefined", "arrow function in static block"); + shouldBe(blockInStaticBlock(), "undefined", "block in static block"); + shouldBe(keyOfClassInField(), "undefined", "key of a class in a field"); + shouldBe(fieldOfClassInField(), "undefined", "field of a class in a field"); + shouldBe(fieldOfClassInStaticBlock(), "undefined", "field of a class in a static block"); + shouldBe(parameterOfArrowInField(), "undefined", "parameter of an arrow function in a field"); + shouldBe(evalInField(), "undefined", "eval in field"); + shouldBe(evalInStaticBlock(), "undefined", "eval in static block"); +} + +// The class is in an async arrow function in global code. The call must not throw. +{ + const asyncArrow = async () => { class A { x = typeof new.target; } return new A().x; }; + const asyncArrowWithAwait = async () => { await 1; class A { static x = typeof new.target; static y; static { A.y = typeof new.target; } } return A.x + " " + A.y; }; + const asyncArrowWithParameters = async (a = 1, ...rest) => { class A { x = typeof new.target; } return new A().x; }; + + shouldBeAsync(asyncArrow(), "undefined", "async arrow function"); + shouldBeAsync(asyncArrowWithAwait(), "undefined undefined", "async arrow function with await"); + shouldBeAsync(asyncArrowWithParameters(), "undefined", "async arrow function with parameters"); +} + +// The class is in an arrow function with parameters that are not simple. +{ + const arrowWithParameters = (a = 1, { b } = { b: 2 }, ...rest) => { class A { x = typeof new.target; static { A.y = typeof new.target; } } return new A().x + " " + A.y; }; + shouldBe(arrowWithParameters(), "undefined undefined", "arrow function with parameters"); +} + +// The class is in an arrow function in a function. The function does not save new.target for a static block. +{ + function staticBlockInArrowInFunction() { + return (() => { let r; class A { static { r = typeof new.target; } } return r; })(); + } + function fieldInArrowInFunction() { + return (() => { class A { x = typeof new.target; static y = typeof new.target; } return new A().x + " " + A.y; })(); + } + function Constructor() { + this.staticBlock = (() => { let r = 1; class A { static { r = new.target; } } return r; })(); + this.field = (() => { class A { x = new.target; } return new A().x; })(); + this.target = (() => new.target)(); + } + const method = { method() { return (() => { let r; class A { x = typeof new.target; static { r = typeof new.target; } } return new A().x + " " + r; })(); } }.method; + + shouldBe(staticBlockInArrowInFunction(), "undefined", "static block, arrow function in function"); + shouldBe(new staticBlockInArrowInFunction() instanceof staticBlockInArrowInFunction, true, "static block, arrow function in constructor"); + shouldBe(fieldInArrowInFunction(), "undefined undefined", "field, arrow function in function"); + let object = new Constructor(); + shouldBe(object.staticBlock, undefined, "static block, arrow function in constructor"); + shouldBe(object.field, undefined, "field, arrow function in constructor"); + shouldBe(object.target, Constructor, "arrow function in constructor"); + shouldBe(method(), "undefined undefined", "arrow function in method"); +} + +// The class is in eval code. +{ + shouldBe((0, eval)("class A { x = typeof new.target; } new A().x"), "undefined", "field, indirect eval"); + shouldBe((0, eval)("class B { static x = typeof new.target; } B.x"), "undefined", "static field, indirect eval"); + shouldBe((0, eval)("var r; class C { static { r = typeof new.target; } } r"), "undefined", "static block, indirect eval"); + shouldBe((0, eval)("(() => { class A { x = typeof new.target; } return new A().x; })()"), "undefined", "arrow function, indirect eval"); + shouldBe(eval("class D { x = typeof new.target; } new D().x"), "undefined", "field, direct eval in global code"); + shouldBe((() => eval("class A { x = typeof new.target; static { A.y = typeof new.target; } } new A().x + ' ' + A.y"))(), "undefined undefined", "direct eval in arrow function"); + function directEvalInFunction() { + return eval("class A { x = new.target; static { A.y = new.target; } } [new A().x, A.y, new.target]"); + } + let [field, staticBlock, target] = new directEvalInFunction(); + shouldBe(field, undefined, "field, direct eval in constructor"); + shouldBe(staticBlock, undefined, "static block, direct eval in constructor"); + shouldBe(target, directEvalInFunction, "direct eval in constructor"); + shouldBe(new Function("return (() => { class A { x = typeof new.target; } return new A().x; })()")(), "undefined", "Function constructor"); +} + +// A function in a class element has its own new.target. A key and a heritage belong to the code around the class. +{ + const functionInField = () => { class A { f = function () { return new.target; }; } return new A().f; }; + const functionInStaticBlock = () => { let f; class A { static { f = function () { return new.target; }; } } return f; }; + function Keys() { + class A { [new.target.name] = 1; static [new.target.name + "Static"] = 2; [new.target.name + "Method"]() { } } + this.keys = Object.keys(new A()).concat(Object.keys(A), Object.getOwnPropertyNames(A.prototype)); + } + function Heritage() { + class A extends new.target.Base { } + this.result = new A() instanceof Heritage.Base; + } + Heritage.Base = class { }; + function KeysInArrow() { + this.keys = (() => { class A { [new.target.name] = typeof new.target; } let a = new A(); return Object.keys(a)[0] + " " + a.KeysInArrow; })(); + } + + let f = functionInField(); + shouldBe(f(), undefined, "function in field, call"); + shouldBe(new f(), f, "function in field, construct"); + let g = functionInStaticBlock(); + shouldBe(g(), undefined, "function in static block, call"); + shouldBe(new g(), g, "function in static block, construct"); + shouldBe(new Keys().keys.join(), "Keys,KeysStatic,constructor,KeysMethod", "keys"); + shouldBe(new Heritage().result, true, "heritage"); + shouldBe(new KeysInArrow().keys, "KeysInArrow undefined", "key and field in arrow function in constructor"); +} diff --git a/Source/JavaScriptCore/parser/ASTBuilder.h b/Source/JavaScriptCore/parser/ASTBuilder.h index 12d4a4264a7a3..74cf1a0ba11cf 100644 --- a/Source/JavaScriptCore/parser/ASTBuilder.h +++ b/Source/JavaScriptCore/parser/ASTBuilder.h @@ -189,9 +189,12 @@ class ASTBuilder { setExceptionLocation(node, start, divot, end); return node; } - ExpressionNode* createNewTargetExpr(const JSTokenLocation location) + // usesNewTargetOfThisCode is false for a class field initializer or a class static block that + // the parser parses in place. Their new.target is not the new.target of the code that this builds. + ExpressionNode* createNewTargetExpr(const JSTokenLocation location, bool usesNewTargetOfThisCode) { - usesNewTarget(); + if (usesNewTargetOfThisCode) + usesNewTarget(); return new (m_parserArena) NewTargetNode(location); } ExpressionNode* createImportMetaExpr(const JSTokenLocation& location, ExpressionNode* expr) { return new (m_parserArena) ImportMetaNode(location, expr); } diff --git a/Source/JavaScriptCore/parser/Parser.cpp b/Source/JavaScriptCore/parser/Parser.cpp index d3f28f7d139f3..47fb4c2d8dedd 100644 --- a/Source/JavaScriptCore/parser/Parser.cpp +++ b/Source/JavaScriptCore/parser/Parser.cpp @@ -3370,7 +3370,9 @@ template TreeClassExpression Parser::parseClass(T SetForScope overrideParsingClassFieldInitializer(m_parserState.isParsingClassFieldInitializer, true); SetForScope maskAsync(m_parserState.classFieldInitMasksAsync, true); classScope->setExpectedSuperBinding(SuperBinding::Needed); + classScope->setIsParsingFieldInitializerInPlace(true); initializer = parseAssignmentExpression(context); + classScope->setIsParsingFieldInitializerInPlace(false); classScope->setExpectedSuperBinding(SuperBinding::NotNeeded); failIfFalse(initializer, "Cannot parse initializer for class field"); classScope->markLastUsedVariablesSetAsCaptured(usedVariablesSize); @@ -5483,11 +5485,16 @@ template TreeExpression Parser::parseMemberExpres bool isClassFieldInitializer = m_parserState.isParsingClassFieldInitializer; bool isFunctionEvalContextType = m_isInsideOrdinaryFunction && (closestOrdinaryFunctionScope->evalContextType() == EvalContextType::FunctionEvalContext || closestOrdinaryFunctionScope->evalContextType() == EvalContextType::InstanceFieldEvalContext); semanticFailIfFalse(currentScope()->isFunction() || currentScope()->isStaticBlock() || isFunctionEvalContextType || isClassFieldInitializer, "new.target is only valid inside functions or static blocks"); - if (currentScope()->isArrowFunction()) { + // The code around a class must not become a user of new.target because of a class element + // that parseClass() parses in place. An arrow function, or eval code, that uses new.target + // loads it from the scope of a function that saved it there. No function saves it for + // a new.target that it does not contain. + bool usesNewTargetOfClassElementParsedInPlace = isDirectlyInClassElementParsedInPlace(); + if (currentScope()->isArrowFunction() && !usesNewTargetOfClassElementParsedInPlace) { semanticFailIfFalse(!closestOrdinaryFunctionScope->isGlobalCode() || isFunctionEvalContextType || isClassFieldInitializer, "new.target is not valid inside arrow functions in global code"); currentScope()->setInnerArrowFunctionUsesNewTarget(); } - base = context.createNewTargetExpr(location); + base = context.createNewTargetExpr(location, !usesNewTargetOfClassElementParsedInPlace); newCount--; next(); } else { diff --git a/Source/JavaScriptCore/parser/Parser.h b/Source/JavaScriptCore/parser/Parser.h index 96a6a5a833e26..4540269cd9ca2 100644 --- a/Source/JavaScriptCore/parser/Parser.h +++ b/Source/JavaScriptCore/parser/Parser.h @@ -327,6 +327,10 @@ struct Scope { m_isClassScope = true; } + // True for a class scope while parseClass() parses one of its field initializers in place. + void setIsParsingFieldInitializerInPlace(bool isParsingFieldInitializerInPlace) { m_isParsingFieldInitializerInPlace = isParsingFieldInitializerInPlace; } + bool isParsingFieldInitializerInPlace() const { return m_isParsingFieldInitializerInPlace; } + bool isLexicalScope() const { return m_isLexicalScope; } bool usesEval() const { return m_usesEval; } bool usesImportMeta() const { return m_usesImportMeta; } @@ -1043,6 +1047,7 @@ struct Scope { bool m_isEvalContext : 1 { false }; bool m_hasNonSimpleParameterList : 1 { false }; bool m_isClassScope : 1 { false }; + bool m_isParsingFieldInitializerInPlace : 1 { false }; bool m_asyncFunctionBodyDoesNotUseAwait : 1 { false }; bool m_usesAwait : 1 { false }; int m_loopDepth { 0 }; @@ -1338,6 +1343,24 @@ class JSC_CACHE_LINE_ALIGNED Parser { return scope; } + // A class field initializer and a class static block are functions of their own at run time. + // parseClass() parses the source of each in place, with the scopes and the tree builder of the + // code around the class. That parse checks the syntax and finds the end. The function is parsed + // again, on its own, when it is compiled. This returns true when the current position is in + // such a class element and not in an arrow function inside it. The code there uses the + // new.target of the class element, and not the new.target of the code around the class. + bool isDirectlyInClassElementParsedInPlace() + { + for (Scope* scope = currentScope(); scope; scope = scope->containingScope()) { + // The static block that this parser compiles is the outermost scope. + if (scope->isFunctionBoundary()) + return scope->isStaticBlockBoundary() && scope->containingScope(); + if (scope->isParsingFieldInitializerInPlace()) + return true; + } + return false; + } + Scope* pushScope() { ImplementationVisibility implementationVisibility = m_implementationVisibility; diff --git a/Source/JavaScriptCore/parser/SyntaxChecker.h b/Source/JavaScriptCore/parser/SyntaxChecker.h index ef23c234f976c..c98791041e9c6 100644 --- a/Source/JavaScriptCore/parser/SyntaxChecker.h +++ b/Source/JavaScriptCore/parser/SyntaxChecker.h @@ -168,7 +168,7 @@ class SyntaxChecker { ExpressionType createImportExpr(const JSTokenLocation&, ExpressionType, ExpressionType, bool, int, int, int) { return ImportExpr; } ExpressionType createThisExpr(const JSTokenLocation&) { return ThisExpr; } ExpressionType createSuperExpr(const JSTokenLocation&) { return SuperExpr; } - ExpressionType createNewTargetExpr(const JSTokenLocation&) { return NewTargetExpr; } + ExpressionType createNewTargetExpr(const JSTokenLocation&, bool) { return NewTargetExpr; } ExpressionType createImportMetaExpr(const JSTokenLocation&, ExpressionType) { return ImportMetaExpr; } ALWAYS_INLINE bool isMetaProperty(ExpressionType type) { return type & MetaPropertyBit; } ALWAYS_INLINE bool isNewTarget(ExpressionType type) { return type == NewTargetExpr; }