Files
omniai-web/scripts/static-analysis.mjs
stringadmin 4a298d205b
Web Quality / verify (push) Has been cancelled
chore: reduce frontend lint warnings
2026-06-09 12:02:30 +08:00

148 lines
5.7 KiB
JavaScript

import { readdirSync, readFileSync } 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');
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}`);