Skip to content

Commit

Permalink
Fix decorated class static field private access (#16344)
Browse files Browse the repository at this point in the history
  • Loading branch information
JLHwung committed Mar 14, 2024
1 parent c04eccf commit dc891b6
Show file tree
Hide file tree
Showing 34 changed files with 1,769 additions and 15 deletions.
167 changes: 153 additions & 14 deletions packages/babel-helper-create-class-features-plugin/src/decorators.ts
Expand Up @@ -836,6 +836,18 @@ function staticBlockToIIFE(block: t.StaticBlock) {
);
}

function staticBlockToFunctionClosure(block: t.StaticBlock) {
return t.functionExpression(null, [], t.blockStatement(block.body));
}

function fieldInitializerToClosure(value: t.Expression) {
return t.functionExpression(
null,
[],
t.blockStatement([t.returnStatement(value)]),
);
}

function maybeSequenceExpression(exprs: t.Expression[]) {
if (exprs.length === 0) return t.unaryExpression("void", t.numericLiteral(0));
if (exprs.length === 1) return exprs[0];
Expand Down Expand Up @@ -936,6 +948,31 @@ function convertToComputedKey(path: NodePath<t.ClassProperty | t.ClassMethod>) {
}
}

function hasInstancePrivateAccess(path: NodePath, privateNames: string[]) {
let containsInstancePrivateAccess = false;
if (privateNames.length > 0) {
const privateNameVisitor = privateNameVisitorFactory<
PrivateNameVisitorState<null>,
null
>({
PrivateName(path, state) {
if (state.privateNamesMap.has(path.node.id.name)) {
containsInstancePrivateAccess = true;
path.stop();
}
},
});
const privateNamesMap = new Map<string, null>();
for (const name of privateNames) {
privateNamesMap.set(name, null);
}
path.traverse(privateNameVisitor, {
privateNamesMap: privateNamesMap,
});
}
return containsInstancePrivateAccess;
}

