Skip to content

Commit

Permalink
fix: support mutated outer decorated class binding (#16387)
Browse files Browse the repository at this point in the history
* fix: support mutated outer decorated class binding

* enable es2015 test and add todo item

* make node 6 happy
  • Loading branch information
JLHwung committed Mar 28, 2024
1 parent f0a63db commit 91f55bf
Show file tree
Hide file tree
Showing 9 changed files with 1,041 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -896,30 +896,6 @@ function createPrivateBrandCheckClosure(brandName: t.PrivateName) {
);
}

// Check if the expression does not reference function-specific
// context or the given identifier name.
// `true` means "maybe" and `false` means "no".
function usesFunctionContextOrYieldAwait(expression: t.Node) {
try {
t.traverseFast(expression, node => {
if (
t.isThisExpression(node) ||
t.isSuper(node) ||
t.isYieldExpression(node) ||
t.isAwaitExpression(node) ||
t.isIdentifier(node, { name: "arguments" }) ||
(t.isMetaProperty(node) && node.meta.name !== "import")
) {
// TODO: Add early return support to t.traverseFast
throw null;
}
});
return false;
} catch {
return true;
}
}

function usesPrivateField(expression: t.Node) {
try {
t.traverseFast(expression, node => {
Expand Down Expand Up @@ -1054,6 +1030,32 @@ function transformClass(

let protoInitLocal: t.Identifier;
let staticInitLocal: t.Identifier;
const classIdName = path.node.id?.name;
// Check if the expression does not reference function-specific
// context or the given identifier name.
// `true` means "maybe" and `false` means "no".
const usesFunctionContextOrYieldAwait = (expression: t.Node) => {
try {
t.traverseFast(expression, node => {
if (
t.isThisExpression(node) ||
t.isSuper(node) ||
t.isYieldExpression(node) ||
t.isAwaitExpression(node) ||
t.isIdentifier(node, { name: "arguments" }) ||
(classIdName && t.isIdentifier(node, { name: classIdName })) ||
(t.isMetaProperty(node) && node.meta.name !== "import")
) {
// TODO: Add early return support to t.traverseFast
throw null;
}
});
return false;
} catch {
return true;
}
};

const instancePrivateNames: string[] = [];
// Iterate over the class to see if we need to decorate it, and also to
// transform simple auto accessors which are not decorated, and handle inferred
Expand Down Expand Up @@ -1985,11 +1987,47 @@ function transformClass(
path.insertBefore(classAssignments.map(expr => t.expressionStatement(expr)));

if (needsDeclaraionForClassBinding) {
path.insertBefore(
t.variableDeclaration("let", [
t.variableDeclarator(t.cloneNode(classIdLocal)),
]),
);
const classBindingInfo = scopeParent.getBinding(classIdLocal.name);
if (!classBindingInfo.constantViolations.length) {
// optimization: reuse the inner class binding if the outer class binding is not mutated
path.insertBefore(
t.variableDeclaration("let", [
t.variableDeclarator(t.cloneNode(classIdLocal)),
]),
);
} else {
const classOuterBindingDelegateLocal = scopeParent.generateUidIdentifier(
"t" + classIdLocal.name,
);
const classOuterBindingLocal = classIdLocal;
path.replaceWithMultiple([
t.variableDeclaration("let", [
t.variableDeclarator(t.cloneNode(classOuterBindingLocal)),
t.variableDeclarator(classOuterBindingDelegateLocal),
]),
t.blockStatement([
t.variableDeclaration("let", [
t.variableDeclarator(t.cloneNode(classIdLocal)),
]),
// needsDeclaraionForClassBinding is true ↔ node is a class declaration
path.node as t.ClassDeclaration,
t.expressionStatement(
t.assignmentExpression(
"=",
t.cloneNode(classOuterBindingDelegateLocal),
t.cloneNode(classIdLocal),
),
),
]),
t.expressionStatement(
t.assignmentExpression(
"=",
t.cloneNode(classOuterBindingLocal),
t.cloneNode(classOuterBindingDelegateLocal),
),
),
]);
}
}

if (decoratedPrivateMethods.size > 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
{
"class binding in plain class, decorated field, and computed keys"
const errs = [];
const fns = [];
const capture = function (fn) {
fns.push(fn);
return () => {}
}
const assertUninitialized = function (fn) {
try {
fn();
} catch (err) {
errs.push(err);
} finally {
return () => {}
}
}

capture(() => K);
assertUninitialized(() => K);

class K {
@capture(() => K) @assertUninitialized(() => K) [(capture(() => K), assertUninitialized(() => K))]
}

const E = ReferenceError;
expect(errs.map(e => e.constructor)).toEqual([E, E, E]);

const C = K;
// expect(fns.map(fn => fn())).toEqual([C, C, C]);
// todo: remove these three and enable the assertions above when we properly handle class tdz
expect(fns[0]()).toEqual(C);
expect(fns[1]).toThrow(E);
expect(fns[2]).toThrow(E);

K = null;

// expect(fns.map(fn => fn())).toEqual([null, C, C]);
// todo: remove these three and enable the assertions above when we properly handle class tdz
expect(fns[0]()).toEqual(null);
expect(fns[1]).toThrow(E);
expect(fns[2]).toThrow(E);
}

{
"class binding in decorated class, decorated field, and computed keys"
const errs = [];
const fns = [];
const capture = function (fn) {
fns.push(fn);
return () => {}
}
const assertUninitialized = function (fn) {
try {
fn();
} catch (err) {
errs.push(err);
} finally {
return () => {}
}
}

@capture(() => K)
@assertUninitialized(() => K)
class K {
//todo: add the assertUninitialized decorator when we properly implement class tdz
@capture(() => K) [capture(() => K)]
}

const E = ReferenceError;
expect(errs.map(e => e.constructor)).toEqual([E]);

const C = K;
expect(fns.map(fn => fn())).toEqual([C, C, C]);

[K = null] = [];

expect(fns.map(fn => fn())).toEqual([null, C, C]);
}

{
"class binding in decorated class, decorated static field, and computed keys"
const errs = [];
const fns = [];
const capture = function (fn) {
fns.push(fn);
return () => {}
}
const assertUninitialized = function (fn) {
try {
fn();
} catch (err) {
errs.push(err);
} finally {
return () => {}
}
}

@capture(() => K)
@assertUninitialized(() => K)
class K {
//todo: add the assertUninitialized decorator when we properly implement class tdz
@capture(() => K) static [capture(() => K)]
}

const E = ReferenceError;
expect(errs.map(e => e.constructor)).toEqual([E]);

const C = K;
expect(fns.map(fn => fn())).toEqual([C, C, C]);

({ K = null } = {});

expect(fns.map(fn => fn())).toEqual([null, C, C]);
}

{
"class binding in decorated class, decorated static method, and computed keys with await";
(async () => {
const errs = [];
const fns = [];
const capture = function (fn) {
fns.push(fn);
return () => {}
}
const assertUninitialized = function (fn) {
try {
fn();
} catch (err) {
errs.push(err);
} finally {
return () => {}
}
}

@capture(await (() => K))
@assertUninitialized(await (() => K))
class K {
//todo: add the assertUninitialized decorator when we properly implement class tdz
@capture(await (() => K)) static [capture(await (() => K))]() {}
}

const E = ReferenceError;
expect(errs.map(e => e.constructor)).toEqual([E]);

const C = K;
expect(fns.map(fn => fn())).toEqual([C, C, C]);

[K] = [null];

expect(fns.map(fn => fn())).toEqual([null, C, C]);
})()
}

0 comments on commit 91f55bf

Please sign in to comment.