UNPKG

lmd

Version:

LMD: Lazy Module Declaration

569 lines (514 loc) 16.9 kB
/** * This code was originally made by Fabio Crisci and distributed under MIT licence * * Copyright (c) 2012 Fabio Crisci <fabio.crisci@gmail.com> * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ var parser = require("uglify-js").parser; var uglify = require("uglify-js").uglify; function generateCode (tree) { return uglify.gen_code(tree, {beautify : true}); }; /** * This is the main function of this module (it's the only one exported) * Given a file path and content returns the instrumented file * It won't instrument files that are already instrumented * * options allow to enable/disable coverage metrics * "function" enable function coverage * "condition" enable condition coverage */ exports.interpret = function (moduleName, file, content, lineOffset, options) { options = options || { "function" : true, "condition" : true }; try { var tree = parser.parse(content, false, true); } catch (e) { return { error: "Error instrumentig file " + file + " " + e.message } } var walker = uglify.ast_walker(); // this is the list of nodes being analyzed by the walker // without this, w.walk(this) would re-enter the newly generated code with infinite recursion var analyzing = []; // list of all lines' id encounterd in this file var lines = []; // list of all conditions' id encounterd in this file var allConditions = []; // list of all functions' id encounterd in this file var allFunctions = []; // anonymous function takes the name of their var definition var candidateFunctionName = null; var isFirstFunction = true, isFirstLine = true; /** * A statement was found in the file, remember its id. */ function rememberStatement (id) { lines.push(id); }; /** * A function was found in the file, remember its id. */ function rememberFunction (id) { allFunctions.push(id); }; /** * Generic function for counting a line. * It generates a lineId from the line number and the block name (in minified files there * are more logical lines on the same file line) and adds a function call before the actual * line of code. * * 'this' is any node in the AST */ function countLine() { var ret; // skip first line if (isFirstLine) { isFirstLine = false; return ret; } if (this[0].start && analyzing.indexOf(this) < 0) { giveNameToAnonymousFunction.call(this); var lineId = this[0].start.line + lineOffset + ''; //this[0].name + ':' + this[0].start.line + ":" + this[0].start.pos; rememberStatement(lineId); analyzing.push(this); ret = [ "splice", [ [ "stat", [ "call", ["dot", ["name", "require"], "coverage_line"], [ [ "string", moduleName], [ "string", lineId] ] ] ], walker.walk(this) ] ]; analyzing.pop(this); } return ret; }; /** * Walker for 'if' nodes. It overrides countLine because we want to instrument conditions. * * 'this' is an if node, so * 'this[0]' is the node descriptor * 'this[1]' is the decision block * 'this[2]' is the 'then' code block * 'this[3]' is the 'else' code block * * Note that if/else if/else in AST are represented as nested if/else */ function countIf() { var self = this, ret; if (self[0].start && analyzing.indexOf(self) < 0) { var decision = self[1]; var lineId = self[0].name + ':' + (self[0].start.line + lineOffset); self[1] = wrapCondition(decision, lineId); // We are adding new lines, make sure code blocks are actual blocks if (self[2] && self[2][0].start && self[2][0].start.value != "{") { self[2] = [ "block", [self[2]]]; } if (self[3] && self[3][0].start && self[3][0].start.value != "{") { self[3] = [ "block", [self[3]]]; } } ret = countLine.call(self); if (decision) { analyzing.pop(decision); } return ret; }; /** * This is the key function for condition coverage as it wraps every condition in * a function call. * The condition id is generated fron the lineId (@see countLine) plus the character * position of the condition. */ function wrapCondition(decision, lineId, parentPos) { if (options.condition === false) { // condition coverage is disabled return decision; } if (isSingleCondition(decision)) { var pos = getPositionStart(decision, parentPos); var condId = lineId + ":" + pos; analyzing.push(decision); allConditions.push(condId); return ["call", ["dot", ["name", "require"], "coverage_condition"], [ [ "string", moduleName ], [ "string", condId], decision ] ]; } else { decision[2] = wrapCondition(decision[2], lineId, getPositionStart(decision, parentPos)); decision[3] = wrapCondition(decision[3], lineId, getPositionEnd(decision, parentPos)); return decision; } }; /** * Wheter or not the if decision has only one boolean condition */ function isSingleCondition(decision) { if (decision[0].start && decision[0].name != "binary") { return true; } else if (decision[1] == "&&" || decision[1] == "||") { return false; } else { return true; } }; /** * Get the start position of a given condition, if it has a start it's a true condition * so get the value, otherwise use a default value that is coming from an upper decision */ function getPositionStart (decision, defaultValue) { if (decision[0].start) { return decision[0].start.pos; } else { return defaultValue || "s"; } }; /** * As for getPositionStart but returns end position. It allows to give different ids to * math and binary operations in multiple conditions ifs. */ function getPositionEnd (decision, defaultValue) { if (decision[0].end) { return decision[0].end.pos; } else { return defaultValue || "e"; } }; /** * Generic function for every node that needs to be wrapped in a block. * For instance, the following code * * for (a in b) doSomething(a) * * once converted in AST does not have a block but only a function call. * Instrumentig this code would return * * for (a in b) instrumentation() * doSomething(a) * * which clearly does not have the same behavior as the non instrumented code. * * This function generates a function that can be used by the walker to add * blocks when they are missing depending on where the block is supposed to be */ function wrapBlock(position) { return function countFor() { var self = this; if (self[0].start && analyzing.indexOf(self) < 0) { if (self[0].start && analyzing.indexOf(self) < 0) { if (self[position] && self[position][0].name != "block") { self[position] = [ "block", [self[position]]]; } } } return countLine.call(self); }; }; /** * Label nodes need special treatment as well. * * myLabel : for (;;) { * //whateveer code here * continue myLabel * } * * Label can be wrapped by countLine, hovewer the subsequent for shouldn't be wrapped. * * instrumentation("label"); * mylabel : instrumentation("for") * for (;;) {} * * The above code would be wrong. * * This function makes sure that the 'for' after a label is not instrumented and that * the 'for' content is wrapped in a block. * * I'm don't think it's reasonable to use labels with something that is not a 'for' block. * In that case the instrumented code might easily break. */ function countLabel() { var ret; if (this[0].start && analyzing.indexOf(this) < 0) { var content = this[2]; if (content[0].name == "for" && content[4] && content[4].name != "block") { content[4] = [ "block", [content[4]]]; } analyzing.push(content); var ret = countLine.call(this); analyzing.pop(content); } return ret; }; /** * Instrumenting function strictly needed for statement coverage only in case of 'defun' * (function definition), however the block 'function' does not correspond to a new statement. * This method allows to track every function call (function coverage). * * As far as I can tell, 'function' is different from 'defun' for the fact that 'defun' * refers to the global definition of a function * function something () {} -> defun * something = function () {} -> function * 'function' doesn't need to be counted because the line is covered by 'name' or whatever * other block. * * Strictly speaking full statement coverage does not imply function coverage only if there * are empty function, which however are empty! * * The tracking for functions is also a bit different from countLine (except 'defun'). This * method assigns every function a name and tracks the history of every call throughout the * whole lifetime of the application, It's a sort of profiler. * * * The structure of 'this' is * 'this[0]' node descriptor * 'this[1]' string, name of the function or null * 'this[2]' array of arguments names (string) * 'this[3]' block with the function's body * * As 'function' happens in the middle of a line, the instrumentation should be in the body. */ function countFunction () { var ret; if (isFirstLine) { isFirstLine = false; return ret; } if (this[0].start && analyzing.indexOf(this) < 0) { var defun = this[0].name === "defun"; var lineId = this[0].start.line + lineOffset + ''; //this[0].name + ":" + this[0].start.line + ":" + this[0].start.pos; var fnName = this[1] || this[0].anonymousName || "(?)"; var fnId = fnName + ':' + (this[0].start.line + lineOffset) + ":" + this[0].start.pos; var body = this[3]; analyzing.push(this); // put a new function call inside the body, works also on empty functions if (options["function"]) { body.splice(0, 0, [ "stat", [ "call", ["dot", ["name", "require"], "coverage_function"], [ ["string", moduleName], ["string", fnId] ] ] ]); // It would be great to instrument the 'exit' from a function // but it means tracking all return statements, maybe in the future... rememberFunction(fnId); } if (defun) { // 'defun' should also be remembered as statements rememberStatement(lineId); ret = [ "splice", [ [ "stat", [ "call", ["dot", ["name", "require"], "coverage_line"], [ [ "string", moduleName], [ "string", lineId] ] ] ], walker.walk(this) ] ]; } else { ret = walker.walk(this); } analyzing.pop(this); } return ret; }; /** * This function tries to extract the name of anonymous functions depending on where they are * defined. * * For instance * var something = function () {} * the function itself is anonymous but we can use 'something' as its name * * 'node' is anything that gets counted, function are extracted from * * var * node[0] : node description * node[1] : array of assignments * node[x][0] : variable name * node[x][1] : value, node * * object (when functions are properties of an object) * node[0] : node description * node[1] : array of attributes * node[x][0] : attribute name * node[x][1] : value, node * * assign (things like object.name = function () {}) * node[0] : node description * node[1] : type of assignment, 'true' if '=' or operand (like += |= and others) * node[2] : left value, object property or variable * node[3] : right value, node * * in case of assign, node[2] can be * 'name' if we assign to a variable * name[0] : node description * name[1] : variable's name * 'dot' when we assign to an object's property * dot[0] : node description * dot[1] : container object * dot[2] : property */ function giveNameToAnonymousFunction () { var node = this; if (node[0].name == "var" || node[0].name == "object") { node[1].forEach(function (assignemt) { if (assignemt[1]) { if (assignemt[1][0].name === "function") { assignemt[1][0].anonymousName = assignemt[0]; } else if (assignemt[1][0].name === "conditional") { if (assignemt[1][2][0] && assignemt[1][2][0].name === "function") { assignemt[1][2][0].anonymousName = assignemt[0]; } if (assignemt[1][3][0] && assignemt[1][3][0].name === "function") { assignemt[1][3][0].anonymousName = assignemt[0]; } } } }); } else if (node[0].name == "assign" && node[1] === true) { if (node[3][0].name === "function") { node[3][0].anonymousName = getNameFromAssign(node); } else if (node[3][0] === "conditional") { if (node[3][2][0] && node[3][2][0].name === "function") { node[3][2][0].anonymousName = getNameFromAssign(node); } if (node[3][3][0] && node[3][3][0].name === "function") { node[3][3][0].anonymousName = getNameFromAssign(node); } } } }; function getNameFromAssign (node) { if (node[2][0].name === "name") { return node[2][1]; } else if (node[2][0].name === "dot") { return node[2][2]; } } /** * This function wraps ternary conditionals in order to have condition coverage * * 'this' is a node containing * 'this[0]' node descriptor * 'this[1]' decision block * 'this[2]' first statement * 'this[3]' second statement */ function wrapConditionals () { if (options.condition === false) { // condition coverage is disabled return; } var self = this, ret; if (self[0].start && analyzing.indexOf(self) < 0) { analyzing.push(self); var lineId = self[0].name + ':' + (self[0].start.line + lineOffset); self[1] = wrapCondition(self[1], lineId); self[2] = walker.walk(self[2]); self[3] = walker.walk(self[3]); analyzing.pop(self); return self; } else if (self[1]) { self[1] = wrapCondition(self[1], lineId); } }; function createAstForArray(array) { // ["array",[["string","1"],["string","2"]]] var result = []; for (var i = 0, c = array.length, item; i < c; i++) { item = array[i]; result.push(["string", item]); } return ["array", result]; } function insertRequireFallback() { if (this[0].start) { var body = this[3]; if (isFirstFunction) { isFirstFunction = false; // var require = arguments[0]; body.splice(0, 0, ["var",[["require",["sub",["name","arguments"],["num",0]]]]]); } } } var instrumentedTree = walker.with_walkers({ "stat" : countLine, "label" : countLabel, "break" : countLine, "continue" : countLine, "debugger" : countLine, "var" : countLine, "const" : countLine, "return" : countLine, "throw" : countLine, "try" : countLine, "defun" : countFunction, "if" : countIf, "while" : wrapBlock(2), "do" : wrapBlock(2), "for" : wrapBlock(4), "for-in" : wrapBlock(4), "switch" : countLine, "with" : countLine, "function" : countFunction, "assign" : giveNameToAnonymousFunction, "object" : giveNameToAnonymousFunction, "conditional": wrapConditionals }, function () { return walker.walk(tree); }); instrumentedTree = walker.with_walkers({ "function" : insertRequireFallback }, function () { return walker.walk(instrumentedTree); }); var code = generateCode(instrumentedTree); return { code: code.replace(/;$/, ''), options: { lines: lines, conditions: allConditions, functions: allFunctions } }; };