coffee-coverage
Version:
Istanbul and JSCoverage-style instrumentation for CoffeeScript files.
212 lines (187 loc) • 8.7 kB
JavaScript
// Generated by CoffeeScript 2.3.2
(function() {
// This is an instrumentor which provides [JSCoverage](http://siliconforks.com/jscoverage/) style
// instrumentation. This will add a `_$jscoverage` variable to the source code, which is
// a hash where keys are file names, and values are sparse arrays where keys are line number and
// values are the count that the given line was executed. In addition,
// `_$jscoverage['filename'].source` will be an array containing a copy of the original source code
// split into lines.
var JSCoverage, _, fileToLines, generateUniqueName, getRelativeFilename, path, stripLeadingDotOrSlash, toQuotedString,
indexOf = [].indexOf;
path = require('path');
_ = require('lodash');
({toQuotedString, stripLeadingDotOrSlash, getRelativeFilename} = require('../utils/helpers'));
({fileToLines} = require('../utils/codeUtils'));
// Generate a unique file name
generateUniqueName = function(usedNames, desiredName) {
var answer, suffix;
answer = "";
suffix = 1;
while (true) {
answer = desiredName + " (" + suffix + ")";
if (!(indexOf.call(usedNames, answer) >= 0)) {
break;
}
suffix++;
}
return answer;
};
module.exports = JSCoverage = class JSCoverage {
// Return default options for this instrumentor.
static getDefaultOptions() {
return {
path: 'bare',
usedFileNameMap: {},
coverageVar: '_$jscoverage'
};
}
// `options` is a `{log, coverageVar, basePath, path, usedFileNameMap}` object.
// * `options.path` should be one of:
// * 'relative' - file names will have the `basePath` stripped from them.
// * 'abbr' - an abbreviated file name will be constructed, with each parent in the path
// replaced by the first character in its name.
// * 'bare' (default) - Path names will be omitted. Only the base file name will be used.
// * If `options.usedFileNameMap` is present, it must be an object. This method will add a
// mapping from the absolute file path to the short filename in usedFileNameMap. If the name
// of the file is already in usedFileNameMap then this method will generate a unique name.
// `options.usedFileNames` is the deprecated array version of `usedFileNameMap`.
constructor(fileName, source, options = {}) {
var ref, relativeFileName;
this.fileName = fileName;
this.source = source;
({log: this.log, coverageVar: this.coverageVar} = options);
options = _.defaults({}, options, JSCoverage.getDefaultOptions());
this.instrumentedLines = [];
relativeFileName = getRelativeFilename(options.basePath, this.fileName);
this.shortFileName = ((ref = options.usedFileNameMap) != null ? ref[this.fileName] : void 0) || (() => {
var shortFileName, usedFileNames;
shortFileName = (function() {
switch (options.path) {
case 'relative':
return stripLeadingDotOrSlash(relativeFileName);
case 'abbr':
return this._abbreviatedPath(stripLeadingDotOrSlash(relativeFileName));
default:
return path.basename(relativeFileName);
}
}).call(this);
// Generate a unique fileName if required.
if (options.usedFileNames != null) {
// `usedFileNames` is deprecated, but prefer it over `userFileNameMap`, since
// `usedFileNameMap` will always be present thanks to the defaults.
if (indexOf.call(options.usedFileNames, shortFileName) >= 0) {
shortFileName = generateUniqueName(options.usedFileNames, shortFileName);
}
options.usedFileNames.push(shortFileName);
} else if (options.usedFileNameMap != null) {
usedFileNames = _.values(options.usedFileNameMap);
if (indexOf.call(usedFileNames, shortFileName) >= 0) {
shortFileName = generateUniqueName(usedFileNames, shortFileName);
}
options.usedFileNameMap[this.fileName] = shortFileName;
}
return shortFileName;
})();
this.quotedFileName = toQuotedString(this.shortFileName);
}
// Converts a path like "./foo/bar/baz" to "./f/b/baz"
_abbreviatedPath(pathName) {
var answer, filename, i, len, needTrailingSlash, pathElement, splitPath;
needTrailingSlash = false;
splitPath = pathName.split(path.sep);
if (splitPath.slice(-1)[0] === '') {
needTrailingSlash = true;
splitPath.pop();
}
filename = splitPath.pop();
answer = "";
for (i = 0, len = splitPath.length; i < len; i++) {
pathElement = splitPath[i];
if (pathElement.length === 0) {
answer += "";
} else if (pathElement === "..") {
answer += pathElement;
} else if (_.startsWith(pathElement, ".")) {
answer += pathElement.slice(0, 2);
} else {
answer += pathElement[0];
}
answer += path.sep;
}
answer += filename;
if (needTrailingSlash) {
answer += path.sep;
}
return answer;
}
// 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 line, ref, ref1;
// Don't instrument skipped lines.
if (node.isMarked('skip') || node.isMarked('noCoverage')) {
return;
}
line = node.locationData.first_line + 1;
if (indexOf.call(this.instrumentedLines, line) >= 0) {
// Never instrument the same line twice. This can happen in a situation like:
// if x then console.log "foo"
// Here the "if" statement can be instrumented, but we could also instrument the
// "console.log" statement on the same line.
// Note that we also run into a weird situation here:
// x = if y then {name: "foo"} \
// else {name: "bar"}
// Because here we're going to instrument the inside of the "else" block,
// but not the inside of the "if" block, which is OK, but a bit weird.
return (ref = this.log) != null ? typeof ref.debug === "function" ? ref.debug(`Skipping ${node.toString()}`) : void 0 : void 0;
} else {
if ((ref1 = this.log) != null) {
if (typeof ref1.debug === "function") {
ref1.debug(`Instrumenting ${node.toString()}`);
}
}
this.instrumentedLines.push(line);
return node.insertBefore(`${this.coverageVar}[${this.quotedFileName}][${line}]++`);
}
}
visitIf(node) {
var ref;
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");
}
}
return node.node.isChain = false;
}
}
getInitString() {
var fileToInstrumentLines, i, index, init, j, len, len1, line, lineNumber, ref;
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.coverageVar}[${this.quotedFileName}]) {\n ${this.coverageVar}[${this.quotedFileName}] = [];\n`;
ref = this.instrumentedLines;
for (i = 0, len = ref.length; i < len; i++) {
lineNumber = ref[i];
init += ` ${this.coverageVar}[${this.quotedFileName}][${lineNumber}] = 0;\n`;
}
init += "}\n\n";
// Write the original source code into the ".source" array.
init += `${this.coverageVar}[${this.quotedFileName}].source = [`;
fileToInstrumentLines = fileToLines(this.source);
for (index = j = 0, len1 = fileToInstrumentLines.length; j < len1; index = ++j) {
line = fileToInstrumentLines[index];
if (!!index) {
init += ", ";
}
init += toQuotedString(line);
}
return init += "];\n\n";
}
getInstrumentedLineCount() {
return this.instrumentedLines.length;
}
};
}).call(this);