Skip to content

Commit 50c143f

Browse files
committed
Refactor: use closure for gitignore instance instead of parameter
- Remove ig and rootPath from TraversalOptions interface - Convert buildFileTree to inner function that captures these via closure - Cleaner interface - only exposes actual options, not implementation details - Same efficiency - still loads .gitignore once - Net: -5 lines
1 parent 8314c15 commit 50c143f

File tree

1 file changed

+134
-135
lines changed

1 file changed

+134
-135
lines changed

src/services/tools/file_list.ts

Lines changed: 134 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ import {
1515

1616
interface TraversalOptions {
1717
pattern?: string;
18-
useGitignore: boolean;
1918
maxEntries: number;
20-
ig: ReturnType<typeof ignore> | null; // Ignore instance loaded once from .gitignore, reused across recursion
21-
rootPath: string; // Root path for calculating relative paths in gitignore matching
2219
}
2320

2421
interface TraversalResult {
@@ -27,134 +24,6 @@ interface TraversalResult {
2724
exceeded: boolean;
2825
}
2926

30-
/**
31-
* Recursively build a file tree structure with depth control and filtering.
32-
* Counts entries as they're added and stops when limit is reached.
33-
*
34-
* @param dir - Directory to traverse
35-
* @param currentDepth - Current depth level (1 = immediate children)
36-
* @param maxDepth - Maximum depth to traverse
37-
* @param options - Filtering options (pattern, gitignore, entry limit)
38-
* @param currentCount - Shared counter tracking total entries across recursion
39-
* @returns Tree structure with entries, total count, and exceeded flag
40-
*/
41-
async function buildFileTree(
42-
dir: string,
43-
currentDepth: number,
44-
maxDepth: number,
45-
options: TraversalOptions,
46-
currentCount: { value: number }
47-
): Promise<TraversalResult> {
48-
// Check if we've already exceeded the limit
49-
if (currentCount.value >= options.maxEntries) {
50-
return { entries: [], totalCount: currentCount.value, exceeded: true };
51-
}
52-
53-
let dirents;
54-
try {
55-
dirents = await fs.readdir(dir, { withFileTypes: true });
56-
} catch (err) {
57-
// If we can't read the directory (permissions, etc.), skip it
58-
return { entries: [], totalCount: currentCount.value, exceeded: false };
59-
}
60-
61-
// Sort: directories first, then files, alphabetically within each group
62-
dirents.sort((a, b) => {
63-
const aIsDir = a.isDirectory();
64-
const bIsDir = b.isDirectory();
65-
if (aIsDir && !bIsDir) return -1;
66-
if (!aIsDir && bIsDir) return 1;
67-
return a.name.localeCompare(b.name);
68-
});
69-
70-
const entries: FileEntry[] = [];
71-
72-
for (const dirent of dirents) {
73-
const fullPath = path.join(dir, dirent.name);
74-
const entryType = dirent.isDirectory() ? "directory" : dirent.isFile() ? "file" : "symlink";
75-
76-
// Always skip .git directory regardless of gitignore setting
77-
if (dirent.name === ".git" && entryType === "directory") {
78-
continue;
79-
}
80-
81-
// Check gitignore filtering
82-
if (options.useGitignore && options.ig) {
83-
const relativePath = path.relative(options.rootPath, fullPath);
84-
// Add trailing slash for directories for proper gitignore matching
85-
const pathToCheck = entryType === "directory" ? relativePath + "/" : relativePath;
86-
if (options.ig.ignores(pathToCheck)) {
87-
continue;
88-
}
89-
}
90-
91-
// For pattern matching:
92-
// - If it's a file, check if it matches the pattern
93-
// - If it's a directory, we'll add it provisionally and remove it later if it has no matches
94-
let matchesPattern = true;
95-
if (options.pattern && entryType === "file") {
96-
matchesPattern = minimatch(dirent.name, options.pattern, { matchBase: true });
97-
}
98-
99-
// Skip files that don't match pattern
100-
if (entryType === "file" && !matchesPattern) {
101-
continue;
102-
}
103-
104-
// Check limit before adding (even for directories we'll explore)
105-
if (currentCount.value >= options.maxEntries) {
106-
return { entries, totalCount: currentCount.value + 1, exceeded: true };
107-
}
108-
109-
// Increment counter
110-
currentCount.value++;
111-
112-
const entry: FileEntry = {
113-
name: dirent.name,
114-
type: entryType,
115-
};
116-
117-
// Get size for files
118-
if (entryType === "file") {
119-
try {
120-
const stats = await fs.stat(fullPath);
121-
entry.size = stats.size;
122-
} catch {
123-
// If we can't stat the file, skip size
124-
}
125-
}
126-
127-
// Recurse into directories if within depth limit
128-
if (entryType === "directory" && currentDepth < maxDepth) {
129-
const result = await buildFileTree(
130-
fullPath,
131-
currentDepth + 1,
132-
maxDepth,
133-
options,
134-
currentCount
135-
);
136-
137-
if (result.exceeded) {
138-
// Don't add this directory since we exceeded the limit while processing it
139-
currentCount.value--; // Revert the increment for this directory
140-
return { entries, totalCount: result.totalCount, exceeded: true };
141-
}
142-
143-
entry.children = result.entries;
144-
145-
// If we have a pattern and this directory has no matching descendants, skip it
146-
if (options.pattern && entry.children.length === 0) {
147-
currentCount.value--; // Revert the increment
148-
continue;
149-
}
150-
}
151-
152-
entries.push(entry);
153-
}
154-
155-
return { entries, totalCount: currentCount.value, exceeded: false };
156-
}
157-
15827
/**
15928
* Load and parse .gitignore file if it exists
16029
*/
@@ -232,9 +101,142 @@ export function createFileListTool(config: { cwd: string }) {
232101
// Enforce entry limit
233102
const effectiveMaxEntries = Math.min(Math.max(1, max_entries), FILE_LIST_HARD_MAX_ENTRIES);
234103

235-
// Load .gitignore if requested
104+
// Load .gitignore if requested (loaded once, used across entire traversal via closure)
236105
const ig = gitignore ? await loadGitignore(resolvedPath) : null;
237106

107+
/**
108+
* Recursively build a file tree structure with depth control and filtering.
109+
* Uses closure to access ig (ignore instance) and resolvedPath without passing them through recursion.
110+
* Counts entries as they're added and stops when limit is reached.
111+
*
112+
* @param dir - Directory to traverse
113+
* @param currentDepth - Current depth level (1 = immediate children)
114+
* @param maxDepth - Maximum depth to traverse
115+
* @param options - Filtering options (pattern, entry limit)
116+
* @param currentCount - Shared counter tracking total entries across recursion
117+
* @returns Tree structure with entries, total count, and exceeded flag
118+
*/
119+
async function buildFileTree(
120+
dir: string,
121+
currentDepth: number,
122+
maxDepth: number,
123+
options: TraversalOptions,
124+
currentCount: { value: number }
125+
): Promise<TraversalResult> {
126+
// Check if we've already exceeded the limit
127+
if (currentCount.value >= options.maxEntries) {
128+
return { entries: [], totalCount: currentCount.value, exceeded: true };
129+
}
130+
131+
let dirents;
132+
try {
133+
dirents = await fs.readdir(dir, { withFileTypes: true });
134+
} catch (err) {
135+
// If we can't read the directory (permissions, etc.), skip it
136+
return { entries: [], totalCount: currentCount.value, exceeded: false };
137+
}
138+
139+
// Sort: directories first, then files, alphabetically within each group
140+
dirents.sort((a, b) => {
141+
const aIsDir = a.isDirectory();
142+
const bIsDir = b.isDirectory();
143+
if (aIsDir && !bIsDir) return -1;
144+
if (!aIsDir && bIsDir) return 1;
145+
return a.name.localeCompare(b.name);
146+
});
147+
148+
const entries: FileEntry[] = [];
149+
150+
for (const dirent of dirents) {
151+
const fullPath = path.join(dir, dirent.name);
152+
const entryType = dirent.isDirectory()
153+
? "directory"
154+
: dirent.isFile()
155+
? "file"
156+
: "symlink";
157+
158+
// Always skip .git directory regardless of gitignore setting
159+
if (dirent.name === ".git" && entryType === "directory") {
160+
continue;
161+
}
162+
163+
// Check gitignore filtering (uses ig from closure)
164+
if (gitignore && ig) {
165+
const relativePath = path.relative(resolvedPath, fullPath);
166+
// Add trailing slash for directories for proper gitignore matching
167+
const pathToCheck = entryType === "directory" ? relativePath + "/" : relativePath;
168+
if (ig.ignores(pathToCheck)) {
169+
continue;
170+
}
171+
}
172+
173+
// For pattern matching:
174+
// - If it's a file, check if it matches the pattern
175+
// - If it's a directory, we'll add it provisionally and remove it later if it has no matches
176+
let matchesPattern = true;
177+
if (options.pattern && entryType === "file") {
178+
matchesPattern = minimatch(dirent.name, options.pattern, { matchBase: true });
179+
}
180+
181+
// Skip files that don't match pattern
182+
if (entryType === "file" && !matchesPattern) {
183+
continue;
184+
}
185+
186+
// Check limit before adding (even for directories we'll explore)
187+
if (currentCount.value >= options.maxEntries) {
188+
return { entries, totalCount: currentCount.value + 1, exceeded: true };
189+
}
190+
191+
// Increment counter
192+
currentCount.value++;
193+
194+
const entry: FileEntry = {
195+
name: dirent.name,
196+
type: entryType,
197+
};
198+
199+
// Get size for files
200+
if (entryType === "file") {
201+
try {
202+
const stats = await fs.stat(fullPath);
203+
entry.size = stats.size;
204+
} catch {
205+
// If we can't stat the file, skip size
206+
}
207+
}
208+
209+
// Recurse into directories if within depth limit
210+
if (entryType === "directory" && currentDepth < maxDepth) {
211+
const result = await buildFileTree(
212+
fullPath,
213+
currentDepth + 1,
214+
maxDepth,
215+
options,
216+
currentCount
217+
);
218+
219+
if (result.exceeded) {
220+
// Don't add this directory since we exceeded the limit while processing it
221+
currentCount.value--; // Revert the increment for this directory
222+
return { entries, totalCount: result.totalCount, exceeded: true };
223+
}
224+
225+
entry.children = result.entries;
226+
227+
// If we have a pattern and this directory has no matching descendants, skip it
228+
if (options.pattern && entry.children.length === 0) {
229+
currentCount.value--; // Revert the increment
230+
continue;
231+
}
232+
}
233+
234+
entries.push(entry);
235+
}
236+
237+
return { entries, totalCount: currentCount.value, exceeded: false };
238+
}
239+
238240
// Build the file tree
239241
const currentCount = { value: 0 };
240242
const result = await buildFileTree(
@@ -243,10 +245,7 @@ export function createFileListTool(config: { cwd: string }) {
243245
effectiveDepth,
244246
{
245247
pattern,
246-
useGitignore: gitignore,
247248
maxEntries: effectiveMaxEntries,
248-
ig,
249-
rootPath: resolvedPath,
250249
},
251250
currentCount
252251
);

0 commit comments

Comments
 (0)