When you write
```
for (const x of l) {
setTimeout(() => x);
}
```
we need to add a closure because the variable is meant to be block-scoped and recreated each time the block runs. We do this.
However, we also add the closure when no loop is present. This isn't necessary, because if no loop is present then each piece of code runs at most once. I changed the transform to only add a closure if a variable is referenced from within a loop.
700 lines
20 KiB
JavaScript
700 lines
20 KiB
JavaScript
import type NodePath from "babel-traverse";
|
|
import type Scope from "babel-traverse";
|
|
import type File from "../../../file";
|
|
import traverse from "babel-traverse";
|
|
import { visitor as tdzVisitor } from "./tdz";
|
|
import * as t from "babel-types";
|
|
import values from "lodash/values";
|
|
import extend from "lodash/extend";
|
|
import template from "babel-template";
|
|
|
|
export default function () {
|
|
return {
|
|
visitor: {
|
|
VariableDeclaration(path, file) {
|
|
const { node, parent, scope } = path;
|
|
if (!isBlockScoped(node)) return;
|
|
convertBlockScopedToVar(path, null, parent, scope, true);
|
|
|
|
if (node._tdzThis) {
|
|
const nodes = [node];
|
|
|
|
for (let i = 0; i < node.declarations.length; i++) {
|
|
const decl = node.declarations[i];
|
|
if (decl.init) {
|
|
const assign = t.assignmentExpression("=", decl.id, decl.init);
|
|
assign._ignoreBlockScopingTDZ = true;
|
|
nodes.push(t.expressionStatement(assign));
|
|
}
|
|
decl.init = file.addHelper("temporalUndefined");
|
|
}
|
|
|
|
node._blockHoist = 2;
|
|
|
|
if (path.isCompletionRecord()) {
|
|
// ensure we don't break completion record semantics by returning
|
|
// the initialiser of the last declarator
|
|
nodes.push(t.expressionStatement(scope.buildUndefinedNode()));
|
|
}
|
|
|
|
path.replaceWithMultiple(nodes);
|
|
}
|
|
},
|
|
|
|
Loop(path, file) {
|
|
const { node, parent, scope } = path;
|
|
t.ensureBlock(node);
|
|
const blockScoping = new BlockScoping(path, path.get("body"), parent, scope, file);
|
|
const replace = blockScoping.run();
|
|
if (replace) path.replaceWith(replace);
|
|
},
|
|
|
|
CatchClause(path, file) {
|
|
const { parent, scope } = path;
|
|
const blockScoping = new BlockScoping(null, path.get("body"), parent, scope, file);
|
|
blockScoping.run();
|
|
},
|
|
|
|
"BlockStatement|SwitchStatement|Program"(path, file) {
|
|
if (!ignoreBlock(path)) {
|
|
const blockScoping = new BlockScoping(null, path, path.parent, path.scope, file);
|
|
blockScoping.run();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function ignoreBlock(path) {
|
|
return t.isLoop(path.parent) || t.isCatchClause(path.parent);
|
|
}
|
|
|
|
const buildRetCheck = template(`
|
|
if (typeof RETURN === "object") return RETURN.v;
|
|
`);
|
|
|
|
function isBlockScoped(node) {
|
|
if (!t.isVariableDeclaration(node)) return false;
|
|
if (node[t.BLOCK_SCOPED_SYMBOL]) return true;
|
|
if (node.kind !== "let" && node.kind !== "const") return false;
|
|
return true;
|
|
}
|
|
|
|
function convertBlockScopedToVar(path, node, parent, scope, moveBindingsToParent = false) {
|
|
if (!node) {
|
|
node = path.node;
|
|
}
|
|
// https://github.com/babel/babel/issues/255
|
|
if (!t.isFor(parent)) {
|
|
for (let i = 0; i < node.declarations.length; i++) {
|
|
const declar = node.declarations[i];
|
|
declar.init = declar.init || scope.buildUndefinedNode();
|
|
}
|
|
}
|
|
|
|
node[t.BLOCK_SCOPED_SYMBOL] = true;
|
|
node.kind = "var";
|
|
|
|
// Move bindings from current block scope to function scope.
|
|
if (moveBindingsToParent) {
|
|
const parentScope = scope.getFunctionParent();
|
|
const ids = path.getBindingIdentifiers();
|
|
for (const name in ids) {
|
|
const binding = scope.getOwnBinding(name);
|
|
if (binding) binding.kind = "var";
|
|
scope.moveBindingTo(name, parentScope);
|
|
}
|
|
}
|
|
}
|
|
|
|
function isVar(node) {
|
|
return t.isVariableDeclaration(node, { kind: "var" }) && !isBlockScoped(node);
|
|
}
|
|
|
|
const letReferenceBlockVisitor = traverse.visitors.merge([{
|
|
Loop: {
|
|
enter(path, state) {
|
|
state.loopDepth++;
|
|
},
|
|
exit(path, state) {
|
|
state.loopDepth--;
|
|
},
|
|
},
|
|
Function(path, state) {
|
|
// References to block-scoped variables only require added closures if it's
|
|
// possible for the code to run more than once -- otherwise it is safe to
|
|
// simply rename the variables.
|
|
if (state.loopDepth > 0) {
|
|
path.traverse(letReferenceFunctionVisitor, state);
|
|
}
|
|
return path.skip();
|
|
}
|
|
}, tdzVisitor]);
|
|
|
|
const letReferenceFunctionVisitor = traverse.visitors.merge([{
|
|
ReferencedIdentifier(path, state) {
|
|
const ref = state.letReferences[path.node.name];
|
|
|
|
// not a part of our scope
|
|
if (!ref) return;
|
|
|
|
// this scope has a variable with the same name so it couldn't belong
|
|
// to our let scope
|
|
const localBinding = path.scope.getBindingIdentifier(path.node.name);
|
|
if (localBinding && localBinding !== ref) return;
|
|
|
|
state.closurify = true;
|
|
}
|
|
}, tdzVisitor]);
|
|
|
|
const hoistVarDeclarationsVisitor = {
|
|
enter(path, self) {
|
|
const { node, parent } = path;
|
|
|
|
if (path.isForStatement()) {
|
|
if (isVar(node.init, node)) {
|
|
const nodes = self.pushDeclar(node.init);
|
|
if (nodes.length === 1) {
|
|
node.init = nodes[0];
|
|
} else {
|
|
node.init = t.sequenceExpression(nodes);
|
|
}
|
|
}
|
|
} else if (path.isFor()) {
|
|
if (isVar(node.left, node)) {
|
|
self.pushDeclar(node.left);
|
|
node.left = node.left.declarations[0].id;
|
|
}
|
|
} else if (isVar(node, parent)) {
|
|
path.replaceWithMultiple(self.pushDeclar(node).map((expr) => t.expressionStatement(expr)));
|
|
} else if (path.isFunction()) {
|
|
return path.skip();
|
|
}
|
|
}
|
|
};
|
|
|
|
const loopLabelVisitor = {
|
|
LabeledStatement({ node }, state) {
|
|
state.innerLabels.push(node.label.name);
|
|
}
|
|
};
|
|
|
|
const continuationVisitor = {
|
|
enter(path, state) {
|
|
if (path.isAssignmentExpression() || path.isUpdateExpression()) {
|
|
const bindings = path.getBindingIdentifiers();
|
|
for (const name in bindings) {
|
|
if (state.outsideReferences[name] !== path.scope.getBindingIdentifier(name)) continue;
|
|
state.reassignments[name] = true;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
function loopNodeTo(node) {
|
|
if (t.isBreakStatement(node)) {
|
|
return "break";
|
|
} else if (t.isContinueStatement(node)) {
|
|
return "continue";
|
|
}
|
|
}
|
|
|
|
const loopVisitor = {
|
|
Loop(path, state) {
|
|
const oldIgnoreLabeless = state.ignoreLabeless;
|
|
state.ignoreLabeless = true;
|
|
path.traverse(loopVisitor, state);
|
|
state.ignoreLabeless = oldIgnoreLabeless;
|
|
path.skip();
|
|
},
|
|
|
|
Function(path) {
|
|
path.skip();
|
|
},
|
|
|
|
SwitchCase(path, state) {
|
|
const oldInSwitchCase = state.inSwitchCase;
|
|
state.inSwitchCase = true;
|
|
path.traverse(loopVisitor, state);
|
|
state.inSwitchCase = oldInSwitchCase;
|
|
path.skip();
|
|
},
|
|
|
|
"BreakStatement|ContinueStatement|ReturnStatement"(path, state) {
|
|
const { node, parent, scope } = path;
|
|
if (node[this.LOOP_IGNORE]) return;
|
|
|
|
let replace;
|
|
let loopText = loopNodeTo(node);
|
|
|
|
if (loopText) {
|
|
if (node.label) {
|
|
// we shouldn't be transforming this because it exists somewhere inside
|
|
if (state.innerLabels.indexOf(node.label.name) >= 0) {
|
|
return;
|
|
}
|
|
|
|
loopText = `${loopText}|${node.label.name}`;
|
|
} else {
|
|
// we shouldn't be transforming these statements because
|
|
// they don't refer to the actual loop we're scopifying
|
|
if (state.ignoreLabeless) return;
|
|
|
|
//
|
|
if (state.inSwitchCase) return;
|
|
|
|
// break statements mean something different in this context
|
|
if (t.isBreakStatement(node) && t.isSwitchCase(parent)) return;
|
|
}
|
|
|
|
state.hasBreakContinue = true;
|
|
state.map[loopText] = node;
|
|
replace = t.stringLiteral(loopText);
|
|
}
|
|
|
|
if (path.isReturnStatement()) {
|
|
state.hasReturn = true;
|
|
replace = t.objectExpression([
|
|
t.objectProperty(t.identifier("v"), node.argument || scope.buildUndefinedNode())
|
|
]);
|
|
}
|
|
|
|
if (replace) {
|
|
replace = t.returnStatement(replace);
|
|
replace[this.LOOP_IGNORE] = true;
|
|
path.skip();
|
|
path.replaceWith(t.inherits(replace, node));
|
|
}
|
|
}
|
|
};
|
|
|
|
class BlockScoping {
|
|
constructor(loopPath?: NodePath, blockPath: NodePath, parent: Object, scope: Scope, file: File) {
|
|
this.parent = parent;
|
|
this.scope = scope;
|
|
this.file = file;
|
|
|
|
this.blockPath = blockPath;
|
|
this.block = blockPath.node;
|
|
|
|
this.outsideLetReferences = Object.create(null);
|
|
this.hasLetReferences = false;
|
|
this.letReferences = Object.create(null);
|
|
this.body = [];
|
|
|
|
if (loopPath) {
|
|
this.loopParent = loopPath.parent;
|
|
this.loopLabel = t.isLabeledStatement(this.loopParent) && this.loopParent.label;
|
|
this.loopPath = loopPath;
|
|
this.loop = loopPath.node;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the ball rolling.
|
|
*/
|
|
|
|
run() {
|
|
const block = this.block;
|
|
if (block._letDone) return;
|
|
block._letDone = true;
|
|
|
|
const needsClosure = this.getLetReferences();
|
|
|
|
// this is a block within a `Function/Program` so we can safely leave it be
|
|
if (t.isFunction(this.parent) || t.isProgram(this.block)) {
|
|
this.updateScopeInfo();
|
|
return;
|
|
}
|
|
|
|
// we can skip everything
|
|
if (!this.hasLetReferences) return;
|
|
|
|
if (needsClosure) {
|
|
this.wrapClosure();
|
|
} else {
|
|
this.remap();
|
|
}
|
|
|
|
this.updateScopeInfo(needsClosure);
|
|
|
|
if (this.loopLabel && !t.isLabeledStatement(this.loopParent)) {
|
|
return t.labeledStatement(this.loopLabel, this.loop);
|
|
}
|
|
}
|
|
|
|
updateScopeInfo(wrappedInClosure) {
|
|
const scope = this.scope;
|
|
const parentScope = scope.getFunctionParent();
|
|
const letRefs = this.letReferences;
|
|
|
|
for (const key in letRefs) {
|
|
const ref = letRefs[key];
|
|
const binding = scope.getBinding(ref.name);
|
|
if (!binding) continue;
|
|
if (binding.kind === "let" || binding.kind === "const") {
|
|
binding.kind = "var";
|
|
|
|
if (wrappedInClosure) {
|
|
scope.removeBinding(ref.name);
|
|
} else {
|
|
scope.moveBindingTo(ref.name, parentScope);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
remap() {
|
|
const letRefs = this.letReferences;
|
|
const scope = this.scope;
|
|
|
|
// alright, so since we aren't wrapping this block in a closure
|
|
// we have to check if any of our let variables collide with
|
|
// those in upper scopes and then if they do, generate a uid
|
|
// for them and replace all references with it
|
|
|
|
for (const key in letRefs) {
|
|
// just an Identifier node we collected in `getLetReferences`
|
|
// this is the defining identifier of a declaration
|
|
const ref = letRefs[key];
|
|
|
|
// todo: could skip this if the colliding binding is in another function
|
|
if (scope.parentHasBinding(key) || scope.hasGlobal(key)) {
|
|
// The same identifier might have been bound separately in the block scope and
|
|
// the enclosing scope (e.g. loop or catch statement), so we should handle both
|
|
// individually
|
|
if (scope.hasOwnBinding(key))
|
|
scope.rename(ref.name);
|
|
|
|
if (this.blockPath.scope.hasOwnBinding(key))
|
|
this.blockPath.scope.rename(ref.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
wrapClosure() {
|
|
if (this.file.opts.throwIfClosureRequired) {
|
|
throw this.blockPath.buildCodeFrameError(
|
|
"Compiling let/const in this block would add a closure " +
|
|
"(throwIfClosureRequired)."
|
|
);
|
|
}
|
|
const block = this.block;
|
|
|
|
const outsideRefs = this.outsideLetReferences;
|
|
|
|
// remap loop heads with colliding variables
|
|
if (this.loop) {
|
|
for (const name in outsideRefs) {
|
|
const id = outsideRefs[name];
|
|
|
|
if (this.scope.hasGlobal(id.name) || this.scope.parentHasBinding(id.name)) {
|
|
delete outsideRefs[id.name];
|
|
delete this.letReferences[id.name];
|
|
|
|
this.scope.rename(id.name);
|
|
|
|
this.letReferences[id.name] = id;
|
|
outsideRefs[id.name] = id;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we're inside of a for loop then we search to see if there are any
|
|
// `break`s, `continue`s, `return`s etc
|
|
this.has = this.checkLoop();
|
|
|
|
// hoist let references to retain scope
|
|
this.hoistVarDeclarations();
|
|
|
|
// turn outsideLetReferences into an array
|
|
const params = values(outsideRefs);
|
|
const args = values(outsideRefs);
|
|
|
|
const isSwitch = this.blockPath.isSwitchStatement();
|
|
|
|
// build the closure that we're going to wrap the block with, possible wrapping switch(){}
|
|
const fn = t.functionExpression(null, params,
|
|
t.blockStatement(isSwitch ? [block] : block.body));
|
|
fn.shadow = true;
|
|
|
|
// continuation
|
|
this.addContinuations(fn);
|
|
|
|
let ref = fn;
|
|
|
|
if (this.loop) {
|
|
ref = this.scope.generateUidIdentifier("loop");
|
|
this.loopPath.insertBefore(t.variableDeclaration("var", [
|
|
t.variableDeclarator(ref, fn)
|
|
]));
|
|
}
|
|
|
|
// build a call and a unique id that we can assign the return value to
|
|
let call = t.callExpression(ref, args);
|
|
const ret = this.scope.generateUidIdentifier("ret");
|
|
|
|
// handle generators
|
|
const hasYield = traverse.hasType(fn.body, this.scope, "YieldExpression", t.FUNCTION_TYPES);
|
|
if (hasYield) {
|
|
fn.generator = true;
|
|
call = t.yieldExpression(call, true);
|
|
}
|
|
|
|
// handlers async functions
|
|
const hasAsync = traverse.hasType(fn.body, this.scope, "AwaitExpression", t.FUNCTION_TYPES);
|
|
if (hasAsync) {
|
|
fn.async = true;
|
|
call = t.awaitExpression(call);
|
|
}
|
|
|
|
this.buildClosure(ret, call);
|
|
|
|
// replace the current block body with the one we're going to build
|
|
if (isSwitch) this.blockPath.replaceWithMultiple(this.body);
|
|
else block.body = this.body;
|
|
}
|
|
|
|
/**
|
|
* Push the closure to the body.
|
|
*/
|
|
|
|
buildClosure(ret: { type: "Identifier" }, call: { type: "CallExpression" }) {
|
|
const has = this.has;
|
|
if (has.hasReturn || has.hasBreakContinue) {
|
|
this.buildHas(ret, call);
|
|
} else {
|
|
this.body.push(t.expressionStatement(call));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If any of the outer let variables are reassigned then we need to rename them in
|
|
* the closure so we can get direct access to the outer variable to continue the
|
|
* iteration with bindings based on each iteration.
|
|
*
|
|
* Reference: https://github.com/babel/babel/issues/1078
|
|
*/
|
|
|
|
addContinuations(fn) {
|
|
const state = {
|
|
reassignments: {},
|
|
outsideReferences: this.outsideLetReferences
|
|
};
|
|
|
|
this.scope.traverse(fn, continuationVisitor, state);
|
|
|
|
for (let i = 0; i < fn.params.length; i++) {
|
|
const param = fn.params[i];
|
|
if (!state.reassignments[param.name]) continue;
|
|
|
|
const newParam = this.scope.generateUidIdentifier(param.name);
|
|
fn.params[i] = newParam;
|
|
|
|
this.scope.rename(param.name, newParam.name, fn);
|
|
|
|
// assign outer reference as it's been modified internally and needs to be retained
|
|
fn.body.body.push(t.expressionStatement(t.assignmentExpression("=", param, newParam)));
|
|
}
|
|
}
|
|
|
|
getLetReferences() {
|
|
const block = this.block;
|
|
|
|
let declarators = [];
|
|
|
|
if (this.loop) {
|
|
const init = this.loop.left || this.loop.init;
|
|
if (isBlockScoped(init)) {
|
|
declarators.push(init);
|
|
extend(this.outsideLetReferences, t.getBindingIdentifiers(init));
|
|
}
|
|
}
|
|
|
|
const addDeclarationsFromChild = (path, node) => {
|
|
node = node || path.node;
|
|
if (t.isClassDeclaration(node) || t.isFunctionDeclaration(node) || isBlockScoped(node)) {
|
|
if (isBlockScoped(node)) {
|
|
convertBlockScopedToVar(path, node, block, this.scope);
|
|
}
|
|
declarators = declarators.concat(node.declarations || node);
|
|
}
|
|
if (t.isLabeledStatement(node)) {
|
|
addDeclarationsFromChild(path.get("body"), node.body);
|
|
}
|
|
};
|
|
|
|
//
|
|
if (block.body) {
|
|
for (let i = 0; i < block.body.length; i++) {
|
|
const declarPath = this.blockPath.get("body")[i];
|
|
addDeclarationsFromChild(declarPath);
|
|
}
|
|
}
|
|
|
|
if (block.cases) {
|
|
for (let i = 0; i < block.cases.length; i++) {
|
|
const consequents = block.cases[i].consequent;
|
|
|
|
for (let j = 0; j < consequents.length; j++) {
|
|
const declarPath = this.blockPath.get("cases")[i];
|
|
const declar = consequents[j];
|
|
addDeclarationsFromChild(declarPath, declar);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
for (let i = 0; i < declarators.length; i++) {
|
|
const declar = declarators[i];
|
|
// Passing true as the third argument causes t.getBindingIdentifiers
|
|
// to return only the *outer* binding identifiers of this
|
|
// declaration, rather than (for example) mistakenly including the
|
|
// parameters of a function declaration. Fixes #4880.
|
|
const keys = t.getBindingIdentifiers(declar, false, true);
|
|
extend(this.letReferences, keys);
|
|
this.hasLetReferences = true;
|
|
}
|
|
|
|
// no let references so we can just quit
|
|
if (!this.hasLetReferences) return;
|
|
|
|
const state = {
|
|
letReferences: this.letReferences,
|
|
closurify: false,
|
|
file: this.file,
|
|
loopDepth: 0,
|
|
};
|
|
|
|
const loopOrFunctionParent = this.blockPath.find(
|
|
(path) => path.isLoop() || path.isFunction()
|
|
);
|
|
if (loopOrFunctionParent && loopOrFunctionParent.isLoop()) {
|
|
// There is a loop ancestor closer than the closest function, so we
|
|
// consider ourselves to be in a loop.
|
|
state.loopDepth++;
|
|
}
|
|
|
|
// traverse through this block, stopping on functions and checking if they
|
|
// contain any local let references
|
|
this.blockPath.traverse(letReferenceBlockVisitor, state);
|
|
|
|
return state.closurify;
|
|
}
|
|
|
|
/**
|
|
* If we're inside of a loop then traverse it and check if it has one of
|
|
* the following node types `ReturnStatement`, `BreakStatement`,
|
|
* `ContinueStatement` and replace it with a return value that we can track
|
|
* later on.
|
|
*/
|
|
|
|
checkLoop(): Object {
|
|
const state = {
|
|
hasBreakContinue: false,
|
|
ignoreLabeless: false,
|
|
inSwitchCase: false,
|
|
innerLabels: [],
|
|
hasReturn: false,
|
|
isLoop: !!this.loop,
|
|
map: {},
|
|
LOOP_IGNORE: Symbol()
|
|
};
|
|
|
|
this.blockPath.traverse(loopLabelVisitor, state);
|
|
this.blockPath.traverse(loopVisitor, state);
|
|
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Hoist all let declarations in this block to before it so they retain scope
|
|
* once we wrap everything in a closure.
|
|
*/
|
|
|
|
hoistVarDeclarations() {
|
|
this.blockPath.traverse(hoistVarDeclarationsVisitor, this);
|
|
}
|
|
|
|
/**
|
|
* Turn a `VariableDeclaration` into an array of `AssignmentExpressions` with
|
|
* their declarations hoisted to before the closure wrapper.
|
|
*/
|
|
|
|
pushDeclar(node: { type: "VariableDeclaration" }): Array<Object> {
|
|
const declars = [];
|
|
const names = t.getBindingIdentifiers(node);
|
|
for (const name in names) {
|
|
declars.push(t.variableDeclarator(names[name]));
|
|
}
|
|
|
|
this.body.push(t.variableDeclaration(node.kind, declars));
|
|
|
|
const replace = [];
|
|
|
|
for (let i = 0; i < node.declarations.length; i++) {
|
|
const declar = node.declarations[i];
|
|
if (!declar.init) continue;
|
|
|
|
const expr = t.assignmentExpression("=", declar.id, declar.init);
|
|
replace.push(t.inherits(expr, declar));
|
|
}
|
|
|
|
return replace;
|
|
}
|
|
|
|
buildHas(ret: { type: "Identifier" }, call: { type: "CallExpression" }) {
|
|
const body = this.body;
|
|
|
|
body.push(t.variableDeclaration("var", [
|
|
t.variableDeclarator(ret, call)
|
|
]));
|
|
|
|
let retCheck;
|
|
const has = this.has;
|
|
const cases = [];
|
|
|
|
if (has.hasReturn) {
|
|
// typeof ret === "object"
|
|
retCheck = buildRetCheck({
|
|
RETURN: ret
|
|
});
|
|
}
|
|
|
|
if (has.hasBreakContinue) {
|
|
for (const key in has.map) {
|
|
cases.push(t.switchCase(t.stringLiteral(key), [has.map[key]]));
|
|
}
|
|
|
|
if (has.hasReturn) {
|
|
cases.push(t.switchCase(null, [retCheck]));
|
|
}
|
|
|
|
if (cases.length === 1) {
|
|
const single = cases[0];
|
|
body.push(t.ifStatement(
|
|
t.binaryExpression("===", ret, single.test),
|
|
single.consequent[0]
|
|
));
|
|
} else {
|
|
if (this.loop) {
|
|
// https://github.com/babel/babel/issues/998
|
|
for (let i = 0; i < cases.length; i++) {
|
|
const caseConsequent = cases[i].consequent[0];
|
|
if (t.isBreakStatement(caseConsequent) && !caseConsequent.label) {
|
|
caseConsequent.label = this.loopLabel = this.loopLabel ||
|
|
this.scope.generateUidIdentifier("loop");
|
|
}
|
|
}
|
|
}
|
|
|
|
body.push(t.switchStatement(ret, cases));
|
|
}
|
|
} else {
|
|
if (has.hasReturn) {
|
|
body.push(retCheck);
|
|
}
|
|
}
|
|
}
|
|
}
|