Skip to content

Commit df8f38b

Browse files
committed
[MERGE #5218 @sethbrenith] Fix construction of bound classes
Merge pull request #5218 from sethbrenith:user/sethb/bound-class The calling convention for class constructors is totally different than ordinary functions: they expect newTarget in the arguments 0 slot, where "this" usually goes. Recent changes to fix the construction of bound functions regressed the construction of bound classes. This change adds support (and tests) for the class case. Fixes OS:17555846 Fixes OS:17554002
2 parents 550d2ea + 73975d9 commit df8f38b

File tree

2 files changed

+128
-47
lines changed

2 files changed

+128
-47
lines changed

lib/Runtime/Library/BoundFunction.cpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,25 @@ namespace Js
142142
Var newTargetVar = args.GetNewTarget();
143143
AssertOrFailFastMsg(JavascriptOperators::IsConstructor(newTargetVar), "newTarget must be a constructor");
144144
RecyclableObject* newTarget = RecyclableObject::UnsafeFromVar(newTargetVar);
145-
args.Values[0] = newVarInstance = JavascriptOperators::CreateFromConstructor(newTarget, scriptContext);
145+
146+
// Class constructors expect newTarget to be in args slot 0 (usually "this"),
147+
// because "this" is not constructed until we reach the most-super superclass.
148+
FunctionInfo* functionInfo = JavascriptOperators::GetConstructorFunctionInfo(targetFunction, scriptContext);
149+
if (functionInfo && functionInfo->IsClassConstructor())
150+
{
151+
args.Values[0] = newVarInstance = newTarget;
152+
}
153+
else
154+
{
155+
args.Values[0] = newVarInstance = JavascriptOperators::CreateFromConstructor(newTarget, scriptContext);
156+
}
146157
}
147158
else if (!JavascriptProxy::Is(targetFunction))
148159
{
149-
// no new target and target is not a proxy can make a new object in a "normal" way
150-
args.Values[0] = newVarInstance = JavascriptOperators::CreateFromConstructor(targetFunction, scriptContext);
160+
// No new target and target is not a proxy can make a new object in a "normal" way.
161+
// NewScObjectNoCtor will either construct an object or return targetFunction depending
162+
// on whether targetFunction is a class constructor.
163+
args.Values[0] = newVarInstance = JavascriptOperators::NewScObjectNoCtor(targetFunction, scriptContext);
151164
}
152165
else
153166
{

test/es6/boundConstruction.js

Lines changed: 112 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,22 @@ WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js");
88
//setup code
99
let constructionCount = 0;
1010
let functionCallCount = 0;
11+
let classConstructorCount = 0;
1112
function a(arg1, arg2) {
1213
this[arg1] = arg2;
14+
this.localVal = this.protoVal;
1315
++functionCallCount;
1416
}
17+
a.prototype.protoVal = 1;
18+
19+
class A {
20+
constructor(arg1, arg2) {
21+
this[arg1] = arg2;
22+
this.localVal = this.protoVal;
23+
++classConstructorCount;
24+
}
25+
}
26+
A.prototype.protoVal = 2;
1527

1628
const trapped = new Proxy(a, {
1729
construct: function (x, y, z) {
@@ -25,84 +37,140 @@ const noTrap = new Proxy(a, {});
2537
const boundObject = {};
2638

2739
const boundFunc = a.bind(boundObject, "prop-name");
40+
boundFunc.prototype = {}; // so we can extend from it
41+
const boundClass = A.bind(boundObject, "prop-name");
42+
boundClass.prototype = {};
2843

2944
const boundTrapped = trapped.bind(boundObject, "prop-name");
3045
const boundUnTrapped = noTrap.bind(boundObject, "prop-name");
3146

47+
class newTarget {}
48+
newTarget.prototype.protoVal = 3;
49+
50+
class ExtendsBoundFunc extends boundFunc {}
51+
ExtendsBoundFunc.prototype.protoVal = 4;
52+
class ExtendsBoundClass extends boundClass {}
53+
ExtendsBoundClass.prototype.protoVal = 5;
54+
55+
class ExtendsFunc extends a {}
56+
ExtendsFunc.prototype.protoVal = 6;
57+
class ExtendsClass extends A {}
58+
ExtendsClass.prototype.protoVal = 7;
59+
60+
const boundClassExtendsFunc = ExtendsFunc.bind(boundObject, "prop-name");
61+
const boundClassExtendsClass = ExtendsClass.bind(boundObject, "prop-name");
62+
63+
function test(ctor, useReflect, expectedPrototype, expectedConstructionCount, expectedFunctionCallCount, expectedClassConstructorCount) {
64+
constructionCount = 0;
65+
functionCallCount = 0;
66+
classConstructorCount = 0;
67+
const obj = useReflect ? Reflect.construct(ctor, ["prop-value"], newTarget) : new ctor("prop-value");
68+
assert.areNotEqual(boundObject, obj, "bound function should ignore bound this when constructing");
69+
assert.areEqual("prop-value", obj["prop-name"], "bound function should keep bound arguments when constructing");
70+
assert.areEqual(expectedConstructionCount, constructionCount, `proxy construct trap should be called ${expectedConstructionCount} times`);
71+
assert.areEqual(expectedFunctionCallCount, functionCallCount, `base function-style constructor should be called ${expectedFunctionCallCount} times`);
72+
assert.areEqual(expectedClassConstructorCount, classConstructorCount, `base class constructor should be called ${expectedClassConstructorCount} times`);
73+
assert.strictEqual(expectedPrototype.prototype, obj.__proto__, useReflect ? "bound function should use explicit newTarget if provided" : "constructed object should be instance of original function");
74+
assert.areEqual(expectedPrototype.prototype.protoVal, obj.localVal, "prototype should be available during construction");
75+
}
76+
3277
const tests = [
3378
{
3479
name : "Construct trapped bound proxy with new",
3580
body : function() {
36-
constructionCount = 0;
37-
functionCallCount = 0;
38-
const obj = new boundTrapped("prop-value");
39-
assert.areNotEqual(boundObject, obj, "bound function should ignore bound this when constructing");
40-
assert.areEqual("prop-value", obj["prop-name"], "bound function should keep bound arguments when constructing");
41-
assert.areEqual(1, constructionCount, "bound proxy should be constructed once");
42-
assert.areEqual(1, functionCallCount, "bound proxy function should be called once");
43-
assert.strictEqual(a.prototype, obj.__proto__, "constructed object should be instance of original function");
81+
test(boundTrapped, false, a, 1, 1, 0);
4482
}
4583
},
4684
{
4785
name : "Construct trapped bound proxy with Reflect",
4886
body : function() {
49-
class newTarget {}
50-
constructionCount = 0;
51-
functionCallCount = 0;
52-
const obj = Reflect.construct(boundTrapped, ["prop-value-2"], newTarget);
53-
assert.areNotEqual(boundObject, obj, "bound function should ignore bound this when constructing");
54-
assert.strictEqual(newTarget.prototype, obj.__proto__, "bound function should use explicit newTarget if provided");
55-
assert.areEqual("prop-value-2", obj["prop-name"], "bound function should keep bound arguments when constructing");
56-
assert.areEqual(1, constructionCount, "bound proxy should be constructed once");
57-
assert.areEqual(1, functionCallCount, "bound proxy function should be called once");
87+
test(boundTrapped, true, newTarget, 1, 1, 0);
5888
}
5989
},
6090
{
6191
name : "Construct bound proxy with new",
6292
body : function() {
63-
functionCallCount = 0;
64-
const obj = new boundUnTrapped("prop-value");
65-
assert.areNotEqual(boundObject, obj, "bound function should ignore bound this when constructing");
66-
assert.areEqual("prop-value", obj["prop-name"], "bound function should keep bound arguments when constructing");
67-
assert.areEqual(1, functionCallCount, "bound proxy function should be called once");
68-
assert.strictEqual(a.prototype, obj.__proto__, "constructed object should be instance of original function");
93+
test(boundUnTrapped, false, a, 0, 1, 0);
6994
}
7095
},
7196
{
7297
name : "Construct bound proxy with Reflect",
7398
body : function() {
74-
class newTarget {}
75-
functionCallCount = 0;
76-
const obj = Reflect.construct(boundUnTrapped, ["prop-value-2"], newTarget);
77-
assert.areNotEqual(boundObject, obj, "bound function should ignore bound this when constructing");
78-
assert.strictEqual(newTarget.prototype, obj.__proto__, "bound function should use explicit newTarget if provided");
79-
assert.areEqual("prop-value-2", obj["prop-name"], "bound function should keep bound arguments when constructing");
80-
assert.areEqual(1, functionCallCount, "bound proxy function should be called once");
99+
test(boundUnTrapped, true, newTarget, 0, 1, 0);
81100
}
82101
},
83102
{
84103
name : "Construct bound function with Reflect",
85104
body : function() {
86-
class newTarget {}
87-
functionCallCount = 0;
88-
const obj = Reflect.construct(boundFunc, ["prop-value-2"], newTarget);
89-
assert.areNotEqual(boundObject, obj, "bound function should ignore bound this when constructing");
90-
assert.strictEqual(newTarget.prototype, obj.__proto__, "bound function should use explicit newTarget if provided");
91-
assert.areEqual("prop-value-2", obj["prop-name"], "bound function should keep bound arguments when constructing");
92-
assert.areEqual(1, functionCallCount, "bound function should be called once");
105+
test(boundFunc, true, newTarget, 0, 1, 0);
93106
}
94107
},
95108
{
96109
name : "Construct bound function with new",
97110
body : function() {
98-
functionCallCount = 0;
99-
const obj = new boundFunc("prop-value-2");
100-
assert.areNotEqual(boundObject, obj, "bound function should ignore bound this when constructing");
101-
assert.strictEqual(a.prototype, obj.__proto__, "bound function should use explicit newTarget if provided");
102-
assert.areEqual("prop-value-2", obj["prop-name"], "bound function should keep bound arguments when constructing");
103-
assert.areEqual(1, functionCallCount, "bound function should be called once");
111+
test(boundFunc, false, a, 0, 1, 0);
112+
}
113+
},
114+
{
115+
name : "Construct bound class with new",
116+
body : function() {
117+
test(boundClass, false, A, 0, 0, 1);
118+
}
119+
},
120+
{
121+
name : "Construct bound class with Reflect",
122+
body : function() {
123+
test(boundClass, true, newTarget, 0, 0, 1);
124+
}
125+
},
126+
{
127+
name : "Construct class extending bound function with new",
128+
body : function() {
129+
test(ExtendsBoundFunc, false, ExtendsBoundFunc, 0, 1, 0);
130+
}
131+
},
132+
{
133+
name : "Construct class extending bound function with Reflect",
134+
body : function() {
135+
test(ExtendsBoundFunc, true, newTarget, 0, 1, 0);
136+
}
137+
},
138+
{
139+
name : "Construct class extending bound class with new",
140+
body : function() {
141+
test(ExtendsBoundClass, false, ExtendsBoundClass, 0, 0, 1);
142+
}
143+
},
144+
{
145+
name : "Construct class extending bound class with Reflect",
146+
body : function() {
147+
test(ExtendsBoundClass, true, newTarget, 0, 0, 1);
148+
}
149+
},
150+
{
151+
name : "Construct bound class that extends a function with new",
152+
body : function() {
153+
test(boundClassExtendsFunc, false, ExtendsFunc, 0, 1, 0);
154+
}
155+
},
156+
{
157+
name : "Construct bound class that extends a function with Reflect",
158+
body : function() {
159+
test(boundClassExtendsFunc, true, newTarget, 0, 1, 0);
160+
}
161+
},
162+
{
163+
name : "Construct bound class that extends another class with new",
164+
body : function() {
165+
test(boundClassExtendsClass, false, ExtendsClass, 0, 0, 1);
166+
}
167+
},
168+
{
169+
name : "Construct bound class that extends another class with Reflect",
170+
body : function() {
171+
test(boundClassExtendsClass, true, newTarget, 0, 0, 1);
104172
}
105173
}
106174
];
107175

108-
testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" });
176+
testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" });

0 commit comments

Comments
 (0)