typescript Best Practices September 1, 2025

Code Complexity Analyzer Script

A Node.js script that analyzes cyclomatic complexity, cognitive complexity, and function length across a TypeScript/JavaScript codebase, generating a report of hotspots.

complexityanalysismetricstypescriptcode-quality

Description

A standalone script that scans TypeScript and JavaScript files to compute complexity metrics. It identifies functions that are too complex, too long, or have too many parameters — the prime candidates for refactoring.

Features

  • Cyclomatic complexity calculation per function
  • Lines of code per function with configurable threshold
  • Parameter count tracking
  • Sorted output — worst offenders first
  • JSON and table output formats
  • Exit code for CI integration — fails if thresholds exceeded

The Script

#!/usr/bin/env npx tsx
// complexity-analyzer.ts — Analyze code complexity in TS/JS files

import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';

interface FunctionMetrics {
  file: string;
  name: string;
  line: number;
  complexity: number;
  lines: number;
  params: number;
}

interface Thresholds {
  maxComplexity: number;
  maxLines: number;
  maxParams: number;
}

const DEFAULTS: Thresholds = {
  maxComplexity: 10,
  maxLines: 50,
  maxParams: 4,
};

// Count branching keywords for cyclomatic complexity approximation
const COMPLEXITY_PATTERNS = [
  /\bif\s*\(/g,
  /\belse\s+if\s*\(/g,
  /\bfor\s*\(/g,
  /\bwhile\s*\(/g,
  /\bcase\s+/g,
  /\bcatch\s*\(/g,
  /\?\?/g,       // nullish coalescing
  /\?\./g,       // optional chaining (as a branching indicator)
  /&&/g,
  /\|\|/g,
  /\?[^.?]/g,   // ternary operator
];

function countComplexity(code: string): number {
  let count = 1; // Base complexity
  for (const pattern of COMPLEXITY_PATTERNS) {
    const matches = code.match(pattern);
    if (matches) count += matches.length;
  }
  return count;
}

function extractFunctions(code: string, filePath: string): FunctionMetrics[] {
  const functions: FunctionMetrics[] = [];
  const lines = code.split('\n');

  // Simple regex-based function detection
  const funcPatterns = [
    /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
    /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|(\w+))\s*=>/,
    /(\w+)\s*\(([^)]*)\)\s*(?::\s*\w[^{]*)?\s*\{/,
  ];

  let braceDepth = 0;
  let currentFunc: { name: string; startLine: number; params: number; body: string } | null = null;

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];

    if (!currentFunc) {
      for (const pattern of funcPatterns) {
        const match = line.match(pattern);
        if (match) {
          const name = match[1] || 'anonymous';
          const paramStr = match[2] || '';
          const params = paramStr.trim() ? paramStr.split(',').length : 0;
          currentFunc = { name, startLine: i + 1, params, body: '' };
          braceDepth = 0;
          break;
        }
      }
    }

    if (currentFunc) {
      currentFunc.body += line + '\n';
      braceDepth += (line.match(/\{/g) || []).length;
      braceDepth -= (line.match(/\}/g) || []).length;

      if (braceDepth <= 0 && currentFunc.body.includes('{')) {
        const bodyLines = currentFunc.body.split('\n').filter(l => l.trim()).length;
        functions.push({
          file: filePath,
          name: currentFunc.name,
          line: currentFunc.startLine,
          complexity: countComplexity(currentFunc.body),
          lines: bodyLines,
          params: currentFunc.params,
        });
        currentFunc = null;
      }
    }
  }

  return functions;
}

function walkDir(dir: string, extensions: string[]): string[] {
  const files: string[] = [];
  const entries = readdirSync(dir);

  for (const entry of entries) {
    const fullPath = join(dir, entry);
    if (entry === 'node_modules' || entry === 'dist' || entry.startsWith('.')) continue;

    const stat = statSync(fullPath);
    if (stat.isDirectory()) {
      files.push(...walkDir(fullPath, extensions));
    } else if (extensions.some(ext => fullPath.endsWith(ext))) {
      files.push(fullPath);
    }
  }

  return files;
}

function analyze(srcDir: string, thresholds: Thresholds = DEFAULTS): void {
  const files = walkDir(srcDir, ['.ts', '.tsx', '.js', '.jsx']);
  const allMetrics: FunctionMetrics[] = [];

  for (const file of files) {
    const code = readFileSync(file, 'utf-8');
    const relPath = relative(process.cwd(), file);
    allMetrics.push(...extractFunctions(code, relPath));
  }

  // Find violations
  const violations = allMetrics.filter(
    m => m.complexity > thresholds.maxComplexity
      || m.lines > thresholds.maxLines
      || m.params > thresholds.maxParams,
  );

  // Sort by complexity descending
  violations.sort((a, b) => b.complexity - a.complexity);

  // Report
  console.log(`\n📊 Complexity Analysis Report`);
  console.log(`   Files scanned: ${files.length}`);
  console.log(`   Functions found: ${allMetrics.length}`);
  console.log(`   Violations: ${violations.length}\n`);

  if (violations.length === 0) {
    console.log('✅ No complexity violations found!\n');
    process.exit(0);
  }

  console.log('⚠️  Functions exceeding thresholds:\n');
  console.log(
    'File'.padEnd(40) +
    'Function'.padEnd(25) +
    'Line'.padEnd(8) +
    'CC'.padEnd(6) +
    'LOC'.padEnd(6) +
    'Params',
  );
  console.log(''.repeat(95));

  for (const v of violations) {
    const flags: string[] = [];
    if (v.complexity > thresholds.maxComplexity) flags.push(`CC:${v.complexity}`);
    if (v.lines > thresholds.maxLines) flags.push(`LOC:${v.lines}`);
    if (v.params > thresholds.maxParams) flags.push(`P:${v.params}`);

    console.log(
      v.file.padEnd(40) +
      v.name.padEnd(25) +
      String(v.line).padEnd(8) +
      String(v.complexity).padEnd(6) +
      String(v.lines).padEnd(6) +
      String(v.params),
    );
  }

  console.log(`\n${violations.length} function(s) exceed thresholds`);
  process.exit(1);
}

// Run
const srcDir = process.argv[2] || 'src';
analyze(srcDir);

Usage

# Analyze the src directory
npx tsx complexity-analyzer.ts src/

# Example output:
# 📊 Complexity Analysis Report
#    Files scanned: 42
#    Functions found: 186
#    Violations: 3
#
# ⚠️  Functions exceeding thresholds:
#
# File                                    Function                 Line    CC    LOC   Params
# ───────────────────────────────────────────────────────────────────────────────────────────
# src/services/OrderService.ts            processOrder             45      15    62    5
# src/utils/parser.ts                     parseConfig              12      12    55    3
# src/handlers/webhook.ts                 handleWebhook            8       11    48    6

# Add to CI
# "scripts": { "quality": "npx tsx complexity-analyzer.ts src/" }