Skip to content
Permalink
9bfb9ba527
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
1483 lines (1259 sloc) 46.5 KB
/**
* @fileoverview A class to manage state of generating a code path.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const CodePathSegment = require("./code-path-segment"),
ForkContext = require("./fork-context");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Adds given segments into the `dest` array.
* If the `others` array does not includes the given segments, adds to the `all`
* array as well.
*
* This adds only reachable and used segments.
* @param {CodePathSegment[]} dest A destination array (`returnedSegments` or `thrownSegments`).
* @param {CodePathSegment[]} others Another destination array (`returnedSegments` or `thrownSegments`).
* @param {CodePathSegment[]} all The unified destination array (`finalSegments`).
* @param {CodePathSegment[]} segments Segments to add.
* @returns {void}
*/
function addToReturnedOrThrown(dest, others, all, segments) {
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
dest.push(segment);
if (!others.includes(segment)) {
all.push(segment);
}
}
}
/**
* Gets a loop-context for a `continue` statement.
* @param {CodePathState} state A state to get.
* @param {string} label The label of a `continue` statement.
* @returns {LoopContext} A loop-context for a `continue` statement.
*/
function getContinueContext(state, label) {
if (!label) {
return state.loopContext;
}
let context = state.loopContext;
while (context) {
if (context.label === label) {
return context;
}
context = context.upper;
}
/* c8 ignore next */
return null;
}
/**
* Gets a context for a `break` statement.
* @param {CodePathState} state A state to get.
* @param {string} label The label of a `break` statement.
* @returns {LoopContext|SwitchContext} A context for a `break` statement.
*/
function getBreakContext(state, label) {
let context = state.breakContext;
while (context) {
if (label ? context.label === label : context.breakable) {
return context;
}
context = context.upper;
}
/* c8 ignore next */
return null;
}
/**
* Gets a context for a `return` statement.
* @param {CodePathState} state A state to get.
* @returns {TryContext|CodePathState} A context for a `return` statement.
*/
function getReturnContext(state) {
let context = state.tryContext;
while (context) {
if (context.hasFinalizer && context.position !== "finally") {
return context;
}
context = context.upper;
}
return state;
}
/**
* Gets a context for a `throw` statement.
* @param {CodePathState} state A state to get.
* @returns {TryContext|CodePathState} A context for a `throw` statement.
*/
function getThrowContext(state) {
let context = state.tryContext;
while (context) {
if (context.position === "try" ||
(context.hasFinalizer && context.position === "catch")
) {
return context;
}
context = context.upper;
}
return state;
}
/**
* Removes a given element from a given array.
* @param {any[]} xs An array to remove the specific element.
* @param {any} x An element to be removed.
* @returns {void}
*/
function remove(xs, x) {
xs.splice(xs.indexOf(x), 1);
}
/**
* Disconnect given segments.
*
* This is used in a process for switch statements.
* If there is the "default" chunk before other cases, the order is different
* between node's and running's.
* @param {CodePathSegment[]} prevSegments Forward segments to disconnect.
* @param {CodePathSegment[]} nextSegments Backward segments to disconnect.
* @returns {void}
*/
function removeConnection(prevSegments, nextSegments) {
for (let i = 0; i < prevSegments.length; ++i) {
const prevSegment = prevSegments[i];
const nextSegment = nextSegments[i];
remove(prevSegment.nextSegments, nextSegment);
remove(prevSegment.allNextSegments, nextSegment);
remove(nextSegment.prevSegments, prevSegment);
remove(nextSegment.allPrevSegments, prevSegment);
}
}
/**
* Creates looping path.
* @param {CodePathState} state The instance.
* @param {CodePathSegment[]} unflattenedFromSegments Segments which are source.
* @param {CodePathSegment[]} unflattenedToSegments Segments which are destination.
* @returns {void}
*/
function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) {
const fromSegments = CodePathSegment.flattenUnusedSegments(unflattenedFromSegments);
const toSegments = CodePathSegment.flattenUnusedSegments(unflattenedToSegments);
const end = Math.min(fromSegments.length, toSegments.length);
for (let i = 0; i < end; ++i) {
const fromSegment = fromSegments[i];
const toSegment = toSegments[i];
if (toSegment.reachable) {
fromSegment.nextSegments.push(toSegment);
}
if (fromSegment.reachable) {
toSegment.prevSegments.push(fromSegment);
}
fromSegment.allNextSegments.push(toSegment);
toSegment.allPrevSegments.push(fromSegment);
if (toSegment.allPrevSegments.length >= 2) {
CodePathSegment.markPrevSegmentAsLooped(toSegment, fromSegment);
}
state.notifyLooped(fromSegment, toSegment);
}
}
/**
* Finalizes segments of `test` chunk of a ForStatement.
*
* - Adds `false` paths to paths which are leaving from the loop.
* - Sets `true` paths to paths which go to the body.
* @param {LoopContext} context A loop context to modify.
* @param {ChoiceContext} choiceContext A choice context of this loop.
* @param {CodePathSegment[]} head The current head paths.
* @returns {void}
*/
function finalizeTestSegmentsOfFor(context, choiceContext, head) {
if (!choiceContext.processed) {
choiceContext.trueForkContext.add(head);
choiceContext.falseForkContext.add(head);
choiceContext.qqForkContext.add(head);
}
if (context.test !== true) {
context.brokenForkContext.addAll(choiceContext.falseForkContext);
}
context.endOfTestSegments = choiceContext.trueForkContext.makeNext(0, -1);
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* A class which manages state to analyze code paths.
*/
class CodePathState {
/**
* @param {IdGenerator} idGenerator An id generator to generate id for code
* path segments.
* @param {Function} onLooped A callback function to notify looping.
*/
constructor(idGenerator, onLooped) {
this.idGenerator = idGenerator;
this.notifyLooped = onLooped;
this.forkContext = ForkContext.newRoot(idGenerator);
this.choiceContext = null;
this.switchContext = null;
this.tryContext = null;
this.loopContext = null;
this.breakContext = null;
this.chainContext = null;
this.currentSegments = [];
this.initialSegment = this.forkContext.head[0];
// returnedSegments and thrownSegments push elements into finalSegments also.
const final = this.finalSegments = [];
const returned = this.returnedForkContext = [];
const thrown = this.thrownForkContext = [];
returned.add = addToReturnedOrThrown.bind(null, returned, thrown, final);
thrown.add = addToReturnedOrThrown.bind(null, thrown, returned, final);
}
/**
* The head segments.
* @type {CodePathSegment[]}
*/
get headSegments() {
return this.forkContext.head;
}
/**
* The parent forking context.
* This is used for the root of new forks.
* @type {ForkContext}
*/
get parentForkContext() {
const current = this.forkContext;
return current && current.upper;
}
/**
* Creates and stacks new forking context.
* @param {boolean} forkLeavingPath A flag which shows being in a
* "finally" block.
* @returns {ForkContext} The created context.
*/
pushForkContext(forkLeavingPath) {
this.forkContext = ForkContext.newEmpty(
this.forkContext,
forkLeavingPath
);
return this.forkContext;
}
/**
* Pops and merges the last forking context.
* @returns {ForkContext} The last context.
*/
popForkContext() {
const lastContext = this.forkContext;
this.forkContext = lastContext.upper;
this.forkContext.replaceHead(lastContext.makeNext(0, -1));
return lastContext;
}
/**
* Creates a new path.
* @returns {void}
*/
forkPath() {
this.forkContext.add(this.parentForkContext.makeNext(-1, -1));
}
/**
* Creates a bypass path.
* This is used for such as IfStatement which does not have "else" chunk.
* @returns {void}
*/
forkBypassPath() {
this.forkContext.add(this.parentForkContext.head);
}
//--------------------------------------------------------------------------
// ConditionalExpression, LogicalExpression, IfStatement
//--------------------------------------------------------------------------
/**
* Creates a context for ConditionalExpression, LogicalExpression, AssignmentExpression (logical assignments only),
* IfStatement, WhileStatement, DoWhileStatement, or ForStatement.
*
* LogicalExpressions have cases that it goes different paths between the
* `true` case and the `false` case.
*
* For Example:
*
* if (a || b) {
* foo();
* } else {
* bar();
* }
*
* In this case, `b` is evaluated always in the code path of the `else`
* block, but it's not so in the code path of the `if` block.
* So there are 3 paths.
*
* a -> foo();
* a -> b -> foo();
* a -> b -> bar();
* @param {string} kind A kind string.
* If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`.
* If it's IfStatement's or ConditionalExpression's, this is `"test"`.
* Otherwise, this is `"loop"`.
* @param {boolean} isForkingAsResult A flag that shows that goes different
* paths between `true` and `false`.
* @returns {void}
*/
pushChoiceContext(kind, isForkingAsResult) {
this.choiceContext = {
upper: this.choiceContext,
kind,
isForkingAsResult,
trueForkContext: ForkContext.newEmpty(this.forkContext),
falseForkContext: ForkContext.newEmpty(this.forkContext),
qqForkContext: ForkContext.newEmpty(this.forkContext),
processed: false
};
}
/**
* Pops the last choice context and finalizes it.
* @throws {Error} (Unreachable.)
* @returns {ChoiceContext} The popped context.
*/
popChoiceContext() {
const context = this.choiceContext;
this.choiceContext = context.upper;
const forkContext = this.forkContext;
const headSegments = forkContext.head;
switch (context.kind) {
case "&&":
case "||":
case "??":
/*
* If any result were not transferred from child contexts,
* this sets the head segments to both cases.
* The head segments are the path of the right-hand operand.
*/
if (!context.processed) {
context.trueForkContext.add(headSegments);
context.falseForkContext.add(headSegments);
context.qqForkContext.add(headSegments);
}
/*
* Transfers results to upper context if this context is in
* test chunk.
*/
if (context.isForkingAsResult) {
const parentContext = this.choiceContext;
parentContext.trueForkContext.addAll(context.trueForkContext);
parentContext.falseForkContext.addAll(context.falseForkContext);
parentContext.qqForkContext.addAll(context.qqForkContext);
parentContext.processed = true;
return context;
}
break;
case "test":
if (!context.processed) {
/*
* The head segments are the path of the `if` block here.
* Updates the `true` path with the end of the `if` block.
*/
context.trueForkContext.clear();
context.trueForkContext.add(headSegments);
} else {
/*
* The head segments are the path of the `else` block here.
* Updates the `false` path with the end of the `else`
* block.
*/
context.falseForkContext.clear();
context.falseForkContext.add(headSegments);
}
break;
case "loop":
/*
* Loops are addressed in popLoopContext().
* This is called from popLoopContext().
*/
return context;
/* c8 ignore next */
default:
throw new Error("unreachable");
}
// Merges all paths.
const prevForkContext = context.trueForkContext;
prevForkContext.addAll(context.falseForkContext);
forkContext.replaceHead(prevForkContext.makeNext(0, -1));
return context;
}
/**
* Makes a code path segment of the right-hand operand of a logical
* expression.
* @throws {Error} (Unreachable.)
* @returns {void}
*/
makeLogicalRight() {
const context = this.choiceContext;
const forkContext = this.forkContext;
if (context.processed) {
/*
* This got segments already from the child choice context.
* Creates the next path from own true/false fork context.
*/
let prevForkContext;
switch (context.kind) {
case "&&": // if true then go to the right-hand side.
prevForkContext = context.trueForkContext;
break;
case "||": // if false then go to the right-hand side.
prevForkContext = context.falseForkContext;
break;
case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's qqForkContext.
prevForkContext = context.qqForkContext;
break;
default:
throw new Error("unreachable");
}
forkContext.replaceHead(prevForkContext.makeNext(0, -1));
prevForkContext.clear();
context.processed = false;
} else {
/*
* This did not get segments from the child choice context.
* So addresses the head segments.
* The head segments are the path of the left-hand operand.
*/
switch (context.kind) {
case "&&": // the false path can short-circuit.
context.falseForkContext.add(forkContext.head);
break;
case "||": // the true path can short-circuit.
context.trueForkContext.add(forkContext.head);
break;
case "??": // both can short-circuit.
context.trueForkContext.add(forkContext.head);
context.falseForkContext.add(forkContext.head);
break;
default:
throw new Error("unreachable");
}
forkContext.replaceHead(forkContext.makeNext(-1, -1));
}
}
/**
* Makes a code path segment of the `if` block.
* @returns {void}
*/
makeIfConsequent() {
const context = this.choiceContext;
const forkContext = this.forkContext;
/*
* If any result were not transferred from child contexts,
* this sets the head segments to both cases.
* The head segments are the path of the test expression.
*/
if (!context.processed) {
context.trueForkContext.add(forkContext.head);
context.falseForkContext.add(forkContext.head);
context.qqForkContext.add(forkContext.head);
}
context.processed = false;
// Creates new path from the `true` case.
forkContext.replaceHead(
context.trueForkContext.makeNext(0, -1)
);
}
/**
* Makes a code path segment of the `else` block.
* @returns {void}
*/
makeIfAlternate() {
const context = this.choiceContext;
const forkContext = this.forkContext;
/*
* The head segments are the path of the `if` block.
* Updates the `true` path with the end of the `if` block.
*/
context.trueForkContext.clear();
context.trueForkContext.add(forkContext.head);
context.processed = true;
// Creates new path from the `false` case.
forkContext.replaceHead(
context.falseForkContext.makeNext(0, -1)
);
}
//--------------------------------------------------------------------------
// ChainExpression
//--------------------------------------------------------------------------
/**
* Push a new `ChainExpression` context to the stack.
* This method is called on entering to each `ChainExpression` node.
* This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node.
* @returns {void}
*/
pushChainContext() {
this.chainContext = {
upper: this.chainContext,
countChoiceContexts: 0
};
}
/**
* Pop a `ChainExpression` context from the stack.
* This method is called on exiting from each `ChainExpression` node.
* This merges all forks of the last optional chaining.
* @returns {void}
*/
popChainContext() {
const context = this.chainContext;
this.chainContext = context.upper;
// pop all choice contexts of this.
for (let i = context.countChoiceContexts; i > 0; --i) {
this.popChoiceContext();
}
}
/**
* Create a choice context for optional access.
* This method is called on entering to each `(Call|Member)Expression[optional=true]` node.
* This creates a choice context as similar to `LogicalExpression[operator="??"]` node.
* @returns {void}
*/
makeOptionalNode() {
if (this.chainContext) {
this.chainContext.countChoiceContexts += 1;
this.pushChoiceContext("??", false);
}
}
/**
* Create a fork.
* This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node.
* @returns {void}
*/
makeOptionalRight() {
if (this.chainContext) {
this.makeLogicalRight();
}
}
//--------------------------------------------------------------------------
// SwitchStatement
//--------------------------------------------------------------------------
/**
* Creates a context object of SwitchStatement and stacks it.
* @param {boolean} hasCase `true` if the switch statement has one or more
* case parts.
* @param {string|null} label The label text.
* @returns {void}
*/
pushSwitchContext(hasCase, label) {
this.switchContext = {
upper: this.switchContext,
hasCase,
defaultSegments: null,
defaultBodySegments: null,
foundDefault: false,
lastIsDefault: false,
countForks: 0
};
this.pushBreakContext(true, label);
}
/**
* Pops the last context of SwitchStatement and finalizes it.
*
* - Disposes all forking stack for `case` and `default`.
* - Creates the next code path segment from `context.brokenForkContext`.
* - If the last `SwitchCase` node is not a `default` part, creates a path
* to the `default` body.
* @returns {void}
*/
popSwitchContext() {
const context = this.switchContext;
this.switchContext = context.upper;
const forkContext = this.forkContext;
const brokenForkContext = this.popBreakContext().brokenForkContext;
if (context.countForks === 0) {
/*
* When there is only one `default` chunk and there is one or more
* `break` statements, even if forks are nothing, it needs to merge
* those.
*/
if (!brokenForkContext.empty) {
brokenForkContext.add(forkContext.makeNext(-1, -1));
forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
}
return;
}
const lastSegments = forkContext.head;
this.forkBypassPath();
const lastCaseSegments = forkContext.head;
/*
* `brokenForkContext` is used to make the next segment.
* It must add the last segment into `brokenForkContext`.
*/
brokenForkContext.add(lastSegments);
/*
* A path which is failed in all case test should be connected to path
* of `default` chunk.
*/
if (!context.lastIsDefault) {
if (context.defaultBodySegments) {
/*
* Remove a link from `default` label to its chunk.
* It's false route.
*/
removeConnection(context.defaultSegments, context.defaultBodySegments);
makeLooped(this, lastCaseSegments, context.defaultBodySegments);
} else {
/*
* It handles the last case body as broken if `default` chunk
* does not exist.
*/
brokenForkContext.add(lastCaseSegments);
}
}
// Pops the segment context stack until the entry segment.
for (let i = 0; i < context.countForks; ++i) {
this.forkContext = this.forkContext.upper;
}
/*
* Creates a path from all brokenForkContext paths.
* This is a path after switch statement.
*/
this.forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
}
/**
* Makes a code path segment for a `SwitchCase` node.
* @param {boolean} isEmpty `true` if the body is empty.
* @param {boolean} isDefault `true` if the body is the default case.
* @returns {void}
*/
makeSwitchCaseBody(isEmpty, isDefault) {
const context = this.switchContext;
if (!context.hasCase) {
return;
}
/*
* Merge forks.
* The parent fork context has two segments.
* Those are from the current case and the body of the previous case.
*/
const parentForkContext = this.forkContext;
const forkContext = this.pushForkContext();
forkContext.add(parentForkContext.makeNext(0, -1));
/*
* Save `default` chunk info.
* If the `default` label is not at the last, we must make a path from
* the last `case` to the `default` chunk.
*/
if (isDefault) {
context.defaultSegments = parentForkContext.head;
if (isEmpty) {
context.foundDefault = true;
} else {
context.defaultBodySegments = forkContext.head;
}
} else {
if (!isEmpty && context.foundDefault) {
context.foundDefault = false;
context.defaultBodySegments = forkContext.head;
}
}
context.lastIsDefault = isDefault;
context.countForks += 1;
}
//--------------------------------------------------------------------------
// TryStatement
//--------------------------------------------------------------------------
/**
* Creates a context object of TryStatement and stacks it.
* @param {boolean} hasFinalizer `true` if the try statement has a
* `finally` block.
* @returns {void}
*/
pushTryContext(hasFinalizer) {
this.tryContext = {
upper: this.tryContext,
position: "try",
hasFinalizer,
returnedForkContext: hasFinalizer
? ForkContext.newEmpty(this.forkContext)
: null,
thrownForkContext: ForkContext.newEmpty(this.forkContext),
lastOfTryIsReachable: false,
lastOfCatchIsReachable: false
};
}
/**
* Pops the last context of TryStatement and finalizes it.
* @returns {void}
*/
popTryContext() {
const context = this.tryContext;
this.tryContext = context.upper;
if (context.position === "catch") {
// Merges two paths from the `try` block and `catch` block merely.
this.popForkContext();
return;
}
/*
* The following process is executed only when there is the `finally`
* block.
*/
const returned = context.returnedForkContext;
const thrown = context.thrownForkContext;
if (returned.empty && thrown.empty) {
return;
}
// Separate head to normal paths and leaving paths.
const headSegments = this.forkContext.head;
this.forkContext = this.forkContext.upper;
const normalSegments = headSegments.slice(0, headSegments.length / 2 | 0);
const leavingSegments = headSegments.slice(headSegments.length / 2 | 0);
// Forwards the leaving path to upper contexts.
if (!returned.empty) {
getReturnContext(this).returnedForkContext.add(leavingSegments);
}
if (!thrown.empty) {
getThrowContext(this).thrownForkContext.add(leavingSegments);
}
// Sets the normal path as the next.
this.forkContext.replaceHead(normalSegments);
/*
* If both paths of the `try` block and the `catch` block are
* unreachable, the next path becomes unreachable as well.
*/
if (!context.lastOfTryIsReachable && !context.lastOfCatchIsReachable) {
this.forkContext.makeUnreachable();
}
}
/**
* Makes a code path segment for a `catch` block.
* @returns {void}
*/
makeCatchBlock() {
const context = this.tryContext;
const forkContext = this.forkContext;
const thrown = context.thrownForkContext;
// Update state.
context.position = "catch";
context.thrownForkContext = ForkContext.newEmpty(forkContext);
context.lastOfTryIsReachable = forkContext.reachable;
// Merge thrown paths.
thrown.add(forkContext.head);
const thrownSegments = thrown.makeNext(0, -1);
// Fork to a bypass and the merged thrown path.
this.pushForkContext();
this.forkBypassPath();
this.forkContext.add(thrownSegments);
}
/**
* Makes a code path segment for a `finally` block.
*
* In the `finally` block, parallel paths are created. The parallel paths
* are used as leaving-paths. The leaving-paths are paths from `return`
* statements and `throw` statements in a `try` block or a `catch` block.
* @returns {void}
*/
makeFinallyBlock() {
const context = this.tryContext;
let forkContext = this.forkContext;
const returned = context.returnedForkContext;
const thrown = context.thrownForkContext;
const headOfLeavingSegments = forkContext.head;
// Update state.
if (context.position === "catch") {
// Merges two paths from the `try` block and `catch` block.
this.popForkContext();
forkContext = this.forkContext;
context.lastOfCatchIsReachable = forkContext.reachable;
} else {
context.lastOfTryIsReachable = forkContext.reachable;
}
context.position = "finally";
if (returned.empty && thrown.empty) {
// This path does not leave.
return;
}
/*
* Create a parallel segment from merging returned and thrown.
* This segment will leave at the end of this finally block.
*/
const segments = forkContext.makeNext(-1, -1);
for (let i = 0; i < forkContext.count; ++i) {
const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]];
for (let j = 0; j < returned.segmentsList.length; ++j) {
prevSegsOfLeavingSegment.push(returned.segmentsList[j][i]);
}
for (let j = 0; j < thrown.segmentsList.length; ++j) {
prevSegsOfLeavingSegment.push(thrown.segmentsList[j][i]);
}
segments.push(
CodePathSegment.newNext(
this.idGenerator.next(),
prevSegsOfLeavingSegment
)
);
}
this.pushForkContext(true);
this.forkContext.add(segments);
}
/**
* Makes a code path segment from the first throwable node to the `catch`
* block or the `finally` block.
* @returns {void}
*/
makeFirstThrowablePathInTryBlock() {
const forkContext = this.forkContext;
if (!forkContext.reachable) {
return;
}
const context = getThrowContext(this);
if (context === this ||
context.position !== "try" ||
!context.thrownForkContext.empty
) {
return;
}
context.thrownForkContext.add(forkContext.head);
forkContext.replaceHead(forkContext.makeNext(-1, -1));
}
//--------------------------------------------------------------------------
// Loop Statements
//--------------------------------------------------------------------------
/**
* Creates a context object of a loop statement and stacks it.
* @param {string} type The type of the node which was triggered. One of
* `WhileStatement`, `DoWhileStatement`, `ForStatement`, `ForInStatement`,
* and `ForStatement`.
* @param {string|null} label A label of the node which was triggered.
* @throws {Error} (Unreachable - unknown type.)
* @returns {void}
*/
pushLoopContext(type, label) {
const forkContext = this.forkContext;
const breakContext = this.pushBreakContext(true, label);
switch (type) {
case "WhileStatement":
this.pushChoiceContext("loop", false);
this.loopContext = {
upper: this.loopContext,
type,
label,
test: void 0,
continueDestSegments: null,
brokenForkContext: breakContext.brokenForkContext
};
break;
case "DoWhileStatement":
this.pushChoiceContext("loop", false);
this.loopContext = {
upper: this.loopContext,
type,
label,
test: void 0,
entrySegments: null,
continueForkContext: ForkContext.newEmpty(forkContext),
brokenForkContext: breakContext.brokenForkContext
};
break;
case "ForStatement":
this.pushChoiceContext("loop", false);
this.loopContext = {
upper: this.loopContext,
type,
label,
test: void 0,
endOfInitSegments: null,
testSegments: null,
endOfTestSegments: null,
updateSegments: null,
endOfUpdateSegments: null,
continueDestSegments: null,
brokenForkContext: breakContext.brokenForkContext
};
break;
case "ForInStatement":
case "ForOfStatement":
this.loopContext = {
upper: this.loopContext,
type,
label,
prevSegments: null,
leftSegments: null,
endOfLeftSegments: null,
continueDestSegments: null,
brokenForkContext: breakContext.brokenForkContext
};
break;
/* c8 ignore next */
default:
throw new Error(`unknown type: "${type}"`);
}
}
/**
* Pops the last context of a loop statement and finalizes it.
* @throws {Error} (Unreachable - unknown type.)
* @returns {void}
*/
popLoopContext() {
const context = this.loopContext;
this.loopContext = context.upper;
const forkContext = this.forkContext;
const brokenForkContext = this.popBreakContext().brokenForkContext;
// Creates a looped path.
switch (context.type) {
case "WhileStatement":
case "ForStatement":
this.popChoiceContext();
makeLooped(
this,
forkContext.head,
context.continueDestSegments
);
break;
case "DoWhileStatement": {
const choiceContext = this.popChoiceContext();
if (!choiceContext.processed) {
choiceContext.trueForkContext.add(forkContext.head);
choiceContext.falseForkContext.add(forkContext.head);
}
if (context.test !== true) {
brokenForkContext.addAll(choiceContext.falseForkContext);
}
// `true` paths go to looping.
const segmentsList = choiceContext.trueForkContext.segmentsList;
for (let i = 0; i < segmentsList.length; ++i) {
makeLooped(
this,
segmentsList[i],
context.entrySegments
);
}
break;
}
case "ForInStatement":
case "ForOfStatement":
brokenForkContext.add(forkContext.head);
makeLooped(
this,
forkContext.head,
context.leftSegments
);
break;
/* c8 ignore next */
default:
throw new Error("unreachable");
}
// Go next.
if (brokenForkContext.empty) {
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
} else {
forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
}
}
/**
* Makes a code path segment for the test part of a WhileStatement.
* @param {boolean|undefined} test The test value (only when constant).
* @returns {void}
*/
makeWhileTest(test) {
const context = this.loopContext;
const forkContext = this.forkContext;
const testSegments = forkContext.makeNext(0, -1);
// Update state.
context.test = test;
context.continueDestSegments = testSegments;
forkContext.replaceHead(testSegments);
}
/**
* Makes a code path segment for the body part of a WhileStatement.
* @returns {void}
*/
makeWhileBody() {
const context = this.loopContext;
const choiceContext = this.choiceContext;
const forkContext = this.forkContext;
if (!choiceContext.processed) {
choiceContext.trueForkContext.add(forkContext.head);
choiceContext.falseForkContext.add(forkContext.head);
}
// Update state.
if (context.test !== true) {
context.brokenForkContext.addAll(choiceContext.falseForkContext);
}
forkContext.replaceHead(choiceContext.trueForkContext.makeNext(0, -1));
}
/**
* Makes a code path segment for the body part of a DoWhileStatement.
* @returns {void}
*/
makeDoWhileBody() {
const context = this.loopContext;
const forkContext = this.forkContext;
const bodySegments = forkContext.makeNext(-1, -1);
// Update state.
context.entrySegments = bodySegments;
forkContext.replaceHead(bodySegments);
}
/**
* Makes a code path segment for the test part of a DoWhileStatement.
* @param {boolean|undefined} test The test value (only when constant).
* @returns {void}
*/
makeDoWhileTest(test) {
const context = this.loopContext;
const forkContext = this.forkContext;
context.test = test;
// Creates paths of `continue` statements.
if (!context.continueForkContext.empty) {
context.continueForkContext.add(forkContext.head);
const testSegments = context.continueForkContext.makeNext(0, -1);
forkContext.replaceHead(testSegments);
}
}
/**
* Makes a code path segment for the test part of a ForStatement.
* @param {boolean|undefined} test The test value (only when constant).
* @returns {void}
*/
makeForTest(test) {
const context = this.loopContext;
const forkContext = this.forkContext;
const endOfInitSegments = forkContext.head;
const testSegments = forkContext.makeNext(-1, -1);
// Update state.
context.test = test;
context.endOfInitSegments = endOfInitSegments;
context.continueDestSegments = context.testSegments = testSegments;
forkContext.replaceHead(testSegments);
}
/**
* Makes a code path segment for the update part of a ForStatement.
* @returns {void}
*/
makeForUpdate() {
const context = this.loopContext;
const choiceContext = this.choiceContext;
const forkContext = this.forkContext;
// Make the next paths of the test.
if (context.testSegments) {
finalizeTestSegmentsOfFor(
context,
choiceContext,
forkContext.head
);
} else {
context.endOfInitSegments = forkContext.head;
}
// Update state.
const updateSegments = forkContext.makeDisconnected(-1, -1);
context.continueDestSegments = context.updateSegments = updateSegments;
forkContext.replaceHead(updateSegments);
}
/**
* Makes a code path segment for the body part of a ForStatement.
* @returns {void}
*/
makeForBody() {
const context = this.loopContext;
const choiceContext = this.choiceContext;
const forkContext = this.forkContext;
// Update state.
if (context.updateSegments) {
context.endOfUpdateSegments = forkContext.head;
// `update` -> `test`
if (context.testSegments) {
makeLooped(
this,
context.endOfUpdateSegments,
context.testSegments
);
}
} else if (context.testSegments) {
finalizeTestSegmentsOfFor(
context,
choiceContext,
forkContext.head
);
} else {
context.endOfInitSegments = forkContext.head;
}
let bodySegments = context.endOfTestSegments;
if (!bodySegments) {
/*
* If there is not the `test` part, the `body` path comes from the
* `init` part and the `update` part.
*/
const prevForkContext = ForkContext.newEmpty(forkContext);
prevForkContext.add(context.endOfInitSegments);
if (context.endOfUpdateSegments) {
prevForkContext.add(context.endOfUpdateSegments);
}
bodySegments = prevForkContext.makeNext(0, -1);
}
context.continueDestSegments = context.continueDestSegments || bodySegments;
forkContext.replaceHead(bodySegments);
}
/**
* Makes a code path segment for the left part of a ForInStatement and a
* ForOfStatement.
* @returns {void}
*/
makeForInOfLeft() {
const context = this.loopContext;
const forkContext = this.forkContext;
const leftSegments = forkContext.makeDisconnected(-1, -1);
// Update state.
context.prevSegments = forkContext.head;
context.leftSegments = context.continueDestSegments = leftSegments;
forkContext.replaceHead(leftSegments);
}
/**
* Makes a code path segment for the right part of a ForInStatement and a
* ForOfStatement.
* @returns {void}
*/
makeForInOfRight() {
const context = this.loopContext;
const forkContext = this.forkContext;
const temp = ForkContext.newEmpty(forkContext);
temp.add(context.prevSegments);
const rightSegments = temp.makeNext(-1, -1);
// Update state.
context.endOfLeftSegments = forkContext.head;
forkContext.replaceHead(rightSegments);
}
/**
* Makes a code path segment for the body part of a ForInStatement and a
* ForOfStatement.
* @returns {void}
*/
makeForInOfBody() {
const context = this.loopContext;
const forkContext = this.forkContext;
const temp = ForkContext.newEmpty(forkContext);
temp.add(context.endOfLeftSegments);
const bodySegments = temp.makeNext(-1, -1);
// Make a path: `right` -> `left`.
makeLooped(this, forkContext.head, context.leftSegments);
// Update state.
context.brokenForkContext.add(forkContext.head);
forkContext.replaceHead(bodySegments);
}
//--------------------------------------------------------------------------
// Control Statements
//--------------------------------------------------------------------------
/**
* Creates new context for BreakStatement.
* @param {boolean} breakable The flag to indicate it can break by
* an unlabeled BreakStatement.
* @param {string|null} label The label of this context.
* @returns {Object} The new context.
*/
pushBreakContext(breakable, label) {
this.breakContext = {
upper: this.breakContext,
breakable,
label,
brokenForkContext: ForkContext.newEmpty(this.forkContext)
};
return this.breakContext;
}
/**
* Removes the top item of the break context stack.
* @returns {Object} The removed context.
*/
popBreakContext() {
const context = this.breakContext;
const forkContext = this.forkContext;
this.breakContext = context.upper;
// Process this context here for other than switches and loops.
if (!context.breakable) {
const brokenForkContext = context.brokenForkContext;
if (!brokenForkContext.empty) {
brokenForkContext.add(forkContext.head);
forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
}
}
return context;
}
/**
* Makes a path for a `break` statement.
*
* It registers the head segment to a context of `break`.
* It makes new unreachable segment, then it set the head with the segment.
* @param {string} label A label of the break statement.
* @returns {void}
*/
makeBreak(label) {
const forkContext = this.forkContext;
if (!forkContext.reachable) {
return;
}
const context = getBreakContext(this, label);
if (context) {
context.brokenForkContext.add(forkContext.head);
}
/* c8 ignore next */
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
}
/**
* Makes a path for a `continue` statement.
*
* It makes a looping path.
* It makes new unreachable segment, then it set the head with the segment.
* @param {string} label A label of the continue statement.
* @returns {void}
*/
makeContinue(label) {
const forkContext = this.forkContext;
if (!forkContext.reachable) {
return;
}
const context = getContinueContext(this, label);
if (context) {
if (context.continueDestSegments) {
makeLooped(this, forkContext.head, context.continueDestSegments);
// If the context is a for-in/of loop, this effects a break also.
if (context.type === "ForInStatement" ||
context.type === "ForOfStatement"
) {
context.brokenForkContext.add(forkContext.head);
}
} else {
context.continueForkContext.add(forkContext.head);
}
}
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
}
/**
* Makes a path for a `return` statement.
*
* It registers the head segment to a context of `return`.
* It makes new unreachable segment, then it set the head with the segment.
* @returns {void}
*/
makeReturn() {
const forkContext = this.forkContext;
if (forkContext.reachable) {
getReturnContext(this).returnedForkContext.add(forkContext.head);
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
}
}
/**
* Makes a path for a `throw` statement.
*
* It registers the head segment to a context of `throw`.
* It makes new unreachable segment, then it set the head with the segment.
* @returns {void}
*/
makeThrow() {
const forkContext = this.forkContext;
if (forkContext.reachable) {
getThrowContext(this).thrownForkContext.add(forkContext.head);
forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
}
}
/**
* Makes the final path.
* @returns {void}
*/
makeFinal() {
const segments = this.currentSegments;
if (segments.length > 0 && segments[0].reachable) {
this.returnedForkContext.add(segments);
}
}
}
module.exports = CodePathState;