UNPKG

19 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
8
9var _sourceCoverage = require('./source-coverage');
10
11var _crypto = require('crypto');
12
13var _babelTemplate = require('babel-template');
14
15var _babelTemplate2 = _interopRequireDefault(_babelTemplate);
16
17function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
19function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
20
21// function to use for creating hashes
22var SHA = 'sha1';
23// istanbul ignore comment pattern
24var COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/;
25// source map URL pattern
26var SOURCE_MAP_RE = /[#@]\s*sourceMappingURL=(.*)\s*$/m;
27
28// generate a variable name from hashing the supplied file path
29function genVar(filename) {
30 var hash = (0, _crypto.createHash)(SHA),
31 suffix;
32 hash.update(filename);
33 suffix = hash.digest('base64');
34 //trim trailing equal signs, turn identifier unsafe chars to safe ones + => _ and / => $
35 suffix = suffix.replace(new RegExp('=', 'g'), '').replace(new RegExp('\\+', 'g'), '_').replace(new RegExp('/', 'g'), '$');
36 return '__cov_' + suffix;
37}
38
39// VisitState holds the state of the visitor, provides helper functions
40// and is the `this` for the individual coverage visitors.
41
42var VisitState = function () {
43 function VisitState(types, sourceFilePath) {
44 _classCallCheck(this, VisitState);
45
46 this.varName = genVar(sourceFilePath);
47 this.attrs = {};
48 this.nextIgnore = null;
49 this.cov = new _sourceCoverage.SourceCoverage(sourceFilePath);
50 this.types = types;
51 this.sourceMappingURL = null;
52 }
53
54 // should we ignore the node? Yes, if specifically ignoring
55 // or if the node is generated.
56
57
58 _createClass(VisitState, [{
59 key: 'shouldIgnore',
60 value: function shouldIgnore(path) {
61 return this.nextIgnore || !path.node.loc;
62 }
63
64 // extract the ignore comment hint (next|if|else) or null
65
66 }, {
67 key: 'hintFor',
68 value: function hintFor(node) {
69 var hint = null;
70 if (node.leadingComments) {
71 node.leadingComments.forEach(function (c) {
72 var v = (c.value || /* istanbul ignore next: paranoid check */"").trim();
73 var groups = v.match(COMMENT_RE);
74 if (groups) {
75 hint = groups[1];
76 }
77 });
78 }
79 return hint;
80 }
81
82 // extract a source map URL from comments and keep track of it
83
84 }, {
85 key: 'maybeAssignSourceMapURL',
86 value: function maybeAssignSourceMapURL(node) {
87 var that = this;
88 var extractURL = function extractURL(comments) {
89 if (!comments) {
90 return;
91 }
92 comments.forEach(function (c) {
93 var v = (c.value || /* istanbul ignore next: paranoid check */"").trim();
94 var groups = v.match(SOURCE_MAP_RE);
95 if (groups) {
96 that.sourceMappingURL = groups[1];
97 }
98 });
99 };
100 extractURL(node.leadingComments);
101 extractURL(node.trailingComments);
102 }
103
104 // all the generic stuff that needs to be done on enter for every node
105
106 }, {
107 key: 'onEnter',
108 value: function onEnter(path) {
109 var n = path.node;
110
111 this.maybeAssignSourceMapURL(n);
112
113 // if already ignoring, nothing more to do
114 if (this.nextIgnore !== null) {
115 return;
116 }
117 // check hint to see if ignore should be turned on
118 var hint = this.hintFor(n);
119 if (hint === 'next') {
120 this.nextIgnore = n;
121 return;
122 }
123 // else check custom node attribute set by a prior visitor
124 if (this.getAttr(path.node, 'skip-all')) {
125 this.nextIgnore = n;
126 }
127 }
128
129 // all the generic stuff on exit of a node,
130 // including reseting ignores and custom node attrs
131
132 }, {
133 key: 'onExit',
134 value: function onExit(path) {
135 // restore ignore status, if needed
136 if (path.node === this.nextIgnore) {
137 this.nextIgnore = null;
138 }
139 // nuke all attributes for the node
140 delete path.node.__cov__;
141 }
142
143 // set a node attribute for the supplied node
144
145 }, {
146 key: 'setAttr',
147 value: function setAttr(node, name, value) {
148 node.__cov__ = node.__cov__ || {};
149 node.__cov__[name] = value;
150 }
151
152 // retrieve a node attribute for the supplied node or null
153
154 }, {
155 key: 'getAttr',
156 value: function getAttr(node, name) {
157 var c = node.__cov__;
158 if (!c) {
159 return null;
160 }
161 return c[name];
162 }
163
164 //
165
166 }, {
167 key: 'increase',
168 value: function increase(type, id, index) {
169 var T = this.types;
170 var wrap = index !== null
171 // If `index` present, turn `x` into `x[index]`.
172 ? function (x) {
173 return T.memberExpression(x, T.numericLiteral(index), true);
174 } : function (x) {
175 return x;
176 };
177 return T.unaryExpression('++', wrap(T.memberExpression(T.memberExpression(T.identifier(this.varName), T.identifier(type)), T.stringLiteral(String(id)), true)));
178 }
179 }, {
180 key: 'insertCounter',
181 value: function insertCounter(path, increment) {
182 var T = this.types;
183 if (path.isBlockStatement()) {
184 path.node.body.unshift(T.expressionStatement(increment));
185 } else if (path.isStatement()) {
186 path.insertBefore(T.expressionStatement(increment));
187 } else /* istanbul ignore else: not expected */if (path.isExpression()) {
188 path.replaceWith(T.sequenceExpression([increment, path.node]));
189 } else {
190 console.error('Unable to insert counter for node type:', path.node.type);
191 }
192 }
193 }, {
194 key: 'insertStatementCounter',
195 value: function insertStatementCounter(path) {
196 /* istanbul ignore if: paranoid check */
197 if (!(path.node && path.node.loc)) {
198 return;
199 }
200 var index = this.cov.newStatement(path.node.loc);
201 var increment = this.increase('s', index, null);
202 this.insertCounter(path, increment);
203 }
204 }, {
205 key: 'insertFunctionCounter',
206 value: function insertFunctionCounter(path) {
207 var T = this.types;
208 /* istanbul ignore if: paranoid check */
209 if (!(path.node && path.node.loc)) {
210 return;
211 }
212 var n = path.node;
213 var dloc = null;
214 // get location for declaration
215 switch (n.type) {
216 case "FunctionDeclaration":
217 /* istanbul ignore else: paranoid check */
218 if (n.id) {
219 dloc = n.id.loc;
220 }
221 break;
222 case "FunctionExpression":
223 if (n.id) {
224 dloc = n.id.loc;
225 }
226 break;
227 }
228 if (!dloc) {
229 dloc = {
230 start: n.loc.start,
231 end: { line: n.loc.start.line, column: n.loc.start.column + 1 }
232 };
233 }
234 var name = path.node.id ? path.node.id.name : path.node.name;
235 var index = this.cov.newFunction(name, dloc, path.node.body.loc);
236 var increment = this.increase('f', index, null);
237 var body = path.get('body');
238 /* istanbul ignore else: not expected */
239 if (body.isBlockStatement()) {
240 body.node.body.unshift(T.expressionStatement(increment));
241 } else {
242 console.error('Unable to process function body node type:', path.node.type);
243 }
244 }
245 }, {
246 key: 'getBranchIncrement',
247 value: function getBranchIncrement(branchName, loc) {
248 var index = this.cov.addBranchPath(branchName, loc);
249 return this.increase('b', branchName, index);
250 }
251 }, {
252 key: 'insertBranchCounter',
253 value: function insertBranchCounter(path, branchName, loc) {
254 var increment = this.getBranchIncrement(branchName, loc || path.node.loc);
255 this.insertCounter(path, increment);
256 }
257 }, {
258 key: 'findLeaves',
259 value: function findLeaves(node, accumulator, parent, property) {
260 if (!node) {
261 return;
262 }
263 if (node.type === "LogicalExpression") {
264 var hint = this.hintFor(node);
265 if (hint !== 'next') {
266 this.findLeaves(node.left, accumulator, node, 'left');
267 this.findLeaves(node.right, accumulator, node, 'right');
268 }
269 } else {
270 accumulator.push({
271 node: node,
272 parent: parent,
273 property: property
274 });
275 }
276 }
277 }]);
278
279 return VisitState;
280}();
281
282// generic function that takes a set of visitor methods and
283// returns a visitor object with `enter` and `exit` properties,
284// such that:
285//
286// * standard entry processing is done
287// * the supplied visitors are called only when ignore is not in effect
288// This relieves them from worrying about ignore states and generated nodes.
289// * standard exit processing is done
290//
291
292
293function entries() {
294 var enter = Array.prototype.slice.call(arguments);
295 // the enter function
296 var wrappedEntry = function wrappedEntry(path, node) {
297 this.onEnter(path);
298 if (this.shouldIgnore(path)) {
299 return;
300 }
301 var that = this;
302 enter.forEach(function (e) {
303 e.call(that, path, node);
304 });
305 };
306 var exit = function exit(path, node) {
307 this.onExit(path, node);
308 };
309 return {
310 enter: wrappedEntry,
311 exit: exit
312 };
313}
314
315function coverStatement(path) {
316 this.insertStatementCounter(path);
317}
318
319/* istanbul ignore next: no node.js support */
320function coverAssignmentPattern(path) {
321 var n = path.node;
322 var b = this.cov.newBranch('default-arg', n.loc);
323 this.insertBranchCounter(path.get('right'), b);
324}
325
326function coverFunction(path) {
327 this.insertFunctionCounter(path);
328}
329
330function coverVariableDeclarator(path) {
331 this.insertStatementCounter(path.get('init'));
332}
333
334function skipInit(path) {
335 if (path.node.init) {
336 this.setAttr(path.node.init, 'skip-all', true);
337 }
338}
339
340function makeBlock(path) {
341 var T = this.types;
342 if (!path.node) {
343 path.replaceWith(T.blockStatement([]));
344 }
345 if (!path.isBlockStatement()) {
346 path.replaceWith(T.blockStatement([path.node]));
347 path.node.loc = path.node.body[0].loc;
348 }
349}
350
351function blockProp(prop) {
352 return function (path) {
353 makeBlock.call(this, path.get(prop));
354 };
355}
356
357function convertArrowExpression(path) {
358 var n = path.node;
359 var T = this.types;
360 if (n.expression) {
361 var bloc = n.body.loc;
362 n.expression = false;
363 n.body = T.blockStatement([T.returnStatement(n.body)]);
364 // restore body location
365 n.body.loc = bloc;
366 // set up the location for the return statement so it gets
367 // instrumented
368 n.body.body[0].loc = bloc;
369 }
370}
371
372function coverIfBranches(path) {
373 var n = path.node,
374 hint = this.hintFor(n),
375 ignoreIf = hint === 'if',
376 ignoreElse = hint === 'else',
377 branch = this.cov.newBranch('if', n.loc);
378
379 if (ignoreIf) {
380 this.setAttr(n.consequent, 'skip-all', true);
381 } else {
382 this.insertBranchCounter(path.get('consequent'), branch, n.loc);
383 }
384 if (ignoreElse) {
385 this.setAttr(n.alternate, 'skip-all', true);
386 } else {
387 this.insertBranchCounter(path.get('alternate'), branch, n.loc);
388 }
389}
390
391function createSwitchBranch(path) {
392 var b = this.cov.newBranch('switch', path.node.loc);
393 this.setAttr(path.node, 'branchName', b);
394}
395
396function coverSwitchCase(path) {
397 var T = this.types;
398 var b = this.getAttr(path.parentPath.node, 'branchName');
399 /* istanbul ignore if: paranoid check */
400 if (!b) {
401 throw new Error('Unable to get switch branch name');
402 }
403 var increment = this.getBranchIncrement(b, path.node.loc);
404 path.node.consequent.unshift(T.expressionStatement(increment));
405}
406
407function coverTernary(path) {
408 var n = path.node,
409 branch = this.cov.newBranch('cond-expr', path.node.loc),
410 cHint = this.hintFor(n.consequent),
411 aHint = this.hintFor(n.alternate);
412
413 if (cHint !== 'next') {
414 this.insertBranchCounter(path.get('consequent'), branch);
415 }
416 if (aHint !== 'next') {
417 this.insertBranchCounter(path.get('alternate'), branch);
418 }
419}
420
421function coverLogicalExpression(path) {
422 var T = this.types;
423 if (path.parentPath.node.type === "LogicalExpression") {
424 return; // already processed
425 }
426 var leaves = [];
427 this.findLeaves(path.node, leaves);
428 var b = this.cov.newBranch("binary-expr", path.node.loc);
429 for (var i = 0; i < leaves.length; i += 1) {
430 var leaf = leaves[i];
431 var hint = this.hintFor(leaf.node);
432 if (hint === 'next') {
433 continue;
434 }
435 var increment = this.getBranchIncrement(b, leaf.node.loc);
436 if (!increment) {
437 continue;
438 }
439 leaf.parent[leaf.property] = T.sequenceExpression([increment, leaf.node]);
440 }
441}
442
443var codeVisitor = {
444 ArrowFunctionExpression: entries(convertArrowExpression, coverFunction),
445 AssignmentPattern: entries(coverAssignmentPattern),
446 BlockStatement: entries(), // ignore processing only
447 ClassMethod: entries(coverFunction),
448 ExpressionStatement: entries(coverStatement),
449 BreakStatement: entries(coverStatement),
450 ContinueStatement: entries(coverStatement),
451 DebuggerStatement: entries(coverStatement),
452 ReturnStatement: entries(coverStatement),
453 ThrowStatement: entries(coverStatement),
454 TryStatement: entries(coverStatement),
455 VariableDeclaration: entries(), // ignore processing only
456 VariableDeclarator: entries(coverVariableDeclarator),
457 IfStatement: entries(blockProp('consequent'), blockProp('alternate'), coverStatement, coverIfBranches),
458 ForStatement: entries(blockProp('body'), skipInit, coverStatement),
459 ForInStatement: entries(blockProp('body'), skipInit, coverStatement),
460 ForOfStatement: entries(blockProp('body'), skipInit, coverStatement),
461 WhileStatement: entries(blockProp('body'), coverStatement),
462 DoWhileStatement: entries(blockProp('body'), coverStatement),
463 SwitchStatement: entries(createSwitchBranch, coverStatement),
464 SwitchCase: entries(coverSwitchCase),
465 WithStatement: entries(blockProp('body'), coverStatement),
466 FunctionDeclaration: entries(coverFunction),
467 FunctionExpression: entries(coverFunction),
468 LabeledStatement: entries(coverStatement),
469 ConditionalExpression: entries(coverTernary),
470 LogicalExpression: entries(coverLogicalExpression)
471};
472// the template to insert at the top of the program.
473var coverageTemplate = (0, _babelTemplate2.default)('\n var COVERAGE_VAR = (function () {\n var path = PATH, \n hash = HASH,\n global = (new Function(\'return this\'))(),\n gcv = GLOBAL_COVERAGE_VAR,\n coverageData = INITIAL,\n coverage = global[gcv] || (global[gcv] = {});\n if (coverage[path] && coverage[path].hash === hash) {\n return coverage[path];\n }\n coverageData.hash = hash;\n return coverage[path] = coverageData;\n })();\n');
474/**
475 * programVisitor is a `babel` adaptor for instrumentation.
476 * It returns an object with two methods `enter` and `exit`.
477 * These should be assigned to or called from `Program` entry and exit functions
478 * in a babel visitor.
479 * These functions do not make assumptions about the state set by Babel and thus
480 * can be used in a context other than a Babel plugin.
481 *
482 * The exit function returns an object that currently has the following keys:
483 *
484 * `fileCoverage` - the file coverage object created for the source file.
485 * `sourceMappingURL` - any source mapping URL found when processing the file.
486 *
487 * @param {Object} types - an instance of babel-types
488 * @param {string} sourceFilePath - the path to source file
489 * @param {Object} opts - additional options
490 * @param {string} [opts.coverageVariable=__coverage__] the global coverage variable name.
491 */
492function programVisitor(types) {
493 var sourceFilePath = arguments.length <= 1 || arguments[1] === undefined ? 'unknown.js' : arguments[1];
494 var opts = arguments.length <= 2 || arguments[2] === undefined ? { coverageVariable: '__coverage__' } : arguments[2];
495
496 var T = types;
497 var visitState = new VisitState(types, sourceFilePath);
498 return {
499 enter: function enter(path) {
500 path.traverse(codeVisitor, visitState);
501 },
502 exit: function exit(path) {
503 visitState.cov.freeze();
504 var coverageData = visitState.cov.toJSON();
505 var hash = (0, _crypto.createHash)(SHA).update(JSON.stringify(coverageData)).digest('hex');
506 var coverageNode = T.valueToNode(coverageData);
507 var cv = coverageTemplate({
508 GLOBAL_COVERAGE_VAR: T.stringLiteral(opts.coverageVariable),
509 COVERAGE_VAR: T.identifier(visitState.varName),
510 PATH: T.stringLiteral(sourceFilePath),
511 INITIAL: coverageNode,
512 HASH: T.stringLiteral(hash)
513 });
514 path.node.body.unshift(cv);
515 return {
516 fileCoverage: coverageData,
517 sourceMappingURL: visitState.sourceMappingURL
518 };
519 }
520 };
521}
522
523exports.default = programVisitor;
\No newline at end of file