Files
metabuilder/tools/analyze-implementation-completeness.ts
2025-12-25 16:00:00 +00:00

231 lines
7.0 KiB
TypeScript

#!/usr/bin/env tsx
import { readdirSync, readFileSync, statSync, writeFileSync } from 'fs'
import { join, extname } from 'path'
interface ComponentAnalysis {
file: string
line: number
name: string
type: 'component' | 'function'
returnLines: number
jsxLines: number
logicalLines: number
completeness: number
flags: string[]
severity: 'critical' | 'high' | 'medium' | 'low' | 'info'
summary: string
}
function analyzeImplementations(): ComponentAnalysis[] {
const results: ComponentAnalysis[] = []
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))) {
analyzeFile(fullPath, results)
}
}
} catch (e) {
// Skip inaccessible directories
}
}
walkDir(srcDir)
return results
}
function analyzeFile(filePath: string, results: ComponentAnalysis[]): void {
try {
const content = readFileSync(filePath, 'utf8')
// Find all functions and components
const defPattern = /(?:export\s+)?(?:async\s+)?(?:function|const)\s+(\w+)\s*(?::<[^=]*>)?\s*(?:=|{)/g
let match
while ((match = defPattern.exec(content)) !== null) {
const name = match[1]
const startIndex = match.index
const lineNumber = content.substring(0, startIndex).split('\n').length
// Find the 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
}
}
if (bodyEnd > bodyStart) {
const body = content.substring(bodyStart + 1, bodyEnd)
const isComponent = name[0] === name[0].toUpperCase() && filePath.includes('component')
const analysis = analyzeBody(body, name, isComponent ? 'component' : 'function', filePath, lineNumber)
if (analysis.flags.length > 0 || analysis.completeness < 50) {
results.push(analysis)
}
}
}
} catch (e) {
// Skip
}
}
function analyzeBody(body: string, name: string, type: 'component' | 'function', filePath: string, lineNumber: number): ComponentAnalysis {
const lines = body.split('\n').filter(l => l.trim().length > 0)
const returnLines = lines.filter(l => l.includes('return')).length
const jsxLines = lines.filter(l => l.match(/<[A-Z]/)).length
const logicalLines = lines.filter(l =>
!l.match(/^\/\//) &&
!l.match(/^\*/) &&
l.trim().length > 0 &&
!l.includes('return')
).length
const flags: string[] = []
let completeness = 100
// Check for stub indicators
if (body.includes('TODO') || body.includes('FIXME')) {
flags.push('has-todo-comments')
completeness -= 20
}
if (body.match(/throw\s+new\s+Error\s*\(\s*['"]not\s+implemented/i)) {
flags.push('throws-not-implemented')
completeness = 0
}
if (body.includes('console.log') && logicalLines <= 1) {
flags.push('only-console-log')
completeness -= 30
}
if (body.match(/return\s+(null|undefined|{}\s*;)/)) {
flags.push('returns-empty-value')
completeness -= 25
}
if (body.match(/\/\/\s*(mock|stub|placeholder)/i)) {
flags.push('marked-as-mock')
completeness -= 40
}
if (type === 'component' && jsxLines === 0) {
flags.push('component-no-jsx')
completeness -= 50
}
if (body.match(/<>\s*<\/>/)) {
flags.push('empty-fragment')
completeness -= 50
}
if (logicalLines <= 1 && returnLines === 1) {
flags.push('minimal-body')
completeness -= 30
}
if (body.match(/return\s+\{[^}]*\}\s*\/\/\s*(mock|stub|example|placeholder)/i)) {
flags.push('mock-data-return')
completeness -= 40
}
// Determine severity
let severity: 'critical' | 'high' | 'medium' | 'low' | 'info' = 'info'
if (completeness === 0) severity = 'critical'
else if (completeness < 30) severity = 'high'
else if (completeness < 60) severity = 'medium'
else if (completeness < 80) severity = 'low'
const summary = flags.length > 0
? `Potential stub: ${flags.join(', ')}`
: `Low implementation density (${completeness}%)`
return {
file: filePath,
line: lineNumber,
name,
type,
returnLines,
jsxLines,
logicalLines,
completeness: Math.max(0, Math.min(100, completeness)),
flags,
severity,
summary
}
}
// Main execution
const analyses = analyzeImplementations()
const bySeverity = {
critical: analyses.filter(a => a.severity === 'critical'),
high: analyses.filter(a => a.severity === 'high'),
medium: analyses.filter(a => a.severity === 'medium'),
low: analyses.filter(a => a.severity === 'low')
}
const summary = {
totalAnalyzed: analyses.length,
bySeverity: {
critical: bySeverity.critical.length,
high: bySeverity.high.length,
medium: bySeverity.medium.length,
low: bySeverity.low.length
},
flagTypes: {
'has-todo-comments': analyses.filter(a => a.flags.includes('has-todo-comments')).length,
'throws-not-implemented': analyses.filter(a => a.flags.includes('throws-not-implemented')).length,
'only-console-log': analyses.filter(a => a.flags.includes('only-console-log')).length,
'returns-empty-value': analyses.filter(a => a.flags.includes('returns-empty-value')).length,
'marked-as-mock': analyses.filter(a => a.flags.includes('marked-as-mock')).length,
'component-no-jsx': analyses.filter(a => a.flags.includes('component-no-jsx')).length,
'empty-fragment': analyses.filter(a => a.flags.includes('empty-fragment')).length,
'minimal-body': analyses.filter(a => a.flags.includes('minimal-body')).length,
'mock-data-return': analyses.filter(a => a.flags.includes('mock-data-return')).length
},
averageCompleteness: (analyses.reduce((sum, a) => sum + a.completeness, 0) / analyses.length).toFixed(1),
criticalStubs: bySeverity.critical.map(a => ({
file: a.file,
line: a.line,
name: a.name,
type: a.type,
flags: a.flags,
summary: a.summary
})),
details: analyses.sort((a, b) => {
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 }
return severityOrder[a.severity] - severityOrder[b.severity]
}).slice(0, 50), // Top 50 issues
timestamp: new Date().toISOString()
}
const serialized = JSON.stringify(summary, null, 2)
const outputPath = process.argv[2] || 'implementation-analysis.json'
try {
writeFileSync(outputPath, serialized)
console.error(`Implementation analysis written to ${outputPath}`)
} catch (error) {
console.error(`Failed to write implementation analysis to ${outputPath}:`, error)
}
console.log(serialized)