Exclude false positive TODOs from scan results: - todo-scan.js script's own markers (in comments/strings) - Documentation comments that mention TODO intentionally This ensures core code count accurately reflects production code TODOs. Verification: - Core code count now shows actual production TODOs only - Script's own markers excluded - Documentation comments excluded
217 lines
6.2 KiB
JavaScript
Executable File
217 lines
6.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Scans repo for TODO/FIXME markers and emits:
|
|
* - machine-readable JSON
|
|
* - human-readable markdown summary
|
|
*
|
|
* Output:
|
|
* - docs/TODO-CLASSIFICATION.md (overwritten)
|
|
* - docs/todo-scan.json
|
|
*
|
|
* Note: This script itself may contain "TODO" or "FIXME" in comments or strings.
|
|
* These are intentional and should be excluded from scan results.
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const ROOT = process.cwd();
|
|
|
|
const TARGET_DIRS = [
|
|
"src",
|
|
"ios/Plugin",
|
|
"ios/Tests",
|
|
"android/src/main",
|
|
"android/src/test",
|
|
"scripts",
|
|
"docs",
|
|
];
|
|
|
|
const EXCLUDE_DIR_NAMES = new Set([
|
|
".git",
|
|
"node_modules",
|
|
"dist",
|
|
"build",
|
|
".venv",
|
|
"venv",
|
|
"__pycache__",
|
|
]);
|
|
|
|
const FILE_EXTS = new Set([
|
|
".ts", ".tsx", ".js", ".jsx",
|
|
".swift",
|
|
".java", ".kt",
|
|
".md",
|
|
".json", ".yml", ".yaml",
|
|
".py",
|
|
]);
|
|
|
|
const MARKERS = ["TODO", "FIXME"];
|
|
|
|
function walk(dir, out = []) {
|
|
if (!fs.existsSync(dir)) return out;
|
|
const st = fs.statSync(dir);
|
|
if (!st.isDirectory()) return out;
|
|
|
|
for (const name of fs.readdirSync(dir)) {
|
|
if (EXCLUDE_DIR_NAMES.has(name)) continue;
|
|
const p = path.join(dir, name);
|
|
const s = fs.statSync(p);
|
|
if (s.isDirectory()) walk(p, out);
|
|
else out.push(p);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function scanFile(fp) {
|
|
const ext = path.extname(fp);
|
|
if (!FILE_EXTS.has(ext)) return [];
|
|
const text = fs.readFileSync(fp, "utf8");
|
|
const lines = text.split(/\r?\n/);
|
|
|
|
const hits = [];
|
|
const relPath = path.relative(ROOT, fp).replace(/\\/g, "/");
|
|
const isThisScript = relPath === "scripts/todo-scan.js";
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
for (const m of MARKERS) {
|
|
// require marker as a token-ish substring
|
|
if (line.includes(m + ":") || line.includes(m + " ")) {
|
|
// Exclude this script's own markers (in comments/strings)
|
|
if (isThisScript) continue;
|
|
// Exclude comment-only lines that are documentation
|
|
if (line.trim().startsWith("*") && line.includes("intentionally")) continue;
|
|
hits.push({ line: i + 1, marker: m, text: line.trim() });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return hits;
|
|
}
|
|
|
|
function bucketForPath(rel) {
|
|
if (rel.startsWith("ios/")) return "iOS";
|
|
if (rel.startsWith("android/")) return "Android";
|
|
if (rel.startsWith("src/")) return "TypeScript";
|
|
if (rel.startsWith("docs/")) return "Docs";
|
|
if (rel.startsWith("scripts/")) return "Scripts";
|
|
return "Other";
|
|
}
|
|
|
|
function isCoreCode(rel) {
|
|
// Core code directories (production code)
|
|
return (
|
|
rel.startsWith("ios/Plugin/") ||
|
|
rel.startsWith("android/src/main/") ||
|
|
rel.startsWith("src/") ||
|
|
rel.startsWith("packages/") ||
|
|
rel.startsWith("lib/") ||
|
|
(rel.startsWith("scripts/") && !rel.includes("test"))
|
|
);
|
|
}
|
|
|
|
function isDocsOrTestApp(rel) {
|
|
// Documentation and test harness directories
|
|
return (
|
|
rel.startsWith("docs/") ||
|
|
rel.startsWith("test-apps/") ||
|
|
rel.startsWith("ios/Tests/") ||
|
|
rel.startsWith("android/src/test/") ||
|
|
rel.startsWith("tests/")
|
|
);
|
|
}
|
|
|
|
function main() {
|
|
const results = [];
|
|
for (const d of TARGET_DIRS) {
|
|
const dirAbs = path.join(ROOT, d);
|
|
const files = walk(dirAbs);
|
|
for (const fpAbs of files) {
|
|
const rel = path.relative(ROOT, fpAbs).replace(/\\/g, "/");
|
|
const hits = scanFile(fpAbs);
|
|
for (const h of hits) results.push({ file: rel, ...h, bucket: bucketForPath(rel) });
|
|
}
|
|
}
|
|
|
|
// sort stable: bucket, file, line
|
|
results.sort((a, b) =>
|
|
a.bucket.localeCompare(b.bucket) ||
|
|
a.file.localeCompare(b.file) ||
|
|
a.line - b.line
|
|
);
|
|
|
|
// Split core vs docs/test-apps
|
|
const coreResults = results.filter(r => isCoreCode(r.file));
|
|
const docsTestResults = results.filter(r => isDocsOrTestApp(r.file));
|
|
const otherResults = results.filter(r => !isCoreCode(r.file) && !isDocsOrTestApp(r.file));
|
|
|
|
// Enhanced JSON output with split counts
|
|
const jsonOutput = {
|
|
summary: {
|
|
total: results.length,
|
|
coreCount: coreResults.length,
|
|
docsTestCount: docsTestResults.length,
|
|
otherCount: otherResults.length,
|
|
generatedAt: new Date().toISOString()
|
|
},
|
|
core: coreResults,
|
|
docsTest: docsTestResults,
|
|
other: otherResults,
|
|
all: results
|
|
};
|
|
|
|
fs.writeFileSync(path.join(ROOT, "docs/todo-scan.json"), JSON.stringify(jsonOutput, null, 2), "utf8");
|
|
|
|
// markdown
|
|
const byBucket = new Map();
|
|
for (const r of results) {
|
|
if (!byBucket.has(r.bucket)) byBucket.set(r.bucket, []);
|
|
byBucket.get(r.bucket).push(r);
|
|
}
|
|
|
|
let md = "";
|
|
md += `# TODO Classification (auto-generated)\n\n`;
|
|
md += `Generated by \`scripts/todo-scan.js\`\n\n`;
|
|
md += `## Summary\n\n`;
|
|
md += `- **Total markers:** ${results.length}\n`;
|
|
md += `- **Core code (production):** ${coreResults.length} ⚠️\n`;
|
|
md += `- **Docs/test-apps:** ${docsTestResults.length} ✅ (expected)\n`;
|
|
md += `- **Other:** ${otherResults.length}\n\n`;
|
|
md += `> **Note:** Core code TODOs should be near zero. Docs/test-app TODOs are expected and acceptable.\n\n`;
|
|
md += `---\n\n`;
|
|
|
|
for (const [bucket, items] of byBucket.entries()) {
|
|
md += `## ${bucket} (${items.length})\n\n`;
|
|
// group by file
|
|
const byFile = new Map();
|
|
for (const it of items) {
|
|
if (!byFile.has(it.file)) byFile.set(it.file, []);
|
|
byFile.get(it.file).push(it);
|
|
}
|
|
for (const [file, hits] of byFile.entries()) {
|
|
md += `### ${file}\n\n`;
|
|
for (const h of hits) {
|
|
md += `- L${h.line}: **${h.marker}** — ${h.text}\n`;
|
|
}
|
|
md += `\n`;
|
|
}
|
|
}
|
|
|
|
fs.writeFileSync(path.join(ROOT, "docs/TODO-CLASSIFICATION.md"), md, "utf8");
|
|
|
|
// Console output with split summary
|
|
console.log(`\n📊 TODO Scan Complete`);
|
|
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
console.log(`Total markers: ${results.length}`);
|
|
console.log(`Core code: ${coreResults.length} ${coreResults.length === 0 ? '✅' : '⚠️ (should be 0)'}`);
|
|
console.log(`Docs/test-apps: ${docsTestResults.length} ✅ (expected)`);
|
|
console.log(`Other: ${otherResults.length}`);
|
|
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
}
|
|
|
|
main();
|
|
|