Skip to content

Commit ae349ec

Browse files
davidotlinusg
authored andcommitted
LibJS: Use a synthetic constructor if class with parent doesn't have one
We already did this but it called the @@iterator method of %Array.prototype% visible to the user for example by overriding that method. This should not be visible so we use a special version of SuperCall now.
1 parent b79f031 commit ae349ec

File tree

4 files changed

+100
-4
lines changed

4 files changed

+100
-4
lines changed

Userland/Libraries/LibJS/AST.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,22 @@ Completion SuperCall::execute(Interpreter& interpreter, GlobalObject& global_obj
463463

464464
// 4. Let argList be ? ArgumentListEvaluation of Arguments.
465465
MarkedVector<Value> arg_list(vm.heap());
466-
TRY(argument_list_evaluation(interpreter, global_object, m_arguments, arg_list));
466+
if (m_is_synthetic == IsPartOfSyntheticConstructor::Yes) {
467+
// NOTE: This is the case where we have a fake constructor(...args) { super(...args); } which
468+
// shouldn't call @@iterator of %Array.prototype%.
469+
VERIFY(m_arguments.size() == 1);
470+
VERIFY(m_arguments[0].is_spread);
471+
auto const& argument = m_arguments[0];
472+
auto value = MUST(argument.value->execute(interpreter, global_object)).release_value();
473+
VERIFY(value.is_object() && is<Array>(value.as_object()));
474+
475+
auto& array_value = static_cast<Array const&>(value.as_object());
476+
auto length = MUST(length_of_array_like(global_object, array_value));
477+
for (size_t i = 0; i < length; ++i)
478+
arg_list.append(array_value.get_without_side_effects(PropertyKey { i }));
479+
} else {
480+
TRY(argument_list_evaluation(interpreter, global_object, m_arguments, arg_list));
481+
}
467482

468483
// 5. If IsConstructor(func) is false, throw a TypeError exception.
469484
if (!func || !Value(func).is_constructor())
@@ -1827,7 +1842,7 @@ ThrowCompletionOr<ECMAScriptFunctionObject*> ClassExpression::class_definition_e
18271842
vm.running_execution_context().private_environment = outer_private_environment;
18281843
};
18291844

1830-
// FIXME: Step 14.a is done in the parser. But maybe it shouldn't?
1845+
// FIXME: Step 14.a is done in the parser. By using a synthetic super(...args) which does not call @@iterator of %Array.prototype%
18311846
auto class_constructor_value = TRY(m_constructor->execute(interpreter, global_object)).release_value();
18321847

18331848
update_function_name(class_constructor_value, class_name);

