Skip to content

Commit 131bec7

Browse files
committed
fix(search): simplify literal-dir pin detection
Replace the stack-collapse hasLiteralBefore with a terse fold over the collapsed prefix segments, and trim over-verbose test comments.
1 parent 253ff9c commit 131bec7

2 files changed

Lines changed: 11 additions & 38 deletions

File tree

‎src/plugins/search-files-fail-closed.test.ts‎

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,6 @@ describe("CL-9469 unbounded-root predicate", () => {
277277
true,
278278
);
279279
});
280-
// A recursive descent is only a whole-tree walk when nothing literal pins it
281-
// down; a leading literal dir keeps it bounded even at the workspace root.
282280
test("recursive glob with a leading literal dir is bounded", () => {
283281
for (const pattern of [
284282
"src/**/*.ts",
@@ -299,9 +297,7 @@ describe("CL-9469 unbounded-root predicate", () => {
299297
);
300298
}
301299
});
302-
// F1 soundness hole: `.`/`..` are relative-notation, not literal pins, so a
303-
// leading dot segment must not whitelist a root walk (`./**`, `./**/*.ts`)
304-
// and `..` must not be readable as an anchored workspace-escape.
300+
// A leading `.`/`..` is relative notation, not a literal pin.
305301
test("leading dot or dotdot is not a literal pin", () => {
306302
for (const pattern of [
307303
"./**",
@@ -315,11 +311,7 @@ describe("CL-9469 unbounded-root predicate", () => {
315311
);
316312
}
317313
});
318-
// F3 soundness hole: interior `..` collapse must be normalized, not read
319-
// verbatim. `a/../**` collapses to `./**` (root-wide walk), `src/../../**`
320-
// pops above the workspace root (escape), and `./a/../**` is still root
321-
// relative. Only a prefix that collapses to a surviving name-bearing pin
322-
// (`src/../packages/**` -> `packages/**`) stays bounded.
314+
// Interior `..` collapses first, so the pin must survive the fold.
323315
test("interior dotdot collapse is normalized, so the pin must survive", () => {
324316
for (const pattern of [
325317
"a/../**",

‎src/plugins/tool-time-budget.ts‎

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -42,44 +42,25 @@ export function isUnboundedSearchGlob(pattern: string): boolean {
4242
return !hasLiteralBefore(pattern.slice(0, firstRecursive));
4343
}
4444

45-
// True when the pre-`**` prefix collapses to a real, name-bearing directory
46-
// that anchors the walk away from the workspace root. The prefix is split on
47-
// `/` and collapsed left-to-right on a stack: `.` and empty segments are
48-
// skipped and `..` pops the preceding segment. A segment of only `*` wildcards
49-
// is pushed as a non-name pin (it occupies a slot but cannot anchor the walk);
50-
// the first surviving name-bearing segment is the pin. A prefix whose collapse
51-
// runs the stack empty (the pattern reaches the root, e.g. `a/../**`) or where
52-
// a `..` pops above the root (`src/../../**` escapes the workspace) leaves no
53-
// literal pin, so the recursion is a whole-tree walk. A leading separator or
54-
// dot-relative step (`/`, `./src/**`) likewise never pins to a real boundary.
45+
// True when the pre-`**` prefix names a real directory that anchors the walk
46+
// away from the root. Leading root-relative notation (`.`/`..`/empty) does not
47+
// pin; interior `.`/`..`/empty segments collapse first, so only a surviving
48+
// name segment counts.
5549
function hasLiteralBefore(prefix: string): boolean {
5650
const segments = prefix.split("/");
57-
// A leading separator, dot, or dotdot step is root-relative notation that
58-
// does not pin to a concrete boundary, even when a name follows (`./src/**`
59-
// still walks from the root). `*/src/**` is unaffected: `*` is not a `..`.
6051
if (segments[0] === "" || segments[0] === "." || segments[0] === "..") {
6152
return false;
6253
}
63-
// Stack of surviving segments (true = name-bearing pin). `.`/empty are
64-
// skipped; `..` pops the top; all-wildcard runs push a non-name slot.
65-
const stack: boolean[] = [];
54+
const names: string[] = [];
6655
for (const segment of segments) {
67-
if (segment.length === 0 || segment === ".") continue;
56+
if (segment === "" || segment === ".") continue;
6857
if (segment === "..") {
69-
if (stack.length === 0) return false; // pop above the workspace root
70-
stack.pop();
58+
names.pop();
7159
continue;
7260
}
73-
let isName = false;
74-
for (const char of segment) {
75-
if (char !== "*") {
76-
isName = true;
77-
break;
78-
}
79-
}
80-
stack.push(isName);
61+
names.push(segment);
8162
}
82-
return stack.some(Boolean); // at least one name-bearing pin survives
63+
return names.some((segment) => [...segment].some((char) => char !== "*"));
8364
}
8465

8566
// The raw path argument resolved against the session root: omitted, empty,

0 commit comments

Comments
 (0)