ucn 3.8.13 → 3.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +3 -1
- package/.github/workflows/ci.yml +13 -1
- package/README.md +1 -0
- package/cli/index.js +165 -246
- package/core/analysis.js +1400 -0
- package/core/build-worker.js +194 -0
- package/core/cache.js +105 -7
- package/core/callers.js +194 -64
- package/core/deadcode.js +22 -66
- package/core/discovery.js +9 -54
- package/core/execute.js +139 -54
- package/core/graph.js +615 -0
- package/core/output/analysis-ext.js +271 -0
- package/core/output/analysis.js +491 -0
- package/core/output/extraction.js +188 -0
- package/core/output/find.js +355 -0
- package/core/output/graph.js +399 -0
- package/core/output/refactoring.js +293 -0
- package/core/output/reporting.js +331 -0
- package/core/output/search.js +307 -0
- package/core/output/shared.js +271 -0
- package/core/output/tracing.js +416 -0
- package/core/output.js +15 -3293
- package/core/parallel-build.js +165 -0
- package/core/project.js +299 -3633
- package/core/registry.js +59 -0
- package/core/reporting.js +258 -0
- package/core/search.js +890 -0
- package/core/stacktrace.js +1 -1
- package/core/tracing.js +631 -0
- package/core/verify.js +10 -13
- package/eslint.config.js +43 -0
- package/jsconfig.json +10 -0
- package/languages/go.js +21 -2
- package/languages/html.js +8 -0
- package/languages/index.js +102 -40
- package/languages/java.js +13 -0
- package/languages/javascript.js +17 -1
- package/languages/python.js +14 -0
- package/languages/rust.js +13 -0
- package/languages/utils.js +1 -1
- package/mcp/server.js +45 -28
- package/package.json +8 -3
package/core/search.js
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/search.js — Symbol search, text search, usages, example, typedef, tests
|
|
3
|
+
*
|
|
4
|
+
* Extracted from project.js. All functions take an `index` (ProjectIndex)
|
|
5
|
+
* as the first argument instead of using `this`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { escapeRegExp } = require('./shared');
|
|
12
|
+
const { isTestFile } = require('./discovery');
|
|
13
|
+
const { detectLanguage, getParser, langTraits } = require('../languages');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a glob-style matcher: * matches any sequence, ? matches one char.
|
|
17
|
+
* Case-insensitive by default. Returns a function (string) => boolean.
|
|
18
|
+
*/
|
|
19
|
+
function buildGlobMatcher(pattern, caseSensitive) {
|
|
20
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
21
|
+
.replace(/\*/g, '.*')
|
|
22
|
+
.replace(/\?/g, '.');
|
|
23
|
+
const regex = new RegExp('^' + escaped + '$', caseSensitive ? '' : 'i');
|
|
24
|
+
return (name) => regex.test(name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const STRUCTURAL_TYPES = new Set(['function', 'class', 'call', 'method', 'type']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Substring match. Case-insensitive by default.
|
|
31
|
+
*/
|
|
32
|
+
function matchesSubstring(text, pattern, caseSensitive) {
|
|
33
|
+
if (!text) return false;
|
|
34
|
+
if (caseSensitive) return text.includes(pattern);
|
|
35
|
+
return text.toLowerCase().includes(pattern.toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find symbols by name with fuzzy/glob matching.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} index - ProjectIndex instance
|
|
42
|
+
* @param {string} name - Symbol name (supports glob patterns)
|
|
43
|
+
* @param {object} options - { exact, file, className, exclude, in, skipCounts }
|
|
44
|
+
* @returns {Array} Matching symbols with usage counts
|
|
45
|
+
*/
|
|
46
|
+
function find(index, name, options = {}) {
|
|
47
|
+
index._beginOp();
|
|
48
|
+
try {
|
|
49
|
+
// Glob pattern matching (e.g., _update*, handle*Request, get?ata)
|
|
50
|
+
const isGlob = name.includes('*') || name.includes('?');
|
|
51
|
+
if (isGlob && !options.exact) {
|
|
52
|
+
// Bare wildcard: return all symbols
|
|
53
|
+
const stripped = name.replace(/[*?]/g, '');
|
|
54
|
+
if (stripped.length === 0) {
|
|
55
|
+
const all = [];
|
|
56
|
+
for (const [, symbols] of index.symbols) {
|
|
57
|
+
for (const sym of symbols) {
|
|
58
|
+
all.push({ ...sym, _fuzzyScore: 800 });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
all.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
|
62
|
+
return _applyFindFilters(index, all, options);
|
|
63
|
+
}
|
|
64
|
+
const globRegex = new RegExp('^' + name.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
|
|
65
|
+
const matches = [];
|
|
66
|
+
for (const [symName, symbols] of index.symbols) {
|
|
67
|
+
if (globRegex.test(symName)) {
|
|
68
|
+
for (const sym of symbols) {
|
|
69
|
+
matches.push({ ...sym, _fuzzyScore: 800 });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
matches.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
|
74
|
+
return _applyFindFilters(index, matches, options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const matches = index.symbols.get(name) || [];
|
|
78
|
+
|
|
79
|
+
if (matches.length === 0 && !options.exact) {
|
|
80
|
+
// Smart fuzzy search with scoring
|
|
81
|
+
const candidates = [];
|
|
82
|
+
for (const [symName, symbols] of index.symbols) {
|
|
83
|
+
const score = index.fuzzyScore(name, symName);
|
|
84
|
+
if (score > 0) {
|
|
85
|
+
for (const sym of symbols) {
|
|
86
|
+
candidates.push({ ...sym, _fuzzyScore: score });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Sort by fuzzy score descending
|
|
91
|
+
candidates.sort((a, b) => b._fuzzyScore - a._fuzzyScore);
|
|
92
|
+
matches.push(...candidates);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return _applyFindFilters(index, matches, options);
|
|
96
|
+
} finally { index._endOp(); }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Apply file/exclude/in filters and usage counts to find results
|
|
101
|
+
*
|
|
102
|
+
* @param {object} index - ProjectIndex instance
|
|
103
|
+
* @param {Array} matches - Raw symbol matches
|
|
104
|
+
* @param {object} options - { className, file, exclude, in, skipCounts }
|
|
105
|
+
* @returns {Array} Filtered and sorted results
|
|
106
|
+
*/
|
|
107
|
+
function _applyFindFilters(index, matches, options) {
|
|
108
|
+
let filtered = matches;
|
|
109
|
+
|
|
110
|
+
// Filter by class name (Class.method syntax)
|
|
111
|
+
if (options.className) {
|
|
112
|
+
filtered = filtered.filter(m => m.className === options.className);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Filter by file pattern
|
|
116
|
+
if (options.file) {
|
|
117
|
+
filtered = filtered.filter(m =>
|
|
118
|
+
m.relativePath && m.relativePath.includes(options.file)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Apply semantic filters (--exclude, --in)
|
|
123
|
+
if (options.exclude || options.in) {
|
|
124
|
+
filtered = filtered.filter(m =>
|
|
125
|
+
index.matchesFilters(m.relativePath, { exclude: options.exclude, in: options.in })
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Skip expensive usage counting when caller doesn't need it
|
|
130
|
+
if (options.skipCounts) {
|
|
131
|
+
return filtered;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add per-symbol usage counts for disambiguation
|
|
135
|
+
const withCounts = filtered.map(m => {
|
|
136
|
+
const counts = index.countSymbolUsages(m);
|
|
137
|
+
return {
|
|
138
|
+
...m,
|
|
139
|
+
usageCount: counts.total,
|
|
140
|
+
usageCounts: counts // { total, calls, definitions, imports, references }
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Sort by usage count (most-used first)
|
|
145
|
+
withCounts.sort((a, b) => b.usageCount - a.usageCount);
|
|
146
|
+
|
|
147
|
+
return withCounts;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Find all usages of a symbol grouped by type
|
|
152
|
+
*
|
|
153
|
+
* @param {object} index - ProjectIndex instance
|
|
154
|
+
* @param {string} name - Symbol name
|
|
155
|
+
* @param {object} options - { codeOnly, context, exclude, in, file, className }
|
|
156
|
+
* @returns {Array} Usages grouped as definitions, calls, imports, references
|
|
157
|
+
*/
|
|
158
|
+
function usages(index, name, options = {}) {
|
|
159
|
+
index._beginOp();
|
|
160
|
+
try {
|
|
161
|
+
const usagesList = [];
|
|
162
|
+
|
|
163
|
+
// Resolve file pattern for --file filter
|
|
164
|
+
const fileFilter = options.file ? index.resolveFilePathForQuery(options.file) : null;
|
|
165
|
+
|
|
166
|
+
// Get definitions (filtered)
|
|
167
|
+
let allDefinitions = index.symbols.get(name) || [];
|
|
168
|
+
if (options.className) {
|
|
169
|
+
allDefinitions = allDefinitions.filter(d => d.className === options.className);
|
|
170
|
+
}
|
|
171
|
+
if (fileFilter) {
|
|
172
|
+
allDefinitions = allDefinitions.filter(d => d.file === fileFilter);
|
|
173
|
+
}
|
|
174
|
+
const definitions = options.exclude || options.in
|
|
175
|
+
? allDefinitions.filter(d => index.matchesFilters(d.relativePath, options))
|
|
176
|
+
: allDefinitions;
|
|
177
|
+
|
|
178
|
+
for (const def of definitions) {
|
|
179
|
+
usagesList.push({
|
|
180
|
+
...def,
|
|
181
|
+
isDefinition: true,
|
|
182
|
+
line: def.startLine,
|
|
183
|
+
content: index.getLineContent(def.file, def.startLine),
|
|
184
|
+
signature: index.formatSignature(def)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Scan all files for usages
|
|
189
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
190
|
+
// Apply --file filter
|
|
191
|
+
if (fileFilter && filePath !== fileFilter) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// Apply filters
|
|
195
|
+
if (!index.matchesFilters(fileEntry.relativePath, options)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const content = index._readFile(filePath);
|
|
201
|
+
|
|
202
|
+
// Fast pre-check: skip if name doesn't appear in file at all
|
|
203
|
+
if (!content.includes(name)) continue;
|
|
204
|
+
|
|
205
|
+
const lines = content.split('\n');
|
|
206
|
+
|
|
207
|
+
// Try AST-based detection first (with per-operation cache)
|
|
208
|
+
const astUsages = index._getCachedUsages(filePath, name);
|
|
209
|
+
if (astUsages !== null) {
|
|
210
|
+
// Pre-compute: does any imported project file define this name?
|
|
211
|
+
// Used to filter namespace member expressions (e.g., DropdownMenuPrimitive.Separator)
|
|
212
|
+
// while keeping module access patterns (e.g., output.formatExample())
|
|
213
|
+
let _importedHasDef = null;
|
|
214
|
+
const importedFileHasDef = () => {
|
|
215
|
+
if (_importedHasDef !== null) return _importedHasDef;
|
|
216
|
+
const importedFiles = index.importGraph.get(filePath) || [];
|
|
217
|
+
_importedHasDef = importedFiles.some(imp => {
|
|
218
|
+
const impEntry = index.files.get(imp);
|
|
219
|
+
return impEntry?.symbols?.some(s => s.name === name);
|
|
220
|
+
});
|
|
221
|
+
return _importedHasDef;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
for (const u of astUsages) {
|
|
225
|
+
// Skip if this is a definition line (already added above)
|
|
226
|
+
if (definitions.some(d => d.file === filePath && d.startLine === u.line)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Filter member expressions with unrelated receivers in JS/TS/Python.
|
|
231
|
+
// Keeps: standalone usages, self/this/cls/super, method calls on known types,
|
|
232
|
+
// and module access (output.fn()) when the imported file defines the name.
|
|
233
|
+
// Filters: namespace access to external packages (DropdownMenuPrimitive.Separator).
|
|
234
|
+
if (u.receiver && !['self', 'this', 'cls', 'super'].includes(u.receiver) &&
|
|
235
|
+
fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust') {
|
|
236
|
+
const hasMethodDef = definitions.some(d => d.className);
|
|
237
|
+
if (!hasMethodDef && !importedFileHasDef()) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const lineContent = lines[u.line - 1] || '';
|
|
243
|
+
|
|
244
|
+
const usage = {
|
|
245
|
+
file: filePath,
|
|
246
|
+
relativePath: fileEntry.relativePath,
|
|
247
|
+
line: u.line,
|
|
248
|
+
content: lineContent,
|
|
249
|
+
usageType: u.usageType,
|
|
250
|
+
isDefinition: false,
|
|
251
|
+
...(u.receiver && { receiver: u.receiver })
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Add context lines if requested
|
|
255
|
+
if (options.context && options.context > 0) {
|
|
256
|
+
const idx = u.line - 1;
|
|
257
|
+
const before = [];
|
|
258
|
+
const after = [];
|
|
259
|
+
for (let i = 1; i <= options.context; i++) {
|
|
260
|
+
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
261
|
+
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
262
|
+
}
|
|
263
|
+
usage.before = before;
|
|
264
|
+
usage.after = after;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
usagesList.push(usage);
|
|
268
|
+
}
|
|
269
|
+
continue; // Skip to next file
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Fallback to regex-based detection
|
|
273
|
+
const regex = new RegExp('\\b' + escapeRegExp(name) + '\\b');
|
|
274
|
+
lines.forEach((line, idx) => {
|
|
275
|
+
const lineNum = idx + 1;
|
|
276
|
+
|
|
277
|
+
// Skip if this is a definition line
|
|
278
|
+
if (definitions.some(d => d.file === filePath && d.startLine === lineNum)) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (regex.test(line)) {
|
|
283
|
+
// Skip if codeOnly and line is comment/string
|
|
284
|
+
if (options.codeOnly && index.isCommentOrStringAtPosition(content, lineNum, 0, filePath)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Skip if the match is inside a string literal
|
|
289
|
+
if (index.isInsideStringAST(content, lineNum, line, name, filePath)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Classify usage type (AST-based, defaults to 'reference' for unsupported languages)
|
|
294
|
+
const usageType = index.classifyUsageAST(content, lineNum, name, filePath) ?? 'reference';
|
|
295
|
+
|
|
296
|
+
const usage = {
|
|
297
|
+
file: filePath,
|
|
298
|
+
relativePath: fileEntry.relativePath,
|
|
299
|
+
line: lineNum,
|
|
300
|
+
content: line,
|
|
301
|
+
usageType,
|
|
302
|
+
isDefinition: false
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Add context lines if requested
|
|
306
|
+
if (options.context && options.context > 0) {
|
|
307
|
+
const before = [];
|
|
308
|
+
const after = [];
|
|
309
|
+
for (let i = 1; i <= options.context; i++) {
|
|
310
|
+
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
311
|
+
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
312
|
+
}
|
|
313
|
+
usage.before = before;
|
|
314
|
+
usage.after = after;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
usagesList.push(usage);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
} catch (e) {
|
|
321
|
+
// Skip unreadable files
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Deduplicate same-file, same-line, same-usageType entries
|
|
326
|
+
// (e.g., `detectLanguage: parser.detectLanguage` has the name twice on one line)
|
|
327
|
+
const seen = new Set();
|
|
328
|
+
const deduped = [];
|
|
329
|
+
for (const u of usagesList) {
|
|
330
|
+
const key = `${u.file}:${u.line}:${u.usageType}:${u.isDefinition}`;
|
|
331
|
+
if (!seen.has(key)) {
|
|
332
|
+
seen.add(key);
|
|
333
|
+
deduped.push(u);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return deduped;
|
|
337
|
+
} finally { index._endOp(); }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Text/regex search across all project files
|
|
342
|
+
*
|
|
343
|
+
* @param {object} index - ProjectIndex instance
|
|
344
|
+
* @param {string} term - Search term (string or regex)
|
|
345
|
+
* @param {object} options - { caseSensitive, regex, codeOnly, file, exclude, in, context, top }
|
|
346
|
+
* @returns {Array} Search results with meta
|
|
347
|
+
*/
|
|
348
|
+
function search(index, term, options = {}) {
|
|
349
|
+
index._beginOp();
|
|
350
|
+
try {
|
|
351
|
+
const results = [];
|
|
352
|
+
let filesScanned = 0;
|
|
353
|
+
let filesSkipped = 0;
|
|
354
|
+
let filesFilteredByFlag = 0;
|
|
355
|
+
const regexFlags = options.caseSensitive ? 'g' : 'gi';
|
|
356
|
+
const useRegex = options.regex !== false; // Default: regex ON
|
|
357
|
+
let regex;
|
|
358
|
+
let regexFallback = false;
|
|
359
|
+
if (useRegex) {
|
|
360
|
+
try {
|
|
361
|
+
regex = new RegExp(term, regexFlags);
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// Invalid regex — fall back to plain text
|
|
364
|
+
regex = new RegExp(escapeRegExp(term), regexFlags);
|
|
365
|
+
regexFallback = e.message;
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
regex = new RegExp(escapeRegExp(term), regexFlags);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
372
|
+
// Apply --file filter
|
|
373
|
+
if (options.file) {
|
|
374
|
+
const fp = fileEntry.relativePath;
|
|
375
|
+
if (!fp.includes(options.file) && !fp.endsWith(options.file)) {
|
|
376
|
+
filesFilteredByFlag++;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Apply exclude/in filters
|
|
381
|
+
if ((options.exclude && options.exclude.length > 0) || options.in) {
|
|
382
|
+
if (!index.matchesFilters(fileEntry.relativePath, { exclude: options.exclude, in: options.in })) {
|
|
383
|
+
filesSkipped++;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
filesScanned++;
|
|
388
|
+
try {
|
|
389
|
+
const content = index._readFile(filePath);
|
|
390
|
+
const lines = content.split('\n');
|
|
391
|
+
const matches = [];
|
|
392
|
+
|
|
393
|
+
// Use AST-based filtering for codeOnly mode when language is supported
|
|
394
|
+
if (options.codeOnly) {
|
|
395
|
+
const language = detectLanguage(filePath);
|
|
396
|
+
if (language) {
|
|
397
|
+
try {
|
|
398
|
+
const parser = getParser(language);
|
|
399
|
+
const { findMatchesWithASTFilter } = require('../languages/utils');
|
|
400
|
+
const astMatches = findMatchesWithASTFilter(content, term, parser, { codeOnly: true, regex: useRegex });
|
|
401
|
+
|
|
402
|
+
for (const m of astMatches) {
|
|
403
|
+
const match = {
|
|
404
|
+
line: m.line,
|
|
405
|
+
content: m.content
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Add context lines if requested
|
|
409
|
+
if (options.context && options.context > 0) {
|
|
410
|
+
const idx = m.line - 1;
|
|
411
|
+
const before = [];
|
|
412
|
+
const after = [];
|
|
413
|
+
for (let i = 1; i <= options.context; i++) {
|
|
414
|
+
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
415
|
+
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
416
|
+
}
|
|
417
|
+
match.before = before;
|
|
418
|
+
match.after = after;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
matches.push(match);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (matches.length > 0) {
|
|
425
|
+
results.push({
|
|
426
|
+
file: fileEntry.relativePath,
|
|
427
|
+
matches
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
continue; // Skip to next file
|
|
431
|
+
} catch (e) {
|
|
432
|
+
// Fall through to regex-based search
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Fallback to regex-based search (non-codeOnly or unsupported language)
|
|
438
|
+
lines.forEach((line, idx) => {
|
|
439
|
+
regex.lastIndex = 0; // Reset regex state
|
|
440
|
+
if (regex.test(line)) {
|
|
441
|
+
const lineNum = idx + 1;
|
|
442
|
+
// Skip if codeOnly and line is comment/string
|
|
443
|
+
if (options.codeOnly && index.isCommentOrStringAtPosition(content, lineNum, 0, filePath)) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const match = {
|
|
448
|
+
line: idx + 1,
|
|
449
|
+
content: line
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// Add context lines if requested
|
|
453
|
+
if (options.context && options.context > 0) {
|
|
454
|
+
const before = [];
|
|
455
|
+
const after = [];
|
|
456
|
+
for (let i = 1; i <= options.context; i++) {
|
|
457
|
+
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
458
|
+
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
459
|
+
}
|
|
460
|
+
match.before = before;
|
|
461
|
+
match.after = after;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
matches.push(match);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
if (matches.length > 0) {
|
|
469
|
+
results.push({
|
|
470
|
+
file: fileEntry.relativePath,
|
|
471
|
+
matches
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
} catch (e) {
|
|
475
|
+
// Expected: binary/minified files fail to read or parse.
|
|
476
|
+
// These are not actionable errors — silently skip.
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Apply top limit (limits total matches across all files)
|
|
481
|
+
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
482
|
+
let truncatedMatches = 0;
|
|
483
|
+
if (options.top && options.top > 0 && totalMatches > options.top) {
|
|
484
|
+
let remaining = options.top;
|
|
485
|
+
const truncated = [];
|
|
486
|
+
for (const r of results) {
|
|
487
|
+
if (remaining <= 0) break;
|
|
488
|
+
if (r.matches.length <= remaining) {
|
|
489
|
+
truncated.push(r);
|
|
490
|
+
remaining -= r.matches.length;
|
|
491
|
+
} else {
|
|
492
|
+
truncated.push({ ...r, matches: r.matches.slice(0, remaining) });
|
|
493
|
+
remaining = 0;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
truncatedMatches = totalMatches - options.top;
|
|
497
|
+
results.length = 0;
|
|
498
|
+
results.push(...truncated);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
results.meta = { filesScanned, filesSkipped, filesFilteredByFlag, totalFiles: index.files.size, regexFallback, totalMatches, truncatedMatches, projectLanguage: index._getPredominantLanguage() };
|
|
502
|
+
return results;
|
|
503
|
+
} finally { index._endOp(); }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Structural search — query the symbol table and call index, not raw text.
|
|
508
|
+
* Answers questions like "functions taking Request param", "all db.* calls",
|
|
509
|
+
* "exported async functions", "decorated route handlers".
|
|
510
|
+
*
|
|
511
|
+
* @param {object} index - ProjectIndex instance
|
|
512
|
+
* @param {object} options
|
|
513
|
+
* @param {string} [options.term] - Name filter (glob: * and ? supported)
|
|
514
|
+
* @param {string} [options.type] - Symbol kind: function, class, call, method, type
|
|
515
|
+
* @param {string} [options.param] - Parameter name or type substring
|
|
516
|
+
* @param {string} [options.receiver] - Call receiver pattern (for type=call)
|
|
517
|
+
* @param {string} [options.returns] - Return type substring
|
|
518
|
+
* @param {string} [options.decorator] - Decorator/annotation name substring
|
|
519
|
+
* @param {boolean} [options.exported] - Only exported symbols
|
|
520
|
+
* @param {boolean} [options.unused] - Only symbols with zero callers
|
|
521
|
+
* @param {string[]} [options.exclude] - Exclude file patterns
|
|
522
|
+
* @param {string} [options.in] - Restrict to subdirectory
|
|
523
|
+
* @param {string} [options.file] - File pattern filter
|
|
524
|
+
* @param {number} [options.top] - Limit results
|
|
525
|
+
* @returns {{ results: Array, meta: object }}
|
|
526
|
+
*/
|
|
527
|
+
function structuralSearch(index, options = {}) {
|
|
528
|
+
index._beginOp();
|
|
529
|
+
try {
|
|
530
|
+
const { term, param, receiver, returns: returnType, decorator, exported, unused } = options;
|
|
531
|
+
// Auto-infer type: --receiver implies type=call
|
|
532
|
+
const type = options.type || (receiver ? 'call' : undefined);
|
|
533
|
+
const results = [];
|
|
534
|
+
|
|
535
|
+
// Validate type if provided
|
|
536
|
+
if (type && !STRUCTURAL_TYPES.has(type)) {
|
|
537
|
+
return {
|
|
538
|
+
results: [],
|
|
539
|
+
meta: {
|
|
540
|
+
mode: 'structural',
|
|
541
|
+
query: { type },
|
|
542
|
+
totalMatched: 0,
|
|
543
|
+
shown: 0,
|
|
544
|
+
error: `Invalid type "${type}". Valid types: ${[...STRUCTURAL_TYPES].join(', ')}`,
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Build glob-style name matcher from term
|
|
550
|
+
const nameMatcher = term ? buildGlobMatcher(term, options.caseSensitive) : null;
|
|
551
|
+
|
|
552
|
+
// Helper: check if file passes filters
|
|
553
|
+
const passesFileFilter = (fileEntry) => {
|
|
554
|
+
if (!fileEntry) return false;
|
|
555
|
+
if (options.file) {
|
|
556
|
+
const rp = fileEntry.relativePath;
|
|
557
|
+
if (!rp.includes(options.file) && !rp.endsWith(options.file)) return false;
|
|
558
|
+
}
|
|
559
|
+
if ((options.exclude && options.exclude.length > 0) || options.in) {
|
|
560
|
+
if (!index.matchesFilters(fileEntry.relativePath, { exclude: options.exclude, in: options.in })) return false;
|
|
561
|
+
}
|
|
562
|
+
return true;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
if (type === 'call') {
|
|
566
|
+
// Search call sites from callee index
|
|
567
|
+
const { getCachedCalls } = require('./callers');
|
|
568
|
+
const seenFiles = new Set();
|
|
569
|
+
|
|
570
|
+
// If term is given, only scan files that might contain that call
|
|
571
|
+
if (term && !term.includes('*') && !term.includes('?')) {
|
|
572
|
+
// Exact or substring — use callee index for fast lookup
|
|
573
|
+
index.buildCalleeIndex();
|
|
574
|
+
const files = index.calleeIndex.get(term);
|
|
575
|
+
if (files) for (const f of files) seenFiles.add(f);
|
|
576
|
+
} else {
|
|
577
|
+
// Scan all files
|
|
578
|
+
for (const fp of index.files.keys()) seenFiles.add(fp);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
for (const filePath of seenFiles) {
|
|
582
|
+
const fileEntry = index.files.get(filePath);
|
|
583
|
+
if (!passesFileFilter(fileEntry)) continue;
|
|
584
|
+
const calls = getCachedCalls(index, filePath);
|
|
585
|
+
if (!calls) continue;
|
|
586
|
+
for (const call of calls) {
|
|
587
|
+
if (nameMatcher && !nameMatcher(call.name)) continue;
|
|
588
|
+
if (receiver) {
|
|
589
|
+
if (!call.receiver) continue;
|
|
590
|
+
if (!matchesSubstring(call.receiver, receiver, options.caseSensitive)) continue;
|
|
591
|
+
}
|
|
592
|
+
results.push({
|
|
593
|
+
kind: 'call',
|
|
594
|
+
name: call.receiver ? `${call.receiver}.${call.name}` : call.name,
|
|
595
|
+
file: fileEntry.relativePath,
|
|
596
|
+
line: call.line,
|
|
597
|
+
receiver: call.receiver || null,
|
|
598
|
+
isMethod: call.isMethod || false,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
// Search symbols (functions, classes, methods, types)
|
|
604
|
+
const functionTypes = new Set(['function', 'constructor', 'method', 'arrow', 'static', 'classmethod', 'abstract']);
|
|
605
|
+
const classTypes = new Set(['class', 'struct', 'interface', 'impl', 'trait']);
|
|
606
|
+
const typeTypes = new Set(['type', 'enum', 'interface', 'trait']);
|
|
607
|
+
const methodTypes = new Set(['method', 'constructor']);
|
|
608
|
+
|
|
609
|
+
for (const [symbolName, definitions] of index.symbols) {
|
|
610
|
+
if (nameMatcher && !nameMatcher(symbolName)) continue;
|
|
611
|
+
|
|
612
|
+
for (const def of definitions) {
|
|
613
|
+
// Type filter
|
|
614
|
+
if (type === 'function' && !functionTypes.has(def.type)) continue;
|
|
615
|
+
if (type === 'class' && !classTypes.has(def.type)) continue;
|
|
616
|
+
if (type === 'method' && !methodTypes.has(def.type) && !def.isMethod) continue;
|
|
617
|
+
if (type === 'type' && !typeTypes.has(def.type)) continue;
|
|
618
|
+
|
|
619
|
+
// File filters
|
|
620
|
+
const fileEntry = index.files.get(def.file);
|
|
621
|
+
if (!passesFileFilter(fileEntry)) continue;
|
|
622
|
+
|
|
623
|
+
// Param filter: match param name or type
|
|
624
|
+
if (param) {
|
|
625
|
+
const cs = options.caseSensitive;
|
|
626
|
+
const ps = def.paramsStructured || [];
|
|
627
|
+
const paramStr = def.params || '';
|
|
628
|
+
const hasMatch = ps.some(p =>
|
|
629
|
+
matchesSubstring(p.name, param, cs) ||
|
|
630
|
+
(p.type && matchesSubstring(p.type, param, cs))
|
|
631
|
+
) || matchesSubstring(paramStr, param, cs);
|
|
632
|
+
if (!hasMatch) continue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Receiver filter: match className for methods
|
|
636
|
+
if (receiver) {
|
|
637
|
+
if (!def.className || !matchesSubstring(def.className, receiver, options.caseSensitive)) continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Return type filter
|
|
641
|
+
if (returnType) {
|
|
642
|
+
if (!def.returnType || !matchesSubstring(def.returnType, returnType, options.caseSensitive)) continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Decorator filter: checks decorators (Python), modifiers (Java annotations stored lowercase)
|
|
646
|
+
if (decorator) {
|
|
647
|
+
const cs = options.caseSensitive;
|
|
648
|
+
const hasDecorator = (def.decorators && def.decorators.some(d => matchesSubstring(d, decorator, cs))) ||
|
|
649
|
+
(def.modifiers && def.modifiers.some(m => matchesSubstring(m, decorator, cs)));
|
|
650
|
+
if (!hasDecorator) continue;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Exported filter
|
|
654
|
+
if (exported) {
|
|
655
|
+
const mods = def.modifiers || [];
|
|
656
|
+
const isExp = (fileEntry && fileEntry.exports.includes(symbolName)) ||
|
|
657
|
+
mods.includes('export') || mods.includes('public') ||
|
|
658
|
+
mods.some(m => m.startsWith('pub')) ||
|
|
659
|
+
(fileEntry && langTraits(fileEntry.language)?.exportVisibility === 'capitalization' && /^[A-Z]/.test(symbolName));
|
|
660
|
+
if (!isExp) continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Unused filter (expensive — last check)
|
|
664
|
+
if (unused) {
|
|
665
|
+
index.buildCalleeIndex();
|
|
666
|
+
if (index.calleeIndex.has(symbolName)) continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Merge decorators from both Python-style decorators and Java-style modifiers
|
|
670
|
+
const allDecorators = def.decorators || null;
|
|
671
|
+
|
|
672
|
+
results.push({
|
|
673
|
+
kind: def.type,
|
|
674
|
+
name: symbolName,
|
|
675
|
+
file: def.relativePath,
|
|
676
|
+
line: def.startLine,
|
|
677
|
+
params: def.params || null,
|
|
678
|
+
returnType: def.returnType || null,
|
|
679
|
+
decorators: allDecorators,
|
|
680
|
+
className: def.className || null,
|
|
681
|
+
exported: exported ? true : undefined,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Sort by file, then line
|
|
688
|
+
results.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
689
|
+
|
|
690
|
+
// Apply top limit
|
|
691
|
+
const total = results.length;
|
|
692
|
+
const top = options.top;
|
|
693
|
+
if (top && top > 0 && results.length > top) {
|
|
694
|
+
results.length = top;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
results,
|
|
699
|
+
meta: {
|
|
700
|
+
mode: 'structural',
|
|
701
|
+
query: Object.fromEntries(Object.entries({
|
|
702
|
+
type: type || 'any', term, param, receiver, returns: returnType,
|
|
703
|
+
decorator, exported: exported || undefined, unused: unused || undefined,
|
|
704
|
+
}).filter(([, v]) => v !== undefined && v !== null)),
|
|
705
|
+
totalMatched: total,
|
|
706
|
+
shown: results.length,
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
} finally { index._endOp(); }
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Find the best usage example of a function
|
|
714
|
+
*
|
|
715
|
+
* @param {object} index - ProjectIndex instance
|
|
716
|
+
* @param {string} name - Function name
|
|
717
|
+
* @param {object} options - { className }
|
|
718
|
+
* @returns {object|null} Best example with score
|
|
719
|
+
*/
|
|
720
|
+
function example(index, name, options = {}) {
|
|
721
|
+
index._beginOp();
|
|
722
|
+
try {
|
|
723
|
+
const usageResults = usages(index, name, {
|
|
724
|
+
codeOnly: true,
|
|
725
|
+
className: options.className,
|
|
726
|
+
exclude: ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'],
|
|
727
|
+
context: 5
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const calls = usageResults.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
731
|
+
if (calls.length === 0) return null;
|
|
732
|
+
|
|
733
|
+
const scored = calls.map(call => {
|
|
734
|
+
let score = 0;
|
|
735
|
+
const reasons = [];
|
|
736
|
+
const line = call.content.trim();
|
|
737
|
+
|
|
738
|
+
const astInfo = index._analyzeCallSiteAST(call.file, call.line, name);
|
|
739
|
+
|
|
740
|
+
if (astInfo.isTypedAssignment) { score += 15; reasons.push('typed assignment'); }
|
|
741
|
+
if (astInfo.isInReturn) { score += 10; reasons.push('in return'); }
|
|
742
|
+
if (astInfo.isAwait) { score += 10; reasons.push('async usage'); }
|
|
743
|
+
if (astInfo.isDestructured) { score += 8; reasons.push('destructured'); }
|
|
744
|
+
if (astInfo.isStandalone) { score += 5; reasons.push('standalone'); }
|
|
745
|
+
if (astInfo.hasComment) { score += 3; reasons.push('documented'); }
|
|
746
|
+
if (astInfo.isInCatch) { score -= 5; reasons.push('in catch block'); }
|
|
747
|
+
if (astInfo.isInConditional) { score -= 3; reasons.push('in conditional'); }
|
|
748
|
+
|
|
749
|
+
if (score === 0) {
|
|
750
|
+
if (/^(const|let|var|return)\s/.test(line) || /^\w+\s*=/.test(line)) {
|
|
751
|
+
score += 10; reasons.push('return value used');
|
|
752
|
+
}
|
|
753
|
+
if (line.startsWith(name + '(') || /^(const|let|var)\s+\w+\s*=\s*\w*$/.test(line.split(name)[0])) {
|
|
754
|
+
score += 5; reasons.push('clear usage');
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (call.before && call.before.length > 0) score += 3;
|
|
759
|
+
if (call.after && call.after.length > 0) score += 3;
|
|
760
|
+
if (call.before?.length > 0 && call.after?.length > 0) reasons.push('has context');
|
|
761
|
+
|
|
762
|
+
const beforeCall = line.split(name + '(')[0];
|
|
763
|
+
if (!beforeCall.includes('(') || /^\s*(const|let|var|return)?\s*\w+\s*=\s*$/.test(beforeCall)) {
|
|
764
|
+
score += 2;
|
|
765
|
+
}
|
|
766
|
+
if (call.line < 100) score += 1;
|
|
767
|
+
|
|
768
|
+
return { ...call, score, reasons };
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
scored.sort((a, b) => b.score - a.score);
|
|
772
|
+
return { best: scored[0], totalCalls: calls.length };
|
|
773
|
+
} finally { index._endOp(); }
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Find type definitions
|
|
778
|
+
*
|
|
779
|
+
* @param {object} index - ProjectIndex instance
|
|
780
|
+
* @param {string} name - Type name to find
|
|
781
|
+
* @param {object} options - Find options
|
|
782
|
+
* @returns {Array} Matching type definitions
|
|
783
|
+
*/
|
|
784
|
+
function typedef(index, name, options = {}) {
|
|
785
|
+
const typeKinds = ['type', 'interface', 'enum', 'struct', 'trait', 'class', 'record'];
|
|
786
|
+
const matches = find(index, name, options);
|
|
787
|
+
|
|
788
|
+
return matches.filter(m => typeKinds.includes(m.type)).map(m => ({
|
|
789
|
+
...m,
|
|
790
|
+
code: index.extractCode(m)
|
|
791
|
+
}));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Find tests for a function or file
|
|
796
|
+
*
|
|
797
|
+
* @param {object} index - ProjectIndex instance
|
|
798
|
+
* @param {string} nameOrFile - Function name or file path
|
|
799
|
+
* @param {object} options - { callsOnly }
|
|
800
|
+
* @returns {Array} Test files and matches
|
|
801
|
+
*/
|
|
802
|
+
function tests(index, nameOrFile, options = {}) {
|
|
803
|
+
index._beginOp();
|
|
804
|
+
try {
|
|
805
|
+
const results = [];
|
|
806
|
+
|
|
807
|
+
// Check if it's a file path
|
|
808
|
+
const isFilePath = nameOrFile.includes('/') || nameOrFile.includes('\\') ||
|
|
809
|
+
nameOrFile.endsWith('.js') || nameOrFile.endsWith('.ts') ||
|
|
810
|
+
nameOrFile.endsWith('.py') || nameOrFile.endsWith('.go') ||
|
|
811
|
+
nameOrFile.endsWith('.java') || nameOrFile.endsWith('.rs');
|
|
812
|
+
|
|
813
|
+
// Find all test files
|
|
814
|
+
const testFiles = [];
|
|
815
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
816
|
+
if (isTestFile(fileEntry.relativePath, fileEntry.language)) {
|
|
817
|
+
testFiles.push({ path: filePath, entry: fileEntry });
|
|
818
|
+
} else if (fileEntry.language === 'rust') {
|
|
819
|
+
// Rust idiomatically puts tests in #[cfg(test)] modules inside source files.
|
|
820
|
+
// Check if file has any symbols with 'test' modifier (#[test] attribute).
|
|
821
|
+
const hasInlineTests = fileEntry.symbols?.some(s =>
|
|
822
|
+
s.modifiers?.includes('test')
|
|
823
|
+
);
|
|
824
|
+
if (hasInlineTests) {
|
|
825
|
+
testFiles.push({ path: filePath, entry: fileEntry });
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const searchTerm = isFilePath
|
|
831
|
+
? path.basename(nameOrFile, path.extname(nameOrFile))
|
|
832
|
+
: nameOrFile;
|
|
833
|
+
|
|
834
|
+
// Note: no 'g' flag - we only need to test for presence per line
|
|
835
|
+
// The 'i' flag is kept for case-insensitive matching
|
|
836
|
+
const regex = new RegExp('\\b' + escapeRegExp(searchTerm) + '\\b', 'i');
|
|
837
|
+
// Pre-compile patterns used inside per-line loop
|
|
838
|
+
const callPattern = new RegExp(escapeRegExp(searchTerm) + '\\s*\\(');
|
|
839
|
+
const strPattern = new RegExp("['\"`]" + escapeRegExp(searchTerm) + "['\"`]");
|
|
840
|
+
|
|
841
|
+
for (const { path: testPath, entry } of testFiles) {
|
|
842
|
+
try {
|
|
843
|
+
const content = index._readFile(testPath);
|
|
844
|
+
const lines = content.split('\n');
|
|
845
|
+
const matches = [];
|
|
846
|
+
|
|
847
|
+
lines.forEach((line, idx) => {
|
|
848
|
+
if (regex.test(line)) {
|
|
849
|
+
let matchType = 'reference';
|
|
850
|
+
if (/\b(describe|it|test|spec)\s*\(/.test(line)) {
|
|
851
|
+
matchType = 'test-case';
|
|
852
|
+
} else if (/\b(import|require|from)\b/.test(line)) {
|
|
853
|
+
matchType = 'import';
|
|
854
|
+
} else if (callPattern.test(line)) {
|
|
855
|
+
matchType = 'call';
|
|
856
|
+
}
|
|
857
|
+
// Detect if the match is inside a string literal (e.g., 'parseFile' or "parseFile")
|
|
858
|
+
if (matchType === 'reference' || matchType === 'call') {
|
|
859
|
+
if (strPattern.test(line)) {
|
|
860
|
+
matchType = 'string-ref';
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
matches.push({
|
|
865
|
+
line: idx + 1,
|
|
866
|
+
content: line.trim(),
|
|
867
|
+
matchType
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
const filtered = options.callsOnly
|
|
873
|
+
? matches.filter(m => m.matchType === 'call' || m.matchType === 'test-case')
|
|
874
|
+
: matches;
|
|
875
|
+
if (filtered.length > 0) {
|
|
876
|
+
results.push({
|
|
877
|
+
file: entry.relativePath,
|
|
878
|
+
matches: filtered
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
} catch (e) {
|
|
882
|
+
// Skip unreadable files
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return results;
|
|
887
|
+
} finally { index._endOp(); }
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
module.exports = { find, _applyFindFilters, usages, search, structuralSearch, example, typedef, tests };
|