← back to Site Factory
critic/checks/code.js
133 lines
// code.js — heuristic JS/TS lint
// Rules:
// * file > 500 lines → low "long file"
// * console.log( count > 5 → low "noisy logging"
// * async fn with no try block → medium "unhandled async"
// * @ts-ignore / `any` count >3 in a TS file → medium "type escape hatches"
const fs = require('fs');
const path = require('path');
const { glob } = require('glob');
const SOURCE = 'code';
const SCAN_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx']);
async function expandPaths(paths) {
const files = new Set();
for (const p of paths || []) {
if (!p) continue;
let stat;
try { stat = fs.statSync(p); } catch { continue; }
if (stat.isFile()) {
if (SCAN_EXTS.has(path.extname(p))) files.add(p);
} else if (stat.isDirectory()) {
const matches = await glob('**/*.{js,ts,tsx,jsx}', {
cwd: p,
ignore: ['**/node_modules/**', '**/.next/**', '**/dist/**', '**/build/**'],
absolute: true,
nodir: true,
});
for (const m of matches) files.add(m);
}
}
return [...files];
}
function countMatches(src, re) {
const m = src.match(re);
return m ? m.length : 0;
}
// Heuristic: an async function declaration / arrow that does not contain `try {`
// in its (rough) body. Body = from the opening `{` after the declaration to the
// matching `}`. We do balance counting; not perfect but good enough for a fast
// static check.
function findUnhandledAsync(src) {
const findings = [];
const re = /\basync\s+(?:function\s+\w*|\w+\s*=|\([^)]*\)\s*=>|function)/g;
let m;
while ((m = re.exec(src)) != null) {
// find the next `{` after match
let i = src.indexOf('{', m.index + m[0].length);
if (i < 0) continue;
let depth = 0;
let end = -1;
for (let j = i; j < src.length; j++) {
const c = src[j];
if (c === '{') depth++;
else if (c === '}') { depth--; if (depth === 0) { end = j; break; } }
}
if (end < 0) continue;
const body = src.slice(i, end);
if (!/\btry\s*\{/.test(body)) {
// get line number of the async keyword
const line = src.slice(0, m.index).split('\n').length;
findings.push({ line });
}
}
return findings;
}
async function run({ paths /* , screenshots, domain */ }) {
const findings = [];
const files = await expandPaths(paths);
for (const file of files) {
let src;
try { src = fs.readFileSync(file, 'utf8'); } catch { continue; }
const ext = path.extname(file);
const isTs = ext === '.ts' || ext === '.tsx';
const lines = src.split('\n').length;
if (lines > 500) {
findings.push({
source: SOURCE,
severity: 'low',
title: `Long file: ${path.basename(file)} (${lines} lines)`,
detail: `${file} is ${lines} lines. Long files are harder to review and refactor.`,
suggested_fix: 'Split into smaller modules grouped by responsibility (one component / one concern per file).',
});
}
const consoleLogs = countMatches(src, /\bconsole\.log\s*\(/g);
if (consoleLogs > 5) {
findings.push({
source: SOURCE,
severity: 'low',
title: `Noisy logging: ${consoleLogs} console.log() in ${path.basename(file)}`,
detail: `${file} contains ${consoleLogs} console.log calls.`,
suggested_fix: 'Replace with a debug() helper or pino/winston logger; strip debug noise from production paths.',
});
}
const unhandled = findUnhandledAsync(src);
if (unhandled.length) {
findings.push({
source: SOURCE,
severity: 'medium',
title: `Unhandled async: ${unhandled.length} async fn(s) without try/catch in ${path.basename(file)}`,
detail: `${file} — async functions at line(s) ${unhandled.slice(0, 5).map(u => u.line).join(', ')}${unhandled.length > 5 ? '…' : ''} have no try block.`,
suggested_fix: 'Wrap async bodies with try/catch (or .catch() at the call site) so rejections do not become unhandledPromiseRejections.',
});
}
if (isTs) {
const tsIgnores = countMatches(src, /\/\/\s*@ts-ignore/g) + countMatches(src, /\/\/\s*@ts-expect-error/g);
const anyCount = countMatches(src, /:\s*any\b/g) + countMatches(src, /\bas\s+any\b/g);
const total = tsIgnores + anyCount;
if (total > 3) {
findings.push({
source: SOURCE,
severity: 'medium',
title: `Type escape hatches: ${total} in ${path.basename(file)}`,
detail: `${file} has ${tsIgnores} ts-ignore/expect-error comments and ${anyCount} \`any\` uses.`,
suggested_fix: 'Tighten types: replace `any` with `unknown` + narrow, or define proper interfaces. Remove ts-ignore lines once types resolve.',
});
}
}
}
return findings;
}
module.exports = { run };