226 lines
5.7 KiB
JavaScript
226 lines
5.7 KiB
JavaScript
// 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 '<anonymous>'
|
|
}
|
|
return '<unknown>'
|
|
}
|
|
|
|
/**
|
|
* @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()
|