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

private-in-obj and integration between private and ?. #11586

Merged
merged 3 commits into from May 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
@@ -1,9 +1,3 @@
{
"args": [
"src",
"--out-dir",
"lib",
"--copy-files",
"--verbose"
]
"args": ["src", "--out-dir", "lib", "--copy-files", "--verbose"]
}
@@ -1,9 +1,3 @@
{
"args": [
"src",
"--out-dir",
"lib",
"--copy-files",
"--verbose"
]
"args": ["src", "--out-dir", "lib", "--copy-files", "--verbose"]
}
@@ -1,3 +1,9 @@
{
"args": ["--out-file", "script2.js", "--no-comments", "--minified", "script.js"]
"args": [
"--out-file",
"script2.js",
"--no-comments",
"--minified",
"script.js"
]
}
51 changes: 44 additions & 7 deletions packages/babel-helper-create-class-features-plugin/src/features.js
Expand Up @@ -5,6 +5,7 @@ export const FEATURES = Object.freeze({
fields: 1 << 1,
privateMethods: 1 << 2,
decorators: 1 << 3,
privateIn: 1 << 4,
});

// We can't use a symbol because this needs to always be the same, even if
Expand All @@ -28,6 +29,39 @@ export function enableFeature(file, feature, loose) {
file.set(featuresKey, file.get(featuresKey) | feature);
if (loose) file.set(looseKey, file.get(looseKey) | feature);
}

if (
hasFeature(file, FEATURES.fields) &&
hasFeature(file, FEATURES.privateMethods) &&
isLoose(file, FEATURES.fields) !== isLoose(file, FEATURES.privateMethods)
) {
throw new Error(
"'loose' mode configuration must be the same for both @babel/plugin-proposal-class-properties " +
"and @babel/plugin-proposal-private-methods",
);
}

if (
hasFeature(file, FEATURES.fields) &&
hasFeature(file, FEATURES.privateIn) &&
isLoose(file, FEATURES.fields) !== isLoose(file, FEATURES.privateIn)
) {
throw new Error(
"'loose' mode configuration must be the same for both @babel/plugin-proposal-class-properties " +
"and @babel/plugin-proposal-private-property-in-object",
);
}

if (
hasFeature(file, FEATURES.privateMethods) &&
hasFeature(file, FEATURES.privateIn) &&
isLoose(file, FEATURES.privateMethods) !== isLoose(file, FEATURES.privateIn)
) {
throw new Error(
"'loose' mode configuration must be the same for both @babel/plugin-proposal-private-methods " +
"and @babel/plugin-proposal-private-property-in-object",
);
}
}

