main
js 802 lines 21.2 KB
Raw
1 'use strict';
2
3 /* eslint-disable react-internal/no-primitive-constructors */
4
5 //------------------------------------------------------------------------------
6 // Requirements
7 //------------------------------------------------------------------------------
8
9 // eslint-disable-next-line
10 const assert = require('./assert');
11 // eslint-disable-next-line
12 const CodePath = require('./code-path');
13 // eslint-disable-next-line
14 const CodePathSegment = require('./code-path-segment');
15 // eslint-disable-next-line
16 const IdGenerator = require('./id-generator');
17
18 const breakableTypePattern =
19 /^(?:(?:Do)?While|For(?:In|Of)?|Switch)Statement$/u;
20
21 //------------------------------------------------------------------------------
22 // Helpers
23 //------------------------------------------------------------------------------
24
25 /**
26 * Checks whether or not a given node is a `case` node (not `default` node).
27 * @param {ASTNode} node A `SwitchCase` node to check.
28 * @returns {boolean} `true` if the node is a `case` node (not `default` node).
29 */
30 function isCaseNode(node) {
31 return Boolean(node.test);
32 }
33
34 /**
35 * Checks if a given node appears as the value of a PropertyDefinition node.
36 * @param {ASTNode} node THe node to check.
37 * @returns {boolean} `true` if the node is a PropertyDefinition value,
38 * false if not.
39 */
40 function isPropertyDefinitionValue(node) {
41 const parent = node.parent;
42
43 return (
44 parent && parent.type === 'PropertyDefinition' && parent.value === node
45 );
46 }
47
48 /**
49 * Checks whether the given logical operator is taken into account for the code
50 * path analysis.
51 * @param {string} operator The operator found in the LogicalExpression node
52 * @returns {boolean} `true` if the operator is "&&" or "||" or "??"
53 */
54 function isHandledLogicalOperator(operator) {
55 return operator === '&&' || operator === '||' || operator === '??';
56 }
57
58 /**
59 * Checks whether the given assignment operator is a logical assignment operator.
60 * Logical assignments are taken into account for the code path analysis
61 * because of their short-circuiting semantics.
62 * @param {string} operator The operator found in the AssignmentExpression node
63 * @returns {boolean} `true` if the operator is "&&=" or "||=" or "??="
64 */
65 function isLogicalAssignmentOperator(operator) {
66 return operator === '&&=' || operator === '||=' || operator === '??=';
67 }
68
69 /**
70 * Gets the label if the parent node of a given node is a LabeledStatement.
71 * @param {ASTNode} node A node to get.
72 * @returns {string|null} The label or `null`.
73 */
74 function getLabel(node) {
75 if (node.parent.type === 'LabeledStatement') {
76 return node.parent.label.name;
77 }
78 return null;
79 }
80
81 /**
82 * Checks whether or not a given logical expression node goes different path
83 * between the `true` case and the `false` case.
84 * @param {ASTNode} node A node to check.
85 * @returns {boolean} `true` if the node is a test of a choice statement.
86 */
87 function isForkingByTrueOrFalse(node) {
88 const parent = node.parent;
89
90 switch (parent.type) {
91 case 'ConditionalExpression':
92 case 'IfStatement':
93 case 'WhileStatement':
94 case 'DoWhileStatement':
95 case 'ForStatement':
96 return parent.test === node;
97
98 case 'LogicalExpression':
99 return isHandledLogicalOperator(parent.operator);
100
101 case 'AssignmentExpression':
102 return isLogicalAssignmentOperator(parent.operator);
103
104 default:
105 return false;
106 }
107 }
108
109 /**
110 * Gets the boolean value of a given literal node.
111 *
112 * This is used to detect infinity loops (e.g. `while (true) {}`).
113 * Statements preceded by an infinity loop are unreachable if the loop didn't
114 * have any `break` statement.
115 * @param {ASTNode} node A node to get.
116 * @returns {boolean|undefined} a boolean value if the node is a Literal node,
117 * otherwise `undefined`.
118 */
119 function getBooleanValueIfSimpleConstant(node) {
120 if (node.type === 'Literal') {
121 return Boolean(node.value);
122 }
123 return void 0;
124 }
125
126 /**
127 * Checks that a given identifier node is a reference or not.
128 *
129 * This is used to detect the first throwable node in a `try` block.
130 * @param {ASTNode} node An Identifier node to check.
131 * @returns {boolean} `true` if the node is a reference.
132 */
133 function isIdentifierReference(node) {
134 const parent = node.parent;
135
136 switch (parent.type) {
137 case 'LabeledStatement':
138 case 'BreakStatement':
139 case 'ContinueStatement':
140 case 'ArrayPattern':
141 case 'RestElement':
142 case 'ImportSpecifier':
143 case 'ImportDefaultSpecifier':
144 case 'ImportNamespaceSpecifier':
145 case 'CatchClause':
146 return false;
147
148 case 'FunctionDeclaration':
149 case 'ComponentDeclaration':
150 case 'HookDeclaration':
151 case 'FunctionExpression':
152 case 'ArrowFunctionExpression':
153 case 'ClassDeclaration':
154 case 'ClassExpression':
155 case 'VariableDeclarator':
156 return parent.id !== node;
157
158 case 'Property':
159 case 'PropertyDefinition':
160 case 'MethodDefinition':
161 return parent.key !== node || parent.computed || parent.shorthand;
162
163 case 'AssignmentPattern':
164 return parent.key !== node;
165
166 default:
167 return true;
168 }
169 }
170
171 /**
172 * Updates the current segment with the head segment.
173 * This is similar to local branches and tracking branches of git.
174 *
175 * To separate the current and the head is in order to not make useless segments.
176 *
177 * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
178 * events are fired.
179 * @param {CodePathAnalyzer} analyzer The instance.
180 * @param {ASTNode} node The current AST node.
181 * @returns {void}
182 */
183 function forwardCurrentToHead(analyzer, node) {
184 const codePath = analyzer.codePath;
185 const state = CodePath.getState(codePath);
186 const currentSegments = state.currentSegments;
187 const headSegments = state.headSegments;
188 const end = Math.max(currentSegments.length, headSegments.length);
189 let i, currentSegment, headSegment;
190
191 // Fires leaving events.
192 for (i = 0; i < end; ++i) {
193 currentSegment = currentSegments[i];
194 headSegment = headSegments[i];
195
196 if (currentSegment !== headSegment && currentSegment) {
197 if (currentSegment.reachable) {
198 analyzer.emitter.emit('onCodePathSegmentEnd', currentSegment, node);
199 }
200 }
201 }
202
203 // Update state.
204 state.currentSegments = headSegments;
205
206 // Fires entering events.
207 for (i = 0; i < end; ++i) {
208 currentSegment = currentSegments[i];
209 headSegment = headSegments[i];
210
211 if (currentSegment !== headSegment && headSegment) {
212 CodePathSegment.markUsed(headSegment);
213 if (headSegment.reachable) {
214 analyzer.emitter.emit('onCodePathSegmentStart', headSegment, node);
215 }
216 }
217 }
218 }
219
220 /**
221 * Updates the current segment with empty.
222 * This is called at the last of functions or the program.
223 * @param {CodePathAnalyzer} analyzer The instance.
224 * @param {ASTNode} node The current AST node.
225 * @returns {void}
226 */
227 function leaveFromCurrentSegment(analyzer, node) {
228 const state = CodePath.getState(analyzer.codePath);
229 const currentSegments = state.currentSegments;
230
231 for (let i = 0; i < currentSegments.length; ++i) {
232 const currentSegment = currentSegments[i];
233 if (currentSegment.reachable) {
234 analyzer.emitter.emit('onCodePathSegmentEnd', currentSegment, node);
235 }
236 }
237
238 state.currentSegments = [];
239 }
240
241 /**
242 * Updates the code path due to the position of a given node in the parent node
243 * thereof.
244 *
245 * For example, if the node is `parent.consequent`, this creates a fork from the
246 * current path.
247 * @param {CodePathAnalyzer} analyzer The instance.
248 * @param {ASTNode} node The current AST node.
249 * @returns {void}
250 */
251 function preprocess(analyzer, node) {
252 const codePath = analyzer.codePath;
253 const state = CodePath.getState(codePath);
254 const parent = node.parent;
255
256 switch (parent.type) {
257 // The `arguments.length == 0` case is in `postprocess` function.
258 case 'CallExpression':
259 if (
260 parent.optional === true &&
261 parent.arguments.length >= 1 &&
262 parent.arguments[0] === node
263 ) {
264 state.makeOptionalRight();
265 }
266 break;
267 case 'MemberExpression':
268 if (parent.optional === true && parent.property === node) {
269 state.makeOptionalRight();
270 }
271 break;
272
273 case 'LogicalExpression':
274 if (parent.right === node && isHandledLogicalOperator(parent.operator)) {
275 state.makeLogicalRight();
276 }
277 break;
278
279 case 'AssignmentExpression':
280 if (
281 parent.right === node &&
282 isLogicalAssignmentOperator(parent.operator)
283 ) {
284 state.makeLogicalRight();
285 }
286 break;
287
288 case 'ConditionalExpression':
289 case 'IfStatement':
290 /*
291 * Fork if this node is at `consequent`/`alternate`.
292 * `popForkContext()` exists at `IfStatement:exit` and
293 * `ConditionalExpression:exit`.
294 */
295 if (parent.consequent === node) {
296 state.makeIfConsequent();
297 } else if (parent.alternate === node) {
298 state.makeIfAlternate();
299 }
300 break;
301
302 case 'SwitchCase':
303 if (parent.consequent[0] === node) {
304 state.makeSwitchCaseBody(false, !parent.test);
305 }
306 break;
307
308 case 'TryStatement':
309 if (parent.handler === node) {
310 state.makeCatchBlock();
311 } else if (parent.finalizer === node) {
312 state.makeFinallyBlock();
313 }
314 break;
315
316 case 'WhileStatement':
317 if (parent.test === node) {
318 state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
319 } else {
320 assert(parent.body === node);
321 state.makeWhileBody();
322 }
323 break;
324
325 case 'DoWhileStatement':
326 if (parent.body === node) {
327 state.makeDoWhileBody();
328 } else {
329 assert(parent.test === node);
330 state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
331 }
332 break;
333
334 case 'ForStatement':
335 if (parent.test === node) {
336 state.makeForTest(getBooleanValueIfSimpleConstant(node));
337 } else if (parent.update === node) {
338 state.makeForUpdate();
339 } else if (parent.body === node) {
340 state.makeForBody();
341 }
342 break;
343
344 case 'ForInStatement':
345 case 'ForOfStatement':
346 if (parent.left === node) {
347 state.makeForInOfLeft();
348 } else if (parent.right === node) {
349 state.makeForInOfRight();
350 } else {
351 assert(parent.body === node);
352 state.makeForInOfBody();
353 }
354 break;
355
356 case 'AssignmentPattern':
357 /*
358 * Fork if this node is at `right`.
359 * `left` is executed always, so it uses the current path.
360 * `popForkContext()` exists at `AssignmentPattern:exit`.
361 */
362 if (parent.right === node) {
363 state.pushForkContext();
364 state.forkBypassPath();
365 state.forkPath();
366 }
367 break;
368
369 default:
370 break;
371 }
372 }
373
374 /**
375 * Updates the code path due to the type of a given node in entering.
376 * @param {CodePathAnalyzer} analyzer The instance.
377 * @param {ASTNode} node The current AST node.
378 * @returns {void}
379 */
380 function processCodePathToEnter(analyzer, node) {
381 let codePath = analyzer.codePath;
382 let state = codePath && CodePath.getState(codePath);
383 const parent = node.parent;
384
385 /**
386 * Creates a new code path and trigger the onCodePathStart event
387 * based on the currently selected node.
388 * @param {string} origin The reason the code path was started.
389 * @returns {void}
390 */
391 function startCodePath(origin) {
392 if (codePath) {
393 // Emits onCodePathSegmentStart events if updated.
394 forwardCurrentToHead(analyzer, node);
395 }
396
397 // Create the code path of this scope.
398 codePath = analyzer.codePath = new CodePath({
399 id: analyzer.idGenerator.next(),
400 origin,
401 upper: codePath,
402 onLooped: analyzer.onLooped,
403 });
404 state = CodePath.getState(codePath);
405
406 // Emits onCodePathStart events.
407 analyzer.emitter.emit('onCodePathStart', codePath, node);
408 }
409
410 /*
411 * Special case: The right side of class field initializer is considered
412 * to be its own function, so we need to start a new code path in this
413 * case.
414 */
415 if (isPropertyDefinitionValue(node)) {
416 startCodePath('class-field-initializer');
417
418 /*
419 * Intentional fall through because `node` needs to also be
420 * processed by the code below. For example, if we have:
421 *
422 * class Foo {
423 * a = () => {}
424 * }
425 *
426 * In this case, we also need start a second code path.
427 */
428 }
429
430 switch (node.type) {
431 case 'Program':
432 startCodePath('program');
433 break;
434
435 case 'FunctionDeclaration':
436 case 'ComponentDeclaration':
437 case 'HookDeclaration':
438 case 'FunctionExpression':
439 case 'ArrowFunctionExpression':
440 startCodePath('function');
441 break;
442
443 case 'StaticBlock':
444 startCodePath('class-static-block');
445 break;
446
447 case 'ChainExpression':
448 state.pushChainContext();
449 break;
450 case 'CallExpression':
451 if (node.optional === true) {
452 state.makeOptionalNode();
453 }
454 break;
455 case 'MemberExpression':
456 if (node.optional === true) {
457 state.makeOptionalNode();
458 }
459 break;
460
461 case 'LogicalExpression':
462 if (isHandledLogicalOperator(node.operator)) {
463 state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
464 }
465 break;
466
467 case 'AssignmentExpression':
468 if (isLogicalAssignmentOperator(node.operator)) {
469 state.pushChoiceContext(
470 node.operator.slice(0, -1), // removes `=` from the end
471 isForkingByTrueOrFalse(node),
472 );
473 }
474 break;
475
476 case 'ConditionalExpression':
477 case 'IfStatement':
478 state.pushChoiceContext('test', false);
479 break;
480
481 case 'SwitchStatement':
482 state.pushSwitchContext(node.cases.some(isCaseNode), getLabel(node));
483 break;
484
485 case 'TryStatement':
486 state.pushTryContext(Boolean(node.finalizer));
487 break;
488
489 case 'SwitchCase':
490 /*
491 * Fork if this node is after the 2st node in `cases`.
492 * It's similar to `else` blocks.
493 * The next `test` node is processed in this path.
494 */
495 if (parent.discriminant !== node && parent.cases[0] !== node) {
496 state.forkPath();
497 }
498 break;
499
500 case 'WhileStatement':
501 case 'DoWhileStatement':
502 case 'ForStatement':
503 case 'ForInStatement':
504 case 'ForOfStatement':
505 state.pushLoopContext(node.type, getLabel(node));
506 break;
507
508 case 'LabeledStatement':
509 if (!breakableTypePattern.test(node.body.type)) {
510 state.pushBreakContext(false, node.label.name);
511 }
512 break;
513
514 default:
515 break;
516 }
517
518 // Emits onCodePathSegmentStart events if updated.
519 forwardCurrentToHead(analyzer, node);
520 }
521
522 /**
523 * Updates the code path due to the type of a given node in leaving.
524 * @param {CodePathAnalyzer} analyzer The instance.
525 * @param {ASTNode} node The current AST node.
526 * @returns {void}
527 */
528 function processCodePathToExit(analyzer, node) {
529 const codePath = analyzer.codePath;
530 const state = CodePath.getState(codePath);
531 let dontForward = false;
532
533 switch (node.type) {
534 case 'ChainExpression':
535 state.popChainContext();
536 break;
537
538 case 'IfStatement':
539 case 'ConditionalExpression':
540 state.popChoiceContext();
541 break;
542
543 case 'LogicalExpression':
544 if (isHandledLogicalOperator(node.operator)) {
545 state.popChoiceContext();
546 }
547 break;
548
549 case 'AssignmentExpression':
550 if (isLogicalAssignmentOperator(node.operator)) {
551 state.popChoiceContext();
552 }
553 break;
554
555 case 'SwitchStatement':
556 state.popSwitchContext();
557 break;
558
559 case 'SwitchCase':
560 /*
561 * This is the same as the process at the 1st `consequent` node in
562 * `preprocess` function.
563 * Must do if this `consequent` is empty.
564 */
565 if (node.consequent.length === 0) {
566 state.makeSwitchCaseBody(true, !node.test);
567 }
568 if (state.forkContext.reachable) {
569 dontForward = true;
570 }
571 break;
572
573 case 'TryStatement':
574 state.popTryContext();
575 break;
576
577 case 'BreakStatement':
578 forwardCurrentToHead(analyzer, node);
579 state.makeBreak(node.label && node.label.name);
580 dontForward = true;
581 break;
582
583 case 'ContinueStatement':
584 forwardCurrentToHead(analyzer, node);
585 state.makeContinue(node.label && node.label.name);
586 dontForward = true;
587 break;
588
589 case 'ReturnStatement':
590 forwardCurrentToHead(analyzer, node);
591 state.makeReturn();
592 dontForward = true;
593 break;
594
595 case 'ThrowStatement':
596 forwardCurrentToHead(analyzer, node);
597 state.makeThrow();
598 dontForward = true;
599 break;
600
601 case 'Identifier':
602 if (isIdentifierReference(node)) {
603 state.makeFirstThrowablePathInTryBlock();
604 dontForward = true;
605 }
606 break;
607
608 case 'CallExpression':
609 case 'ImportExpression':
610 case 'MemberExpression':
611 case 'NewExpression':
612 case 'YieldExpression':
613 state.makeFirstThrowablePathInTryBlock();
614 break;
615
616 case 'WhileStatement':
617 case 'DoWhileStatement':
618 case 'ForStatement':
619 case 'ForInStatement':
620 case 'ForOfStatement':
621 state.popLoopContext();
622 break;
623
624 case 'AssignmentPattern':
625 state.popForkContext();
626 break;
627
628 case 'LabeledStatement':
629 if (!breakableTypePattern.test(node.body.type)) {
630 state.popBreakContext();
631 }
632 break;
633
634 default:
635 break;
636 }
637
638 // Emits onCodePathSegmentStart events if updated.
639 if (!dontForward) {
640 forwardCurrentToHead(analyzer, node);
641 }
642 }
643
644 /**
645 * Updates the code path to finalize the current code path.
646 * @param {CodePathAnalyzer} analyzer The instance.
647 * @param {ASTNode} node The current AST node.
648 * @returns {void}
649 */
650 function postprocess(analyzer, node) {
651 /**
652 * Ends the code path for the current node.
653 * @returns {void}
654 */
655 function endCodePath() {
656 let codePath = analyzer.codePath;
657
658 // Mark the current path as the final node.
659 CodePath.getState(codePath).makeFinal();
660
661 // Emits onCodePathSegmentEnd event of the current segments.
662 leaveFromCurrentSegment(analyzer, node);
663
664 // Emits onCodePathEnd event of this code path.
665 analyzer.emitter.emit('onCodePathEnd', codePath, node);
666
667 codePath = analyzer.codePath = analyzer.codePath.upper;
668 }
669
670 switch (node.type) {
671 case 'Program':
672 case 'FunctionDeclaration':
673 case 'ComponentDeclaration':
674 case 'HookDeclaration':
675 case 'FunctionExpression':
676 case 'ArrowFunctionExpression':
677 case 'StaticBlock': {
678 endCodePath();
679 break;
680 }
681
682 // The `arguments.length >= 1` case is in `preprocess` function.
683 case 'CallExpression':
684 if (node.optional === true && node.arguments.length === 0) {
685 CodePath.getState(analyzer.codePath).makeOptionalRight();
686 }
687 break;
688
689 default:
690 break;
691 }
692
693 /*
694 * Special case: The right side of class field initializer is considered
695 * to be its own function, so we need to end a code path in this
696 * case.
697 *
698 * We need to check after the other checks in order to close the
699 * code paths in the correct order for code like this:
700 *
701 *
702 * class Foo {
703 * a = () => {}
704 * }
705 *
706 * In this case, The ArrowFunctionExpression code path is closed first
707 * and then we need to close the code path for the PropertyDefinition
708 * value.
709 */
710 if (isPropertyDefinitionValue(node)) {
711 endCodePath();
712 }
713 }
714
715 //------------------------------------------------------------------------------
716 // Public Interface
717 //------------------------------------------------------------------------------
718
719 /**
720 * The class to analyze code paths.
721 * This class implements the EventGenerator interface.
722 */
723 class CodePathAnalyzer {
724 /**
725 * @param {EventGenerator} eventGenerator An event generator to wrap.
726 */
727 constructor(emitters) {
728 this.emitter = {
729 emit(event, ...args) {
730 emitters[event]?.(...args);
731 },
732 };
733 this.codePath = null;
734 this.idGenerator = new IdGenerator('s');
735 this.currentNode = null;
736 this.onLooped = this.onLooped.bind(this);
737 }
738
739 /**
740 * Does the process to enter a given AST node.
741 * This updates state of analysis and calls `enterNode` of the wrapped.
742 * @param {ASTNode} node A node which is entering.
743 * @returns {void}
744 */
745 enterNode(node) {
746 this.currentNode = node;
747
748 // Updates the code path due to node's position in its parent node.
749 if (node.parent) {
750 preprocess(this, node);
751 }
752
753 /*
754 * Updates the code path.
755 * And emits onCodePathStart/onCodePathSegmentStart events.
756 */
757 processCodePathToEnter(this, node);
758
759 this.currentNode = null;
760 }
761
762 /**
763 * Does the process to leave a given AST node.
764 * This updates state of analysis and calls `leaveNode` of the wrapped.
765 * @param {ASTNode} node A node which is leaving.
766 * @returns {void}
767 */
768 leaveNode(node) {
769 this.currentNode = node;
770
771 /*
772 * Updates the code path.
773 * And emits onCodePathStart/onCodePathSegmentStart events.
774 */
775 processCodePathToExit(this, node);
776
777 // Emits the last onCodePathStart/onCodePathSegmentStart events.
778 postprocess(this, node);
779
780 this.currentNode = null;
781 }
782
783 /**
784 * This is called on a code path looped.
785 * Then this raises a looped event.
786 * @param {CodePathSegment} fromSegment A segment of prev.
787 * @param {CodePathSegment} toSegment A segment of next.
788 * @returns {void}
789 */
790 onLooped(fromSegment, toSegment) {
791 if (fromSegment.reachable && toSegment.reachable) {
792 this.emitter.emit(
793 'onCodePathSegmentLoop',
794 fromSegment,
795 toSegment,
796 this.currentNode,
797 );
798 }
799 }
800 }
801
802 module.exports = CodePathAnalyzer;