import { readdirSync, readFileSync, statSync } from 'fs'; import { join, relative } from 'path'; const SRC = join(import.meta.dirname, '..', 'src'); const results = []; function walk(dir) { for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, entry.name); if (entry.isDirectory() && entry.name !== 'node_modules') walk(full); else if (/\.(tsx?|jsx?)$/.test(entry.name)) { const content = readFileSync(full, 'utf-8'); const lines = content.split('\n').length; results.push({ file: relative(join(SRC, '..'), full), lines, content }); } } } walk(SRC); results.sort((a, b) => b.lines - a.lines); console.log('=== TOP 30 FILES BY LINE COUNT ==='); for (const r of results.slice(0, 30)) { console.log(`${String(r.lines).padStart(5)} ${r.file}`); } // Detect nested loops (3+ levels) console.log('\n=== NESTED LOOP DETECTION (3+ levels) ==='); const loopPatterns = [ /for\s*\(/g, /while\s*\(/g, /\.forEach\s*\(/g, /\.map\s*\(/g, /\.filter\s*\(/g, /\.reduce\s*\(/g, /\.some\s*\(/g, /\.every\s*\(/g, /\.flatMap\s*\(/g, /\.find\s*\(/g ]; for (const r of results) { const lines = r.content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; let loopCount = 0; for (const p of loopPatterns) { p.lastIndex = 0; if (p.test(line)) loopCount++; } // Check surrounding context for nesting if (loopCount > 0 || /for\s*\(/.test(line) || /\.map\(/.test(line) || /\.forEach\(/.test(line) || /\.filter\(/.test(line) || /\.reduce\(/.test(line)) { // Count loop keywords on this single line let singleLineLoops = 0; for (const p of loopPatterns) { p.lastIndex = 0; const matches = line.match(new RegExp(p.source, 'g')); if (matches) singleLineLoops += matches.length; } if (singleLineLoops >= 2) { console.log(` [NESTED] ${r.file}:${i + 1} (${singleLineLoops} loops on one line)`); console.log(` ${line.trim().substring(0, 120)}`); } } } } // Detect missing cleanup in useEffect console.log('\n=== MEMORY LEAK RISK: useEffect without cleanup ==='); for (const r of results) { if (!r.file.endsWith('.tsx') && !r.file.endsWith('.ts')) continue; const content = r.content; // Find useEffect blocks that contain setInterval/setTimeout/addEventListener but no return const useEffectRegex = /useEffect\s*\(\s*\(\)\s*=>\s*\{([\s\S]*?)\}\s*,/g; let match; while ((match = useEffectRegex.exec(content)) !== null) { const body = match[1]; const hasTimer = /setInterval|setTimeout/.test(body); const hasListener = /addEventListener/.test(body); const hasSubscribe = /\.subscribe\(/.test(body); const hasCleanup = /return\s*\(\)\s*=>|return\s*function|return\s*\(\{/.test(body); if ((hasTimer || hasListener || hasSubscribe) && !hasCleanup) { const lineNum = content.substring(0, match.index).split('\n').length; console.log(` [RISK] ${r.file}:${lineNum}`); if (hasTimer) console.log(` - Has setInterval/setTimeout without cleanup`); if (hasListener) console.log(` - Has addEventListener without cleanup`); if (hasSubscribe) console.log(` - Has subscribe without cleanup`); console.log(` ${body.trim().substring(0, 200)}`); console.log(''); } } } // Detect objects/arrays/functions created in render body (not memoized) console.log('\n=== REDUNDANT COMPUTATION: Non-memoized values in components ==='); for (const r of results) { if (!r.file.endsWith('.tsx')) continue; const lines = r.content.split('\n'); // Look for const x = [...], const x = {...}, const x = (...) => patterns outside useMemo let inUseMemo = 0; for (let i = 0; i < lines.length; i++) { if (/useMemo\s*\(/.test(lines[i])) inUseMemo++; if (inUseMemo > 0 && /\)/.test(lines[i])) { // Rough heuristic - not perfect } if (inUseMemo === 0) { // Expensive operations in render if (/\.map\s*\(.*\.map\s*\(/.test(lines[i])) { console.log(` [PERF] ${r.file}:${i + 1} - Chained .map calls in render`); console.log(` ${lines[i].trim().substring(0, 120)}`); } if (/\.filter\s*\(.*\.map\s*\(/.test(lines[i]) || /\.map\s*\(.*\.filter\s*\(/.test(lines[i])) { console.log(` [PERF] ${r.file}:${i + 1} - filter+map chain in render`); console.log(` ${lines[i].trim().substring(0, 120)}`); } } } } // Detect deeply nested conditionals (4+ levels) console.log('\n=== HIGH COMPLEXITY: Deep nesting ==='); for (const r of results) { const lines = r.content.split('\n'); let maxIndent = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.trim() === '') continue; const indent = line.match(/^(\s*)/)[1].length; // Only flag if inside if/else/switch/ternary if (indent >= 16 && /if\s*\(|else|switch\s*\(|case\s+/.test(line.trim())) { console.log(` [DEEP] ${r.file}:${i + 1} (indent=${indent})`); console.log(` ${line.trim().substring(0, 120)}`); } } } // Detect inline style objects in JSX (recreated every render) console.log('\n=== REDUNDANT: Inline style objects in JSX ==='); for (const r of results) { if (!r.file.endsWith('.tsx')) continue; const lines = r.content.split('\n'); for (let i = 0; i < lines.length; i++) { if (/style\s*=\s*\{\s*\{/.test(lines[i]) && !/useMemo/.test(lines[i])) { console.log(` [INLINE] ${r.file}:${i + 1}`); console.log(` ${lines[i].trim().substring(0, 120)}`); } } } // Total stats console.log('\n=== SUMMARY ==='); console.log(`Total files: ${results.length}`); console.log(`Total lines: ${results.reduce((s, r) => s + r.lines, 0)}`); console.log(`Files > 500 lines: ${results.filter(r => r.lines > 500).length}`); console.log(`Files > 1000 lines: ${results.filter(r => r.lines > 1000).length}`);