From 8e6a272a7bf9de9ff8230d5f0b3a525d6de62502 Mon Sep 17 00:00:00 2001 From: Tirth Kanani Date: Sun, 14 Jun 2026 18:16:44 +0100 Subject: [PATCH 1/2] fix(ignore-filter): tolerate ./-prefixed and empty relative paths in isIgnored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IgnoreFilter.isIgnored delegated straight to ignore@7's ig.ignores(), which throws on an empty string ("path must not be empty") and on a leading './' ("path should be a path.relative()'d string"). Both arise from ordinary inputs (path.relative(root, root) === "" and ./-prefixed glob results), crashing the scan instead of returning a boolean. Fix: normalize first — treat falsy/empty as not-ignored and strip a leading './' before delegating to ig.ignores(). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../packages/core/src/__tests__/ignore-filter.test.ts | 11 +++++++++++ .../packages/core/src/ignore-filter.ts | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts b/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts index d7f962663..10f5d6e74 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts @@ -84,6 +84,17 @@ describe("IgnoreFilter", () => { expect(filter.isIgnored(".idea/workspace.xml")).toBe(true); expect(filter.isIgnored(".vscode/settings.json")).toBe(true); }); + + it("does not throw on './'-prefixed paths and matches correctly", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("./dist/index.js")).toBe(true); + expect(filter.isIgnored("./src/index.ts")).toBe(false); + }); + + it("treats the empty relative path (project root) as not ignored", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("")).toBe(false); + }); }); describe("createIgnoreFilter with user .understandignore", () => { diff --git a/understand-anything-plugin/packages/core/src/ignore-filter.ts b/understand-anything-plugin/packages/core/src/ignore-filter.ts index a56d2e3f3..c8bb14086 100644 --- a/understand-anything-plugin/packages/core/src/ignore-filter.ts +++ b/understand-anything-plugin/packages/core/src/ignore-filter.ts @@ -105,7 +105,10 @@ export function createIgnoreFilter(projectRoot: string): IgnoreFilter { return { isIgnored(relativePath: string): boolean { - return ig.ignores(relativePath); + if (!relativePath) return false; + const normalized = relativePath.replace(/^\.\//, ""); + if (!normalized) return false; + return ig.ignores(normalized); }, }; } From a7b5426c46d58a41e64924def50e4d952c8ad62a Mon Sep 17 00:00:00 2001 From: Tirth Kanani Date: Tue, 16 Jun 2026 23:32:36 +0100 Subject: [PATCH 2/2] fix(core): make IgnoreFilter.isIgnored total over path.relative() output shapes Normalize backslash separators, repeated leading ./, and duplicate slash runs before delegating to ignore@7, and treat out-of-root ../ paths as not-ignored instead of letting ig.ignores() throw. Documents the accepted input contract on the interface JSDoc and locks the previously-throwing inputs with not.toThrow assertions plus a bare './' (trim-to-root) case. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/src/__tests__/ignore-filter.test.ts | 40 +++++++++++++++++++ .../packages/core/src/ignore-filter.ts | 27 +++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts b/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts index 10f5d6e74..d8a8dc793 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts @@ -95,6 +95,46 @@ describe("IgnoreFilter", () => { const filter = createIgnoreFilter(testDir); expect(filter.isIgnored("")).toBe(false); }); + + it("does not throw on the input shapes that ignore@7 rejects", () => { + const filter = createIgnoreFilter(testDir); + // These are the previously-throwing inputs from the PR body. The whole + // point of the fix is that they return a boolean rather than throw. + expect(() => filter.isIgnored("")).not.toThrow(); + expect(() => filter.isIgnored("./")).not.toThrow(); + expect(() => filter.isIgnored("./dist/index.js")).not.toThrow(); + expect(() => filter.isIgnored("././foo.log")).not.toThrow(); + expect(() => filter.isIgnored(".//foo.log")).not.toThrow(); + expect(() => filter.isIgnored("../escapes/root.ts")).not.toThrow(); + expect(() => filter.isIgnored("dist\\index.js")).not.toThrow(); + expect(() => filter.isIgnored(".\\dist\\index.js")).not.toThrow(); + }); + + it("treats a bare './' (trim-to-empty / project root) as not ignored", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("./")).toBe(false); + }); + + it("collapses repeated leading './' and duplicate slash runs", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("././dist/index.js")).toBe(true); + expect(filter.isIgnored(".//dist/index.js")).toBe(true); + expect(filter.isIgnored("./src//index.ts")).toBe(false); + }); + + it("normalizes Windows backslash separators", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("dist\\index.js")).toBe(true); + expect(filter.isIgnored(".\\dist\\index.js")).toBe(true); + expect(filter.isIgnored("src\\index.ts")).toBe(false); + }); + + it("treats out-of-root '../' paths as not ignored instead of throwing", () => { + const filter = createIgnoreFilter(testDir); + expect(filter.isIgnored("../foo")).toBe(false); + expect(filter.isIgnored("..")).toBe(false); + expect(filter.isIgnored("..\\foo")).toBe(false); + }); }); describe("createIgnoreFilter with user .understandignore", () => { diff --git a/understand-anything-plugin/packages/core/src/ignore-filter.ts b/understand-anything-plugin/packages/core/src/ignore-filter.ts index c8bb14086..c1ed0d06f 100644 --- a/understand-anything-plugin/packages/core/src/ignore-filter.ts +++ b/understand-anything-plugin/packages/core/src/ignore-filter.ts @@ -70,7 +70,16 @@ export const DEFAULT_IGNORE_PATTERNS: string[] = [ ]; export interface IgnoreFilter { - /** Returns true if the given relative path should be excluded from analysis. */ + /** + * Returns true if the given path should be excluded from analysis. + * + * Accepts a root-relative path. Inputs are normalized before delegating to + * ignore@7 (which throws on anything that isn't a clean `path.relative()`d + * string): backslashes are converted to `/`, any leading `./` segments and + * duplicate slashes are collapsed, and an empty path (the project root) + * returns false. A path that escapes the root (`../...`) is treated as + * not-ignored (returns false) rather than throwing. + */ isIgnored(relativePath: string): boolean; } @@ -106,8 +115,20 @@ export function createIgnoreFilter(projectRoot: string): IgnoreFilter { return { isIgnored(relativePath: string): boolean { if (!relativePath) return false; - const normalized = relativePath.replace(/^\.\//, ""); - if (!normalized) return false; + // ignore@7's ig.ignores() throws on any input that isn't a clean, + // root-relative POSIX path, so normalize the shapes path.relative() can + // produce before delegating. Production callers always pass + // toPosix(relative(root, target)), but normalizing here keeps the + // function total against the other shapes too. + const normalized = relativePath + .replace(/\\/g, "/") // Windows separators -> POSIX + .replace(/^(\.\/)+/, "") // collapse one-or-more leading "./" + .replace(/\/{2,}/g, "/") // collapse duplicate slash runs + .replace(/^\/+/, ""); // drop any leading root slash + if (!normalized) return false; // empty path == project root, not ignored + // A path that escapes the root can't be under analysis; report not-ignored + // instead of letting ig.ignores() throw on the "../" prefix. + if (normalized === ".." || normalized.startsWith("../")) return false; return ig.ignores(normalized); }, };