'use strict';
const assert = require('./assert');
const CodePath = require('./code-path');
const CodePathSegment = require('./code-path-segment');
const IdGenerator = require('./id-generator');
const breakableTypePattern =
/^(?:(?:Do)?While|For(?:In|Of)?|Switch)Statement$/u;
function isCaseNode(node) {
return Boolean(node.test);
}
function isPropertyDefinitionValue(node) {
const parent = node.parent;
return (
parent && parent.type === 'PropertyDefinition' && parent.value === node
);
}
function isHandledLogicalOperator(operator) {
return operator === '&&' || operator === '||' || operator === '??';
}
function isLogicalAssignmentOperator(operator) {
return operator === '&&=' || operator === '||=' || operator === '??=';
}
function getLabel(node) {
if (node.parent.type === 'LabeledStatement') {
return node.parent.label.name;
}
return null;
}
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;
}
}
function getBooleanValueIfSimpleConstant(node) {
if (node.type === 'Literal') {
return Boolean(node.value);
}
return void 0;
}
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 'ComponentDeclaration':
case 'HookDeclaration':
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;
}
}
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;
for (i = 0; i < end; ++i) {
currentSegment = currentSegments[i];
headSegment = headSegments[i];
if (currentSegment !== headSegment && currentSegment) {
if (currentSegment.reachable) {
analyzer.emitter.emit('onCodePathSegmentEnd', currentSegment, node);
}
}
}
state.currentSegments = headSegments;
for (i = 0; i < end; ++i) {
currentSegment = currentSegments[i];
headSegment = headSegments[i];
if (currentSegment !== headSegment && headSegment) {
CodePathSegment.markUsed(headSegment);
if (headSegment.reachable) {
analyzer.emitter.emit('onCodePathSegmentStart', headSegment, node);
}
}
}
}
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];
if (currentSegment.reachable) {
analyzer.emitter.emit('onCodePathSegmentEnd', currentSegment, node);
}
}
state.currentSegments = [];
}
function preprocess(analyzer, node) {
const codePath = analyzer.codePath;
const state = CodePath.getState(codePath);
const parent = node.parent;
switch (parent.type) {
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':
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':
if (parent.right === node) {
state.pushForkContext();
state.forkBypassPath();
state.forkPath();
}
break;
default:
break;
}
}
function processCodePathToEnter(analyzer, node) {
let codePath = analyzer.codePath;
let state = codePath && CodePath.getState(codePath);
const parent = node.parent;
function startCodePath(origin) {
if (codePath) {
forwardCurrentToHead(analyzer, node);
}
codePath = analyzer.codePath = new CodePath({
id: analyzer.idGenerator.next(),
origin,
upper: codePath,
onLooped: analyzer.onLooped,
});
state = CodePath.getState(codePath);
analyzer.emitter.emit('onCodePathStart', codePath, node);
}
if (isPropertyDefinitionValue(node)) {
startCodePath('class-field-initializer');
}
switch (node.type) {
case 'Program':
startCodePath('program');
break;
case 'FunctionDeclaration':
case 'ComponentDeclaration':
case 'HookDeclaration':
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),
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':
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;
}
forwardCurrentToHead(analyzer, node);
}
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':
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;
}
if (!dontForward) {
forwardCurrentToHead(analyzer, node);
}
}
function postprocess(analyzer, node) {
function endCodePath() {
let codePath = analyzer.codePath;
CodePath.getState(codePath).makeFinal();
leaveFromCurrentSegment(analyzer, node);
analyzer.emitter.emit('onCodePathEnd', codePath, node);
codePath = analyzer.codePath = analyzer.codePath.upper;
}
switch (node.type) {
case 'Program':
case 'FunctionDeclaration':
case 'ComponentDeclaration':
case 'HookDeclaration':
case 'FunctionExpression':
case 'ArrowFunctionExpression':
case 'StaticBlock': {
endCodePath();
break;
}
case 'CallExpression':
if (node.optional === true && node.arguments.length === 0) {
CodePath.getState(analyzer.codePath).makeOptionalRight();
}
break;
default:
break;
}
if (isPropertyDefinitionValue(node)) {
endCodePath();
}
}
class CodePathAnalyzer {
constructor(emitters) {
this.emitter = {
emit(event, ...args) {
emitters[event]?.(...args);
},
};
this.codePath = null;
this.idGenerator = new IdGenerator('s');
this.currentNode = null;
this.onLooped = this.onLooped.bind(this);
}
enterNode(node) {
this.currentNode = node;
if (node.parent) {
preprocess(this, node);
}
processCodePathToEnter(this, node);
this.currentNode = null;
}
leaveNode(node) {
this.currentNode = node;
processCodePathToExit(this, node);
postprocess(this, node);
this.currentNode = null;
}
onLooped(fromSegment, toSegment) {
if (fromSegment.reachable && toSegment.reachable) {
this.emitter.emit(
'onCodePathSegmentLoop',
fromSegment,
toSegment,
this.currentNode,
);
}
}
}
module.exports = CodePathAnalyzer;