mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 14:25:02 +00:00
216 lines
6.7 KiB
TypeScript
216 lines
6.7 KiB
TypeScript
#!/usr/bin/env tsx
|
|
|
|
import { readdirSync, readFileSync, statSync, writeFileSync } from 'fs'
|
|
import { join, extname } from 'path'
|
|
|
|
interface StubLocation {
|
|
file: string
|
|
line: number
|
|
type: 'placeholder-return' | 'not-implemented' | 'empty-body' | 'todo-comment' | 'console-log-only' | 'placeholder-render' | 'mock-data' | 'stub-component'
|
|
name: string
|
|
severity: 'high' | 'medium' | 'low'
|
|
code: string
|
|
}
|
|
|
|
const STUB_PATTERNS = [
|
|
{
|
|
name: 'Not implemented error',
|
|
pattern: /throw\s+new\s+Error\s*\(\s*['"]not\s+implemented/i,
|
|
type: 'not-implemented' as const,
|
|
severity: 'high' as const,
|
|
description: 'Function throws "not implemented"'
|
|
},
|
|
{
|
|
name: 'TODO comment in function',
|
|
pattern: /\/\/\s*TODO|\/\/\s*FIXME|\/\/\s*XXX|\/\/\s*HACK/i,
|
|
type: 'todo-comment' as const,
|
|
severity: 'medium' as const,
|
|
description: 'Function has TODO/FIXME comment'
|
|
},
|
|
{
|
|
name: 'Console.log only',
|
|
pattern: /function\s+\w+[^{]*{\s*console\.(log|debug)\s*\([^)]*\)\s*}|const\s+\w+\s*=\s*[^=>\s]*=>\s*console\.(log|debug)/,
|
|
type: 'console-log-only' as const,
|
|
severity: 'high' as const,
|
|
description: 'Function only logs to console'
|
|
},
|
|
{
|
|
name: 'Return null/undefined stub',
|
|
pattern: /return\s+(null|undefined)|return\s*;(?=\s*})/,
|
|
type: 'placeholder-return' as const,
|
|
severity: 'low' as const,
|
|
description: 'Function only returns null/undefined'
|
|
},
|
|
{
|
|
name: 'Return mock data',
|
|
pattern: /return\s+(\{[^}]*\}|\[[^\]]*\])\s*\/\/\s*(mock|stub|todo|placeholder|example)/i,
|
|
type: 'mock-data' as const,
|
|
severity: 'medium' as const,
|
|
description: 'Function returns hardcoded mock data'
|
|
},
|
|
{
|
|
name: 'Placeholder text in JSX',
|
|
pattern: /<[A-Z]\w*[^>]*>\s*(placeholder|TODO|FIXME|stub|mock|example|not implemented)/i,
|
|
type: 'placeholder-render' as const,
|
|
severity: 'medium' as const,
|
|
description: 'Component renders placeholder text'
|
|
},
|
|
{
|
|
name: 'Empty component body',
|
|
pattern: /export\s+(?:default\s+)?(?:function|const)\s+(\w+).*?\{[\s\n]*return\s+<[^>]+>\s*<\/[^>]+>\s*;?[\s\n]*\}/,
|
|
type: 'stub-component' as const,
|
|
severity: 'high' as const,
|
|
description: 'Component has empty/minimal body'
|
|
}
|
|
]
|
|
|
|
function findStubs(): StubLocation[] {
|
|
const results: StubLocation[] = []
|
|
const srcDir = 'src'
|
|
|
|
function walkDir(dir: string) {
|
|
try {
|
|
const files = readdirSync(dir)
|
|
|
|
for (const file of files) {
|
|
const fullPath = join(dir, file)
|
|
const stat = statSync(fullPath)
|
|
|
|
if (stat.isDirectory() && !['node_modules', '.next', 'dist', 'build', '.git'].includes(file)) {
|
|
walkDir(fullPath)
|
|
} else if (['.ts', '.tsx', '.js', '.jsx'].includes(extname(file))) {
|
|
scanFile(fullPath, results)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Skip inaccessible directories
|
|
}
|
|
}
|
|
|
|
walkDir(srcDir)
|
|
return results
|
|
}
|
|
|
|
function scanFile(filePath: string, results: StubLocation[]): void {
|
|
try {
|
|
const content = readFileSync(filePath, 'utf8')
|
|
const lines = content.split('\n')
|
|
|
|
// Find function/component boundaries
|
|
const functionPattern = /(?:export\s+)?(?:async\s+)?(?:function|const)\s+(\w+)/g
|
|
let match
|
|
|
|
while ((match = functionPattern.exec(content)) !== null) {
|
|
const functionName = match[1]
|
|
const startIndex = match.index
|
|
const lineNumber = content.substring(0, startIndex).split('\n').length
|
|
|
|
// Extract function body
|
|
const bodyStart = content.indexOf('{', startIndex)
|
|
let braceCount = 0
|
|
let bodyEnd = bodyStart
|
|
|
|
for (let i = bodyStart; i < content.length; i++) {
|
|
if (content[i] === '{') braceCount++
|
|
if (content[i] === '}') braceCount--
|
|
if (braceCount === 0) {
|
|
bodyEnd = i
|
|
break
|
|
}
|
|
}
|
|
|
|
const functionBody = content.substring(bodyStart, bodyEnd + 1)
|
|
|
|
// Check against stub patterns
|
|
checkPatterns(functionBody, filePath, lineNumber, functionName, results)
|
|
}
|
|
|
|
// Check for stub comments in file
|
|
lines.forEach((line, idx) => {
|
|
if (line.match(/stub|placeholder|mock|not implemented|TODO.*implementation/i)) {
|
|
results.push({
|
|
file: filePath,
|
|
line: idx + 1,
|
|
type: 'todo-comment',
|
|
name: 'Stub indicator',
|
|
severity: 'low',
|
|
code: line.trim()
|
|
})
|
|
}
|
|
})
|
|
} catch (e) {
|
|
// Skip files that can't be analyzed
|
|
}
|
|
}
|
|
|
|
function checkPatterns(body: string, filePath: string, lineNumber: number, name: string, results: StubLocation[]): void {
|
|
for (const pattern of STUB_PATTERNS) {
|
|
const regex = new RegExp(pattern.pattern.source, 'i')
|
|
if (regex.test(body)) {
|
|
const bodyLineNum = body.split('\n')[0]?.length > 0 ?
|
|
lineNumber : lineNumber + 1
|
|
|
|
results.push({
|
|
file: filePath,
|
|
line: bodyLineNum,
|
|
type: pattern.type,
|
|
name: name,
|
|
severity: pattern.severity,
|
|
code: body.split('\n').slice(0, 3).join('\n').substring(0, 80)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Main execution
|
|
const stubs = findStubs()
|
|
|
|
// Categorize by severity
|
|
const bySeverity = {
|
|
high: stubs.filter(s => s.severity === 'high'),
|
|
medium: stubs.filter(s => s.severity === 'medium'),
|
|
low: stubs.filter(s => s.severity === 'low')
|
|
}
|
|
|
|
const summary = {
|
|
totalStubsFound: stubs.length,
|
|
bySeverity: {
|
|
high: bySeverity.high.length,
|
|
medium: bySeverity.medium.length,
|
|
low: bySeverity.low.length
|
|
},
|
|
byType: {
|
|
'not-implemented': stubs.filter(s => s.type === 'not-implemented').length,
|
|
'todo-comment': stubs.filter(s => s.type === 'todo-comment').length,
|
|
'console-log-only': stubs.filter(s => s.type === 'console-log-only').length,
|
|
'placeholder-return': stubs.filter(s => s.type === 'placeholder-return').length,
|
|
'mock-data': stubs.filter(s => s.type === 'mock-data').length,
|
|
'placeholder-render': stubs.filter(s => s.type === 'placeholder-render').length,
|
|
'stub-component': stubs.filter(s => s.type === 'stub-component').length,
|
|
'empty-body': stubs.filter(s => s.type === 'empty-body').length
|
|
},
|
|
criticalIssues: bySeverity.high.map(s => ({
|
|
file: s.file,
|
|
line: s.line,
|
|
function: s.name,
|
|
type: s.type
|
|
})),
|
|
details: stubs.sort((a, b) => {
|
|
const severityOrder = { high: 0, medium: 1, low: 2 }
|
|
return severityOrder[a.severity] - severityOrder[b.severity]
|
|
}),
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
|
|
const serialized = JSON.stringify(summary, null, 2)
|
|
const outputPath = process.argv[2] || 'stub-patterns.json'
|
|
|
|
try {
|
|
writeFileSync(outputPath, serialized)
|
|
console.error(`Stub summary written to ${outputPath}`)
|
|
} catch (error) {
|
|
console.error(`Failed to write stub summary to ${outputPath}:`, error)
|
|
}
|
|
|
|
console.log(serialized)
|