UNPKG

coffee-coverage

Version:

Istanbul and JSCoverage-style instrumentation for CoffeeScript files.

498 lines (457 loc) 19.9 kB
// Generated by CoffeeScript 2.3.2 (function() { // This is an instrumentor which provides [Istanbul](https://github.com/gotwarlost/istanbul) style // instrumentation. This will add a JSON report object to the source code. The report object is a // hash where keys are file names (absolute paths), and values are coverage data for that file // (the result of `json.stringify(collector.fileCoverageFor(filename))`) Each coverage data // consists of: // * `path` - The path to the file. This is an absolute path. // * `s` - Hash of statement counts, where keys as statement IDs. // * `b` - Hash of branch counts, where keys are branch IDs and values are arrays of counts. // For an if statement, the value would have two counts; one for the if, and one for the // else. Switch statements would have an array of values for each case. // * `f` - Hash of function counts, where keys are function IDs. // * `fnMap` - Hash of functions where keys are function IDs, and values are `{name, line, loc}`, // where `name` is the name of the function, `line` is the line the function is declared on, // and `loc` is the `Location` of the function declaration (just the declaration, not the entire // function body.) // * `statementMap` - Hash where keys are statement IDs, and values are `Location` objects for each // statement. The `Location` for a function definition is really an assignment, and should // include the entire function. // * `branchMap` - Hash where keys are branch IDs, and values are `{line, type, locations}` objects. // `line` is the line the branch starts on. `type` is the type of the branch (e.g. "if", "switch"). // `locations` is an array of `Location` objects, one for each possible outcome of the branch. // Note for an `if` statement where there is no `else` clause, there will still be two `locations` // generated. Istanbul does *not* generate coverage for the `default` case of a switch statement // if `default` is not explicitly present in the source code. // `locations` for an if statement are always 0-length and located at the start of the `if` (even // the location for the "else"). For a `switch` statement, `locations` start at the start of the // `case` statement and go to the end of the line before the next case statement (note Istanbul // does nothing clever here if a `case` is missing a `break`.) // ## Location Objects // Location objects are a `{start: {line, column}, end: {line, column}, skip}` object that describes // the start and end of a piece of code. Note that `line` is 1-based, but `column` is 0-based. // `skip` is optional - if true it instructs Istanbul to ignore if this location has no executions. // An `### istanbul ignore next ###` before a statement would cause that statement's location // in the `staementMap` to be marked `skip: true`. For an `if` or a `switch`, this should also // cause all desendant statments to be marked `skip`, as well as all locations in the `branchMap`. // An `### istanbul ignore if ###` should cause the loction for the `if` in the `branchMap` to be // marked `skip`, along with all statements inside the `if`. Similar for // `### istanbul ignore else ###`. // An `### istanbul ignore next ###` before a `when` in a `switch` should cause the appropriate // entry in the `branchMap` to be marked skip, and all statements inside the `when`. // (coffeescript doesn't allow block comments at top scope inside a switch. Might not be // able to do this.) // An `### istanbul ignore next ###` before a function declaration should cause the function (not // the location) in the `fnMap` to be marked `skip`, the statement for the function delcaration and // all statements in the function to be marked `skip` in the `statementMap`. var Istanbul, NodeWrapper, _, assert, compareLocations, fileToLines, findInCode, minLocation, nodeToLocation, toQuotedString; assert = require('assert'); _ = require('lodash'); NodeWrapper = require('../NodeWrapper'); ({toQuotedString} = require('../utils/helpers')); ({compareLocations, fileToLines, minLocation} = require('../utils/codeUtils')); nodeToLocation = function(node) { var answer, ref; // Istanbul uses 1-based lines, but 0-based columns answer = { start: { line: node.locationData.first_line + 1, column: node.locationData.first_column }, end: { line: node.locationData.last_line + 1, column: node.locationData.last_column } }; if (((ref = node.coffeeCoverage) != null ? ref.skip : void 0) || (typeof node.isMarked === "function" ? node.isMarked('skip') : void 0)) { answer.skip = true; } return answer; }; // Find a string in the source code, and return a `{line, column}`. // Line is 1-based and column is 0-based. findInCode = function(code, str, options = {}) { var column, currentCol, currentLine, end, ref, ref1, start; start = (ref = options.start) != null ? ref : { line: 1, column: 0 }; end = (ref1 = options.end) != null ? ref1 : { line: code.length + 1, column: 0 }; currentLine = start.line; currentCol = start.column; while (currentLine < end.line) { column = code[currentLine - 1].indexOf(str, currentCol); if (column > -1 && compareLocations({ line: currentLine, column }, end) < 1) { return { line: currentLine, column }; } currentLine++; currentCol = 0; } return null; }; module.exports = Istanbul = class Istanbul { // Return default options for this instrumentor. static getDefaultOptions() { var ref; return { coverageVar: (ref = module.exports.findIstanbulVariable()) != null ? ref : '__coverage__' }; } // Find the runtime Istanbul variable, if it exists. Otherwise, fall back to a sensible default. static findIstanbulVariable() { var coverageVar, coverageVars; coverageVar = `$$cov_${Date.now()}$$`; if (global[coverageVar] == null) { coverageVars = Object.keys(global).filter(function(key) { return _.startsWith(key, '$$cov_'); }); if (coverageVars.length === 1) { coverageVar = coverageVars[0]; } else { // Needs to be undefined and not `null`, because `_.defaults()` treats them differently. coverageVar = void 0; } } return coverageVar; } // `options` is a `{log, coverageVar}` object. constructor(fileName, source, options = {}) { this.fileName = fileName; this.source = source; ({log: this.log, coverageVar: this.coverageVar} = options); options = _.defaults({}, options, Istanbul.getDefaultOptions()); this.sourceLines = fileToLines(this.source); this.quotedFileName = toQuotedString(this.fileName); this.statementMap = []; this.branchMap = []; this.fnMap = []; this.instrumentedLineCount = 0; this.anonId = 1; this._prefix = `${this.coverageVar}[${this.quotedFileName}]`; } /* !pragma coverage-skip-next */ _warn(message, options = {}) { var lineNumber, ref, str; str = message; str += `\n file: ${this.fileName}`; if (options.node) { str += `\n node: ${options.node.toString()}`; } if ((options.line != null) || options.node) { lineNumber = options.line != null ? options.line : options.node.locationData.first_line + 1; str += `\n source: ${this.sourceLines[lineNumber - 1]}`; } return (ref = this.log) != null ? ref.warn(str) : void 0; } // Called on each non-comment statement within a Block. If a `visitXXX` exists for the // specific node type, it will also be called after `visitStatement`. visitStatement(node) { var grandParentType, location, ref, ref1, ref2, ref3, statementId; grandParentType = (ref = node.parent) != null ? (ref1 = ref.parent) != null ? (ref2 = ref1.node) != null ? (ref3 = ref2.constructor) != null ? ref3.name : void 0 : void 0 : void 0 : void 0; if (grandParentType === "StringWithInterpolations" && !node.parent.parent.skipped) { node.parent.parent.skipped = true; return; } // Ignore nodes marked 'noCoverage' if (node.isMarked('noCoverage')) { return; } statementId = this.statementMap.length + 1; location = nodeToLocation(node); if (node.type === 'If') { location.end = this._findEndOfIf(node); } this.statementMap.push(location); node.insertBefore(`${this._prefix}.s[${statementId}]++`); return this.instrumentedLineCount++; } // coffeescript will put the end of an 'If' statement as being right before the start of // the 'else' (which is probably a bug.) Istanbul expects the end to be the end of the last // line in the else (and for chained ifs, Istanbul expects the end of the very last else.) _findEndOfIf(ifNode) { var elseBody, elseChild; assert(ifNode.type === 'If'); elseBody = ifNode.child('elseBody'); if (ifNode.node.isChain || ifNode.isMarked('wasChain')) { assert(elseBody != null); elseChild = elseBody.child('expressions', 0); assert(elseChild.type === 'If'); return this._findEndOfIf(elseChild); } else if (elseBody != null) { return nodeToLocation(elseBody).end; } else { return nodeToLocation(ifNode).end; } } visitIf(node) { var body, bodyPresent, branchId, elseBody, elseBodyPresent, elseLocation, ifLocation, ref; // Ignore nodes marked 'noCoverage' if (node.isMarked('noCoverage')) { return; } branchId = this.branchMap.length + 1; // Make a 0-length `Location` object. ifLocation = nodeToLocation(node); ifLocation.end.line = ifLocation.start.line; ifLocation.end.column = ifLocation.start.column; elseLocation = ifLocation; if (!ifLocation.skip) { elseLocation = _.clone(ifLocation); if (node.isMarked('skipIf')) { ifLocation.skip = true; } if (node.isMarked('skipElse')) { elseLocation.skip = true; } } if (node.node.isChain) { // Chaining is where coffee compiles something into `... else if ...` // instead of '... else {if ...}`. Chaining produces nicer looking coder // with fewer indents, but it also produces code that's harder to instrument // (because we can't add code between the `else` and the `if`), so we turn it off. if ((ref = this.log) != null) { if (typeof ref.debug === "function") { ref.debug(" Disabling chaining for if statement"); } } node.node.isChain = false; node.mark('wasChain', true); } body = node.child('body'); elseBody = node.child('elseBody'); bodyPresent = body && body.node.expressions.length > 0; elseBodyPresent = elseBody && elseBody.node.expressions.length > 0; if (node.parent.type === 'Return' && (!bodyPresent || !elseBodyPresent)) { if (bodyPresent && !elseBodyPresent) { // This is kind of a weird case: // fn = (x) -> // return if x then 10 // return 20 // You might think that second `return` statement was unreachable, but it will actually // be hit if `x` is fasley. We can't add anything to the `elseBody` here, though // without making the the second return statement unreachable. The solution is // to add the instrumentation for the "else" case after the `return`. node.insertAtStart('body', `${this._prefix}.b[${branchId}][0]++`); node.parent.insertAfter(`${this._prefix}.b[${branchId}][1]++`); this.instrumentedLineCount += 2; } else if (elseBodyPresent && !bodyPresent) { // This is the even weirder case: // fn = (x) -> // return if x then else 10 // return 20 node.insertAtStart('elseBody', `${this._prefix}.b[${branchId}][1]++`); node.parent.insertAfter(`${this._prefix}.b[${branchId}][0]++`); this.instrumentedLineCount += 2; } else if (!elseBodyPresent && !bodyPresent) { // Yes, you can do this: // fn = (x) -> // return if x then else // return 20 // but it's a bit stupid, so if you do it, we're just going to ignore this // statement. this._warn("If statement could not be instrumented", {node}); ifLocation.skip = true; elseLocation.skip = true; } } else { if (!bodyPresent) { node.insertAtStart('body', "undefined"); } if (!elseBodyPresent) { node.insertAtStart('elseBody', "undefined"); } node.insertAtStart('body', `${this._prefix}.b[${branchId}][0]++`); node.insertAtStart('elseBody', `${this._prefix}.b[${branchId}][1]++`); this.instrumentedLineCount += 2; } return this.branchMap.push({ line: ifLocation.start.line, loc: ifLocation, type: 'if', locations: [ifLocation, elseLocation] }); } visitSwitch(node) { var branchId, loc, locations; // Ignore nodes marked 'noCoverage' if (node.isMarked('noCoverage')) { return; } branchId = this.branchMap.length + 1; locations = []; locations = node.node.cases.map(([conditions, block]) => { var answer, blockLocation, ref, start, startColumn; start = minLocation(_.flatten([conditions], true).map(function(condition) { return nodeToLocation(condition).start; })); blockLocation = nodeToLocation(block); // start.column is the start of the condition, but we want the start of the // `when`. if ((startColumn = (ref = this.sourceLines[start.line - 1]) != null ? ref.indexOf('when') : void 0) > -1) { start.column = startColumn; } else { /* !pragma coverage-skip-block */ this._warn("Couldn't find 'when'", { node, line: start.line }); // Intelligent guess start.column -= 5; if (start.column < 0) { start.column = 0; } } answer = { start, end: blockLocation.end }; if (node.isMarked('skip') || blockLocation.skip) { answer.skip = true; } return answer; }); if (node.node.otherwise != null) { locations.push(nodeToLocation(node.node.otherwise)); } loc = nodeToLocation(node); this.branchMap.push({ line: loc.start.line, loc, type: 'switch', locations }); node.node.cases.forEach(([conditions, block], index) => { var caseNode; caseNode = new NodeWrapper(block, node, 'cases', index, node.depth + 1); assert.equal(caseNode.type, 'Block'); return caseNode.insertAtStart('expressions', `${this._prefix}.b[${branchId}][${index}]++`); }); return node.forEachChildOfType('otherwise', (otherwise) => { var index; index = node.node.cases.length; assert.equal(otherwise.type, 'Block'); return otherwise.insertAtStart('expressions', `${this._prefix}.b[${branchId}][${index}]++`); }); } visitCode(node) { var arrow, end, endOfFn, fnMapEntry, functionId, isAssign, lastParam, name, paramCount, ref, ref1, ref2, ref3, ref4, start; // Ignore nodes marked 'noCoverage' if (node.isMarked('noCoverage')) { return; } functionId = this.fnMap.length + 1; paramCount = (ref = (ref1 = node.node.params) != null ? ref1.length : void 0) != null ? ref : 0; isAssign = node.parent.type === 'Assign' && (((ref2 = node.parent.node.variable) != null ? (ref3 = ref2.base) != null ? ref3.value : void 0 : void 0) != null); // Figure out the name of this funciton name = isAssign ? node.parent.node.variable.base.value : `(anonymous_${this.anonId++})`; // Find the start and end of the function declaration. start = isAssign ? nodeToLocation(node.parent).start : nodeToLocation(node).start; if (paramCount > 0) { lastParam = node.child('params', paramCount - 1); end = nodeToLocation(lastParam).end; // Coffee-script doesn't tell us where the `->` is, so we have to find it arrow = node.node.bound ? '=>' : '->'; endOfFn = findInCode(this.sourceLines, arrow, { start: { line: end.line, column: end.column }, end: nodeToLocation(node).end }); if (endOfFn) { end = endOfFn; end.column += 1; } else { /* !pragma coverage-skip-block */ this._warn("Couldn't find '->' or '=>'", { node, line: start.line }); // Educated guess end.column += 4; } } else { end = nodeToLocation(node).start; // Fix off-by-one error end.column++; } fnMapEntry = { name, line: start.line, loc: nodeToLocation(node), decl: {start, end} }; if ((ref4 = node.node.coffeeCoverage) != null ? ref4.skip : void 0) { fnMapEntry.skip = true; } this.fnMap.push(fnMapEntry); return node.insertAtStart('body', `${this._prefix}.f[${functionId}]++`); } visitClass(node) { var functionId, loc, ref; // Ignore nodes marked 'noCoverage' if (node.isMarked('noCoverage')) { return; } functionId = this.fnMap.length + 1; if (node.node.variable != null) { loc = nodeToLocation(node.node.variable); } else { loc = nodeToLocation(node); loc.end = loc.start; } this.fnMap.push({ name: (ref = node.node.determineName()) != null ? ref : '_Class', line: loc.start.line, loc: nodeToLocation(node), decl: loc }); return node.insertAtStart('body', `${this._prefix}.f[${functionId}]++`); } getInitString() { var init, initData; initData = { path: this.fileName, s: {}, b: {}, f: {}, fnMap: {}, statementMap: {}, branchMap: {} }; this.statementMap.forEach(function(statement, id) { initData.s[id + 1] = 0; return initData.statementMap[id + 1] = statement; }); this.branchMap.forEach(function(branch, id) { initData.b[id + 1] = (function() { var i, ref, results; results = []; for (i = 0, ref = branch.locations.length; (0 <= ref ? i < ref : i > ref); 0 <= ref ? i++ : i--) { results.push(0); } return results; })(); return initData.branchMap[id + 1] = branch; }); this.fnMap.forEach(function(fn, id) { initData.f[id + 1] = 0; return initData.fnMap[id + 1] = fn; }); return init = `if (typeof ${this.coverageVar} === 'undefined') ${this.coverageVar} = {};\n(function(_export) {\n if (typeof _export.${this.coverageVar} === 'undefined') {\n _export.${this.coverageVar} = ${this.coverageVar};\n }\n})(typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : this);\nif (! ${this._prefix}) { ${this._prefix} = ${JSON.stringify(initData)} }`; } getInstrumentedLineCount() { return this.instrumentedLineCount; } }; }).call(this);