Code Path Analysis Details
ESLint's rules can use code paths. The code path is execution routes of programs. It forks/joins at such as if
statements.
if (a && b) { foo(); } bar();
Objects
Program is expressed with several code paths. A code path is expressed with objects of two kinds: CodePath
and CodePathSegment
.
CodePath
CodePath
expresses whole of one code path. This object exists for each function and the global. This has references of both the initial segment and the final segments of a code path.
CodePath
has the following properties:
-
id
(string
) - A unique string. Respective rules can useid
to save additional information for each code path. -
origin
(string
) - The reason that the code path was started. May be"program"
,"function"
, or"class-field-initializer"
. -
initialSegment
(CodePathSegment
) - The initial segment of this code path. -
finalSegments
(CodePathSegment[]
) - The final segments which includes both returned and thrown. -
returnedSegments
(CodePathSegment[]
) - The final segments which includes only returned. -
thrownSegments
(CodePathSegment[]
) - The final segments which includes only thrown. -
currentSegments
(CodePathSegment[]
) - Segments of the current position. -
upper
(CodePath|null
) - The code path of the upper function/global scope. -
childCodePaths
(CodePath[]
) - Code paths of functions this code path contains.
CodePathSegment
CodePathSegment
is a part of a code path. A code path is expressed with plural CodePathSegment
objects, it's similar to doubly linked list. Difference from doubly linked list is what there are forking and merging (the next/prev are plural).
CodePathSegment
has the following properties:
-
id
(string
) - A unique string. Respective rules can useid
to save additional information for each segment. -
nextSegments
(CodePathSegment[]
) - The next segments. If forking, there are two or more. If final, there is nothing. -
prevSegments
(CodePathSegment[]
) - The previous segments. If merging, there are two or more. If initial, there is nothing. -
reachable
(boolean
) - A flag which shows whether or not it's reachable. This becomesfalse
when preceded byreturn
,throw
,break
, orcontinue
.
Events
There are five events related to code paths, and you can define event handlers in rules.
module.exports = function(context) { return { /** * This is called at the start of analyzing a code path. * In this time, the code path object has only the initial segment. * * @param {CodePath} codePath - The new code path. * @param {ASTNode} node - The current node. * @returns {void} */ "onCodePathStart": function(codePath, node) { // do something with codePath }, /** * This is called at the end of analyzing a code path. * In this time, the code path object is complete. * * @param {CodePath} codePath - The completed code path. * @param {ASTNode} node - The current node. * @returns {void} */ "onCodePathEnd": function(codePath, node) { // do something with codePath }, /** * This is called when a code path segment was created. * It meant the code path is forked or merged. * In this time, the segment has the previous segments and has been * judged reachable or not. * * @param {CodePathSegment} segment - The new code path segment. * @param {ASTNode} node - The current node. * @returns {void} */ "onCodePathSegmentStart": function(segment, node) { // do something with segment }, /** * This is called when a code path segment was leaved. * In this time, the segment does not have the next segments yet. * * @param {CodePathSegment} segment - The leaved code path segment. * @param {ASTNode} node - The current node. * @returns {void} */ "onCodePathSegmentEnd": function(segment, node) { // do something with segment }, /** * This is called when a code path segment was looped. * Usually segments have each previous segments when created, * but when looped, a segment is added as a new previous segment into a * existing segment. * * @param {CodePathSegment} fromSegment - A code path segment of source. * @param {CodePathSegment} toSegment - A code path segment of destination. * @param {ASTNode} node - The current node. * @returns {void} */ "onCodePathSegmentLoop": function(fromSegment, toSegment, node) { // do something with segment } }; };
About onCodePathSegmentLoop
This event is always fired when the next segment has existed already. That timing is the end of loops mainly.
For Example 1:
while (a) { a = foo(); } bar();
-
First, the analysis advances to the end of loop.
-
Second, it creates the looping path. At this time, the next segment has existed already, so the
onCodePathSegmentStart
event is not fired. It firesonCodePathSegmentLoop
instead. -
Last, it advances to the end.
For example 2:
for (let i = 0; i < 10; ++i) { foo(i); } bar();
-
for
statements are more complex. First, the analysis advances toForStatement.update
. Theupdate
segment is hovered at first. -
Second, it advances to
ForStatement.body
. Of course thebody
segment is preceded by thetest
segment. It keeps theupdate
segment hovering. -
Third, it creates the looping path from
body
segment toupdate
segment. At this time, the next segment has existed already, so theonCodePathSegmentStart
event is not fired. It firesonCodePathSegmentLoop
instead. -
Fourth, also it creates the looping path from
update
segment totest
segment. At this time, the next segment has existed already, so theonCodePathSegmentStart
event is not fired. It firesonCodePathSegmentLoop
instead. -
Last, it advances to the end.
Usage Examples
To check whether or not this is reachable
function isReachable(segment) { return segment.reachable; } module.exports = function(context) { var codePathStack = []; return { // Stores CodePath objects. "onCodePathStart": function(codePath) { codePathStack.push(codePath); }, "onCodePathEnd": function(codePath) { codePathStack.pop(); }, // Checks reachable or not. "ExpressionStatement": function(node) { var codePath = codePathStack[codePathStack.length - 1]; // Checks the current code path segments. if (!codePath.currentSegments.some(isReachable)) { context.report({message: "Unreachable!", node: node}); } } }; };
See Also: no-unreachable, no-fallthrough, consistent-return
To check state of a code path
This example is checking whether or not the parameter cb
is called in every path. Instances of CodePath
and CodePathSegment
are shared to every rule. So a rule must not modify those instances. Please use a map of information instead.
function hasCb(node, context) { if (node.type.indexOf("Function") !== -1) { return context.getDeclaredVariables(node).some(function(v) { return v.type === "Parameter" && v.name === "cb"; }); } return false; } function isCbCalled(info) { return info.cbCalled; } module.exports = function(context) { var funcInfoStack = []; var segmentInfoMap = Object.create(null); return { // Checks `cb`. "onCodePathStart": function(codePath, node) { funcInfoStack.push({ codePath: codePath, hasCb: hasCb(node, context) }); }, "onCodePathEnd": function(codePath, node) { funcInfoStack.pop(); // Checks `cb` was called in every paths. var cbCalled = codePath.finalSegments.every(function(segment) { var info = segmentInfoMap[segment.id]; return info.cbCalled; }); if (!cbCalled) { context.report({ message: "`cb` should be called in every path.", node: node }); } }, // Manages state of code paths. "onCodePathSegmentStart": function(segment) { var funcInfo = funcInfoStack[funcInfoStack.length - 1]; // Ignores if `cb` doesn't exist. if (!funcInfo.hasCb) { return; } // Initialize state of this path. var info = segmentInfoMap[segment.id] = { cbCalled: false }; // If there are the previous paths, merges state. // Checks `cb` was called in every previous path. if (segment.prevSegments.length > 0) { info.cbCalled = segment.prevSegments.every(isCbCalled); } }, // Checks reachable or not. "CallExpression": function(node) { var funcInfo = funcInfoStack[funcInfoStack.length - 1]; // Ignores if `cb` doesn't exist. if (!funcInfo.hasCb) { return; } // Sets marks that `cb` was called. var callee = node.callee; if (callee.type === "Identifier" && callee.name === "cb") { funcInfo.codePath.currentSegments.forEach(function(segment) { var info = segmentInfoMap[segment.id]; info.cbCalled = true; }); } } }; };
See Also: constructor-super, no-this-before-super
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) { hoge(); }
SwitchStatement
switch (a) { case 0: foo(); break; case 1: case 2: bar(); // fallthrough case 3: hoge(); break; }
SwitchStatement
(has default
)
switch (a) { case 0: foo(); break; case 1: case 2: bar(); // fallthrough case 3: hoge(); break; default: fuga(); break; }
TryStatement
(try-catch)
try { foo(); if (a) { throw new Error(); } bar(); } catch (err) { hoge(err); } last();
It creates the paths from try
block to catch
block at:
-
throw
statements. - The first throwable node (e.g. a function call) in the
try
block. - The end of the
try
block.
TryStatement
(try-finally)
try { foo(); bar(); } finally { fuga(); } last();
If there is not catch
block, finally
block has two current segments. At this time, CodePath.currentSegments.length
is 2
. One is the normal path, and another is the leaving path (throw
or return
).
TryStatement
(try-catch-finally)
try { foo(); bar(); } catch (err) { hoge(err); } finally { fuga(); } 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
(for ever)
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);
It creates two code paths.
-
The global's
-
The function's
© OpenJS Foundation and other contributors
Licensed under the MIT License.
https://eslint.org/docs/developer-guide/code-path-analysis