var traverse = require("../../traverse"); var util = require("../../util"); var t = require("../../types"); var _ = require("lodash"); var isLet = function (node, parent) { if (!t.isVariableDeclaration(node)) return false; if (node._let) return true; if (node.kind !== "let") return false; // https://github.com/6to5/6to5/issues/255 if (!t.isFor(parent) || t.isFor(parent) && parent.left !== node) { _.each(node.declarations, function (declar) { declar.init = declar.init || t.identifier("undefined"); }); } node._let = true; node.kind = "var"; return true; }; var isVar = function (node, parent) { return t.isVariableDeclaration(node, { kind: "var" }) && !isLet(node, parent); }; var standardiseLets = function (declars) { for (var i = 0; i < declars.length; i++) { delete declars[i]._let; } }; exports.VariableDeclaration = function (node, parent) { isLet(node, parent); }; exports.Loop = function (node, parent, scope, context, file) { var init = node.left || node.init; if (isLet(init, node)) { t.ensureBlock(node); node.body._letDeclars = [init]; } if (t.isLabeledStatement(parent)) { // set label so `run` has access to it node.label = parent.label; } var letScoping = new LetScoping(node, node.body, parent, scope, file); letScoping.run(); if (node.label && !t.isLabeledStatement(parent)) { // we've been given a label so let's wrap ourselves return t.labeledStatement(node.label, node); } }; exports.BlockStatement = function (block, parent, scope, context, file) { if (!t.isLoop(parent)) { var letScoping = new LetScoping(false, block, parent, scope, file); letScoping.run(); } }; /** * Description * * @param {Boolean|Node} loopParent * @param {Node} block * @param {Node} parent * @param {Scope} scope * @param {File} file */ function LetScoping(loopParent, block, parent, scope, file) { this.loopParent = loopParent; this.parent = parent; this.scope = scope; this.block = block; this.file = file; this.letReferences = {}; this.body = []; } /** * Start the ball rolling. */ LetScoping.prototype.run = function () { var block = this.block; if (block._letDone) return; block._letDone = true; this.info = this.getInfo(); // remap all let references that exist in upper scopes to their uid this.remap(); // this is a block within a `Function` so we can safely leave it be if (t.isFunction(this.parent)) return this.noClosure(); // this block has no let references so let's clean up if (!this.info.keys.length) return this.noClosure(); // returns whether or not there are any outside let references within any // functions var referencesInClosure = this.getLetReferences(); // no need for a closure so let's clean up if (!referencesInClosure) return this.noClosure(); // 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 var references to retain scope this.hoistVarDeclarations(); // set let references to plain var references standardiseLets(this.info.declarators); // turn letReferences into an array var letReferences = _.values(this.letReferences); // build the closure that we're going to wrap the block with var fn = t.functionExpression(null, letReferences, t.blockStatement(block.body)); fn._aliasFunction = true; // replace the current block body with the one we're going to build block.body = this.body; // change upper scope references with their uid if they have one var params = this.getParams(letReferences); // build a call and a unique id that we can assign the return value to var call = t.callExpression(fn, params); var ret = this.file.generateUidIdentifier("ret", this.scope); var hasYield = traverse.hasType(fn.body, "YieldExpression", t.FUNCTION_TYPES); if (hasYield) { fn.generator = true; call = t.yieldExpression(call, true); } this.build(ret, call); }; /** * There are no let references accessed within a closure so we can just turn the * lets into vars. */ LetScoping.prototype.noClosure = function () { standardiseLets(this.info.declarators); }; /** * Traverse through block and replace all references that exist in a higher * scope to their uids. */ LetScoping.prototype.remap = function () { var duplicates = this.info.duplicates; var block = this.block; var scope = this.scope; if (!this.info.hasDuplicates) return; var replace = function (node, parent, scope, context, duplicates) { if (!t.isIdentifier(node)) return; if (!t.isReferenced(node, parent)) return; var duplicate = duplicates[node.name]; if (!duplicate) return; var own = scope.get(node.name, true); if (own === duplicate.node) { node.name = duplicate.uid; } else { // scope already has it's own declaration that doesn't // match the one we have a stored replacement for context.skip(); } }; var traverseReplace = function (node, parent) { replace(node, parent); traverse(node, { enter: replace }, scope, duplicates); }; var loopParent = this.loopParent; if (loopParent) { traverseReplace(loopParent.right, loopParent); traverseReplace(loopParent.test, loopParent); traverseReplace(loopParent.update, loopParent); } traverse(block, { enter: replace }, scope, duplicates); }; /** * Description * * @returns {Object} */ LetScoping.prototype.getInfo = function () { var block = this.block; var scope = this.scope; var file = this.file; var opts = { // array of `Identifier` names of let variables that appear lexically out of // this scope but should be accessible - eg. `ForOfStatement`.left outsideKeys: [], // array of let `VariableDeclarator`s that are a part of this block declarators: block._letDeclars || [], // object of duplicate ids and their aliases - if there's an `Identifier` // name that's used in an upper scope we generate a unique id and replace // all references with it duplicates: {}, hasDuplicates: false, // array of `Identifier` names of let variables that are accessible within // the current block keys: [] }; var duplicates = function (id, key) { // there's a variable with this exact name in an upper scope so we need // to generate a new name if (scope.parentHas(key, true)) { var duplicate = opts.duplicates[key] = { uid: file.generateUidIdentifier(key, scope).name, node: id }; id.name = duplicate.uid; opts.hasDuplicates = true; } }; var declar; for (var i = 0; i < opts.declarators.length; i++) { declar = opts.declarators[i]; var keys = t.getIds(declar, true); _.each(keys, duplicates); keys = Object.keys(keys); opts.outsideKeys = opts.outsideKeys.concat(keys); opts.keys = opts.keys.concat(keys); } if (block.body) { for (i = 0; i < block.body.length; i++) { declar = block.body[i]; if (!isLet(declar, block)) continue; var declars = t.getIds(declar, true); for (var key in declars) { duplicates(declars[key], key); opts.keys.push(key); } } } return opts; }; /** * 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. * * @returns {Object} */ LetScoping.prototype.checkLoop = function () { var has = { hasContinue: false, hasReturn: false, hasBreak: false, }; traverse(this.block, { enter: function (node, parent, scope, context) { var replace; if (t.isFunction(node) || t.isLoop(node)) { return context.skip(); } if (node && !node.label) { if (t.isBreakStatement(node)) { if (t.isSwitchCase(parent)) return; has.hasBreak = true; replace = t.returnStatement(t.literal("break")); } else if (t.isContinueStatement(node)) { has.hasContinue = true; replace = t.returnStatement(t.literal("continue")); } } if (t.isReturnStatement(node)) { has.hasReturn = true; replace = t.returnStatement(t.objectExpression([ t.property("init", t.identifier("v"), node.argument || t.identifier("undefined")) ])); } if (replace) return t.inherits(replace, node); } }, this.scope, has); return has; }; /** * Hoist all var declarations in this block to before it so they retain scope * once we wrap everything in a closure. */ LetScoping.prototype.hoistVarDeclarations = function () { traverse(this.block, { enter: function (node, parent, scope, context, self) { if (t.isForStatement(node)) { if (isVar(node.init, node)) { node.init = t.sequenceExpression(self.pushDeclar(node.init)); } } else if (t.isFor(node)) { if (isVar(node.left, node)) { node.left = node.left.declarations[0].id; } } else if (isVar(node, parent)) { return self.pushDeclar(node).map(t.expressionStatement); } else if (t.isFunction(node)) { return context.skip(); } } }, this.scope, this); }; /** * Build up a parameter list that we'll call our closure wrapper with, replacing * all duplicate ids with their uid. * * @param {Array} params * @returns {Array} */ LetScoping.prototype.getParams = function (params) { var info = this.info; params = _.cloneDeep(params); _.each(params, function (param) { var duplicate = info.duplicates[param.name]; if (duplicate) param.name = duplicate.uid; }); return params; }; /** * Get all let references within this block. Stopping whenever we reach another * block. */ LetScoping.prototype.getLetReferences = function () { var state = { self: this, closurify: false }; // traverse through this block, stopping on functions and checking if they // contain any outside let references traverse(this.block, { enter: function (node, parent, scope, context, state) { if (t.isFunction(node)) { traverse(node, { enter: function (node, parent) { // not an identifier so we have no use if (!t.isIdentifier(node)) return; // not a direct reference if (!t.isReferenced(node, parent)) return; // this scope has a variable with the same name so it couldn't belong // to our let scope if (scope.hasOwn(node.name, true)) return; state.closurify = true; // this key doesn't appear just outside our scope if (!_.contains(state.self.info.outsideKeys, node.name)) return; // push this badboy state.self.letReferences[node.name] = node; } }, scope, state); return context.skip(); } } }, this.scope, state); return state.closurify; }; /** * Turn a `VariableDeclaration` into an array of `AssignmentExpressions` with * their declarations hoisted to before the closure wrapper. * * @param {Node} node VariableDeclaration * @returns {Array} */ LetScoping.prototype.pushDeclar = function (node) { this.body.push(t.variableDeclaration(node.kind, node.declarations.map(function (declar) { return t.variableDeclarator(declar.id); }))); var replace = []; for (var i = 0; i < node.declarations.length; i++) { var declar = node.declarations[i]; if (!declar.init) continue; var expr = t.assignmentExpression("=", declar.id, declar.init); replace.push(t.inherits(expr, declar)); } return replace; }; /** * Push the closure to the body. * * @param {Node} ret Identifier * @param {Node} call CallExpression */ LetScoping.prototype.build = function (ret, call) { var has = this.has; if (has.hasReturn || has.hasBreak || has.hasContinue) { this.buildHas(ret, call); } else { this.body.push(t.expressionStatement(call)); } }; /** * Description * * @param {Node} ret Identifier * @param {Node} call CallExpression */ LetScoping.prototype.buildHas = function (ret, call) { var body = this.body; body.push(t.variableDeclaration("var", [ t.variableDeclarator(ret, call) ])); var loopParent = this.loopParent; var retCheck; var has = this.has; var cases = []; if (has.hasReturn) { // typeof ret === "object" retCheck = util.template("let-scoping-return", { RETURN: ret }); } if (has.hasBreak || has.hasContinue) { // ensure that the parent has a label as we're building a switch and we // need to be able to access it var label = loopParent.label = loopParent.label || this.file.generateUidIdentifier("loop", this.scope); if (has.hasBreak) { cases.push(t.switchCase(t.literal("break"), [t.breakStatement(label)])); } if (has.hasContinue) { cases.push(t.switchCase(t.literal("continue"), [t.continueStatement(label)])); } if (has.hasReturn) { cases.push(t.switchCase(null, [retCheck])); } if (cases.length === 1) { var single = cases[0]; body.push(t.ifStatement( t.binaryExpression("===", ret, single.test), single.consequent[0] )); } else { body.push(t.switchStatement(ret, cases)); } } else { if (has.hasReturn) body.push(retCheck); } };