UNPKG

coffee-coverage

Version:

Istanbul and JSCoverage-style instrumentation for CoffeeScript files.

212 lines (187 loc) 8.7 kB
// 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);