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