function hasFeature(file, feature) {
Expand Down Expand Up @@ -69,14 +103,17 @@ export function verifyUsedFeatures(path, file) {
}

if (
hasFeature(file, FEATURES.privateMethods) &&
hasFeature(file, FEATURES.fields) &&
isLoose(file, FEATURES.privateMethods) !== isLoose(file, FEATURES.fields)
path.isPrivateName() &&
path.parentPath.isBinaryExpression({
operator: "in",
left: path.node,
})
) {
throw path.buildCodeFrameError(
"'loose' mode configuration must be the same for both @babel/plugin-proposal-class-properties " +
"and @babel/plugin-proposal-private-methods",
);
if (!hasFeature(file, FEATURES.privateIn)) {
throw path.buildCodeFrameError(
"Private property in checks are not enabled.",
);
}
}

if (path.isProperty()) {
Expand Down
200 changes: 129 additions & 71 deletions packages/babel-helper-create-class-features-plugin/src/fields.js
Expand Up @@ -70,69 +70,108 @@ export function buildPrivateNamesNodes(privateNamesMap, loose, state) {
// Traverses the class scope, handling private name references. If an inner
// class redeclares the same private name, it will hand off traversal to the
// restricted visitor (which doesn't traverse the inner class's inner scope).
const privateNameVisitor = {
function privateNameVisitorFactory(visitor) {
const privateNameVisitor = {
...visitor,

Class(path) {
const { privateNamesMap } = this;
const body = path.get("body.body");

const visiblePrivateNames = new Map(privateNamesMap);
const redeclared = [];
for (const prop of body) {
if (!prop.isPrivate()) continue;
const { name } = prop.node.key.id;
visiblePrivateNames.delete(name);
redeclared.push(name);
}

// If the class doesn't redeclare any private fields, we can continue with
// our overall traversal.
if (!redeclared.length) {
return;
}

// This class redeclares some private field. We need to process the outer
// environment with access to all the outer privates, then we can process
// the inner environment with only the still-visible outer privates.
path.get("body").traverse(nestedVisitor, {
...this,
redeclared,
});
path.traverse(privateNameVisitor, {
...this,
privateNamesMap: visiblePrivateNames,
});

// We'll eventually hit this class node again with the overall Class
// Features visitor, which'll process the redeclared privates.
path.skipKey("body");
},
};

// Traverses the outer portion of a class, without touching the class's inner
// scope, for private names.
const nestedVisitor = traverse.visitors.merge([
{
...visitor,
},
environmentVisitor,
]);

return privateNameVisitor;
}

const privateNameVisitor = privateNameVisitorFactory({
PrivateName(path) {
const { privateNamesMap } = this;
const { privateNamesMap, redeclared } = this;
const { node, parentPath } = path;

if (!parentPath.isMemberExpression({ property: node })) return;
if (!privateNamesMap.has(node.id.name)) 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;

this.handle(parentPath);
},
});

Class(path) {
const { privateNamesMap } = this;
const body = path.get("body.body");
const privateInVisitor = privateNameVisitorFactory({
BinaryExpression(path) {
const { operator, left, right } = path.node;
if (operator !== "in") return;
if (!path.get("left").isPrivateName()) return;

const visiblePrivateNames = new Map(privateNamesMap);
const redeclared = [];
for (const prop of body) {
if (!prop.isPrivate()) continue;
const { name } = prop.node.key.id;
visiblePrivateNames.delete(name);
redeclared.push(name);
}
const { loose, privateNamesMap, redeclared } = this;
const { name } = left.id;

if (!privateNamesMap.has(name)) return;
if (redeclared && redeclared.includes(name)) return;

// If the class doesn't redeclare any private fields, we can continue with
// our overall traversal.
if (!redeclared.length) {
if (loose) {
const { id } = privateNamesMap.get(name);
path.replaceWith(template.expression.ast`
Object.prototype.hasOwnProperty.call(${right}, ${id})
`);
return;
}

// This class redeclares some private field. We need to process the outer
// environment with access to all the outer privates, then we can process
// the inner environment with only the still-visible outer privates.
path.get("body").traverse(privateNameNestedVisitor, {
...this,
redeclared,
});
path.traverse(privateNameVisitor, {
...this,
privateNamesMap: visiblePrivateNames,
});
const { id, static: isStatic } = privateNamesMap.get(name);

// We'll eventually hit this class node again with the overall Class
// Features visitor, which'll process the redeclared privates.
path.skipKey("body");
},
};
if (isStatic) {
path.replaceWith(template.expression.ast`${right} === ${this.classRef}`);
return;
}

// Traverses the outer portion of a class, without touching the class's inner
// scope, for private names.
const privateNameNestedVisitor = traverse.visitors.merge([
{
PrivateName(path) {
const { redeclared } = this;
const { name } = path.node.id;
if (redeclared.includes(name)) path.skip();
},
},
{
PrivateName: privateNameVisitor.PrivateName,
path.replaceWith(template.expression.ast`${id}.has(${right})`);
},
environmentVisitor,
]);
});

const privateNameHandlerSpec = {
memoise(member, count) {
Expand Down Expand Up @@ -261,23 +300,43 @@ const privateNameHandlerSpec = {
// The first access (the get) should do the memo assignment.
this.memoise(member, 1);

return optimiseCall(this.get(member), this.receiver(member), args);
return optimiseCall(this.get(member), this.receiver(member), args, false);
},

optionalCall(member, args) {
this.memoise(member, 1);

return optimiseCall(this.get(member), this.receiver(member), args, true);
},
};

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);
},

optionalCall(member, args) {
return t.optionalCallExpression(this.get(member), args, true);
},
};

Expand All @@ -291,21 +350,20 @@ 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,
});
}
memberExpressionToFunctions(body, privateNameVisitor, {
privateNamesMap,
classRef: ref,
file: state,
...handler,
});
body.traverse(privateInVisitor, {
privateNamesMap,
classRef: ref,
file: state,
loose,
});
}

function buildPrivateFieldInitLoose(ref, prop, privateNamesMap) {
Expand Down
@@ -1,9 +1,7 @@
{
"presets": [
["typescript"]
],
"presets": [["typescript"]],
"plugins": [
["proposal-decorators", { "decoratorsBeforeExport": true }],
["proposal-class-properties"]
]
}
}