Userland/Libraries/LibJS/AST.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,17 +1492,34 @@ class NewExpression final : public CallExpression {
14921492

14931493
class SuperCall final : public Expression {
14941494
public:
1495+
// This is here to be able to make a constructor like
1496+
// constructor(...args) { super(...args); } which does not use @@iterator of %Array.prototype%.
1497+
enum class IsPartOfSyntheticConstructor {
1498+
No,
1499+
Yes,
1500+
};
1501+
14951502
SuperCall(SourceRange source_range, Vector<CallExpression::Argument> arguments)
14961503
: Expression(source_range)
14971504
, m_arguments(move(arguments))
1505+
, m_is_synthetic(IsPartOfSyntheticConstructor::No)
1506+
{
1507+
}
1508+
1509+
SuperCall(SourceRange source_range, IsPartOfSyntheticConstructor is_part_of_synthetic_constructor, CallExpression::Argument constructor_argument)
1510+
: Expression(source_range)
1511+
, m_arguments({ move(constructor_argument) })
1512+
, m_is_synthetic(IsPartOfSyntheticConstructor::Yes)
14981513
{
1514+
VERIFY(is_part_of_synthetic_constructor == IsPartOfSyntheticConstructor::Yes);
14991515
}
15001516

15011517
virtual Completion execute(Interpreter&, GlobalObject&) const override;
15021518
virtual void dump(int indent) const override;
15031519

15041520
private:
15051521
Vector<CallExpression::Argument> const m_arguments;
1522+
IsPartOfSyntheticConstructor const m_is_synthetic;
15061523
};
15071524

15081525
enum class AssignmentOp {

Userland/Libraries/LibJS/Parser.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,17 +1313,24 @@ NonnullRefPtr<ClassExpression> Parser::parse_class_expression(bool expect_class_
13131313
if (!super_class.is_null()) {
13141314
// Set constructor to the result of parsing the source text
13151315
// constructor(... args){ super (...args);}
1316+
// However: The most notable distinction is that while the aforementioned ECMAScript
1317+
// source text observably calls the @@iterator method on %Array.prototype%,
1318+
// this function does not.
1319+
// So we use a custom version of SuperCall which doesn't use the @@iterator
1320+
// method on %Array.prototype% visibly.
1321+
FlyString argument_name = "args";
13161322
auto super_call = create_ast_node<SuperCall>(
13171323
{ m_state.current_token.filename(), rule_start.position(), position() },
1318-
Vector { CallExpression::Argument { create_ast_node<Identifier>({ m_state.current_token.filename(), rule_start.position(), position() }, "args"), true } });
1324+
SuperCall::IsPartOfSyntheticConstructor::Yes,
1325+
CallExpression::Argument { create_ast_node<Identifier>({ m_state.current_token.filename(), rule_start.position(), position() }, "args"), true });
13191326
// NOTE: While the JS approximation above doesn't do `return super(...args)`, the
13201327
// abstract closure is expected to capture and return the result, so we do need a
13211328
// return statement here to create the correct completion.
13221329
constructor_body->append(create_ast_node<ReturnStatement>({ m_state.current_token.filename(), rule_start.position(), position() }, move(super_call)));
13231330

13241331
constructor = create_ast_node<FunctionExpression>(
13251332
{ m_state.current_token.filename(), rule_start.position(), position() }, class_name, "",
1326-
move(constructor_body), Vector { FunctionNode::Parameter { FlyString { "args" }, nullptr, true } }, 0, FunctionKind::Normal,
1333+
move(constructor_body), Vector { FunctionNode::Parameter { move(argument_name), nullptr, true } }, 0, FunctionKind::Normal,
13271334
/* is_strict_mode */ true, /* might_need_arguments_object */ false, /* contains_direct_call_to_eval */ false);
13281335
} else {
13291336
constructor = create_ast_node<FunctionExpression>(

Userland/Libraries/LibJS/Tests/classes/class-inheritance.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,3 +449,60 @@ test("super outside of derived class fails to parse", () => {
449449
new J();
450450
}).toThrowWithMessage(SyntaxError, "'super' keyword unexpected here");
451451
});
452+
453+
test("When no constructor on deriving class @@iterator of %Array.prototype% is not visibly called", () => {
454+
const oldIterator = Array.prototype[Symbol.iterator];
455+
var calls = 0;
456+
Array.prototype[Symbol.iterator] = function () {
457+
++calls;
458+
expect().fail("Called @@iterator");
459+
};
460+
461+
class Base {
462+
constructor(value1, value2) {
463+
this.value1 = value1;
464+
this.value2 = value2;
465+
}
466+
}
467+
468+
class Derived extends Base {}
469+
470+
const noArgumentsDerived = new Derived();
471+
expect(noArgumentsDerived.value1).toBeUndefined();
472+
expect(noArgumentsDerived.value2).toBeUndefined();
473+
expect(noArgumentsDerived).toBeInstanceOf(Base);
474+
expect(noArgumentsDerived).toBeInstanceOf(Derived);
475+
476+
const singleArgumentDerived = new Derived(1);
477+
expect(singleArgumentDerived.value1).toBe(1);
478+
expect(singleArgumentDerived.value2).toBeUndefined();
479+
480+
const singleArrayArgumentDerived = new Derived([1, 2]);
481+
expect(singleArrayArgumentDerived.value1).toEqual([1, 2]);
482+
expect(singleArrayArgumentDerived.value2).toBeUndefined();
483+
484+
const doubleArgumentDerived = new Derived(1, 2);
485+
expect(doubleArgumentDerived.value1).toBe(1);
486+
expect(doubleArgumentDerived.value2).toBe(2);
487+
488+
expect(calls).toBe(0);
489+
490+
class Derived2 extends Base {
491+
constructor(...args) {
492+
super(...args);
493+
}
494+
}
495+
496+
expect(() => {
497+
new Derived2();
498+
}).toThrowWithMessage(ExpectationError, "Called @@iterator");
499+
500+
expect(calls).toBe(1);
501+
502+
Array.prototype[Symbol.iterator] = oldIterator;
503+
504+
// Now Derived2 is fine again.
505+
expect(new Derived2()).toBeInstanceOf(Derived2);
506+
507+
expect(calls).toBe(1);
508+
});

0 commit comments

Comments
 (0)