Files
metabuilder/tools/detect-stub-implementations.ts
2025-12-25 16:00:00 +00:00

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)