turn the let scoping transformer into a class because it's quite complicated and the logic needs to be WAY more organised
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user