From eb2d31da8d2a53a53fc96b881a1d52ffe8f35601 Mon Sep 17 00:00:00 2001 From: jdalton Date: Mon, 20 Apr 2026 14:50:47 -0400 Subject: [PATCH] fix(lint): correct glob-to-regex translation for oxlint excludes `isExcludedByOxlint` had two defects that caused the pre-commit lint runner to exit non-zero whenever the only changed files were inside oxlint-excluded directories like `.claude/`: 1. `.replace(/\./g, '\\.')` ran *after* the glob substitutions, so it escaped the dots in the just-introduced `.*` and `[^/]*` patterns, corrupting them. The first `*` replacement also consumed the `*` inside the `.*` produced by the `**/` replacement. 2. The resulting regex used `$` anchoring, so a pattern like `**/dist` only matched the bare path `dist`, never files *inside* it like `dist/foo.js`. Fix: escape regex metacharacters on the original pattern first, then translate `**/` and `*` in a single callback-based pass so later substitutions can't corrupt earlier ones. Anchor with `(?:/|$)` so directory patterns also match descendants. Verified against all 13 patterns in `.oxlintrc.json` with a 21-case harness covering directories, descendants, file globs, lookalike siblings (e.g. `coverage-other` must not match `**/coverage`), and nested path prefixes. --- scripts/lint.mts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/scripts/lint.mts b/scripts/lint.mts index 7c512fb0..7bda7227 100644 --- a/scripts/lint.mts +++ b/scripts/lint.mts @@ -59,20 +59,25 @@ function getOxlintExcludePatterns(): string[] { /** * Check if a file matches any of the exclude patterns. + * + * Handles directory patterns (`**\/dist` → matches `dist` and any file inside + * it) and file-glob patterns (`**\/*.d.ts` → matches any `.d.ts` file). The + * substitution order is deliberate: regex metacharacters in the literal + * pattern are escaped first, then `**\/` and `*` are translated in a single + * pass via a callback so later substitutions can't corrupt earlier ones. */ function isExcludedByOxlint(file: string, excludePatterns: string[]): boolean { for (const pattern of excludePatterns) { - // Convert glob pattern to regex-like matching - // Support **/ for directory wildcards and * for filename wildcards + // Escape regex metacharacters in the literal pattern (dots etc.), then + // translate glob tokens in a single pass. const regexPattern = pattern - // **/ matches any directory - .replace(/\*\*\//g, '.*') - // * matches any characters except / - .replace(/\*/g, '[^/]*') - // Escape dots - .replace(/\./g, '\\.') - - const regex = new RegExp(`^${regexPattern}$`) + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') + .replace(/\*\*\/|\*/g, m => (m === '*' ? '[^/]*' : '(?:.*/)?')) + + // Match the pattern as an exact path OR as a directory prefix (pattern + // followed by `/`). This makes `**/dist` exclude both `dist` itself and + // every descendant. + const regex = new RegExp(`^${regexPattern}(?:/|$)`) if (regex.test(file)) { return true }