Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | 1x 1x 1x 9x 9x 9x 9x 9x 9x 9x 29x 29x 29x 7x 7x 7x 5x 9x 1x 1x | #!/usr/bin/env node
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* lint-src-todos.js — Fails CI when `TODO:`, `FIXME:`, or `XXX:`
* markers appear in `src/**\/*.ts` without a tracking issue link in
* the canonical `(#NNNN)` form.
*
* Why this exists. Architecture splits and SEO follow-ups land via
* code comments that say "deferred to follow-up". When those comments
* lose their tracking issue we forget the work. This script makes the
* issue link compulsory: every long-lived comment marker must be
* grep-traceable back to a ticket.
*
* Allowed forms (each must include a GitHub issue number):
* - `TODO(#1988): defer X to follow-up PR`
* - `FIXME(#42): coerce Y to UTC before compare`
* - `XXX(#7): legacy compat — remove after 2027-Q1`
*
* Rejected forms (require a tracking issue, fail CI):
* - `TODO: defer X` — no `(#NNNN)` issue link
* - `FIXME handle null case` — no `(#NNNN)` issue link
* - `XXX(legacy)` — parenthetical present but not the `(#NNNN)` form
*
* Scope. Only the upper-case spellings `TODO`, `FIXME`, and `XXX`
* followed by `:`, `(`, or whitespace are matched. Lower-case forms
* like `// fixme later` and identifiers like `todoList` are *not*
* flagged — they generate too many false positives across the
* codebase and aren't the editorial markers this guard is targeting.
*
* Exit codes:
* 0 — no marker, or every marker carries `(#NNNN)`
* 1 — at least one marker is missing an issue link
*
* Usage:
* node scripts/lint-src-todos.js
* npm run lint:src-todos
*
* Allowlist:
* Genuine regex/string literals that *contain* `TODO:` must be
* exempt — see `LITERAL_ALLOWLIST` below. Keep the list short and
* each entry must include the file + the literal's purpose.
*/
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
const SRC = fileURLToPath(new URL('../src/', import.meta.url));
// Allowlist: regex literals or string constants that *contain* TODO/FIXME
// markers but are not themselves tracked items. Keep one-line, file-scoped.
const LITERAL_ALLOWLIST = new Set([
'workflows/completeness-gate/constants.ts',
]);
// Match the markers when followed by a colon. Capture the surrounding
// content so the error message can include line context.
const MARKER_RE = /\b(TODO|FIXME|XXX)\b\s*(?:\(#(\d+)\))?\s*[:( ]/g;
function walk(dir) {
const out = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const st = statSync(full);
if (st.isDirectory()) out.push(...walk(full));
else if (full.endsWith('.ts')) out.push(full);
}
return out;
}
export function findOffenders(files = walk(SRC)) {
const offenders = [];
for (const file of files) {
const rel = file.slice(SRC.length).replaceAll('\\', '/');
Iif (LITERAL_ALLOWLIST.has(rel)) continue;
const src = readFileSync(file, 'utf8');
const lines = src.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Re-anchor each iteration so /g state doesn't leak between lines.
const re = new RegExp(MARKER_RE.source, 'g');
let m;
while ((m = re.exec(line))) {
const marker = m[1];
const issueNumber = m[2];
if (!issueNumber) {
offenders.push({
file: rel,
line: i + 1,
marker,
text: line.trim(),
});
}
}
}
}
return offenders;
}
function main() {
const offenders = findOffenders();
if (offenders.length === 0) {
console.log('✅ lint-src-todos: every TODO/FIXME/XXX carries a (#NNNN) issue link');
return 0;
}
console.error(
`❌ lint-src-todos: ${offenders.length} marker(s) missing a (#NNNN) issue link:`
);
for (const o of offenders) {
console.error(` ${o.file}:${o.line} ${o.marker}: ${o.text}`);
}
console.error(
'\nAdd a tracking issue and rewrite the marker as `TODO(#NNNN): …` or remove it.'
);
return 1;
}
// Run as CLI only when invoked directly.
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
Iif (isMain) {
process.exit(main());
}
|