CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tsutils

Utility functions for working with TypeScript's Abstract Syntax Tree (AST) and type system

Pending
Overview
Eval results
Files

control-flow.mddocs/

Control Flow Analysis

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.

Capabilities

Control Flow Termination Analysis

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

Function Signature Analysis

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:

  1. Unreachable Code Detection: Identify code that can never be executed
  2. Dead Code Elimination: Remove statements after control flow termination
  3. Control Flow Graph Construction: Build flow graphs for analysis
  4. Switch Statement Exhaustiveness: Verify all code paths are handled
  5. Return Path Analysis: Ensure all function paths return values
  6. Exception Flow Analysis: Track throw statements and exception propagation
  7. Loop Termination Analysis: Detect infinite loops and guaranteed exits
  8. Conditional Branch Analysis: Analyze if-else chains and nested conditions

The control flow analysis utilities are essential for:

  • Static analysis tools and linters
  • Code optimization and dead code elimination
  • Compiler warnings about unreachable code
  • Code coverage analysis
  • Refactoring tools that need to understand execution paths
  • Security analysis tools that track data flow

Install with Tessl CLI

npx tessl i tessl/npm-tsutils

docs

ast-traversal.md

code-analysis.md

control-flow.md

index.md

node-typeguards.md

text-processing.md

type-guards.md

type-utilities.md

variable-usage.md

tile.json