Versions
Index

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();

Code Path Example

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 use id as 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 with first (CodePathSegment) and last (CodePathSegment) properties to limit the traversal. The callback receives a CodePathSegment and a controller object, and is called with this set to the current CodePath. The controller has a skip() method to skip the following segments in the current branch and a break() 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 use id as 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 is false in segments preceded by return, throw, break, or continue.

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:

  1. The top-level code (origin: "program"):

    Simple Program Code Path 1

  2. The function foo (origin: "function"):

    Simple Program Code Path 2

  3. The arrow function callback defined inside foo (origin: "function"):

    Simple Program Code Path 3

  4. The function bar (origin: "function"):

    Simple Program Code Path 4

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();

Loop Event's Example

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!");

Hello World

IfStatement

if (a) {
	foo();
} else {
	bar();
}

IfStatement (chain)

if (a) {
	foo();
} else if (b) {
	bar();
} else if (c) {
	baz();
}

 (chain)

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;
}

 (has )

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:

  • throw statements.
  • The first throwable node (e.g. a function call) in the try block.
  • The end of the try block.

 (try-catch)

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.

 (try-finally)

TryStatement (try-catch-finally)

try {
	foo();
	bar();
} catch (err) {
	baz(err);
} finally {
	quux();
}
last();

 (try-catch-finally)

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();

 (infinite loop)

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:

  1. The global code path

    When there is a function — global code path

  2. The function foo()'s code path

    When there is a function — function 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.

 in generator functions

Change Language