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 |
1 |
(function (isNode) { |
8 |
|
|
9 |
1 |
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 |
|
astgen, |
17 |
|
preconditions, |
18 |
|
cond, |
19 |
|
isArray = Array.isArray; |
20 |
|
|
21 |
1 |
Iif (!isArray) { |
22 |
|
isArray = function (thing) { return thing && Object.prototype.toString.call(thing) === '[object Array]'; }; |
23 |
|
} |
24 |
|
|
25 |
1 |
Iif (!isNode) { |
26 |
|
preconditions = { |
27 |
|
'Could not find esprima': ESP, |
28 |
|
'Could not find escodegen': ESPGEN, |
29 |
|
'JSON object not in scope': JSON, |
30 |
|
'Array does not implement push': [].push, |
31 |
|
'Array does not implement unshift': [].unshift |
32 |
|
}; |
33 |
|
for (cond in preconditions) { |
34 |
|
if (preconditions.hasOwnProperty(cond)) { |
35 |
|
if (!preconditions[cond]) { throw new Error(cond); } |
36 |
|
} |
37 |
|
} |
38 |
|
} |
39 |
|
|
40 |
1 |
function generateTrackerVar(filename, omitSuffix) { |
41 |
139 |
var hash, suffix; |
42 |
139 |
Eif (crypto !== null) { |
43 |
139 |
hash = crypto.createHash('md5'); |
44 |
139 |
hash.update(filename); |
45 |
139 |
suffix = hash.digest('base64'); |
46 |
|
//trim trailing equal signs, turn identifier unsafe chars to safe ones + => _ and / => $ |
47 |
139 |
suffix = suffix.replace(new RegExp('=', 'g'), '') |
48 |
|
.replace(new RegExp('\\+', 'g'), '_') |
49 |
|
.replace(new RegExp('/', 'g'), '$'); |
50 |
|
} else { |
51 |
|
/*jslint nomen: true */ |
52 |
|
window.__cov_seq = window.__cov_seq || 0; |
53 |
|
window.__cov_seq += 1; |
54 |
|
suffix = window.__cov_seq; |
55 |
|
} |
56 |
139 |
return '__cov_' + (omitSuffix ? '' : suffix); |
57 |
|
} |
58 |
|
|
59 |
1 |
function pushAll(ary, thing) { |
60 |
9063 |
if (!isArray(thing)) { |
61 |
7369 |
thing = [ thing ]; |
62 |
|
} |
63 |
9063 |
Array.prototype.push.apply(ary, thing); |
64 |
|
} |
65 |
|
|
66 |
1 |
SYNTAX = { |
67 |
|
ArrayExpression: [ 'elements' ], |
68 |
|
AssignmentExpression: ['left', 'right'], |
69 |
|
BinaryExpression: ['left', 'right' ], |
70 |
|
BlockStatement: [ 'body' ], |
71 |
|
BreakStatement: [ 'label' ], |
72 |
|
CallExpression: [ 'callee', 'arguments'], |
73 |
|
CatchClause: ['param', 'body'], |
74 |
|
ConditionalExpression: [ 'test', 'consequent', 'alternate' ], |
75 |
|
ContinueStatement: [ 'label' ], |
76 |
|
DebuggerStatement: [ ], |
77 |
|
DoWhileStatement: [ 'test', 'body' ], |
78 |
|
EmptyStatement: [], |
79 |
|
ExpressionStatement: [ 'expression'], |
80 |
|
ForInStatement: [ 'left', 'right', 'body' ], |
81 |
|
ForStatement: ['init', 'test', 'update', 'body' ], |
82 |
|
FunctionDeclaration: ['id', 'params', 'body'], |
83 |
|
FunctionExpression: ['id', 'params', 'defaults', 'body'], |
84 |
|
Identifier: [], |
85 |
|
IfStatement: ['test', 'consequent', 'alternate'], |
86 |
|
LabeledStatement: ['label', 'body'], |
87 |
|
Literal: [], |
88 |
|
LogicalExpression: [ 'left', 'right' ], |
89 |
|
MemberExpression: ['object', 'property'], |
90 |
|
NewExpression: ['callee', 'arguments'], |
91 |
|
ObjectExpression: [ 'properties' ], |
92 |
|
Program: [ 'body' ], |
93 |
|
Property: [ 'key', 'value'], |
94 |
|
ReturnStatement: ['argument'], |
95 |
|
SequenceExpression: ['expressions'], |
96 |
|
SwitchCase: [ 'test', 'consequent' ], |
97 |
|
SwitchStatement: ['discriminant', 'cases' ], |
98 |
|
ThisExpression: [], |
99 |
|
ThrowStatement: ['argument'], |
100 |
|
TryStatement: [ 'block', 'handlers', 'finalizer' ], |
101 |
|
UnaryExpression: ['argument'], |
102 |
|
UpdateExpression: [ 'argument' ], |
103 |
|
VariableDeclaration: [ 'declarations' ], |
104 |
|
VariableDeclarator: [ 'id', 'init' ], |
105 |
|
WhileStatement: [ 'test', 'body' ], |
106 |
|
WithStatement: [ 'object', 'body' ] |
107 |
|
|
108 |
|
}; |
109 |
|
|
110 |
1 |
for (nodeType in SYNTAX) { |
111 |
40 |
Eif (SYNTAX.hasOwnProperty(nodeType)) { |
112 |
40 |
SYNTAX[nodeType] = { name: nodeType, children: SYNTAX[nodeType] }; |
113 |
|
} |
114 |
|
} |
115 |
|
|
116 |
1 |
astgen = { |
117 |
5558 |
variable: function (name) { return { type: SYNTAX.Identifier.name, name: name }; }, |
118 |
2779 |
stringLiteral: function (str) { return { type: SYNTAX.Literal.name, value: String(str) }; }, |
119 |
795 |
numericLiteral: function (num) { return { type: SYNTAX.Literal.name, value: Number(num) }; }, |
120 |
2508 |
statement: function (contents) { return { type: SYNTAX.ExpressionStatement.name, expression: contents }; }, |
121 |
2779 |
dot: function (obj, field) { return { type: SYNTAX.MemberExpression.name, computed: false, object: obj, property: field }; }, |
122 |
3574 |
subscript: function (obj, sub) { return { type: SYNTAX.MemberExpression.name, computed: true, object: obj, property: sub }; }, |
123 |
2779 |
postIncrement: function (obj) { return { type: SYNTAX.UpdateExpression.name, operator: '++', prefix: false, argument: obj }; }, |
124 |
271 |
sequence: function (one, two) { return { type: SYNTAX.SequenceExpression.name, expressions: [one, two] }; } |
125 |
|
}; |
126 |
|
|
127 |
1 |
function Walker(walkMap, scope, debug) { |
128 |
109 |
this.walkMap = walkMap; |
129 |
109 |
this.scope = scope; |
130 |
109 |
this.debug = debug; |
131 |
109 |
if (this.debug) { |
132 |
2 |
this.level = 0; |
133 |
2 |
this.seq = true; |
134 |
|
} |
135 |
|
} |
136 |
|
|
137 |
1 |
function defaultWalker(node, walker) { |
138 |
|
|
139 |
25233 |
var type = node.type, |
140 |
|
children = SYNTAX[type].children, |
141 |
|
applyCustomWalker = !!node.loc || node.type === SYNTAX.Program.name, // don't run generated nodes thru custom walks otherwise we will attempt to instrument the instrumentation code :) |
142 |
|
walkerFn = applyCustomWalker ? walker.walkMap[type] : null, |
143 |
|
i, |
144 |
|
j, |
145 |
|
walkFnIndex, |
146 |
|
childType, |
147 |
|
childNode, |
148 |
|
ret = node, |
149 |
|
childArray, |
150 |
|
childElement, |
151 |
|
tmpNode, |
152 |
|
pathElement, |
153 |
|
assignNode, |
154 |
|
isLast; |
155 |
|
|
156 |
25233 |
Iif (node.walking) { throw new Error('Infinite regress: Custom walkers may NOT call walker.walk(node)'); } |
157 |
25233 |
node.walking = true; |
158 |
25233 |
if (isArray(walkerFn)) { |
159 |
373 |
for (walkFnIndex = 0; walkFnIndex < walkerFn.length; walkFnIndex += 1) { |
160 |
1026 |
isLast = walkFnIndex === walkerFn.length - 1; |
161 |
1026 |
ret = walker.walk(ret, walkerFn[walkFnIndex]) || ret; |
162 |
1026 |
Iif (ret.type !== type && !isLast) { throw new Error('Only the last walker is allowed to change the node type: [type was: ' + type + ' ]'); } |
163 |
|
} |
164 |
|
} else { |
165 |
24860 |
if (walkerFn) { |
166 |
1688 |
ret = walker.walk(node, walkerFn) || ret; |
167 |
|
} |
168 |
|
} |
169 |
|
|
170 |
25233 |
for (i = 0; i < children.length; i += 1) { |
171 |
22998 |
childType = children[i]; |
172 |
22998 |
childNode = node[childType]; |
173 |
22998 |
if (childNode && !childNode.skipWalk) { |
174 |
22341 |
pathElement = { node: node, property: childType }; |
175 |
22341 |
if (isArray(childNode)) { |
176 |
2914 |
childArray = []; |
177 |
2914 |
for (j = 0; j < childNode.length; j += 1) { |
178 |
5670 |
childElement = childNode[j]; |
179 |
5670 |
pathElement.index = j; |
180 |
5670 |
assignNode = walker.walk(childElement, null, pathElement) || childElement; |
181 |
5670 |
if (isArray(assignNode.prepend)) { |
182 |
1694 |
pushAll(childArray, assignNode.prepend); |
183 |
1694 |
delete assignNode.prepend; |
184 |
|
} |
185 |
5670 |
pushAll(childArray, assignNode); |
186 |
|
} |
187 |
2914 |
node[childType] = childArray; |
188 |
|
} else { |
189 |
19427 |
assignNode = walker.walk(childNode, null, pathElement) || childNode; |
190 |
19427 |
Iif (isArray(assignNode.prepend)) { |
191 |
|
throw new Error('Internal error: attempt to prepend statements in disallowed (non-array) context'); |
192 |
|
/* if this should be allowed, this is how to solve it |
193 |
|
tmpNode = { type: 'BlockStatement', body: [] }; |
194 |
|
pushAll(tmpNode.body, assignNode.prepend); |
195 |
|
pushAll(tmpNode.body, assignNode); |
196 |
|
node[childType] = tmpNode; |
197 |
|
delete assignNode.prepend; |
198 |
|
*/ |
199 |
|
} else { |
200 |
19427 |
node[childType] = assignNode; |
201 |
|
} |
202 |
|
} |
203 |
|
} |
204 |
|
} |
205 |
|
|
206 |
25233 |
delete node.walking; |
207 |
25233 |
return ret; |
208 |
|
} |
209 |
|
|
210 |
1 |
Walker.prototype = { |
211 |
|
startWalk: function (node) { |
212 |
136 |
this.path = []; |
213 |
136 |
this.walk(node); |
214 |
|
}, |
215 |
|
|
216 |
|
walk: function (node, walkFn, pathElement) { |
217 |
27947 |
var ret, i, seq, prefix; |
218 |
|
|
219 |
27947 |
walkFn = walkFn || defaultWalker; |
220 |
27947 |
if (this.debug) { |
221 |
8 |
this.seq += 1; |
222 |
8 |
this.level += 1; |
223 |
8 |
seq = this.seq; |
224 |
8 |
prefix = ''; |
225 |
27 |
for (i = 0; i < this.level; i += 1) { prefix += ' '; } |
226 |
8 |
console.log(prefix + 'Enter (' + seq + '):' + node.type); |
227 |
|
} |
228 |
27947 |
if (pathElement) { this.path.push(pathElement); } |
229 |
27947 |
ret = walkFn.call(this.scope, node, this); |
230 |
27947 |
if (pathElement) { this.path.pop(); } |
231 |
27947 |
if (this.debug) { |
232 |
8 |
this.level -= 1; |
233 |
8 |
console.log(prefix + 'Return (' + seq + '):' + node.type); |
234 |
|
} |
235 |
27947 |
return ret; |
236 |
|
}, |
237 |
|
|
238 |
|
startLineForNode: function (node) { |
239 |
672 |
return node && node.loc && node.loc.start ? node.loc.start.line : null; |
240 |
|
}, |
241 |
|
|
242 |
|
parent: function () { |
243 |
1789 |
return this.path.length > 0 ? this.path[this.path.length - 1] : null; |
244 |
|
}, |
245 |
|
|
246 |
|
isLabeled: function () { |
247 |
1699 |
var el = this.parent(); |
248 |
1699 |
return el && el.node.type === SYNTAX.LabeledStatement.name; |
249 |
|
} |
250 |
|
}; |
251 |
|
|
252 |
|
/** |
253 |
|
* mechanism to instrument code for coverage. It uses the `esprima` and |
254 |
|
* `escodegen` libraries for JS parsing and code generation respectively. |
255 |
|
* |
256 |
|
* Works on `node` as well as the browser. |
257 |
|
* |
258 |
|
* Usage on nodejs |
259 |
|
* --------------- |
260 |
|
* |
261 |
|
* var instrumenter = new require('istanbul').Instrumenter(), |
262 |
|
* changed = instrumenter.instrumentSync('function meaningOfLife() { return 42; }', 'filename.js'); |
263 |
|
* |
264 |
|
* Usage in a browser |
265 |
|
* ------------------ |
266 |
|
* |
267 |
|
* Load `esprima.js`, `escodegen.js` and `instrumenter.js` (this file) using `script` tags or other means. |
268 |
|
* |
269 |
|
* Create an instrumenter object as: |
270 |
|
* |
271 |
|
* var instrumenter = new Instrumenter(), |
272 |
|
* changed = instrumenter.instrumentSync('function meaningOfLife() { return 42; }', 'filename.js'); |
273 |
|
* |
274 |
|
* Aside from demonstration purposes, it is unclear why you would want to instrument code in a browser. |
275 |
|
* |
276 |
|
* @class Instrumenter |
277 |
|
* @constructor |
278 |
|
* @param {Object} options Optional. Configuration options. |
279 |
|
* @param {String} [options.coverageVariable] the global variable name to use for |
280 |
|
* tracking coverage. Defaults to `__coverage__` |
281 |
|
* @param {Boolean} [options.embedSource] whether to embed the source code of every |
282 |
|
* file as an array in the file coverage object for that file. Defaults to `false` |
283 |
|
* @param {Boolean} [options.noCompact] emit readable code when set. Defaults to `false` |
284 |
|
* @param {Boolean} [options.noAutoWrap] do not automatically wrap the source in |
285 |
|
* an anonymous function before covering it. By default, code is wrapped in |
286 |
|
* an anonymous function before it is parsed. This is done because |
287 |
|
* some nodejs libraries have `return` statements outside of |
288 |
|
* a function which is technically invalid Javascript and causes the parser to fail. |
289 |
|
* This construct, however, works correctly in node since module loading |
290 |
|
* is done in the context of an anonymous function. |
291 |
|
* |
292 |
|
* Note that the semantics of the code *returned* by the instrumenter does not change in any way. |
293 |
|
* The function wrapper is "unwrapped" before the instrumented code is generated. |
294 |
|
* @param {Object} [options.codeGenerationOptions] an object that is directly passed to the `escodegen` |
295 |
|
* library as configuration for code generation. The `noCompact` setting is not honored when this |
296 |
|
* option is specified |
297 |
|
* @param {Boolean} [options.debug] assist in debugging. Currently, the only effect of |
298 |
|
* setting this option is a pretty-print of the coverage variable. Defaults to `false` |
299 |
|
* @param {Boolean} [options.walkDebug] assist in debugging of the AST walker used by this class. |
300 |
|
* |
301 |
|
*/ |
302 |
1 |
function Instrumenter(options) { |
303 |
109 |
this.opts = options || { |
304 |
|
debug: false, |
305 |
|
walkDebug: false, |
306 |
|
coverageVariable: '__coverage__', |
307 |
|
codeGenerationOptions: undefined, |
308 |
|
noAutoWrap: false, |
309 |
|
noCompact: false, |
310 |
|
embedSource: false |
311 |
|
}; |
312 |
|
|
313 |
109 |
this.walker = new Walker({ |
314 |
|
ExpressionStatement: this.coverStatement, |
315 |
|
BreakStatement: this.coverStatement, |
316 |
|
ContinueStatement: this.coverStatement, |
317 |
|
DebuggerStatement: this.coverStatement, |
318 |
|
ReturnStatement: this.coverStatement, |
319 |
|
ThrowStatement: this.coverStatement, |
320 |
|
TryStatement: this.coverStatement, |
321 |
|
VariableDeclaration: this.coverStatement, |
322 |
|
IfStatement: [ this.ifBlockConverter, this.ifBranchInjector, this.coverStatement ], |
323 |
|
ForStatement: [ this.skipInit, this.loopBlockConverter, this.coverStatement ], |
324 |
|
ForInStatement: [ this.skipLeft, this.loopBlockConverter, this.coverStatement ], |
325 |
|
WhileStatement: [ this.loopBlockConverter, this.coverStatement ], |
326 |
|
DoWhileStatement: [ this.loopBlockConverter, this.coverStatement ], |
327 |
|
SwitchStatement: [ this.switchBranchInjector, this.coverStatement ], |
328 |
|
WithStatement: this.coverStatement, |
329 |
|
FunctionDeclaration: [ this.coverFunction, this.coverStatement ], |
330 |
|
FunctionExpression: this.coverFunction, |
331 |
|
LabeledStatement: this.coverStatement, |
332 |
|
ConditionalExpression: this.conditionalBranchInjector, |
333 |
|
LogicalExpression: this.logicalExpressionBranchInjector |
334 |
|
}, this, this.opts.walkDebug); |
335 |
|
|
336 |
|
//unit testing purposes only |
337 |
109 |
if (this.opts.backdoor && this.opts.backdoor.omitTrackerSuffix) { |
338 |
1 |
this.omitTrackerSuffix = true; |
339 |
|
} |
340 |
|
} |
341 |
|
|
342 |
1 |
Instrumenter.prototype = { |
343 |
|
/** |
344 |
|
* synchronous instrumentation method. Throws when illegal code is passed to it |
345 |
|
* @method instrumentSync |
346 |
|
* @param {String} code the code to be instrumented as a String |
347 |
|
* @param {String} filename Optional. The name of the file from which |
348 |
|
* the code was read. A temporary filename is generated when not specified. |
349 |
|
* Not specifying a filename is only useful for unit tests and demonstrations |
350 |
|
* of this library. |
351 |
|
*/ |
352 |
|
instrumentSync: function (code, filename) { |
353 |
139 |
var codegenOptions, |
354 |
|
program; |
355 |
139 |
filename = filename || String(new Date().getTime()) + '.js'; |
356 |
139 |
this.coverState = { |
357 |
|
path: filename, |
358 |
|
s: {}, |
359 |
|
b: {}, |
360 |
|
f: {}, |
361 |
|
fnMap: {}, |
362 |
|
statementMap: {}, |
363 |
|
branchMap: {} |
364 |
|
}; |
365 |
139 |
this.currentState = { |
366 |
|
trackerVar: generateTrackerVar(filename, this.omitTrackerSuffix), |
367 |
|
func: 0, |
368 |
|
branch: 0, |
369 |
|
variable: 0, |
370 |
|
statement: 0 |
371 |
|
}; |
372 |
|
|
373 |
139 |
if (typeof code !== 'string') { throw new Error('Code must be string'); } //protect from users accidentally passing in a Buffer object instead |
374 |
138 |
if (code.charAt(0) === '#') { //shebang, 'comment' it out, won't affect syntax tree locations for things we care about |
375 |
5 |
code = '//' + code; |
376 |
|
} |
377 |
138 |
if (!this.opts.noAutoWrap) { |
378 |
135 |
code = LEADER_WRAP + code + TRAILER_WRAP; |
379 |
|
} |
380 |
138 |
program = ESP.parse(code, { loc: true }); |
381 |
136 |
if (!this.opts.noAutoWrap) { |
382 |
134 |
program = { type: SYNTAX.Program.name, body: program.body[0].expression.callee.body.body }; |
383 |
|
} |
384 |
136 |
this.walker.startWalk(program); |
385 |
136 |
codegenOptions = this.opts.codeGenerationOptions || { format: { compact: !this.opts.noCompact }}; |
386 |
|
//console.log(JSON.stringify(program, undefined, 2)); |
387 |
136 |
return this.getPreamble(code) + '\n' + ESPGEN.generate(program, codegenOptions) + '\n'; |
388 |
|
}, |
389 |
|
/** |
390 |
|
* Callback based instrumentation. Note that this still executes synchronously in the same process tick |
391 |
|
* and calls back immediately. It only provides the options for callback style error handling as |
392 |
|
* opposed to a `try-catch` style and nothing more. Implemented as a wrapper over `instrumentSync` |
393 |
|
* |
394 |
|
* @method instrument |
395 |
|
* @param {String} code the code to be instrumented as a String |
396 |
|
* @param {String} filename Optional. The name of the file from which |
397 |
|
* the code was read. A temporary filename is generated when not specified. |
398 |
|
* Not specifying a filename is only useful for unit tests and demonstrations |
399 |
|
* of this library. |
400 |
|
* @param {Function(err, instrumentedCode)} callback - the callback function |
401 |
|
*/ |
402 |
|
instrument: function (code, filename, callback) { |
403 |
|
|
404 |
107 |
if (!callback && typeof filename === 'function') { |
405 |
2 |
callback = filename; |
406 |
2 |
filename = null; |
407 |
|
} |
408 |
107 |
try { |
409 |
107 |
callback(null, this.instrumentSync(code, filename)); |
410 |
|
} catch (ex) { |
411 |
2 |
callback(ex); |
412 |
|
} |
413 |
|
}, |
414 |
|
|
415 |
|
fixColumnPositions: function (coverState) { |
416 |
134 |
var offset = LEADER_WRAP.length, |
417 |
|
fixer = function (loc) { |
418 |
2771 |
if (loc.start.line === 1) { |
419 |
238 |
loc.start.column -= offset; |
420 |
|
} |
421 |
2771 |
if (loc.end.line === 1) { |
422 |
222 |
loc.end.column -= offset; |
423 |
|
} |
424 |
|
}, |
425 |
|
k, |
426 |
|
obj, |
427 |
|
i, |
428 |
|
locations; |
429 |
|
|
430 |
134 |
obj = coverState.statementMap; |
431 |
134 |
for (k in obj) { |
432 |
1695 |
Eif (obj.hasOwnProperty(k)) { fixer(obj[k]); } |
433 |
|
} |
434 |
134 |
obj = coverState.fnMap; |
435 |
134 |
for (k in obj) { |
436 |
285 |
Eif (obj.hasOwnProperty(k)) { fixer(obj[k].loc); } |
437 |
|
} |
438 |
134 |
obj = coverState.branchMap; |
439 |
134 |
for (k in obj) { |
440 |
385 |
Eif (obj.hasOwnProperty(k)) { |
441 |
385 |
locations = obj[k].locations; |
442 |
385 |
for (i = 0; i < locations.length; i += 1) { |
443 |
791 |
fixer(locations[i]); |
444 |
|
} |
445 |
|
} |
446 |
|
} |
447 |
|
}, |
448 |
|
|
449 |
|
getPreamble: function (sourceCode) { |
450 |
136 |
var varName = this.opts.coverageVariable || '__coverage__', |
451 |
|
file = this.coverState.path, |
452 |
|
tracker = this.currentState.trackerVar, |
453 |
|
coverState, |
454 |
|
replacer = function (s) { //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 |
455 |
1360 |
return function () { return s; }; |
456 |
|
}, |
457 |
|
code; |
458 |
136 |
if (!this.opts.noAutoWrap) { |
459 |
134 |
this.fixColumnPositions(this.coverState); |
460 |
|
} |
461 |
136 |
if (this.opts.embedSource) { |
462 |
1 |
this.coverState.code = sourceCode.split(/\n/); |
463 |
|
} |
464 |
136 |
coverState = this.opts.debug ? JSON.stringify(this.coverState, undefined, 4) : JSON.stringify(this.coverState); |
465 |
136 |
code = [ |
466 |
|
"if (typeof %GLOBAL% === 'undefined') { %GLOBAL% = {}; }", |
467 |
|
"if (!%GLOBAL%['%FILE%']) {", |
468 |
|
" %GLOBAL%['%FILE%'] = %OBJECT%;", |
469 |
|
"}", |
470 |
|
"var %VAR% = %GLOBAL%['%FILE%'];" |
471 |
|
].join("\n") |
472 |
|
.replace(/%VAR%/g, replacer(tracker)) |
473 |
|
.replace(/%GLOBAL%/g, replacer(varName)) |
474 |
|
.replace(/%FILE%/g, replacer(file)) |
475 |
|
.replace(/%OBJECT%/g, replacer(coverState)); |
476 |
136 |
return code; |
477 |
|
}, |
478 |
|
|
479 |
|
convertToBlock: function (node) { |
480 |
535 |
if (!node) { |
481 |
151 |
return { type: 'BlockStatement', body: [] }; |
482 |
384 |
} else if (node.type === 'BlockStatement') { |
483 |
261 |
return node; |
484 |
|
} else { |
485 |
123 |
return { type: 'BlockStatement', body: [ node ] }; |
486 |
|
} |
487 |
|
}, |
488 |
|
|
489 |
|
ifBlockConverter: function (node) { |
490 |
241 |
node.consequent = this.convertToBlock(node.consequent); |
491 |
241 |
node.alternate = this.convertToBlock(node.alternate); |
492 |
|
}, |
493 |
|
|
494 |
|
loopBlockConverter: function (node) { |
495 |
53 |
node.body = this.convertToBlock(node.body); |
496 |
|
}, |
497 |
|
|
498 |
|
statementName: function (location) { |
499 |
1699 |
var sName; |
500 |
1699 |
this.currentState.statement += 1; |
501 |
1699 |
sName = this.currentState.statement; |
502 |
1699 |
this.coverState.statementMap[sName] = location; |
503 |
1699 |
this.coverState.s[sName] = 0; |
504 |
1699 |
return sName; |
505 |
|
}, |
506 |
|
|
507 |
|
skipInit: function (node, walker) { |
508 |
32 |
if (node.init) { |
509 |
31 |
node.init.skipWalk = true; |
510 |
|
} |
511 |
|
}, |
512 |
|
|
513 |
|
skipLeft: function (node, walker) { |
514 |
7 |
node.left.skipWalk = true; |
515 |
|
}, |
516 |
|
|
517 |
|
coverStatement: function (node, walker) { |
518 |
1699 |
var sName, |
519 |
|
incrStatementCount; |
520 |
|
|
521 |
1699 |
sName = this.statementName(node.loc); |
522 |
1699 |
incrStatementCount = astgen.statement( |
523 |
|
astgen.postIncrement( |
524 |
|
astgen.subscript( |
525 |
|
astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('s')), |
526 |
|
astgen.stringLiteral(sName) |
527 |
|
) |
528 |
|
) |
529 |
|
); |
530 |
1699 |
this.splice(incrStatementCount, node, walker); |
531 |
|
}, |
532 |
|
|
533 |
|
splice: function (statements, node, walker) { |
534 |
1699 |
var targetNode = walker.isLabeled() ? walker.parent().node : node; |
535 |
1699 |
targetNode.prepend = targetNode.prepend || []; |
536 |
1699 |
pushAll(targetNode.prepend, statements); |
537 |
|
}, |
538 |
|
|
539 |
|
functionName: function (node, line, location) { |
540 |
285 |
this.currentState.func += 1; |
541 |
285 |
var id = this.currentState.func, |
542 |
|
name = node.id ? node.id.name : '(anonymous_' + id + ')'; |
543 |
285 |
this.coverState.fnMap[id] = { name: name, line: line, loc: location }; |
544 |
285 |
this.coverState.f[id] = 0; |
545 |
285 |
return id; |
546 |
|
}, |
547 |
|
|
548 |
|
coverFunction: function (node, walker) { |
549 |
285 |
var id = this.functionName(node, walker.startLineForNode(node), { start: node.loc.start, end: { line: node.body.loc.start.line, column: node.body.loc.start.column } }), |
550 |
|
body = node.body, |
551 |
|
blockBody = body.body; |
552 |
285 |
blockBody.unshift( |
553 |
|
astgen.statement( |
554 |
|
astgen.postIncrement( |
555 |
|
astgen.subscript( |
556 |
|
astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('f')), |
557 |
|
astgen.stringLiteral(id) |
558 |
|
) |
559 |
|
) |
560 |
|
) |
561 |
|
); |
562 |
|
}, |
563 |
|
|
564 |
|
branchName: function (type, startLine, pathLocations) { |
565 |
387 |
var bName, |
566 |
|
paths = [], |
567 |
|
locations = [], |
568 |
|
i; |
569 |
387 |
this.currentState.branch += 1; |
570 |
387 |
bName = this.currentState.branch; |
571 |
387 |
for (i = 0; i < pathLocations.length; i += 1) { |
572 |
795 |
paths.push(0); |
573 |
795 |
locations.push(pathLocations[i]); |
574 |
|
} |
575 |
387 |
this.coverState.b[bName] = paths; |
576 |
387 |
this.coverState.branchMap[bName] = { line: startLine, type: type, locations: locations }; |
577 |
387 |
return bName; |
578 |
|
}, |
579 |
|
|
580 |
|
branchIncrementExprAst: function (varName, branchIndex, down) { |
581 |
795 |
var ret = astgen.postIncrement( |
582 |
|
astgen.subscript( |
583 |
|
astgen.subscript( |
584 |
|
astgen.dot(astgen.variable(this.currentState.trackerVar), astgen.variable('b')), |
585 |
|
astgen.stringLiteral(varName) |
586 |
|
), |
587 |
|
astgen.numericLiteral(branchIndex) |
588 |
|
), |
589 |
|
down |
590 |
|
); |
591 |
795 |
return ret; |
592 |
|
}, |
593 |
|
|
594 |
|
locationsForNodes: function (nodes) { |
595 |
146 |
var ret = [], |
596 |
|
i; |
597 |
146 |
for (i = 0; i < nodes.length; i += 1) { |
598 |
313 |
ret.push(nodes[i].loc); |
599 |
|
} |
600 |
146 |
return ret; |
601 |
|
}, |
602 |
|
|
603 |
|
ifBranchInjector: function (node, walker) { |
604 |
241 |
var line = node.loc.start.line, |
605 |
|
col = node.loc.start.column, |
606 |
|
start = { line: line, column: col }, |
607 |
|
end = { line: line, column: col }, |
608 |
|
bName = this.branchName('if', walker.startLineForNode(node), [ |
609 |
|
{ start: start, end: end }, |
610 |
|
{ start: start, end: end } |
611 |
|
]), |
612 |
|
thenBody = node.consequent.body, |
613 |
|
elseBody = node.alternate.body; |
614 |
241 |
thenBody.unshift(astgen.statement(this.branchIncrementExprAst(bName, 0))); |
615 |
241 |
elseBody.unshift(astgen.statement(this.branchIncrementExprAst(bName, 1))); |
616 |
|
}, |
617 |
|
|
618 |
|
switchBranchInjector: function (node, walker) { |
619 |
16 |
var cases = node.cases, |
620 |
|
bName, |
621 |
|
i; |
622 |
|
|
623 |
16 |
if (!cases) { |
624 |
1 |
return; |
625 |
|
} |
626 |
15 |
bName = this.branchName('switch', walker.startLineForNode(node), this.locationsForNodes(cases)); |
627 |
15 |
for (i = 0; i < cases.length; i += 1) { |
628 |
42 |
cases[i].consequent.unshift(astgen.statement(this.branchIncrementExprAst(bName, i))); |
629 |
|
} |
630 |
|
}, |
631 |
|
|
632 |
|
conditionalBranchInjector: function (node, walker) { |
633 |
55 |
var bName = this.branchName('cond-expr', walker.startLineForNode(node), this.locationsForNodes([ node.consequent, node.alternate ])), |
634 |
|
ast1 = this.branchIncrementExprAst(bName, 0), |
635 |
|
ast2 = this.branchIncrementExprAst(bName, 1); |
636 |
|
|
637 |
55 |
node.consequent = astgen.sequence(ast1, node.consequent); |
638 |
55 |
node.alternate = astgen.sequence(ast2, node.alternate); |
639 |
|
}, |
640 |
|
|
641 |
|
logicalExpressionBranchInjector: function (node, walker) { |
642 |
85 |
var parent = walker.parent(), |
643 |
|
leaves = [], |
644 |
|
bName, |
645 |
|
tuple, |
646 |
|
i; |
647 |
|
|
648 |
85 |
if (parent && parent.node.type === SYNTAX.LogicalExpression.name) { |
649 |
|
//already covered |
650 |
9 |
return; |
651 |
|
} |
652 |
|
|
653 |
76 |
this.findLeaves(node, leaves); |
654 |
161 |
bName = this.branchName('binary-expr', walker.startLineForNode(node), this.locationsForNodes(leaves.map(function (item) { return item.node; }))); |
655 |
76 |
for (i = 0; i < leaves.length; i += 1) { |
656 |
161 |
tuple = leaves[i]; |
657 |
161 |
tuple.parent[tuple.property] = astgen.sequence(this.branchIncrementExprAst(bName, i), tuple.node); |
658 |
|
} |
659 |
|
}, |
660 |
|
|
661 |
|
findLeaves: function (node, accumulator, parent, property) { |
662 |
246 |
if (node.type === SYNTAX.LogicalExpression.name) { |
663 |
85 |
this.findLeaves(node.left, accumulator, node, 'left'); |
664 |
85 |
this.findLeaves(node.right, accumulator, node, 'right'); |
665 |
|
} else { |
666 |
161 |
accumulator.push({ node: node, parent: parent, property: property }); |
667 |
|
} |
668 |
|
} |
669 |
|
}; |
670 |
|
|
671 |
1 |
Eif (isNode) { |
672 |
1 |
module.exports = Instrumenter; |
673 |
|
} else { |
674 |
|
window.Instrumenter = Instrumenter; |
675 |
|
} |
676 |
|
|
677 |
|
}(typeof module !== 'undefined' && typeof module.exports !== 'undefined' && typeof exports !== 'undefined')); |
678 |
|
|
679 |
|
|