/** * @fileoverview A class of the code path analyzer. * @author Toru Nagashima */ 'use strict'; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const assert = require('../../shared/assert'), { breakableTypePattern } = require('../../shared/ast-utils'), CodePath = require('./code-path'), CodePathSegment = require('./code-path-segment'), IdGenerator = require('./id-generator'), debug = require('./debug-helpers'); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** * Checks whether or not a given node is a `case` node (not `default` node). * @param {ASTNode} node A `SwitchCase` node to check. * @returns {boolean} `true` if the node is a `case` node (not `default` node). */ function isCaseNode(node) { return Boolean(node.test); } /** * Checks if a given node appears as the value of a PropertyDefinition node. * @param {ASTNode} node THe node to check. * @returns {boolean} `true` if the node is a PropertyDefinition value, * false if not. */ function isPropertyDefinitionValue(node) { const parent = node.parent; return ( parent && parent.type === 'PropertyDefinition' && parent.value === node ); } /** * Checks whether the given logical operator is taken into account for the code * path analysis. * @param {string} operator The operator found in the LogicalExpression node * @returns {boolean} `true` if the operator is "&&" or "||" or "??" */ function isHandledLogicalOperator(operator) { return operator === '&&' || operator === '||' || operator === '??'; } /** * Checks whether the given assignment operator is a logical assignment operator. * Logical assignments are taken into account for the code path analysis * because of their short-circuiting semantics. * @param {string} operator The operator found in the AssignmentExpression node * @returns {boolean} `true` if the operator is "&&=" or "||=" or "??=" */ function isLogicalAssignmentOperator(operator) { return operator === '&&=' || operator === '||=' || operator === '??='; } /** * Gets the label if the parent node of a given node is a LabeledStatement. * @param {ASTNode} node A node to get. * @returns {string|null} The label or `null`. */ function getLabel(node) { if (node.parent.type === 'LabeledStatement') { return node.parent.label.name; } return null; } /** * Checks whether or not a given logical expression node goes different path * between the `true` case and the `false` case. * @param {ASTNode} node A node to check. * @returns {boolean} `true` if the node is a test of a choice statement. */ function isForkingByTrueOrFalse(node) { const parent = node.parent; switch (parent.type) { case 'ConditionalExpression': case 'IfStatement': case 'WhileStatement': case 'DoWhileStatement': case 'ForStatement': return parent.test === node; case 'LogicalExpression': return isHandledLogicalOperator(parent.operator); case 'AssignmentExpression': return isLogicalAssignmentOperator(parent.operator); default: return false; } } /** * Gets the boolean value of a given literal node. * * This is used to detect infinity loops (e.g. `while (true) {}`). * Statements preceded by an infinity loop are unreachable if the loop didn't * have any `break` statement. * @param {ASTNode} node A node to get. * @returns {boolean|undefined} a boolean value if the node is a Literal node, * otherwise `undefined`. */ function getBooleanValueIfSimpleConstant(node) { if (node.type === 'Literal') { return Boolean(node.value); } return void 0; } /** * Checks that a given identifier node is a reference or not. * * This is used to detect the first throwable node in a `try` block. * @param {ASTNode} node An Identifier node to check. * @returns {boolean} `true` if the node is a reference. */ function isIdentifierReference(node) { const parent = node.parent; switch (parent.type) { case 'LabeledStatement': case 'BreakStatement': case 'ContinueStatement': case 'ArrayPattern': case 'RestElement': case 'ImportSpecifier': case 'ImportDefaultSpecifier': case 'ImportNamespaceSpecifier': case 'CatchClause': return false; case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': case 'ClassDeclaration': case 'ClassExpression': case 'VariableDeclarator': return parent.id !== node; case 'Property': case 'PropertyDefinition': case 'MethodDefinition': return parent.key !== node || parent.computed || parent.shorthand; case 'AssignmentPattern': return parent.key !== node; default: return true; } } /** * Updates the current segment with the head segment. * This is similar to local branches and tracking branches of git. * * To separate the current and the head is in order to not make useless segments. * * In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd" * events are fired. * @param {CodePathAnalyzer} analyzer The instance. * @param {ASTNode} node The current AST node. * @returns {void} */ function forwardCurrentToHead(analyzer, node) { const codePath = analyzer.codePath; const state = CodePath.getState(codePath); const currentSegments = state.currentSegments; const headSegments = state.headSegments; const end = Math.max(currentSegments.length, headSegments.length); let i, currentSegment, headSegment; // Fires leaving events. for (i = 0; i < end; ++i) { currentSegment = currentSegments[i]; headSegment = headSegments[i]; if (currentSegment !== headSegment && currentSegment) { const eventName = currentSegment.reachable ? 'onCodePathSegmentEnd' : 'onUnreachableCodePathSegmentEnd'; debug.dump(`${eventName} ${currentSegment.id}`); analyzer.emitter.emit(eventName, currentSegment, node); } } // Update state. state.currentSegments = headSegments; // Fires entering events. for (i = 0; i < end; ++i) { currentSegment = currentSegments[i]; headSegment = headSegments[i]; if (currentSegment !== headSegment && headSegment) { const eventName = headSegment.reachable ? 'onCodePathSegmentStart' : 'onUnreachableCodePathSegmentStart'; debug.dump(`${eventName} ${headSegment.id}`); CodePathSegment.markUsed(headSegment); analyzer.emitter.emit(eventName, headSegment, node); } } } /** * Updates the current segment with empty. * This is called at the last of functions or the program. * @param {CodePathAnalyzer} analyzer The instance. * @param {ASTNode} node The current AST node. * @returns {void} */ function leaveFromCurrentSegment(analyzer, node) { const state = CodePath.getState(analyzer.codePath); const currentSegments = state.currentSegments; for (let i = 0; i < currentSegments.length; ++i) { const currentSegment = currentSegments[i]; const eventName = currentSegment.reachable ? 'onCodePathSegmentEnd' : 'onUnreachableCodePathSegmentEnd'; debug.dump(`${eventName} ${currentSegment.id}`); analyzer.emitter.emit(eventName, currentSegment, node); } state.currentSegments = []; } /** * Updates the code path due to the position of a given node in the parent node * thereof. * * For example, if the node is `parent.consequent`, this creates a fork from the * current path. * @param {CodePathAnalyzer} analyzer The instance. * @param {ASTNode} node The current AST node. * @returns {void} */ function preprocess(analyzer, node) { const codePath = analyzer.codePath; const state = CodePath.getState(codePath); const parent = node.parent; switch (parent.type) { // The `arguments.length == 0` case is in `postprocess` function. case 'CallExpression': if ( parent.optional === true && parent.arguments.length >= 1 && parent.arguments[0] === node ) { state.makeOptionalRight(); } break; case 'MemberExpression': if (parent.optional === true && parent.property === node) { state.makeOptionalRight(); } break; case 'LogicalExpression': if (parent.right === node && isHandledLogicalOperator(parent.operator)) { state.makeLogicalRight(); } break; case 'AssignmentExpression': if ( parent.right === node && isLogicalAssignmentOperator(parent.operator) ) { state.makeLogicalRight(); } break; case 'ConditionalExpression': case 'IfStatement': /* * Fork if this node is at `consequent`/`alternate`. * `popForkContext()` exists at `IfStatement:exit` and * `ConditionalExpression:exit`. */ if (parent.consequent === node) { state.makeIfConsequent(); } else if (parent.alternate === node) { state.makeIfAlternate(); } break; case 'SwitchCase': if (parent.consequent[0] === node) { state.makeSwitchCaseBody(false, !parent.test); } break; case 'TryStatement': if (parent.handler === node) { state.makeCatchBlock(); } else if (parent.finalizer === node) { state.makeFinallyBlock(); } break; case 'WhileStatement': if (parent.test === node) { state.makeWhileTest(getBooleanValueIfSimpleConstant(node)); } else { assert(parent.body === node); state.makeWhileBody(); } break; case 'DoWhileStatement': if (parent.body === node) { state.makeDoWhileBody(); } else { assert(parent.test === node); state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node)); } break; case 'ForStatement': if (parent.test === node) { state.makeForTest(getBooleanValueIfSimpleConstant(node)); } else if (parent.update === node) { state.makeForUpdate(); } else if (parent.body === node) { state.makeForBody(); } break; case 'ForInStatement': case 'ForOfStatement': if (parent.left === node) { state.makeForInOfLeft(); } else if (parent.right === node) { state.makeForInOfRight(); } else { assert(parent.body === node); state.makeForInOfBody(); } break; case 'AssignmentPattern': /* * Fork if this node is at `right`. * `left` is executed always, so it uses the current path. * `popForkContext()` exists at `AssignmentPattern:exit`. */ if (parent.right === node) { state.pushForkContext(); state.forkBypassPath(); state.forkPath(); } break; default: break; } } /** * Updates the code path due to the type of a given node in entering. * @param {CodePathAnalyzer} analyzer The instance. * @param {ASTNode} node The current AST node. * @returns {void} */ function processCodePathToEnter(analyzer, node) { let codePath = analyzer.codePath; let state = codePath && CodePath.getState(codePath); const parent = node.parent; /** * Creates a new code path and trigger the onCodePathStart event * based on the currently selected node. * @param {string} origin The reason the code path was started. * @returns {void} */ function startCodePath(origin) { if (codePath) { // Emits onCodePathSegmentStart events if updated. forwardCurrentToHead(analyzer, node); debug.dumpState(node, state, false); } // Create the code path of this scope. codePath = analyzer.codePath = new CodePath({ id: analyzer.idGenerator.next(), origin, upper: codePath, onLooped: analyzer.onLooped, }); state = CodePath.getState(codePath); // Emits onCodePathStart events. debug.dump(`onCodePathStart ${codePath.id}`); analyzer.emitter.emit('onCodePathStart', codePath, node); } /* * Special case: The right side of class field initializer is considered * to be its own function, so we need to start a new code path in this * case. */ if (isPropertyDefinitionValue(node)) { startCodePath('class-field-initializer'); /* * Intentional fall through because `node` needs to also be * processed by the code below. For example, if we have: * * class Foo { * a = () => {} * } * * In this case, we also need start a second code path. */ } switch (node.type) { case 'Program': startCodePath('program'); break; case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': startCodePath('function'); break; case 'StaticBlock': startCodePath('class-static-block'); break; case 'ChainExpression': state.pushChainContext(); break; case 'CallExpression': if (node.optional === true) { state.makeOptionalNode(); } break; case 'MemberExpression': if (node.optional === true) { state.makeOptionalNode(); } break; case 'LogicalExpression': if (isHandledLogicalOperator(node.operator)) { state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node)); } break; case 'AssignmentExpression': if (isLogicalAssignmentOperator(node.operator)) { state.pushChoiceContext( node.operator.slice(0, -1), // removes `=` from the end isForkingByTrueOrFalse(node) ); } break; case 'ConditionalExpression': case 'IfStatement': state.pushChoiceContext('test', false); break; case 'SwitchStatement': state.pushSwitchContext(node.cases.some(isCaseNode), getLabel(node)); break; case 'TryStatement': state.pushTryContext(Boolean(node.finalizer)); break; case 'SwitchCase': /* * Fork if this node is after the 2st node in `cases`. * It's similar to `else` blocks. * The next `test` node is processed in this path. */ if (parent.discriminant !== node && parent.cases[0] !== node) { state.forkPath(); } break; case 'WhileStatement': case 'DoWhileStatement': case 'ForStatement': case 'ForInStatement': case 'ForOfStatement': state.pushLoopContext(node.type, getLabel(node)); break; case 'LabeledStatement': if (!breakableTypePattern.test(node.body.type)) { state.pushBreakContext(false, node.label.name); } break; default: break; } // Emits onCodePathSegmentStart events if updated. forwardCurrentToHead(analyzer, node); debug.dumpState(node, state, false); } /** * Updates the code path due to the type of a given node in leaving. * @param {CodePathAnalyzer} analyzer The instance. * @param {ASTNode} node The current AST node. * @returns {void} */ function processCodePathToExit(analyzer, node) { const codePath = analyzer.codePath; const state = CodePath.getState(codePath); let dontForward = false; switch (node.type) { case 'ChainExpression': state.popChainContext(); break; case 'IfStatement': case 'ConditionalExpression': state.popChoiceContext(); break; case 'LogicalExpression': if (isHandledLogicalOperator(node.operator)) { state.popChoiceContext(); } break; case 'AssignmentExpression': if (isLogicalAssignmentOperator(node.operator)) { state.popChoiceContext(); } break; case 'SwitchStatement': state.popSwitchContext(); break; case 'SwitchCase': /* * This is the same as the process at the 1st `consequent` node in * `preprocess` function. * Must do if this `consequent` is empty. */ if (node.consequent.length === 0) { state.makeSwitchCaseBody(true, !node.test); } if (state.forkContext.reachable) { dontForward = true; } break; case 'TryStatement': state.popTryContext(); break; case 'BreakStatement': forwardCurrentToHead(analyzer, node); state.makeBreak(node.label && node.label.name); dontForward = true; break; case 'ContinueStatement': forwardCurrentToHead(analyzer, node); state.makeContinue(node.label && node.label.name); dontForward = true; break; case 'ReturnStatement': forwardCurrentToHead(analyzer, node); state.makeReturn(); dontForward = true; break; case 'ThrowStatement': forwardCurrentToHead(analyzer, node); state.makeThrow(); dontForward = true; break; case 'Identifier': if (isIdentifierReference(node)) { state.makeFirstThrowablePathInTryBlock(); dontForward = true; } break; case 'CallExpression': case 'ImportExpression': case 'MemberExpression': case 'NewExpression': case 'YieldExpression': state.makeFirstThrowablePathInTryBlock(); break; case 'WhileStatement': case 'DoWhileStatement': case 'ForStatement': case 'ForInStatement': case 'ForOfStatement': state.popLoopContext(); break; case 'AssignmentPattern': state.popForkContext(); break; case 'LabeledStatement': if (!breakableTypePattern.test(node.body.type)) { state.popBreakContext(); } break; default: break; } // Emits onCodePathSegmentStart events if updated. if (!dontForward) { forwardCurrentToHead(analyzer, node); } debug.dumpState(node, state, true); } /** * Updates the code path to finalize the current code path. * @param {CodePathAnalyzer} analyzer The instance. * @param {ASTNode} node The current AST node. * @returns {void} */ function postprocess(analyzer, node) { /** * Ends the code path for the current node. * @returns {void} */ function endCodePath() { let codePath = analyzer.codePath; // Mark the current path as the final node. CodePath.getState(codePath).makeFinal(); // Emits onCodePathSegmentEnd event of the current segments. leaveFromCurrentSegment(analyzer, node); // Emits onCodePathEnd event of this code path. debug.dump(`onCodePathEnd ${codePath.id}`); analyzer.emitter.emit('onCodePathEnd', codePath, node); debug.dumpDot(codePath); codePath = analyzer.codePath = analyzer.codePath.upper; if (codePath) { debug.dumpState(node, CodePath.getState(codePath), true); } } switch (node.type) { case 'Program': case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': case 'StaticBlock': { endCodePath(); break; } // The `arguments.length >= 1` case is in `preprocess` function. case 'CallExpression': if (node.optional === true && node.arguments.length === 0) { CodePath.getState(analyzer.codePath).makeOptionalRight(); } break; default: break; } /* * Special case: The right side of class field initializer is considered * to be its own function, so we need to end a code path in this * case. * * We need to check after the other checks in order to close the * code paths in the correct order for code like this: * * * class Foo { * a = () => {} * } * * In this case, The ArrowFunctionExpression code path is closed first * and then we need to close the code path for the PropertyDefinition * value. */ if (isPropertyDefinitionValue(node)) { endCodePath(); } } //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ /** * The class to analyze code paths. * This class implements the EventGenerator interface. */ class CodePathAnalyzer { /** * @param {EventGenerator} eventGenerator An event generator to wrap. */ constructor(eventGenerator) { this.original = eventGenerator; this.emitter = eventGenerator.emitter; this.codePath = null; this.idGenerator = new IdGenerator('s'); this.currentNode = null; this.onLooped = this.onLooped.bind(this); } /** * Does the process to enter a given AST node. * This updates state of analysis and calls `enterNode` of the wrapped. * @param {ASTNode} node A node which is entering. * @returns {void} */ enterNode(node) { this.currentNode = node; // Updates the code path due to node's position in its parent node. if (node.parent) { preprocess(this, node); } /* * Updates the code path. * And emits onCodePathStart/onCodePathSegmentStart events. */ processCodePathToEnter(this, node); // Emits node events. this.original.enterNode(node); this.currentNode = null; } /** * Does the process to leave a given AST node. * This updates state of analysis and calls `leaveNode` of the wrapped. * @param {ASTNode} node A node which is leaving. * @returns {void} */ leaveNode(node) { this.currentNode = node; /* * Updates the code path. * And emits onCodePathStart/onCodePathSegmentStart events. */ processCodePathToExit(this, node); // Emits node events. this.original.leaveNode(node); // Emits the last onCodePathStart/onCodePathSegmentStart events. postprocess(this, node); this.currentNode = null; } /** * This is called on a code path looped. * Then this raises a looped event. * @param {CodePathSegment} fromSegment A segment of prev. * @param {CodePathSegment} toSegment A segment of next. * @returns {void} */ onLooped(fromSegment, toSegment) { if (fromSegment.reachable && toSegment.reachable) { debug.dump(`onCodePathSegmentLoop ${fromSegment.id} -> ${toSegment.id}`); this.emitter.emit( 'onCodePathSegmentLoop', fromSegment, toSegment, this.currentNode ); } } } module.exports = CodePathAnalyzer;