UNPKG

@babel/plugin-transform-block-scoping

Version:

Compile ES2015 block scoping (const and let) to ES5

587 lines (582 loc) 19.8 kB
import { declare } from '@babel/helper-plugin-utils'; import { types, template } from '@babel/core'; const collectLoopBodyBindingsVisitor = { "Expression|Declaration|Loop"(path) { path.skip(); }, Scope(path, state) { if (path.isFunctionParent()) path.skip(); const { bindings } = path.scope; for (const name of Object.keys(bindings)) { const binding = bindings[name]; if (binding.kind === "let" || binding.kind === "const" || binding.kind === "hoisted") { state.blockScoped.push(binding); } } } }; function getLoopBodyBindings(loopPath) { const state = { blockScoped: [] }; loopPath.traverse(collectLoopBodyBindingsVisitor, state); return state.blockScoped; } function getUsageInBody(binding, loopPath) { const seen = new WeakSet(); let capturedInClosure = false; const constantViolations = filterMap(binding.constantViolations, path => { const { inBody, inClosure } = relativeLoopLocation(path, loopPath); if (!inBody) return null; capturedInClosure ||= inClosure; const id = path.isUpdateExpression() ? path.get("argument") : path.isAssignmentExpression() ? path.get("left") : null; if (id) seen.add(id.node); return id; }); const references = filterMap(binding.referencePaths, path => { if (seen.has(path.node)) return null; const { inBody, inClosure } = relativeLoopLocation(path, loopPath); if (!inBody) return null; capturedInClosure ||= inClosure; return path; }); return { capturedInClosure, hasConstantViolations: constantViolations.length > 0, usages: references.concat(constantViolations) }; } function relativeLoopLocation(path, loopPath) { const bodyPath = loopPath.get("body"); let inClosure = false; for (let currPath = path; currPath; currPath = currPath.parentPath) { if (currPath.isFunction() || currPath.isClass() || currPath.isMethod()) { inClosure = true; } if (currPath === bodyPath) { return { inBody: true, inClosure }; } else if (currPath === loopPath) { return { inBody: false, inClosure }; } } throw new Error("Internal Babel error: path is not in loop. Please report this as a bug."); } function wrapLoopBody(loopPath, captured, updatedBindingsUsages) { const loopNode = loopPath.node; const state = { breaksContinues: [], returns: [], labelsStack: [], labellessBreakTargets: 0, labellessContinueTargets: 0, vars: [], loopNode }; loopPath.traverse({ Function(path) { path.skip(); }, LabeledStatement: { enter({ node }, state) { state.labelsStack.push(node.label.name); }, exit({ node }, state) { const popped = state.labelsStack.pop(); if (popped !== node.label.name) { throw new Error("Assertion failure. Please report this bug to Babel."); } } }, Loop: { enter(_, state) { state.labellessContinueTargets++; state.labellessBreakTargets++; }, exit(_, state) { state.labellessContinueTargets--; state.labellessBreakTargets--; } }, SwitchStatement: { enter(_, state) { state.labellessBreakTargets++; }, exit(_, state) { state.labellessBreakTargets--; } }, "BreakStatement|ContinueStatement"(path, state) { const { label } = path.node; if (label) { if (state.labelsStack.includes(label.name)) return; } else if (path.isBreakStatement() ? state.labellessBreakTargets > 0 : state.labellessContinueTargets > 0) { return; } state.breaksContinues.push(path); }, ReturnStatement(path, state) { state.returns.push(path); }, VariableDeclaration(path, state) { if (path.parent === state.loopNode && isVarInLoopHead(path)) return; if (path.node.kind === "var") state.vars.push(path); } }, state); const callArgs = []; const closureParams = []; const updater = []; for (const [name, updatedUsage] of updatedBindingsUsages) { callArgs.push(types.identifier(name)); const innerName = loopPath.scope.generateUid(name); closureParams.push(types.identifier(innerName)); updater.push(types.assignmentExpression("=", types.identifier(name), types.identifier(innerName))); for (const path of updatedUsage) path.replaceWith(types.identifier(innerName)); } for (const name of captured) { if (updatedBindingsUsages.has(name)) continue; callArgs.push(types.identifier(name)); closureParams.push(types.identifier(name)); } const id = loopPath.scope.generateUid("loop"); const fn = types.functionExpression(null, closureParams, types.toBlock(loopNode.body)); let call = types.callExpression(types.identifier(id), callArgs); const fnParent = loopPath.findParent(p => p.isFunction()); if (fnParent) { const { async, generator } = fnParent.node; fn.async = async; fn.generator = generator ?? false; if (generator) call = types.yieldExpression(call, true);else if (async) call = types.awaitExpression(call); } const updaterNode = updater.length > 0 ? types.expressionStatement(types.sequenceExpression(updater)) : null; if (updaterNode) fn.body.body.push(updaterNode); const [varPath] = loopPath.insertBefore(types.variableDeclaration("var", [types.variableDeclarator(types.identifier(id), fn)])); const bodyStmts = []; const varNames = []; for (const varPath of state.vars) { const assign = []; for (const decl of varPath.node.declarations) { varNames.push(...Object.keys(types.getBindingIdentifiers(decl.id))); if (decl.init) { assign.push(types.assignmentExpression("=", decl.id, decl.init)); } else if (types.isForXStatement(varPath.parent, { left: varPath.node })) { assign.push(decl.id); } } if (assign.length > 0) { const replacement = assign.length === 1 ? assign[0] : types.sequenceExpression(assign); varPath.replaceWith(replacement); } else { varPath.remove(); } } if (varNames.length) { varPath.pushContainer("declarations", varNames.map(name => types.variableDeclarator(types.identifier(name)))); } const labelNum = state.breaksContinues.length; const returnNum = state.returns.length; if (labelNum + returnNum === 0) { bodyStmts.push(types.expressionStatement(call)); } else if (labelNum === 1 && returnNum === 0) { for (const path of state.breaksContinues) { const { node } = path; const { type, label } = node; let name = type === "BreakStatement" ? "break" : "continue"; if (label) name += " " + label.name; path.replaceWith(types.addComment(types.returnStatement(types.numericLiteral(1)), "trailing", " " + name, true)); if (updaterNode) path.insertBefore(types.cloneNode(updaterNode)); bodyStmts.push(template.statement.ast` if (${call}) ${node} `); } } else { const completionId = loopPath.scope.generateUid("ret"); if (varPath.isVariableDeclaration()) { varPath.pushContainer("declarations", [types.variableDeclarator(types.identifier(completionId))]); bodyStmts.push(types.expressionStatement(types.assignmentExpression("=", types.identifier(completionId), call))); } else { bodyStmts.push(types.variableDeclaration("var", [types.variableDeclarator(types.identifier(completionId), call)])); } const injected = []; for (const path of state.breaksContinues) { const { node } = path; const { type, label } = node; let name = type === "BreakStatement" ? "break" : "continue"; if (label) name += " " + label.name; let i = injected.indexOf(name); const hasInjected = i !== -1; if (!hasInjected) { injected.push(name); i = injected.length - 1; } path.replaceWith(types.addComment(types.returnStatement(types.numericLiteral(i)), "trailing", " " + name, true)); if (updaterNode) path.insertBefore(types.cloneNode(updaterNode)); if (hasInjected) continue; bodyStmts.push(template.statement.ast` if (${types.identifier(completionId)} === ${types.numericLiteral(i)}) ${node} `); } if (returnNum) { for (const path of state.returns) { const arg = path.node.argument || types.buildUndefinedNode(); path.replaceWith(template.statement.ast` return { v: ${arg} }; `); } bodyStmts.push(template.statement.ast` if (${types.identifier(completionId)}) return ${types.identifier(completionId)}.v; `); } } loopNode.body = types.blockStatement(bodyStmts); return varPath; } function isVarInLoopHead(path) { if (types.isForStatement(path.parent)) return path.key === "init"; if (types.isForXStatement(path.parent)) return path.key === "left"; return false; } function filterMap(list, fn) { const result = []; for (const item of list) { const mapped = fn(item); if (mapped) result.push(mapped); } return result; } function validateUsage(path, state, tdzEnabled) { const dynamicTDZNames = []; for (const name of Object.keys(path.getBindingIdentifiers())) { const binding = path.scope.getBinding(name); if (!binding) continue; if (tdzEnabled) { if (injectTDZChecks(binding, state)) dynamicTDZNames.push(name); } if (path.node.kind === "const") { disallowConstantViolations(name, binding, state); } } return dynamicTDZNames; } function disallowConstantViolations(name, binding, state) { for (const violation of binding.constantViolations) { const readOnlyError = state.addHelper("readOnlyError"); const throwNode = types.callExpression(readOnlyError, [types.stringLiteral(name)]); if (violation.isAssignmentExpression()) { const { operator, left, right } = violation.node; if (operator === "=") { const exprs = [right]; exprs.push(throwNode); violation.replaceWith(types.sequenceExpression(exprs)); } else if (["&&=", "||=", "??="].includes(operator)) { violation.replaceWith(types.logicalExpression(operator.slice(0, -1), left, types.sequenceExpression([right, throwNode]))); } else { violation.replaceWith(types.sequenceExpression([types.binaryExpression(operator.slice(0, -1), left, right), throwNode])); } } else if (violation.isUpdateExpression()) { violation.replaceWith(types.sequenceExpression([types.unaryExpression("+", violation.get("argument").node), throwNode])); } else if (violation.isForXStatement()) { violation.ensureBlock(); violation.get("left").replaceWith(types.variableDeclaration("var", [types.variableDeclarator(violation.scope.generateUidIdentifier(name))])); violation.node.body.body.unshift(types.expressionStatement(throwNode)); } } } function getTDZStatus(refPath, bindingPath) { const executionStatus = bindingPath._guessExecutionStatusRelativeTo(refPath); if (executionStatus === "before") { return "outside"; } else if (executionStatus === "after") { return "inside"; } else { return "maybe"; } } const skipTDZChecks = new WeakSet(); function buildTDZAssert(status, node, state) { if (status === "maybe") { const clone = types.cloneNode(node); skipTDZChecks.add(clone); return types.callExpression(state.addHelper("temporalRef"), [clone, types.stringLiteral(node.name)]); } else { return types.callExpression(state.addHelper("tdz"), [types.stringLiteral(node.name)]); } } function getTDZReplacement(path, state, id = path.node) { if (skipTDZChecks.has(id)) return; skipTDZChecks.add(id); const bindingPath = path.scope.getBinding(id.name)?.path; if (!bindingPath || bindingPath.isFunctionDeclaration()) return; const status = getTDZStatus(path, bindingPath); if (status === "outside") return; if (status === "maybe") { bindingPath.parent._tdzThis = true; } return { status, node: buildTDZAssert(status, id, state) }; } function injectTDZChecks(binding, state) { const allUsages = new Set(binding.referencePaths); binding.constantViolations.forEach(allUsages.add, allUsages); let dynamicTdz = false; for (const path of binding.constantViolations) { const { node } = path; if (skipTDZChecks.has(node)) continue; skipTDZChecks.add(node); if (path.isUpdateExpression()) { const arg = path.get("argument"); const replacement = getTDZReplacement(path, state, arg.node); if (!replacement) continue; if (replacement.status === "maybe") { dynamicTdz = true; path.insertBefore(replacement.node); } else { path.replaceWith(replacement.node); } } else if (path.isAssignmentExpression()) { const nodes = []; const ids = path.getAssignmentIdentifiers(); for (const name of Object.keys(ids)) { const replacement = getTDZReplacement(path, state, ids[name]); if (replacement) { nodes.push(types.expressionStatement(replacement.node)); if (replacement.status === "inside") break; if (replacement.status === "maybe") dynamicTdz = true; } } if (nodes.length > 0) path.insertBefore(nodes); } } for (const path of binding.referencePaths) { if (path.parentPath.isUpdateExpression()) continue; if (path.parentPath.isForXStatement({ left: path.node })) continue; const replacement = getTDZReplacement(path, state); if (!replacement) continue; if (replacement.status === "maybe") dynamicTdz = true; path.replaceWith(replacement.node); } return dynamicTdz; } const index = declare((api, opts) => { api.assertVersion("^7.0.0-0 || ^8.0.0"); const { throwIfClosureRequired = false, tdz: tdzEnabled = false } = opts; if (typeof throwIfClosureRequired !== "boolean") { throw new Error(`.throwIfClosureRequired must be a boolean, or undefined`); } if (typeof tdzEnabled !== "boolean") { throw new Error(`.tdz must be a boolean, or undefined`); } return { name: "transform-block-scoping", visitor: { Loop(path, state) { const isForStatement = path.isForStatement(); const headPath = isForStatement ? path.get("init") : path.isForXStatement() ? path.get("left") : null; let needsBodyWrap = false; const markNeedsBodyWrap = () => { if (throwIfClosureRequired) { throw path.buildCodeFrameError("Compiling let/const in this block would add a closure " + "(throwIfClosureRequired)."); } needsBodyWrap = true; }; const body = path.get("body"); let bodyScope; if (body.isBlockStatement()) { bodyScope = body.scope; } const bindings = getLoopBodyBindings(path); for (const binding of bindings) { const { capturedInClosure } = getUsageInBody(binding, path); if (capturedInClosure) markNeedsBodyWrap(); } const captured = []; const updatedBindingsUsages = new Map(); if (headPath && isBlockScoped(headPath)) { const names = Object.keys(headPath.getBindingIdentifiers()); const headScope = headPath.scope; for (let name of names) { if (bodyScope?.hasOwnBinding(name)) continue; let binding = headScope.getOwnBinding(name); if (!binding) { headScope.crawl(); binding = headScope.getOwnBinding(name); } const { usages, capturedInClosure, hasConstantViolations } = getUsageInBody(binding, path); if (headScope.parent.hasBinding(name) || headScope.parent.hasGlobal(name)) { const newName = headScope.generateUid(name); headScope.rename(name, newName); name = newName; } if (capturedInClosure) { markNeedsBodyWrap(); captured.push(name); } if (isForStatement && hasConstantViolations) { updatedBindingsUsages.set(name, usages); } } } if (needsBodyWrap) { const varPath = wrapLoopBody(path, captured, updatedBindingsUsages); if (headPath?.isVariableDeclaration()) { transformBlockScopedVariable(headPath, state, tdzEnabled); } varPath.get("declarations.0.init").unwrapFunctionEnvironment(); } }, VariableDeclaration(path, state) { transformBlockScopedVariable(path, state, tdzEnabled); }, ClassDeclaration(path) { const { id } = path.node; if (!id) return; const { scope } = path.parentPath; if (!isVarScope(scope) && scope.parent.hasBinding(id.name, { noUids: true })) { path.scope.rename(id.name); } } } }; }); const conflictingFunctionsVisitor = { Scope(path, { names }) { for (const name of names) { const binding = path.scope.getOwnBinding(name); if (binding?.kind === "hoisted") { path.scope.rename(name); } } }, "Expression|Declaration"(path) { path.skip(); } }; function transformBlockScopedVariable(path, state, tdzEnabled) { if (!isBlockScoped(path)) return; const dynamicTDZNames = validateUsage(path, state, tdzEnabled); path.node.kind = "var"; const bindingNames = Object.keys(path.getBindingIdentifiers()); for (const name of bindingNames) { const binding = path.scope.getOwnBinding(name); if (!binding) continue; binding.kind = "var"; } if (isInLoop(path) && !isVarInLoopHead(path) || dynamicTDZNames.length > 0) { for (const decl of path.node.declarations) { decl.init ??= types.buildUndefinedNode(); } } const blockScope = path.scope; const varScope = blockScope.getFunctionParent() || blockScope.getProgramParent(); if (varScope !== blockScope) { for (const name of bindingNames) { let newName = name; if (blockScope.parent.hasBinding(name, { noUids: true }) || blockScope.parent.hasGlobal(name)) { newName = blockScope.generateUid(name); blockScope.rename(name, newName); } blockScope.moveBindingTo(newName, varScope); } } blockScope.path.traverse(conflictingFunctionsVisitor, { names: bindingNames }); for (const name of dynamicTDZNames) { path.scope.push({ id: types.identifier(name), init: state.addHelper("temporalUndefined") }); } } function isLetOrConst(kind) { return kind === "let" || kind === "const"; } function isInLoop(path) { if (!path.parentPath) return false; if (path.parentPath.isLoop()) return true; if (path.parentPath.isFunctionParent()) return false; return isInLoop(path.parentPath); } function isBlockScoped(path) { const { node } = path; if (!types.isVariableDeclaration(node)) return false; const { kind } = node; if (kind === "using" || kind === "await using") { throw path.buildCodeFrameError(`The ${kind} declaration should be first transformed by \`@babel/plugin-transform-explicit-resource-management\`.`); } else if (!isLetOrConst(kind)) { return false; } return true; } function isVarScope(scope) { return scope.path.isFunctionParent() || scope.path.isProgram(); } export { index as default, isVarScope }; //# sourceMappingURL=index.js.map