function checkPrivateMethodUpdateError(
path: NodePath<t.Class>,
decoratedPrivateMethods: Set<string>,
Expand Down Expand Up @@ -987,9 +1024,10 @@ function transformClass(
path: NodePath<t.Class>,
state: PluginPass,
constantSuper: boolean,
version: DecoratorVersionKind,
ignoreFunctionLength: boolean,
className: string | t.Identifier | t.StringLiteral | undefined,
propertyVisitor: Visitor<PluginPass>,
version: DecoratorVersionKind,
): NodePath {
const body = path.get("body.body");

Expand All @@ -1014,6 +1052,7 @@ function transformClass(

let protoInitLocal: t.Identifier;
let staticInitLocal: t.Identifier;
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
// class name when the initializer of the class field is a class expression
Expand All @@ -1022,8 +1061,14 @@ function transformClass(
continue;
}

if (isDecorated(element.node)) {
switch (element.node.type) {
const elementNode = element.node;

if (!elementNode.static && t.isPrivateName(elementNode.key)) {
instancePrivateNames.push(elementNode.key.id.name);
}

if (isDecorated(elementNode)) {
switch (elementNode.type) {
case "ClassProperty":
// @ts-expect-error todo: propertyVisitor.ClassProperty should be callable. Improve typings.
propertyVisitor.ClassProperty(
Expand All @@ -1049,7 +1094,7 @@ function transformClass(
}
/* fallthrough */
default:
if (element.node.static) {
if (elementNode.static) {
staticInitLocal ??= generateLetUidIdentifier(
scopeParent,
"initStatic",
Expand All @@ -1063,16 +1108,16 @@ function transformClass(
break;
}
hasElementDecorators = true;
elemDecsUseFnContext ||= element.node.decorators.some(
elemDecsUseFnContext ||= elementNode.decorators.some(
usesFunctionContextOrYieldAwait,
);
} else if (element.node.type === "ClassAccessorProperty") {
} else if (elementNode.type === "ClassAccessorProperty") {
// @ts-expect-error todo: propertyVisitor.ClassAccessorProperty should be callable. Improve typings.
propertyVisitor.ClassAccessorProperty(
element as NodePath<t.ClassAccessorProperty>,
state,
);
const { key, value, static: isStatic, computed } = element.node;
const { key, value, static: isStatic, computed } = elementNode;

const newId = generateClassPrivateUid();
const newField = generateClassProperty(newId, value, isStatic);
Expand Down Expand Up @@ -1616,6 +1661,7 @@ function transformClass(
let originalClassPath = path;
const originalClass = path.node;

const staticClosures: t.AssignmentExpression[] = [];
if (classDecorators) {
classLocals.push(classIdLocal, classInitLocal);
const statics: (
Expand All @@ -1627,26 +1673,112 @@ function transformClass(
// Static blocks cannot be compiled to "instance blocks", but we can inline
// them as IIFEs in the next property.
if (element.isStaticBlock()) {
staticFieldInitializerExpressions.push(staticBlockToIIFE(element.node));
if (hasInstancePrivateAccess(element, instancePrivateNames)) {
const staticBlockClosureId = memoiseExpression(
staticBlockToFunctionClosure(element.node),
"staticBlock",
staticClosures,
);
staticFieldInitializerExpressions.push(
t.callExpression(
t.memberExpression(staticBlockClosureId, t.identifier("call")),
[t.thisExpression()],
),
);
} else {
staticFieldInitializerExpressions.push(
staticBlockToIIFE(element.node),
);
}
element.remove();
return;
}

const isProperty =
element.isClassProperty() || element.isClassPrivateProperty();

if (
(isProperty || element.isClassPrivateMethod()) &&
(element.isClassProperty() || element.isClassPrivateProperty()) &&
element.node.static
) {
if (isProperty && staticFieldInitializerExpressions.length > 0) {
const valuePath = (
element as NodePath<t.ClassProperty | t.ClassPrivateProperty>
).get("value");
if (hasInstancePrivateAccess(valuePath, instancePrivateNames)) {
const fieldValueClosureId = memoiseExpression(
fieldInitializerToClosure(valuePath.node),
"fieldValue",
staticClosures,
);
valuePath.replaceWith(
t.callExpression(
t.memberExpression(fieldValueClosureId, t.identifier("call")),
[t.thisExpression()],
),
);
}
if (staticFieldInitializerExpressions.length > 0) {
prependExpressionsToFieldInitializer(
staticFieldInitializerExpressions,
element,
);
staticFieldInitializerExpressions = [];
}
element.node.static = false;
statics.push(element.node);
element.remove();
} else if (element.isClassPrivateMethod({ static: true })) {
// At this moment the element must not have decorators, so any private name
// within the element must come from either params or body
if (hasInstancePrivateAccess(element, instancePrivateNames)) {
const replaceSupers = new ReplaceSupers({
constantSuper,
methodPath: element,
objectRef: classIdLocal,
superRef: path.node.superClass,
file: state.file,
refToPreserve: classIdLocal,
});

replaceSupers.replace();

const privateMethodDelegateId = memoiseExpression(
createFunctionExpressionFromPrivateMethod(element.node),
element.get("key.id").node.name,
staticClosures,
);

if (ignoreFunctionLength) {
element.node.params = [t.restElement(t.identifier("arg"))];
element.node.body = t.blockStatement([
t.returnStatement(
t.callExpression(
t.memberExpression(
privateMethodDelegateId,
t.identifier("apply"),
),
[t.thisExpression(), t.identifier("arg")],
),
),
]);
} else {
element.node.params = element.node.params.map((p, i) => {
if (t.isRestElement(p)) {
return t.restElement(t.identifier("arg"));
} else {
return t.identifier("_" + i);
}
});
element.node.body = t.blockStatement([
t.returnStatement(
t.callExpression(
t.memberExpression(
privateMethodDelegateId,
t.identifier("apply"),
),
[t.thisExpression(), t.identifier("arguments")],
),
),
]);
}
}
element.node.static = false;
statics.push(element.node);
element.remove();
Expand Down Expand Up @@ -1806,6 +1938,11 @@ function transformClass(
),
);
}
if (staticClosures.length > 0) {
applyDecsBody.push(
...staticClosures.map(expr => t.expressionStatement(expr)),
);
}

// When path is a ClassExpression, path.insertBefore will convert `path`
// into a SequenceExpression
Expand Down Expand Up @@ -2170,6 +2307,7 @@ export default function (

const VISITED = new WeakSet<NodePath>();
const constantSuper = assumption("constantSuper") ?? loose;
const ignoreFunctionLength = assumption("ignoreFunctionLength") ?? loose;

const namedEvaluationVisitor: Visitor<PluginPass> =
NamedEvaluationVisitoryFactory(
Expand All @@ -2189,9 +2327,10 @@ export default function (
path,
state,
constantSuper,
version,
ignoreFunctionLength,
className,
namedEvaluationVisitor,
version,
);
if (newPath) {
VISITED.add(newPath);
Expand Down
@@ -0,0 +1,66 @@
{
let hasX, getX, setX, hasA, getA, setA, hasM, callM, staticThis, OriginalFoo;

class Base {
static id(v) { return v; }
}

class Bar extends Base {}

const dec = (Foo) => {
OriginalFoo = Foo;
return Bar;
};

@dec
class Foo extends class {} {
#x;
accessor #a;
#m() { return "#m" }

x;
accessor a;
m() {}

static #method() {
staticThis = super.id(this);
hasX = (o) => #x in o;
getX = (o) => o.#x;
setX = (o, v) => o.#x = v;
hasA = (o) => #a in o;
getA = (o) => o.#a;
setA = (o, v) => o.#a = v;
hasM = (o) => #m in o;
callM = (o) => o.#m();
};

static method() {
Foo.#method()
}
}

OriginalFoo.method();

const foo = new OriginalFoo();
const bar = new Foo();

expect(hasX(foo)).toBe(true);
expect(getX((setX(foo, "#x"), foo))).toBe("#x");
expect(hasA(foo)).toBe(true);
expect(getA((setA(foo, "#a"), foo))).toBe("#a");
expect(hasM(foo)).toBe(true);
expect(callM(foo)).toBe("#m");
expect(hasX(bar)).toBe(false);
expect(hasA(bar)).toBe(false);
expect(hasM(bar)).toBe(false);

expect(foo.hasOwnProperty("x")).toBe(true);
expect(bar.hasOwnProperty("x")).toBe(false);

expect(OriginalFoo.prototype.hasOwnProperty("a")).toBe(true);
expect(Bar.prototype.hasOwnProperty("a")).toBe(false);
expect(OriginalFoo.prototype.hasOwnProperty("m")).toBe(true);
expect(Bar.prototype.hasOwnProperty("m")).toBe(false);

expect(staticThis).toBe(Bar);
}
@@ -0,0 +1,25 @@
const dec = () => {};
let hasX, hasA, hasM;

class Base {
static id(v) { return v; }
}

@dec
class Foo extends Base {
#x;
accessor #a;
#m() {}

x;
accessor a;
m() {}

static #method() {
super.id(this);
hasX = o => #x in o;
hasA = o => #a in o;
hasM = o => #m in o;
}
}

@@ -0,0 +1,5 @@
{
"assumptions": {
"ignoreFunctionLength": true
}
}

0 comments on commit dc891b6

Please sign in to comment.