| "use strict"; |
| |
| Object.defineProperty(exports, "__esModule", { |
| value: true |
| }); |
| exports.default = void 0; |
| |
| var _crypto = require("crypto"); |
| |
| var _template = _interopRequireDefault(require("@babel/template")); |
| |
| var _sourceCoverage = require("./source-coverage"); |
| |
| var _constants = require("./constants"); |
| |
| var _instrumenter = require("./instrumenter"); |
| |
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } |
| |
| // pattern for istanbul to ignore a section |
| const COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/; // pattern for istanbul to ignore the whole file |
| |
| const COMMENT_FILE_RE = /^\s*istanbul\s+ignore\s+(file)(?=\W|$)/; // source map URL pattern |
| |
| const SOURCE_MAP_RE = /[#@]\s*sourceMappingURL=(.*)\s*$/m; // generate a variable name from hashing the supplied file path |
| |
| function genVar(filename) { |
| const hash = (0, _crypto.createHash)(_constants.SHA); |
| hash.update(filename); |
| return 'cov_' + parseInt(hash.digest('hex').substr(0, 12), 16).toString(36); |
| } // VisitState holds the state of the visitor, provides helper functions |
| // and is the `this` for the individual coverage visitors. |
| |
| |
| class VisitState { |
| constructor(types, sourceFilePath, inputSourceMap, ignoreClassMethods = []) { |
| this.varName = genVar(sourceFilePath); |
| this.attrs = {}; |
| this.nextIgnore = null; |
| this.cov = new _sourceCoverage.SourceCoverage(sourceFilePath); |
| |
| if (typeof inputSourceMap !== 'undefined') { |
| this.cov.inputSourceMap(inputSourceMap); |
| } |
| |
| this.ignoreClassMethods = ignoreClassMethods; |
| this.types = types; |
| this.sourceMappingURL = null; |
| } // should we ignore the node? Yes, if specifically ignoring |
| // or if the node is generated. |
| |
| |
| shouldIgnore(path) { |
| return this.nextIgnore || !path.node.loc; |
| } // extract the ignore comment hint (next|if|else) or null |
| |
| |
| hintFor(node) { |
| let hint = null; |
| |
| if (node.leadingComments) { |
| node.leadingComments.forEach(c => { |
| const v = (c.value || |
| /* istanbul ignore next: paranoid check */ |
| '').trim(); |
| const groups = v.match(COMMENT_RE); |
| |
| if (groups) { |
| hint = groups[1]; |
| } |
| }); |
| } |
| |
| return hint; |
| } // extract a source map URL from comments and keep track of it |
| |
| |
| maybeAssignSourceMapURL(node) { |
| const extractURL = comments => { |
| if (!comments) { |
| return; |
| } |
| |
| comments.forEach(c => { |
| const v = (c.value || |
| /* istanbul ignore next: paranoid check */ |
| '').trim(); |
| const groups = v.match(SOURCE_MAP_RE); |
| |
| if (groups) { |
| this.sourceMappingURL = groups[1]; |
| } |
| }); |
| }; |
| |
| extractURL(node.leadingComments); |
| extractURL(node.trailingComments); |
| } // for these expressions the statement counter needs to be hoisted, so |
| // function name inference can be preserved |
| |
| |
| counterNeedsHoisting(path) { |
| return path.isFunctionExpression() || path.isArrowFunctionExpression() || path.isClassExpression(); |
| } // all the generic stuff that needs to be done on enter for every node |
| |
| |
| onEnter(path) { |
| const n = path.node; |
| this.maybeAssignSourceMapURL(n); // if already ignoring, nothing more to do |
| |
| if (this.nextIgnore !== null) { |
| return; |
| } // check hint to see if ignore should be turned on |
| |
| |
| const hint = this.hintFor(n); |
| |
| if (hint === 'next') { |
| this.nextIgnore = n; |
| return; |
| } // else check custom node attribute set by a prior visitor |
| |
| |
| if (this.getAttr(path.node, 'skip-all') !== null) { |
| this.nextIgnore = n; |
| } // else check for ignored class methods |
| |
| |
| if (path.isFunctionExpression() && this.ignoreClassMethods.some(name => path.node.id && name === path.node.id.name)) { |
| this.nextIgnore = n; |
| return; |
| } |
| |
| if (path.isClassMethod() && this.ignoreClassMethods.some(name => name === path.node.key.name)) { |
| this.nextIgnore = n; |
| return; |
| } |
| } // all the generic stuff on exit of a node, |
| // including reseting ignores and custom node attrs |
| |
| |
| onExit(path) { |
| // restore ignore status, if needed |
| if (path.node === this.nextIgnore) { |
| this.nextIgnore = null; |
| } // nuke all attributes for the node |
| |
| |
| delete path.node.__cov__; |
| } // set a node attribute for the supplied node |
| |
| |
| setAttr(node, name, value) { |
| node.__cov__ = node.__cov__ || {}; |
| node.__cov__[name] = value; |
| } // retrieve a node attribute for the supplied node or null |
| |
| |
| getAttr(node, name) { |
| const c = node.__cov__; |
| |
| if (!c) { |
| return null; |
| } |
| |
| return c[name]; |
| } // |
| |
| |
| increase(type, id, index) { |
| const T = this.types; |
| const wrap = index !== null ? // If `index` present, turn `x` into `x[index]`. |
| x => T.memberExpression(x, T.numericLiteral(index), true) : x => x; |
| return T.updateExpression('++', wrap(T.memberExpression(T.memberExpression(T.identifier(this.varName), T.identifier(type)), T.numericLiteral(id), true))); |
| } |
| |
| insertCounter(path, increment) { |
| const T = this.types; |
| |
| if (path.isBlockStatement()) { |
| path.node.body.unshift(T.expressionStatement(increment)); |
| } else if (path.isStatement()) { |
| path.insertBefore(T.expressionStatement(increment)); |
| } else if (this.counterNeedsHoisting(path) && T.isVariableDeclarator(path.parentPath)) { |
| // make an attempt to hoist the statement counter, so that |
| // function names are maintained. |
| const parent = path.parentPath.parentPath; |
| |
| if (parent && T.isExportNamedDeclaration(parent.parentPath)) { |
| parent.parentPath.insertBefore(T.expressionStatement(increment)); |
| } else if (parent && (T.isProgram(parent.parentPath) || T.isBlockStatement(parent.parentPath))) { |
| parent.insertBefore(T.expressionStatement(increment)); |
| } else { |
| path.replaceWith(T.sequenceExpression([increment, path.node])); |
| } |
| } |
| /* istanbul ignore else: not expected */ |
| else if (path.isExpression()) { |
| path.replaceWith(T.sequenceExpression([increment, path.node])); |
| } else { |
| console.error('Unable to insert counter for node type:', path.node.type); |
| } |
| } |
| |
| insertStatementCounter(path) { |
| /* istanbul ignore if: paranoid check */ |
| if (!(path.node && path.node.loc)) { |
| return; |
| } |
| |
| const index = this.cov.newStatement(path.node.loc); |
| const increment = this.increase('s', index, null); |
| this.insertCounter(path, increment); |
| } |
| |
| insertFunctionCounter(path) { |
| const T = this.types; |
| /* istanbul ignore if: paranoid check */ |
| |
| if (!(path.node && path.node.loc)) { |
| return; |
| } |
| |
| const n = path.node; |
| let dloc = null; // get location for declaration |
| |
| switch (n.type) { |
| case 'FunctionDeclaration': |
| /* istanbul ignore else: paranoid check */ |
| if (n.id) { |
| dloc = n.id.loc; |
| } |
| |
| break; |
| |
| case 'FunctionExpression': |
| if (n.id) { |
| dloc = n.id.loc; |
| } |
| |
| break; |
| } |
| |
| if (!dloc) { |
| dloc = { |
| start: n.loc.start, |
| end: { |
| line: n.loc.start.line, |
| column: n.loc.start.column + 1 |
| } |
| }; |
| } |
| |
| const name = path.node.id ? path.node.id.name : path.node.name; |
| const index = this.cov.newFunction(name, dloc, path.node.body.loc); |
| const increment = this.increase('f', index, null); |
| const body = path.get('body'); |
| /* istanbul ignore else: not expected */ |
| |
| if (body.isBlockStatement()) { |
| body.node.body.unshift(T.expressionStatement(increment)); |
| } else { |
| console.error('Unable to process function body node type:', path.node.type); |
| } |
| } |
| |
| getBranchIncrement(branchName, loc) { |
| const index = this.cov.addBranchPath(branchName, loc); |
| return this.increase('b', branchName, index); |
| } |
| |
| insertBranchCounter(path, branchName, loc) { |
| const increment = this.getBranchIncrement(branchName, loc || path.node.loc); |
| this.insertCounter(path, increment); |
| } |
| |
| findLeaves(node, accumulator, parent, property) { |
| if (!node) { |
| return; |
| } |
| |
| if (node.type === 'LogicalExpression') { |
| const hint = this.hintFor(node); |
| |
| if (hint !== 'next') { |
| this.findLeaves(node.left, accumulator, node, 'left'); |
| this.findLeaves(node.right, accumulator, node, 'right'); |
| } |
| } else { |
| accumulator.push({ |
| node, |
| parent, |
| property |
| }); |
| } |
| } |
| |
| } // generic function that takes a set of visitor methods and |
| // returns a visitor object with `enter` and `exit` properties, |
| // such that: |
| // |
| // * standard entry processing is done |
| // * the supplied visitors are called only when ignore is not in effect |
| // This relieves them from worrying about ignore states and generated nodes. |
| // * standard exit processing is done |
| // |
| |
| |
| function entries(...enter) { |
| // the enter function |
| const wrappedEntry = function wrappedEntry(path, node) { |
| this.onEnter(path); |
| |
| if (this.shouldIgnore(path)) { |
| return; |
| } |
| |
| enter.forEach(e => { |
| e.call(this, path, node); |
| }); |
| }; |
| |
| const exit = function exit(path, node) { |
| this.onExit(path, node); |
| }; |
| |
| return { |
| enter: wrappedEntry, |
| exit |
| }; |
| } |
| |
| function coverStatement(path) { |
| this.insertStatementCounter(path); |
| } |
| /* istanbul ignore next: no node.js support */ |
| |
| |
| function coverAssignmentPattern(path) { |
| const n = path.node; |
| const b = this.cov.newBranch('default-arg', n.loc); |
| this.insertBranchCounter(path.get('right'), b); |
| } |
| |
| function coverFunction(path) { |
| this.insertFunctionCounter(path); |
| } |
| |
| function coverVariableDeclarator(path) { |
| this.insertStatementCounter(path.get('init')); |
| } |
| |
| function coverClassPropDeclarator(path) { |
| this.insertStatementCounter(path.get('value')); |
| } |
| |
| function makeBlock(path) { |
| const T = this.types; |
| |
| if (!path.node) { |
| path.replaceWith(T.blockStatement([])); |
| } |
| |
| if (!path.isBlockStatement()) { |
| path.replaceWith(T.blockStatement([path.node])); |
| path.node.loc = path.node.body[0].loc; |
| } |
| } |
| |
| function blockProp(prop) { |
| return function (path) { |
| makeBlock.call(this, path.get(prop)); |
| }; |
| } |
| |
| function makeParenthesizedExpressionForNonIdentifier(path) { |
| const T = this.types; |
| |
| if (path.node && !path.isIdentifier()) { |
| path.replaceWith(T.parenthesizedExpression(path.node)); |
| } |
| } |
| |
| function parenthesizedExpressionProp(prop) { |
| return function (path) { |
| makeParenthesizedExpressionForNonIdentifier.call(this, path.get(prop)); |
| }; |
| } |
| |
| function convertArrowExpression(path) { |
| const n = path.node; |
| const T = this.types; |
| |
| if (!T.isBlockStatement(n.body)) { |
| const bloc = n.body.loc; |
| |
| if (n.expression === true) { |
| n.expression = false; |
| } |
| |
| n.body = T.blockStatement([T.returnStatement(n.body)]); // restore body location |
| |
| n.body.loc = bloc; // set up the location for the return statement so it gets |
| // instrumented |
| |
| n.body.body[0].loc = bloc; |
| } |
| } |
| |
| function coverIfBranches(path) { |
| const n = path.node; |
| const hint = this.hintFor(n); |
| const ignoreIf = hint === 'if'; |
| const ignoreElse = hint === 'else'; |
| const branch = this.cov.newBranch('if', n.loc); |
| |
| if (ignoreIf) { |
| this.setAttr(n.consequent, 'skip-all', true); |
| } else { |
| this.insertBranchCounter(path.get('consequent'), branch, n.loc); |
| } |
| |
| if (ignoreElse) { |
| this.setAttr(n.alternate, 'skip-all', true); |
| } else { |
| this.insertBranchCounter(path.get('alternate'), branch, n.loc); |
| } |
| } |
| |
| function createSwitchBranch(path) { |
| const b = this.cov.newBranch('switch', path.node.loc); |
| this.setAttr(path.node, 'branchName', b); |
| } |
| |
| function coverSwitchCase(path) { |
| const T = this.types; |
| const b = this.getAttr(path.parentPath.node, 'branchName'); |
| /* istanbul ignore if: paranoid check */ |
| |
| if (b === null) { |
| throw new Error('Unable to get switch branch name'); |
| } |
| |
| const increment = this.getBranchIncrement(b, path.node.loc); |
| path.node.consequent.unshift(T.expressionStatement(increment)); |
| } |
| |
| function coverTernary(path) { |
| const n = path.node; |
| const branch = this.cov.newBranch('cond-expr', path.node.loc); |
| const cHint = this.hintFor(n.consequent); |
| const aHint = this.hintFor(n.alternate); |
| |
| if (cHint !== 'next') { |
| this.insertBranchCounter(path.get('consequent'), branch); |
| } |
| |
| if (aHint !== 'next') { |
| this.insertBranchCounter(path.get('alternate'), branch); |
| } |
| } |
| |
| function coverLogicalExpression(path) { |
| const T = this.types; |
| |
| if (path.parentPath.node.type === 'LogicalExpression') { |
| return; // already processed |
| } |
| |
| const leaves = []; |
| this.findLeaves(path.node, leaves); |
| const b = this.cov.newBranch('binary-expr', path.node.loc); |
| |
| for (let i = 0; i < leaves.length; i += 1) { |
| const leaf = leaves[i]; |
| const hint = this.hintFor(leaf.node); |
| |
| if (hint === 'next') { |
| continue; |
| } |
| |
| const increment = this.getBranchIncrement(b, leaf.node.loc); |
| |
| if (!increment) { |
| continue; |
| } |
| |
| leaf.parent[leaf.property] = T.sequenceExpression([increment, leaf.node]); |
| } |
| } |
| |
| const codeVisitor = { |
| ArrowFunctionExpression: entries(convertArrowExpression, coverFunction), |
| AssignmentPattern: entries(coverAssignmentPattern), |
| BlockStatement: entries(), |
| // ignore processing only |
| ExportDefaultDeclaration: entries(), |
| // ignore processing only |
| ExportNamedDeclaration: entries(), |
| // ignore processing only |
| ClassMethod: entries(coverFunction), |
| ClassDeclaration: entries(parenthesizedExpressionProp('superClass')), |
| ClassProperty: entries(coverClassPropDeclarator), |
| ClassPrivateProperty: entries(coverClassPropDeclarator), |
| ObjectMethod: entries(coverFunction), |
| ExpressionStatement: entries(coverStatement), |
| BreakStatement: entries(coverStatement), |
| ContinueStatement: entries(coverStatement), |
| DebuggerStatement: entries(coverStatement), |
| ReturnStatement: entries(coverStatement), |
| ThrowStatement: entries(coverStatement), |
| TryStatement: entries(coverStatement), |
| VariableDeclaration: entries(), |
| // ignore processing only |
| VariableDeclarator: entries(coverVariableDeclarator), |
| IfStatement: entries(blockProp('consequent'), blockProp('alternate'), coverStatement, coverIfBranches), |
| ForStatement: entries(blockProp('body'), coverStatement), |
| ForInStatement: entries(blockProp('body'), coverStatement), |
| ForOfStatement: entries(blockProp('body'), coverStatement), |
| WhileStatement: entries(blockProp('body'), coverStatement), |
| DoWhileStatement: entries(blockProp('body'), coverStatement), |
| SwitchStatement: entries(createSwitchBranch, coverStatement), |
| SwitchCase: entries(coverSwitchCase), |
| WithStatement: entries(blockProp('body'), coverStatement), |
| FunctionDeclaration: entries(coverFunction), |
| FunctionExpression: entries(coverFunction), |
| LabeledStatement: entries(coverStatement), |
| ConditionalExpression: entries(coverTernary), |
| LogicalExpression: entries(coverLogicalExpression) |
| }; |
| const globalTemplateAlteredFunction = (0, _template.default)(` |
| var Function = (function(){}).constructor; |
| var global = (new Function(GLOBAL_COVERAGE_SCOPE))(); |
| `); |
| const globalTemplateFunction = (0, _template.default)(` |
| var global = (new Function(GLOBAL_COVERAGE_SCOPE))(); |
| `); |
| const globalTemplateVariable = (0, _template.default)(` |
| var global = GLOBAL_COVERAGE_SCOPE; |
| `); // the template to insert at the top of the program. |
| |
| const coverageTemplate = (0, _template.default)(` |
| var COVERAGE_VAR = (function () { |
| var path = PATH; |
| var hash = HASH; |
| GLOBAL_COVERAGE_TEMPLATE |
| var gcv = GLOBAL_COVERAGE_VAR; |
| var coverageData = INITIAL; |
| var coverage = global[gcv] || (global[gcv] = {}); |
| if (coverage[path] && coverage[path].hash === hash) { |
| return coverage[path]; |
| } |
| return coverage[path] = coverageData; |
| })(); |
| `); // the rewire plugin (and potentially other babel middleware) |
| // may cause files to be instrumented twice, see: |
| // https://github.com/istanbuljs/babel-plugin-istanbul/issues/94 |
| // we should only instrument code for coverage the first time |
| // it's run through istanbul-lib-instrument. |
| |
| function alreadyInstrumented(path, visitState) { |
| return path.scope.hasBinding(visitState.varName); |
| } |
| |
| function shouldIgnoreFile(programNode) { |
| return programNode.parent && programNode.parent.comments.some(c => COMMENT_FILE_RE.test(c.value)); |
| } |
| |
| const defaultProgramVisitorOpts = { |
| inputSourceMap: undefined |
| }; |
| /** |
| * programVisitor is a `babel` adaptor for instrumentation. |
| * It returns an object with two methods `enter` and `exit`. |
| * These should be assigned to or called from `Program` entry and exit functions |
| * in a babel visitor. |
| * These functions do not make assumptions about the state set by Babel and thus |
| * can be used in a context other than a Babel plugin. |
| * |
| * The exit function returns an object that currently has the following keys: |
| * |
| * `fileCoverage` - the file coverage object created for the source file. |
| * `sourceMappingURL` - any source mapping URL found when processing the file. |
| * |
| * @param {Object} types - an instance of babel-types |
| * @param {string} sourceFilePath - the path to source file |
| * @param {Object} opts - additional options |
| * @param {string} [opts.coverageVariable=__coverage__] the global coverage variable name. |
| * @param {string} [opts.coverageGlobalScope=this] the global coverage variable scope. |
| * @param {boolean} [opts.coverageGlobalScopeFunc=true] use an evaluated function to find coverageGlobalScope. |
| * @param {Array} [opts.ignoreClassMethods=[]] names of methods to ignore by default on classes. |
| * @param {object} [opts.inputSourceMap=undefined] the input source map, that maps the uninstrumented code back to the |
| * original code. |
| */ |
| |
| function programVisitor(types, sourceFilePath = 'unknown.js', opts = defaultProgramVisitorOpts) { |
| const T = types; // This sets some unused options but ensures all required options are initialized |
| |
| opts = Object.assign({}, (0, _instrumenter.defaultOpts)(), defaultProgramVisitorOpts, opts); |
| const visitState = new VisitState(types, sourceFilePath, opts.inputSourceMap, opts.ignoreClassMethods); |
| return { |
| enter(path) { |
| if (shouldIgnoreFile(path.find(p => p.isProgram()))) { |
| return; |
| } |
| |
| if (alreadyInstrumented(path, visitState)) { |
| return; |
| } |
| |
| path.traverse(codeVisitor, visitState); |
| }, |
| |
| exit(path) { |
| if (alreadyInstrumented(path, visitState)) { |
| return; |
| } |
| |
| visitState.cov.freeze(); |
| const coverageData = visitState.cov.toJSON(); |
| |
| if (shouldIgnoreFile(path.find(p => p.isProgram()))) { |
| return { |
| fileCoverage: coverageData, |
| sourceMappingURL: visitState.sourceMappingURL |
| }; |
| } |
| |
| coverageData[_constants.MAGIC_KEY] = _constants.MAGIC_VALUE; |
| const hash = (0, _crypto.createHash)(_constants.SHA).update(JSON.stringify(coverageData)).digest('hex'); |
| coverageData.hash = hash; |
| const coverageNode = T.valueToNode(coverageData); |
| delete coverageData[_constants.MAGIC_KEY]; |
| delete coverageData.hash; |
| let gvTemplate; |
| |
| if (opts.coverageGlobalScopeFunc) { |
| if (path.scope.getBinding('Function')) { |
| gvTemplate = globalTemplateAlteredFunction({ |
| GLOBAL_COVERAGE_SCOPE: T.stringLiteral('return ' + opts.coverageGlobalScope) |
| }); |
| } else { |
| gvTemplate = globalTemplateFunction({ |
| GLOBAL_COVERAGE_SCOPE: T.stringLiteral('return ' + opts.coverageGlobalScope) |
| }); |
| } |
| } else { |
| gvTemplate = globalTemplateVariable({ |
| GLOBAL_COVERAGE_SCOPE: opts.coverageGlobalScope |
| }); |
| } |
| |
| const cv = coverageTemplate({ |
| GLOBAL_COVERAGE_VAR: T.stringLiteral(opts.coverageVariable), |
| GLOBAL_COVERAGE_TEMPLATE: gvTemplate, |
| COVERAGE_VAR: T.identifier(visitState.varName), |
| PATH: T.stringLiteral(sourceFilePath), |
| INITIAL: coverageNode, |
| HASH: T.stringLiteral(hash) |
| }); |
| cv._blockHoist = 5; |
| path.node.body.unshift(cv); |
| return { |
| fileCoverage: coverageData, |
| sourceMappingURL: visitState.sourceMappingURL |
| }; |
| } |
| |
| }; |
| } |
| |
| var _default = programVisitor; |
| exports.default = _default; |