Repository navigation
fix(glob): expand braces containing path separators before walking #25789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -814,3 +814,174 @@ describe("glob.scan wildcard fast path", async () => { | |
| ); | ||
| }); | ||
| }); | ||
|
|
||
| // brace alternation without path separators (handled by normal matching) | ||
| describe("brace alternation (basic)", () => { | ||
| test("braces in first segment", async () => { | ||
| const tempdir = tempDirWithFiles("glob-brace-first", { | ||
| apps: { | ||
| "a.txt": "content", | ||
| }, | ||
| packages: { | ||
| "b.txt": "content", | ||
| }, | ||
| other: { | ||
| "c.txt": "content", | ||
| }, | ||
| }); | ||
|
|
||
| const glob = new Glob("{apps,packages}/**"); | ||
| const entries = await Array.fromAsync(glob.scan({ cwd: tempdir, onlyFiles: false })); | ||
|
|
||
| expect(entries.length).toBeGreaterThan(0); | ||
| // should find both apps and packages directories/files | ||
| expect(entries.some(e => e.startsWith("apps"))).toBe(true); | ||
| expect(entries.some(e => e.startsWith("packages"))).toBe(true); | ||
| // should NOT find the "other" directory | ||
| expect(entries.some(e => e.startsWith("other"))).toBe(false); | ||
| }); | ||
|
|
||
| test("braces in first segment (sync)", () => { | ||
| const tempdir = tempDirWithFiles("glob-brace-first-sync", { | ||
| apps: { | ||
| "a.txt": "content", | ||
| }, | ||
| packages: { | ||
| "b.txt": "content", | ||
| }, | ||
| other: { | ||
| "c.txt": "content", | ||
| }, | ||
| }); | ||
|
|
||
| const glob = new Glob("{apps,packages}/**"); | ||
| const entries = Array.from(glob.scanSync({ cwd: tempdir, onlyFiles: false })); | ||
|
|
||
| expect(entries.length).toBeGreaterThan(0); | ||
| expect(entries.some(e => e.startsWith("apps"))).toBe(true); | ||
| expect(entries.some(e => e.startsWith("packages"))).toBe(true); | ||
| expect(entries.some(e => e.startsWith("other"))).toBe(false); | ||
| }); | ||
|
|
||
| test("braces with only files option", async () => { | ||
| const tempdir = tempDirWithFiles("glob-brace-onlyfiles", { | ||
| src: { | ||
| "index.ts": "content", | ||
| lib: { | ||
| "helper.ts": "content", | ||
| }, | ||
| }, | ||
| test: { | ||
| "index.test.ts": "content", | ||
| }, | ||
| }); | ||
|
|
||
| const glob = new Glob("{src,test}/**/*.ts"); | ||
| const entries = await Array.fromAsync(glob.scan({ cwd: tempdir, onlyFiles: true })); | ||
|
|
||
| expect(entries.length).toBe(3); | ||
| expect(entries.sort()).toEqual( | ||
| [`src${path.sep}index.ts`, `src${path.sep}lib${path.sep}helper.ts`, `test${path.sep}index.test.ts`].sort(), | ||
| ); | ||
| }); | ||
|
|
||
| test("single alternative in braces (edge case)", async () => { | ||
| const tempdir = tempDirWithFiles("glob-brace-single", { | ||
| apps: { | ||
| "a.txt": "content", | ||
| }, | ||
| packages: { | ||
| "b.txt": "content", | ||
| }, | ||
| }); | ||
|
|
||
| // single alternative should still work | ||
| const glob = new Glob("{apps}/**"); | ||
| const entries = await Array.fromAsync(glob.scan({ cwd: tempdir, onlyFiles: false })); | ||
|
|
||
| expect(entries.length).toBeGreaterThan(0); | ||
| expect(entries.some(e => e.startsWith("apps"))).toBe(true); | ||
| expect(entries.some(e => e.startsWith("packages"))).toBe(false); | ||
| }); | ||
| }); | ||
|
Comment on lines
+818
to
+906
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider making these tests concurrent. These tests create separate temporary directories and don't share state, so they can safely run concurrently. Using 🔎 Proposed refactor to use concurrent tests // brace alternation without path separators (handled by normal matching)
describe("brace alternation (basic)", () => {
- test("braces in first segment", async () => {
+ test.concurrent("braces in first segment", async () => {
const tempdir = tempDirWithFiles("glob-brace-first", {
apps: {
"a.txt": "content",
},
packages: {
"b.txt": "content",
},
other: {
"c.txt": "content",
},
});
const glob = new Glob("{apps,packages}/**");
const entries = await Array.fromAsync(glob.scan({ cwd: tempdir, onlyFiles: false }));
expect(entries.length).toBeGreaterThan(0);
// should find both apps and packages directories/files
expect(entries.some(e => e.startsWith("apps"))).toBe(true);
expect(entries.some(e => e.startsWith("packages"))).toBe(true);
// should NOT find the "other" directory
expect(entries.some(e => e.startsWith("other"))).toBe(false);
});
- test("braces in first segment (sync)", () => {
+ test.concurrent("braces in first segment (sync)", () => {
const tempdir = tempDirWithFiles("glob-brace-first-sync", {
apps: {
"a.txt": "content",
},
packages: {
"b.txt": "content",
},
other: {
"c.txt": "content",
},
});
const glob = new Glob("{apps,packages}/**");
const entries = Array.from(glob.scanSync({ cwd: tempdir, onlyFiles: false }));
expect(entries.length).toBeGreaterThan(0);
expect(entries.some(e => e.startsWith("apps"))).toBe(true);
expect(entries.some(e => e.startsWith("packages"))).toBe(true);
expect(entries.some(e => e.startsWith("other"))).toBe(false);
});
- test("braces with only files option", async () => {
+ test.concurrent("braces with only files option", async () => {
const tempdir = tempDirWithFiles("glob-brace-onlyfiles", {
src: {
"index.ts": "content",
lib: {
"helper.ts": "content",
},
},
test: {
"index.test.ts": "content",
},
});
const glob = new Glob("{src,test}/**/*.ts");
const entries = await Array.fromAsync(glob.scan({ cwd: tempdir, onlyFiles: true }));
expect(entries.length).toBe(3);
expect(entries.sort()).toEqual(
[`src${path.sep}index.ts`, `src${path.sep}lib${path.sep}helper.ts`, `test${path.sep}index.test.ts`].sort(),
);
});
- test("single alternative in braces (edge case)", async () => {
+ test.concurrent("single alternative in braces (edge case)", async () => {
const tempdir = tempDirWithFiles("glob-brace-single", {
apps: {
"a.txt": "content",
},
packages: {
"b.txt": "content",
},
});
// single alternative should still work
const glob = new Glob("{apps}/**");
const entries = await Array.fromAsync(glob.scan({ cwd: tempdir, onlyFiles: false }));
expect(entries.length).toBeGreaterThan(0);
expect(entries.some(e => e.startsWith("apps"))).toBe(true);
expect(entries.some(e => e.startsWith("packages"))).toBe(false);
});
});Based on coding guidelines: "Prefer concurrent tests over sequential tests using 🤖 Prompt for AI Agents |
||
|
|
||
| // brace alternation with path separators inside braces (requires expansion) | ||
| describe("brace alternation with path separators", () => { | ||
| test("nested braces with path separators", async () => { | ||
| const tempdir = tempDirWithFiles("glob-brace-nested", { | ||
| apps: { | ||
| api: { | ||
| "config.yaml": "content", | ||
| }, | ||
| other: { | ||
| "skip.txt": "content", | ||
| }, | ||
| }, | ||
| packages: { | ||
| utils: { | ||
| "index.ts": "content", | ||
| }, | ||
| common: { | ||
| "helpers.ts": "content", | ||
| }, | ||
| core: { | ||
| "types.ts": "content", | ||
| }, | ||
| unrelated: { | ||
| "skip.txt": "content", | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const glob = new Glob("{apps/api,packages/{utils,common,core}}/**"); | ||
| const entries = await Array.fromAsync(glob.scan({ cwd: tempdir, onlyFiles: false })); | ||
|
|
||
| expect(entries.length).toBeGreaterThan(0); | ||
| // should find files in the specified paths | ||
| expect(entries.some(e => e.startsWith(`apps${path.sep}api`))).toBe(true); | ||
| expect(entries.some(e => e.startsWith(`packages${path.sep}utils`))).toBe(true); | ||
| expect(entries.some(e => e.startsWith(`packages${path.sep}common`))).toBe(true); | ||
| expect(entries.some(e => e.startsWith(`packages${path.sep}core`))).toBe(true); | ||
| // should NOT find files in unrelated paths | ||
| expect(entries.some(e => e.startsWith(`apps${path.sep}other`))).toBe(false); | ||
| expect(entries.some(e => e.startsWith(`packages${path.sep}unrelated`))).toBe(false); | ||
| }); | ||
|
|
||
| test("nested braces with path separators (sync)", () => { | ||
| const tempdir = tempDirWithFiles("glob-brace-nested-sync", { | ||
| apps: { | ||
| api: { | ||
| "config.yaml": "content", | ||
| }, | ||
| other: { | ||
| "skip.txt": "content", | ||
| }, | ||
| }, | ||
| packages: { | ||
| utils: { | ||
| "index.ts": "content", | ||
| }, | ||
| common: { | ||
| "helpers.ts": "content", | ||
| }, | ||
| core: { | ||
| "types.ts": "content", | ||
| }, | ||
| unrelated: { | ||
| "skip.txt": "content", | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const glob = new Glob("{apps/api,packages/{utils,common,core}}/**"); | ||
| const entries = Array.from(glob.scanSync({ cwd: tempdir, onlyFiles: false })); | ||
|
|
||
| expect(entries.length).toBeGreaterThan(0); | ||
| expect(entries.some(e => e.startsWith(`apps${path.sep}api`))).toBe(true); | ||
| expect(entries.some(e => e.startsWith(`packages${path.sep}utils`))).toBe(true); | ||
| expect(entries.some(e => e.startsWith(`packages${path.sep}common`))).toBe(true); | ||
| expect(entries.some(e => e.startsWith(`packages${path.sep}core`))).toBe(true); | ||
| expect(entries.some(e => e.startsWith(`apps${path.sep}other`))).toBe(false); | ||
| expect(entries.some(e => e.startsWith(`packages${path.sep}unrelated`))).toBe(false); | ||
| }); | ||
| }); | ||
|
Comment on lines
+909
to
+987
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider making these tests concurrent. Similar to the previous test suite, these tests create separate temporary directories and can safely run concurrently. 🔎 Proposed refactor to use concurrent tests // brace alternation with path separators inside braces (requires expansion)
describe("brace alternation with path separators", () => {
- test("nested braces with path separators", async () => {
+ test.concurrent("nested braces with path separators", async () => {
const tempdir = tempDirWithFiles("glob-brace-nested", {
apps: {
api: {
"config.yaml": "content",
},
other: {
"skip.txt": "content",
},
},
packages: {
utils: {
"index.ts": "content",
},
common: {
"helpers.ts": "content",
},
core: {
"types.ts": "content",
},
unrelated: {
"skip.txt": "content",
},
},
});
const glob = new Glob("{apps/api,packages/{utils,common,core}}/**");
const entries = await Array.fromAsync(glob.scan({ cwd: tempdir, onlyFiles: false }));
expect(entries.length).toBeGreaterThan(0);
// should find files in the specified paths
expect(entries.some(e => e.startsWith(`apps${path.sep}api`))).toBe(true);
expect(entries.some(e => e.startsWith(`packages${path.sep}utils`))).toBe(true);
expect(entries.some(e => e.startsWith(`packages${path.sep}common`))).toBe(true);
expect(entries.some(e => e.startsWith(`packages${path.sep}core`))).toBe(true);
// should NOT find files in unrelated paths
expect(entries.some(e => e.startsWith(`apps${path.sep}other`))).toBe(false);
expect(entries.some(e => e.startsWith(`packages${path.sep}unrelated`))).toBe(false);
});
- test("nested braces with path separators (sync)", () => {
+ test.concurrent("nested braces with path separators (sync)", () => {
const tempdir = tempDirWithFiles("glob-brace-nested-sync", {
apps: {
api: {
"config.yaml": "content",
},
other: {
"skip.txt": "content",
},
},
packages: {
utils: {
"index.ts": "content",
},
common: {
"helpers.ts": "content",
},
core: {
"types.ts": "content",
},
unrelated: {
"skip.txt": "content",
},
},
});
const glob = new Glob("{apps/api,packages/{utils,common,core}}/**");
const entries = Array.from(glob.scanSync({ cwd: tempdir, onlyFiles: false }));
expect(entries.length).toBeGreaterThan(0);
expect(entries.some(e => e.startsWith(`apps${path.sep}api`))).toBe(true);
expect(entries.some(e => e.startsWith(`packages${path.sep}utils`))).toBe(true);
expect(entries.some(e => e.startsWith(`packages${path.sep}common`))).toBe(true);
expect(entries.some(e => e.startsWith(`packages${path.sep}core`))).toBe(true);
expect(entries.some(e => e.startsWith(`apps${path.sep}other`))).toBe(false);
expect(entries.some(e => e.startsWith(`packages${path.sep}unrelated`))).toBe(false);
});
});Based on coding guidelines: "Prefer concurrent tests over sequential tests using 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Logic is correct but escape handling could be clearer.
The function correctly detects whether brace patterns contain path separators and need expansion. The escape handling is correct but could be more explicit.
📝 Optional clarification for escape handling
Consider adding a comment to clarify the escape handling logic:
'\\' => { if (comptime !isWindows) { - // skip escaped character on non-windows + // skip escaped character on non-windows (i += 1 here, then continue expression adds 1) i += 1; } else { // on windows, backslash is a path separator if (brace_depth > 0 and !in_brackets) return true; } },This makes it clearer that the loop's continue expression
(i += 1)will skip the escaped character.📝 Committable suggestion
🤖 Prompt for AI Agents