From e74c7cb0b71becb75ebc0184df2e054f2e401242 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Wed, 12 Nov 2014 01:20:51 +1100 Subject: [PATCH] turn the let scoping transformer into a class because it's quite complicated and the logic needs to be WAY more organised --- .../transformers/let-scoping.js | 369 +++++++++++------- 1 file changed, 235 insertions(+), 134 deletions(-) diff --git a/lib/6to5/transformation/transformers/let-scoping.js b/lib/6to5/transformation/transformers/let-scoping.js index a276eb0a11..a61674e9c6 100644 --- a/lib/6to5/transformation/transformers/let-scoping.js +++ b/lib/6to5/transformation/transformers/let-scoping.js @@ -17,6 +17,12 @@ var isVar = function (node) { return t.isVariableDeclaration(node, { kind: "var" }) && !isLet(node); }; +var standardiseLets = function (declars) { + _.each(declars, function (declar) { + delete declar._let; + }); +}; + exports.VariableDeclaration = function (node) { isLet(node); }; @@ -33,7 +39,8 @@ exports.For = function (node, parent, file, scope) { node.label = parent.label; } - run(node, node.body, parent, file, scope); + var letScoping = new LetScoping(node, node.body, parent, file, scope); + letScoping.run(); if (node.label && !t.isLabeledStatement(parent)) { // we've been given a label so let's wrap ourselves @@ -43,12 +50,98 @@ exports.For = function (node, parent, file, scope) { exports.BlockStatement = function (block, parent, file, scope) { if (!t.isFor(parent)) { - run(false, block, parent, file, scope); + var letScoping = new LetScoping(false, block, parent, file, scope); + letScoping.run(); } }; -var noClosure = function (letDeclars, block, replacements) { - standardiseLets(letDeclars); +/** + * Description + * + * @param {Boolean|Node} forParent + * @param {Node} block + * @param {Node} parent + * @param {File} file + * @param {Scope} scope + */ + +function LetScoping(forParent, block, parent, file, scope) { + this.forParent = forParent; + this.parent = parent; + this.scope = scope; + this.block = block; + this.file = file; + + this.letReferences = {}; + this.body = []; + this.info = this.getInfo(); +} + +/** + * Start the ball rolling. + */ + +LetScoping.prototype.run = function () { + var block = this.block; + + if (block._letDone) return; + block._letDone = true; + + // this is a block within a `Function` so we can safely leave it be + if (t.isFunction(this.parent)) return; + + // 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.checkFor(); + + // 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 = t.identifier(this.file.generateUid("ret", this.scope)); + + this.build(ret, call); +}; + +/** + * There are no let references accessed within a closure so we can just traverse + * through this block and replace all references that exist in a high scope to + * their uids. + */ + +LetScoping.prototype.noClosure = function () { + var replacements = this.info.duplicates; + var declarators = this.info.declarators; + var block = this.block; + + standardiseLets(declarators); if (_.isEmpty(replacements)) return; @@ -59,13 +152,17 @@ var noClosure = function (letDeclars, block, replacements) { }); }; -var standardiseLets = function (declars) { - _.each(declars, function (declar) { - delete declar._let; - }); -}; +/** + * Description + * + * @returns {Object} + */ + +LetScoping.prototype.getInfo = function () { + var block = this.block; + var scope = this.scope; + var file = this.file; -var getInfo = function (block, file, scope) { var opts = { // array of `Identifier` names of let variables that appear lexically out of // this scope but should be accessible - eg. `ForOfStatement`.left @@ -111,15 +208,23 @@ var getInfo = function (block, file, scope) { return opts; }; -var checkFor = function (forParent, block) { +/** + * If we're inside of a `For*Statement` then traverse it and check if it has one + * of the following node types `ReturnStatement`, `BreakStatement`, + * `ContinueStatement` and replace it with a return value we can track later on. + * + * @returns {Object} + */ + +LetScoping.prototype.checkFor = function () { var has = { hasContinue: false, hasReturn: false, hasBreak: false, }; - if (forParent) { - traverse(block, function (node) { + if (this.forParent) { + traverse(this.block, function (node) { var replace; if (t.isFunction(node) || t.isFor(node)) { @@ -144,38 +249,59 @@ var checkFor = function (forParent, block) { return has; }; -var hoistVarDeclarations = function (block, pushDeclar) { - traverse(block, function (node) { +/** + * Hoist all var declarations in this block to before it so they retain scope + * once we wrap everything is in a closure. + */ + +LetScoping.prototype.hoistVarDeclarations = function () { + var self = this; + traverse(this.block, function (node) { if (t.isForStatement(node)) { if (isVar(node.init)) { - node.init = t.sequenceExpression(pushDeclar(node.init)); + node.init = t.sequenceExpression(self.pushDeclar(node.init)); } } else if (t.isFor(node)) { if (isVar(node.left)) { node.left = node.left.declarations[0].id; } } else if (isVar(node)) { - return pushDeclar(node).map(t.expressionStatement); + return self.pushDeclar(node).map(t.expressionStatement); } else if (t.isFunction(node)) { return false; } }); }; -var getParams = function (info, letReferences) { - var params = _.cloneDeep(letReferences); +/** + * 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) { param.name = info.duplicates[param.name] || param.name; }); return params; }; -var getLetReferences = function (block, info, letReferences) { +/** + * Get all let references within this block. Stopping whenever we reach another + * block. + */ + +LetScoping.prototype.getLetReferences = function () { var closurify = false; + var self = this; // traverse through this block, stopping on functions and checking if they // contain any outside let references - traverse(block, function (node, parent, scope) { + traverse(this.block, function (node, parent, scope) { if (t.isFunction(node)) { traverse(node, function (node, parent) { // not an identifier so we have no use @@ -191,10 +317,10 @@ var getLetReferences = function (block, info, letReferences) { closurify = true; // this key doesn't appear just outside our scope - if (!_.contains(info.outsideKeys, node.name)) return; + if (!_.contains(self.info.outsideKeys, node.name)) return; // push this badboy - letReferences[node.name] = node; + self.letReferences[node.name] = node; }); return false; @@ -206,125 +332,100 @@ var getLetReferences = function (block, info, letReferences) { return closurify; }; -var buildPushDeclar = function (body) { - return function (node) { - body.push(t.variableDeclaration(node.kind, node.declarations.map(function (declar) { - return t.variableDeclarator(declar.id); - }))); +/** + * Turn a `VariableDeclaration` into an array of `AssignmentExpressions` with + * their declarations hoisted to before the closure wrapper. + * + * @param {VariableDeclaration} node + * @returns {Array} + */ - var replace = []; +LetScoping.prototype.buildPushDeclar = function (node) { + this.body.push(t.variableDeclaration(node.kind, node.declarations.map(function (declar) { + return t.variableDeclarator(declar.id); + }))); - _.each(node.declarations, function (declar) { - if (!declar.init) return; + var replace = []; - var expr = t.assignmentExpression("=", declar.id, declar.init); - replace.push(t.inherits(expr, declar)); - }); + _.each(node.declarations, function (declar) { + if (!declar.init) return; - return replace; - }; + var expr = t.assignmentExpression("=", declar.id, declar.init); + replace.push(t.inherits(expr, declar)); + }); + + return replace; }; -var run = function (forParent, block, parent, file, scope) { - if (block._letDone) return; - block._letDone = true; - - var info = getInfo(block, file, scope); - var declarators = info.declarators; - var letKeys = info.keys; - - // this is a block within a `Function` so we can safely leave it be - if (t.isFunction(parent)) return; - - // this block has no let references so let's clean up - if (!letKeys.length) return noClosure(declarators, block, info.duplicates); - - // outside let references that we need to wrap - var letReferences = {}; - - // returns whether or not there are any outside let references within any - // functions - var closurify = getLetReferences(block, info, letReferences); - - letReferences = _.values(letReferences); - - // no need for a closure so let's clean up - if (!closurify) return noClosure(declarators, block, info.duplicates); - - // if we're inside of a for loop then we search to see if there are any - // `break`s, `continue`s, `return`s etc - var has = checkFor(forParent, block); - - var body = []; - - // hoist a `VariableDeclaration` and add `AssignmentExpression`s in it's place - var pushDeclar = buildPushDeclar(body); - - // hoist var references to retain scope - hoistVarDeclarations(block, pushDeclar); - - // set let references to plain var references - standardiseLets(declarators); - - // 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've built - block.body = body; - - // change duplicate let references to their uid if they have one - var params = getParams(info, letReferences); - - var call = t.callExpression(fn, params); - var ret = t.identifier(file.generateUid("ret", scope)); +/** + * Push the closure to the body. + * + * @param {Identifier} ret + * @param {CallExpression} call + */ +LetScoping.prototype.build = function (ret, call) { + var has = this.has; if (has.hasReturn || has.hasBreak || has.hasContinue) { - body.push(t.variableDeclaration("var", [ - t.variableDeclarator(ret, call) - ])); - - var retCheck; - - 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 = forParent.label = forParent.label || t.identifier(file.generateUid("loop", 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); - } + this.buildHas(ret, call); } else { - body.push(t.expressionStatement(call)); + this.body.push(t.expressionStatement(call)); + } +}; + +/** + * Description + * + * @param {Identifier} ret + * @param {CallExpression} call + */ + +LetScoping.prototype.buildHas = function (ret, call) { + var body = this.body; + + body.push(t.variableDeclaration("var", [ + t.variableDeclarator(ret, call) + ])); + + var forParent = this.forParent; + 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 = forParent.label = forParent.label || t.identifier(this.file.generateUid("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); } };