diff --git a/dist/rules/matcher.js b/dist/rules/matcher.js index 7e2e2f6..e9c4438 100644 --- a/dist/rules/matcher.js +++ b/dist/rules/matcher.js @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import picomatch from "picomatch"; const compiledPatternSets = new Map(); export function matchRule(input) { if (input.isSingleFile) { @@ -77,53 +78,7 @@ function compilePatternSet(patterns) { return { positivePatterns, negativeMatchers }; } function createGlobMatcher(pattern) { - const expression = globToRegExp(normalizePath(pattern)); - return (path) => expression.test(path); -} -function globToRegExp(pattern) { - let source = "^"; - for (let index = 0; index < pattern.length; index += 1) { - const char = pattern[index]; - const nextChar = pattern[index + 1]; - if (char === "*" && nextChar === "*") { - const afterGlobStar = pattern[index + 2]; - if (afterGlobStar === "/") { - source += "(?:.*/)?"; - index += 2; - } - else { - source += ".*"; - index += 1; - } - continue; - } - if (char === "*") { - source += "[^/]*"; - continue; - } - if (char === "?") { - source += "[^/]"; - continue; - } - if (char === "{") { - const closeIndex = pattern.indexOf("}", index + 1); - if (closeIndex !== -1) { - const alternatives = pattern - .slice(index + 1, closeIndex) - .split(",") - .map(escapeRegExp) - .join("|"); - source += `(?:${alternatives})`; - index = closeIndex; - continue; - } - } - source += escapeRegExp(char ?? ""); - } - return new RegExp(`${source}$`); -} -function escapeRegExp(value) { - return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&"); + return picomatch(normalizePath(pattern), { bash: true, dot: true }); } function isExcluded(pathBase, negativeMatchers) { for (const isMatch of negativeMatchers) { diff --git a/package-lock.json b/package-lock.json index 46abb68..b90e452 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,16 @@ "name": "@code-yeongyu/codex-rules", "version": "0.1.0", "license": "MIT", + "dependencies": { + "picomatch": "^4.0.3" + }, "bin": { "codex-rules": "dist/cli.js" }, "devDependencies": { "@biomejs/biome": "2.4.15", "@types/node": "^25.7.0", + "@types/picomatch": "^4.0.0", "typescript": "^6.0.3", "vitest": "^4.1.5" }, @@ -601,6 +605,13 @@ "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/@types/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/expect": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", @@ -1142,7 +1153,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 7614d49..ff8cb0b 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,13 @@ "lint:fix": "biome check --write .", "check": "tsc --noEmit && biome check . && npm run build" }, + "dependencies": { + "picomatch": "^4.0.3" + }, "devDependencies": { "@biomejs/biome": "2.4.15", "@types/node": "^25.7.0", + "@types/picomatch": "^4.0.0", "typescript": "^6.0.3", "vitest": "^4.1.5" }, diff --git a/src/rules/matcher.ts b/src/rules/matcher.ts index 4d69da8..227e427 100644 --- a/src/rules/matcher.ts +++ b/src/rules/matcher.ts @@ -1,4 +1,5 @@ import { createHash } from "node:crypto"; +import picomatch from "picomatch"; import type { MatchReason, RuleFrontmatter } from "./types.js"; export interface MatcherInput { @@ -123,60 +124,7 @@ function compilePatternSet(patterns: ReadonlyArray): CompiledPatternSet } function createGlobMatcher(pattern: string): (path: string) => boolean { - const expression = globToRegExp(normalizePath(pattern)); - return (path: string) => expression.test(path); -} - -function globToRegExp(pattern: string): RegExp { - let source = "^"; - for (let index = 0; index < pattern.length; index += 1) { - const char = pattern[index]; - const nextChar = pattern[index + 1]; - - if (char === "*" && nextChar === "*") { - const afterGlobStar = pattern[index + 2]; - if (afterGlobStar === "/") { - source += "(?:.*/)?"; - index += 2; - } else { - source += ".*"; - index += 1; - } - continue; - } - - if (char === "*") { - source += "[^/]*"; - continue; - } - - if (char === "?") { - source += "[^/]"; - continue; - } - - if (char === "{") { - const closeIndex = pattern.indexOf("}", index + 1); - if (closeIndex !== -1) { - const alternatives = pattern - .slice(index + 1, closeIndex) - .split(",") - .map(escapeRegExp) - .join("|"); - source += `(?:${alternatives})`; - index = closeIndex; - continue; - } - } - - source += escapeRegExp(char ?? ""); - } - - return new RegExp(`${source}$`); -} - -function escapeRegExp(value: string): string { - return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&"); + return picomatch(normalizePath(pattern), { bash: true, dot: true }); } function isExcluded(pathBase: string, negativeMatchers: ReadonlyArray<(path: string) => boolean>): boolean { diff --git a/test/matcher.test.ts b/test/matcher.test.ts index a4e7150..5a4b5c2 100644 --- a/test/matcher.test.ts +++ b/test/matcher.test.ts @@ -168,6 +168,28 @@ describe("matchRule", () => { expect(matched).toBe(true); }); + it("#given character class glob #when matching listed extension #then target matches", () => { + // given + const globs = "src/**/*.[tj]s"; + + // when + const matched = matchGlobs(globs, "src/features/app.ts"); + + // then + expect(matched).toBe(true); + }); + + it("#given extglob pattern #when matching allowed extension #then target matches", () => { + // given + const globs = "src/**/*.@(ts|tsx)"; + + // when + const matched = matchGlobs(globs, "src/features/app.tsx"); + + // then + expect(matched).toBe(true); + }); + it("#given duplicate normalized patterns #when normalizing #then first unique pattern order is kept", () => { // given const frontmatter = { diff --git a/test/package-smoke.test.ts b/test/package-smoke.test.ts index 97e5b03..b9b7225 100644 --- a/test/package-smoke.test.ts +++ b/test/package-smoke.test.ts @@ -63,7 +63,7 @@ describe("plugin package metadata", () => { // then expect(packageJson.type).toBe("module"); expect(packageJson.packageManager).toBe("npm@11.12.1"); - expect(packageJson.dependencies ?? {}).toEqual({}); + expect(packageJson.dependencies ?? {}).toEqual({ picomatch: "^4.0.3" }); expect(packageJson.bin["codex-rules"]).toBe("./dist/cli.js"); expect(pluginJson.hooks).toBe("./hooks/hooks.json"); expect(cliSource.startsWith("#!/usr/bin/env node")).toBe(true);