1 | import { createHash } from 'crypto';
|
2 | import { util } from 'babel-core';
|
3 | import prelude from './prelude';
|
4 | import meta from './meta';
|
5 | import { applyRules, addRules } from './tags';
|
6 |
|
7 | export function hash(code) {
|
8 | return createHash('sha1').update(code).digest('hex');
|
9 | }
|
10 |
|
11 | export function skip({ opts, file } = { }) {
|
12 | if (file && opts) {
|
13 | const { ignore = [], only } = opts;
|
14 | return util.shouldIgnore(
|
15 | file.opts.filename,
|
16 | util.arrayify(ignore, util.regexify),
|
17 | only ? util.arrayify(only, util.regexify) : null
|
18 | );
|
19 | }
|
20 | return false;
|
21 | }
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | export function key(path) {
|
30 | const node = path.node;
|
31 | if (node.loc) {
|
32 | const location = node.loc.start;
|
33 | return `${location.line}:${location.column}`;
|
34 | }
|
35 | throw new TypeError('Path must have valid location.');
|
36 | }
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 | function X(node) {
|
46 | node.__adana = true;
|
47 | return node;
|
48 | }
|
49 |
|
50 | function ignore(path) {
|
51 | return (!path.node || !path.node.loc || path.node.__adana);
|
52 | }
|
53 |
|
54 | function standardize(listener) {
|
55 | return (path, state) => ignore(path) ? undefined : listener(path, state);
|
56 | }
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | export default function adana({ types }) {
|
64 | |
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | function createMarker(state, options) {
|
72 | const { tags, loc, name, group } = options;
|
73 | const coverage = meta(state);
|
74 | const id = coverage.entries.length;
|
75 |
|
76 | coverage.entries.push({
|
77 | id,
|
78 | loc,
|
79 | tags,
|
80 | name,
|
81 | group,
|
82 | count: 0,
|
83 | });
|
84 |
|
85 |
|
86 | return X(types.unaryExpression('++', types.memberExpression(
|
87 | types.memberExpression(
|
88 | coverage.variable,
|
89 | types.numericLiteral(id),
|
90 | true
|
91 | ),
|
92 | types.stringLiteral('count'),
|
93 | true
|
94 | )));
|
95 | }
|
96 |
|
97 | |
98 |
|
99 |
|
100 |
|
101 |
|
102 | function isInstrumentableStatement(path) {
|
103 | const parent = path.parentPath;
|
104 | return !parent.isReturnStatement() &&
|
105 | !parent.isVariableDeclaration() &&
|
106 | !parent.isExportDeclaration() &&
|
107 | !parent.isFunctionDeclaration() &&
|
108 | !parent.isIfStatement();
|
109 | }
|
110 |
|
111 | |
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 | function instrument(path, state, options) {
|
120 |
|
121 |
|
122 |
|
123 | function marker() {
|
124 | return createMarker(state, {
|
125 | loc: path.node.loc,
|
126 | ...options,
|
127 | });
|
128 | }
|
129 |
|
130 | if (path.isBlockStatement()) {
|
131 | path.unshiftContainer('body', X(types.expressionStatement(marker())));
|
132 | } else if (path.isExpression()) {
|
133 | path.replaceWith(X(types.sequenceExpression([ marker(), path.node ])));
|
134 | } else if (path.isStatement()) {
|
135 | if (isInstrumentableStatement(path)) {
|
136 | path.insertBefore(X(types.expressionStatement(marker())));
|
137 | }
|
138 | }
|
139 | }
|
140 |
|
141 | |
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 | function visitStatement(path, state) {
|
148 | instrument(path, state, {
|
149 | tags: [ 'statement', 'line' ],
|
150 | loc: path.node.loc,
|
151 | });
|
152 | }
|
153 |
|
154 | |
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 | function visitFunction(path, state) {
|
162 | instrument(path.get('body'), state, {
|
163 | tags: [ 'function' ],
|
164 | name: path.node.id ? path.node.id.name : `@${key(path)}`,
|
165 | loc: path.node.loc,
|
166 | });
|
167 | }
|
168 |
|
169 | |
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 | function visitSwitchStatement(path, state) {
|
178 | let hasDefault = false;
|
179 | path.get('cases').forEach(entry => {
|
180 | if (entry.node.test) {
|
181 | addRules(state, entry.node.loc, entry.node.test.trailingComments);
|
182 | }
|
183 | if (entry.node.consequent.length > 1) {
|
184 | addRules(
|
185 | state,
|
186 | entry.node.loc,
|
187 | entry.node.consequent[0].leadingComments
|
188 | );
|
189 | }
|
190 |
|
191 | if (entry.node.test === null) {
|
192 | hasDefault = true;
|
193 | }
|
194 | entry.unshiftContainer('consequent', createMarker(state, {
|
195 | tags: [ 'branch' ],
|
196 | loc: entry.node.loc,
|
197 | group: key(path),
|
198 | }));
|
199 | });
|
200 |
|
201 |
|
202 |
|
203 | if (!hasDefault) {
|
204 |
|
205 |
|
206 | const cases = path.get('cases');
|
207 | if (cases.length > 0) {
|
208 | cases[cases.length - 1].pushContainer(
|
209 | 'consequent',
|
210 | types.breakStatement()
|
211 | );
|
212 | }
|
213 |
|
214 | path.pushContainer('cases', types.switchCase(null, [
|
215 | types.expressionStatement(createMarker(state, {
|
216 | tags: [ 'branch' ],
|
217 | loc: {
|
218 | start: path.node.loc.end,
|
219 | end: path.node.loc.end,
|
220 | },
|
221 | group: key(path),
|
222 | })),
|
223 | types.breakStatement(),
|
224 | ]));
|
225 | }
|
226 | }
|
227 |
|
228 | |
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 | function visitVariableDeclaration(path, state) {
|
235 | path.get('declarations').forEach(decl => {
|
236 | if (decl.has('init')) {
|
237 | instrument(decl.get('init'), state, {
|
238 | tags: [ 'statement', 'variable', 'line' ],
|
239 | });
|
240 | }
|
241 | });
|
242 | }
|
243 |
|
244 | |
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | function visitWhileLoop(path, state) {
|
252 | const test = path.get('test');
|
253 | const group = key(path);
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 | test.replaceWith(types.logicalExpression(
|
263 | '||',
|
264 | types.logicalExpression(
|
265 | '&&',
|
266 | X(test.node),
|
267 | createMarker(state, {
|
268 | tags: [ 'branch', 'line', 'statement' ],
|
269 | loc: test.node.loc,
|
270 | group,
|
271 | })
|
272 | ),
|
273 | types.unaryExpression(
|
274 | '!',
|
275 | createMarker(state, {
|
276 | tags: [ 'branch', 'line' ],
|
277 | loc: test.node.loc,
|
278 | group,
|
279 | })
|
280 | )
|
281 | ));
|
282 | }
|
283 |
|
284 | |
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 | function visitTryStatement(path, state) {
|
292 | const group = key(path);
|
293 | path.get('block').pushContainer('body', types.expressionStatement(
|
294 | createMarker(state, {
|
295 | tags: [ 'branch', 'line' ],
|
296 | loc: path.get('block').node.loc,
|
297 | group,
|
298 | })
|
299 | ));
|
300 | if (path.has('handler')) {
|
301 | path.get('handler.body').unshiftContainer(
|
302 | 'body',
|
303 | types.expressionStatement(
|
304 | createMarker(state, {
|
305 | tags: [ 'branch', 'line' ],
|
306 | loc: path.get('handler').node.loc,
|
307 | group,
|
308 | })
|
309 | )
|
310 | );
|
311 | } else {
|
312 | const loc = path.get('block').node.loc.end;
|
313 | path.get('handler').replaceWith(types.catchClause(
|
314 | types.identifier('err'), types.blockStatement([
|
315 | types.expressionStatement(
|
316 | createMarker(state, {
|
317 | tags: [ 'branch' ],
|
318 | loc: {
|
319 | start: loc,
|
320 | end: loc,
|
321 | },
|
322 | group,
|
323 | })
|
324 | ),
|
325 | types.throwStatement(
|
326 | types.identifier('err')
|
327 | ),
|
328 | ])
|
329 | ));
|
330 | }
|
331 | }
|
332 |
|
333 | |
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 | function visitReturnStatement(path, state) {
|
342 | if (!path.has('argument')) {
|
343 | path.get('argument').replaceWith(types.sequenceExpression([
|
344 | createMarker(state, {
|
345 | loc: path.node.loc,
|
346 | tags: [ 'line', 'statement' ],
|
347 | }),
|
348 | types.identifier('undefined'),
|
349 | ]));
|
350 | } else {
|
351 | instrument(path.get('argument'), state, {
|
352 | tags: [ 'line', 'statement' ],
|
353 | });
|
354 | }
|
355 | }
|
356 |
|
357 | |
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 | function visitObjectProperty(path, state) {
|
366 | if (!path.node.shorthand && !path.parentPath.isPattern()) {
|
367 | const key = path.get('key');
|
368 | const value = path.get('value');
|
369 | if (path.node.computed) {
|
370 | instrument(key, state, {
|
371 | tags: [ 'line' ],
|
372 | });
|
373 | }
|
374 | instrument(value, state, {
|
375 | tags: [ 'line' ],
|
376 | });
|
377 | }
|
378 | }
|
379 |
|
380 | |
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 |
|
388 | function visitArrayExpression(path, state) {
|
389 | if (!path.parentPath.isPattern()) {
|
390 | path.get('elements').forEach(element => {
|
391 | instrument(element, state, {
|
392 | tags: [ 'line' ],
|
393 | });
|
394 | });
|
395 | }
|
396 | }
|
397 |
|
398 | |
399 |
|
400 |
|
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 | function visitLogicalExpression(path, state) {
|
407 | const group = key(path);
|
408 | const test = path.scope.generateDeclaredUidIdentifier('test');
|
409 |
|
410 | path.replaceWith(X(types.conditionalExpression(
|
411 | types.assignmentExpression('=', test, X(path.node)),
|
412 | types.sequenceExpression([ createMarker(state, {
|
413 | tags: [ 'branch' ],
|
414 | loc: path.get('left').node.loc,
|
415 | group,
|
416 | }), test ]),
|
417 | types.sequenceExpression([ createMarker(state, {
|
418 | tags: [ 'branch' ],
|
419 | loc: path.get('right').node.loc,
|
420 | group,
|
421 | }), test ])
|
422 | )));
|
423 | }
|
424 |
|
425 | |
426 |
|
427 |
|
428 |
|
429 |
|
430 |
|
431 |
|
432 |
|
433 |
|
434 | function visitConditional(path, state) {
|
435 |
|
436 |
|
437 |
|
438 |
|
439 | const root = path.findParent(search => {
|
440 | return search.node.type === path.node.type &&
|
441 | !ignore(search) &&
|
442 | (!search.parentPath || search.parentPath.node.type !== path.node.type);
|
443 | }) || path;
|
444 |
|
445 |
|
446 | const group = key(root);
|
447 |
|
448 | function tagBranch(path) {
|
449 | addRules(state, path.node.loc, path.node.leadingComments);
|
450 | if (path.isBlockStatement() && path.node.body.length > 0) {
|
451 | addRules(state, path.node.loc, path.node.body[0].leadingComments);
|
452 | }
|
453 | }
|
454 |
|
455 | tagBranch(path.get('consequent'));
|
456 | if (path.has('alternate')) {
|
457 | tagBranch(path.get('alternate'));
|
458 | }
|
459 |
|
460 | instrument(path.get('consequent'), state, {
|
461 | tags: [ 'branch', 'line' ],
|
462 | loc: path.node.consequent.loc,
|
463 | group,
|
464 | });
|
465 |
|
466 | if (path.has('alternate') && !path.get('alternate').isIfStatement()) {
|
467 | instrument(path.get('alternate'), state, {
|
468 | tags: [ 'branch', 'line' ],
|
469 | loc: path.node.alternate.loc,
|
470 | group,
|
471 | });
|
472 | } else if (!path.has('alternate')) {
|
473 | path.get('alternate').replaceWith(types.expressionStatement(
|
474 | createMarker(state, {
|
475 | tags: [ 'branch' ],
|
476 | loc: {
|
477 | start: path.node.loc.end,
|
478 | end: path.node.loc.end,
|
479 | },
|
480 | group,
|
481 | }))
|
482 | );
|
483 | }
|
484 | }
|
485 |
|
486 | const visitor = {
|
487 |
|
488 | ArrowFunctionExpression: visitFunction,
|
489 | FunctionExpression: visitFunction,
|
490 | ObjectMethod: visitFunction,
|
491 | LogicalExpression: visitLogicalExpression,
|
492 | ConditionalExpression: visitConditional,
|
493 | ObjectProperty: visitObjectProperty,
|
494 | ArrayExpression: visitArrayExpression,
|
495 |
|
496 |
|
497 | FunctionDeclaration: visitFunction,
|
498 | VariableDeclaration: visitVariableDeclaration,
|
499 |
|
500 |
|
501 | ContinueStatement: visitStatement,
|
502 | BreakStatement: visitStatement,
|
503 | ExpressionStatement: visitStatement,
|
504 | ThrowStatement: visitStatement,
|
505 | ReturnStatement: visitReturnStatement,
|
506 | TryStatement: visitTryStatement,
|
507 | WhileStatement: visitWhileLoop,
|
508 | DoWhileStatement: visitWhileLoop,
|
509 | IfStatement: visitConditional,
|
510 | SwitchStatement: visitSwitchStatement,
|
511 | };
|
512 |
|
513 | Object.keys(visitor).forEach(key => {
|
514 | visitor[key] = standardize(visitor[key]);
|
515 | });
|
516 |
|
517 |
|
518 | return {
|
519 | visitor: {
|
520 | Program(path, state) {
|
521 |
|
522 | if (skip(state)) {
|
523 | return;
|
524 | }
|
525 | meta(state, {
|
526 | hash: hash(state.file.code),
|
527 | entries: [],
|
528 | rules: [],
|
529 | tags: {},
|
530 | variable: path.scope.generateUidIdentifier('coverage'),
|
531 | });
|
532 | path.traverse(visitor, state);
|
533 | applyRules(state);
|
534 | path.unshiftContainer('body', prelude(state));
|
535 | },
|
536 | },
|
537 | };
|
538 | }
|