#!/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)