coffee-coverage
Version:
Istanbul and JSCoverage-style instrumentation for CoffeeScript files.
439 lines (374 loc) • 17.5 kB
JavaScript
// Generated by CoffeeScript 2.3.2
(function() {
//### CoffeeCoverage
// JSCoverage-style instrumentation for CoffeeScript files.
// By Jason Walton, Benbria
var CoverageError, EXTENSIONS, INSTRUMENTORS, NodeWrapper, SkipVisitor, _, assert, coffeeScript, events, excludeFile, factoryDefaults, fs, getInstrumentorClass, mkdirs, path, statFile, util;
assert = require('assert');
events = require('events');
fs = require('fs');
util = require('util');
path = require('path');
coffeeScript = require('coffeescript');
_ = require('lodash');
NodeWrapper = require('./NodeWrapper');
({mkdirs, statFile, excludeFile} = require('./utils/helpers'));
({EXTENSIONS} = require('./constants'));
SkipVisitor = require('./SkipVisitor');
exports.INSTRUMENTORS = INSTRUMENTORS = {
jscoverage: require('./instrumentors/JSCoverage'),
istanbul: require('./instrumentors/Istanbul')
};
CoverageError = class CoverageError extends Error {
constructor(message) {
super();
this.message = message;
this.name = "CoverageError";
Error.captureStackTrace(this, CoverageError);
}
};
// Default options.
factoryDefaults = {
exclude: [],
recursive: true,
bare: false,
instrumentor: 'jscoverage'
};
exports.getInstrumentorClass = getInstrumentorClass = function(instrumentorName) {
var instrumentor;
instrumentor = INSTRUMENTORS[instrumentorName];
if (!instrumentor) {
throw new Error(`Invalid instrumentor ${instrumentorName}. Valid options are: ${Object.keys(INSTRUMENTORS).join(', ')}`);
}
return instrumentor;
};
exports.CoverageInstrumentor = (function() {
var getEffectiveOptions, validateSrcDest, writeToFile;
//### CoverageInstrumentor
// Instruments .coffee files to provide code-coverage data.
class CoverageInstrumentor extends events.EventEmitter {
//### Create a new CoverageInstrumentor
// For a list of available options see `@instrument`.
constructor(options = {}) {
super();
this.defaultOptions = _.defaults({}, options, factoryDefaults);
_.defaults(this.defaultOptions, getInstrumentorClass(this.defaultOptions.instrumentor).getDefaultOptions());
}
//### Instrument a file or directory.
// This calls @instrumentFile or @instrumentDirectory, depending on whether "source" is
// a file or directory respectively.
// * `options.coverageVar` gives the name of the global variable to use to store
// coverage data in. This defaults to '_$jscoverage' to be compatible with
// JSCoverage.
// * `options.recursive` controls whether or not this will descend recursively into
// subdirectories. This defaults to true.
// * `options.exclude` is an array of files to ignore. instrumentDirectory will
// not instrument a file if it is in this list, nor will it recursively traverse
// into a directory if it is in this list. This defaults to [].
// Note that this field is case sensitive!
// * `options.basePath` if provided, then all excludes will be evaluated relative
// to this base path. For example, if `options.exclude` is `['a/b']`, and
// `options.basePath` is "/Users/jwalton/myproject", then this will prevent
// coffeeCoverage from traversing "/Users/jwalton/myproject/a/b". `basePath`
// will also be stripped from the front of any files when generating names.
// * `options.initFileStream` is a stream to which all global initialization will be
// written to via `initFileStream.write(data)`.
// * `options.log` should be a `{debug(), info(), warn(), error()}` object, where each is a function
// that takes multiple parameters and logs them (similar to `console.log()`.)
// * `options.instrumentor` is the name of the instrumentor to use (see `INSTURMENTORS`.)
// All options passed in will be passed along to the instrumentor implementation, so
// instrumentor-specific options may be added to `options` as well.
// Throws CoverageError if there is a problem with the `source` or `out` parameters.
instrument(source, out, options = {}) {
var sourceStat;
({sourceStat} = validateSrcDest(source, out));
if (sourceStat.isFile()) {
return this.instrumentFile(source, out, options);
} else if (sourceStat.isDirectory()) {
return this.instrumentDirectory(source, out, options);
} else {
throw new CoverageError(`Can't instrument ${source}.`);
}
}
// Return the output file name for a given input file name.
// e.g. `getOutputFileName('foo.coffee') # => 'foo.js'`
getOutputFileName(fileName) {
var coffee_extension, ext, outFile;
if (fileName == null) {
return null;
}
outFile = fileName;
for (coffee_extension in EXTENSIONS) {
ext = EXTENSIONS[coffee_extension];
if (_.endsWith(fileName.toLowerCase(), coffee_extension)) {
outFile = fileName.slice(0, +(-(coffee_extension.length + 1)) + 1 || 9e9) + ext.js_extension;
break;
}
}
return outFile;
}
//### Instrument a directory.
// This finds all .coffee files in the specified `sourceDirectory`, and writes instrumented
// files into `outDirectory`. `outDirectory` will be created if it does not already exist.
// For a list of available options see `@instrument`.
// Emits an "instrumentingDirectory" event before doing any work, with the names of the source
// and out directories. The directory names are guaranteed to end in path.sep. Emits a
// "skip" event for any files which are skipped because they are in the `options.exclude` list.
// Throws CoverageError if there is a problem with the `sourceDirectory` or `outDirectory`
// parameters.
// Returns an object consisting of:
// - `lines` - the total number of instrumented lines.
// Returns null if `sourceDirectory` does not exist.
instrumentDirectory(sourceDirectory, outDirectory, options = {}) {
var answer, coffee_extension, effectiveOptions, file, inst, j, len, outDirectoryStat, outFile, outputDirectoryExists, processed, ref, ref1, ref2, sourceDirectoryMode, sourceDirectoryStat, sourceFile, sourceStat;
// Turn the source directory into an absolute path
sourceDirectory = path.resolve(sourceDirectory);
sourceDirectoryStat = statFile(sourceDirectory);
if (!sourceDirectoryStat) {
return null;
}
this.emit("instrumentingDirectory", sourceDirectory, outDirectory);
effectiveOptions = getEffectiveOptions(options, this.defaultOptions);
effectiveOptions.basePath = effectiveOptions.basePath ? path.resolve(effectiveOptions.basePath) : sourceDirectory;
answer = {
lines: 0
};
validateSrcDest(sourceDirectory, outDirectory);
if (!_.endsWith(sourceDirectory, path.sep)) {
sourceDirectory += path.sep;
}
sourceDirectoryMode = sourceDirectoryStat.mode;
if (outDirectory) {
if (!_.endsWith(outDirectory, path.sep)) {
outDirectory += path.sep;
}
// Check to see if the output directory exists
outDirectoryStat = statFile(outDirectory);
outputDirectoryExists = !!outDirectoryStat;
}
ref = fs.readdirSync(sourceDirectory);
// Instrument every file in the directory
for (j = 0, len = ref.length; j < len; j++) {
file = ref[j];
sourceFile = sourceDirectory + file;
if (excludeFile(sourceFile, effectiveOptions)) {
this.emit("skip", sourceDirectory + file);
continue;
}
sourceStat = statFile(sourceFile);
if (!sourceStat) {
// Can happen if file or folder is deleted while we're instrumenting.
// Also, see https://github.com/benbria/coffee-coverage/issues/54.
continue;
}
outFile = outDirectory ? outDirectory + file : null;
if (effectiveOptions.recursive && sourceStat.isDirectory()) {
inst = this.instrumentDirectory(sourceFile, outFile, effectiveOptions);
answer.lines += (ref1 = inst != null ? inst.lines : void 0) != null ? ref1 : 0;
} else {
processed = false;
for (coffee_extension in EXTENSIONS) {
// TODO: Make this work for streamline files.
if (coffee_extension === '._coffee') {
continue;
}
if (_.endsWith(file.toLowerCase(), coffee_extension) && sourceStat.isFile()) {
// lazy-create the output directory.
if ((outDirectory != null) && !outputDirectoryExists) {
mkdirs(outDirectory, sourceDirectoryMode);
outputDirectoryExists = true;
}
// Replace the ".(lit)coffee(.md)" extension with a ".js" extension
outFile = this.getOutputFileName(outFile);
inst = this.instrumentFile(sourceFile, outFile, effectiveOptions);
answer.lines += (ref2 = inst != null ? inst.lines : void 0) != null ? ref2 : 0;
processed = true;
break;
}
}
}
}
return answer;
}
//### Instrument a .coffee file.
// Same as `@instrumentCoffee` but takes a file name instead of file data.
// Emits an "instrumentingFile" event with the name of the input and output file.
// * `outFile` is optional; if present then the compiled JavaScript will be written out to this
// file.
// * `options.fileName` is the fileName to use in the generated instrumentation.
// For other options, see `@instrumentCoffee` and `@instrument`.
// Throws CoverageError if there is a problem with the `sourceFile` or `outFile` parameters.
instrumentFile(sourceFile, outFile = null, options = {}) {
var answer, data, effectiveOptions;
this.emit("instrumentingFile", sourceFile, outFile);
effectiveOptions = getEffectiveOptions(options, this.defaultOptions);
validateSrcDest(sourceFile, outFile);
data = fs.readFileSync(sourceFile, 'utf8');
answer = this.instrumentCoffee(path.resolve(sourceFile), data, effectiveOptions);
if (outFile) {
writeToFile(outFile, answer.init + answer.js);
}
return answer;
}
//### Instrument a .coffee file.
// Parameters:
// * `fileName` is the name of the file. This should be an absolute path.
// * `source` is the contents of the coffee file.
// * `options.fileName` - if rpresent, this will be the filename passed to the instrumentor.
// Otherwise the absolute path will be passed.
// * 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.
// If `options.usedFileNames` is present, it must be an array - this is the deprecated version
// of `usedFileNameMap`.
// * If `options.initFileStream` is present, then all global initialization will be written
// to `initFileStream.write()`, in addition to being returned.
// Returns an object consisting of:
// * `init` - the intialization JavaScript code.
// * `js` - the compiled JavaScript, instrumented to collect coverage data.
// * `lines` - the total number of instrumented lines.
instrumentCoffee(fileName, source, options = {}) {
var effectiveOptions, instrumentor, instrumentorConstructor, ref, ref1, result;
effectiveOptions = getEffectiveOptions(options, this.defaultOptions);
if ((ref = effectiveOptions.log) != null) {
ref.info(`Instrumenting ${fileName}`);
}
instrumentorConstructor = getInstrumentorClass(effectiveOptions.instrumentor);
instrumentor = new instrumentorConstructor(fileName, source, effectiveOptions);
result = exports._runInstrumentor(instrumentor, fileName, source, effectiveOptions);
if ((ref1 = effectiveOptions.initFileStream) != null) {
ref1.write(result.init);
}
return result;
}
};
// Write a string to a file.
writeToFile = function(outFile, content) {
return fs.writeFileSync(outFile, content);
};
// Some basic valication of source and out files.
validateSrcDest = function(source, out) {
var outStat, sourceStat;
sourceStat = statFile(source);
outStat = out ? statFile(out) : null;
if (!sourceStat) {
throw new CoverageError(`Source file ${source} does not exist.`);
}
if (outStat) {
if (sourceStat.isFile() && outStat.isDirectory()) {
throw new CoverageError(`Refusing to overwrite directory ${out} with file.`);
}
if (sourceStat.isDirectory() && outStat.isFile()) {
throw new CoverageError(`Refusing to overwrite file ${out} with directory.`);
}
}
return {sourceStat, outStat};
};
getEffectiveOptions = function(options = {}, defaultOptions) {
return _.defaults({}, options, defaultOptions);
};
return CoverageInstrumentor;
}).call(this);
// Runs an instrumentor on some source code.
// * `instrumentor` an instance of an instrumentor class to run on.
// * `fileName` the absolute path of the source file.
// * `source` a string containing the sourcecode the instrument.
// * `options.bare` true if we should compile bare coffeescript (no enclosing function).
// * `options.log` log object.
exports._runInstrumentor = function(instrumentor, fileName, source, options = {}) {
var answer, ast, coffeeOptions, err, init, js, ref, ref1, runVisitor, token, tokens, wrappedAST;
assert(instrumentor, "instrumentor");
try {
// Compile coffee to nodes.
if ((ref = options.log) != null) {
if (typeof ref.debug === "function") {
ref.debug(`Instrumenting ${fileName}`);
}
}
coffeeOptions = {
bare: (ref1 = options.bare) != null ? ref1 : false,
literate: /\.(litcoffee|coffee\.md)$/.test(fileName)
};
tokens = coffeeScript.tokens(source, coffeeOptions);
// collect referenced variables
coffeeOptions.referencedVars = _.uniq((function() {
var j, len, results;
results = [];
for (j = 0, len = tokens.length; j < len; j++) {
token = tokens[j];
if (token[0] === 'IDENTIFIER') {
results.push(token[1]);
}
}
return results;
})());
// convert tokens to ast
ast = coffeeScript.nodes(tokens);
} catch (error) {
err = error;
throw new CoverageError(`Could not parse ${fileName}: ${err.stack}`);
}
runVisitor = function(visitor, nodeWrapper) {
var __, i, indent, j, len, name, ref2, ref3, ref4;
// Ignore code that we generated.
if ((ref2 = nodeWrapper.node.coffeeCoverage) != null ? ref2.generated : void 0) {
return;
}
if (((ref3 = options.log) != null ? ref3.debug : void 0) != null) {
indent = ((function() {
var j, ref4, results;
results = [];
for (i = j = 0, ref4 = nodeWrapper.depth; (0 <= ref4 ? j < ref4 : j > ref4); i = 0 <= ref4 ? ++j : --j) {
results.push(" ");
}
return results;
})()).join('');
options.log.debug(`${indent}Examining ${nodeWrapper.toString()}`);
}
if (nodeWrapper.node.comments) {
if (typeof visitor["visitComment"] === "function") {
visitor["visitComment"](nodeWrapper);
}
}
if (nodeWrapper.isStatement) {
if (typeof visitor["visitStatement"] === "function") {
visitor["visitStatement"](nodeWrapper);
}
}
if (typeof visitor[name = `visit${nodeWrapper.type}`] === "function") {
visitor[name](nodeWrapper);
}
if (nodeWrapper.isSwitchCases) {
ref4 = nodeWrapper.node;
for (i = j = 0, len = ref4.length; j < len; i = ++j) {
__ = ref4[i];
nodeWrapper.forEachChildOfType(i, function(child) {
return runVisitor(visitor, child);
});
}
}
// Recurse into child nodes
return nodeWrapper.forEachChild(function(child) {
return runVisitor(visitor, child);
});
};
wrappedAST = new NodeWrapper(ast);
runVisitor(new SkipVisitor(fileName), wrappedAST);
runVisitor(instrumentor, wrappedAST);
init = instrumentor.getInitString();
try {
// Compile the instrumented CoffeeScript and write it to the JS file.
js = ast.compile(coffeeOptions);
} catch (error) {
err = error;
/* !pragma coverage-skip-block */
throw new CoverageError(`Could not compile ${fileName} after instrumenting: ${err.stack}`);
}
answer = {
init: init,
js: js,
lines: instrumentor.getInstrumentedLineCount()
};
return answer;
};
}).call(this);