Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support mutated outer decorated class binding #16387

Merged
merged 3 commits into from Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -894,30 +894,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 @@ -1052,6 +1028,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 @@ -1983,11 +1985,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([
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use a block statement to simulate the inner scope so that it can inherit the Yield and Await production parameter.

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
@@ -0,0 +1,145 @@
{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is disabled due to #16386.

"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]);

K = null;

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

{
"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]);
})()
}