'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(/(? ' '.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(/(? !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.');