/* Copyright (c) 2012, Yahoo! Inc. All rights reserved. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ /*global esprima, escodegen, window */ (function (isNode) { "use strict"; var SYNTAX, nodeType, ESP = isNode ? require('esprima') : esprima, ESPGEN = isNode ? require('escodegen') : escodegen, //TODO - package as dependency crypto = isNode ? require('crypto') : null, LEADER_WRAP = '(function () { ', TRAILER_WRAP = '\n}());', COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/, astgen, preconditions, cond, isArray = Array.isArray; /* istanbul ignore if: untestable */ if (!isArray) { isArray = function (thing) { return thing && Object.prototype.toString.call(thing) === '[object Array]'; }; } if (!isNode) { preconditions = { 'Could not find esprima': ESP, 'Could not find escodegen': ESPGEN, 'JSON object not in scope': JSON, 'Array does not implement push': [].push, 'Array does not implement unshift': [].unshift }; /* istanbul ignore next: untestable */ for (cond in preconditions) { if (preconditions.hasOwnProperty(cond)) { if (!preconditions[cond]) { throw new Error(cond); } } } } function generateTrackerVar(filename, omitSuffix) { var hash, suffix; if (crypto !== null) { hash = crypto.createHash('md5'); hash.update(filename); suffix = hash.digest('base64'); //trim trailing equal signs, turn identifier unsafe chars to safe ones + => _ and / => $ suffix = suffix.replace(new RegExp('=', 'g'), '') .replace(new RegExp('\\+', 'g'), '_') .replace(new RegExp('/', 'g'), '$'); } else { window.__cov_seq = window.__cov_seq || 0; window.__cov_seq += 1; suffix = window.__cov_seq; } return '__cov_' + (omitSuffix ? '' : suffix); } function pushAll(ary, thing) { if (!isArray(thing)) { thing = [ thing ]; } Array.prototype.push.apply(ary, thing); } SYNTAX = { // keep in sync with estraverse's VisitorKeys AssignmentExpression: ['left', 'right'], AssignmentPattern: ['left', 'right'], ArrayExpression: ['elements'], ArrayPattern: ['elements'], ArrowFunctionExpression: ['params', 'body'], AwaitExpression: ['argument'], // CAUTION: It's deferred to ES7. BlockStatement: ['body'], BinaryExpression: ['left', 'right'], BreakStatement: ['label'], CallExpression: ['callee', 'arguments'], CatchClause: ['param', 'body'], ClassBody: ['body'], ClassDeclaration: ['id', 'superClass', 'body'], ClassExpression: ['id', 'superClass', 'body'], ComprehensionBlock: ['left', 'right'], // CAUTION: It's deferred to ES7. ComprehensionExpression: ['blocks', 'filter', 'body'], // CAUTION: It's deferred to ES7. ConditionalExpression: ['test', 'consequent', 'alternate'], ContinueStatement: ['label'], DebuggerStatement: [], DirectiveStatement: [], DoWhileStatement: ['body', 'test'], EmptyStatement: [], ExportAllDeclaration: ['source'], ExportDefaultDeclaration: ['declaration'], ExportNamedDeclaration: ['declaration', 'specifiers', 'source'], ExportSpecifier: ['exported', 'local'], ExpressionStatement: ['expression'], ForStatement: ['init', 'test', 'update', 'body'], ForInStatement: ['left', 'right', 'body'], ForOfStatement: ['left', 'right', 'body'], FunctionDeclaration: ['id', 'params', 'body'], FunctionExpression: ['id', 'params', 'body'], GeneratorExpression: ['blocks', 'filter', 'body'], // CAUTION: It's deferred to ES7. Identifier: [], IfStatement: ['test', 'consequent', 'alternate'], ImportDeclaration: ['specifiers', 'source'], ImportDefaultSpecifier: ['local'], ImportNamespaceSpecifier: ['local'], ImportSpecifier: ['imported', 'local'], Literal: [], LabeledStatement: ['label', 'body'], LogicalExpression: ['left', 'right'], MetaProperty: ['meta', 'property'], MemberExpression: ['object', 'property'], MethodDefinition: ['key', 'value'], ModuleSpecifier: [], NewExpression: ['callee', 'arguments'], ObjectExpression: ['properties'], ObjectPattern: ['properties'], Program: ['body'], Property: ['key', 'value'], RestElement: [ 'argument' ], ReturnStatement: ['argument'], SequenceExpression: ['expressions'], SpreadElement: ['argument'], Super: [], SwitchStatement: ['discriminant', 'cases'], SwitchCase: ['test', 'consequent'], TaggedTemplateExpression: ['tag', 'quasi'], TemplateElement: [], TemplateLiteral: ['quasis', 'expressions'], ThisExpression: [], ThrowStatement: ['argument'], TryStatement: ['block', 'handler', 'finalizer'], UnaryExpression: ['argument'], UpdateExpression: ['argument'], VariableDeclaration: ['declarations'], VariableDeclarator: ['id', 'init'], WhileStatement: ['test', 'body'], WithStatement: ['object', 'body'], YieldExpression: ['argument'] }; for (nodeType in SYNTAX) { /* istanbul ignore else: has own property */ if (SYNTAX.hasOwnProperty(nodeType)) { SYNTAX[nodeType] = { name: nodeType, children: SYNTAX[nodeType] }; } } astgen = { variable: function (name) { return { type: SYNTAX.Identifier.name, name: name }; }, stringLiteral: function (str) { return { type: SYNTAX.Literal.name, value: String(str) }; }, numericLiteral: function (num) { return { type: SYNTAX.Literal.name, value: Number(num) }; }, statement: function (contents) { return { type: SYNTAX.ExpressionStatement.name, expression: contents }; }, dot: function (obj, field) { return { type: SYNTAX.MemberExpression.name, computed: false, object: obj, property: field }; }, subscript: function (obj, sub) { return { type: SYNTAX.MemberExpression.name, computed: true, object: obj, property: sub }; }, postIncrement: function (obj) { return { type: SYNTAX.UpdateExpression.name, operator: '++', prefix: false, argument: obj }; }, sequence: function (one, two) { return { type: SYNTAX.SequenceExpression.name, expressions: [one, two] }; }, returnStatement: function (expr) { return { type: SYNTAX.ReturnStatement.name, argument: expr }; } }; function Walker(walkMap, preprocessor, scope, debug) { this.walkMap = walkMap; this.preprocessor = preprocessor; this.scope = scope; this.debug = debug; if (this.debug) { this.level = 0; this.seq = true; } } function defaultWalker(node, walker) { var type = node.type, preprocessor, postprocessor, children = SYNTAX[type], // don't run generated nodes thru custom walks otherwise we will attempt to instrument the instrumentation code :) applyCustomWalker = !!node.loc || node.type === SYNTAX.Program.name, walkerFn = applyCustomWalker ? walker.walkMap[type] : null, i, j, walkFnIndex, childType, childNode, ret, childArray, childElement, pathElement, assignNode, isLast; if (!SYNTAX[type]) { console.error(node); console.error('Unsupported node type:' + type); return; } children = SYNTAX[type].children; /* istanbul ignore if: guard */ if (node.walking) { throw new Error('Infinite regress: Custom walkers may NOT call walker.apply(node)'); } node.walking = true; ret = walker.apply(node, walker.preprocessor); preprocessor = ret.preprocessor; if (preprocessor) { delete ret.preprocessor; ret = walker.apply(node, preprocessor); } if (isArray(walkerFn)) { for (walkFnIndex = 0; walkFnIndex < walkerFn.length; walkFnIndex += 1) { isLast = walkFnIndex === walkerFn.length - 1; ret = walker.apply(ret, walkerFn[walkFnIndex]); /*istanbul ignore next: paranoid check */ if (ret.type !== type && !isLast) { throw new Error('Only the last walker is allowed to change the node type: [type was: ' + type + ' ]'); } } } else { if (walkerFn) { ret = walker.apply(node, walkerFn); } } if (node.skipSelf) { return; } for (i = 0; i < children.length; i += 1) { childType = children[i]; childNode = node[childType]; if (childNode && !childNode.skipWalk) { pathElement = { node: node, property: childType }; if (isArray(childNode)) { childArray = []; for (j = 0; j < childNode.length; j += 1) { childElement = childNode[j]; pathElement.index = j; if (childElement) { assignNode = walker.apply(childElement, null, pathElement); if (isArray(assignNode.prepend)) { pushAll(childArray, assignNode.prepend); delete assignNode.prepend; } } else { assignNode = undefined; } pushAll(childArray, assignNode); } node[childType] = childArray; } else { assignNode = walker.apply(childNode, null, pathElement); /*istanbul ignore if: paranoid check */ if (isArray(assignNode.prepend)) { throw new Error('Internal error: attempt to prepend statements in disallowed (non-array) context'); /* if this should be allowed, this is how to solve it tmpNode = { type: 'BlockStatement', body: [] }; pushAll(tmpNode.body, assignNode.prepend); pushAll(tmpNode.body, assignNode); node[childType] = tmpNode; delete assignNode.prepend; */ } else { node[childType] = assignNode; } } } } postprocessor = ret.postprocessor; if (postprocessor) { delete ret.postprocessor; ret = walker.apply(ret, postprocessor); } delete node.walking; return ret; } Walker.prototype = { startWalk: function (node) { this.path = []; this.apply(node); }, apply: function (node, walkFn, pathElement) { var ret, i, seq, prefix; walkFn = walkFn || defaultWalker; if (this.debug) { this.seq += 1; this.level += 1; seq = this.seq; prefix = ''; for (i = 0; i < this.level; i += 1) { prefix += ' '; } console.log(prefix + 'Enter (' + seq + '):' + node.type); } if (pathElement) { this.path.push(pathElement); } ret = walkFn.call(this.scope, node, this); if (pathElement) { this.path.pop(); } if (this.debug) { this.level -= 1; console.log(prefix + 'Return (' + seq + '):' + node.type); } return ret || node; }, startLineForNode: function (node) { return node && node.loc && node.loc.start ? node.loc.start.line : /* istanbul ignore next: guard */ null; }, ancestor: function (n) { return this.path.length > n - 1 ? this.path[this.path.length - n] : /* istanbul ignore next: guard */ null; }, parent: function () { return this.ancestor(1); }, isLabeled: function () { var el = this.parent(); return el && el.node.type === SYNTAX.LabeledStatement.name; } }; /** * mechanism to instrument code for coverage. It uses the `esprima` and * `escodegen` libraries for JS parsing and code generation respectively. * * Works on `node` as well as the browser. * * Usage on nodejs * --------------- * * var instrumenter = new require('istanbul').Instrumenter(), * changed = instrumenter.instrumentSync('function meaningOfLife() { return 42; }', 'filename.js'); * * Usage in a browser * ------------------ * * Load `esprima.js`, `escodegen.js` and `instrumenter.js` (this file) using `script` tags or other means. * * Create an instrumenter object as: * * var instrumenter = new Instrumenter(), * changed = instrumenter.instrumentSync('function meaningOfLife() { return 42; }', 'filename.js'); * * Aside from demonstration purposes, it is unclear why you would want to instrument code in a browser. * * @class Instrumenter * @constructor * @param {Object} options Optional. Configuration options. * @param {String} [options.coverageVariable] the global variable name to use for * tracking coverage. Defaults to `__coverage__` * @param {Boolean} [options.embedSource] whether to embed the source code of every * file as an array in the file coverage object for that file. Defaults to `false` * @param {Boolean} [options.preserveComments] whether comments should be preserved in the output. Defaults to `false` * @param {Boolean} [options.noCompact] emit readable code when set. Defaults to `false` * @param {Boolean} [options.esModules] whether the code to instrument contains uses es * imports or exports. * @param {Boolean} [options.noAutoWrap] do not automatically wrap the source in * an anonymous function before covering it. By default, code is wrapped in * an anonymous function before it is parsed. This is done because * some nodejs libraries have `return` statements outside of * a function which is technically invalid Javascript and causes the parser to fail. * This construct, however, works correctly in node since module loading * is done in the context of an anonymous function. * * Note that the semantics of the code *returned* by the instrumenter does not change in any way. * The function wrapper is "unwrapped" before the instrumented code is generated. * @param {Object} [options.codeGenerationOptions] an object that is directly passed to the `escodegen` * library as configuration for code generation. The `noCompact` setting is not honored when this * option is specified * @param {Boolean} [options.debug] assist in debugging. Currently, the only effect of * setting this option is a pretty-print of the coverage variable. Defaults to `false` * @param {Boolean} [options.walkDebug] assist in debugging of the AST walker used by this class. * */ function Instrumenter(options) { this.opts = options || { debug: false, walkDebug: false, coverageVariable: '__coverage__', codeGenerationOptions: undefined, noAutoWrap: false, noCompact: false, embedSource: false, preserveComments: false, esModules: false }; if (this.opts.esModules && !this.opts.noAutoWrap) { this.opts.noAutoWrap = true; if (this.opts.debug) { console.log('Setting noAutoWrap to true as required by esModules'); } } this.walker = new Walker({ ArrowFunctionExpression: [ this.arrowBlockConverter ], ExpressionStatement: this.coverStatement, ExportNamedDeclaration: this.coverExport, BreakStatement: this.coverStatement, ContinueStatement: this.coverStatement, DebuggerStatement: this.coverStatement, ReturnStatement: this.coverStatement, ThrowStatement: this.coverStatement, TryStatement: [ this.paranoidHandlerCheck, this.coverStatement], VariableDeclaration: this.coverStatement, IfStatement: [ this.ifBlockConverter, this.coverStatement, this.ifBranchInjector ], ForStatement: [ this.skipInit, this.loopBlockConverter, this.coverStatement ], ForInStatement: [ this.skipLeft, this.loopBlockConverter, this.coverStatement ], ForOfStatement: [ this.skipLeft, this.loopBlockConverter, this.coverStatement ], WhileStatement: [ this.loopBlockConverter, this.coverStatement ], DoWhileStatement: [ this.loopBlockConverter, this.coverStatement ], SwitchStatement: [ this.coverStatement, this.switchBranchInjector ], SwitchCase: [ this.switchCaseInjector ], WithStatement: [ this.withBlockConverter, this.coverStatement ], FunctionDeclaration: [ this.coverFunction, this.coverStatement ], FunctionExpression: this.coverFunction, LabeledStatement: this.coverStatement, ConditionalExpression: this.conditionalBranchInjector, LogicalExpression: this.logicalExpressionBranchInjector, ObjectExpression: this.maybeAddType, MetaProperty: this.coverMetaProperty, }, this.extractCurrentHint, this, this.opts.walkDebug); //unit testing purposes only if (this.opts.backdoor && this.opts.backdoor.omitTrackerSuffix) { this.omitTrackerSuffix = true; } } Instrumenter.prototype = { /** * synchronous instrumentation method. Throws when illegal code is passed to it * @method instrumentSync * @param {String} code the code to be instrumented as a String * @param {String} filename Optional. The name of the file from which * the code was read. A temporary filename is generated when not specified. * Not specifying a filename is only useful for unit tests and demonstrations * of this library. */ instrumentSync: function (code, filename) { var program; //protect from users accidentally passing in a Buffer object instead if (typeof code !== 'string') { throw new Error('Code must be string'); } if (code.charAt(0) === '#') { //shebang, 'comment' it out, won't affect syntax tree locations for things we care about code = '//' + code; } if (!this.opts.noAutoWrap) { code = LEADER_WRAP + code + TRAILER_WRAP; } try { program = ESP.parse(code, { loc: true, range: true, tokens: this.opts.preserveComments, comment: true, sourceType: this.opts.esModules ? 'module' : 'script' }); } catch (e) { console.log('Failed to parse file: ' + filename); throw e; } if (this.opts.preserveComments) { program = ESPGEN.attachComments(program, program.comments, program.tokens); } if (!this.opts.noAutoWrap) { program = { type: SYNTAX.Program.name, body: program.body[0].expression.callee.body.body, comments: program.comments }; } return this.instrumentASTSync(program, filename, code); }, filterHints: function (comments) { var ret = [], i, comment, groups; if (!(comments && isArray(comments))) { return ret; } for (i = 0; i < comments.length; i += 1) { comment = comments[i]; /* istanbul ignore else: paranoid check */ if (comment && comment.value && comment.range && isArray(comment.range)) { groups = String(comment.value).match(COMMENT_RE); if (groups) { ret.push({ type: groups[1], start: comment.range[0], end: comment.range[1] }); } } } return ret; }, extractCurrentHint: function (node) { if (!node.range) { return; } var i = this.currentState.lastHintPosition + 1, hints = this.currentState.hints, nodeStart = node.range[0], hint; this.currentState.currentHint = null; while (i < hints.length) { hint = hints[i]; if (hint.end < nodeStart) { this.currentState.currentHint = hint; this.currentState.lastHintPosition = i; i += 1; } else { break; } } }, /** * synchronous instrumentation method that instruments an AST instead. * @method instrumentASTSync * @param {String} program the AST to be instrumented * @param {String} filename Optional. The name of the file from which * the code was read. A temporary filename is generated when not specified. * Not specifying a filename is only useful for unit tests and demonstrations * of this library. * @param {String} originalCode the original code corresponding to the AST, * used for embedding the source into the coverage object */ instrumentASTSync: function (program, filename, originalCode) { var usingStrict = false, codegenOptions, generated, preamble, lineCount, i; filename = filename || String(new Date().getTime()) + '.js'; this.sourceMap = null; this.coverState = { path: filename, s: {}, b: {}, f: {}, fnMap: {}, statementMap: {}, branchMap: {} }; this.currentState = { trackerVar: generateTrackerVar(filename, this.omitTrackerSuffix), func: 0, branch: 0, variable: 0, statement: 0, hints: this.filterHints(program.comments), currentHint: null, lastHintPosition: -1, ignoring: 0 }; if (program.body && program.body.length > 0 && this.isUseStrictExpression(program.body[0])) { //nuke it program.body.shift(); //and add it back at code generation time usingStrict = true; } this.walker.startWalk(program); codegenOptions = this.opts.codeGenerationOptions || { format: { compact: !this.opts.noCompact }}; codegenOptions.comment = this.opts.preserveComments; //console.log(JSON.stringify(program, undefined, 2)); generated = ESPGEN.generate(program, codegenOptions); preamble = this.getPreamble(originalCode || '', usingStrict); if (generated.map && generated.code) { lineCount = preamble.split(/\r\n|\r|\n/).length; // offset all the generated line numbers by the number of lines in the preamble for (i = 0; i < generated.map._mappings._array.length; i += 1) { generated.map._mappings._array[i].generatedLine += lineCount; } this.sourceMap = generated.map; generated = generated.code; } return preamble + '\n' + generated + '\n'; }, /** * Callback based instrumentation. Note that this still executes synchronously in the same process tick * and calls back immediately. It only provides the options for callback style error handling as * opposed to a `try-catch` style and nothing more. Implemented as a wrapper over `instrumentSync` * * @method instrument * @param {String} code the code to be instrumented as a String * @param {String} filename Optional. The name of the file from which * the code was read. A temporary filename is generated when not specified. * Not specifying a filename is only useful for unit tests and demonstrations * of this library. * @param {Function(err, instrumentedCode)} callback - the callback function */ instrument: function (code, filename, callback) { if (!callback && typeof filename === 'function') { callback = filename; filename = null; } try { callback(null, this.instrumentSync(code, filename)); } catch (ex) { callback(ex); } }, /** * returns the file coverage object for the code that was instrumented * just before calling this method. Note that this represents a * "zero-coverage" object which is not even representative of the code * being loaded in node or a browser (which would increase the statement * counts for mainline code). * @method lastFileCoverage * @return {Object} a "zero-coverage" file coverage object for the code last instrumented * by this instrumenter */ lastFileCoverage: function () { return this.coverState; }, /** * returns the source map object for the code that was instrumented * just before calling this method. * @method lastSourceMap * @return {Object} a source map object for the code last instrumented * by this instrumenter */ lastSourceMap: function () { return this.sourceMap; }, fixColumnPositions: function (coverState) { var offset = LEADER_WRAP.length, fixer = function (loc) { if (loc.start.line === 1) { loc.start.column -= offset; } if (loc.end.line === 1) { loc.end.column -= offset; } }, k, obj, i, locations; obj = coverState.statementMap; for (k in obj) { /* istanbul ignore else: has own property */ if (obj.hasOwnProperty(k)) { fixer(obj[k]); } } obj = coverState.fnMap; for (k in obj) { /* istanbul ignore else: has own property */ if (obj.hasOwnProperty(k)) { fixer(obj[k].loc); } } obj = coverState.branchMap; for (k in obj) { /* istanbul ignore else: has own property */ if (obj.hasOwnProperty(k)) { locations = obj[k].locations; for (i = 0; i < locations.length; i += 1) { fixer(locations[i]); } } } }, getPreamble: function (sourceCode, emitUseStrict) { var varName = this.opts.coverageVariable || '__coverage__', file = this.coverState.path.replace(/\\/g, '\\\\'), tracker = this.currentState.trackerVar, coverState, strictLine = emitUseStrict ? '"use strict";' : '', // return replacements using the function to ensure that the replacement is // treated like a dumb string and not as a string with RE replacement patterns replacer = function (s) { return function () { return s; }; }, code; if (!this.opts.noAutoWrap) { this.fixColumnPositions(this.coverState); } if (this.opts.embedSource) { this.coverState.code = sourceCode.split(/(?:\r?\n)|\r/); } coverState = this.opts.debug ? JSON.stringify(this.coverState, undefined, 4) : JSON.stringify(this.coverState); code = [ "%STRICT%", "var %VAR% = (Function('return this'))();", "if (!%VAR%.%GLOBAL%) { %VAR%.%GLOBAL% = {}; }", "%VAR% = %VAR%.%GLOBAL%;", "if (!(%VAR%['%FILE%'])) {", " %VAR%['%FILE%'] = %OBJECT%;", "}", "%VAR% = %VAR%['%FILE%'];" ].join("\n") .replace(/%STRICT%/g, replacer(strictLine)) .replace(/%VAR%/g, replacer(tracker)) .replace(/%GLOBAL%/g, replacer(varName)) .replace(/%FILE%/g, replacer(file)) .replace(/%OBJECT%/g, replacer(coverState)); return code; }, startIgnore: function () { this.currentState.ignoring += 1; }, endIgnore: function () { this.currentState.ignoring -= 1; }, convertToBlock: function (node) { if (!node) { return { type: 'BlockStatement', body: [] }; } else if (node.type === 'BlockStatement') { return node; } else { return { type: 'BlockStatement', body: [ node ] }; } }, arrowBlockConverter: function (node) { var retStatement; if (node.expression) { // turn expression nodes into a block with a return statement retStatement = astgen.returnStatement(node.body); // ensure the generated return statement is covered retStatement.loc = node.body.loc; node.body = this.convertToBlock(retStatement); node.expression = false; } }, paranoidHandlerCheck: function (node) { // if someone is using an older esprima on the browser // convert handlers array to single handler attribute // containing its first element /* istanbul ignore next */ if (!node.handler && node.handlers) { node.handler = node.handlers[0]; } }, ifBlockConverter: function (node) { node.consequent = this.convertToBlock(node.consequent); node.alternate = this.convertToBlock(node.alternate); }, loopBlockConverter: function (node) { node.body = this.convertToBlock(node.body); }, withBlockConverter: function (node) { node.body = this.convertToBlock(node.body); }, statementName: function (location, initValue) { var sName, ignoring = !!this.currentState.ignoring; location.skip = ignoring || undefined; initValue = initValue || 0; this.currentState.statement += 1; sName = this.currentState.statement; this.coverState.statementMap[sName] = location; this.coverState.s[sName] = initValue; return sName; }, skipInit: function (node /*, walker */) { if (node.init) { node.init.skipWalk = true; } }, skipLeft: function (node /*, walker */) { node.left.skipWalk = true; }, isUseStrictExpression: function (node) { return node && node.type === SYNTAX.ExpressionStatement.name && node.expression && node.expression.type === SYNTAX.Literal.name && node.expression.value === 'use strict'; }, maybeSkipNode: function (node, type) { var alreadyIgnoring = !!this.currentState.ignoring, hint = this.currentState.currentHint, ignoreThis = !alreadyIgnoring && hint && hint.type === type; if (ignoreThis) { this.startIgnore(); node.postprocessor = this.endIgnore; return true; } return false; }, coverMetaProperty: function(node /* , walker */) { node.skipSelf = true; }, coverStatement: function (node, walker) { var sName, incrStatementCount, parent, grandParent; this.maybeSkipNode(node, 'next'); if (this.isUseStrictExpression(node)) { grandParent = walker.ancestor(2); /* istanbul ignore else: difficult to test */ if (grandParent) { if ((grandParent.node.type === SYNTAX.FunctionExpression.name || grandParent.node.type === SYNTAX.FunctionDeclaration.name) && walker.parent().node.body[0] === node) { return; } } } if (node.type === SYNTAX.FunctionDeclaration.name) { // Called for the side-effect of setting the function's statement count to 1. this.statementName(node.loc, 1); } else { // We let `coverExport` handle ExportNamedDeclarations. parent = walker.parent(); if (parent && parent.node.type === SYNTAX.ExportNamedDeclaration.name) { return; } sName = this.statementName(node.loc); incrStatementCount = astgen.statement( astgen.postIncrement( astgen.subscript( astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('s')), astgen.stringLiteral(sName) ) ) ); this.splice(incrStatementCount, node, walker); } }, coverExport: function (node, walker) { var sName, incrStatementCount; if ( !node.declaration || !node.declaration.declarations ) { return; } this.maybeSkipNode(node, 'next'); sName = this.statementName(node.declaration.loc); incrStatementCount = astgen.statement( astgen.postIncrement( astgen.subscript( astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('s')), astgen.stringLiteral(sName) ) ) ); this.splice(incrStatementCount, node, walker); }, splice: function (statements, node, walker) { var targetNode = walker.isLabeled() ? walker.parent().node : node; targetNode.prepend = targetNode.prepend || []; pushAll(targetNode.prepend, statements); }, functionName: function (node, line, location) { this.currentState.func += 1; var id = this.currentState.func, ignoring = !!this.currentState.ignoring, name = node.id ? node.id.name : '(anonymous_' + id + ')', clone = function (attr) { var obj = location[attr] || /* istanbul ignore next */ {}; return { line: obj.line, column: obj.column }; }; this.coverState.fnMap[id] = { name: name, line: line, loc: { start: clone('start'), end: clone('end') }, skip: ignoring || undefined }; this.coverState.f[id] = 0; return id; }, coverFunction: function (node, walker) { var id, body = node.body, blockBody = body.body, popped; this.maybeSkipNode(node, 'next'); id = this.functionName(node, walker.startLineForNode(node), { start: node.loc.start, end: { line: node.body.loc.start.line, column: node.body.loc.start.column } }); if (blockBody.length > 0 && this.isUseStrictExpression(blockBody[0])) { popped = blockBody.shift(); } blockBody.unshift( astgen.statement( astgen.postIncrement( astgen.subscript( astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('f')), astgen.stringLiteral(id) ) ) ) ); if (popped) { blockBody.unshift(popped); } }, branchName: function (type, startLine, pathLocations) { var bName, paths = [], locations = [], i, ignoring = !!this.currentState.ignoring; this.currentState.branch += 1; bName = this.currentState.branch; for (i = 0; i < pathLocations.length; i += 1) { pathLocations[i].skip = pathLocations[i].skip || ignoring || undefined; locations.push(pathLocations[i]); paths.push(0); } this.coverState.b[bName] = paths; this.coverState.branchMap[bName] = { line: startLine, type: type, locations: locations }; return bName; }, branchIncrementExprAst: function (varName, branchIndex, down) { var ret = astgen.postIncrement( astgen.subscript( astgen.subscript( astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('b')), astgen.stringLiteral(varName) ), astgen.numericLiteral(branchIndex) ), down ); return ret; }, locationsForNodes: function (nodes) { var ret = [], i; for (i = 0; i < nodes.length; i += 1) { ret.push(nodes[i].loc); } return ret; }, ifBranchInjector: function (node, walker) { var alreadyIgnoring = !!this.currentState.ignoring, hint = this.currentState.currentHint, ignoreThen = !alreadyIgnoring && hint && hint.type === 'if', ignoreElse = !alreadyIgnoring && hint && hint.type === 'else', line = node.loc.start.line, col = node.loc.start.column, makeLoc = function () { return { line: line, column: col }; }, bName = this.branchName('if', walker.startLineForNode(node), [ { start: makeLoc(), end: makeLoc(), skip: ignoreThen || undefined }, { start: makeLoc(), end: makeLoc(), skip: ignoreElse || undefined } ]), thenBody = node.consequent.body, elseBody = node.alternate.body, child; thenBody.unshift(astgen.statement(this.branchIncrementExprAst(bName, 0))); elseBody.unshift(astgen.statement(this.branchIncrementExprAst(bName, 1))); if (ignoreThen) { child = node.consequent; child.preprocessor = this.startIgnore; child.postprocessor = this.endIgnore; } if (ignoreElse) { child = node.alternate; child.preprocessor = this.startIgnore; child.postprocessor = this.endIgnore; } }, branchLocationFor: function (name, index) { return this.coverState.branchMap[name].locations[index]; }, switchBranchInjector: function (node, walker) { var cases = node.cases, bName, i; if (!(cases && cases.length > 0)) { return; } bName = this.branchName('switch', walker.startLineForNode(node), this.locationsForNodes(cases)); for (i = 0; i < cases.length; i += 1) { cases[i].branchLocation = this.branchLocationFor(bName, i); cases[i].consequent.unshift(astgen.statement(this.branchIncrementExprAst(bName, i))); } }, switchCaseInjector: function (node) { var location = node.branchLocation; delete node.branchLocation; if (this.maybeSkipNode(node, 'next')) { location.skip = true; } }, conditionalBranchInjector: function (node, walker) { var bName = this.branchName('cond-expr', walker.startLineForNode(node), this.locationsForNodes([ node.consequent, node.alternate ])), ast1 = this.branchIncrementExprAst(bName, 0), ast2 = this.branchIncrementExprAst(bName, 1); node.consequent.preprocessor = this.maybeAddSkip(this.branchLocationFor(bName, 0)); node.alternate.preprocessor = this.maybeAddSkip(this.branchLocationFor(bName, 1)); node.consequent = astgen.sequence(ast1, node.consequent); node.alternate = astgen.sequence(ast2, node.alternate); }, maybeAddSkip: function (branchLocation) { return function (node) { var alreadyIgnoring = !!this.currentState.ignoring, hint = this.currentState.currentHint, ignoreThis = !alreadyIgnoring && hint && hint.type === 'next'; if (ignoreThis) { this.startIgnore(); node.postprocessor = this.endIgnore; } if (ignoreThis || alreadyIgnoring) { branchLocation.skip = true; } }; }, logicalExpressionBranchInjector: function (node, walker) { var parent = walker.parent(), leaves = [], bName, tuple, i; this.maybeSkipNode(node, 'next'); if (parent && parent.node.type === SYNTAX.LogicalExpression.name) { //already covered return; } this.findLeaves(node, leaves); bName = this.branchName('binary-expr', walker.startLineForNode(node), this.locationsForNodes(leaves.map(function (item) { return item.node; })) ); for (i = 0; i < leaves.length; i += 1) { tuple = leaves[i]; tuple.parent[tuple.property] = astgen.sequence(this.branchIncrementExprAst(bName, i), tuple.node); tuple.node.preprocessor = this.maybeAddSkip(this.branchLocationFor(bName, i)); } }, findLeaves: function (node, accumulator, parent, property) { if (node.type === SYNTAX.LogicalExpression.name) { this.findLeaves(node.left, accumulator, node, 'left'); this.findLeaves(node.right, accumulator, node, 'right'); } else { accumulator.push({ node: node, parent: parent, property: property }); } }, maybeAddType: function (node /*, walker */) { var props = node.properties, i, child; for (i = 0; i < props.length; i += 1) { child = props[i]; if (!child.type) { child.type = SYNTAX.Property.name; } } }, }; if (isNode) { module.exports = Instrumenter; } else { window.Instrumenter = Instrumenter; } }(typeof module !== 'undefined' && typeof module.exports !== 'undefined' && typeof exports !== 'undefined'));