refactor: modularize stub tooling

This commit is contained in:
2025-12-27 18:20:45 +00:00
parent b20011a21e
commit 6447e7a203
16 changed files with 736 additions and 732 deletions

View File

@@ -1,332 +1,17 @@
#!/usr/bin/env node
/**
* Function-to-Test Coverage Analyzer
*
* This script:
* 1. Scans all TypeScript/TSX source files for exported functions
* 2. Scans all test files for test cases
* 3. Maps functions to tests
* 4. Reports functions without tests
* 5. Generates a coverage report
*/
import { analyzeCoverage } from './analyze-test-coverage/coverage-runner'
import { printReport, writeJsonReport } from './analyze-test-coverage/reporter'
import * as fs from "fs";
import * as path from "path";
import { glob } from "glob";
interface FunctionDef {
name: string;
file: string;
type: "named" | "default" | "class-method";
line: number;
}
interface TestCase {
name: string;
file: string;
functions: string[];
line: number;
}
interface CoverageReport {
totalFunctions: number;
testedFunctions: number;
untested: FunctionDef[];
tested: Map<string, TestCase[]>;
coverage: number;
}
// Configuration
const CONFIG = {
srcPatterns: [
"src/**/*.ts",
"src/**/*.tsx",
"packages/**/src/**/*.ts",
"packages/**/src/**/*.tsx",
"dbal/development/**/*.ts",
],
testPatterns: [
"src/**/*.test.ts",
"src/**/*.test.tsx",
"packages/**/tests/**/*.test.ts",
"packages/**/tests/**/*.test.tsx",
"dbal/**/*.test.ts",
],
ignorePatterns: [
"**/node_modules/**",
"**/.next/**",
"**/build/**",
"**/*.d.ts",
"**/dist/**",
],
};
// Extract function names from source code
function extractFunctions(content: string, file: string): FunctionDef[] {
const functions: FunctionDef[] = [];
const lines = content.split("\n");
lines.forEach((line, index) => {
const lineNum = index + 1;
// Named exports: export function name() or export const name = () =>
const namedFuncMatch = line.match(
/export\s+(?:function|const)\s+(\w+)\s*(?:\(|=)/
);
if (namedFuncMatch) {
functions.push({
name: namedFuncMatch[1],
file,
type: "named",
line: lineNum,
});
}
// Class methods: methodName() { or methodName = () => {
const classMethodMatch = line.match(/^\s+(\w+)\s*\(.*\)\s*[:{]/);
if (classMethodMatch && line.includes("class")) {
functions.push({
name: classMethodMatch[1],
file,
type: "class-method",
line: lineNum,
});
}
// Default exports
if (line.includes("export default") && line.includes("function")) {
const defaultMatch = line.match(/export\s+default\s+function\s+(\w+)/);
if (defaultMatch) {
functions.push({
name: defaultMatch[1],
file,
type: "default",
line: lineNum,
});
}
}
});
return functions;
}
// Extract test cases and mentioned functions
function extractTestCases(
content: string,
file: string
): Map<string, string[]> {
const testMap = new Map<string, string[]>();
const lines = content.split("\n");
let currentTestName = "";
lines.forEach((line) => {
// Detect test blocks: it(), test(), describe()
const testMatch = line.match(
/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/
);
if (testMatch) {
currentTestName = testMatch[1];
testMap.set(currentTestName, []);
}
if (currentTestName) {
// Look for function calls within the test
const funcCalls = line.match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g);
if (funcCalls) {
funcCalls.forEach((call) => {
const funcName = call.replace(/\s*\($/, "");
if (funcName && !isCommonTestHelper(funcName)) {
testMap.get(currentTestName)!.push(funcName);
}
});
}
}
});
return testMap;
}
// Filter out common test helpers
function isCommonTestHelper(name: string): boolean {
const helpers = [
"it",
"test",
"describe",
"expect",
"beforeEach",
"afterEach",
"beforeAll",
"afterAll",
"jest",
"vi",
"assert",
"eq",
"ok",
"throws",
"doesNotThrow",
"async",
"render",
"screen",
"fireEvent",
"userEvent",
"waitFor",
"within",
];
return helpers.includes(name);
}
// Main analysis function
async function analyze(): Promise<CoverageReport> {
const cwd = process.cwd();
// Get all source files
const srcFiles = await glob(CONFIG.srcPatterns, {
cwd,
ignore: CONFIG.ignorePatterns,
});
// Get all test files
const testFiles = await glob(CONFIG.testPatterns, {
cwd,
ignore: CONFIG.ignorePatterns,
});
// Extract functions from source files
const allFunctions: FunctionDef[] = [];
for (const file of srcFiles) {
try {
const content = fs.readFileSync(path.join(cwd, file), "utf-8");
const funcs = extractFunctions(content, file);
allFunctions.push(...funcs);
} catch (e) {
console.warn(`Error reading ${file}:`, (e as Error).message);
}
}
// Extract test cases and create mapping
const testMapping = new Map<string, TestCase[]>();
const testedFunctionNames = new Set<string>();
for (const file of testFiles) {
try {
const content = fs.readFileSync(path.join(cwd, file), "utf-8");
const testCases = extractTestCases(content, file);
testCases.forEach((functionNames, testName) => {
functionNames.forEach((funcName) => {
testedFunctionNames.add(funcName);
if (!testMapping.has(funcName)) {
testMapping.set(funcName, []);
}
testMapping.get(funcName)!.push({
name: testName,
file,
functions: functionNames,
line: 0,
});
});
});
} catch (e) {
console.warn(`Error reading ${file}:`, (e as Error).message);
}
}
// Identify untested functions
const untested = allFunctions.filter((f) => !testedFunctionNames.has(f.name));
// Calculate coverage
const coverage = (
((allFunctions.length - untested.length) / allFunctions.length) *
100
).toFixed(2);
return {
totalFunctions: allFunctions.length,
testedFunctions: allFunctions.length - untested.length,
untested,
tested: testMapping,
coverage: parseFloat(coverage),
};
}
// Generate report
async function generateReport() {
const run = async () => {
try {
const report = await analyze();
console.log("\n" + "=".repeat(70));
console.log("FUNCTION-TO-TEST COVERAGE ANALYSIS");
console.log("=".repeat(70));
console.log(`\nSummary:`);
console.log(` Total Functions: ${report.totalFunctions}`);
console.log(` Tested Functions: ${report.testedFunctions}`);
console.log(` Untested Functions: ${report.untested.length}`);
console.log(` Coverage: ${report.coverage}%`);
if (report.untested.length > 0) {
console.log(`\n${"─".repeat(70)}`);
console.log("UNTESTED FUNCTIONS:");
console.log(`${"─".repeat(70)}`);
// Group by file
const grouped = new Map<string, FunctionDef[]>();
report.untested.forEach((fn) => {
if (!grouped.has(fn.file)) {
grouped.set(fn.file, []);
}
grouped.get(fn.file)!.push(fn);
});
grouped.forEach((fns, file) => {
console.log(`\n📄 ${file}`);
fns.forEach((fn) => {
console.log(
` └─ ${fn.name} (${fn.type}) [line ${fn.line}]`
);
});
});
// Generate TODO items
console.log(`\n${"─".repeat(70)}`);
console.log("TODO - CREATE TESTS FOR:");
console.log(`${"─".repeat(70)}`);
report.untested.forEach((fn) => {
console.log(`- [ ] Write test for \`${fn.name}\` in ${fn.file}`);
});
}
console.log("\n" + "=".repeat(70) + "\n");
// Generate JSON report
const reportPath = path.join(process.cwd(), "coverage-report.json");
fs.writeFileSync(
reportPath,
JSON.stringify(
{
timestamp: new Date().toISOString(),
summary: {
totalFunctions: report.totalFunctions,
testedFunctions: report.testedFunctions,
untestedFunctions: report.untested.length,
coverage: report.coverage,
},
untested: report.untested,
},
null,
2
)
);
console.log(`✅ Report saved to: coverage-report.json`);
const report = await analyzeCoverage()
printReport(report)
writeJsonReport(report)
} catch (e) {
console.error("Error analyzing coverage:", e);
process.exit(1);
console.error('Error analyzing coverage:', e)
process.exit(1)
}
}
// Run the analysis
generateReport();
run()

View File

@@ -0,0 +1,17 @@
export const COVERAGE_CONFIG = {
srcPatterns: [
'src/**/*.ts',
'src/**/*.tsx',
'packages/**/src/**/*.ts',
'packages/**/src/**/*.tsx',
'dbal/development/**/*.ts'
],
testPatterns: [
'src/**/*.test.ts',
'src/**/*.test.tsx',
'packages/**/tests/**/*.test.ts',
'packages/**/tests/**/*.test.tsx',
'dbal/**/*.test.ts'
],
ignorePatterns: ['**/node_modules/**', '**/.next/**', '**/build/**', '**/*.d.ts', '**/dist/**']
}

View File

@@ -0,0 +1,65 @@
import * as fs from 'fs'
import * as path from 'path'
import { glob } from 'glob'
import { COVERAGE_CONFIG } from './config'
import { extractFunctions, FunctionDef } from './function-extractor'
import { extractTestCases, TestCase } from './test-extractor'
export interface CoverageReport {
totalFunctions: number
testedFunctions: number
untested: FunctionDef[]
tested: Map<string, TestCase[]>
coverage: number
}
export const analyzeCoverage = async (): Promise<CoverageReport> => {
const cwd = process.cwd()
const srcFiles = await glob(COVERAGE_CONFIG.srcPatterns, { cwd, ignore: COVERAGE_CONFIG.ignorePatterns })
const testFiles = await glob(COVERAGE_CONFIG.testPatterns, { cwd, ignore: COVERAGE_CONFIG.ignorePatterns })
const allFunctions: FunctionDef[] = []
for (const file of srcFiles) {
try {
const content = fs.readFileSync(path.join(cwd, file), 'utf-8')
const funcs = extractFunctions(content, file)
allFunctions.push(...funcs)
} catch (e) {
console.warn(`Error reading ${file}:`, (e as Error).message)
}
}
const testMapping = new Map<string, TestCase[]>()
const testedFunctionNames = new Set<string>()
for (const file of testFiles) {
try {
const content = fs.readFileSync(path.join(cwd, file), 'utf-8')
const testCases = extractTestCases(content, file)
testCases.forEach((functionNames, testName) => {
functionNames.forEach(funcName => {
testedFunctionNames.add(funcName)
if (!testMapping.has(funcName)) {
testMapping.set(funcName, [])
}
testMapping.get(funcName)!.push({ name: testName, file, functions: functionNames, line: 0 })
})
})
} catch (e) {
console.warn(`Error reading ${file}:`, (e as Error).message)
}
}
const untested = allFunctions.filter(f => !testedFunctionNames.has(f.name))
const coverage = (((allFunctions.length - untested.length) / allFunctions.length) * 100).toFixed(2)
return {
totalFunctions: allFunctions.length,
testedFunctions: allFunctions.length - untested.length,
untested,
tested: testMapping,
coverage: parseFloat(coverage)
}
}

View File

@@ -0,0 +1,49 @@
export interface FunctionDef {
name: string
file: string
type: 'named' | 'default' | 'class-method'
line: number
}
export const extractFunctions = (content: string, file: string): FunctionDef[] => {
const functions: FunctionDef[] = []
const lines = content.split('\n')
lines.forEach((line, index) => {
const lineNum = index + 1
const namedFuncMatch = line.match(/export\s+(?:function|const)\s+(\w+)\s*(?:\(|=)/)
if (namedFuncMatch) {
functions.push({
name: namedFuncMatch[1],
file,
type: 'named',
line: lineNum
})
}
const classMethodMatch = line.match(/^\s+(\w+)\s*\(.*\)\s*[:{]/)
if (classMethodMatch && line.includes('class')) {
functions.push({
name: classMethodMatch[1],
file,
type: 'class-method',
line: lineNum
})
}
if (line.includes('export default') && line.includes('function')) {
const defaultMatch = line.match(/export\s+default\s+function\s+(\w+)/)
if (defaultMatch) {
functions.push({
name: defaultMatch[1],
file,
type: 'default',
line: lineNum
})
}
}
})
return functions
}

View File

@@ -0,0 +1,68 @@
import * as fs from 'fs'
import * as path from 'path'
import { CoverageReport, FunctionDef } from './coverage-runner'
export const printReport = (report: CoverageReport) => {
console.log('\n' + '='.repeat(70))
console.log('FUNCTION-TO-TEST COVERAGE ANALYSIS')
console.log('='.repeat(70))
console.log(`\nSummary:`)
console.log(` Total Functions: ${report.totalFunctions}`)
console.log(` Tested Functions: ${report.testedFunctions}`)
console.log(` Untested Functions: ${report.untested.length}`)
console.log(` Coverage: ${report.coverage}%`)
if (report.untested.length > 0) {
console.log(`\n${'─'.repeat(70)}`)
console.log('UNTESTED FUNCTIONS:')
console.log(`${'─'.repeat(70)}`)
const grouped = new Map<string, FunctionDef[]>()
report.untested.forEach(fn => {
if (!grouped.has(fn.file)) {
grouped.set(fn.file, [])
}
grouped.get(fn.file)!.push(fn)
})
grouped.forEach((fns, file) => {
console.log(`\n📄 ${file}`)
fns.forEach(fn => {
console.log(` └─ ${fn.name} (${fn.type}) [line ${fn.line}]`)
})
})
console.log(`\n${'─'.repeat(70)}`)
console.log('TODO - CREATE TESTS FOR:')
console.log(`${'─'.repeat(70)}`)
report.untested.forEach(fn => {
console.log(`- [ ] Write test for \`${fn.name}\` in ${fn.file}`)
})
}
console.log('\n' + '='.repeat(70) + '\n')
}
export const writeJsonReport = (report: CoverageReport) => {
const reportPath = path.join(process.cwd(), 'coverage-report.json')
fs.writeFileSync(
reportPath,
JSON.stringify(
{
timestamp: new Date().toISOString(),
summary: {
totalFunctions: report.totalFunctions,
testedFunctions: report.testedFunctions,
untestedFunctions: report.untested.length,
coverage: report.coverage
},
untested: report.untested
},
null,
2
)
)
console.log(`✅ Report saved to: coverage-report.json`)
}

View File

@@ -0,0 +1,62 @@
export interface TestCase {
name: string
file: string
functions: string[]
line: number
}
export const extractTestCases = (content: string, file: string): Map<string, string[]> => {
const testMap = new Map<string, string[]>()
const lines = content.split('\n')
let currentTestName = ''
lines.forEach(line => {
const testMatch = line.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/)
if (testMatch) {
currentTestName = testMatch[1]
testMap.set(currentTestName, [])
}
if (currentTestName) {
const funcCalls = line.match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g)
if (funcCalls) {
funcCalls.forEach(call => {
const funcName = call.replace(/\s*\($/, '')
if (funcName && !isCommonTestHelper(funcName)) {
testMap.get(currentTestName)!.push(funcName)
}
})
}
}
})
return testMap
}
const isCommonTestHelper = (name: string): boolean => {
const helpers = [
'it',
'test',
'describe',
'expect',
'beforeEach',
'afterEach',
'beforeAll',
'afterAll',
'jest',
'vi',
'assert',
'eq',
'ok',
'throws',
'doesNotThrow',
'async',
'render',
'screen',
'fireEvent',
'userEvent',
'waitFor',
'within'
]
return helpers.includes(name)
}

View File

@@ -1,215 +1,14 @@
#!/usr/bin/env tsx
import { readdirSync, readFileSync, statSync, writeFileSync } from 'fs'
import { join, extname } from 'path'
import { collectStubs } from './stub-lambdas/directory-walker'
import { summarizeStubs, writeSummary } from './stub-lambdas/reporter'
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 rootDir = 'src'
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)
}
const stubs = collectStubs(rootDir)
const summary = summarizeStubs(stubs)
console.log(serialized)
writeSummary(summary, outputPath)
console.log(JSON.stringify(summary, null, 2))

View File

@@ -0,0 +1,31 @@
import { readdirSync, statSync } from 'fs'
import { join } from 'path'
import { StubLocation } from './patterns'
import { scanFile } from './file-scanner'
import { isCodeFile } from './patterns'
export const collectStubs = (rootDir: string): StubLocation[] => {
const results: StubLocation[] = []
const 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 (isCodeFile(file)) {
scanFile(fullPath, results)
}
}
} catch {
// Skip inaccessible directories
}
}
walkDir(rootDir)
return results
}

