UNPKG

19.2 kBJavaScriptView Raw
1/**
2 * @fileoverview A class of the code path analyzer.
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const assert = require("assert"),
13 CodePath = require("./code-path"),
14 CodePathSegment = require("./code-path-segment"),
15 IdGenerator = require("./id-generator"),
16 debug = require("./debug-helpers"),
17 astUtils = require("../ast-utils");
18
19//------------------------------------------------------------------------------
20// Helpers
21//------------------------------------------------------------------------------
22
23/**
24 * Checks whether or not a given node is a `case` node (not `default` node).
25 *
26 * @param {ASTNode} node - A `SwitchCase` node to check.
27 * @returns {boolean} `true` if the node is a `case` node (not `default` node).
28 */
29function isCaseNode(node) {
30 return Boolean(node.test);
31}
32
33/**
34 * Checks whether or not a given logical expression node goes different path
35 * between the `true` case and the `false` case.
36 *
37 * @param {ASTNode} node - A node to check.
38 * @returns {boolean} `true` if the node is a test of a choice statement.
39 */
40function isForkingByTrueOrFalse(node) {
41 const parent = node.parent;
42
43 switch (parent.type) {
44 case "ConditionalExpression":
45 case "IfStatement":
46 case "WhileStatement":
47 case "DoWhileStatement":
48 case "ForStatement":
49 return parent.test === node;
50
51 case "LogicalExpression":
52 return true;
53
54 default:
55 return false;
56 }
57}
58
59/**
60 * Gets the boolean value of a given literal node.
61 *
62 * This is used to detect infinity loops (e.g. `while (true) {}`).
63 * Statements preceded by an infinity loop are unreachable if the loop didn't
64 * have any `break` statement.
65 *
66 * @param {ASTNode} node - A node to get.
67 * @returns {boolean|undefined} a boolean value if the node is a Literal node,
68 * otherwise `undefined`.
69 */
70function getBooleanValueIfSimpleConstant(node) {
71 if (node.type === "Literal") {
72 return Boolean(node.value);
73 }
74 return void 0;
75}
76
77/**
78 * Checks that a given identifier node is a reference or not.
79 *
80 * This is used to detect the first throwable node in a `try` block.
81 *
82 * @param {ASTNode} node - An Identifier node to check.
83 * @returns {boolean} `true` if the node is a reference.
84 */
85function isIdentifierReference(node) {
86 const parent = node.parent;
87
88 switch (parent.type) {
89 case "LabeledStatement":
90 case "BreakStatement":
91 case "ContinueStatement":
92 case "ArrayPattern":
93 case "RestElement":
94 case "ImportSpecifier":
95 case "ImportDefaultSpecifier":
96 case "ImportNamespaceSpecifier":
97 case "CatchClause":
98 return false;
99
100 case "FunctionDeclaration":
101 case "FunctionExpression":
102 case "ArrowFunctionExpression":
103 case "ClassDeclaration":
104 case "ClassExpression":
105 case "VariableDeclarator":
106 return parent.id !== node;
107
108 case "Property":
109 case "MethodDefinition":
110 return (
111 parent.key !== node ||
112 parent.computed ||
113 parent.shorthand
114 );
115
116 case "AssignmentPattern":
117 return parent.key !== node;
118
119 default:
120 return true;
121 }
122}
123
124/**
125 * Updates the current segment with the head segment.
126 * This is similar to local branches and tracking branches of git.
127 *
128 * To separate the current and the head is in order to not make useless segments.
129 *
130 * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
131 * events are fired.
132 *
133 * @param {CodePathAnalyzer} analyzer - The instance.
134 * @param {ASTNode} node - The current AST node.
135 * @returns {void}
136 */
137function forwardCurrentToHead(analyzer, node) {
138 const codePath = analyzer.codePath;
139 const state = CodePath.getState(codePath);
140 const currentSegments = state.currentSegments;
141 const headSegments = state.headSegments;
142 const end = Math.max(currentSegments.length, headSegments.length);
143 let i, currentSegment, headSegment;
144
145 // Fires leaving events.
146 for (i = 0; i < end; ++i) {
147 currentSegment = currentSegments[i];
148 headSegment = headSegments[i];
149
150 if (currentSegment !== headSegment && currentSegment) {
151 debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
152
153 if (currentSegment.reachable) {
154 analyzer.emitter.emit(
155 "onCodePathSegmentEnd",
156 currentSegment,
157 node
158 );
159 }
160 }
161 }
162
163 // Update state.
164 state.currentSegments = headSegments;
165
166 // Fires entering events.
167 for (i = 0; i < end; ++i) {
168 currentSegment = currentSegments[i];
169 headSegment = headSegments[i];
170
171 if (currentSegment !== headSegment && headSegment) {
172 debug.dump(`onCodePathSegmentStart ${headSegment.id}`);
173
174 CodePathSegment.markUsed(headSegment);
175 if (headSegment.reachable) {
176 analyzer.emitter.emit(
177 "onCodePathSegmentStart",
178 headSegment,
179 node
180 );
181 }
182 }
183 }
184
185}
186
187/**
188 * Updates the current segment with empty.
189 * This is called at the last of functions or the program.
190 *
191 * @param {CodePathAnalyzer} analyzer - The instance.
192 * @param {ASTNode} node - The current AST node.
193 * @returns {void}
194 */
195function leaveFromCurrentSegment(analyzer, node) {
196 const state = CodePath.getState(analyzer.codePath);
197 const currentSegments = state.currentSegments;
198
199 for (let i = 0; i < currentSegments.length; ++i) {
200 const currentSegment = currentSegments[i];
201
202 debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
203 if (currentSegment.reachable) {
204 analyzer.emitter.emit(
205 "onCodePathSegmentEnd",
206 currentSegment,
207 node
208 );
209 }
210 }
211
212 state.currentSegments = [];
213}
214
215/**
216 * Updates the code path due to the position of a given node in the parent node
217 * thereof.
218 *
219 * For example, if the node is `parent.consequent`, this creates a fork from the
220 * current path.
221 *
222 * @param {CodePathAnalyzer} analyzer - The instance.
223 * @param {ASTNode} node - The current AST node.
224 * @returns {void}
225 */
226function preprocess(analyzer, node) {
227 const codePath = analyzer.codePath;
228 const state = CodePath.getState(codePath);
229 const parent = node.parent;
230
231 switch (parent.type) {
232 case "LogicalExpression":
233 if (parent.right === node) {
234 state.makeLogicalRight();
235 }
236 break;
237
238 case "ConditionalExpression":
239 case "IfStatement":
240
241 /*
242 * Fork if this node is at `consequent`/`alternate`.
243 * `popForkContext()` exists at `IfStatement:exit` and
244 * `ConditionalExpression:exit`.
245 */
246 if (parent.consequent === node) {
247 state.makeIfConsequent();
248 } else if (parent.alternate === node) {
249 state.makeIfAlternate();
250 }
251 break;
252
253 case "SwitchCase":
254 if (parent.consequent[0] === node) {
255 state.makeSwitchCaseBody(false, !parent.test);
256 }
257 break;
258
259 case "TryStatement":
260 if (parent.handler === node) {
261 state.makeCatchBlock();
262 } else if (parent.finalizer === node) {
263 state.makeFinallyBlock();
264 }
265 break;
266
267 case "WhileStatement":
268 if (parent.test === node) {
269 state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
270 } else {
271 assert(parent.body === node);
272 state.makeWhileBody();
273 }
274 break;
275
276 case "DoWhileStatement":
277 if (parent.body === node) {
278 state.makeDoWhileBody();
279 } else {
280 assert(parent.test === node);
281 state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
282 }
283 break;
284
285 case "ForStatement":
286 if (parent.test === node) {
287 state.makeForTest(getBooleanValueIfSimpleConstant(node));
288 } else if (parent.update === node) {
289 state.makeForUpdate();
290 } else if (parent.body === node) {
291 state.makeForBody();
292 }
293 break;
294
295 case "ForInStatement":
296 case "ForOfStatement":
297 if (parent.left === node) {
298 state.makeForInOfLeft();
299 } else if (parent.right === node) {
300 state.makeForInOfRight();
301 } else {
302 assert(parent.body === node);
303 state.makeForInOfBody();
304 }
305 break;
306
307 case "AssignmentPattern":
308
309 /*
310 * Fork if this node is at `right`.
311 * `left` is executed always, so it uses the current path.
312 * `popForkContext()` exists at `AssignmentPattern:exit`.
313 */
314 if (parent.right === node) {
315 state.pushForkContext();
316 state.forkBypassPath();
317 state.forkPath();
318 }
319 break;
320
321 default:
322 break;
323 }
324}
325
326/**
327 * Updates the code path due to the type of a given node in entering.
328 *
329 * @param {CodePathAnalyzer} analyzer - The instance.
330 * @param {ASTNode} node - The current AST node.
331 * @returns {void}
332 */
333function processCodePathToEnter(analyzer, node) {
334 let codePath = analyzer.codePath;
335 let state = codePath && CodePath.getState(codePath);
336 const parent = node.parent;
337
338 switch (node.type) {
339 case "Program":
340 case "FunctionDeclaration":
341 case "FunctionExpression":
342 case "ArrowFunctionExpression":
343 if (codePath) {
344
345 // Emits onCodePathSegmentStart events if updated.
346 forwardCurrentToHead(analyzer, node);
347 debug.dumpState(node, state, false);
348 }
349
350 // Create the code path of this scope.
351 codePath = analyzer.codePath = new CodePath(
352 analyzer.idGenerator.next(),
353 codePath,
354 analyzer.onLooped
355 );
356 state = CodePath.getState(codePath);
357
358 // Emits onCodePathStart events.
359 debug.dump(`onCodePathStart ${codePath.id}`);
360 analyzer.emitter.emit("onCodePathStart", codePath, node);
361 break;
362
363 case "LogicalExpression":
364 state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
365 break;
366
367 case "ConditionalExpression":
368 case "IfStatement":
369 state.pushChoiceContext("test", false);
370 break;
371
372 case "SwitchStatement":
373 state.pushSwitchContext(
374 node.cases.some(isCaseNode),
375 astUtils.getLabel(node)
376 );
377 break;
378
379 case "TryStatement":
380 state.pushTryContext(Boolean(node.finalizer));
381 break;
382
383 case "SwitchCase":
384
385 /*
386 * Fork if this node is after the 2st node in `cases`.
387 * It's similar to `else` blocks.
388 * The next `test` node is processed in this path.
389 */
390 if (parent.discriminant !== node && parent.cases[0] !== node) {
391 state.forkPath();
392 }
393 break;
394
395 case "WhileStatement":
396 case "DoWhileStatement":
397 case "ForStatement":
398 case "ForInStatement":
399 case "ForOfStatement":
400 state.pushLoopContext(node.type, astUtils.getLabel(node));
401 break;
402
403 case "LabeledStatement":
404 if (!astUtils.isBreakableStatement(node.body)) {
405 state.pushBreakContext(false, node.label.name);
406 }
407 break;
408
409 default:
410 break;
411 }
412
413 // Emits onCodePathSegmentStart events if updated.
414 forwardCurrentToHead(analyzer, node);
415 debug.dumpState(node, state, false);
416}
417
418/**
419 * Updates the code path due to the type of a given node in leaving.
420 *
421 * @param {CodePathAnalyzer} analyzer - The instance.
422 * @param {ASTNode} node - The current AST node.
423 * @returns {void}
424 */
425function processCodePathToExit(analyzer, node) {
426 const codePath = analyzer.codePath;
427 const state = CodePath.getState(codePath);
428 let dontForward = false;
429
430 switch (node.type) {
431 case "IfStatement":
432 case "ConditionalExpression":
433 case "LogicalExpression":
434 state.popChoiceContext();
435 break;
436
437 case "SwitchStatement":
438 state.popSwitchContext();
439 break;
440
441 case "SwitchCase":
442
443 /*
444 * This is the same as the process at the 1st `consequent` node in
445 * `preprocess` function.
446 * Must do if this `consequent` is empty.
447 */
448 if (node.consequent.length === 0) {
449 state.makeSwitchCaseBody(true, !node.test);
450 }
451 if (state.forkContext.reachable) {
452 dontForward = true;
453 }
454 break;
455
456 case "TryStatement":
457 state.popTryContext();
458 break;
459
460 case "BreakStatement":
461 forwardCurrentToHead(analyzer, node);
462 state.makeBreak(node.label && node.label.name);
463 dontForward = true;
464 break;
465
466 case "ContinueStatement":
467 forwardCurrentToHead(analyzer, node);
468 state.makeContinue(node.label && node.label.name);
469 dontForward = true;
470 break;
471
472 case "ReturnStatement":
473 forwardCurrentToHead(analyzer, node);
474 state.makeReturn();
475 dontForward = true;
476 break;
477
478 case "ThrowStatement":
479 forwardCurrentToHead(analyzer, node);
480 state.makeThrow();
481 dontForward = true;
482 break;
483
484 case "Identifier":
485 if (isIdentifierReference(node)) {
486 state.makeFirstThrowablePathInTryBlock();
487 dontForward = true;
488 }
489 break;
490
491 case "CallExpression":
492 case "MemberExpression":
493 case "NewExpression":
494 state.makeFirstThrowablePathInTryBlock();
495 break;
496
497 case "WhileStatement":
498 case "DoWhileStatement":
499 case "ForStatement":
500 case "ForInStatement":
501 case "ForOfStatement":
502 state.popLoopContext();
503 break;
504
505 case "AssignmentPattern":
506 state.popForkContext();
507 break;
508
509 case "LabeledStatement":
510 if (!astUtils.isBreakableStatement(node.body)) {
511 state.popBreakContext();
512 }
513 break;
514
515 default:
516 break;
517 }
518
519 // Emits onCodePathSegmentStart events if updated.
520 if (!dontForward) {
521 forwardCurrentToHead(analyzer, node);
522 }
523 debug.dumpState(node, state, true);
524}
525
526/**
527 * Updates the code path to finalize the current code path.
528 *
529 * @param {CodePathAnalyzer} analyzer - The instance.
530 * @param {ASTNode} node - The current AST node.
531 * @returns {void}
532 */
533function postprocess(analyzer, node) {
534 switch (node.type) {
535 case "Program":
536 case "FunctionDeclaration":
537 case "FunctionExpression":
538 case "ArrowFunctionExpression": {
539 let codePath = analyzer.codePath;
540
541 // Mark the current path as the final node.
542 CodePath.getState(codePath).makeFinal();
543
544 // Emits onCodePathSegmentEnd event of the current segments.
545 leaveFromCurrentSegment(analyzer, node);
546
547 // Emits onCodePathEnd event of this code path.
548 debug.dump(`onCodePathEnd ${codePath.id}`);
549 analyzer.emitter.emit("onCodePathEnd", codePath, node);
550 debug.dumpDot(codePath);
551
552 codePath = analyzer.codePath = analyzer.codePath.upper;
553 if (codePath) {
554 debug.dumpState(node, CodePath.getState(codePath), true);
555 }
556 break;
557 }
558
559 default:
560 break;
561 }
562}
563
564//------------------------------------------------------------------------------
565// Public Interface
566//------------------------------------------------------------------------------
567
568/**
569 * The class to analyze code paths.
570 * This class implements the EventGenerator interface.
571 */
572class CodePathAnalyzer {
573
574 /**
575 * @param {EventGenerator} eventGenerator - An event generator to wrap.
576 */
577 constructor(eventGenerator) {
578 this.original = eventGenerator;
579 this.emitter = eventGenerator.emitter;
580 this.codePath = null;
581 this.idGenerator = new IdGenerator("s");
582 this.currentNode = null;
583 this.onLooped = this.onLooped.bind(this);
584 }
585
586 /**
587 * Does the process to enter a given AST node.
588 * This updates state of analysis and calls `enterNode` of the wrapped.
589 *
590 * @param {ASTNode} node - A node which is entering.
591 * @returns {void}
592 */
593 enterNode(node) {
594 this.currentNode = node;
595
596 // Updates the code path due to node's position in its parent node.
597 if (node.parent) {
598 preprocess(this, node);
599 }
600
601 /*
602 * Updates the code path.
603 * And emits onCodePathStart/onCodePathSegmentStart events.
604 */
605 processCodePathToEnter(this, node);
606
607 // Emits node events.
608 this.original.enterNode(node);
609
610 this.currentNode = null;
611 }
612
613 /**
614 * Does the process to leave a given AST node.
615 * This updates state of analysis and calls `leaveNode` of the wrapped.
616 *
617 * @param {ASTNode} node - A node which is leaving.
618 * @returns {void}
619 */
620 leaveNode(node) {
621 this.currentNode = node;
622
623 /*
624 * Updates the code path.
625 * And emits onCodePathStart/onCodePathSegmentStart events.
626 */
627 processCodePathToExit(this, node);
628
629 // Emits node events.
630 this.original.leaveNode(node);
631
632 // Emits the last onCodePathStart/onCodePathSegmentStart events.
633 postprocess(this, node);
634
635 this.currentNode = null;
636 }
637
638 /**
639 * This is called on a code path looped.
640 * Then this raises a looped event.
641 *
642 * @param {CodePathSegment} fromSegment - A segment of prev.
643 * @param {CodePathSegment} toSegment - A segment of next.
644 * @returns {void}
645 */
646 onLooped(fromSegment, toSegment) {
647 if (fromSegment.reachable && toSegment.reachable) {
648 debug.dump(`onCodePathSegmentLoop ${fromSegment.id} -> ${toSegment.id}`);
649 this.emitter.emit(
650 "onCodePathSegmentLoop",
651 fromSegment,
652 toSegment,
653 this.currentNode
654 );
655 }
656 }
657}
658
659module.exports = CodePathAnalyzer;