Code Path Analysis
ESLint rules have access to an API to analyze code paths.
A code path represents a control flow graph that execution can take through a program.
It is composed of code path segments that fork and join at branching constructs such as if, while, return, continue, and other control flow statements.
The following is a code path diagram for a very basic program:
if (someCondition()) {
foo();
}
bar();
Objects
A program/file contains multiple code paths: one for the global/top-level execution, plus one for each function, class field initializer, and class static block.
There are two fundamental objects in the code path analysis API: CodePath and CodePathSegment.
CodePath
A CodePath object describes one control flow graph in the program. It has the following properties:
id(string) - A string that uniquely identifies the code path. Rules can useidas a key to save additional information for each code path.origin(string) - The construct that created the code path in the program. May be"program","function","class-field-initializer", or"class-static-block".initialSegment(CodePathSegment) - The initial segment of the code path.finalSegments(CodePathSegment[]) - The final segments of the code path, including both returned and thrown segments.returnedSegments(CodePathSegment[]) - The final segments of the code path, including only returned segments.thrownSegments(CodePathSegment[]) - The final segments of the code path, including only thrown segments.upper(CodePath | null) - The code path of the containing function/class/global scope.childCodePaths(CodePath[]) - Code paths of functions/classes this code path contains.
CodePath has the following methods:
traverseSegments(optionsOrCallback, callback)- Traverses all reachable segments in this code path from the initial segment to the final segments in a depth-first order. You can optionally pass an options object withfirst(CodePathSegment) andlast(CodePathSegment) properties to limit the traversal. The callback receives aCodePathSegmentand a controller object, and is called withthisset to the currentCodePath. The controller has askip()method to skip the following segments in the current branch and abreak()method to stop traversal.
CodePathSegment
CodePathSegment is a part of a code path with no branching in it. At each branch point in the program, one CodePathSegment ends, and contains links to the next possible CodePathSegments, forming a directed graph (which, in general, can contain cycles, for example when a while loop is present). CodePathSegments also contain links to the possible preceding segments, in cases where branches of a program join (such as after an if block).
A CodePathSegment has the following properties:
id(string) - A string that uniquely identifies the code path segment. Rules can useidas a key to save additional information for each segment.nextSegments(CodePathSegment[]) - The next (reachable) segments. If forking, there are two or more. If final, this is empty.prevSegments(CodePathSegment[]) - The previous (reachable) segments. If joining, there are two or more. If initial, this is empty.allNextSegments(CodePathSegment[]) - The next segments, including both reachable and unreachable segments.allPrevSegments(CodePathSegment[]) - The previous segments, including both reachable and unreachable segments.reachable(boolean) - A flag which indicates whether or not the segment is reachable. This isfalsein segments preceded byreturn,throw,break, orcontinue.
Accessing Code Paths
To use code path analysis in a rule, you can define event handlers for code path events (detailed below) in the object exported from the create() method of a rule (the same object that contains the AST node visitors). All visitors, including code path visitors, are called in source code order during a single pass over the source code. This means multiple code paths can potentially be open at once and require manual tracking (see example “track current segment position” below).
export default {
meta: {
// ...
},
create(context) {
return {
/**
* This is called at the start of traversing a code path.
*
* @param {CodePath} codePath - The new code path.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onCodePathStart(codePath, node) {
// do something with codePath
},
/**
* This is called at the end of traversing a code path.
*
* @param {CodePath} codePath - The completed code path.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onCodePathEnd(codePath, node) {
// do something with codePath
},
/**
* This is called when a reachable code path segment is
* entered.
*
* @param {CodePathSegment} segment - The new code path
* segment.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onCodePathSegmentStart(segment, node) {
// do something with segment
},
/**
* This is called when a reachable code path segment is
* exited.
*
* @param {CodePathSegment} segment - The left code path
* segment.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onCodePathSegmentEnd(segment, node) {
// do something with segment
},
/**
* This is called when an unreachable code path segment
* is entered.
*
* @param {CodePathSegment} segment - The entered code path
* segment.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onUnreachableCodePathSegmentStart(segment, node) {
// do something with segment
},
/**
* This is called when an unreachable code path segment
* is exited.
*
* @param {CodePathSegment} segment - The exited code path
* segment.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onUnreachableCodePathSegmentEnd(segment, node) {
// do something with segment
},
/**
* This is called just before exiting a reachable segment
* (`fromSegment`), if control flow leaving the segment
* can loop back to a position earlier in the source
* (the start of `toSegment`), for example at the end of
* a loop body or at a `continue;` statement. Note that
* in some cases `fromSegment === toSegment`, such as in
* simple `do...while` loops or `for (;;)` loops with an
* empty test/update expression.
*
* This event always occurs between `fromSegment`'s
* `onCodePathSegmentStart` and `onCodePathSegmentEnd`.
* While `toSegment`'s `onCodePathSegmentStart` must already
* have been called, its `onCodePathSegmentEnd` will
* have been called if and only if
* `toSegment !== fromSegment`.
*
* @param {CodePathSegment} fromSegment - The code path
* segment the loop starts from.
* @param {CodePathSegment} toSegment - The code path
* segment the loop goes to.
* @param {ASTNode} node - The current node.
* @returns {void}
*/
onCodePathSegmentLoop(fromSegment, toSegment, node) {
// do something with segment
},
// AST node visitors...
};
},
};
Example: Simple Program
Consider the following simple, but nontrivial program:
function foo(x) {
if (x) {
setTimeout(() => {
console.log("logging x after 100 ms", x);
}, 100);
}
}
function bar(y) {
if (typeof y === "string") {
console.log(y);
} else {
console.log("not a string");
}
}
if (Math.random() < 0.5) {
foo();
} else {
bar();
}
This has 4 code paths:
-
The top-level code (
origin: "program"): -
The function
foo(origin: "function"): -
The arrow function callback defined inside
foo(origin: "function"): -
The function
bar(origin: "function"):
Its code path events are called in the following order (indentation shows nesting between a start/end pair):
onCodePathStart s1 (top level)
onCodePathSegmentStart s1_1 (before the `if`, up through its test)
onCodePathStart s2 (foo)
onCodePathSegmentStart s2_1 (before `if (x)`, up through its test)
onCodePathSegmentEnd s2_1
onCodePathSegmentStart s2_2 (`if (x)` true branch: the `setTimeout(...)` call)
onCodePathStart s3 (arrow function callback)
onCodePathSegmentStart s3_1 (the whole callback body)
onCodePathSegmentEnd s3_1
onCodePathEnd s3
onCodePathSegmentEnd s2_2
onCodePathSegmentStart s2_3 (after `if (x)`; falls off the end of `foo`)
onCodePathSegmentEnd s2_3
onCodePathEnd s2
onCodePathStart s4 (bar)
onCodePathSegmentStart s4_1 (before the `if`, up through its test)
onCodePathSegmentEnd s4_1
onCodePathSegmentStart s4_2 (`if` true branch: `console.log(y)`)
onCodePathSegmentEnd s4_2
onCodePathSegmentStart s4_3 (`else` branch: `console.log("not a string")`)
onCodePathSegmentEnd s4_3
onCodePathSegmentStart s4_4 (after the `if`/`else`, where the branches join; falls off the end of `bar`)
onCodePathSegmentEnd s4_4
onCodePathEnd s4
onCodePathSegmentEnd s1_1
onCodePathSegmentStart s1_2 (top-level `if` true branch: the `foo();` call)
onCodePathSegmentEnd s1_2
onCodePathSegmentStart s1_3 (top-level `else` branch: the `bar();` call)
onCodePathSegmentEnd s1_3
onCodePathSegmentStart s1_4 (after the top-level `if`/`else`, where the branches join)
onCodePathSegmentEnd s1_4
onCodePathEnd s1
About onCodePathSegmentLoop
This event fires whenever the traversal reaches a point where control flow loops back to a segment that was already entered earlier. It fires mainly at the end of loops.
Consider the code path of the following while loop:
while (a) {
a = foo();
}
bar();
The sequence of events for this program looks like the following (indentation shows nesting between a start/end pair):
onCodePathStart (program)
onCodePathSegmentStart (before the loop)
onCodePathSegmentEnd (before the loop)
onCodePathSegmentStart (test: `a`)
onCodePathSegmentEnd (test: `a`)
onCodePathSegmentStart (body: `a = foo();`)
onCodePathSegmentLoop (body -> test, highlighted above in red)
onCodePathSegmentEnd (body)
onCodePathSegmentStart (after the loop: `bar();`)
onCodePathSegmentEnd (after the loop)
onCodePathEnd (program)
Notice that onCodePathSegmentLoop fires before onCodePathSegmentEnd for the body segment: it represents control flow looping back to test from a point at the end of body. Because test was already fully visited in an earlier step, its onCodePathSegmentStart/onCodePathSegmentEnd pair doesn’t fire again.
The onCodePathSegmentLoop event can be useful for analyzing control flow in loops. For example, see the implementation of no-unreachable-loop.
Usage Examples
Track current segment position
To track the current code path segment position, you can define a rule like this:
export default {
meta: {
// ...
},
create(context) {
// tracks the code path we are currently in
let currentCodePath;
// a Set that tracks the segments we've traversed in the current code path
let currentSegments;
// stack to track the current Set of segments for all open paths.
const allCurrentSegments = [];
return {
onCodePathStart(codePath) {
currentCodePath = codePath;
allCurrentSegments.push(currentSegments);
currentSegments = new Set();
},
onCodePathEnd(codePath) {
currentCodePath = codePath.upper;
currentSegments = allCurrentSegments.pop();
},
onCodePathSegmentStart(segment) {
currentSegments.add(segment);
},
onCodePathSegmentEnd(segment) {
currentSegments.delete(segment);
},
onUnreachableCodePathSegmentStart(segment) {
currentSegments.add(segment);
},
onUnreachableCodePathSegmentEnd(segment) {
currentSegments.delete(segment);
},
};
},
};
In this example, the currentCodePath variable is used to access the code path that is currently being traversed and the currentSegments variable tracks the segments in that code path that have been traversed to that point. Note that currentSegments both starts and ends as an empty set, constantly being updated as the traversal progresses.
Tracking the current segment position is helpful for analyzing the code path that led to a particular node, as in the next example.
Find an unreachable node
To find an unreachable node, track the current segment position and then use a node visitor to check if any of the segments are reachable. For example, the following looks for any ExpressionStatement that is unreachable.
function areAnySegmentsReachable(segments) {
for (const segment of segments) {
if (segment.reachable) {
return true;
}
}
return false;
}
export default {
meta: {
// ...
},
create(context) {
// tracks the code path we are currently in
let currentCodePath;
// a Set that tracks the segments we've traversed in the current code path
let currentSegments;
// stack to track the current Set of segments for all open paths.
const allCurrentSegments = [];
return {
onCodePathStart(codePath) {
currentCodePath = codePath;
allCurrentSegments.push(currentSegments);
currentSegments = new Set();
},
onCodePathEnd(codePath) {
currentCodePath = codePath.upper;
currentSegments = allCurrentSegments.pop();
},
onCodePathSegmentStart(segment) {
currentSegments.add(segment);
},
onCodePathSegmentEnd(segment) {
currentSegments.delete(segment);
},
onUnreachableCodePathSegmentStart(segment) {
currentSegments.add(segment);
},
onUnreachableCodePathSegmentEnd(segment) {
currentSegments.delete(segment);
},
ExpressionStatement(node) {
// check all the code path segments that led to this node
if (!areAnySegmentsReachable(currentSegments)) {
context.report({ message: "Unreachable!", node });
}
},
};
},
};
See Also: no-unreachable, no-fallthrough, consistent-return
Check if a function is called in every path
This example checks whether or not the parameter cb is called in every path.
function getCbParameter(codePath, node, context) {
if (codePath.origin !== "function") {
return null;
}
return (
context.sourceCode
.getDeclaredVariables(node)
.find(
variable =>
variable.name === "cb" &&
variable.defs.some(def => def.type === "Parameter"),
) ?? null
);
}
function hasPathWithoutCall(codePath, segmentsWithCall) {
const visited = new Set();
const stack = [codePath.initialSegment];
while (stack.length > 0) {
const segment = stack.pop();
if (visited.has(segment) || segmentsWithCall.has(segment)) {
continue;
}
visited.add(segment);
if (codePath.finalSegments.includes(segment)) {
return true;
}
stack.push(...segment.nextSegments);
}
return false;
}
export default {
meta: {
// ...
},
create(context) {
let codePathInfo = null;
const codePathInfoStack = [];
return {
onCodePathStart(codePath, node) {
codePathInfoStack.push(codePathInfo);
codePathInfo = {
cbVariable: getCbParameter(codePath, node, context),
currentSegments: new Set(),
segmentsWithCall: new Set(),
};
},
onCodePathEnd(codePath, node) {
const { cbVariable, segmentsWithCall } = codePathInfo;
codePathInfo = codePathInfoStack.pop();
if (!cbVariable) {
return;
}
if (hasPathWithoutCall(codePath, segmentsWithCall)) {
context.report({
message: "`cb` should be called in every path.",
node,
});
}
},
onCodePathSegmentStart(segment) {
codePathInfo.currentSegments.add(segment);
},
onCodePathSegmentEnd(segment) {
codePathInfo.currentSegments.delete(segment);
},
CallExpression(node) {
const { cbVariable } = codePathInfo;
if (
cbVariable?.references.some(
reference => reference.identifier === node.callee,
)
) {
for (const segment of codePathInfo.currentSegments) {
codePathInfo.segmentsWithCall.add(segment);
}
}
},
};
},
};
See Also: constructor-super, no-this-before-super
Limitations
ESLint’s code path analysis is an approximation of the actual runtime possibilities of a JavaScript program, not an exact model.
For example, in modern JavaScript, almost anything can technically throw, including function calls, property access, and even referencing a declared identifier.
However, outside of a try block, the code path analysis assumes all of these will succeed, rather than modeling every possible throw point. See the TryStatement examples below for details on how the control flow is modelled within try constructs.
Another example is that the code path analysis treats all function calls as opaque, even when it is clear which function is being called. See the examples with function calls below.
Code Path Examples
Hello World
console.log("Hello world!");
IfStatement
if (a) {
foo();
} else {
bar();
}
IfStatement (chain)
if (a) {
foo();
} else if (b) {
bar();
} else if (c) {
baz();
}
LogicalExpression
Note that && and || are branching constructs, due to their short-circuiting behavior.
const foo = a && b;
SwitchStatement
switch (a) {
case 0:
foo();
break;
case 1:
case 2:
bar();
// fallthrough
case 3:
baz();
break;
}
SwitchStatement (has default)
switch (a) {
case 0:
foo();
break;
case 1:
case 2:
bar();
// fallthrough
case 3:
baz();
break;
default:
quux();
break;
}
TryStatement (try-catch)
try {
foo();
if (a) {
throw new Error();
}
bar();
} catch (err) {
baz(err);
}
last();
Code path analysis creates paths from the try block to the catch block at:
throwstatements.- The first throwable node (e.g. a function call) in the
tryblock. - The end of the
tryblock.
TryStatement (try-finally)
try {
foo();
bar();
} finally {
baz();
}
last();
If there is no catch block, the finally block has two current segments: the normal path and the leaving path. Note that the leaving path exists here even though the try block contains no throw or return, because function calls such as foo() are treated as potentially throwing.
If you ran the “Find an unreachable node” example above on this code, currentSegments.size would be 2 while traversing the finally block.
TryStatement (try-catch-finally)
try {
foo();
bar();
} catch (err) {
baz(err);
} finally {
quux();
}
last();
WhileStatement
while (a) {
foo();
if (b) {
continue;
}
bar();
}
DoWhileStatement
do {
foo();
bar();
} while (a);
ForStatement
for (let i = 0; i < 10; ++i) {
foo();
if (b) {
break;
}
bar();
}
ForStatement (infinite loop)
for (;;) {
foo();
}
bar();
ForInStatement
for (let key in obj) {
foo(key);
}
When there is a function
function foo(a) {
if (a) {
return;
}
bar();
}
foo(false);
This program is composed of two code paths:
-
The global code path
-
The function
foo()'s code path
Note that there is no link from the code path segment containing the call to foo() to the code path segment containing the implementation of foo(). This is because function calls are opaque for the purposes of code path analysis, even when it is unambiguous from the source code which function is being called.
YieldExpression in generator functions
function* generator(flag) {
yield 1;
if (flag) {
foo();
}
bar();
}
In generator functions yield can create three code paths:
- To the next segment.
- To the thrown path.
- To the end of the function.