mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
refactor: modularize stub tooling
This commit is contained in:
@@ -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()
|
||||
|
||||
17
tools/analysis/test/analyze-test-coverage/config.ts
Normal file
17
tools/analysis/test/analyze-test-coverage/config.ts
Normal 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/**']
|
||||
}
|
||||
65
tools/analysis/test/analyze-test-coverage/coverage-runner.ts
Normal file
65
tools/analysis/test/analyze-test-coverage/coverage-runner.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
68
tools/analysis/test/analyze-test-coverage/reporter.ts
Normal file
68
tools/analysis/test/analyze-test-coverage/reporter.ts
Normal 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`)
|
||||
}
|
||||
62
tools/analysis/test/analyze-test-coverage/test-extractor.ts
Normal file
62
tools/analysis/test/analyze-test-coverage/test-extractor.ts
Normal 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)
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
31
tools/detection/stub-lambdas/directory-walker.ts
Normal file
31
tools/detection/stub-lambdas/directory-walker.ts
Normal 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
|
||||
}
|
||||
72
tools/detection/stub-lambdas/file-scanner.ts
Normal file
72
tools/detection/stub-lambdas/file-scanner.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
81
tools/detection/stub-lambdas/patterns.ts
Normal file
81
tools/detection/stub-lambdas/patterns.ts
Normal 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
|
||||
}
|
||||
57
tools/detection/stub-lambdas/reporter.ts
Normal file
57
tools/detection/stub-lambdas/reporter.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
43
tools/generation/stub-report/completeness-section.ts
Normal file
43
tools/generation/stub-report/completeness-section.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
101
tools/generation/stub-report/guidance-sections.ts
Normal file
101
tools/generation/stub-report/guidance-sections.ts
Normal 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()
|
||||
50
tools/generation/stub-report/pattern-section.ts
Normal file
50
tools/generation/stub-report/pattern-section.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
18
tools/generation/stub-report/report-builder.ts
Normal file
18
tools/generation/stub-report/report-builder.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user