From d48a5cb55c8780d891a4c6173f8c9521fa534a3e Mon Sep 17 00:00:00 2001 From: Bogdan Savluk Date: Tue, 10 Aug 2021 20:53:18 +0200 Subject: [PATCH] convert `@babel/plugin-transform-block-scoping` to typescript (#13219) --- .../package.json | 3 +- .../src/{index.js => index.ts} | 137 +++++++++++++----- .../src/{tdz.js => tdz.ts} | 21 ++- tsconfig.json | 4 + yarn.lock | 1 + 5 files changed, 129 insertions(+), 37 deletions(-) rename packages/babel-plugin-transform-block-scoping/src/{index.js => index.ts} (86%) rename packages/babel-plugin-transform-block-scoping/src/{tdz.js => tdz.ts} (77%) diff --git a/packages/babel-plugin-transform-block-scoping/package.json b/packages/babel-plugin-transform-block-scoping/package.json index 2a8da3f51a..51934af0ac 100644 --- a/packages/babel-plugin-transform-block-scoping/package.json +++ b/packages/babel-plugin-transform-block-scoping/package.json @@ -24,7 +24,8 @@ }, "devDependencies": { "@babel/core": "workspace:*", - "@babel/helper-plugin-test-runner": "workspace:*" + "@babel/helper-plugin-test-runner": "workspace:*", + "@babel/traverse": "workspace:*" }, "engines": { "node": ">=6.9.0" diff --git a/packages/babel-plugin-transform-block-scoping/src/index.js b/packages/babel-plugin-transform-block-scoping/src/index.ts similarity index 86% rename from packages/babel-plugin-transform-block-scoping/src/index.js rename to packages/babel-plugin-transform-block-scoping/src/index.ts index 421bbea7ae..f3ffbef6d0 100644 --- a/packages/babel-plugin-transform-block-scoping/src/index.js +++ b/packages/babel-plugin-transform-block-scoping/src/index.ts @@ -1,8 +1,9 @@ import { declare } from "@babel/helper-plugin-utils"; -import type NodePath from "@babel/traverse"; -import type Scope from "@babel/traverse"; +import type { NodePath, Visitor, Scope } from "@babel/traverse"; import { visitor as tdzVisitor } from "./tdz"; +import type { TDZVisitorState } from "./tdz"; import { traverse, template, types as t } from "@babel/core"; +import type { File } from "@babel/core"; const DONE = new WeakSet(); @@ -26,8 +27,9 @@ export default declare((api, opts) => { if (!isBlockScoped(node)) return; convertBlockScopedToVar(path, null, parent, scope, true); + // @ts-expect-error todo(flow->ts): avoid mutations if (node._tdzThis) { - const nodes = [node]; + const nodes: t.Node[] = [node]; for (let i = 0; i < node.declarations.length; i++) { const decl = node.declarations[i]; @@ -36,11 +38,13 @@ export default declare((api, opts) => { t.cloneNode(decl.id), decl.init || scope.buildUndefinedNode(), ); + // @ts-expect-error todo(flow->ts): avoid mutations assign._ignoreBlockScopingTDZ = true; nodes.push(t.expressionStatement(assign)); decl.init = this.addHelper("temporalUndefined"); } + // @ts-expect-error todo(flow->ts): avoid mutations node._blockHoist = 2; if (path.isCompletionRecord()) { @@ -83,7 +87,10 @@ export default declare((api, opts) => { blockScoping.run(); }, - "BlockStatement|SwitchStatement|Program"(path, state) { + "BlockStatement|SwitchStatement|Program"( + path: NodePath, + state, + ) { if (!ignoreBlock(path)) { const blockScoping = new BlockScoping( null, @@ -97,7 +104,7 @@ export default declare((api, opts) => { blockScoping.run(); } }, - }, + } as Visitor, }; }); @@ -161,10 +168,18 @@ function convertBlockScopedToVar( } } -function isVar(node) { +function isVar(node): node is t.VariableDeclaration { return t.isVariableDeclaration(node, { kind: "var" }) && !isBlockScoped(node); } +interface LetReferenceVisitorState extends TDZVisitorState { + tdzEnabled: boolean; + loopDepth: number; + addHelper: (name) => any; + letReferences: any; + closurify: boolean; +} + const letReferenceBlockVisitor = traverse.visitors.merge([ { Loop: { @@ -188,7 +203,7 @@ const letReferenceBlockVisitor = traverse.visitors.merge([ }, }, tdzVisitor, -]); +] as Visitor[]); const letReferenceFunctionVisitor = traverse.visitors.merge([ { @@ -207,14 +222,13 @@ const letReferenceFunctionVisitor = traverse.visitors.merge([ }, }, tdzVisitor, -]); +] as Visitor[]); -const hoistVarDeclarationsVisitor = { +const hoistVarDeclarationsVisitor: Visitor = { enter(path, self) { - const { node, parent } = path; - if (path.isForStatement()) { - if (isVar(node.init, node)) { + const { node } = path; + if (isVar(node.init)) { const nodes = self.pushDeclar(node.init); if (nodes.length === 1) { node.init = nodes[0]; @@ -222,14 +236,15 @@ const hoistVarDeclarationsVisitor = { node.init = t.sequenceExpression(nodes); } } - } else if (path.isFor()) { - if (isVar(node.left, node)) { + } else if (path.isForInStatement() || path.isForOfStatement()) { + const { node } = path; + if (isVar(node.left)) { self.pushDeclar(node.left); node.left = node.left.declarations[0].id; } - } else if (isVar(node, parent)) { + } else if (isVar(path.node)) { path.replaceWithMultiple( - self.pushDeclar(node).map(expr => t.expressionStatement(expr)), + self.pushDeclar(path.node).map(expr => t.expressionStatement(expr)), ); } else if (path.isFunction()) { return path.skip(); @@ -237,13 +252,30 @@ const hoistVarDeclarationsVisitor = { }, }; -const loopLabelVisitor = { +interface LoopVisitorState { + inSwitchCase: boolean; + hasBreakContinue: boolean; + innerLabels: any[]; + hasReturn: boolean; + ignoreLabeless: boolean; + LOOP_IGNORE: symbol; + isLoop: boolean; + map: any; +} + +const loopLabelVisitor: Visitor = { LabeledStatement({ node }, state) { state.innerLabels.push(node.label.name); }, }; -const continuationVisitor = { +interface ContinuationVisitorState { + returnStatements: NodePath[]; + reassignments: { [k: string]: boolean | undefined }; + outsideReferences: Map; +} + +const continuationVisitor: Visitor = { enter(path, state) { if (path.isAssignmentExpression() || path.isUpdateExpression()) { for (const name of Object.keys(path.getBindingIdentifiers())) { @@ -269,7 +301,7 @@ function loopNodeTo(node) { } } -const loopVisitor = { +const loopVisitor: Visitor = { Loop(path, state) { const oldIgnoreLabeless = state.ignoreLabeless; state.ignoreLabeless = true; @@ -290,7 +322,10 @@ const loopVisitor = { path.skip(); }, - "BreakStatement|ContinueStatement|ReturnStatement"(path, state) { + "BreakStatement|ContinueStatement|ReturnStatement"( + path: NodePath, + state, + ) { const { node, scope } = path; if (node[this.LOOP_IGNORE]) return; @@ -298,6 +333,11 @@ const loopVisitor = { let loopText = loopNodeTo(node); if (loopText) { + if (t.isReturnStatement(node)) { + throw new Error( + "Internal error: unexpected return statement with `loopText`", + ); + } if (node.label) { // we shouldn't be transforming this because it exists somewhere inside if (state.innerLabels.indexOf(node.label.name) >= 0) { @@ -319,7 +359,7 @@ const loopVisitor = { replace = t.stringLiteral(loopText); } - if (path.isReturnStatement()) { + if (t.isReturnStatement(node)) { state.hasReturn = true; replace = t.objectExpression([ t.objectProperty( @@ -351,14 +391,31 @@ function isStrict(path) { } class BlockScoping { + private parent: any; + private state: any; + private scope: Scope; + private throwIfClosureRequired: boolean; + private tdzEnabled: boolean; + private blockPath: NodePath; + private block: t.Node; + private outsideLetReferences: Map; + private hasLetReferences: boolean; + private letReferences: any; + private body: any[]; + // todo(flow->ts) add more specific type + private loopParent: t.Node; + private loopLabel: t.Identifier; + private loopPath: NodePath; + private loop: t.Loop; + private has: any; constructor( - loopPath?: NodePath, + loopPath: NodePath | undefined | null, blockPath: NodePath, - parent: Object, + parent: any, scope: Scope, throwIfClosureRequired: boolean, tdzEnabled: boolean, - state: Object, + state: any, ) { this.parent = parent; this.scope = scope; @@ -426,7 +483,7 @@ class BlockScoping { const binding = scope.bindings[name]; if (binding.kind !== "const") continue; - for (const violation of (binding.constantViolations: Array)) { + for (const violation of binding.constantViolations) { const readOnlyError = state.addHelper("readOnlyError"); const throwNode = t.callExpression(readOnlyError, [ t.stringLiteral(name), @@ -441,6 +498,7 @@ class BlockScoping { } else if (["&&=", "||=", "??="].includes(operator)) { violation.replaceWith( t.logicalExpression( + // @ts-expect-error todo(flow->ts) operator.slice(0, -1), violation.get("left").node, t.sequenceExpression([violation.get("right").node, throwNode]), @@ -450,6 +508,7 @@ class BlockScoping { violation.replaceWith( t.sequenceExpression([ t.binaryExpression( + // @ts-expect-error todo(flow->ts) operator.slice(0, -1), violation.get("left").node, violation.get("right").node, @@ -476,13 +535,14 @@ class BlockScoping { ), ]), ); + // @ts-expect-error todo(flow->ts): possible bug "for(…) switch(){}" violation.node.body.body.unshift(t.expressionStatement(throwNode)); } } } } - updateScopeInfo(wrappedInClosure) { + updateScopeInfo(wrappedInClosure?: boolean) { const blockScope = this.blockPath.scope; const parentScope = @@ -530,7 +590,9 @@ class BlockScoping { const parentBinding = scope.parent.getOwnBinding(key); if ( binding.kind === "hoisted" && + // @ts-expect-error todo(flow->ts) !binding.path.node.async && + // @ts-expect-error todo(flow->ts) !binding.path.node.generator && (!parentBinding || isVar(parentBinding.path.parent)) && !isStrict(binding.path.parentPath) @@ -608,13 +670,15 @@ class BlockScoping { const fn = t.functionExpression( null, params, + // @ts-expect-error todo(flow->ts) improve block type annotations t.blockStatement(isSwitch ? [block] : block.body), ); // continuation this.addContinuations(fn); - let call = t.callExpression(t.nullLiteral(), args); + let call: t.CallExpression | t.YieldExpression | t.AwaitExpression = + t.callExpression(t.nullLiteral(), args); let basePath = ".callee"; // handle generators @@ -669,6 +733,7 @@ class BlockScoping { this.blockPath.replaceWithMultiple(this.body); callPath = parentPath.get(listKey)[key + index]; } else { + // @ts-expect-error todo(flow->ts) improve block type annotations block.body = this.body; callPath = this.blockPath.get("body")[index]; } @@ -704,7 +769,7 @@ class BlockScoping { */ addContinuations(fn) { - const state = { + const state: ContinuationVisitorState = { reassignments: {}, returnStatements: [], outsideReferences: this.outsideLetReferences, @@ -753,6 +818,7 @@ class BlockScoping { const declarators = []; if (this.loop) { + // @ts-expect-error todo(flow->ts) add check for loop type const init = this.loop.left || this.loop.init; if (isBlockScoped(init)) { declarators.push(init); @@ -786,17 +852,22 @@ class BlockScoping { } }; - // + // @ts-expect-error todo(flow->ts) check block node type instead if (block.body) { const declarPaths = this.blockPath.get("body"); + // @ts-expect-error todo(flow->ts) for (let i = 0; i < block.body.length; i++) { + // @ts-expect-error todo(flow->ts) addDeclarationsFromChild(declarPaths[i]); } } + // @ts-expect-error todo(flow->ts) check block node type instead if (block.cases) { const declarPaths = this.blockPath.get("cases"); + // @ts-expect-error todo(flow->ts) for (let i = 0; i < block.cases.length; i++) { + // @ts-expect-error todo(flow->ts) const consequents = block.cases[i].consequent; for (let j = 0; j < consequents.length; j++) { @@ -823,7 +894,7 @@ class BlockScoping { // no let references so we can just quit if (!this.hasLetReferences) return; - const state = { + const state: LetReferenceVisitorState = { letReferences: this.letReferences, closurify: false, loopDepth: 0, @@ -849,8 +920,8 @@ class BlockScoping { * later on. */ - checkLoop(): Object { - const state = { + checkLoop(): any { + const state: LoopVisitorState = { hasBreakContinue: false, ignoreLabeless: false, inSwitchCase: false, @@ -881,7 +952,7 @@ class BlockScoping { * their declarations hoisted to before the closure wrapper. */ - pushDeclar(node: { type: "VariableDeclaration" }): Array { + pushDeclar(node: t.VariableDeclaration): Array { const declars = []; const names = t.getBindingIdentifiers(node); for (const name of Object.keys(names)) { diff --git a/packages/babel-plugin-transform-block-scoping/src/tdz.js b/packages/babel-plugin-transform-block-scoping/src/tdz.ts similarity index 77% rename from packages/babel-plugin-transform-block-scoping/src/tdz.js rename to packages/babel-plugin-transform-block-scoping/src/tdz.ts index b0fdbbdd0f..c6ad427556 100644 --- a/packages/babel-plugin-transform-block-scoping/src/tdz.js +++ b/packages/babel-plugin-transform-block-scoping/src/tdz.ts @@ -1,4 +1,5 @@ import { types as t, template } from "@babel/core"; +import type { Visitor } from "@babel/traverse"; function getTDZStatus(refPath, bindingPath) { const executionStatus = bindingPath._guessExecutionStatusRelativeTo(refPath); @@ -29,7 +30,12 @@ function isReference(node, scope, state) { const visitedMaybeTDZNodes = new WeakSet(); -export const visitor = { +export interface TDZVisitorState { + tdzEnabled: boolean; + addHelper: (name) => any; +} + +export const visitor: Visitor = { ReferencedIdentifier(path, state) { if (!state.tdzEnabled) return; @@ -53,16 +59,22 @@ export const visitor = { const assert = buildTDZAssert(node, state); // add tdzThis to parent variable declarator so it's exploded + // @ts-expect-error todo(flow->ts): avoid mutations bindingPath.parent._tdzThis = true; if (path.parentPath.isUpdateExpression()) { + // @ts-expect-error todo(flow->ts): avoid node mutations if (parent._ignoreBlockScopingTDZ) return; - path.parentPath.replaceWith(t.sequenceExpression([assert, parent])); + path.parentPath.replaceWith( + t.sequenceExpression([assert, parent as t.UpdateExpression]), + ); } else { path.replaceWith(assert); } } else if (status === "inside") { - path.replaceWith(template.ast`${state.addHelper("tdz")}("${node.name}")`); + path.replaceWith( + template.ast`${state.addHelper("tdz")}("${node.name}")` as t.Statement, + ); } }, @@ -71,6 +83,8 @@ export const visitor = { if (!state.tdzEnabled) return; const { node } = path; + + // @ts-expect-error todo(flow->ts): avoid node mutations if (node._ignoreBlockScopingTDZ) return; const nodes = []; @@ -85,6 +99,7 @@ export const visitor = { } if (nodes.length) { + // @ts-expect-error todo(flow->ts): avoid mutations node._ignoreBlockScopingTDZ = true; nodes.push(node); path.replaceWithMultiple(nodes.map(n => t.expressionStatement(n))); diff --git a/tsconfig.json b/tsconfig.json index 343b582970..93f02059ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,6 +28,7 @@ "./packages/babel-plugin-bugfix-v8-spread-parameters-in-optional-chaining/src/**/*.ts", "./packages/babel-plugin-proposal-async-do-expressions/src/**/*.ts", "./packages/babel-plugin-syntax-async-do-expressions/src/**/*.ts", + "./packages/babel-plugin-transform-block-scoping/src/**/*.ts", "./packages/babel-plugin-transform-classes/src/**/*.ts", "./packages/babel-plugin-transform-react-jsx/src/**/*.ts", "./packages/babel-plugin-transform-runtime/src/**/*.ts", @@ -118,6 +119,9 @@ "@babel/plugin-syntax-async-do-expressions": [ "./packages/babel-plugin-syntax-async-do-expressions/src" ], + "@babel/plugin-transform-block-scoping": [ + "./packages/babel-plugin-transform-block-scoping/src" + ], "@babel/plugin-transform-classes": [ "./packages/babel-plugin-transform-classes/src" ], diff --git a/yarn.lock b/yarn.lock index f99fd97dcd..ec0421a8e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2099,6 +2099,7 @@ __metadata: "@babel/core": "workspace:*" "@babel/helper-plugin-test-runner": "workspace:*" "@babel/helper-plugin-utils": "workspace:^7.14.5" + "@babel/traverse": "workspace:*" peerDependencies: "@babel/core": ^7.0.0-0 languageName: unknown