|
|
/** * @fileoverview A class of the code path analyzer. * @author Toru Nagashima */
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const assert = require("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 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 "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) { debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`);
if (currentSegment.reachable) { analyzer.emitter.emit( "onCodePathSegmentEnd", 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) { debug.dump(`onCodePathSegmentStart ${headSegment.id}`);
CodePathSegment.markUsed(headSegment); if (headSegment.reachable) { analyzer.emitter.emit( "onCodePathSegmentStart", 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];
debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`); if (currentSegment.reachable) { analyzer.emitter.emit( "onCodePathSegmentEnd", 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;
switch (node.type) { case "Program": case "FunctionDeclaration": case "FunctionExpression": case "ArrowFunctionExpression": 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( analyzer.idGenerator.next(), codePath, analyzer.onLooped ); state = CodePath.getState(codePath);
// Emits onCodePathStart events.
debug.dump(`onCodePathStart ${codePath.id}`); analyzer.emitter.emit("onCodePathStart", codePath, node); 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) { switch (node.type) { case "Program": case "FunctionDeclaration": case "FunctionExpression": case "ArrowFunctionExpression": { 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); } 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; } }
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/** * The class to analyze code paths. * This class implements the EventGenerator interface. */ class CodePathAnalyzer {
// eslint-disable-next-line jsdoc/require-description
/** * @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;
|