Utility functions for working with TypeScript's Abstract Syntax Tree (AST) and type system
—
Control flow analysis utilities for determining statement reachability, analyzing function signatures, and identifying control flow termination points. These functions help understand code execution paths and unreachable code detection.
Utilities for determining if statements end control flow execution.
/**
* Check if statement or block ends control flow
* @param statement - Statement or block to analyze
* @param checker - Optional type checker for advanced analysis
* @returns true if statement terminates control flow
*/
function endsControlFlow(statement: ts.Statement | ts.BlockLike, checker?: ts.TypeChecker): boolean;
/**
* Get detailed control flow end information
* @param statement - Statement or block to analyze
* @param checker - Optional type checker for advanced analysis
* @returns Control flow end details
*/
function getControlFlowEnd(statement: ts.Statement | ts.BlockLike, checker?: ts.TypeChecker): ControlFlowEnd;
/**
* Control flow end result interface
*/
interface ControlFlowEnd {
/** Array of statements that end control flow */
readonly statements: ReadonlyArray<ControlFlowStatement>;
/** Whether control flow definitely ends */
readonly end: boolean;
}
/**
* Types of statements that can end control flow
*/
type ControlFlowStatement = ts.BreakStatement
| ts.ContinueStatement
| ts.ReturnStatement
| ts.ThrowStatement
| ts.ExpressionStatement & {expression: ts.CallExpression};Utilities for analyzing function call effects on control flow.
/**
* Check if call expression affects control flow
* @param node - Call expression to analyze
* @param checker - Type checker instance
* @returns Signature effect if call affects control flow
*/
function callExpressionAffectsControlFlow(
node: ts.CallExpression,
checker: ts.TypeChecker
): SignatureEffect | undefined;
/**
* Function signature effects on control flow
*/
enum SignatureEffect {
/** Function never returns (throws or infinite loop) */
Never = 1,
/** Function performs assertions that may throw */
Asserts = 2
}Usage Examples:
import * as ts from "typescript";
import {
endsControlFlow,
getControlFlowEnd,
callExpressionAffectsControlFlow,
SignatureEffect
} from "tsutils/util";
// Basic control flow analysis
function analyzeControlFlow(statement: ts.Statement) {
if (endsControlFlow(statement)) {
console.log("Statement ends control flow");
const details = getControlFlowEnd(statement);
console.log(`Control flow ends: ${details.end}`);
console.log(`Terminating statements: ${details.statements.length}`);
details.statements.forEach((stmt, index) => {
console.log(` ${index}: ${ts.SyntaxKind[stmt.kind]}`);
});
}
}
// Function call analysis
function analyzeFunctionCalls(checker: ts.TypeChecker, callExpr: ts.CallExpression) {
const effect = callExpressionAffectsControlFlow(callExpr, checker);
if (effect !== undefined) {
switch (effect) {
case SignatureEffect.Never:
console.log("Function never returns");
break;
case SignatureEffect.Asserts:
console.log("Function performs assertions");
break;
}
}
}
// Unreachable code detection
function detectUnreachableCode(block: ts.Block) {
for (let i = 0; i < block.statements.length; i++) {
const statement = block.statements[i];
if (endsControlFlow(statement)) {
// Check if there are statements after this one
if (i < block.statements.length - 1) {
console.log(`Unreachable code detected after statement at index ${i}`);
// Analyze what statements are unreachable
for (let j = i + 1; j < block.statements.length; j++) {
const unreachableStmt = block.statements[j];
console.log(` Unreachable: ${ts.SyntaxKind[unreachableStmt.kind]}`);
}
}
break;
}
}
}
// Advanced control flow analysis with type checker
function analyzeAdvancedControlFlow(checker: ts.TypeChecker, node: ts.Node) {
node.forEachChild(child => {
if (ts.isStatement(child)) {
const details = getControlFlowEnd(child, checker);
if (details.end) {
console.log(`Statement ends control flow: ${ts.SyntaxKind[child.kind]}`);
// Analyze the specific terminating statements
details.statements.forEach(stmt => {
if (ts.isReturnStatement(stmt)) {
console.log(" Ends with return");
} else if (ts.isThrowStatement(stmt)) {
console.log(" Ends with throw");
} else if (ts.isBreakStatement(stmt)) {
console.log(" Ends with break");
} else if (ts.isContinueStatement(stmt)) {
console.log(" Ends with continue");
} else if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
// Check if call never returns
const effect = callExpressionAffectsControlFlow(stmt.expression, checker);
if (effect === SignatureEffect.Never) {
console.log(" Ends with never-returning function call");
}
}
});
}
}
analyzeAdvancedControlFlow(checker, child);
});
}
// Switch statement analysis
function analyzeSwitchControlFlow(switchStmt: ts.SwitchStatement, checker?: ts.TypeChecker) {
let hasDefaultCase = false;
let allCasesEndControlFlow = true;
switchStmt.caseBlock.clauses.forEach(clause => {
if (ts.isDefaultClause(clause)) {
hasDefaultCase = true;
}
// Check if this case ends control flow
const statements = clause.statements;
if (statements.length === 0) {
// Empty case falls through
allCasesEndControlFlow = false;
} else {
const lastStatement = statements[statements.length - 1];
if (!endsControlFlow(lastStatement, checker)) {
allCasesEndControlFlow = false;
}
}
});
if (hasDefaultCase && allCasesEndControlFlow) {
console.log("Switch statement is exhaustive and all cases end control flow");
}
}
// If-else chain analysis
function analyzeIfElseControlFlow(ifStmt: ts.IfStatement, checker?: ts.TypeChecker) {
let allBranchesEndControlFlow = true;
// Check then branch
if (!endsControlFlow(ifStmt.thenStatement, checker)) {
allBranchesEndControlFlow = false;
}
// Check else branch
if (ifStmt.elseStatement) {
if (!endsControlFlow(ifStmt.elseStatement, checker)) {
allBranchesEndControlFlow = false;
}
} else {
// No else branch means control flow can continue
allBranchesEndControlFlow = false;
}
if (allBranchesEndControlFlow) {
console.log("All branches of if-else end control flow");
}
}
// Try-catch-finally analysis
function analyzeTryCatchControlFlow(tryStmt: ts.TryStatement, checker?: ts.TypeChecker) {
const tryEnds = endsControlFlow(tryStmt.tryBlock, checker);
let catchEnds = true;
if (tryStmt.catchClause) {
catchEnds = endsControlFlow(tryStmt.catchClause.block, checker);
}
if (tryStmt.finallyBlock) {
const finallyEnds = endsControlFlow(tryStmt.finallyBlock, checker);
if (finallyEnds) {
console.log("Finally block ends control flow (overrides try/catch)");
}
}
if (tryEnds && catchEnds) {
console.log("Both try and catch blocks end control flow");
}
}
// Dead code elimination helper
function identifyDeadCode(sourceFile: ts.SourceFile, checker: ts.TypeChecker) {
const deadCodeRanges: ts.TextRange[] = [];
function visitNode(node: ts.Node) {
if (ts.isBlock(node)) {
for (let i = 0; i < node.statements.length; i++) {
const statement = node.statements[i];
if (endsControlFlow(statement, checker)) {
// Mark remaining statements as dead code
for (let j = i + 1; j < node.statements.length; j++) {
const deadStatement = node.statements[j];
deadCodeRanges.push({
pos: deadStatement.pos,
end: deadStatement.end
});
}
break;
}
}
}
node.forEachChild(visitNode);
}
visitNode(sourceFile);
return deadCodeRanges;
}Common Use Cases:
The control flow analysis utilities are essential for:
Install with Tessl CLI
npx tessl i tessl/npm-tsutils