UNPKG

39.8 kBJavaScriptView Raw
1/*
2 Copyright (c) 2012, Yahoo! Inc. All rights reserved.
3 Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
4 */
5
6/*global esprima, escodegen, window */
7(function (isNode) {
8 "use strict";
9 var SYNTAX,
10 nodeType,
11 ESP = isNode ? require('esprima') : esprima,
12 ESPGEN = isNode ? require('escodegen') : escodegen, //TODO - package as dependency
13 crypto = isNode ? require('crypto') : null,
14 LEADER_WRAP = '(function () { ',
15 TRAILER_WRAP = '\n}());',
16 COMMENT_RE = /^\s*istanbul\s+ignore\s+(if|else|next)(?=\W|$)/,
17 astgen,
18 preconditions,
19 cond,
20 isArray = Array.isArray;
21
22 /* istanbul ignore if: untestable */
23 if (!isArray) {
24 isArray = function (thing) { return thing && Object.prototype.toString.call(thing) === '[object Array]'; };
25 }
26
27 if (!isNode) {
28 preconditions = {
29 'Could not find esprima': ESP,
30 'Could not find escodegen': ESPGEN,
31 'JSON object not in scope': JSON,
32 'Array does not implement push': [].push,
33 'Array does not implement unshift': [].unshift
34 };
35 /* istanbul ignore next: untestable */
36 for (cond in preconditions) {
37 if (preconditions.hasOwnProperty(cond)) {
38 if (!preconditions[cond]) { throw new Error(cond); }
39 }
40 }
41 }
42
43 function generateTrackerVar(filename, omitSuffix) {
44 var hash, suffix;
45 if (crypto !== null) {
46 hash = crypto.createHash('md5');
47 hash.update(filename);
48 suffix = hash.digest('base64');
49 //trim trailing equal signs, turn identifier unsafe chars to safe ones + => _ and / => $
50 suffix = suffix.replace(new RegExp('=', 'g'), '')
51 .replace(new RegExp('\\+', 'g'), '_')
52 .replace(new RegExp('/', 'g'), '$');
53 } else {
54 window.__cov_seq = window.__cov_seq || 0;
55 window.__cov_seq += 1;
56 suffix = window.__cov_seq;
57 }
58 return '__cov_' + (omitSuffix ? '' : suffix);
59 }
60
61 function pushAll(ary, thing) {
62 if (!isArray(thing)) {
63 thing = [ thing ];
64 }
65 Array.prototype.push.apply(ary, thing);
66 }
67
68 SYNTAX = {
69 ArrayExpression: [ 'elements' ],
70 AssignmentExpression: ['left', 'right'],
71 BinaryExpression: ['left', 'right' ],
72 BlockStatement: [ 'body' ],
73 BreakStatement: [ 'label' ],
74 CallExpression: [ 'callee', 'arguments'],
75 CatchClause: ['param', 'body'],
76 ConditionalExpression: [ 'test', 'consequent', 'alternate' ],
77 ContinueStatement: [ 'label' ],
78 DebuggerStatement: [ ],
79 DoWhileStatement: [ 'body', 'test' ],
80 EmptyStatement: [],
81 ExpressionStatement: [ 'expression'],
82 ForInStatement: [ 'left', 'right', 'body' ],
83 ForStatement: ['init', 'test', 'update', 'body' ],
84 FunctionDeclaration: ['id', 'params', 'body'],
85 FunctionExpression: ['id', 'params', 'defaults', 'body'],
86 Identifier: [],
87 IfStatement: ['test', 'consequent', 'alternate'],
88 LabeledStatement: ['label', 'body'],
89 Literal: [],
90 LogicalExpression: [ 'left', 'right' ],
91 MemberExpression: ['object', 'property'],
92 NewExpression: ['callee', 'arguments'],
93 ObjectExpression: [ 'properties' ],
94 Program: [ 'body' ],
95 Property: [ 'key', 'value'],
96 ReturnStatement: ['argument'],
97 SequenceExpression: ['expressions'],
98 SwitchCase: [ 'test', 'consequent' ],
99 SwitchStatement: ['discriminant', 'cases' ],
100 ThisExpression: [],
101 ThrowStatement: ['argument'],
102 TryStatement: [ 'block', 'handlers', 'finalizer' ],
103 UnaryExpression: ['argument'],
104 UpdateExpression: [ 'argument' ],
105 VariableDeclaration: [ 'declarations' ],
106 VariableDeclarator: [ 'id', 'init' ],
107 WhileStatement: [ 'test', 'body' ],
108 WithStatement: [ 'object', 'body' ]
109
110 };
111
112 for (nodeType in SYNTAX) {
113 /* istanbul ignore else: has own property */
114 if (SYNTAX.hasOwnProperty(nodeType)) {
115 SYNTAX[nodeType] = { name: nodeType, children: SYNTAX[nodeType] };
116 }
117 }
118
119 astgen = {
120 variable: function (name) { return { type: SYNTAX.Identifier.name, name: name }; },
121 stringLiteral: function (str) { return { type: SYNTAX.Literal.name, value: String(str) }; },
122 numericLiteral: function (num) { return { type: SYNTAX.Literal.name, value: Number(num) }; },
123 statement: function (contents) { return { type: SYNTAX.ExpressionStatement.name, expression: contents }; },
124 dot: function (obj, field) { return { type: SYNTAX.MemberExpression.name, computed: false, object: obj, property: field }; },
125 subscript: function (obj, sub) { return { type: SYNTAX.MemberExpression.name, computed: true, object: obj, property: sub }; },
126 postIncrement: function (obj) { return { type: SYNTAX.UpdateExpression.name, operator: '++', prefix: false, argument: obj }; },
127 sequence: function (one, two) { return { type: SYNTAX.SequenceExpression.name, expressions: [one, two] }; }
128 };
129
130 function Walker(walkMap, preprocessor, scope, debug) {
131 this.walkMap = walkMap;
132 this.preprocessor = preprocessor;
133 this.scope = scope;
134 this.debug = debug;
135 if (this.debug) {
136 this.level = 0;
137 this.seq = true;
138 }
139 }
140
141 function defaultWalker(node, walker) {
142
143 var type = node.type,
144 preprocessor,
145 postprocessor,
146 children = SYNTAX[type].children,
147 // don't run generated nodes thru custom walks otherwise we will attempt to instrument the instrumentation code :)
148 applyCustomWalker = !!node.loc || node.type === SYNTAX.Program.name,
149 walkerFn = applyCustomWalker ? walker.walkMap[type] : null,
150 i,
151 j,
152 walkFnIndex,
153 childType,
154 childNode,
155 ret,
156 childArray,
157 childElement,
158 pathElement,
159 assignNode,
160 isLast;
161
162 /* istanbul ignore if: guard */
163 if (node.walking) { throw new Error('Infinite regress: Custom walkers may NOT call walker.apply(node)'); }
164 node.walking = true;
165
166 ret = walker.apply(node, walker.preprocessor);
167
168 preprocessor = ret.preprocessor;
169 if (preprocessor) {
170 delete ret.preprocessor;
171 ret = walker.apply(node, preprocessor);
172 }
173
174 if (isArray(walkerFn)) {
175 for (walkFnIndex = 0; walkFnIndex < walkerFn.length; walkFnIndex += 1) {
176 isLast = walkFnIndex === walkerFn.length - 1;
177 ret = walker.apply(ret, walkerFn[walkFnIndex]);
178 /*istanbul ignore next: paranoid check */
179 if (ret.type !== type && !isLast) {
180 throw new Error('Only the last walker is allowed to change the node type: [type was: ' + type + ' ]');
181 }
182 }
183 } else {
184 if (walkerFn) {
185 ret = walker.apply(node, walkerFn);
186 }
187 }
188
189 for (i = 0; i < children.length; i += 1) {
190 childType = children[i];
191 childNode = node[childType];
192 if (childNode && !childNode.skipWalk) {
193 pathElement = { node: node, property: childType };
194 if (isArray(childNode)) {
195 childArray = [];
196 for (j = 0; j < childNode.length; j += 1) {
197 childElement = childNode[j];
198 pathElement.index = j;
199 if (childElement) {
200 assignNode = walker.apply(childElement, null, pathElement);
201 if (isArray(assignNode.prepend)) {
202 pushAll(childArray, assignNode.prepend);
203 delete assignNode.prepend;
204 }
205 }
206 pushAll(childArray, assignNode);
207 }
208 node[childType] = childArray;
209 } else {
210 assignNode = walker.apply(childNode, null, pathElement);
211 /*istanbul ignore if: paranoid check */
212 if (isArray(assignNode.prepend)) {
213 throw new Error('Internal error: attempt to prepend statements in disallowed (non-array) context');
214 /* if this should be allowed, this is how to solve it
215 tmpNode = { type: 'BlockStatement', body: [] };
216 pushAll(tmpNode.body, assignNode.prepend);
217 pushAll(tmpNode.body, assignNode);
218 node[childType] = tmpNode;
219 delete assignNode.prepend;
220 */
221 } else {
222 node[childType] = assignNode;
223 }
224 }
225 }
226 }
227
228 postprocessor = ret.postprocessor;
229 if (postprocessor) {
230 delete ret.postprocessor;
231 ret = walker.apply(ret, postprocessor);
232 }
233
234 delete node.walking;
235
236 return ret;
237 }
238
239 Walker.prototype = {
240 startWalk: function (node) {
241 this.path = [];
242 this.apply(node);
243 },
244
245 apply: function (node, walkFn, pathElement) {
246 var ret, i, seq, prefix;
247
248 walkFn = walkFn || defaultWalker;
249 if (this.debug) {
250 this.seq += 1;
251 this.level += 1;
252 seq = this.seq;
253 prefix = '';
254 for (i = 0; i < this.level; i += 1) { prefix += ' '; }
255 console.log(prefix + 'Enter (' + seq + '):' + node.type);
256 }
257 if (pathElement) { this.path.push(pathElement); }
258 ret = walkFn.call(this.scope, node, this);
259 if (pathElement) { this.path.pop(); }
260 if (this.debug) {
261 this.level -= 1;
262 console.log(prefix + 'Return (' + seq + '):' + node.type);
263 }
264 return ret || node;
265 },
266
267 startLineForNode: function (node) {
268 return node && node.loc && node.loc.start ? node.loc.start.line : /* istanbul ignore next: guard */ null;
269 },
270
271 ancestor: function (n) {
272 return this.path.length > n - 1 ? this.path[this.path.length - n] : /* istanbul ignore next: guard */ null;
273 },
274
275 parent: function () {
276 return this.ancestor(1);
277 },
278
279 isLabeled: function () {
280 var el = this.parent();
281 return el && el.node.type === SYNTAX.LabeledStatement.name;
282 }
283 };
284
285 /**
286 * mechanism to instrument code for coverage. It uses the `esprima` and
287 * `escodegen` libraries for JS parsing and code generation respectively.
288 *
289 * Works on `node` as well as the browser.
290 *
291 * Usage on nodejs
292 * ---------------
293 *
294 * var instrumenter = new require('istanbul').Instrumenter(),
295 * changed = instrumenter.instrumentSync('function meaningOfLife() { return 42; }', 'filename.js');
296 *
297 * Usage in a browser
298 * ------------------
299 *
300 * Load `esprima.js`, `escodegen.js` and `instrumenter.js` (this file) using `script` tags or other means.
301 *
302 * Create an instrumenter object as:
303 *
304 * var instrumenter = new Instrumenter(),
305 * changed = instrumenter.instrumentSync('function meaningOfLife() { return 42; }', 'filename.js');
306 *
307 * Aside from demonstration purposes, it is unclear why you would want to instrument code in a browser.
308 *
309 * @class Instrumenter
310 * @constructor
311 * @param {Object} options Optional. Configuration options.
312 * @param {String} [options.coverageVariable] the global variable name to use for
313 * tracking coverage. Defaults to `__coverage__`
314 * @param {Boolean} [options.embedSource] whether to embed the source code of every
315 * file as an array in the file coverage object for that file. Defaults to `false`
316 * @param {Boolean} [options.preserveComments] whether comments should be preserved in the output. Defaults to `false`
317 * @param {Boolean} [options.noCompact] emit readable code when set. Defaults to `false`
318 * @param {Boolean} [options.noAutoWrap] do not automatically wrap the source in
319 * an anonymous function before covering it. By default, code is wrapped in
320 * an anonymous function before it is parsed. This is done because
321 * some nodejs libraries have `return` statements outside of
322 * a function which is technically invalid Javascript and causes the parser to fail.
323 * This construct, however, works correctly in node since module loading
324 * is done in the context of an anonymous function.
325 *
326 * Note that the semantics of the code *returned* by the instrumenter does not change in any way.
327 * The function wrapper is "unwrapped" before the instrumented code is generated.
328 * @param {Object} [options.codeGenerationOptions] an object that is directly passed to the `escodegen`
329 * library as configuration for code generation. The `noCompact` setting is not honored when this
330 * option is specified
331 * @param {Boolean} [options.debug] assist in debugging. Currently, the only effect of
332 * setting this option is a pretty-print of the coverage variable. Defaults to `false`
333 * @param {Boolean} [options.walkDebug] assist in debugging of the AST walker used by this class.
334 *
335 */
336 function Instrumenter(options) {
337 this.opts = options || {
338 debug: false,
339 walkDebug: false,
340 coverageVariable: '__coverage__',
341 codeGenerationOptions: undefined,
342 noAutoWrap: false,
343 noCompact: false,
344 embedSource: false,
345 preserveComments: false
346 };
347
348 this.walker = new Walker({
349 ExpressionStatement: this.coverStatement,
350 BreakStatement: this.coverStatement,
351 ContinueStatement: this.coverStatement,
352 DebuggerStatement: this.coverStatement,
353 ReturnStatement: this.coverStatement,
354 ThrowStatement: this.coverStatement,
355 TryStatement: this.coverStatement,
356 VariableDeclaration: this.coverStatement,
357 IfStatement: [ this.ifBlockConverter, this.coverStatement, this.ifBranchInjector ],
358 ForStatement: [ this.skipInit, this.loopBlockConverter, this.coverStatement ],
359 ForInStatement: [ this.skipLeft, this.loopBlockConverter, this.coverStatement ],
360 WhileStatement: [ this.loopBlockConverter, this.coverStatement ],
361 DoWhileStatement: [ this.loopBlockConverter, this.coverStatement ],
362 SwitchStatement: [ this.coverStatement, this.switchBranchInjector ],
363 SwitchCase: [ this.switchCaseInjector ],
364 WithStatement: [ this.withBlockConverter, this.coverStatement ],
365 FunctionDeclaration: [ this.coverFunction, this.coverStatement ],
366 FunctionExpression: this.coverFunction,
367 LabeledStatement: this.coverStatement,
368 ConditionalExpression: this.conditionalBranchInjector,
369 LogicalExpression: this.logicalExpressionBranchInjector,
370 ObjectExpression: this.maybeAddType
371 }, this.extractCurrentHint, this, this.opts.walkDebug);
372
373 //unit testing purposes only
374 if (this.opts.backdoor && this.opts.backdoor.omitTrackerSuffix) {
375 this.omitTrackerSuffix = true;
376 }
377 }
378
379 Instrumenter.prototype = {
380 /**
381 * synchronous instrumentation method. Throws when illegal code is passed to it
382 * @method instrumentSync
383 * @param {String} code the code to be instrumented as a String
384 * @param {String} filename Optional. The name of the file from which
385 * the code was read. A temporary filename is generated when not specified.
386 * Not specifying a filename is only useful for unit tests and demonstrations
387 * of this library.
388 */
389 instrumentSync: function (code, filename) {
390 var program;
391
392 //protect from users accidentally passing in a Buffer object instead
393 if (typeof code !== 'string') { throw new Error('Code must be string'); }
394 if (code.charAt(0) === '#') { //shebang, 'comment' it out, won't affect syntax tree locations for things we care about
395 code = '//' + code;
396 }
397 if (!this.opts.noAutoWrap) {
398 code = LEADER_WRAP + code + TRAILER_WRAP;
399 }
400 program = ESP.parse(code, {
401 loc: true,
402 range: true,
403 tokens: this.opts.preserveComments,
404 comment: true
405 });
406 if (this.opts.preserveComments) {
407 program = ESPGEN.attachComments(program, program.comments, program.tokens);
408 }
409 if (!this.opts.noAutoWrap) {
410 program = {
411 type: SYNTAX.Program.name,
412 body: program.body[0].expression.callee.body.body,
413 comments: program.comments
414 };
415 }
416 return this.instrumentASTSync(program, filename, code);
417 },
418 filterHints: function (comments) {
419 var ret = [],
420 i,
421 comment,
422 groups;
423 if (!(comments && isArray(comments))) {
424 return ret;
425 }
426 for (i = 0; i < comments.length; i += 1) {
427 comment = comments[i];
428 /* istanbul ignore else: paranoid check */
429 if (comment && comment.value && comment.range && isArray(comment.range)) {
430 groups = String(comment.value).match(COMMENT_RE);
431 if (groups) {
432 ret.push({ type: groups[1], start: comment.range[0], end: comment.range[1] });
433 }
434 }
435 }
436 return ret;
437 },
438 extractCurrentHint: function (node) {
439 if (!node.range) { return; }
440 var i = this.currentState.lastHintPosition + 1,
441 hints = this.currentState.hints,
442 nodeStart = node.range[0],
443 hint;
444 this.currentState.currentHint = null;
445 while (i < hints.length) {
446 hint = hints[i];
447 if (hint.end < nodeStart) {
448 this.currentState.currentHint = hint;
449 this.currentState.lastHintPosition = i;
450 i += 1;
451 } else {
452 break;
453 }
454 }
455 },
456 /**
457 * synchronous instrumentation method that instruments an AST instead.
458 * @method instrumentASTSync
459 * @param {String} program the AST to be instrumented
460 * @param {String} filename Optional. The name of the file from which
461 * the code was read. A temporary filename is generated when not specified.
462 * Not specifying a filename is only useful for unit tests and demonstrations
463 * of this library.
464 * @param {String} originalCode the original code corresponding to the AST,
465 * used for embedding the source into the coverage object
466 */
467 instrumentASTSync: function (program, filename, originalCode) {
468 var usingStrict = false,
469 codegenOptions,
470 generated,
471 preamble,
472 lineCount,
473 i;
474 filename = filename || String(new Date().getTime()) + '.js';
475 this.sourceMap = null;
476 this.coverState = {
477 path: filename,
478 s: {},
479 b: {},
480 f: {},
481 fnMap: {},
482 statementMap: {},
483 branchMap: {}
484 };
485 this.currentState = {
486 trackerVar: generateTrackerVar(filename, this.omitTrackerSuffix),
487 func: 0,
488 branch: 0,
489 variable: 0,
490 statement: 0,
491 hints: this.filterHints(program.comments),
492 currentHint: null,
493 lastHintPosition: -1,
494 ignoring: 0
495 };
496 if (program.body && program.body.length > 0 && this.isUseStrictExpression(program.body[0])) {
497 //nuke it
498 program.body.shift();
499 //and add it back at code generation time
500 usingStrict = true;
501 }
502 this.walker.startWalk(program);
503 codegenOptions = this.opts.codeGenerationOptions || { format: { compact: !this.opts.noCompact }};
504 codegenOptions.comment = this.opts.preserveComments;
505 //console.log(JSON.stringify(program, undefined, 2));
506
507 generated = ESPGEN.generate(program, codegenOptions);
508 preamble = this.getPreamble(originalCode || '', usingStrict);
509
510 if (generated.map && generated.code) {
511 lineCount = preamble.split(/\r\n|\r|\n/).length;
512 // offset all the generated line numbers by the number of lines in the preamble
513 for (i = 0; i < generated.map._mappings.length; i += 1) {
514 generated.map._mappings[i].generatedLine += lineCount;
515 }
516 this.sourceMap = generated.map;
517 generated = generated.code;
518 }
519
520 return preamble + '\n' + generated + '\n';
521 },
522 /**
523 * Callback based instrumentation. Note that this still executes synchronously in the same process tick
524 * and calls back immediately. It only provides the options for callback style error handling as
525 * opposed to a `try-catch` style and nothing more. Implemented as a wrapper over `instrumentSync`
526 *
527 * @method instrument
528 * @param {String} code the code to be instrumented as a String
529 * @param {String} filename Optional. The name of the file from which
530 * the code was read. A temporary filename is generated when not specified.
531 * Not specifying a filename is only useful for unit tests and demonstrations
532 * of this library.
533 * @param {Function(err, instrumentedCode)} callback - the callback function
534 */
535 instrument: function (code, filename, callback) {
536
537 if (!callback && typeof filename === 'function') {
538 callback = filename;
539 filename = null;
540 }
541 try {
542 callback(null, this.instrumentSync(code, filename));
543 } catch (ex) {
544 callback(ex);
545 }
546 },
547 /**
548 * returns the file coverage object for the code that was instrumented
549 * just before calling this method. Note that this represents a
550 * "zero-coverage" object which is not even representative of the code
551 * being loaded in node or a browser (which would increase the statement
552 * counts for mainline code).
553 * @method lastFileCoverage
554 * @return {Object} a "zero-coverage" file coverage object for the code last instrumented
555 * by this instrumenter
556 */
557 lastFileCoverage: function () {
558 return this.coverState;
559 },
560 /**
561 * returns the source map object for the code that was instrumented
562 * just before calling this method.
563 * @method lastSourceMap
564 * @return {Object} a source map object for the code last instrumented
565 * by this instrumenter
566 */
567 lastSourceMap: function () {
568 return this.sourceMap;
569 },
570 fixColumnPositions: function (coverState) {
571 var offset = LEADER_WRAP.length,
572 fixer = function (loc) {
573 if (loc.start.line === 1) {
574 loc.start.column -= offset;
575 }
576 if (loc.end.line === 1) {
577 loc.end.column -= offset;
578 }
579 },
580 k,
581 obj,
582 i,
583 locations;
584
585 obj = coverState.statementMap;
586 for (k in obj) {
587 /* istanbul ignore else: has own property */
588 if (obj.hasOwnProperty(k)) { fixer(obj[k]); }
589 }
590 obj = coverState.fnMap;
591 for (k in obj) {
592 /* istanbul ignore else: has own property */
593 if (obj.hasOwnProperty(k)) { fixer(obj[k].loc); }
594 }
595 obj = coverState.branchMap;
596 for (k in obj) {
597 /* istanbul ignore else: has own property */
598 if (obj.hasOwnProperty(k)) {
599 locations = obj[k].locations;
600 for (i = 0; i < locations.length; i += 1) {
601 fixer(locations[i]);
602 }
603 }
604 }
605 },
606
607 getPreamble: function (sourceCode, emitUseStrict) {
608 var varName = this.opts.coverageVariable || '__coverage__',
609 file = this.coverState.path.replace(/\\/g, '\\\\'),
610 tracker = this.currentState.trackerVar,
611 coverState,
612 strictLine = emitUseStrict ? '"use strict";' : '',
613 // return replacements using the function to ensure that the replacement is
614 // treated like a dumb string and not as a string with RE replacement patterns
615 replacer = function (s) {
616 return function () { return s; };
617 },
618 code;
619 if (!this.opts.noAutoWrap) {
620 this.fixColumnPositions(this.coverState);
621 }
622 if (this.opts.embedSource) {
623 this.coverState.code = sourceCode.split(/(?:\r?\n)|\r/);
624 }
625 coverState = this.opts.debug ? JSON.stringify(this.coverState, undefined, 4) : JSON.stringify(this.coverState);
626 code = [
627 "%STRICT%",
628 "var %VAR% = (Function('return this'))();",
629 "if (!%VAR%.%GLOBAL%) { %VAR%.%GLOBAL% = {}; }",
630 "%VAR% = %VAR%.%GLOBAL%;",
631 "if (!(%VAR%['%FILE%'])) {",
632 " %VAR%['%FILE%'] = %OBJECT%;",
633 "}",
634 "%VAR% = %VAR%['%FILE%'];"
635 ].join("\n")
636 .replace(/%STRICT%/g, replacer(strictLine))
637 .replace(/%VAR%/g, replacer(tracker))
638 .replace(/%GLOBAL%/g, replacer(varName))
639 .replace(/%FILE%/g, replacer(file))
640 .replace(/%OBJECT%/g, replacer(coverState));
641 return code;
642 },
643
644 startIgnore: function () {
645 this.currentState.ignoring += 1;
646 },
647
648 endIgnore: function () {
649 this.currentState.ignoring -= 1;
650 },
651
652 convertToBlock: function (node) {
653 if (!node) {
654 return { type: 'BlockStatement', body: [] };
655 } else if (node.type === 'BlockStatement') {
656 return node;
657 } else {
658 return { type: 'BlockStatement', body: [ node ] };
659 }
660 },
661
662 ifBlockConverter: function (node) {
663 node.consequent = this.convertToBlock(node.consequent);
664 node.alternate = this.convertToBlock(node.alternate);
665 },
666
667 loopBlockConverter: function (node) {
668 node.body = this.convertToBlock(node.body);
669 },
670
671 withBlockConverter: function (node) {
672 node.body = this.convertToBlock(node.body);
673 },
674
675 statementName: function (location, initValue) {
676 var sName,
677 ignoring = !!this.currentState.ignoring;
678
679 location.skip = ignoring || undefined;
680 initValue = initValue || 0;
681 this.currentState.statement += 1;
682 sName = this.currentState.statement;
683 this.coverState.statementMap[sName] = location;
684 this.coverState.s[sName] = initValue;
685 return sName;
686 },
687
688 skipInit: function (node /*, walker */) {
689 if (node.init) {
690 node.init.skipWalk = true;
691 }
692 },
693
694 skipLeft: function (node /*, walker */) {
695 node.left.skipWalk = true;
696 },
697
698 isUseStrictExpression: function (node) {
699 return node && node.type === SYNTAX.ExpressionStatement.name &&
700 node.expression && node.expression.type === SYNTAX.Literal.name &&
701 node.expression.value === 'use strict';
702 },
703
704 maybeSkipNode: function (node, type) {
705 var alreadyIgnoring = !!this.currentState.ignoring,
706 hint = this.currentState.currentHint,
707 ignoreThis = !alreadyIgnoring && hint && hint.type === type;
708
709 if (ignoreThis) {
710 this.startIgnore();
711 node.postprocessor = this.endIgnore;
712 return true;
713 }
714 return false;
715 },
716
717 coverStatement: function (node, walker) {
718 var sName,
719 incrStatementCount,
720 grandParent;
721
722 this.maybeSkipNode(node, 'next');
723
724 if (this.isUseStrictExpression(node)) {
725 grandParent = walker.ancestor(2);
726 /* istanbul ignore else: difficult to test */
727 if (grandParent) {
728 if ((grandParent.node.type === SYNTAX.FunctionExpression.name ||
729 grandParent.node.type === SYNTAX.FunctionDeclaration.name) &&
730 walker.parent().node.body[0] === node) {
731 return;
732 }
733 }
734 }
735 if (node.type === SYNTAX.FunctionDeclaration.name) {
736 sName = this.statementName(node.loc, 1);
737 } else {
738 sName = this.statementName(node.loc);
739 incrStatementCount = astgen.statement(
740 astgen.postIncrement(
741 astgen.subscript(
742 astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('s')),
743 astgen.stringLiteral(sName)
744 )
745 )
746 );
747 this.splice(incrStatementCount, node, walker);
748 }
749 },
750
751 splice: function (statements, node, walker) {
752 var targetNode = walker.isLabeled() ? walker.parent().node : node;
753 targetNode.prepend = targetNode.prepend || [];
754 pushAll(targetNode.prepend, statements);
755 },
756
757 functionName: function (node, line, location) {
758 this.currentState.func += 1;
759 var id = this.currentState.func,
760 ignoring = !!this.currentState.ignoring,
761 name = node.id ? node.id.name : '(anonymous_' + id + ')';
762 this.coverState.fnMap[id] = { name: name, line: line, loc: location, skip: ignoring || undefined };
763 this.coverState.f[id] = 0;
764 return id;
765 },
766
767 coverFunction: function (node, walker) {
768 var id,
769 body = node.body,
770 blockBody = body.body,
771 popped;
772
773 this.maybeSkipNode(node, 'next');
774
775 id = this.functionName(node, walker.startLineForNode(node), {
776 start: node.loc.start,
777 end: { line: node.body.loc.start.line, column: node.body.loc.start.column }
778 });
779
780 if (blockBody.length > 0 && this.isUseStrictExpression(blockBody[0])) {
781 popped = blockBody.shift();
782 }
783 blockBody.unshift(
784 astgen.statement(
785 astgen.postIncrement(
786 astgen.subscript(
787 astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('f')),
788 astgen.stringLiteral(id)
789 )
790 )
791 )
792 );
793 if (popped) {
794 blockBody.unshift(popped);
795 }
796 },
797
798 branchName: function (type, startLine, pathLocations) {
799 var bName,
800 paths = [],
801 locations = [],
802 i,
803 ignoring = !!this.currentState.ignoring;
804 this.currentState.branch += 1;
805 bName = this.currentState.branch;
806 for (i = 0; i < pathLocations.length; i += 1) {
807 pathLocations[i].skip = pathLocations[i].skip || ignoring || undefined;
808 locations.push(pathLocations[i]);
809 paths.push(0);
810 }
811 this.coverState.b[bName] = paths;
812 this.coverState.branchMap[bName] = { line: startLine, type: type, locations: locations };
813 return bName;
814 },
815
816 branchIncrementExprAst: function (varName, branchIndex, down) {
817 var ret = astgen.postIncrement(
818 astgen.subscript(
819 astgen.subscript(
820 astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('b')),
821 astgen.stringLiteral(varName)
822 ),
823 astgen.numericLiteral(branchIndex)
824 ),
825 down
826 );
827 return ret;
828 },
829
830 locationsForNodes: function (nodes) {
831 var ret = [],
832 i;
833 for (i = 0; i < nodes.length; i += 1) {
834 ret.push(nodes[i].loc);
835 }
836 return ret;
837 },
838
839 ifBranchInjector: function (node, walker) {
840 var alreadyIgnoring = !!this.currentState.ignoring,
841 hint = this.currentState.currentHint,
842 ignoreThen = !alreadyIgnoring && hint && hint.type === 'if',
843 ignoreElse = !alreadyIgnoring && hint && hint.type === 'else',
844 line = node.loc.start.line,
845 col = node.loc.start.column,
846 start = { line: line, column: col },
847 end = { line: line, column: col },
848 bName = this.branchName('if', walker.startLineForNode(node), [
849 { start: start, end: end, skip: ignoreThen || undefined },
850 { start: start, end: end, skip: ignoreElse || undefined }
851 ]),
852 thenBody = node.consequent.body,
853 elseBody = node.alternate.body,
854 child;
855 thenBody.unshift(astgen.statement(this.branchIncrementExprAst(bName, 0)));
856 elseBody.unshift(astgen.statement(this.branchIncrementExprAst(bName, 1)));
857 if (ignoreThen) { child = node.consequent; child.preprocessor = this.startIgnore; child.postprocessor = this.endIgnore; }
858 if (ignoreElse) { child = node.alternate; child.preprocessor = this.startIgnore; child.postprocessor = this.endIgnore; }
859 },
860
861 branchLocationFor: function (name, index) {
862 return this.coverState.branchMap[name].locations[index];
863 },
864
865 switchBranchInjector: function (node, walker) {
866 var cases = node.cases,
867 bName,
868 i;
869
870 if (!(cases && cases.length > 0)) {
871 return;
872 }
873 bName = this.branchName('switch', walker.startLineForNode(node), this.locationsForNodes(cases));
874 for (i = 0; i < cases.length; i += 1) {
875 cases[i].branchLocation = this.branchLocationFor(bName, i);
876 cases[i].consequent.unshift(astgen.statement(this.branchIncrementExprAst(bName, i)));
877 }
878 },
879
880 switchCaseInjector: function (node) {
881 var location = node.branchLocation;
882 delete node.branchLocation;
883 if (this.maybeSkipNode(node, 'next')) {
884 location.skip = true;
885 }
886 },
887
888 conditionalBranchInjector: function (node, walker) {
889 var bName = this.branchName('cond-expr', walker.startLineForNode(node), this.locationsForNodes([ node.consequent, node.alternate ])),
890 ast1 = this.branchIncrementExprAst(bName, 0),
891 ast2 = this.branchIncrementExprAst(bName, 1);
892
893 node.consequent.preprocessor = this.maybeAddSkip(this.branchLocationFor(bName, 0));
894 node.alternate.preprocessor = this.maybeAddSkip(this.branchLocationFor(bName, 1));
895 node.consequent = astgen.sequence(ast1, node.consequent);
896 node.alternate = astgen.sequence(ast2, node.alternate);
897 },
898
899 maybeAddSkip: function (branchLocation) {
900 return function (node) {
901 var alreadyIgnoring = !!this.currentState.ignoring,
902 hint = this.currentState.currentHint,
903 ignoreThis = !alreadyIgnoring && hint && hint.type === 'next';
904 if (ignoreThis) {
905 this.startIgnore();
906 node.postprocessor = this.endIgnore;
907 }
908 if (ignoreThis || alreadyIgnoring) {
909 branchLocation.skip = true;
910 }
911 };
912 },
913
914 logicalExpressionBranchInjector: function (node, walker) {
915 var parent = walker.parent(),
916 leaves = [],
917 bName,
918 tuple,
919 i;
920
921 this.maybeSkipNode(node, 'next');
922
923 if (parent && parent.node.type === SYNTAX.LogicalExpression.name) {
924 //already covered
925 return;
926 }
927
928 this.findLeaves(node, leaves);
929 bName = this.branchName('binary-expr',
930 walker.startLineForNode(node),
931 this.locationsForNodes(leaves.map(function (item) { return item.node; }))
932 );
933 for (i = 0; i < leaves.length; i += 1) {
934 tuple = leaves[i];
935 tuple.parent[tuple.property] = astgen.sequence(this.branchIncrementExprAst(bName, i), tuple.node);
936 tuple.node.preprocessor = this.maybeAddSkip(this.branchLocationFor(bName, i));
937 }
938 },
939
940 findLeaves: function (node, accumulator, parent, property) {
941 if (node.type === SYNTAX.LogicalExpression.name) {
942 this.findLeaves(node.left, accumulator, node, 'left');
943 this.findLeaves(node.right, accumulator, node, 'right');
944 } else {
945 accumulator.push({ node: node, parent: parent, property: property });
946 }
947 },
948 maybeAddType: function (node /*, walker */) {
949 var props = node.properties,
950 i,
951 child;
952 for (i = 0; i < props.length; i += 1) {
953 child = props[i];
954 if (!child.type) {
955 child.type = SYNTAX.Property.name;
956 }
957 }
958 }
959 };
960
961 if (isNode) {
962 module.exports = Instrumenter;
963 } else {
964 window.Instrumenter = Instrumenter;
965 }
966
967}(typeof module !== 'undefined' && typeof module.exports !== 'undefined' && typeof exports !== 'undefined'));
968