Repository navigation
perf(glob): prune ignored directories during traversal, not just post-filter #6123
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 5 commits
5ffa0af
d9bdcbe
e4dee3d
94642a4
2494009
7552894
38a0e46
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 |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ import { ToolErrorType } from './tool-error.js'; | |
| import { getErrorMessage } from '../utils/errors.js'; | ||
| import type { FileDiscoveryService } from '../services/fileDiscoveryService.js'; | ||
| import { createDebugLogger } from '../utils/debugLogger.js'; | ||
| import { isPathWithinRoot } from '../utils/workspaceContext.js'; | ||
|
|
||
| const debugLogger = createDebugLogger('GLOB'); | ||
|
|
||
|
|
@@ -142,6 +143,35 @@ class GlobToolInvocation extends BaseToolInvocation< | |
| effectivePattern = escape(effectivePattern); | ||
| } | ||
|
|
||
| const projectRoot = this.config.getTargetDir(); | ||
| const fileFilteringOptions = this.getFileFilteringOptions(); | ||
|
|
||
| // Prune ignored directories DURING traversal (glob's `childrenIgnored`) | ||
| // rather than only post-filtering the results. Delegating to | ||
| // FileDiscoveryService reuses the real .gitignore/.qwenignore semantics | ||
| // (anchoring, negation/re-inclusion, nested ignore files) — a hand-rolled | ||
| // gitignore→glob pattern conversion cannot reproduce these correctly. | ||
| const isTraversalIgnored = (entry: { | ||
| fullpath(): string; | ||
| isDirectory(): boolean; | ||
| }): boolean => { | ||
| const relativePath = path.relative(projectRoot, entry.fullpath()); | ||
| // Never prune paths outside the project root (e.g. an external search | ||
|
Collaborator
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. [Suggestion] Redundant
Consider a future optimization to pass the already-computed relative path through, or add a — qwen3.7-max via Qwen Code /review |
||
| // dir); ignore rules are only defined relative to the root. | ||
| if (!relativePath || !isPathWithinRoot(entry.fullpath(), projectRoot)) { | ||
| return false; | ||
| } | ||
| // Append trailing '/' for directories so the ignore library matches | ||
| // directory-only patterns like `node_modules/`. | ||
| const ignorePath = entry.isDirectory() | ||
| ? relativePath + '/' | ||
| : relativePath; | ||
| return this.fileService.shouldIgnoreFile( | ||
|
Collaborator
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. [Performance] For projects with thousands of files and many gitignore patterns, this per-entry overhead could partially offset the traversal-pruning benefit. Consider pre-building a single Not a blocker -- the net effect is still positive since pruning large ignored trees (e.g. |
||
| ignorePath, | ||
| fileFilteringOptions, | ||
| ); | ||
| }; | ||
|
|
||
| const entries = (await glob(effectivePattern, { | ||
| cwd: searchDir, | ||
| withFileTypes: true, | ||
|
|
@@ -151,20 +181,23 @@ class GlobToolInvocation extends BaseToolInvocation< | |
| dot: true, | ||
| follow: false, | ||
| signal, | ||
| ignore: { | ||
| ignored: isTraversalIgnored, | ||
| childrenIgnored: isTraversalIgnored, | ||
| }, | ||
| })) as GlobPath[]; | ||
|
|
||
| // Filter using paths relative to the project root (the base that | ||
| // FileDiscoveryService uses for .gitignore / .qwenignore evaluation). | ||
| // Using searchDir-relative paths would cause ignore rules to be | ||
| // evaluated against incorrect paths when searchDir != projectRoot. | ||
| const projectRoot = this.config.getTargetDir(); | ||
| const relativePaths = entries.map((p) => | ||
| path.relative(projectRoot, p.fullpath()), | ||
| ); | ||
|
|
||
| const { filteredPaths } = this.fileService.filterFilesWithReport( | ||
| relativePaths, | ||
| this.getFileFilteringOptions(), | ||
| fileFilteringOptions, | ||
| ); | ||
|
|
||
| const normalizePathForComparison = (p: string) => | ||
|
|
||
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.
[Critical] Tests don't verify that the
ignoreoption is actually passed to glob.All 3 new tests assert only on
result.llmContent(post-filter output). The post-filter (filterFilesWithReport) would produce identical results without theignoreoption — removing theignore: { ignored, childrenIgnored }block from the glob call would not cause any test to fail.The core change of this PR has no test coverage.
Suggested fix — add at least one assertion verifying the glob spy received the
ignorecallbacks:— qwen3.7-max via Qwen Code /review