diff --git a/src/babel/transformation/transformers/es6/block-scoping.js b/src/babel/transformation/transformers/es6/block-scoping.js index a3ec489dc0..34d68d04b2 100644 --- a/src/babel/transformation/transformers/es6/block-scoping.js +++ b/src/babel/transformation/transformers/es6/block-scoping.js @@ -122,13 +122,13 @@ var letReferenceFunctionVisitor = { // not a direct reference if (!this.isReferencedIdentifier()) return; + // not a part of our scope + if (!state.letReferences[node.name]) return; + // this scope has a variable with the same name so it couldn't belong // to our let scope if (scope.hasOwnBinding(node.name)) return; - // not a part of our scope - if (!state.letReferences[node.name]) return; - state.closurify = true; } }; @@ -159,6 +159,18 @@ var loopLabelVisitor = { } }; +var continuationVisitor = { + enter(node, parent, scope, state) { + if (this.isAssignmentExpression() || this.isUpdateExpression()) { + var bindings = this.getBindingIdentifiers(); + for (var name in bindings) { + if (state.outsideReferences[name] !== scope.getBindingIdentifier(name)) continue; + state.reassignments[name] = true; + } + } + } +}; + var loopNodeTo = function (node) { if (t.isBreakStatement(node)) { return "break"; @@ -352,16 +364,20 @@ class BlockScoping { // turn outsideLetReferences into an array var params = values(outsideRefs); + var args = values(outsideRefs); // build the closure that we're going to wrap the block with var fn = t.functionExpression(null, params, t.blockStatement(block.body)); fn.shadow = true; + // continuation + this.addContinuations(fn); + // replace the current block body with the one we're going to build block.body = this.body; // build a call and a unique id that we can assign the return value to - var call = t.callExpression(fn, params); + var call = t.callExpression(fn, args); var ret = this.scope.generateUidIdentifier("ret"); // handle generators @@ -381,6 +397,36 @@ class BlockScoping { this.build(ret, 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) { + var state = { + reassignments: {}, + outsideReferences: this.outsideLetReferences + }; + + this.scope.traverse(fn, continuationVisitor, state); + + for (var i = 0; i < fn.params.length; i++) { + var param = fn.params[i]; + if (!state.reassignments[param.name]) continue; + + var 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))); + } + } + /** * Description */ @@ -542,7 +588,7 @@ class BlockScoping { single.consequent[0] ))); } else { - // #998 + // https://github.com/babel/babel/issues/998 for (var i = 0; i < cases.length; i++) { var caseConsequent = cases[i].consequent[0]; if (t.isBreakStatement(caseConsequent) && !caseConsequent.label) { diff --git a/src/babel/traversal/scope.js b/src/babel/traversal/scope.js index b71bf0cd33..5fc212aa12 100644 --- a/src/babel/traversal/scope.js +++ b/src/babel/traversal/scope.js @@ -238,16 +238,16 @@ export default class Scope { * Description */ - rename(oldName: string, newName: string) { + rename(oldName: string, newName: string, block?) { newName ||= this.generateUidIdentifier(oldName).name; var info = this.getBinding(oldName); if (!info) return; var binding = info.identifier; - var scope = info.scope; + var scope = info.scope; - scope.traverse(scope.block, { + scope.traverse(block || scope.block, { enter(node, parent, scope) { if (t.isReferencedIdentifier(node, parent) && node.name === oldName) { node.name = newName; @@ -264,10 +264,12 @@ export default class Scope { } }); - scope.removeOwnBinding(oldName); - scope.bindings[newName] = info; + if (!block) { + scope.removeOwnBinding(oldName); + scope.bindings[newName] = info; - binding.name = newName; + binding.name = newName; + } } /** diff --git a/test/core/fixtures/transformation/es6.block-scoping-exec/for-continuation.js b/test/core/fixtures/transformation/es6.block-scoping-exec/for-continuation.js new file mode 100644 index 0000000000..4e99464f72 --- /dev/null +++ b/test/core/fixtures/transformation/es6.block-scoping-exec/for-continuation.js @@ -0,0 +1,12 @@ +var fns = []; + +for (let i = 0; i < 10; i++) { + fns.push(function () { return i; }); + i += 1; +} + +assert.equal(fns[0](), 1); +assert.equal(fns[1](), 3); +assert.equal(fns[2](), 5); +assert.equal(fns[3](), 7); +assert.equal(fns[4](), 9); diff --git a/test/core/fixtures/transformation/es6.block-scoping/for-continuation/actual.js b/test/core/fixtures/transformation/es6.block-scoping/for-continuation/actual.js new file mode 100644 index 0000000000..6e645274d3 --- /dev/null +++ b/test/core/fixtures/transformation/es6.block-scoping/for-continuation/actual.js @@ -0,0 +1,5 @@ +for (let i = 0; i < 2; i++) { + () => { i }; + console.log(i); + i += 1; +} diff --git a/test/core/fixtures/transformation/es6.block-scoping/for-continuation/expected.js b/test/core/fixtures/transformation/es6.block-scoping/for-continuation/expected.js new file mode 100644 index 0000000000..c3ab8c3179 --- /dev/null +++ b/test/core/fixtures/transformation/es6.block-scoping/for-continuation/expected.js @@ -0,0 +1,12 @@ +"use strict"; + +for (var i = 0; i < 2; i++) { + (function (_i) { + (function () { + _i; + }); + console.log(_i); + _i += 1; + i = _i; + })(i); +}