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..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 @@ -84,6 +84,57 @@ 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); + }); + + 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 a56d2e3f3..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; } @@ -105,7 +114,22 @@ export function createIgnoreFilter(projectRoot: string): IgnoreFilter { return { isIgnored(relativePath: string): boolean { - return ig.ignores(relativePath); + if (!relativePath) 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); }, }; }