function testMapping(needle) { var gotResult = needle.exec(got); if (gotResult == null) { process.stderr.write(fmt("Couldn't find {0} in output source", needle)); process.exit(-1); } var expectedResult = needle.exec(original); if (expectedResult == null) { process.stderr.write(fmt("Couldn't find {0} in expected source", needle)); process.exit(-1); } var gotPosition = findLineColumn(got, gotResult.index); var originalPosition = smc.originalPositionFor({ line: gotPosition.line, column: gotPosition.col }); var expectedPosition = findLineColumn(original, expectedResult.index); if (originalPosition.line !== expectedPosition.line || originalPosition.column !== expectedPosition.col) { process.stderr.write(fmt("Sourcemap mapping error for {0}. Expected: ({1},{2}) => ({3},{4}). Got: ({5},{6}) => ({3},{4}).", needle, expectedPosition.line, expectedPosition.col, gotPosition.line, gotPosition.col, originalPosition.line, originalPosition.column)); process.exit(-1); } }
function fail(type, got, expected) { 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"); }
config.plugin = config.plugin.map(function(path) { const absPath = tryor(fs.realpathSync.bind(fs, path), null); if (!absPath) { exit(fmt('error: plugin file not found {0}', path)); } // the require below may throw an exception on parse-error try { return require(absPath); } catch (e) { // node will already print file:line and offending line to stderr exit(fmt("error: couldn't require(\"{0}\")", absPath)); } });
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 = (isForInOf(node) && 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 = options.parse(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); } }
exec(fmt("{0} {1} defs-cmd {2}/{3}", NODE, FLAG, pathToTests, test), function (error, stdout, stderr) { stderr = stderr || ""; stdout = stdout || ""; const expectedStderr = slurp(fmt("{0}/{1}-stderr", pathToTests, noSuffix)); const expectedStdout = slurp(fmt("{0}/{1}-out.js", pathToTests, noSuffix)); const pass = (stderr === expectedStderr && stdout === expectedStdout); if (!pass) { console.log(fmt("FAILED test {0}", test)); } diffOutput(expectedStdout, stdout, fmt("{0}-out.js", test)); diffOutput(expectedStderr, stderr, fmt("{0}-stderr", test)); });
Stats.prototype.toString = function () { // console.log("defs.js stats for file {0}:", filename) const renames = this.renames.map(function (r) { return r; }).sort(function (a, b) { return a.line - b.line; }); // sort a copy of renames const renameStr = renames.map(function (rename) { return fmt("\nline {0}: {1} => {2}", rename.line, rename.oldName, rename.newName); }).join(""); const sum = this.consts + this.lets; const constlets = (sum === 0 ? "can't calculate const coverage (0 consts, 0 lets)" : fmt("{0}% const coverage ({1} consts, {2} lets)", Math.floor(100 * this.consts / sum), this.consts, this.lets)); return constlets + renameStr + "\n"; };
function slurpFile(cb) { if (!fs.existsSync(filename)) { cb(new Error(fmt('error: file not found {0}', filename))); } fs.readFile(filename, cb); }
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); exec(fmt("{0} {1} defs-cmd {2}/{3}", NODE, FLAG, pathToTests, test), function (error, stdout, stderr) { stderr = stderr || ""; stdout = stdout || ""; const expectedStderr = slurp(fmt("{0}/{1}-stderr", pathToTests, noSuffix)); const expectedStdout = slurp(fmt("{0}/{1}-out.js", pathToTests, noSuffix)); const pass = (stderr === expectedStderr && stdout === expectedStdout); if (!pass) { console.log(fmt("FAILED test {0}", test)); } diffOutput(expectedStdout, stdout, fmt("{0}-out.js", test)); diffOutput(expectedStderr, stderr, fmt("{0}-stderr", test)); }); }
function run(test) { const noSuffix = test.slice(0, -3); exec(fmt("{0} {1} defs-wrapper {2}/{3}", NODE, FLAG, pathToTests, test), function(error, stdout, stderr) { stderr = stderr || ""; stdout = stdout || ""; const expectedStderr = slurp(fmt("{0}/{1}-stderr", pathToTests, noSuffix)); const expectedStdout = slurp(fmt("{0}/{1}-out.js", pathToTests, noSuffix)); if (stderr !== expectedStderr) { fail("stderr", stderr, expectedStderr); } if (stdout !== expectedStdout) { fail("stdout", stdout, expectedStdout); } function fail(type, got, expected) { 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"); } }); }
function error(line, var_args) { assert(arguments.length >= 2); const msg = (arguments.length === 2 ? String(var_args) : fmt.apply(fmt, Array.prototype.slice.call(arguments, 1))); error.errors.push(line === -1 ? msg : fmt("line {0}: {1}", line, msg)); }
Scope.prototype.print = function(indent) { indent = indent || 0; var scope = this; var names = this.decls.keys().map(function(name) { return fmt("{0} [{1}]", name, scope.decls.get(name).kind); }).join(", "); var propagates = this.propagates ? this.propagates.items().join(", ") : ""; console.log(fmt("{0}{1}: {2}. propagates: {3}", fmt.repeat(" ", indent), this.node.type, names, propagates)); this.children.forEach(function(c) { c.print(indent + 2); }); };
function testMapping(needle) { const gotPosition = findLineColumn(got, needle.exec(got).index); const originalPosition = smc.originalPositionFor({ line: gotPosition.line, column: gotPosition.col }); const expectedPosition = findLineColumn(original, needle.exec(original).index); if (originalPosition.line !== expectedPosition.line || originalPosition.column !== expectedPosition.col) { process.stderr.write(fmt("Sourcemap mapping error for {0}. Expected: ({1},{2}) => ({3},{4}). Got: ({5},{6}) => ({3},{4}).", needle, expectedPosition.line, expectedPosition.col, gotPosition.line, gotPosition.col, originalPosition.line, originalPosition.column)); process.exit(-1); } }
onode.$parent.body.forEach(function(bnode) { if (bnode === onode) { foundSuspectInBody = true; } if (hasInjectArray(bnode)) { if (existingExpressionStatementWithArray) { throw fmt("conflicting inject arrays at line {0} and {1}", posToLine(existingExpressionStatementWithArray.range[0], ctx.src), posToLine(bnode.range[0], ctx.src)); } existingExpressionStatementWithArray = bnode; } // there's a return statement before our function if (!foundSuspectInBody && bnode.type === "ReturnStatement") { troublesomeReturn = bnode; } });
onode.$parent.body.forEach(function(bnode, idx) { if (bnode === onode) { foundSuspectInBody = true; } if (hasInjectArray(bnode)) { if (existingExpressionStatementWithArray) { throw fmt("conflicting inject arrays at line {0} and {1}", posToLine(existingExpressionStatementWithArray.range[0], ctx.src), posToLine(bnode.range[0], ctx.src)); } existingExpressionStatementWithArray = bnode; } var e; if (!nodeAfterExtends && !foundSuspectInBody && bnode.type === "ExpressionStatement" && (e = bnode.expression).type === "CallExpression" && e.callee.type === "Identifier" && e.callee.name === "__extends") { var nextStatement = onode.$parent.body[idx + 1]; if (nextStatement) { nodeAfterExtends = nextStatement; } } });
function addRemoveInjectArray(params, posAfterFunctionDeclaration, name) { var indent = getIndent(posAfterFunctionDeclaration); var str = fmt("{0}{1}{2}.$inject = {3};", os.EOL, indent, name, ctx.stringify(params, ctx.quot)); ctx.triggers.add({ pos: posAfterFunctionDeclaration, fn: visitNodeFollowingFunctionDeclaration, }); function visitNodeFollowingFunctionDeclaration(nextNode) { var assignment = nextNode.expression; var lvalue; var hasInjectArray = (nextNode.type === "ExpressionStatement" && assignment.type === "AssignmentExpression" && assignment.operator === "=" && (lvalue = assignment.left).type === "MemberExpression" && lvalue.computed === false && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.name === "$inject"); if (ctx.mode === "rebuild" && hasInjectArray) { ctx.fragments.push({ start: posAfterFunctionDeclaration, end: nextNode.range[1], str: str, }); } else if (ctx.mode === "remove" && hasInjectArray) { ctx.fragments.push({ start: posAfterFunctionDeclaration, end: nextNode.range[1], str: "", }); } else if (is.someof(ctx.mode, ["add", "rebuild"]) && !hasInjectArray) { ctx.fragments.push({ start: posAfterFunctionDeclaration, end: posAfterFunctionDeclaration, str: str, }); } } }
function run(src, config) { // alter the options singleton with user configuration for (var key in config) { options[key] = config[key]; } var parsed; if (is.object(src)) { if (!options.ast) { return { errors: [ "Can't produce string output when input is an AST. " + "Did you forget to set options.ast = true?" ], }; } // Received an AST object as src, so no need to parse it. parsed = src; } else if (is.string(src)) { try { parsed = options.parse(src, { loc: true, range: true, }); } catch (e) { return { errors: [ fmt("line {0} column {1}: Error during input file parsing\n{2}\n{3}", e.lineNumber, e.column, src.split("\n")[e.lineNumber - 1], fmt.repeat(" ", e.column - 1) + "^") ], }; } } else { return { errors: ["Input was neither an AST object nor a string."], }; } var ast = parsed; // TODO detect unused variables (never read) error.reset(); var allIdentifiers = setupScopeAndReferences(ast, {}); // static analysis passes detectLoopClosures(ast); detectConstAssignment(ast); //detectConstantLets(ast); var changes = []; transformLoopClosures(ast, changes, options); //ast.$scope.print(); process.exit(-1); if (error.errors.length >= 1) { return { errors: error.errors, }; } if (changes.length > 0) { cleanupTree(ast); allIdentifiers = setupScopeAndReferences(ast, {analyze: false}); } assert(error.errors.length === 0); // change constlet declarations to var, renamed if needed // varify modifies the scopes and AST accordingly and // returns a list of change fragments (to use with alter) var stats = new Stats(); varify(ast, stats, allIdentifiers, changes); if (options.ast) { // return the modified AST instead of src code // get rid of all added $ properties first, such as $parent and $scope cleanupTree(ast); return { stats: stats, ast: ast, }; } else { // apply changes produced by varify and return the transformed src var transformedSrc = alter(src, changes); return { stats: stats, src: transformedSrc, }; } }
function addRemoveInjectArray(params, posAfterFunctionDeclaration, name) { // if an existing something.$inject = [..] exists then is will always be recycled when rebuilding var indent = getIndent(posAfterFunctionDeclaration.pos); var foundSuspectInBody = false; var existingExpressionStatementWithArray = null; var troublesomeReturn = false; onode.$parent.body.forEach(function(bnode) { if (bnode === onode) { foundSuspectInBody = true; } if (hasInjectArray(bnode)) { if (existingExpressionStatementWithArray) { throw fmt("conflicting inject arrays at line {0} and {1}", posToLine(existingExpressionStatementWithArray.range[0], ctx.src), posToLine(bnode.range[0], ctx.src)); } existingExpressionStatementWithArray = bnode; } // there's a return statement before our function if (!foundSuspectInBody && bnode.type === "ReturnStatement") { troublesomeReturn = bnode; } }); assert(foundSuspectInBody); if (onode.type === "FunctionDeclaration") { troublesomeReturn = firstNonPrologueStatement(onode.$parent.body); } if (troublesomeReturn && !existingExpressionStatementWithArray) { posAfterFunctionDeclaration = skipPrevNewline(troublesomeReturn.range[0], troublesomeReturn.loc.start); } function hasInjectArray(node) { var lvalue; var assignment; return (node && node.type === "ExpressionStatement" && (assignment = node.expression).type === "AssignmentExpression" && assignment.operator === "=" && (lvalue = assignment.left).type === "MemberExpression" && ((lvalue.computed === false && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.name === "$inject") || (lvalue.computed === true && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.type === "Literal" && lvalue.property.value === "$inject"))); } function skipPrevNewline(pos, loc) { var prevLF = ctx.src.lastIndexOf("\n", pos); if (prevLF === -1) { return { pos: pos, loc: loc }; } if (prevLF >= 1 && ctx.src[prevLF - 1] === "\r") { --prevLF; } if (/\S/g.test(ctx.src.slice(prevLF, pos - 1))) { // any non-whitespace chars between prev newline and pos? return { pos: pos, loc: loc }; } return { pos: prevLF, loc: { line: loc.line - 1, column: prevLF - ctx.src.lastIndexOf("\n", prevLF) - 1, } }; } if (ctx.mode === "rebuild" && existingExpressionStatementWithArray) { var strNoWhitespace = fmt("{2}.$inject = {3};", null, null, name, ctx.stringify(ctx, params, ctx.quot)); ctx.fragments.push({ start: existingExpressionStatementWithArray.range[0], end: existingExpressionStatementWithArray.range[1], str: strNoWhitespace, loc: { start: existingExpressionStatementWithArray.loc.start, end: existingExpressionStatementWithArray.loc.end } }); } else if (ctx.mode === "remove" && existingExpressionStatementWithArray) { var start = skipPrevNewline(existingExpressionStatementWithArray.range[0], existingExpressionStatementWithArray.loc.start); ctx.fragments.push({ start: start.pos, end: existingExpressionStatementWithArray.range[1], str: "", loc: { start: start.loc, end: existingExpressionStatementWithArray.loc.end } }); } else if (is.someof(ctx.mode, ["add", "rebuild"]) && !existingExpressionStatementWithArray) { var str = fmt("{0}{1}{2}.$inject = {3};", EOL, indent, name, ctx.stringify(ctx, params, ctx.quot)); ctx.fragments.push({ start: posAfterFunctionDeclaration.pos, end: posAfterFunctionDeclaration.pos, str: str, loc: { start: posAfterFunctionDeclaration.loc, end: posAfterFunctionDeclaration.loc } }); } }
(function verifyOptions() { if (argv._.length !== 1) { optimist.showHelp(); exit("error: no input file provided"); } if (!argv.add && !argv.remove) { optimist.showHelp(); exit("error: missing option --add and/or --remove"); } })(); const filename = argv._.shift(); if (!fs.existsSync(filename)) { exit(fmt('error: file not found {0}', filename)); } const src = String(fs.readFileSync(filename)); const config = tryor(function() { return JSON.parse(String(fs.readFileSync("ng-annotate-config.json"))); }, {}); function addOption(opt) { if (opt in argv) { config[opt] = argv[opt]; } } ["add", "remove", "regexp", "single_quotes"].forEach(addOption);
"use strict"; const fs = require("fs"); const fmt = require("simple-fmt"); const tryor = require("tryor"); const defs = require("./defs-main"); if (process.argv.length <= 2) { console.log("USAGE: defs file.js"); process.exit(-1); } const filename = process.argv[2]; if (!fs.existsSync(filename)) { console.log(fmt("error: file not found <{0}>", filename)); process.exit(-1); } const src = String(fs.readFileSync(filename)); const config = findAndReadConfig(); const ret = defs(src, config); if (ret.errors) { process.stderr.write(ret.errors.join("\n")); process.stderr.write("\n"); process.exit(-1); } if (config.stats) { process.stdout.write(ret.stats.toString());
function addRemoveInjectArray(params, posAtFunctionDeclaration, posAfterFunctionDeclaration, name) { // if an existing something.$inject = [..] exists then is will always be recycled when rebuilding const indent = getIndent(posAfterFunctionDeclaration); let foundSuspectInBody = false; let existingExpressionStatementWithArray = null; let troublesomeReturn = false; node.$parent.body.forEach(function(bnode) { if (bnode === node) { foundSuspectInBody = true; } if (hasInjectArray(bnode)) { if (existingExpressionStatementWithArray) { throw fmt("conflicting inject arrays at line {0} and {1}", posToLine(existingExpressionStatementWithArray.range[0], ctx.src), posToLine(bnode.range[0], ctx.src)); } existingExpressionStatementWithArray = bnode; } // there's a return statement before our function if (!foundSuspectInBody && bnode.type === "ReturnStatement") { troublesomeReturn = bnode; } }); assert(foundSuspectInBody); if (troublesomeReturn && !existingExpressionStatementWithArray) { posAfterFunctionDeclaration = skipPrevNewline(troublesomeReturn.range[0]); } function hasInjectArray(node) { let lvalue; let assignment; return (node && node.type === "ExpressionStatement" && (assignment = node.expression).type === "AssignmentExpression" && assignment.operator === "=" && (lvalue = assignment.left).type === "MemberExpression" && ((lvalue.computed === false && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.name === "$inject") || (lvalue.computed === true && ctx.srcForRange(lvalue.object.range) === name && lvalue.property.type === "Literal" && lvalue.property.value === "$inject"))); } function skipNewline(pos) { if (ctx.src[pos] === "\n") { return pos + 1; } else if (ctx.src.slice(pos, pos + 2) === "\r\n") { return pos + 2; } return pos; } function skipPrevNewline(pos) { let prevLF = ctx.src.lastIndexOf("\n", pos); if (prevLF === -1) { return pos; } if (prevLF >= 1 && ctx.src[prevLF] === "\r") { --prevLF; } if (/\S/g.test(ctx.src.slice(prevLF, pos - 1))) { return pos; } return prevLF; } const str = fmt("{0}{1}{2}.$inject = {3};", EOL, indent, name, ctx.stringify(params, ctx.quot)); if (ctx.mode === "rebuild" && existingExpressionStatementWithArray) { ctx.fragments.push({ start: existingExpressionStatementWithArray.range[0], end: existingExpressionStatementWithArray.range[1], str: str, }); } else if (ctx.mode === "remove" && existingExpressionStatementWithArray) { ctx.fragments.push({ start: skipPrevNewline(existingExpressionStatementWithArray.range[0]), end: existingExpressionStatementWithArray.range[1], str: "", }); } else if (is.someof(ctx.mode, ["add", "rebuild"]) && !existingExpressionStatementWithArray) { ctx.fragments.push({ start: posAfterFunctionDeclaration, end: posAfterFunctionDeclaration, str: str, }); } }
function runAnnotate(err, src) { if (err) { exit(err.message); } src = String(src); var config = tryor(function() { return JSON.parse(String(fs.readFileSync("ng-annotate-config.json"))); }, {}); if (filename !== "-") { config.inFile = filename; } ["add", "remove", "o", "sourcemap", "sourceroot", "regexp", "single_quotes", "plugin", "stats"].forEach(function(opt) { if (opt in argv) { config[opt] = argv[opt]; } }); if (config.plugin) { if (!Array.isArray(config.plugin)) { config.plugin = [config.plugin]; } config.plugin = config.plugin.map(function(path) { var absPath = tryor(fs.realpathSync.bind(fs, path), null); if (!absPath) { exit(fmt('error: plugin file not found {0}', path)); } // the require below may throw an exception on parse-error try { return require(absPath); } catch (e) { // node will already print file:line and offending line to stderr exit(fmt("error: couldn't require(\"{0}\")", absPath)); } }); } var run_t0 = Date.now(); var ret = ngAnnotate(src, config); var run_t1 = Date.now(); if (ret.errors) { exit(ret.errors.join("\n")); } var stats = ret._stats; if (config.stats && stats) { var t1 = Date.now(); var all = t1 - t0; var run_esprima = stats.esprima_parse_t1 - stats.esprima_parse_t0; var all_esprima = run_esprima + (stats.esprima_require_t1 - stats.esprima_require_t0); var nga_run = (run_t1 - run_t0) - run_esprima; var nga_init = all - all_esprima - nga_run; var pct = function(n) { return Math.round(100 * n / all); } process.stderr.write(fmt("[{0} ms] esprima: {1}, nga init: {2}, nga run: {3}\n", all, all_esprima, nga_init, nga_run)); process.stderr.write(fmt("[%] esprima: {0}, nga init: {1}, nga run: {2}\n", pct(all_esprima), pct(nga_init), pct(nga_run))); } var output = config.sourcemap ? insertSourcemap(ret.src, ret.map) : ret.src; if (ret.src && config.o) { try { fs.writeFileSync(config.o, output); } catch (e) { exit(e.message); } } else if (ret.src) { process.stdout.write(output); } }
function runAnnotate(err, src) { if (err) { exit(err.message); } src = String(src); const config = tryor(function() { return JSON.parse(String(fs.readFileSync("ng-annotate-config.json"))); }, {}); if (filename !== "-") { config.inFile = filename; } ["add", "remove", "o", "regexp", "rename", "single_quotes", "plugin", "stats"].forEach(function(opt) { if (opt in argv) { config[opt] = argv[opt]; } }); if (argv.sourcemap) { config.map = { inline: true, sourceRoot: argv.sourceroot }; if (filename !== "-") { config.map.inFile = filename; } }; if (config.plugin) { if (!Array.isArray(config.plugin)) { config.plugin = [config.plugin]; } config.plugin = config.plugin.map(function(path) { const absPath = tryor(fs.realpathSync.bind(fs, path), null); if (!absPath) { exit(fmt('error: plugin file not found {0}', path)); } // the require below may throw an exception on parse-error try { return require(absPath); } catch (e) { // node will already print file:line and offending line to stderr exit(fmt("error: couldn't require(\"{0}\")", absPath)); } }); } const trimmedRename = config.rename && config.rename.trim(); if (trimmedRename) { const flattenRename = trimmedRename.split(" "); const renameArray = []; for (let i = 0; i < flattenRename.length; i = i + 2) { renameArray.push({ "from": flattenRename[i], "to": flattenRename[i + 1], }); } config.rename = renameArray; } else { config.rename = null; } const run_t0 = Date.now(); const ret = ngAnnotate(src, config); const run_t1 = Date.now(); if (ret.errors) { exit(ret.errors.join("\n")); } const stats = ret._stats; if (config.stats && stats) { const t1 = Date.now(); const all = t1 - t0; const run_parser = stats.parser_parse_t1 - stats.parser_parse_t0; const all_parser = run_parser + (stats.parser_require_t1 - stats.parser_require_t0); const nga_run = (run_t1 - run_t0) - run_parser; const nga_init = all - all_parser - nga_run; const pct = function(n) { return Math.round(100 * n / all); } process.stderr.write(fmt("[{0} ms] parser: {1}, nga init: {2}, nga run: {3}\n", all, all_parser, nga_init, nga_run)); process.stderr.write(fmt("[%] parser: {0}, nga init: {1}, nga run: {2}\n", pct(all_parser), pct(nga_init), pct(nga_run))); } if (ret.src && config.o) { try { fs.writeFileSync(config.o, ret.src); } catch (e) { exit(e.message); } } else if (ret.src) { process.stdout.write(ret.src); } }
const renameStr = renames.map(function(rename) { return fmt("\nline {0}: {1} => {2}", rename.line, rename.oldName, rename.newName); }).join("");
var names = this.decls.keys().map(function(name) { return fmt("{0} [{1}]", name, scope.decls.get(name).kind); }).join(", ");
function time(str, fn) { var t0 = Date.now(); fn(); var t1 = Date.now(); console.log(fmt(str, t1 - t0)); }