// This script is a local dev helper to identify functions likely violating ESLint `max-lines-per-function` // without running ESLint. It uses the TypeScript AST to compute line spans. // // Usage: // node scripts/findLongFunctions.js [--max 40] [--limit 50] // // Notes: // - Counts *raw* line span (endLine - startLine + 1). ESLint uses a different metric // (skipBlankLines/skipComments). This script is used to pick candidates efficiently. // // It is intentionally plain JS to avoid requiring a TS build step. const fs = require('fs') const path = require('path') const ts = require('typescript') /** * @typedef {Readonly<{ file: string; name: string; startLine: number; endLine: number; span: number }>} LongFn */ /** * @param {string} value * @returns {number | undefined} */ function readNumberArg(value) { const n = Number(value) if (Number.isFinite(n) && n > 0) { return n } return undefined } /** * @param {string[]} argv * @returns {{ max: number; limit: number }} */ function readArgs(argv) { let max = 40 let limit = 50 for (let i = 0; i < argv.length; i += 1) { const token = argv[i] const next = argv[i + 1] if (token === '--max' && next) { max = readNumberArg(next) ?? max i += 1 continue } if (token === '--limit' && next) { limit = readNumberArg(next) ?? limit i += 1 } } return { max, limit } } /** * @param {string} p * @returns {boolean} */ function isIgnoredPath(p) { const normalized = p.split(path.sep).join('/') return ( normalized.includes('/node_modules/') || normalized.includes('/.next/') || normalized.includes('/dist/') || normalized.includes('/out/') ) } /** * @param {string} dir * @param {string[]} out * @returns {void} */ function collectSourceFiles(dir, out) { const entries = fs.readdirSync(dir, { withFileTypes: true }) for (const entry of entries) { const full = path.join(dir, entry.name) if (isIgnoredPath(full)) { continue } if (entry.isDirectory()) { collectSourceFiles(full, out) continue } if (!entry.isFile()) { continue } if (full.endsWith('.ts') || full.endsWith('.tsx')) { out.push(full) } } } /** * @param {ts.Node} node * @returns {string} */ function getReadableName(node) { if (ts.isFunctionDeclaration(node) && node.name) { return node.name.getText() } if (ts.isMethodDeclaration(node) && node.name) { return node.name.getText() } if (ts.isConstructorDeclaration(node)) { return 'constructor' } if (ts.isGetAccessorDeclaration(node) && node.name) { return `get ${node.name.getText()}` } if (ts.isSetAccessorDeclaration(node) && node.name) { return `set ${node.name.getText()}` } if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { const parent = node.parent if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) { return parent.name.text } if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) { return parent.name.text } return '' } return '' } /** * @param {ts.Node} node * @returns {node is (ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration | ts.ConstructorDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration)} */ function isFunctionLikeWithBody(node) { if (!ts.isFunctionLike(node)) { return false } // Arrow functions may have expression bodies. We only flag block bodies for line spans. // Function/method declarations use block bodies. return Boolean(node.body) } /** * @param {string} filename * @param {number} max * @returns {LongFn[]} */ function findLongFunctionsInFile(filename, max) { const sourceText = fs.readFileSync(filename, 'utf8') const sourceFile = ts.createSourceFile( filename, sourceText, ts.ScriptTarget.Latest, true, filename.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS ) /** @type {LongFn[]} */ const results = [] /** * @param {ts.Node} node * @returns {void} */ function visit(node) { if (isFunctionLikeWithBody(node)) { const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile, false)) const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd()) const startLine = start.line + 1 const endLine = end.line + 1 const span = endLine - startLine + 1 if (span > max) { results.push({ file: filename, name: getReadableName(node), startLine, endLine, span, }) } } ts.forEachChild(node, visit) } visit(sourceFile) return results } /** * @param {LongFn} fn * @returns {string} */ function formatFn(fn) { const rel = path.relative(process.cwd(), fn.file).split(path.sep).join('/') return `${fn.span.toString().padStart(4, ' ')} ${rel}:${fn.startLine}-${fn.endLine} ${fn.name}` } function main() { const { max, limit } = readArgs(process.argv.slice(2)) /** @type {string[]} */ const files = [] for (const dir of ['components', 'hooks', 'lib', 'pages', 'types', 'scripts']) { const full = path.join(process.cwd(), dir) if (fs.existsSync(full)) { collectSourceFiles(full, files) } } /** @type {LongFn[]} */ const all = [] for (const file of files) { const found = findLongFunctionsInFile(file, max) all.push(...found) } all.sort((a, b) => b.span - a.span || a.file.localeCompare(b.file) || a.startLine - b.startLine) const top = all.slice(0, limit) console.log(`Found ${all.length} function-like nodes with raw span > ${max} lines.`) console.log(`Showing top ${top.length} (sorted by span desc):\n`) for (const fn of top) { console.log(formatFn(fn)) } } main()