mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 06:14:59 +00:00
231 lines
7.0 KiB
TypeScript
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)
|