View File

@@ -0,0 +1,72 @@
import { readFileSync } from 'fs'
import { StubLocation, STUB_PATTERNS } from './patterns'
export const scanFile = (filePath: string, results: StubLocation[]): void => {
try {
const content = readFileSync(filePath, 'utf8')
const lines = content.split('\n')
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
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)
checkPatterns(functionBody, filePath, lineNumber, functionName, results)
}
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 {
// Skip files that can't be analyzed
}
}
const 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,
severity: pattern.severity,
code: body.split('\n').slice(0, 3).join('\n').substring(0, 80)
})
}
}
}

View File

@@ -0,0 +1,81 @@
import { extname } from 'path'
export 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
}
export 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|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'
}
]
export const isCodeFile = (file: string): boolean =>
['.ts', '.tsx', '.js', '.jsx'].includes(extname(file))
export const severityOrder: Record<'high' | 'medium' | 'low', number> = {
high: 0,
medium: 1,
low: 2
}

View File

@@ -0,0 +1,57 @@
import { writeFileSync } from 'fs'
import { StubLocation, severityOrder } from './patterns'
export interface StubSummary {
totalStubsFound: number
bySeverity: Record<'high' | 'medium' | 'low', number>
byType: Record<StubLocation['type'], number>
criticalIssues: Array<{ file: string; line: number; function: string; type: StubLocation['type'] }>
details: StubLocation[]
timestamp: string
}
export const summarizeStubs = (stubs: StubLocation[]): StubSummary => {
const bySeverity = {
high: stubs.filter(s => s.severity === 'high'),
medium: stubs.filter(s => s.severity === 'medium'),
low: stubs.filter(s => s.severity === 'low')
}
return {
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) => severityOrder[a.severity] - severityOrder[b.severity]),
timestamp: new Date().toISOString()
}
}
export const writeSummary = (summary: StubSummary, outputPath: string): void => {
const serialized = JSON.stringify(summary, null, 2)
try {
writeFileSync(outputPath, serialized)
console.error(`Stub summary written to ${outputPath}`)
} catch (error) {
console.error(`Failed to write stub summary to ${outputPath}:`, error)
}
}

