149 lines
5.9 KiB
JavaScript
149 lines
5.9 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Scans JS files for ALL_CAPS identifiers that are used but never declared
|
|
* in the same file (potential ReferenceError bugs, e.g. bare PARTNER_QUEUE
|
|
* instead of env.PARTNER_QUEUE).
|
|
*
|
|
* Detection strategy:
|
|
* - Strip block comments, line comments, and string literals before scanning
|
|
* - Collect "declared" ALL_CAPS names from:
|
|
* const/let/var FOO = ...
|
|
* const { FOO, BAR } = require(...) (destructure on const/let/var line)
|
|
* function FOO(
|
|
* exports.FOO = ...
|
|
* - Flag any ALL_CAPS usage not preceded by '.' (property access) that is
|
|
* not in the declared set or the known-globals whitelist
|
|
*
|
|
* NOTE: This is a best-effort regex scan, not a full AST analysis.
|
|
* Multi-line destructuring is not handled.
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
|
|
const SCAN_DIRS = ['workers', 'controllers', 'helpers', 'services', 'routes'];
|
|
|
|
// Node/JS builtins and well-known globals that are never "declared" in a file
|
|
const KNOWN_GLOBALS = new Set([
|
|
'NaN', 'Infinity', 'JSON', 'Math', 'Number', 'String', 'Buffer', 'Date',
|
|
'Error', 'Array', 'Object', 'Promise', 'Set', 'Map', 'RegExp', 'Symbol',
|
|
'Boolean', 'URL', 'URLSearchParams', 'process', 'module', 'exports',
|
|
'require', 'console', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
|
|
'ReferenceError', 'TypeError', 'SyntaxError', 'RangeError', 'URIError', 'EvalError',
|
|
'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH', 'OPTIONS',
|
|
'NODE_ENV', 'PRODUCTION', 'DEBUG',
|
|
// Mongoose / ODM types
|
|
'Schema', 'ObjectId', 'Mixed', 'Decimal128',
|
|
// Common pattern abbreviations that appear in strings or conditions
|
|
'NULL', 'TRUE', 'FALSE',
|
|
]);
|
|
|
|
/**
|
|
* Strip block comments, line comments, and string/template literals from source
|
|
* so that ALL_CAPS words inside them don't produce false positives.
|
|
*/
|
|
function stripNonCode(src) {
|
|
// 1. Block comments /* ... */ — preserve newlines so line numbers stay accurate
|
|
src = src.replace(/\/\*[\s\S]*?\*\//g, m => m.replace(/[^\n]/g, ' '));
|
|
// 2+3. Strip single/double-quoted strings BEFORE comments and template literals.
|
|
// Removes any backticks (and `//`) that live inside string literals so they
|
|
// don't confuse the steps below.
|
|
// Use [^"\\\n] / [^'\\\n] to prevent matching across newlines — JS strings
|
|
// cannot actually span source lines, so this keeps line numbers accurate.
|
|
src = src.replace(/"(?:[^"\\\n]|\\.)*"/g, '""');
|
|
src = src.replace(/'(?:[^'\\\n]|\\.)*'/g, "''");
|
|
// 4. Line comments — use negative lookbehind to avoid treating `://` (URLs
|
|
// inside template literals like `${protocol}://host/path`) as comments.
|
|
src = src.replace(/(?<!:)\/\/[^\n]*/g, m => ' '.repeat(m.length));
|
|
// 5. Template literals — strings are already blank, so backtick pairing is clean.
|
|
src = src.replace(/`[^`]*`/gs, m => m.replace(/[^\n]/g, ' '));
|
|
return src;
|
|
}
|
|
|
|
function scanFile(filePath) {
|
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
const src = stripNonCode(raw);
|
|
const lines = src.split('\n');
|
|
|
|
const declared = new Set();
|
|
const uses = []; // { name, line }
|
|
|
|
// ── Declaration pass (whole-file regex, handles multi-line const blocks) ──
|
|
|
|
// 1. const/let/var FOO = ... (keyword on same line)
|
|
for (const m of src.matchAll(/\b(?:const|let|var)\s+([A-Z][A-Z0-9_]{2,})\s*=/g))
|
|
declared.add(m[1]);
|
|
|
|
// 2. FOO = require( — catches continuation lines in multi-line const blocks
|
|
// e.g. const\n FILE = require(...),\n CVCST = require(...),
|
|
for (const m of src.matchAll(/\b([A-Z][A-Z0-9_]{2,})\s*=\s*require\s*\(/g))
|
|
declared.add(m[1]);
|
|
|
|
// 3. { FOO, BAR } = require( — destructuring anywhere (incl. multi-line const)
|
|
for (const m of src.matchAll(/\{([^}]+)\}\s*=\s*require\s*\(/g))
|
|
for (const id of m[1].matchAll(/\b([A-Z][A-Z0-9_]{2,})\b/g))
|
|
declared.add(id[1]);
|
|
|
|
// 4. const/let/var { FOO } = non-require (e.g. model destructuring)
|
|
for (const m of src.matchAll(/\b(?:const|let|var)\s*\{([^}]+)\}\s*=/g))
|
|
for (const id of m[1].matchAll(/\b([A-Z][A-Z0-9_]{2,})\b/g))
|
|
declared.add(id[1]);
|
|
|
|
// 5. function FOO_BAR(
|
|
for (const m of src.matchAll(/\bfunction\s+([A-Z][A-Z0-9_]{2,})\s*\(/g))
|
|
declared.add(m[1]);
|
|
|
|
// 6. exports.FOO = ...
|
|
for (const m of src.matchAll(/\bexports\.([A-Z][A-Z0-9_]{2,})\s*=/g))
|
|
declared.add(m[1]);
|
|
|
|
// ── Usage pass (line by line for accurate line numbers) ─────────────────
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
|
|
// Skip JSDoc / block-comment lines that survived stripping
|
|
if (line.trim().startsWith('*')) continue;
|
|
|
|
// Match ALL_CAPS (4+ chars) NOT preceded by '.' (property access)
|
|
// and NOT followed by a bare ':' (object literal key)
|
|
for (const m of line.matchAll(/(?<![.\w])([A-Z][A-Z0-9_]{3,})\b(?!\s*:(?![:=]))/g)) {
|
|
const name = m[1];
|
|
if (!KNOWN_GLOBALS.has(name)) {
|
|
uses.push({ name, line: i + 1 });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Report used-but-never-declared
|
|
const undeclared = uses.filter(u => !declared.has(u.name));
|
|
if (undeclared.length === 0) return;
|
|
|
|
// Dedupe by name, keep first occurrence line number
|
|
const seen = new Map();
|
|
for (const u of undeclared) {
|
|
if (!seen.has(u.name)) seen.set(u.name, u.line);
|
|
}
|
|
|
|
console.log(`\n${path.relative(ROOT, filePath)}`);
|
|
for (const [name, line] of seen) {
|
|
console.log(` L${String(line).padStart(4)}: ${name}`);
|
|
}
|
|
}
|
|
|
|
// Gather files from each directory
|
|
let files = [];
|
|
for (const dir of SCAN_DIRS) {
|
|
const dirPath = path.join(ROOT, dir);
|
|
if (!fs.existsSync(dirPath)) continue;
|
|
const entries = fs.readdirSync(dirPath).filter(f => f.endsWith('.js'));
|
|
files = files.concat(entries.map(f => path.join(dirPath, f)));
|
|
}
|
|
|
|
console.log(`Scanning ${files.length} files...`);
|
|
for (const f of files) {
|
|
try { scanFile(f); } catch (e) { console.error(`Error scanning ${f}: ${e.message}`); }
|
|
}
|
|
console.log('\nDone.');
|