You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
760 lines
22 KiB
760 lines
22 KiB
/** |
|
* @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;
|
|
|