View File

@@ -1,204 +1,10 @@
#!/usr/bin/env tsx
import { existsSync, readFileSync } from 'fs'
import { generateStubReport } from './stub-report/report-builder'
function generateStubReport(): string {
let report = '# Stub Implementation Detection Report\n\n'
report += '## Overview\n\n'
report += 'This report identifies incomplete, placeholder, or stubbed implementations in the codebase.\n'
report += 'Stubs should be replaced with real implementations before production use.\n\n'
// Load pattern detection results
if (existsSync('stub-patterns.json')) {
try {
const patterns = JSON.parse(readFileSync('stub-patterns.json', 'utf8'))
report += '## Pattern-Based Detection Results\n\n'
report += `**Total Stubs Found**: ${patterns.totalStubsFound}\n\n`
// Severity
report += '### By Severity\n\n'
report += `- 🔴 **Critical**: ${patterns.bySeverity.high} (blocks production)\n`
report += `- 🟠 **Medium**: ${patterns.bySeverity.medium} (should be fixed)\n`
report += `- 🟡 **Low**: ${patterns.bySeverity.low} (nice to fix)\n\n`
// Types
report += '### By Type\n\n'
for (const [type, count] of Object.entries(patterns.byType)) {
if (count > 0) {
report += `- **${type}**: ${count}\n`
}
}
report += '\n'
// Critical issues
if (patterns.criticalIssues && patterns.criticalIssues.length > 0) {
report += '### 🔴 Critical Stubs\n\n'
report += 'These must be implemented before production:\n\n'
report += '| File | Line | Function | Type |\n'
report += '|------|------|----------|------|\n'
patterns.criticalIssues.slice(0, 20).forEach(issue => {
report += `| \`${issue.file}\` | ${issue.line} | \`${issue.function}\` | ${issue.type} |\n`
})
report += '\n'
}
// Top findings
if (patterns.details && patterns.details.length > 0) {
report += '### Detailed Findings\n\n'
report += '<details><summary>Click to expand (showing first 15)</summary>\n\n'
report += '| File | Line | Function | Type | Code Preview |\n'
report += '|------|------|----------|------|---------------|\n'
patterns.details.slice(0, 15).forEach(item => {
const preview = item.code?.substring(0, 50)?.replace(/\n/g, ' ') || 'N/A'
report += `| ${item.file} | ${item.line} | ${item.name} | ${item.type} | \`${preview}...\` |\n`
})
report += '\n</details>\n\n'
}
} catch (e) {
report += '⚠️ Could not parse pattern detection results.\n\n'
}
}
// Load completeness analysis
if (existsSync('implementation-analysis.json')) {
try {
const completeness = JSON.parse(readFileSync('implementation-analysis.json', 'utf8'))
report += '## Implementation Completeness Analysis\n\n'
report += `**Average Completeness Score**: ${completeness.averageCompleteness}%\n\n`
// Breakdown
report += '### Completeness Levels\n\n'
report += `- **Critical** (0% complete): ${completeness.bySeverity.critical}\n`
report += `- **High** (10-30% complete): ${completeness.bySeverity.high}\n`
report += `- **Medium** (40-70% complete): ${completeness.bySeverity.medium}\n`
report += `- **Low** (80-99% complete): ${completeness.bySeverity.low}\n\n`
// Flag types
if (Object.values(completeness.flagTypes).some(v => v > 0)) {
report += '### Common Stub Indicators\n\n'
for (const [flag, count] of Object.entries(completeness.flagTypes)) {
if (count > 0) {
report += `- **${flag}**: ${count} instances\n`
}
}
report += '\n'
}
// Critical stubs
if (completeness.criticalStubs && completeness.criticalStubs.length > 0) {
report += '### 🔴 Incomplete Implementations (0% Completeness)\n\n'
report += '<details><summary>Click to expand</summary>\n\n'
completeness.criticalStubs.forEach(stub => {
report += `#### \`${stub.name}\` in \`${stub.file}:${stub.line}\`\n`
report += `**Type**: ${stub.type}\n`
report += `**Flags**: ${stub.flags.join(', ')}\n`
report += `**Summary**: ${stub.summary}\n\n`
})
report += '</details>\n\n'
}
} catch (e) {
report += '⚠️ Could not parse completeness analysis.\n\n'
}
}
// Recommendations
report += '## How to Fix Stub Implementations\n\n'
report += '### Pattern: "Not Implemented" Errors\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function processData(data) {\n'
report += ' throw new Error("not implemented")\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export function processData(data) {\n'
report += ' return data.map(item => transform(item))\n'
report += '}\n'
report += '```\n\n'
report += '### Pattern: Console.log Only\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function validateEmail(email) {\n'
report += ' console.log("validating:", email)\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export function validateEmail(email: string): boolean {\n'
report += ' return /^[^@]+@[^@]+\\.\\w+$/.test(email)\n'
report += '}\n'
report += '```\n\n'
report += '### Pattern: Return null/undefined\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function fetchUserData(id: string) {\n'
report += ' return null // TODO: implement API call\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export async function fetchUserData(id: string): Promise<User> {\n'
report += ' const response = await fetch(`/api/users/${id}`)\n'
report += ' return response.json()\n'
report += '}\n'
report += '```\n\n'
report += '### Pattern: Placeholder Component\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function Dashboard() {\n'
report += ' return <div>TODO: Build dashboard</div>\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export function Dashboard() {\n'
report += ' return (\n'
report += ' <div className="dashboard">\n'
report += ' <Header />\n'
report += ' <MainContent />\n'
report += ' <Sidebar />\n'
report += ' </div>\n'
report += ' )\n'
report += '}\n'
report += '```\n\n'
report += '### Pattern: Mock Data\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function getUsers() {\n'
report += ' return [ // stub data\n'
report += ' { id: 1, name: "John" },\n'
report += ' { id: 2, name: "Jane" }\n'
report += ' ]\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export async function getUsers(): Promise<User[]> {\n'
report += ' const response = await Database.query("SELECT * FROM users")\n'
report += ' return response.map(row => new User(row))\n'
report += '}\n'
report += '```\n\n'
report += '## Checklist for Implementation\n\n'
report += '- [ ] All critical stubs have been implemented\n'
report += '- [ ] Functions have proper type signatures\n'
report += '- [ ] Components render actual content (not placeholders)\n'
report += '- [ ] All TODO/FIXME comments reference GitHub issues\n'
report += '- [ ] Mock data is replaced with real data sources\n'
report += '- [ ] Error handling is in place\n'
report += '- [ ] Functions are tested with realistic inputs\n'
report += '- [ ] Documentation comments are added (JSDoc)\n\n'
report += '## Best Practices\n\n'
report += '1. **Never commit stubs to main** - Use feature branches and require reviews\n'
report += '2. **Use TypeScript types** - Force implementations by using specific return types\n'
report += '3. **Convert stubs to issues** - Don\'t use TODO in code, create GitHub issues\n'
report += '4. **Test from the start** - Write tests before implementing\n'
report += '5. **Use linting rules** - Configure ESLint to catch console.log and TODO\n\n'
report += `---\n\n`
report += `**Generated**: ${new Date().toISOString()}\n`
return report
function main() {
const report = generateStubReport()
console.log(report)
}
console.log(generateStubReport())
main()

View File

@@ -0,0 +1,43 @@
import { existsSync, readFileSync } from 'fs'
export const buildCompletenessSection = (): string => {
if (!existsSync('implementation-analysis.json')) return ''
try {
const completeness = JSON.parse(readFileSync('implementation-analysis.json', 'utf8'))
let section = '## Implementation Completeness Analysis\n\n'
section += `**Average Completeness Score**: ${completeness.averageCompleteness}%\n\n`
section += '### Completeness Levels\n\n'
section += `- **Critical** (0% complete): ${completeness.bySeverity.critical}\n`
section += `- **High** (10-30% complete): ${completeness.bySeverity.high}\n`
section += `- **Medium** (40-70% complete): ${completeness.bySeverity.medium}\n`
section += `- **Low** (80-99% complete): ${completeness.bySeverity.low}\n\n`
if (Object.values(completeness.flagTypes).some((v: number) => v > 0)) {
section += '### Common Stub Indicators\n\n'
for (const [flag, count] of Object.entries(completeness.flagTypes)) {
if (count > 0) {
section += `- **${flag}**: ${count} instances\n`
}
}
section += '\n'
}
if (completeness.criticalStubs && completeness.criticalStubs.length > 0) {
section += '### 🔴 Incomplete Implementations (0% Completeness)\n\n'
section += '<details><summary>Click to expand</summary>\n\n'
completeness.criticalStubs.forEach((stub: any) => {
section += `#### \`${stub.name}\` in \`${stub.file}:${stub.line}\`\n`
section += `**Type**: ${stub.type}\n`
section += `**Flags**: ${stub.flags.join(', ')}\n`
section += `**Summary**: ${stub.summary}\n\n`
})
section += '</details>\n\n'
}
return section
} catch {
return '⚠️ Could not parse completeness analysis.\n\n'
}
}

View File

@@ -0,0 +1,101 @@
const recommendations = () => {
let report = '## How to Fix Stub Implementations\n\n'
report += '### Pattern: "Not Implemented" Errors\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function processData(data) {\n'
report += ' throw new Error("not implemented")\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export function processData(data) {\n'
report += ' return data.map(item => transform(item))\n'
report += '}\n'
report += '```\n\n'
report += '### Pattern: Console.log Only\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function validateEmail(email) {\n'
report += ' console.log("validating:", email)\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export function validateEmail(email: string): boolean {\n'
report += ' return /^[^@]+@[^@]+\\.\\w+$/.test(email)\n'
report += '}\n'
report += '```\n\n'
report += '### Pattern: Return null/undefined\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function fetchUserData(id: string) {\n'
report += ' return null // TODO: implement API call\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export async function fetchUserData(id: string): Promise<User> {\n'
report += ' const response = await fetch(`/api/users/${id}`)\n'
report += ' return response.json()\n'
report += '}\n'
report += '```\n\n'
report += '### Pattern: Placeholder Component\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function Dashboard() {\n'
report += ' return <div>TODO: Build dashboard</div>\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export function Dashboard() {\n'
report += ' return (\n'
report += ' <div className="dashboard">\n'
report += ' <Header />\n'
report += ' <MainContent />\n'
report += ' <Sidebar />\n'
report += ' </div>\n'
report += ' )\n'
report += '}\n'
report += '```\n\n'
report += '### Pattern: Mock Data\n\n'
report += '```typescript\n'
report += '// ❌ Stub\n'
report += 'export function getUsers() {\n'
report += ' return [ // stub data\n'
report += ' { id: 1, name: "John" },\n'
report += ' { id: 2, name: "Jane" }\n'
report += ' ]\n'
report += '}\n\n'
report += '// ✅ Real implementation\n'
report += 'export async function getUsers(): Promise<User[]> {\n'
report += ' const response = await Database.query("SELECT * FROM users")\n'
report += ' return response.map(row => new User(row))\n'
report += '}\n'
report += '```\n\n'
return report
}
const checklist = () => {
let report = '## Checklist for Implementation\n\n'
report += '- [ ] All critical stubs have been implemented\n'
report += '- [ ] Functions have proper type signatures\n'
report += '- [ ] Components render actual content (not placeholders)\n'
report += '- [ ] All TODO/FIXME comments reference GitHub issues\n'
report += '- [ ] Mock data is replaced with real data sources\n'
report += '- [ ] Error handling is in place\n'
report += '- [ ] Functions are tested with realistic inputs\n'
report += '- [ ] Documentation comments are added (JSDoc)\n\n'
return report
}
const bestPractices = () => {
let report = '## Best Practices\n\n'
report += '1. **Never commit stubs to main** - Use feature branches and require reviews\n'
report += '2. **Use TypeScript types** - Force implementations by using specific return types\n'
report += "3. **Convert stubs to issues** - Don't use TODO in code, create GitHub issues\n"
report += '4. **Test from the start** - Write tests before implementing\n'
report += '5. **Use linting rules** - Configure ESLint to catch console.log and TODO\n\n'
return report
}
export const buildGuidanceSections = (): string => recommendations() + checklist() + bestPractices()

View File

@@ -0,0 +1,50 @@
import { existsSync, readFileSync } from 'fs'
export const buildPatternSection = (): string => {
if (!existsSync('stub-patterns.json')) return ''
try {
const patterns = JSON.parse(readFileSync('stub-patterns.json', 'utf8'))
let section = '## Pattern-Based Detection Results\n\n'
section += `**Total Stubs Found**: ${patterns.totalStubsFound}\n\n`
section += '### By Severity\n\n'
section += `- 🔴 **Critical**: ${patterns.bySeverity.high} (blocks production)\n`
section += `- 🟠 **Medium**: ${patterns.bySeverity.medium} (should be fixed)\n`
section += `- 🟡 **Low**: ${patterns.bySeverity.low} (nice to fix)\n\n`
section += '### By Type\n\n'
for (const [type, count] of Object.entries(patterns.byType)) {
if (count > 0) {
section += `- **${type}**: ${count}\n`
}
}
section += '\n'
if (patterns.criticalIssues && patterns.criticalIssues.length > 0) {
section += '### 🔴 Critical Stubs\n\n'
section += 'These must be implemented before production:\n\n'
section += '| File | Line | Function | Type |\n'
section += '|------|------|----------|------|\n'
patterns.criticalIssues.slice(0, 20).forEach(issue => {
section += `| \`${issue.file}\` | ${issue.line} | \`${issue.function}\` | ${issue.type} |\n`
})
section += '\n'
}
if (patterns.details && patterns.details.length > 0) {
section += '### Detailed Findings\n\n'
section += '<details><summary>Click to expand (showing first 15)</summary>\n\n'
section += '| File | Line | Function | Type | Code Preview |\n'
section += '|------|------|----------|------|---------------|\n'
patterns.details.slice(0, 15).forEach(item => {
const preview = item.code?.substring(0, 50)?.replace(/\n/g, ' ') || 'N/A'
section += `| ${item.file} | ${item.line} | ${item.name} | ${item.type} | \`${preview}...\` |\n`
})
section += '\n</details>\n\n'
}
return section
} catch {
return '⚠️ Could not parse pattern detection results.\n\n'
}
}

View File

@@ -0,0 +1,18 @@
import { buildPatternSection } from './pattern-section'
import { buildCompletenessSection } from './completeness-section'
import { buildGuidanceSections } from './guidance-sections'
export const generateStubReport = (): string => {
let report = '# Stub Implementation Detection Report\n\n'
report += '## Overview\n\n'
report += 'This report identifies incomplete, placeholder, or stubbed implementations in the codebase.\n'
report += 'Stubs should be replaced with real implementations before production use.\n\n'
report += buildPatternSection()
report += buildCompletenessSection()
report += buildGuidanceSections()
report += `---\n\n`
report += `**Generated**: ${new Date().toISOString()}\n`
return report
}