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

Convert proposal-object-rest-spread to TS #13948

Merged
merged 4 commits into from Nov 24, 2021
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 change: 1 addition & 0 deletions Gulpfile.mjs
Expand Up @@ -461,6 +461,7 @@ function copyDts(packages) {

const libBundles = [
"packages/babel-parser",
"packages/babel-plugin-proposal-object-rest-spread",
"packages/babel-plugin-proposal-optional-chaining",
"packages/babel-preset-react",
"packages/babel-preset-typescript",
Expand Down
@@ -1,21 +1,24 @@
import { declare } from "@babel/helper-plugin-utils";
import syntaxObjectRestSpread from "@babel/plugin-syntax-object-rest-spread";
import { types as t } from "@babel/core";
import type { PluginPass } from "@babel/core";
import type { NodePath, Visitor, Scope } from "@babel/traverse";
import { convertFunctionParams } from "@babel/plugin-transform-parameters";
import { isRequired } from "@babel/helper-compilation-targets";
import compatData from "@babel/compat-data/corejs2-built-ins";
import shouldStoreRHSInTemporaryVariable from "./shouldStoreRHSInTemporaryVariable";

// TODO: Remove in Babel 8
const { isAssignmentPattern, isObjectProperty } = t;
// @babel/types <=7.3.3 counts FOO as referenced in var { x: FOO }.
// We need to detect this bug to know if "unused" means 0 or 1 references.
const ZERO_REFS = (() => {
if (!process.env.BABEL_8_BREAKING) {
const node = t.identifier("a");
const property = t.objectProperty(t.identifier("key"), node);
const pattern = t.objectPattern([property]);

return t.isReferenced(node, property, pattern) ? 1 : 0;
})();
// eslint-disable-next-line no-var
var ZERO_REFS = t.isReferenced(node, property, pattern) ? 1 : 0;
}

export default declare((api, opts) => {
api.assertVersion(7);
Expand All @@ -36,7 +39,9 @@ export default declare((api, opts) => {
const pureGetters = api.assumption("pureGetters") ?? loose;
const setSpreadProperties = api.assumption("setSpreadProperties") ?? loose;

function getExtendsHelper(file) {
function getExtendsHelper(
file: PluginPass,
): t.MemberExpression | t.Identifier {
return useBuiltIns
? t.memberExpression(t.identifier("Object"), t.identifier("assign"))
: file.addHelper("extends");
Expand All @@ -51,7 +56,7 @@ export default declare((api, opts) => {
return foundRestElement;
}

function hasObjectPatternRestElement(path) {
function hasObjectPatternRestElement(path: NodePath): boolean {
let foundRestElement = false;
visitRestElements(path, restElement => {
if (restElement.parentPath.isObjectPattern()) {
Expand All @@ -62,15 +67,16 @@ export default declare((api, opts) => {
return foundRestElement;
}

function visitRestElements(path, visitor) {
function visitRestElements(
path: NodePath,
visitor: (path: NodePath<t.RestElement>) => any,
) {
path.traverse({
Expression(path) {
const parentType = path.parent.type;
const { parent, key } = path;
if (
(parentType === "AssignmentPattern" && path.key === "right") ||
(parentType === "ObjectProperty" &&
path.parent.computed &&
path.key === "key")
(isAssignmentPattern(parent) && key === "right") ||
(isObjectProperty(parent) && parent.computed && key === "key")
) {
path.skip();
}
Expand All @@ -79,7 +85,7 @@ export default declare((api, opts) => {
});
}

function hasSpread(node) {
function hasSpread(node: t.ObjectExpression): boolean {
for (const prop of node.properties) {
if (t.isSpreadElement(prop)) {
return true;
Expand All @@ -92,9 +98,10 @@ export default declare((api, opts) => {
// were converted to stringLiterals or not
// e.g. extracts {keys: ["a", "b", "3", ++x], allLiteral: false }
// from ast of {a: "foo", b, 3: "bar", [++x]: "baz"}
function extractNormalizedKeys(path) {
const props = path.node.properties;
const keys = [];
function extractNormalizedKeys(node: t.ObjectPattern) {
// RestElement has been removed in createObjectRest
const props = node.properties as t.ObjectProperty[];
const keys: t.Expression[] = [];
let allLiteral = true;
let hasTemplateLiteral = false;

Expand All @@ -106,7 +113,14 @@ export default declare((api, opts) => {
keys.push(t.cloneNode(prop.key));
hasTemplateLiteral = true;
} else if (t.isLiteral(prop.key)) {
keys.push(t.stringLiteral(String(prop.key.value)));
keys.push(
t.stringLiteral(
String(
//@ts-ignore prop.key can not be a NullLiteral
prop.key.value,
),
),
);
} else {
keys.push(t.cloneNode(prop.key));
allLiteral = false;
Expand All @@ -118,8 +132,11 @@ export default declare((api, opts) => {

// replaces impure computed keys with new identifiers
// and returns variable declarators of these new identifiers
function replaceImpureComputedKeys(properties, scope) {
const impureComputedPropertyDeclarators = [];
function replaceImpureComputedKeys(
properties: NodePath<t.ObjectProperty>[],
scope: Scope,
) {
const impureComputedPropertyDeclarators: t.VariableDeclarator[] = [];
for (const propPath of properties) {
const key = propPath.get("key");
if (propPath.node.computed && !key.isPure()) {
Expand All @@ -132,13 +149,14 @@ export default declare((api, opts) => {
return impureComputedPropertyDeclarators;
}

function removeUnusedExcludedKeys(path) {
function removeUnusedExcludedKeys(path: NodePath<t.ObjectPattern>): void {
const bindings = path.getOuterBindingIdentifierPaths();

Object.keys(bindings).forEach(bindingName => {
const bindingParentPath = bindings[bindingName].parentPath;
if (
path.scope.getBinding(bindingName).references > ZERO_REFS ||
path.scope.getBinding(bindingName).references >
(process.env.BABEL_8_BREAKING ? 0 : ZERO_REFS) ||
!bindingParentPath.isObjectProperty()
) {
return;
Expand All @@ -148,19 +166,24 @@ export default declare((api, opts) => {
}

//expects path to an object pattern
function createObjectRest(path, file, objRef) {
function createObjectRest(
path: NodePath<t.ObjectPattern>,
file: PluginPass,
objRef: t.Identifier | t.MemberExpression,
): [t.VariableDeclarator[], t.LVal, t.CallExpression] {
const props = path.get("properties");
const last = props[props.length - 1];
t.assertRestElement(last.node);
const restElement = t.cloneNode(last.node);
last.remove();

const impureComputedPropertyDeclarators = replaceImpureComputedKeys(
path.get("properties"),
path.get("properties") as NodePath<t.ObjectProperty>[],
path.scope,
);
const { keys, allLiteral, hasTemplateLiteral } =
extractNormalizedKeys(path);
const { keys, allLiteral, hasTemplateLiteral } = extractNormalizedKeys(
path.node,
);

if (keys.length === 0) {
return [
Expand Down Expand Up @@ -210,7 +233,11 @@ export default declare((api, opts) => {
];
}

function replaceRestElement(parentPath, paramPath, container) {
function replaceRestElement(
parentPath: NodePath<t.Function | t.CatchClause>,
paramPath: NodePath,
container?: t.VariableDeclaration[],
): void {
if (paramPath.isAssignmentPattern()) {
replaceRestElement(parentPath, paramPath.get("left"), container);
return;
Expand Down Expand Up @@ -299,7 +326,7 @@ export default declare((api, opts) => {
for (let i = 0; i < params.length; ++i) {
const param = params[i];
if (paramsWithRestElement.has(i)) {
replaceRestElement(param.parentPath, param);
replaceRestElement(path, param);
}
}
} else {
Expand Down Expand Up @@ -360,14 +387,15 @@ export default declare((api, opts) => {
}

let ref = originalPath.node.init;
const refPropertyPath = [];
const refPropertyPath: NodePath<t.ObjectProperty>[] = [];
let kind;

path.findParent(path => {
path.findParent((path: NodePath): boolean => {
if (path.isObjectProperty()) {
refPropertyPath.unshift(path);
} else if (path.isVariableDeclarator()) {
kind = path.parentPath.node.kind;
kind = (path.parentPath as NodePath<t.VariableDeclaration>).node
.kind;
return true;
}
});
Expand All @@ -385,12 +413,17 @@ export default declare((api, opts) => {
);
});

const objectPatternPath = path.findParent(path =>
path.isObjectPattern(),
//@ts-expect-error: findParent can not apply assertions on result shape
const objectPatternPath: NodePath<t.ObjectPattern> = path.findParent(
Copy link
Member

Choose a reason for hiding this comment

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

Not in this PR, but maybe something like this would work:

function findParent<T extends t.Node>(test: (path: NodePath<t.Node>) => path is NodePath<T>): T | null;

path => path.isObjectPattern(),
);

const [impureComputedPropertyDeclarators, argument, callExpression] =
createObjectRest(objectPatternPath, file, ref);
createObjectRest(
objectPatternPath,
file,
ref as t.MemberExpression,
);

if (pureGetters) {
removeUnusedExcludedKeys(objectPatternPath);
Expand All @@ -402,11 +435,9 @@ export default declare((api, opts) => {

insertionPath.insertBefore(impureObjRefComputedDeclarators);

insertionPath.insertAfter(
insertionPath = insertionPath.insertAfter(
t.variableDeclarator(argument, callExpression),
);

insertionPath = insertionPath.getSibling(insertionPath.key + 1);
)[0] as NodePath<t.VariableDeclarator>;

path.scope.registerBinding(kind, insertionPath);

Expand All @@ -433,7 +464,7 @@ export default declare((api, opts) => {

const specifiers = [];

for (const name of Object.keys(path.getOuterBindingIdentifiers(path))) {
for (const name of Object.keys(path.getOuterBindingIdentifiers(true))) {
specifiers.push(
t.exportSpecifier(t.identifier(name), t.identifier(name)),
);
Expand All @@ -449,7 +480,7 @@ export default declare((api, opts) => {
// try {} catch ({a, ...b}) {}
CatchClause(path) {
const paramPath = path.get("param");
replaceRestElement(paramPath.parentPath, paramPath);
replaceRestElement(path, paramPath);
},

// ({a, ...b} = c);
Expand Down Expand Up @@ -511,14 +542,15 @@ export default declare((api, opts) => {
]);

path.ensureBlock();
const body = node.body as t.BlockStatement;

if (node.body.body.length === 0 && path.isCompletionRecord()) {
node.body.body.unshift(
if (body.body.length === 0 && path.isCompletionRecord()) {
body.body.unshift(
t.expressionStatement(scope.buildUndefinedNode()),
);
}

node.body.body.unshift(
body.body.unshift(
t.expressionStatement(
t.assignmentExpression("=", left, t.cloneNode(temp)),
),
Expand All @@ -533,8 +565,9 @@ export default declare((api, opts) => {
]);

path.ensureBlock();
const body = node.body as t.BlockStatement;

node.body.body.unshift(
body.body.unshift(
t.variableDeclaration(node.left.kind, [
t.variableDeclarator(pattern, t.cloneNode(key)),
]),
Expand Down Expand Up @@ -565,11 +598,13 @@ export default declare((api, opts) => {

if (objectPatterns.length > 0) {
const statementPath = path.getStatementParent();
const statementNode = statementPath.node;
const kind =
statementNode.type === "VariableDeclaration"
? statementNode.kind
: "var";
statementPath.insertAfter(
t.variableDeclaration(
statementPath.node.kind || "var",
objectPatterns,
),
t.variableDeclaration(kind, objectPatterns),
);
}
},
Expand Down Expand Up @@ -627,7 +662,7 @@ export default declare((api, opts) => {
]);
}

for (const prop of (path.node.properties: Array)) {
for (const prop of path.node.properties) {
if (t.isSpreadElement(prop)) {
make();
exp.arguments.push(prop.argument);
Expand All @@ -640,6 +675,6 @@ export default declare((api, opts) => {

path.replaceWith(exp);
},
},
} as Visitor<PluginPass>,
};
});
@@ -1,23 +1,31 @@
import { types as t } from "@babel/core";

const { isObjectProperty } = t;
/**
* This is a helper function to determine if we should create an intermediate variable
* such that the RHS of an assignment is not duplicated.
*
* See https://github.com/babel/babel/pull/13711#issuecomment-914388382 for discussion
* on further optimizations.
*/
export default function shouldStoreRHSInTemporaryVariable(node) {
export default function shouldStoreRHSInTemporaryVariable(node: t.LVal) {
if (t.isArrayPattern(node)) {
const nonNullElements = node.elements.filter(element => element !== null);
if (nonNullElements.length > 1) return true;
else return shouldStoreRHSInTemporaryVariable(nonNullElements[0]);
} else if (t.isObjectPattern(node)) {
if (node.properties.length > 1) return true;
else if (node.properties.length === 0) return false;
else return shouldStoreRHSInTemporaryVariable(node.properties[0]);
} else if (t.isObjectProperty(node)) {
return shouldStoreRHSInTemporaryVariable(node.value);
const { properties } = node;
if (properties.length > 1) return true;
else if (properties.length === 0) return false;
else {
const firstProperty = properties[0];
if (isObjectProperty(firstProperty)) {
// the value of the property must be an LVal
return shouldStoreRHSInTemporaryVariable(firstProperty.value as t.LVal);
} else {
return shouldStoreRHSInTemporaryVariable(firstProperty);
}
}
} else if (t.isAssignmentPattern(node)) {
return shouldStoreRHSInTemporaryVariable(node.left);
} else if (t.isRestElement(node)) {
Expand Down
1 change: 1 addition & 0 deletions packages/babel-traverse/src/path/ancestry.ts
Expand Up @@ -12,6 +12,7 @@ import NodePath from "./index";
*/

export function findParent(
this: NodePath,
callback: (path: NodePath) => boolean,
): NodePath | null {
let path = this;
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-traverse/src/scope/index.ts
Expand Up @@ -983,7 +983,7 @@ export default class Scope {
init?: t.Expression;
unique?: boolean;
_blockHoist?: number | undefined;
kind?: "var" | "let";
kind?: "var" | "let" | "const";
}) {
let path = this.path;

Expand Down