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.
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/" }