149 lines
5.8 KiB
JavaScript
149 lines
5.8 KiB
JavaScript
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}`);
|