82f2b76e25
We now use webpack instead of SystemJS, effectively bundling modules into one file (plus commons chunks) for every entry point. This results in a much smaller extension size (almost half). Furthermore we use yarn/npm even for extension run-time dependencies. This relieves us from manually vendoring and building dependencies. It's also easier to understand for new developers familiar with node.
1098 lines
44 KiB
JavaScript
1098 lines
44 KiB
JavaScript
/*
|
|
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'));
|