Comparing version 0.4.3 to 0.5.0
@@ -21,5 +21,3 @@ "use strict"; | ||
var config = tryor(function() { | ||
return JSON.parse(String(fs.readFileSync("defs-config.json"))); | ||
}, {}); | ||
var config = findAndReadConfig(); | ||
@@ -43,1 +41,25 @@ var ret = defs(src, config); | ||
} | ||
function findAndReadConfig() { | ||
var path = ""; | ||
var filename = "defs-config.json"; | ||
var filenamePath = null; | ||
while (fs.existsSync(path || ".")) { | ||
filenamePath = path + filename; | ||
if (fs.existsSync(filenamePath)) { | ||
var config = tryor(function() { | ||
return JSON.parse(String(fs.readFileSync(filenamePath))); | ||
}, null); | ||
if (config === null) { | ||
console.error("error: bad JSON in %s", filenamePath); | ||
process.exit(-1); | ||
} | ||
return config; | ||
} | ||
path = "../" + path; | ||
} | ||
return {}; | ||
} |
@@ -11,2 +11,3 @@ "use strict"; | ||
var traverse = require("ast-traverse"); | ||
var breakable = require("breakable"); | ||
var Scope = require("./scope"); | ||
@@ -214,3 +215,5 @@ var error = require("./error"); | ||
function setupReferences(ast, allIdentifiers) { | ||
function setupReferences(ast, allIdentifiers, opts) { | ||
var analyze = (is.own(opts, "analyze") ? opts.analyze : true); | ||
function visit(node) { | ||
@@ -223,7 +226,7 @@ if (!isReference(node)) { | ||
var scope = node.$scope.lookup(node.name); | ||
if (!scope && options.disallowUnknownReferences) { | ||
if (analyze && !scope && options.disallowUnknownReferences) { | ||
error(getline(node), "reference to unknown global variable {0}", node.name); | ||
} | ||
// check const and let for referenced-before-declaration | ||
if (scope && is.someof(scope.getKind(node.name), ["const", "let"])) { | ||
if (analyze && scope && is.someof(scope.getKind(node.name), ["const", "let"])) { | ||
var allowedFromPos = scope.getFromPos(node.name); | ||
@@ -248,5 +251,3 @@ var referencedAtPos = node.range[0]; | ||
function varify(ast, stats, allIdentifiers) { | ||
var changes = []; | ||
function varify(ast, stats, allIdentifiers, changes) { | ||
function unique(name) { | ||
@@ -330,7 +331,23 @@ assert(allIdentifiers.has(name)); | ||
changes.push({ | ||
start: node.range[0], | ||
end: node.range[1], | ||
str: move.name, | ||
}); | ||
if (node.alterop) { | ||
// node has no range because it is the result of another alter operation | ||
var existingOp = null; | ||
for (var i = 0; i < changes.length; i++) { | ||
var op = changes[i]; | ||
if (op.node === node) { | ||
existingOp = op; | ||
break; | ||
} | ||
} | ||
assert(existingOp); | ||
// modify op | ||
existingOp.str = move.name; | ||
} else { | ||
changes.push({ | ||
start: node.range[0], | ||
end: node.range[1], | ||
str: move.name, | ||
}); | ||
} | ||
} | ||
@@ -344,40 +361,101 @@ } | ||
}}); | ||
return changes; | ||
} | ||
function detectLoopClosures(node) { | ||
// forbidden pattern: | ||
// <any>* <loop> <non-fn>* <constlet-def> <any>* <fn> <any>* <constlet-ref> | ||
if (isReference(node) && node.$refToScope && isConstLet(node.$refToScope.getKind(node.name))) { | ||
// traverse nodes up towards root from var-def | ||
// if we hit a function (before a loop) - ok! | ||
// if we hit a loop - maybe-ouch | ||
// if we reach root - ok! | ||
for (var n = node.$refToScope.node; ; ) { | ||
if (isFunction(n)) { | ||
// we're ok (function-local) | ||
return; | ||
} else if (isLoop(n)) { | ||
// not ok (between loop and function) | ||
break; | ||
function detectLoopClosures(ast) { | ||
traverse(ast, {pre: visit}); | ||
function detectIifyBodyBlockers(body, node) { | ||
return breakable(function(brk) { | ||
traverse(body, {pre: function(n) { | ||
// if we hit an inner function of the loop body, don't traverse further | ||
if (isFunction(n)) { | ||
return false; | ||
} | ||
var err = true; // reset to false in else-statement below | ||
var msg = "loop-variable {0} is captured by a loop-closure that can't be transformed due to use of {1} at line {2}"; | ||
if (n.type === "BreakStatement") { | ||
error(getline(node), msg, node.name, "break", getline(n)); | ||
} else if (n.type === "ContinueStatement") { | ||
error(getline(node), msg, node.name, "continue", getline(n)); | ||
} else if (n.type === "ReturnStatement") { | ||
error(getline(node), msg, node.name, "return", getline(n)); | ||
} else if (n.type === "Identifier" && n.name === "arguments") { | ||
error(getline(node), msg, node.name, "arguments", getline(n)); | ||
} else if (n.type === "VariableDeclaration" && n.kind === "var") { | ||
error(getline(node), msg, node.name, "var", getline(n)); | ||
} else { | ||
err = false; | ||
} | ||
if (err) { | ||
brk(true); // break traversal | ||
} | ||
}}); | ||
return false; | ||
}); | ||
} | ||
function visit(node) { | ||
// forbidden pattern: | ||
// <any>* <loop> <non-fn>* <constlet-def> <any>* <fn> <any>* <constlet-ref> | ||
var loopNode = null; | ||
if (isReference(node) && node.$refToScope && isConstLet(node.$refToScope.getKind(node.name))) { | ||
// traverse nodes up towards root from constlet-def | ||
// if we hit a function (before a loop) - ok! | ||
// if we hit a loop - maybe-ouch | ||
// if we reach root - ok! | ||
for (var n = node.$refToScope.node; ; ) { | ||
if (isFunction(n)) { | ||
// we're ok (function-local) | ||
return; | ||
} else if (isLoop(n)) { | ||
loopNode = n; | ||
// maybe not ok (between loop and function) | ||
break; | ||
} | ||
n = n.$parent; | ||
if (!n) { | ||
// ok (reached root) | ||
return; | ||
} | ||
} | ||
n = n.$parent; | ||
if (!n) { | ||
// ok (reached root) | ||
return; | ||
} | ||
} | ||
// traverse scopes from reference-scope up towards definition-scope | ||
// if we hit a function, ouch! | ||
var defScope = node.$refToScope; | ||
for (var s = node.$scope; s; s = s.parent) { | ||
if (s === defScope) { | ||
// we're ok | ||
return; | ||
} else if (isFunction(s.node)) { | ||
// not ok (there's a function between the reference and definition) | ||
error(getline(node), "can't transform closure. {0} is defined outside closure, inside loop", node.name); | ||
assert(isLoop(loopNode)); | ||
// traverse scopes from reference-scope up towards definition-scope | ||
// if we hit a function, ouch! | ||
var defScope = node.$refToScope; | ||
var generateIIFE = (options.loopClosures === "iife"); | ||
for (var s = node.$scope; s; s = s.parent) { | ||
if (s === defScope) { | ||
// we're ok | ||
return; | ||
} else if (isFunction(s.node)) { | ||
// not ok (there's a function between the reference and definition) | ||
// may be transformable via IIFE | ||
if (!generateIIFE) { | ||
var msg = "loop-variable {0} is captured by a loop-closure. Tried \"loopClosures\": \"iife\" in defs-config.json?"; | ||
return error(getline(node), msg, node.name); | ||
} | ||
// here be dragons | ||
// for (let x = ..; .. ; ..) { (function(){x})() } is forbidden because of current | ||
// spec and VM status | ||
if (loopNode.type === "ForStatement" && defScope.node === loopNode) { | ||
var declarationNode = defScope.getNode(node.name); | ||
return error(getline(declarationNode), "Not yet specced ES6 feature. {0} is declared in for-loop header and then captured in loop closure", declarationNode.name); | ||
} | ||
// speak now or forever hold your peace | ||
if (detectIifyBodyBlockers(loopNode.body, node)) { | ||
// error already generated | ||
return; | ||
} | ||
// mark loop for IIFE-insertion | ||
loopNode.$iify = true; | ||
} | ||
} | ||
@@ -388,11 +466,78 @@ } | ||
function detectConstAssignment(node) { | ||
if (isLvalue(node)) { | ||
var scope = node.$scope.lookup(node.name); | ||
if (scope && scope.getKind(node.name) === "const") { | ||
error(getline(node), "can't assign to const variable {0}", node.name); | ||
function transformLoopClosures(root, ops) { | ||
function insertOp(pos, str, node) { | ||
var op = { | ||
start: pos, | ||
end: pos, | ||
str: str, | ||
} | ||
if (node) { | ||
op.node = node; | ||
} | ||
ops.push(op); | ||
} | ||
traverse(root, {pre: function(node) { | ||
if (!node.$iify) { | ||
return; | ||
} | ||
var hasBlock = (node.body.type === "BlockStatement"); | ||
var insertHead = (hasBlock ? | ||
node.body.range[0] + 1 : // just after body { | ||
node.body.range[0]); // just before existing expression | ||
var insertFoot = (hasBlock ? | ||
node.body.range[1] - 1 : // just before body } | ||
node.body.range[1]); // just after existing expression | ||
var forInName = (node.type === "ForInStatement" && node.left.declarations[0].id.name);; | ||
var iifeHead = fmt("(function({0}){", forInName ? forInName : ""); | ||
var iifeTail = fmt("}).call(this{0});", forInName ? ", " + forInName : ""); | ||
// modify AST | ||
var iifeFragment = esprima(iifeHead + iifeTail); | ||
var iifeExpressionStatement = iifeFragment.body[0]; | ||
var iifeBlockStatement = iifeExpressionStatement.expression.callee.object.body; | ||
if (hasBlock) { | ||
var forBlockStatement = node.body; | ||
var tmp = forBlockStatement.body; | ||
forBlockStatement.body = [iifeExpressionStatement]; | ||
iifeBlockStatement.body = tmp; | ||
} else { | ||
var tmp$0 = node.body; | ||
node.body = iifeExpressionStatement; | ||
iifeBlockStatement.body[0] = tmp$0; | ||
} | ||
// create ops | ||
insertOp(insertHead, iifeHead); | ||
if (forInName) { | ||
insertOp(insertFoot, "}).call(this, "); | ||
var args = iifeExpressionStatement.expression.arguments; | ||
var iifeArgumentIdentifier = args[1]; | ||
iifeArgumentIdentifier.alterop = true; | ||
insertOp(insertFoot, forInName, iifeArgumentIdentifier); | ||
insertOp(insertFoot, ");"); | ||
} else { | ||
insertOp(insertFoot, iifeTail); | ||
} | ||
}}); | ||
} | ||
function detectConstAssignment(ast) { | ||
traverse(ast, {pre: function(node) { | ||
if (isLvalue(node)) { | ||
var scope = node.$scope.lookup(node.name); | ||
if (scope && scope.getKind(node.name) === "const") { | ||
error(getline(node), "can't assign to const variable {0}", node.name); | ||
} | ||
} | ||
}}); | ||
} | ||
function detectConstantLets(ast) { | ||
@@ -411,3 +556,3 @@ traverse(ast, {pre: function(node) { | ||
function setupScopeAndReferences(root) { | ||
function setupScopeAndReferences(root, opts) { | ||
// setup scopes | ||
@@ -426,3 +571,3 @@ traverse(root, {pre: createScopes}); | ||
// also collects all referenced names to allIdentifiers | ||
setupReferences(root, allIdentifiers); | ||
setupReferences(root, allIdentifiers, opts); | ||
return allIdentifiers; | ||
@@ -455,9 +600,12 @@ } | ||
var allIdentifiers = setupScopeAndReferences(ast); | ||
var allIdentifiers = setupScopeAndReferences(ast, {}); | ||
// static analysis passes | ||
traverse(ast, {pre: detectLoopClosures}); | ||
traverse(ast, {pre: detectConstAssignment}); | ||
detectLoopClosures(ast); | ||
detectConstAssignment(ast); | ||
//detectConstantLets(ast); | ||
var changes = []; | ||
transformLoopClosures(ast, changes); | ||
//ast.$scope.print(); process.exit(-1); | ||
@@ -471,2 +619,8 @@ | ||
if (changes.length > 0) { | ||
cleanupTree(ast); | ||
allIdentifiers = setupScopeAndReferences(ast, {analyze: false}); | ||
} | ||
assert(error.errors.length === 0); | ||
// change constlet declarations to var, renamed if needed | ||
@@ -476,3 +630,3 @@ // varify modifies the scopes and AST accordingly and | ||
var stats = new Stats(); | ||
var changes = varify(ast, stats, allIdentifiers); | ||
varify(ast, stats, allIdentifiers, changes); | ||
@@ -479,0 +633,0 @@ if (options.ast) { |
var fs = require("fs"); | ||
var fmt = require("simple-fmt"); | ||
var exec = require("child_process").exec; | ||
var diff = require("diff"); | ||
@@ -16,2 +17,10 @@ function slurp(filename) { | ||
function run(test) { | ||
function diffOutput(correct, got, name) { | ||
if (got !== correct) { | ||
var patch = diff.createPatch(name, correct, got); | ||
process.stdout.write(patch); | ||
process.stdout.write("\n\n"); | ||
} | ||
} | ||
var noSuffix = test.slice(0, -3); | ||
@@ -24,17 +33,9 @@ exec(fmt("{0} {1} defs-cmd {2}/{3}", NODE, FLAG, pathToTests, test), function(error, stdout, stderr) { | ||
if (stderr !== expectedStderr) { | ||
fail("stderr", stderr, expectedStderr); | ||
} | ||
if (stdout !== expectedStdout) { | ||
fail("stdout", stdout, expectedStdout); | ||
} | ||
var pass = (stderr === expectedStderr && stdout === expectedStdout); | ||
function fail(type, got, expected) { | ||
if (!pass) { | ||
console.log(fmt("FAILED test {0}", test)); | ||
console.log(fmt("\nEXPECTED {0}:", type)); | ||
process.stdout.write(expected); | ||
console.log(fmt("\nGOT {0}:", type)); | ||
process.stdout.write(got); | ||
console.log("---------------------------\n"); | ||
} | ||
diffOutput(expectedStdout, stdout, fmt("{0}-out.js", test)); | ||
diffOutput(expectedStderr, stderr, fmt("{0}-stderr", test)); | ||
}); | ||
@@ -41,0 +42,0 @@ } |
@@ -131,3 +131,3 @@ "use strict"; | ||
Scope.prototype.remove = function(name) { | ||
return this.decls.delete(name); | ||
return this.decls.remove(name); | ||
}; | ||
@@ -134,0 +134,0 @@ |
@@ -0,1 +1,6 @@ | ||
## v0.5.0 2013-09-30 | ||
* Loop closure IIFE transformation support | ||
* Search for defs-config.json upwards in filesystem | ||
* Improved error messages | ||
## v0.4.3 2013-09-05 | ||
@@ -2,0 +7,0 @@ * Improved loop closure detection as to remove false positives |
@@ -21,5 +21,3 @@ "use strict"; | ||
const config = tryor(function() { | ||
return JSON.parse(String(fs.readFileSync("defs-config.json"))); | ||
}, {}); | ||
const config = findAndReadConfig(); | ||
@@ -43,1 +41,25 @@ const ret = defs(src, config); | ||
} | ||
function findAndReadConfig() { | ||
let path = ""; | ||
let filename = "defs-config.json"; | ||
let filenamePath = null; | ||
while (fs.existsSync(path || ".")) { | ||
filenamePath = path + filename; | ||
if (fs.existsSync(filenamePath)) { | ||
const config = tryor(function() { | ||
return JSON.parse(String(fs.readFileSync(filenamePath))); | ||
}, null); | ||
if (config === null) { | ||
console.error("error: bad JSON in %s", filenamePath); | ||
process.exit(-1); | ||
} | ||
return config; | ||
} | ||
path = "../" + path; | ||
} | ||
return {}; | ||
} |
264
defs-main.js
@@ -11,2 +11,3 @@ "use strict"; | ||
const traverse = require("ast-traverse"); | ||
const breakable = require("breakable"); | ||
const Scope = require("./scope"); | ||
@@ -214,3 +215,5 @@ const error = require("./error"); | ||
function setupReferences(ast, allIdentifiers) { | ||
function setupReferences(ast, allIdentifiers, opts) { | ||
const analyze = (is.own(opts, "analyze") ? opts.analyze : true); | ||
function visit(node) { | ||
@@ -223,7 +226,7 @@ if (!isReference(node)) { | ||
const scope = node.$scope.lookup(node.name); | ||
if (!scope && options.disallowUnknownReferences) { | ||
if (analyze && !scope && options.disallowUnknownReferences) { | ||
error(getline(node), "reference to unknown global variable {0}", node.name); | ||
} | ||
// check const and let for referenced-before-declaration | ||
if (scope && is.someof(scope.getKind(node.name), ["const", "let"])) { | ||
if (analyze && scope && is.someof(scope.getKind(node.name), ["const", "let"])) { | ||
const allowedFromPos = scope.getFromPos(node.name); | ||
@@ -248,5 +251,3 @@ const referencedAtPos = node.range[0]; | ||
function varify(ast, stats, allIdentifiers) { | ||
const changes = []; | ||
function varify(ast, stats, allIdentifiers, changes) { | ||
function unique(name) { | ||
@@ -330,7 +331,23 @@ assert(allIdentifiers.has(name)); | ||
changes.push({ | ||
start: node.range[0], | ||
end: node.range[1], | ||
str: move.name, | ||
}); | ||
if (node.alterop) { | ||
// node has no range because it is the result of another alter operation | ||
let existingOp = null; | ||
for (let i = 0; i < changes.length; i++) { | ||
const op = changes[i]; | ||
if (op.node === node) { | ||
existingOp = op; | ||
break; | ||
} | ||
} | ||
assert(existingOp); | ||
// modify op | ||
existingOp.str = move.name; | ||
} else { | ||
changes.push({ | ||
start: node.range[0], | ||
end: node.range[1], | ||
str: move.name, | ||
}); | ||
} | ||
} | ||
@@ -344,40 +361,101 @@ } | ||
}}); | ||
return changes; | ||
} | ||
function detectLoopClosures(node) { | ||
// forbidden pattern: | ||
// <any>* <loop> <non-fn>* <constlet-def> <any>* <fn> <any>* <constlet-ref> | ||
if (isReference(node) && node.$refToScope && isConstLet(node.$refToScope.getKind(node.name))) { | ||
// traverse nodes up towards root from var-def | ||
// if we hit a function (before a loop) - ok! | ||
// if we hit a loop - maybe-ouch | ||
// if we reach root - ok! | ||
for (let n = node.$refToScope.node; ; ) { | ||
if (isFunction(n)) { | ||
// we're ok (function-local) | ||
return; | ||
} else if (isLoop(n)) { | ||
// not ok (between loop and function) | ||
break; | ||
function detectLoopClosures(ast) { | ||
traverse(ast, {pre: visit}); | ||
function detectIifyBodyBlockers(body, node) { | ||
return breakable(function(brk) { | ||
traverse(body, {pre: function(n) { | ||
// if we hit an inner function of the loop body, don't traverse further | ||
if (isFunction(n)) { | ||
return false; | ||
} | ||
let err = true; // reset to false in else-statement below | ||
const msg = "loop-variable {0} is captured by a loop-closure that can't be transformed due to use of {1} at line {2}"; | ||
if (n.type === "BreakStatement") { | ||
error(getline(node), msg, node.name, "break", getline(n)); | ||
} else if (n.type === "ContinueStatement") { | ||
error(getline(node), msg, node.name, "continue", getline(n)); | ||
} else if (n.type === "ReturnStatement") { | ||
error(getline(node), msg, node.name, "return", getline(n)); | ||
} else if (n.type === "Identifier" && n.name === "arguments") { | ||
error(getline(node), msg, node.name, "arguments", getline(n)); | ||
} else if (n.type === "VariableDeclaration" && n.kind === "var") { | ||
error(getline(node), msg, node.name, "var", getline(n)); | ||
} else { | ||
err = false; | ||
} | ||
if (err) { | ||
brk(true); // break traversal | ||
} | ||
}}); | ||
return false; | ||
}); | ||
} | ||
function visit(node) { | ||
// forbidden pattern: | ||
// <any>* <loop> <non-fn>* <constlet-def> <any>* <fn> <any>* <constlet-ref> | ||
var loopNode = null; | ||
if (isReference(node) && node.$refToScope && isConstLet(node.$refToScope.getKind(node.name))) { | ||
// traverse nodes up towards root from constlet-def | ||
// if we hit a function (before a loop) - ok! | ||
// if we hit a loop - maybe-ouch | ||
// if we reach root - ok! | ||
for (let n = node.$refToScope.node; ; ) { | ||
if (isFunction(n)) { | ||
// we're ok (function-local) | ||
return; | ||
} else if (isLoop(n)) { | ||
loopNode = n; | ||
// maybe not ok (between loop and function) | ||
break; | ||
} | ||
n = n.$parent; | ||
if (!n) { | ||
// ok (reached root) | ||
return; | ||
} | ||
} | ||
n = n.$parent; | ||
if (!n) { | ||
// ok (reached root) | ||
return; | ||
} | ||
} | ||
// traverse scopes from reference-scope up towards definition-scope | ||
// if we hit a function, ouch! | ||
const defScope = node.$refToScope; | ||
for (let s = node.$scope; s; s = s.parent) { | ||
if (s === defScope) { | ||
// we're ok | ||
return; | ||
} else if (isFunction(s.node)) { | ||
// not ok (there's a function between the reference and definition) | ||
error(getline(node), "can't transform closure. {0} is defined outside closure, inside loop", node.name); | ||
assert(isLoop(loopNode)); | ||
// traverse scopes from reference-scope up towards definition-scope | ||
// if we hit a function, ouch! | ||
const defScope = node.$refToScope; | ||
const generateIIFE = (options.loopClosures === "iife"); | ||
for (let s = node.$scope; s; s = s.parent) { | ||
if (s === defScope) { | ||
// we're ok | ||
return; | ||
} else if (isFunction(s.node)) { | ||
// not ok (there's a function between the reference and definition) | ||
// may be transformable via IIFE | ||
if (!generateIIFE) { | ||
const msg = "loop-variable {0} is captured by a loop-closure. Tried \"loopClosures\": \"iife\" in defs-config.json?"; | ||
return error(getline(node), msg, node.name); | ||
} | ||
// here be dragons | ||
// for (let x = ..; .. ; ..) { (function(){x})() } is forbidden because of current | ||
// spec and VM status | ||
if (loopNode.type === "ForStatement" && defScope.node === loopNode) { | ||
const declarationNode = defScope.getNode(node.name); | ||
return error(getline(declarationNode), "Not yet specced ES6 feature. {0} is declared in for-loop header and then captured in loop closure", declarationNode.name); | ||
} | ||
// speak now or forever hold your peace | ||
if (detectIifyBodyBlockers(loopNode.body, node)) { | ||
// error already generated | ||
return; | ||
} | ||
// mark loop for IIFE-insertion | ||
loopNode.$iify = true; | ||
} | ||
} | ||
@@ -388,11 +466,78 @@ } | ||
function detectConstAssignment(node) { | ||
if (isLvalue(node)) { | ||
const scope = node.$scope.lookup(node.name); | ||
if (scope && scope.getKind(node.name) === "const") { | ||
error(getline(node), "can't assign to const variable {0}", node.name); | ||
function transformLoopClosures(root, ops) { | ||
function insertOp(pos, str, node) { | ||
const op = { | ||
start: pos, | ||
end: pos, | ||
str: str, | ||
} | ||
if (node) { | ||
op.node = node; | ||
} | ||
ops.push(op); | ||
} | ||
traverse(root, {pre: function(node) { | ||
if (!node.$iify) { | ||
return; | ||
} | ||
const hasBlock = (node.body.type === "BlockStatement"); | ||
const insertHead = (hasBlock ? | ||
node.body.range[0] + 1 : // just after body { | ||
node.body.range[0]); // just before existing expression | ||
const insertFoot = (hasBlock ? | ||
node.body.range[1] - 1 : // just before body } | ||
node.body.range[1]); // just after existing expression | ||
const forInName = (node.type === "ForInStatement" && node.left.declarations[0].id.name);; | ||
const iifeHead = fmt("(function({0}){", forInName ? forInName : ""); | ||
const iifeTail = fmt("}).call(this{0});", forInName ? ", " + forInName : ""); | ||
// modify AST | ||
const iifeFragment = esprima(iifeHead + iifeTail); | ||
const iifeExpressionStatement = iifeFragment.body[0]; | ||
const iifeBlockStatement = iifeExpressionStatement.expression.callee.object.body; | ||
if (hasBlock) { | ||
const forBlockStatement = node.body; | ||
const tmp = forBlockStatement.body; | ||
forBlockStatement.body = [iifeExpressionStatement]; | ||
iifeBlockStatement.body = tmp; | ||
} else { | ||
const tmp = node.body; | ||
node.body = iifeExpressionStatement; | ||
iifeBlockStatement.body[0] = tmp; | ||
} | ||
// create ops | ||
insertOp(insertHead, iifeHead); | ||
if (forInName) { | ||
insertOp(insertFoot, "}).call(this, "); | ||
const args = iifeExpressionStatement.expression.arguments; | ||
const iifeArgumentIdentifier = args[1]; | ||
iifeArgumentIdentifier.alterop = true; | ||
insertOp(insertFoot, forInName, iifeArgumentIdentifier); | ||
insertOp(insertFoot, ");"); | ||
} else { | ||
insertOp(insertFoot, iifeTail); | ||
} | ||
}}); | ||
} | ||
function detectConstAssignment(ast) { | ||
traverse(ast, {pre: function(node) { | ||
if (isLvalue(node)) { | ||
const scope = node.$scope.lookup(node.name); | ||
if (scope && scope.getKind(node.name) === "const") { | ||
error(getline(node), "can't assign to const variable {0}", node.name); | ||
} | ||
} | ||
}}); | ||
} | ||
function detectConstantLets(ast) { | ||
@@ -411,3 +556,3 @@ traverse(ast, {pre: function(node) { | ||
function setupScopeAndReferences(root) { | ||
function setupScopeAndReferences(root, opts) { | ||
// setup scopes | ||
@@ -426,3 +571,3 @@ traverse(root, {pre: createScopes}); | ||
// also collects all referenced names to allIdentifiers | ||
setupReferences(root, allIdentifiers); | ||
setupReferences(root, allIdentifiers, opts); | ||
return allIdentifiers; | ||
@@ -455,9 +600,12 @@ } | ||
let allIdentifiers = setupScopeAndReferences(ast); | ||
let allIdentifiers = setupScopeAndReferences(ast, {}); | ||
// static analysis passes | ||
traverse(ast, {pre: detectLoopClosures}); | ||
traverse(ast, {pre: detectConstAssignment}); | ||
detectLoopClosures(ast); | ||
detectConstAssignment(ast); | ||
//detectConstantLets(ast); | ||
const changes = []; | ||
transformLoopClosures(ast, changes); | ||
//ast.$scope.print(); process.exit(-1); | ||
@@ -471,2 +619,8 @@ | ||
if (changes.length > 0) { | ||
cleanupTree(ast); | ||
allIdentifiers = setupScopeAndReferences(ast, {analyze: false}); | ||
} | ||
assert(error.errors.length === 0); | ||
// change constlet declarations to var, renamed if needed | ||
@@ -476,3 +630,3 @@ // varify modifies the scopes and AST accordingly and | ||
const stats = new Stats(); | ||
const changes = varify(ast, stats, allIdentifiers); | ||
varify(ast, stats, allIdentifiers, changes); | ||
@@ -479,0 +633,0 @@ if (options.ast) { |
{ | ||
"name": "defs", | ||
"version": "0.4.3", | ||
"version": "0.5.0", | ||
"description": "Static scope analysis and transpilation of ES6 block scoped const and let variables, to ES3.", | ||
@@ -11,11 +11,15 @@ "main": "build/es5/defs-main.js", | ||
"dependencies": { | ||
"alter": "~0.1.0", | ||
"ast-traverse": "~0.1.0", | ||
"alter": "~0.2.0", | ||
"breakable": "~0.1.0", | ||
"ast-traverse": "~0.1.1", | ||
"simple-fmt": "~0.1.0", | ||
"simple-is": "~0.2.0", | ||
"stringmap": "~0.2.0", | ||
"stringset": "~0.2.0", | ||
"tryor": "~0.1.0", | ||
"stringmap": "~0.2.2", | ||
"stringset": "~0.2.1", | ||
"tryor": "~0.1.2", | ||
"esprima": "~1.0.0" | ||
}, | ||
"devDependencies": { | ||
"diff": "~1.0.7" | ||
}, | ||
"keywords": [ | ||
@@ -22,0 +26,0 @@ "defs", |
@@ -40,3 +40,3 @@ # defs.js | ||
`defs` looks for a `defs-config.json` configuration file in your current | ||
directory. It will search for it in parent directories soon as you'd expect. | ||
directory. If not found there, it searches parent directories until it hits `/`. | ||
@@ -52,2 +52,3 @@ Example `defs-config.json`: | ||
}, | ||
"loopClosures": "iife", | ||
"disallowVars": false, | ||
@@ -65,2 +66,5 @@ "disallowDuplicated": true, | ||
`loopClosures` (defaults to `false`) can be set to "iife" to enable transformation | ||
of loop-closures via immediately-invoked function expressions. | ||
`disallowVars` (defaults to `false`) can be enabled to make | ||
@@ -136,59 +140,9 @@ usage of `var` an error. | ||
## Compatibility | ||
`defs.js` strives to transpile your program as true to the ES6 block scope semantics as | ||
possible, while being as maximally non-intrusive as possible. The only textual | ||
differences you'll find between your original and transpiled program is that the latter | ||
uses `var` and occasional variable renames. | ||
`defs.js` strives to transpile your program as true to ES6 block scope semantics as | ||
possible while being as maximally non-intrusive as possible. | ||
It can optionally transform loop closures via IIFE's (when possible), if you include | ||
`"loopClosures": "iife"` in your `defs-config.json`. More info in | ||
[loop-closures.md](loop-closures.md). | ||
### Loop closures limitation | ||
`defs.js` won't transpile a closure-that-captures-a-block-scoped-variable-inside-a-loop, such | ||
as the following example: | ||
```javascript | ||
for (let x = 0; x < 10; x++) { | ||
let y = x; | ||
arr.push(function() { return y; }); | ||
} | ||
``` | ||
With ES6 semantics `y` is bound fresh per loop iteration, so each closure captures a separate | ||
instance of `y`, unlike if `y` would have been a `var`. [Actually, even `x` is bound per | ||
iteration, but v8 (so node) has an | ||
[open bug](https://code.google.com/p/v8/issues/detail?id=2560) for that]. | ||
To transpile this example, an IIFE or `try-catch` must be inserted, which isn't maximally | ||
non-intrusive. `defs.js` will detect this case and spit out an error instead, like so: | ||
line 3: can't transform closure. y is defined outside closure, inside loop | ||
You need to manually handle this the way we've always done pre-`ES6`, | ||
for instance like so: | ||
```javascript | ||
for (let x = 0; x < 10; x++) { | ||
(function(y) { | ||
arr.push(function() { return y; }); | ||
})(x); | ||
} | ||
``` | ||
I'm interested in feedback on this based on real-world usage of `defs.js`. | ||
### Referenced (inside closure) before declaration | ||
`defs.js` detects the vast majority of cases where a variable is referenced prior to | ||
its declaration. The one case it cannot detect is the following: | ||
```javascript | ||
function printx() { console.log(x); } | ||
printx(); // illegal | ||
let x = 1; | ||
printx(); // legal | ||
``` | ||
The first call to `printx` is not legal because `x` hasn't been initialized at that point | ||
of *time*, which is impossible to catch reliably with statical analysis. | ||
`v8 --harmony` will detect and error on this via run-time checking. `defs.js` will | ||
happily transpile this example (`let` => `var` and that's it), and the transpiled code | ||
will print `undefined` on the first call to `printx`. This difference should be a very | ||
minor problem in practice. | ||
See [semantic-differences.md](semantic-differences.md) for other minor differences. |
const fs = require("fs"); | ||
const fmt = require("simple-fmt"); | ||
const exec = require("child_process").exec; | ||
const diff = require("diff"); | ||
@@ -16,2 +17,10 @@ function slurp(filename) { | ||
function run(test) { | ||
function diffOutput(correct, got, name) { | ||
if (got !== correct) { | ||
const patch = diff.createPatch(name, correct, got); | ||
process.stdout.write(patch); | ||
process.stdout.write("\n\n"); | ||
} | ||
} | ||
const noSuffix = test.slice(0, -3); | ||
@@ -24,17 +33,9 @@ exec(fmt("{0} {1} defs-cmd {2}/{3}", NODE, FLAG, pathToTests, test), function(error, stdout, stderr) { | ||
if (stderr !== expectedStderr) { | ||
fail("stderr", stderr, expectedStderr); | ||
} | ||
if (stdout !== expectedStdout) { | ||
fail("stdout", stdout, expectedStdout); | ||
} | ||
const pass = (stderr === expectedStderr && stdout === expectedStdout); | ||
function fail(type, got, expected) { | ||
if (!pass) { | ||
console.log(fmt("FAILED test {0}", test)); | ||
console.log(fmt("\nEXPECTED {0}:", type)); | ||
process.stdout.write(expected); | ||
console.log(fmt("\nGOT {0}:", type)); | ||
process.stdout.write(got); | ||
console.log("---------------------------\n"); | ||
} | ||
diffOutput(expectedStdout, stdout, fmt("{0}-out.js", test)); | ||
diffOutput(expectedStderr, stderr, fmt("{0}-stderr", test)); | ||
}); | ||
@@ -41,0 +42,0 @@ } |
@@ -131,3 +131,3 @@ "use strict"; | ||
Scope.prototype.remove = function(name) { | ||
return this.decls.delete(name); | ||
return this.decls.remove(name); | ||
}; | ||
@@ -134,0 +134,0 @@ |
@@ -9,3 +9,3 @@ "use strict"; | ||
// can be transformed | ||
// can be transformed (common manual work-around) | ||
for (var x$0 in [0,1,2]) { | ||
@@ -15,3 +15,3 @@ arr.push((function(x) { return function() { return x; } })(x$0)); | ||
// can be transformed | ||
// can be transformed (no extra IIFE will be inserted) | ||
for (var x$1 = 0; x$1 < 3; x$1++) {(function(){ | ||
@@ -22,4 +22,79 @@ var y = 1; | ||
// can be transformed (added IIFE) | ||
for (var x$2 = 0; x$2 < 3; x$2++) {(function(){ | ||
var y = 1; | ||
arr.push(function() { return y; }); | ||
}).call(this);} | ||
// can be transformed (added IIFE) | ||
for (var x$3 = 0; x$3 < 3; x$3++) {(function(){ | ||
var y = x$3; | ||
arr.push(function() { return y; }); | ||
}).call(this);} | ||
// can be transformed (added IIFE) | ||
for (var x$4 = 0; x$4 < 3; x$4++) {(function(){ | ||
var y = x$4, z = arr.push(function() { return y; }); | ||
}).call(this);} | ||
// can be transformed (added IIFE) | ||
for (var x$5 = 0; x$5 < 3; x$5++) {(function(){ | ||
var x = 1; | ||
arr.push(function() { return x; }); | ||
}).call(this);} | ||
// can be transformed (added IIFE) | ||
while (true) { | ||
var f = function() { | ||
for (var x = 0; x < 10; x++) {(function(){ | ||
var y = x; | ||
arr.push(function() { return y; }); | ||
}).call(this);} | ||
}; | ||
f(); | ||
} | ||
// it's fine to use break, continue, return and arguments as long as | ||
// it's contained within a function below the loop so that it doesn't | ||
// interfere with the inserted IIFE | ||
(function() { | ||
for (var x = 0; x < 3; x++) {(function(){ | ||
var y = x; | ||
(function() { | ||
for(;;) break; | ||
return; | ||
})(); | ||
(function() { | ||
for(;;) continue; | ||
arguments | ||
})(); | ||
arr.push(function() { return y; }); | ||
}).call(this);} | ||
})(); | ||
// For-In | ||
for (var x$6 in [0,1,2]) {(function(x){ | ||
arr.push(function() { return x; }); | ||
}).call(this, x$6);} | ||
// Block-less For-In | ||
for (var x$7 in [0,1,2]) (function(x){arr.push(function() { return x; });}).call(this, x$7);/*with semicolon*/ | ||
for (var x$8 in [0,1,2]) (function(x){arr.push(function() { return x; })/*no semicolon*/ | ||
}).call(this, x$8);null; // previous semicolon-less for statement's range ends just before 'n' in 'null' | ||
// While | ||
while (true) {(function(){ | ||
var x = 1; | ||
arr.push(function() { return x; }); | ||
}).call(this);} | ||
// Do-While | ||
do {(function(){ | ||
var x = 1; | ||
arr.push(function() { return x; }); | ||
}).call(this);} while (true); | ||
arr.forEach(function(f) { | ||
console.log(f()); | ||
}); |
@@ -9,3 +9,3 @@ "use strict"; | ||
// can be transformed | ||
// can be transformed (common manual work-around) | ||
for (let x in [0,1,2]) { | ||
@@ -15,10 +15,85 @@ arr.push((function(x) { return function() { return x; } })(x)); | ||
// can be transformed | ||
// can be transformed (no extra IIFE will be inserted) | ||
for (let x = 0; x < 3; x++) {(function(){ | ||
var y = 1; | ||
let y = 1; | ||
arr.push(function() { return y; }); | ||
}).call(this);} | ||
// can be transformed (added IIFE) | ||
for (let x = 0; x < 3; x++) { | ||
let y = 1; | ||
arr.push(function() { return y; }); | ||
} | ||
// can be transformed (added IIFE) | ||
for (let x = 0; x < 3; x++) { | ||
let y = x; | ||
arr.push(function() { return y; }); | ||
} | ||
// can be transformed (added IIFE) | ||
for (let x = 0; x < 3; x++) { | ||
let y = x, z = arr.push(function() { return y; }); | ||
} | ||
// can be transformed (added IIFE) | ||
for (let x = 0; x < 3; x++) { | ||
let x = 1; | ||
arr.push(function() { return x; }); | ||
} | ||
// can be transformed (added IIFE) | ||
while (true) { | ||
let f = function() { | ||
for (let x = 0; x < 10; x++) { | ||
let y = x; | ||
arr.push(function() { return y; }); | ||
} | ||
}; | ||
f(); | ||
} | ||
// it's fine to use break, continue, return and arguments as long as | ||
// it's contained within a function below the loop so that it doesn't | ||
// interfere with the inserted IIFE | ||
(function() { | ||
for (let x = 0; x < 3; x++) { | ||
let y = x; | ||
(function() { | ||
for(;;) break; | ||
return; | ||
})(); | ||
(function() { | ||
for(;;) continue; | ||
arguments | ||
})(); | ||
arr.push(function() { return y; }); | ||
} | ||
})(); | ||
// For-In | ||
for (let x in [0,1,2]) { | ||
arr.push(function() { return x; }); | ||
} | ||
// Block-less For-In | ||
for (let x in [0,1,2]) arr.push(function() { return x; });/*with semicolon*/ | ||
for (let x in [0,1,2]) arr.push(function() { return x; })/*no semicolon*/ | ||
null; // previous semicolon-less for statement's range ends just before 'n' in 'null' | ||
// While | ||
while (true) { | ||
let x = 1; | ||
arr.push(function() { return x; }); | ||
} | ||
// Do-While | ||
do { | ||
let x = 1; | ||
arr.push(function() { return x; }); | ||
} while (true); | ||
arr.forEach(function(f) { | ||
console.log(f()); | ||
}); |
"use strict"; | ||
var arr = []; | ||
// fresh x per iteration so can't be transformed | ||
// fresh x per iteration but semantics not determined yet | ||
// in ES6 spec draft (transfer in particular). Also inconsistent | ||
// between VM implementations. | ||
// once ES6 nails down the semantics (and VM's catch up) we'll | ||
// revisit | ||
// note v8 bug https://code.google.com/p/v8/issues/detail?id=2560 | ||
// also see other/v8-bug.js | ||
for (let x = 0; x < 10; x++) { | ||
for (let x = 0; x < 3; x++) { | ||
arr.push(function() { return x; }); | ||
} | ||
for (let z, x = 0; x < 3; x++) { | ||
arr.push(function() { return x; }); | ||
} | ||
// fresh y per iteration so can't be transformed | ||
for (var x = 0; x < 10; x++) { | ||
// as a consequence of the above, defs is unable to transform | ||
// the code below (even though it is the output of an earlier | ||
// defs transformation). we should be able to detect this case | ||
// (and pass it through unmodified) but is it worth the effort? | ||
for (let x = 0; x < 3; x++) {(function(){ | ||
let y = x; | ||
arr.push(function() { return y; }); | ||
}).call(this);} | ||
// return is not allowed inside the loop body because the IIFE would break it | ||
(function() { | ||
for (let x = 0; x < 3; x++) { | ||
let y = x; | ||
return 1; | ||
arr.push(function() { return y; }); | ||
} | ||
})(); | ||
// break is not allowed inside the loop body because the IIFE would break it | ||
for (let x = 0; x < 3; x++) { | ||
let y = x; | ||
break; | ||
arr.push(function() { return y; }); | ||
} | ||
// fresh y per iteration so can't be transformed | ||
while (true) { | ||
var f = function() { | ||
for (var x = 0; x < 10; x++) { | ||
let y = x; | ||
arr.push(function() { return y; }); | ||
} | ||
}; | ||
f(); | ||
// continue is not allowed inside the loop body because the IIFE would break it | ||
for (let x = 0; x < 3; x++) { | ||
let y = x; | ||
continue; | ||
arr.push(function() { return y; }); | ||
} | ||
// fresh x per iteration so can't be transformed | ||
for (let x in [0,1,2]) { | ||
arr.push(function() { return x; }); | ||
// arguments is not allowed inside the loop body because the IIFE would break it | ||
// (and I don't want to re-apply outer arguments in the inserted IIFE) | ||
for (let x = 0; x < 3; x++) { | ||
let y = x; | ||
arguments[0]; | ||
arr.push(function() { return y; }); | ||
} | ||
// continue is not allowed inside the loop body because the IIFE would break it | ||
for (let x = 0; x < 3; x++) { | ||
let y = x; | ||
var z = 1; | ||
arr.push(function() { return y; }); | ||
} | ||
// TODO block-less loops (is that even applicable?) | ||
arr.forEach(function(f) { | ||
console.log(f()); | ||
}); |
Sorry, the diff of this file is not supported yet
117737
79
3073
9
1
145
+ Addedbreakable@~0.1.0
+ Addedalter@0.2.0(transitive)
+ Addedbreakable@0.1.0(transitive)
+ Addedstable@0.1.8(transitive)
- Removedalter@0.1.1(transitive)
Updatedalter@~0.2.0
Updatedast-traverse@~0.1.1
Updatedstringmap@~0.2.2
Updatedstringset@~0.2.1
Updatedtryor@~0.1.2