Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions scripts/check-ts-allowlist.affine
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
//
Expand Down Expand Up @@ -88,6 +88,14 @@

type Exemption = { raw: String, rx: String }

fn normalizeRepoPath(p: String) -> String {
let mut out = p;
while (len(out) > 0 && (string_sub(out, 0, 1) == "." || string_sub(out, 0, 1) == "/")) {
out = string_sub(out, 1, len(out) - 1);
}
return out;
}

fn loadExemptionsFromClaudeMd() -> [Exemption] {
let mut exemptions = [];
let text = try {
Expand Down Expand Up @@ -116,11 +124,11 @@
i = i + 1;
continue;
}
if (inTable && len(line) > 0 && string_sub(line, 0, 1) == "|") {
if (regexMatch(line, "^\\|\\s*`[^`]+`")) {
if (inTable && len(line) > 0) {
if (regexMatch(line, "^\\s*\\|\\s*`[^`]+`")) {
let parts = split(line, "`");
if (len(parts) >= 3) {
let raw = parts[1];
let raw = normalizeRepoPath(trim(parts[1]));
exemptions = exemptions ++ [#{ raw: raw, rx: globToRegex(raw) }];
}
}
Expand All @@ -144,7 +152,7 @@
let lines_len = len(lines);
while (i < lines_len) {
let rawLine = lines[i];
let line = trim(rawLine);
let line = normalizeRepoPath(trim(rawLine));
if (line == "" || string_sub(line, 0, 1) == "#") {
i = i + 1;
continue;
Expand All @@ -160,22 +168,28 @@
}

fn isExempt(p: String, exemptions: [Exemption]) -> Bool {
let target = normalizeRepoPath(trim(p));
let mut i = 0;
let ex_len = len(exemptions);
while (i < ex_len) {
let e = exemptions[i];
if (regexMatch(p, e.rx)) { return true; }
let mut bare = e.raw;
while (len(bare) > 0 && (string_sub(bare, 0, 1) == "." || string_sub(bare, 0, 1) == "/")) {
bare = string_sub(bare, 1, len(bare) - 1);
}
if (p == bare) { return true; }
if (ends_with(e.raw, "/") && regexMatch(p, "^" ++ bare)) { return true; }
if (regexMatch(target, e.rx)) { return true; }
let bare = normalizeRepoPath(trim(e.raw));
if (target == bare) { return true; }
if (ends_with(bare, "/") && regexMatch(target, "^" ++ bare)) { return true; }
i = i + 1;
}
return false;
}

fn isTypeScriptArtifact(name: String) -> Bool {
if (ends_with(name, ".ts")) { return true; }
if (ends_with(name, ".tsx")) { return true; }
if (ends_with(name, ".ts.bak")) { return true; }
if (ends_with(name, ".tsx.bak")) { return true; }
return false;
}

pub fn main() -> Int {
let exemptions = loadExemptions();
let mut found = [];
Expand All @@ -189,7 +203,7 @@
let af_len = len(all_files);
while (i < af_len) {
let f = all_files[i];
if (ends_with(f, ".ts") || ends_with(f, ".tsx")) {
if (isTypeScriptArtifact(f)) {
let mut skip = false;
let segs = split(f, "/");
let mut j = 0;
Expand Down
24 changes: 20 additions & 4 deletions scripts/check-ts-allowlist.deno.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 44 additions & 13 deletions scripts/check-ts-allowlist.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// check-ts-allowlist.ts — Deno port of the inline python3 heredoc that used
// to live in `.github/workflows/governance-reusable.yml` step
Expand All @@ -12,7 +12,8 @@
// self-loop fixed in hypatia#328. This script eliminates the violation.
//
// Behaviour MUST stay byte-identical to the previous Python implementation:
// * Walk every `*.ts` / `*.tsx` file under cwd, skipping dotted dirs.
// * Walk every `*.ts` / `*.tsx` file under cwd, skipping dotted dirs
// and treating `.ts.bak` / `.tsx.bak` backups as banned TS artifacts.
// * Allow files in the built-in directory/path allowlist
// (bindings/tests/scripts/vendor/examples/ffi/benchmarks/cli, plus any
// segment containing 'vscode' or starting with 'deno-').
Expand Down Expand Up @@ -72,6 +73,27 @@ function globToRegex(g: string): RegExp {

interface Exemption { raw: string; rx: RegExp; }

function normalizeRepoPath(p: string): string {
let out = p.trim();
while (out.length > 0 && (out[0] === "." || out[0] === "/")) {
out = out.slice(1);
}
return out;
}

function normalizeExemptionCell(cell: string): string {
let out = cell.trim();
const codeSpan = out.match(/^`([^`]+)`$/) ?? out.match(/^`([^`]+)`/);
if (codeSpan) {
out = codeSpan[1].trim();
}
return normalizeRepoPath(out);
}

function nonExemptionCell(cell: string): boolean {
return cell === "" || /^:?-{3,}:?$/.test(cell) || /^path\b/i.test(cell);
}

async function loadExemptionsFromClaudeMd(): Promise<Exemption[]> {
// Layer 2 — heading-table exemptions parsed from `.claude/CLAUDE.md`.
//
Expand Down Expand Up @@ -110,10 +132,14 @@ async function loadExemptionsFromClaudeMd(): Promise<Exemption[]> {
inTable = false;
continue;
}
if (inTable && line.startsWith("|")) {
const m = line.match(/^\|\s*`([^`]+)`/);
if (m) {
exemptions.push({ raw: m[1], rx: globToRegex(m[1]) });
const tableLine = line.trim();
if (inTable && tableLine.startsWith("|")) {
const cells = tableLine.split("|");
if (cells.length >= 3) {
const raw = normalizeExemptionCell(cells[1]);
if (!nonExemptionCell(raw)) {
exemptions.push({ raw, rx: globToRegex(raw) });
}
}
}
}
Expand All @@ -135,7 +161,7 @@ async function loadExemptionsFromAllowlistFile(): Promise<Exemption[]> {
return exemptions;
}
for (const rawLine of text.split("\n")) {
const line = rawLine.trim();
const line = normalizeExemptionCell(rawLine);
if (line === "" || line.startsWith("#")) continue;
exemptions.push({ raw: line, rx: globToRegex(line) });
}
Expand All @@ -149,16 +175,21 @@ async function loadExemptions(): Promise<Exemption[]> {
}

function exempt(p: string, exemptions: Exemption[]): boolean {
const target = normalizeRepoPath(p);
for (const e of exemptions) {
if (e.rx.test(p)) return true;
let bare = e.raw;
while (bare.length > 0 && (bare[0] === "." || bare[0] === "/")) bare = bare.slice(1);
if (p === bare) return true;
if (e.raw.endsWith("/") && p.startsWith(bare)) return true;
if (e.rx.test(target)) return true;
const bare = normalizeRepoPath(e.raw);
if (target === bare) return true;
if (bare.endsWith("/") && target.startsWith(bare)) return true;
}
return false;
}

function isTypeScriptArtifact(name: string): boolean {
return name.endsWith(".ts") || name.endsWith(".tsx") ||
name.endsWith(".ts.bak") || name.endsWith(".tsx.bak");
}

async function* walkTs(dir: string): AsyncIterable<string> {
for await (const entry of Deno.readDir(dir)) {
const name = entry.name;
Expand All @@ -168,7 +199,7 @@ async function* walkTs(dir: string): AsyncIterable<string> {
if (entry.isDirectory) {
yield* walkTs(full);
} else if (entry.isFile) {
if (name.endsWith(".ts") || name.endsWith(".tsx")) {
if (isTypeScriptArtifact(name)) {
yield full;
}
}
Expand Down
Loading
Loading