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

Transform destructuring private #14304

Merged
merged 35 commits into from May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
284bcc1
misc fix
JLHwung Mar 1, 2022
32ec061
types: allow PrivateName in ObjectProperty
JLHwung Mar 1, 2022
e4e90e5
optimize: ignore completion record in static block
JLHwung Mar 1, 2022
36b6ae1
fix: push new binding from initializer out of function
JLHwung Mar 1, 2022
98dbb05
optimize: don't memoise RHS when in SequenceExpression
JLHwung Mar 1, 2022
cfc8c50
feat: expose buildObjectExcludingKeys
JLHwung Mar 1, 2022
65575c2
refactor: remove parameter property in constructor visitor
JLHwung Mar 2, 2022
9af24c7
implement transform
JLHwung Mar 1, 2022
291987a
feat: add destructuring-private to standalone
JLHwung Mar 1, 2022
14cd6df
Update packages/babel-plugin-proposal-destructuring-private/package.json
JLHwung Mar 25, 2022
f1bbd3b
Update packages/babel-plugin-proposal-destructuring-private/src/index.ts
JLHwung Mar 25, 2022
a0ca19b
fix: avoid reusing AST node
JLHwung Mar 28, 2022
9df6e7c
fix: wrap forStmt body in a new block when required
JLHwung Mar 28, 2022
1577980
fix: move unshiftForXStatementBody to transform-destructuring
JLHwung Mar 28, 2022
701ff4c
optimize: transpile array pattern from the first private element
JLHwung Mar 29, 2022
60f681f
optimize: transpile function params from the first destructuring private
JLHwung Mar 29, 2022
a607850
fix: rest should exclude key containing deep private destructuring
JLHwung Mar 29, 2022
66261a1
type: fine-tuned transform types
JLHwung Mar 29, 2022
828a9f0
refactor: simplify growRestExcludingKeys
JLHwung Mar 29, 2022
f8c059e
fix: catch param should be lowered to block scoped
JLHwung Mar 29, 2022
14d0309
optimize: skip memoising right for ArrayPattern
JLHwung Mar 29, 2022
73447bd
optimize: don't memoise non-root right when there is only one destruc…
JLHwung Mar 29, 2022
3a5848c
fix: mark the last statement in do block as completion
JLHwung Mar 29, 2022
d2b6815
fix: preserve function length when lowering params
JLHwung Apr 4, 2022
b5768c1
test: more stringent ordering test
JLHwung Apr 11, 2022
39a26f1
moves types import to plugin
JLHwung May 2, 2022
acbd8a6
fix typings
JLHwung May 2, 2022
d3da6fb
simplify typings
JLHwung May 2, 2022
993febb
Update packages/babel-plugin-proposal-destructuring-private/src/index.ts
JLHwung May 4, 2022
eeb77b6
crawl after catch clause transform
JLHwung May 4, 2022
c8d22de
review comments
JLHwung May 4, 2022
2a7b8bc
perf: avoid out-of-bound array read
JLHwung May 4, 2022
603192c
address review comment
JLHwung May 5, 2022
ce7d90d
chore: rename test case
JLHwung May 5, 2022
e57ff74
fix merging errors
JLHwung May 5, 2022
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 change: 1 addition & 0 deletions Gulpfile.mjs
Expand Up @@ -459,6 +459,7 @@ function copyDts(packages) {

const libBundles = [
"packages/babel-parser",
"packages/babel-plugin-proposal-destructuring-private",
"packages/babel-plugin-proposal-object-rest-spread",
"packages/babel-plugin-proposal-optional-chaining",
"packages/babel-preset-react",
Expand Down
Expand Up @@ -181,7 +181,7 @@ export function shouldTransform(path: NodePath<t.Class>, file: File): boolean {
if (privateMethodPath && !hasFeature(file, FEATURES.privateMethods)) {
throw privateMethodPath.buildCodeFrameError(
"Class private methods are not enabled. " +
"Please add `@babel/plugin-proposal-private-method` to your configuration.",
"Please add `@babel/plugin-proposal-private-methods` to your configuration.",
);
}

Expand Down
6 changes: 5 additions & 1 deletion packages/babel-helper-define-map/src/index.ts
Expand Up @@ -121,7 +121,11 @@ export function toComputedObjectFromClass(obj: any) {
const prop = obj.properties[i];
const val = prop.value;
val.properties.unshift(
objectProperty(identifier("key"), toComputedKey(prop)),
objectProperty(
identifier("key"),
// @ts-expect-error toComputedObjectFromClass is not used, maybe we can remove it
toComputedKey(prop),
),
);
objExpr.elements.push(val);
}
Expand Down
@@ -1,7 +1,7 @@
{
"name": "@babel/plugin-proposal-class-static-block",
"version": "7.17.12",
"description": "Allow parsing of class static blocks",
"description": "Transform class static blocks",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
Expand Down
@@ -0,0 +1,3 @@
src
test
*.log
19 changes: 19 additions & 0 deletions packages/babel-plugin-proposal-destructuring-private/README.md
@@ -0,0 +1,19 @@
# @babel/plugin-proposal-destructuring-private

> Transform destructuring private proposal
See our website [@babel/plugin-proposal-destructuring-private](https://babeljs.io/docs/en/babel-plugin-proposal-destructuring-private) for more information.

## Install

Using npm:

```sh
npm install --save-dev @babel/plugin-proposal-destructuring-private
```

or using yarn:

```sh
yarn add @babel/plugin-proposal-destructuring-private --dev
```
42 changes: 42 additions & 0 deletions packages/babel-plugin-proposal-destructuring-private/package.json
@@ -0,0 +1,42 @@
{
"name": "@babel/plugin-proposal-destructuring-private",
"version": "0.0.0",
"description": "Transform destructuring private proposal",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-plugin-proposal-destructuring-private"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"keywords": [
"babel-plugin"
],
"dependencies": {
"@babel/helper-plugin-utils": "workspace:^",
"@babel/plugin-syntax-destructuring-private": "workspace:^",
"@babel/plugin-transform-destructuring": "workspace:^",
"@babel/plugin-transform-parameters": "workspace:^"
},
"peerDependencies": {
"@babel/core": "^7.17.0"
},
"devDependencies": {
"@babel/core": "workspace:^",
"@babel/helper-plugin-test-runner": "workspace:^",
"@babel/traverse": "workspace:^",
"@babel/types": "workspace:^"
},
"homepage": "https://babel.dev/docs/en/next/babel-plugin-proposal-destructuring-private",
"engines": {
"node": ">=6.9.0"
},
"author": "The Babel Team (https://babel.dev/team)",
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json"
}
}
214 changes: 214 additions & 0 deletions packages/babel-plugin-proposal-destructuring-private/src/index.ts
@@ -0,0 +1,214 @@
import { declare } from "@babel/helper-plugin-utils";
import syntaxDestructuringPrivate from "@babel/plugin-syntax-destructuring-private";
import {
hasPrivateKeys,
hasPrivateClassElement,
transformPrivateKeyDestructuring,
buildVariableDeclarationFromParams,
} from "./util";
import { convertFunctionParams } from "@babel/plugin-transform-parameters";
import { unshiftForXStatementBody } from "@babel/plugin-transform-destructuring";

import type { PluginPass } from "@babel/core";
import type { Visitor } from "@babel/traverse";

export default declare(function ({ assertVersion, assumption, types: t }) {
assertVersion("^7.17.0");
const {
assignmentExpression,
assignmentPattern,
cloneNode,
expressionStatement,
isExpressionStatement,
isIdentifier,
isSequenceExpression,
sequenceExpression,
variableDeclaration,
variableDeclarator,
} = t;

const ignoreFunctionLength = assumption("ignoreFunctionLength");
const objectRestNoSymbols = assumption("objectRestNoSymbols");

const privateKeyDestructuringVisitor: Visitor<PluginPass> = {
Function(path) {
// (b, { #x: x } = I) => body
// transforms to:
// (b, p1) => { var { #x: x } = p1 === undefined ? I : p1; body; }
const firstPrivateIndex = path.node.params.findIndex(param =>
hasPrivateKeys(param),
);
if (firstPrivateIndex === -1) return;
// wrap function body within IIFE if any param is shadowed
convertFunctionParams(path, ignoreFunctionLength, () => false, false);
// invariant: path.body is always a BlockStatement after `convertFunctionParams`
const { node, scope } = path;
const { params } = node;
const firstAssignmentPatternIndex = ignoreFunctionLength
? -1
: params.findIndex(param => param.type === "AssignmentPattern");
const paramsAfterIndex = params.splice(firstPrivateIndex);
const { params: transformedParams, variableDeclaration } =
buildVariableDeclarationFromParams(paramsAfterIndex, scope);

path.get("body").unshiftContainer("body", variableDeclaration);
params.push(...transformedParams);
// preserve function.length
// (b, p1) => {}
// transforms to
// (b, p1 = void 0) => {}
if (firstAssignmentPatternIndex >= firstPrivateIndex) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Since this is the only real use for firstAssignmentPatternIndex, wouldn't it be clearer to check !ignoreFunctionLength && … and then do the specific assignment code here?

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 can not assign the firstAssignmentPatternIndex here because the transform will convert any params after the first private destructuring. In other words, firstAssignmentPatternIndex will always be less than firstPrivateIndex if we assign firstAssignmentPatternIndex here.

params[firstAssignmentPatternIndex] = assignmentPattern(
// @ts-ignore The transformed assignment pattern must not be a RestElement
params[firstAssignmentPatternIndex],
scope.buildUndefinedNode(),
);
}
scope.crawl();
// the pattern will be handled by VariableDeclaration visitor.
},
CatchClause(path) {
// catch({ #x: x }) { body }
// transforms to:
// catch(_e) { var {#x: x } = _e; body }
const { node, scope } = path;
if (!hasPrivateKeys(node.param)) return;
// todo: handle shadowed param as we did in convertFunctionParams
const ref = scope.generateUidIdentifier("e");
path
.get("body")
.unshiftContainer(
"body",
variableDeclaration("let", [variableDeclarator(node.param, ref)]),
);
node.param = cloneNode(ref);
scope.crawl();
// the pattern will be handled by VariableDeclaration visitor.
JLHwung marked this conversation as resolved.
Show resolved Hide resolved
},
ForXStatement(path) {
const { node, scope } = path;
const leftPath = path.get("left");
if (leftPath.isVariableDeclaration()) {
const left = leftPath.node;
if (!hasPrivateKeys(left.declarations[0].id)) return;
// for (const { #x: x } of cls) body;
// transforms to:
// for (const ref of cls) { const { #x: x } = ref; body; }
// todo: the transform here assumes that any expression within
// the destructuring pattern (`{ #x: x }`), when evluated, do not interfere
// with the iterator of cls. Otherwise we have to pause the iterator and
// interleave the expressions.
// See also https://gist.github.com/nicolo-ribaudo/f8ac7916f89450f2ead77d99855b2098
const temp = scope.generateUidIdentifier("ref");
node.left = variableDeclaration(left.kind, [
variableDeclarator(temp, null),
]);
left.declarations[0].init = cloneNode(temp);
unshiftForXStatementBody(path, [left]);
scope.crawl();
// the pattern will be handled by VariableDeclaration visitor.
} else if (leftPath.isPattern()) {
if (!hasPrivateKeys(leftPath.node)) return;
// for ({ #x: x } of cls);
// transforms to:
// for (const ref of cls) { ({ #x: x } = ref); body; }
// This transform assumes that any expression within the pattern
// does not interfere with the iterable `cls`.
const temp = scope.generateUidIdentifier("ref");
node.left = variableDeclaration("const", [
variableDeclarator(temp, null),
]);
const assignExpr = expressionStatement(
assignmentExpression("=", leftPath.node, cloneNode(temp)),
);
unshiftForXStatementBody(path, [assignExpr]);
scope.crawl();
}
},
VariableDeclaration(path, state) {
const { scope, node } = path;
const { declarations } = node;
if (!declarations.some(declarator => hasPrivateKeys(declarator.id))) {
return;
}
const newDeclarations = [];
for (const declarator of declarations) {
for (const { left, right } of transformPrivateKeyDestructuring(
// @ts-expect-error The id of a variable declarator must not be a RestElement
declarator.id,
declarator.init,
scope,
/* isAssignment */ false,
/* shouldPreserveCompletion */ false,
name => state.addHelper(name),
objectRestNoSymbols,
/* useBuiltIns */ true,
)) {
newDeclarations.push(variableDeclarator(left, right));
}
}
node.declarations = newDeclarations;
scope.crawl();
},

AssignmentExpression(path, state) {
const { node, scope, parent } = path;
if (!hasPrivateKeys(node.left)) return;
const assignments = [];
const shouldPreserveCompletion =
(!isExpressionStatement(parent) && !isSequenceExpression(parent)) ||
path.isCompletionRecord();
Comment on lines +158 to +160
Copy link
Member

Choose a reason for hiding this comment

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

Why do we exclude ExpressionStatement and SequenceExpression?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is copied from

if (path.isCompletionRecord() || !path.parentPath.isExpressionStatement()) {

NodePath#isCompletionRecord essentially checks alongside the ancestry if the path is the last item of its container. This approach works for ExpressionStatement in Block and expression in SequenceExpression.

for (const { left, right } of transformPrivateKeyDestructuring(
// @ts-expect-error The left of an assignment expression must not be a RestElement
node.left,
node.right,
scope,
/* isAssignment */ true,
shouldPreserveCompletion,
name => state.addHelper(name),
objectRestNoSymbols,
/* useBuiltIns */ true,
)) {
assignments.push(assignmentExpression("=", left, right));
}
// preserve completion record
if (shouldPreserveCompletion) {
const { left, right } = assignments[0];
// If node.right is right and left is an identifier, then the left is an effectively-constant memoised id
if (isIdentifier(left) && right === node.right) {
if (
!isIdentifier(assignments[assignments.length - 1].right, {
name: left.name,
})
) {
// If the last assignment does not end with left, then we push `left` as the completion value
assignments.push(cloneNode(left));
}
// do nothing as `left` is already at the end of assignments
} else {
const tempId = scope.generateDeclaredUidIdentifier("m");
assignments.unshift(
assignmentExpression("=", tempId, cloneNode(node.right)),
);
assignments.push(cloneNode(tempId));
}
}

path.replaceWith(sequenceExpression(assignments));
scope.crawl();
},
};

const visitor: Visitor<PluginPass> = {
Class(path, state) {
if (!hasPrivateClassElement(path.node.body)) return;
path.traverse(privateKeyDestructuringVisitor, state);
},
};

return {
name: "proposal-destructuring-private",
inherits: syntaxDestructuringPrivate,
visitor: visitor,
};
});