Skip to content

Commit

Permalink
Handle private access chained on an optional chain
Browse files Browse the repository at this point in the history
See the resolution from tc39/proposal-class-fields#301.
  • Loading branch information
jridgewell authored and JLHwung committed May 21, 2020
1 parent b1b28b7 commit d8a38b8
Show file tree
Hide file tree
Showing 33 changed files with 2,827 additions and 32 deletions.
53 changes: 27 additions & 26 deletions packages/babel-helper-create-class-features-plugin/src/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,12 @@ const privateNameVisitor = privateNameVisitorFactory({
const { privateNamesMap, redeclared } = this;
const { node, parentPath } = path;

if (!parentPath.isMemberExpression({ property: node })) return;

if (
!parentPath.isMemberExpression({ property: node }) &&
!parentPath.isOptionalMemberExpression({ property: node })
) {
return;
}
const { name } = node.id;
if (!privateNamesMap.has(name)) return;
if (redeclared && redeclared.includes(name)) return;
Expand Down Expand Up @@ -301,18 +305,28 @@ const privateNameHandlerSpec = {
};

const privateNameHandlerLoose = {
handle(member) {
get(member) {
const { privateNamesMap, file } = this;
const { object } = member.node;
const { name } = member.node.property.id;

member.replaceWith(
template.expression`BASE(REF, PROP)[PROP]`({
BASE: file.addHelper("classPrivateFieldLooseBase"),
REF: object,
PROP: privateNamesMap.get(name).id,
}),
);
return template.expression`BASE(REF, PROP)[PROP]`({
BASE: file.addHelper("classPrivateFieldLooseBase"),
REF: object,
PROP: privateNamesMap.get(name).id,
});
},

simpleSet(member) {
return this.get(member);
},

destructureSet(member) {
return this.get(member);
},

call(member, args) {
return t.callExpression(this.get(member), args);
},
};

Expand All @@ -326,26 +340,13 @@ export function transformPrivateNamesUsage(
if (!privateNamesMap.size) return;

const body = path.get("body");
const handler = loose ? privateNameHandlerLoose : privateNameHandlerSpec;

if (loose) {
body.traverse(privateNameVisitor, {
privateNamesMap,
file: state,
...privateNameHandlerLoose,
});
} else {
memberExpressionToFunctions(body, privateNameVisitor, {
privateNamesMap,
classRef: ref,
file: state,
...privateNameHandlerSpec,
});
}
body.traverse(privateInVisitor, {
memberExpressionToFunctions(body, privateNameVisitor, {
privateNamesMap,
classRef: ref,
file: state,
loose,
...handler,
});
}

Expand Down
115 changes: 111 additions & 4 deletions packages/babel-helper-member-expression-to-functions/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,113 @@ const handle = {
handle(member) {
const { node, parent, parentPath } = member;

if (member.isOptionalMemberExpression()) {
const root = member.find(({ node, parent, parentPath }) => {
if (parentPath.isOptionalMemberExpression()) {
return parent.optional || parent.object !== node;
}
if (parentPath.isOptionalCallExpression()) {
return parent.optional || parent.callee !== node;
}
return true;
});

const rootParentPath = root.parentPath;
if (
rootParentPath.isUpdateExpression({ argument: node }) ||
rootParentPath.isAssignmentExpression({ left: node })
) {
throw member.buildCodeFrameError(`can't handle assignment`);
}
if (rootParentPath.isUnaryExpression({ operator: "delete" })) {
throw member.buildCodeFrameError(`can't handle delete`);
}

if (node.optional) {
throw member.buildCodeFrameError(
`can't handle '?.' directly before ${node.property.type}`,
);
}

let nearestOptional = member;
for (;;) {
if (nearestOptional.isOptionalMemberExpression()) {
if (nearestOptional.node.optional) break;
nearestOptional = nearestOptional.get("object");
continue;
} else if (nearestOptional.isOptionalCallExpression()) {
if (nearestOptional.node.optional) break;
nearestOptional = nearestOptional.get("object");
continue;
}

throw nearestOptional.buildCodeFrameError(
"failed to find nearest optional",
);
}

const { scope } = member;
const { object } = node;
const baseRef = scope.generateUidIdentifierBasedOnNode(
nearestOptional.node.object,
);
const valueRef = scope.generateUidIdentifierBasedOnNode(object);
scope.push({ id: baseRef });
scope.push({ id: valueRef });

nearestOptional
.get("object")
.replaceWith(
t.assignmentExpression("=", baseRef, nearestOptional.node.object),
);
member.replaceWith(t.memberExpression(valueRef, node.property));

if (parentPath.isOptionalCallExpression({ callee: node })) {
parentPath.replaceWith(this.call(member, parent.arguments));
} else {
member.replaceWith(this.get(member));
}

let regular = member.node;
for (let current = member; current !== root; ) {
const { parentPath, parent } = current;
if (parentPath.isOptionalMemberExpression()) {
regular = t.memberExpression(
regular,
parent.property,
parent.computed,
);
} else {
regular = t.callExpression(regular, parent.arguments);
}
current = parentPath;
}

root.replaceWith(
t.conditionalExpression(
t.sequenceExpression([
t.assignmentExpression("=", valueRef, object),
t.logicalExpression(
"||",
t.binaryExpression("===", baseRef, scope.buildUndefinedNode()),
t.binaryExpression("===", baseRef, t.nullLiteral()),
),
]),
scope.buildUndefinedNode(),
regular,
),
);
return;
}

// MEMBER++ -> _set(MEMBER, (_ref = (+_get(MEMBER))) + 1), _ref
// ++MEMBER -> _set(MEMBER, (+_get(MEMBER)) + 1)
if (parentPath.isUpdateExpression({ argument: node })) {
if (this.simpleSet) {
member.replaceWith(this.simpleSet(member));
return;
}

const { operator, prefix } = parent;

// Give the state handler a chance to memoise the member, since we'll
Expand Down Expand Up @@ -72,6 +176,11 @@ const handle = {
// MEMBER = VALUE -> _set(MEMBER, VALUE)
// MEMBER += VALUE -> _set(MEMBER, _get(MEMBER) + VALUE)
if (parentPath.isAssignmentExpression({ left: node })) {
if (this.simpleSet) {
member.replaceWith(this.simpleSet(member));
return;
}

const { operator, right } = parent;
let value = right;

Expand All @@ -92,11 +201,9 @@ const handle = {
return;
}

// MEMBER(ARGS) -> _call(MEMBER, ARGS)
// MEMBER(ARGS) -> _call(MEMBER, ARGS)
if (parentPath.isCallExpression({ callee: node })) {
const { arguments: args } = parent;

parentPath.replaceWith(this.call(member, args));
parentPath.replaceWith(this.call(member, parent.arguments));
return;
}

Expand Down
5 changes: 3 additions & 2 deletions packages/babel-parser/src/parser/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -616,15 +616,16 @@ export default class ExpressionParser extends LValParser {
node.object = base;
node.property = computed
? this.parseExpression()
: optional
? this.parseIdentifier(true)
: this.parseMaybePrivateName(true);
node.computed = computed;

if (node.property.type === "PrivateName") {
if (node.object.type === "Super") {
this.raise(startPos, Errors.SuperPrivateField);
}
if (optional) {
this.raise(node.property.start, Errors.OptionalChainingNoPrivate);
}
this.classScope.usePrivateName(
node.property.id.name,
node.property.start,
Expand Down
2 changes: 2 additions & 0 deletions packages/babel-parser/src/parser/location.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export const Errors = Object.freeze({
"await* has been removed from the async functions proposal. Use Promise.all() instead.",
OptionalChainingNoNew:
"constructors in/after an Optional Chain are not allowed",
OptionalChainingNoPrivate:
"Private property access cannot immediately follow an optional chain's `?.`",
OptionalChainingNoTemplate:
"Tagged Template Literals are not allowed in optionalChain",
ParamDupe: "Argument name clash",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Foo {
static #x = 1;
static #m = function() {};

static test() {
const o = { Foo: Foo };
return [
o?.Foo.#x,
o?.Foo.#x.toFixed,
o?.Foo.#x.toFixed(2),
o?.Foo.#m(),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": ["classPrivateProperties"]
}
Loading

0 comments on commit d8a38b8

Please sign in to comment.