ucn 4.2.2 → 5.0.1
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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +445 -300
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -131
- package/core/cache.js +533 -11
- package/core/callers.js +5533 -494
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +421 -20
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +204 -42
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +216 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -177
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +371 -116
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +428 -16
- package/languages/javascript.js +452 -49
- package/languages/python.js +1041 -32
- package/languages/rust.js +1415 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +41 -24
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/search.js
CHANGED
|
@@ -8,11 +8,19 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { escapeRegExp, codeUnitCompare, inlineTestRanges, lineInRanges, classDispatchNames } = require('./shared');
|
|
11
|
+
const { escapeRegExp, codeUnitCompare, inlineTestRanges, lineInRanges, classDispatchNames, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
|
-
const { detectLanguage, getParser,
|
|
14
|
-
const { getCachedCalls
|
|
13
|
+
const { detectLanguage, getParser, getLanguageAdapter, langTraits } = require('../languages');
|
|
14
|
+
const { getCachedCalls } = require('./callers');
|
|
15
15
|
const { extractImports } = require('./imports');
|
|
16
|
+
const isSafeRegex = require('safe-regex2');
|
|
17
|
+
const { RE2JS } = require('re2js');
|
|
18
|
+
|
|
19
|
+
// Keep the established search-input contract for the canonical catastrophic
|
|
20
|
+
// nested-single-atom family even though the RE2-compatible engine could run it
|
|
21
|
+
// safely. Rejecting these patterns avoids silently changing behavior if an
|
|
22
|
+
// advanced construct later forces the guarded JavaScript fallback.
|
|
23
|
+
const NESTED_SINGLE_ATOM_REPEAT = /\((?:\?:)?(?:\\.|\[[^\]]*\]|[^\\()[\]{}|?+*])(?:[+*]|\{\d+(?:,\d*)?\})\)(?:[+*]|\{\d+(?:,\d*)?\})/;
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* Build a glob-style matcher: * matches any sequence, ? matches one char.
|
|
@@ -26,7 +34,11 @@ function buildGlobMatcher(pattern, caseSensitive) {
|
|
|
26
34
|
return (name) => regex.test(name);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
const STRUCTURAL_TYPES = new Set([
|
|
37
|
+
const STRUCTURAL_TYPES = new Set([
|
|
38
|
+
'function', 'class', 'call', 'method', 'type', 'constructor',
|
|
39
|
+
'state', 'field', 'constant', 'macro', 'variable',
|
|
40
|
+
'interface', 'enum', 'struct', 'trait', 'record', 'namespace',
|
|
41
|
+
]);
|
|
30
42
|
|
|
31
43
|
/**
|
|
32
44
|
* Substring match. Case-insensitive by default.
|
|
@@ -37,6 +49,61 @@ function matchesSubstring(text, pattern, caseSensitive) {
|
|
|
37
49
|
return text.toLowerCase().includes(pattern.toLowerCase());
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
function literalNameRegex(name) {
|
|
53
|
+
return new RegExp(
|
|
54
|
+
`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Complete the AST-classified usage inventory with literal-name lines that
|
|
60
|
+
* occur only in comments, strings, or docstrings. Do not reintroduce code
|
|
61
|
+
* references that the language adapter intentionally filtered (for example,
|
|
62
|
+
* a Rust enum variant that shares a struct's name).
|
|
63
|
+
* `usages` promises this observed-text complement unless codeOnly=true.
|
|
64
|
+
*/
|
|
65
|
+
function appendTextComplements(index, {
|
|
66
|
+
usagesList,
|
|
67
|
+
filePath,
|
|
68
|
+
fileEntry,
|
|
69
|
+
content,
|
|
70
|
+
lines,
|
|
71
|
+
name,
|
|
72
|
+
context,
|
|
73
|
+
}) {
|
|
74
|
+
const represented = new Set(usagesList
|
|
75
|
+
.filter(usage => usage.file === filePath)
|
|
76
|
+
.map(usage => usage.line));
|
|
77
|
+
const pattern = literalNameRegex(name);
|
|
78
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
79
|
+
const line = lines[idx];
|
|
80
|
+
const match = pattern.exec(line);
|
|
81
|
+
if (!match || represented.has(idx + 1)) continue;
|
|
82
|
+
const lineNum = idx + 1;
|
|
83
|
+
const commentOrString = index.isCommentOrStringAtPosition(
|
|
84
|
+
content, lineNum, match.index, filePath);
|
|
85
|
+
if (!commentOrString) continue;
|
|
86
|
+
const usage = {
|
|
87
|
+
file: filePath,
|
|
88
|
+
relativePath: fileEntry.relativePath,
|
|
89
|
+
line: lineNum,
|
|
90
|
+
content: line,
|
|
91
|
+
usageType: 'text',
|
|
92
|
+
textKind: 'comment-or-string',
|
|
93
|
+
isDefinition: false,
|
|
94
|
+
};
|
|
95
|
+
if (context > 0) {
|
|
96
|
+
usage.before = [];
|
|
97
|
+
usage.after = [];
|
|
98
|
+
for (let i = 1; i <= context; i++) {
|
|
99
|
+
if (idx - i >= 0) usage.before.unshift(lines[idx - i]);
|
|
100
|
+
if (idx + i < lines.length) usage.after.push(lines[idx + i]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
usagesList.push(usage);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
40
107
|
/**
|
|
41
108
|
* Find symbols by name with fuzzy/glob matching.
|
|
42
109
|
*
|
|
@@ -116,9 +183,11 @@ function _applyFindFilters(index, matches, options) {
|
|
|
116
183
|
|
|
117
184
|
// Filter by file pattern
|
|
118
185
|
if (options.file) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
186
|
+
const resolved = index.resolveFilePathForQuery(options.file);
|
|
187
|
+
filtered = typeof resolved === 'string'
|
|
188
|
+
? filtered.filter(m => m.file === resolved)
|
|
189
|
+
: filtered.filter(m =>
|
|
190
|
+
m.relativePath && m.relativePath.includes(options.file));
|
|
122
191
|
}
|
|
123
192
|
|
|
124
193
|
// Apply semantic filters (--exclude, --in)
|
|
@@ -136,10 +205,40 @@ function _applyFindFilters(index, matches, options) {
|
|
|
136
205
|
// Add per-symbol usage counts for disambiguation
|
|
137
206
|
const withCounts = filtered.map(m => {
|
|
138
207
|
const counts = index.countSymbolUsages(m);
|
|
208
|
+
// The fast count supplies cheap definition/import totals, but its
|
|
209
|
+
// name-only call bucket cannot distinguish json.dumps from a project
|
|
210
|
+
// dumps, or one class's save from another's. The public `find`
|
|
211
|
+
// activity count must use the same pinned caller adjudication as
|
|
212
|
+
// show/impact so command answers never contradict each other.
|
|
213
|
+
const callers = index.findCallers(m.name, {
|
|
214
|
+
targetDefinitions: [m],
|
|
215
|
+
includeTests: true,
|
|
216
|
+
collectAccount: true,
|
|
217
|
+
});
|
|
218
|
+
const confirmedCalls = callers.filter(caller =>
|
|
219
|
+
caller.tier !== 'unverified').length;
|
|
220
|
+
const unverifiedCalls = callers.unverifiedEntries?.length || 0;
|
|
221
|
+
const otherTargetCalls = callers.accountRaw?.excludedEntries?.length || 0;
|
|
222
|
+
// `find` is an inventory/orientation command. Its activity headline
|
|
223
|
+
// must not turn engine abstention into "0 calls", but it also must not
|
|
224
|
+
// attribute calls adjudicated as a different same-name target to this
|
|
225
|
+
// pinned definition. Report confirmed + visible unverified candidates;
|
|
226
|
+
// retain excluded same-name calls as an explicit boundary only.
|
|
227
|
+
const targetCallCandidates = confirmedCalls + unverifiedCalls;
|
|
228
|
+
const exactCounts = {
|
|
229
|
+
...counts,
|
|
230
|
+
calls: targetCallCandidates,
|
|
231
|
+
confirmedCalls,
|
|
232
|
+
unverifiedCalls,
|
|
233
|
+
otherTargetCalls,
|
|
234
|
+
total: targetCallCandidates + counts.definitions + counts.imports +
|
|
235
|
+
(counts.references || 0),
|
|
236
|
+
countKind: 'target-call-candidates-with-tier-breakdown-excludes-references',
|
|
237
|
+
};
|
|
139
238
|
return {
|
|
140
239
|
...m,
|
|
141
|
-
usageCount:
|
|
142
|
-
usageCounts:
|
|
240
|
+
usageCount: exactCounts.total,
|
|
241
|
+
usageCounts: exactCounts,
|
|
143
242
|
};
|
|
144
243
|
});
|
|
145
244
|
|
|
@@ -162,7 +261,8 @@ function usages(index, name, options = {}) {
|
|
|
162
261
|
try {
|
|
163
262
|
const usagesList = [];
|
|
164
263
|
|
|
165
|
-
// Resolve file pattern for --file filter
|
|
264
|
+
// Resolve file pattern for --file scan filter. A stable-handle caller can
|
|
265
|
+
// separately pin definitions with definitionFile while scanning globally.
|
|
166
266
|
const fileFilterRaw = options.file ? index.resolveFilePathForQuery(options.file) : null;
|
|
167
267
|
// resolveFilePathForQuery may return error objects for ambiguous/not-found — fall back to substring matching
|
|
168
268
|
const fileFilter = typeof fileFilterRaw === 'string' ? fileFilterRaw : null;
|
|
@@ -173,7 +273,12 @@ function usages(index, name, options = {}) {
|
|
|
173
273
|
if (options.className) {
|
|
174
274
|
allDefinitions = allDefinitions.filter(d => d.className === options.className);
|
|
175
275
|
}
|
|
176
|
-
|
|
276
|
+
const definitionFileRaw = options.definitionFile
|
|
277
|
+
? index.resolveFilePathForQuery(options.definitionFile) : null;
|
|
278
|
+
const definitionFile = typeof definitionFileRaw === 'string' ? definitionFileRaw : null;
|
|
279
|
+
if (definitionFile) {
|
|
280
|
+
allDefinitions = allDefinitions.filter(d => d.file === definitionFile);
|
|
281
|
+
} else if (fileFilter) {
|
|
177
282
|
allDefinitions = allDefinitions.filter(d => d.file === fileFilter);
|
|
178
283
|
} else if (fileSubstring) {
|
|
179
284
|
allDefinitions = allDefinitions.filter(d => d.relativePath && d.relativePath.includes(fileSubstring));
|
|
@@ -181,14 +286,13 @@ function usages(index, name, options = {}) {
|
|
|
181
286
|
const definitions = options.exclude || options.in
|
|
182
287
|
? allDefinitions.filter(d => index.matchesFilters(d.relativePath, options))
|
|
183
288
|
: allDefinitions;
|
|
184
|
-
const targetFiles = new Set(allDefinitions.map(d => d.file).filter(Boolean));
|
|
185
|
-
|
|
186
289
|
for (const def of definitions) {
|
|
187
290
|
usagesList.push({
|
|
188
291
|
...def,
|
|
292
|
+
usageType: 'definition',
|
|
189
293
|
isDefinition: true,
|
|
190
|
-
line: def.startLine,
|
|
191
|
-
content: index.getLineContent(def.file, def.startLine),
|
|
294
|
+
line: def.nameLine || def.startLine,
|
|
295
|
+
content: index.getLineContent(def.file, def.nameLine || def.startLine),
|
|
192
296
|
signature: index.formatSignature(def)
|
|
193
297
|
});
|
|
194
298
|
}
|
|
@@ -217,65 +321,17 @@ function usages(index, name, options = {}) {
|
|
|
217
321
|
// Try AST-based detection first (with per-operation cache)
|
|
218
322
|
const astUsages = index._getCachedUsages(filePath, name);
|
|
219
323
|
if (astUsages !== null) {
|
|
220
|
-
// Pre-compute: does any imported project file define this name?
|
|
221
|
-
// Used to filter namespace member expressions (e.g., DropdownMenuPrimitive.Separator)
|
|
222
|
-
// while keeping module access patterns (e.g., output.formatExample())
|
|
223
|
-
let _importedHasDef = null;
|
|
224
|
-
const importedFileHasDef = () => {
|
|
225
|
-
if (_importedHasDef !== null) return _importedHasDef;
|
|
226
|
-
const importedFiles = index.importGraph.get(filePath);
|
|
227
|
-
_importedHasDef = false;
|
|
228
|
-
if (importedFiles) for (const imp of importedFiles) {
|
|
229
|
-
const impEntry = index.files.get(imp);
|
|
230
|
-
if (impEntry?.symbols?.some(s => s.name === name)) {
|
|
231
|
-
_importedHasDef = true;
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
// A module namespace may expose the target through a
|
|
235
|
-
// re-export chain (`import httpx; httpx.URL(...)`,
|
|
236
|
-
// where httpx/__init__.py re-exports URL). Direct-file
|
|
237
|
-
// symbol checks silently dropped these compiler-true
|
|
238
|
-
// usages. Reuse the caller engine's conservative
|
|
239
|
-
// name-ownership chase: yes confirms; unknown stays
|
|
240
|
-
// visible in this raw-usage inventory; only a proven
|
|
241
|
-
// no is filtering evidence.
|
|
242
|
-
if (targetFiles.size > 0 &&
|
|
243
|
-
_nameBindingReaches(index, imp, name, targetFiles) !== 'no') {
|
|
244
|
-
_importedHasDef = true;
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return _importedHasDef;
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
// A qualified usage whose RECEIVER is a symbol defined in this
|
|
252
|
-
// file is never external-package noise — `Geometry.area(3, 4)`
|
|
253
|
-
// in the file declaring namespace Geometry was dropped by the
|
|
254
|
-
// receiver filter while find's usageCounts counted it (fix
|
|
255
|
-
// #241). Keyed on the receiver, not the target name: a file
|
|
256
|
-
// defining its own `Separator` while using external
|
|
257
|
-
// `Ns.Separator` must still filter the latter (bug #23).
|
|
258
|
-
const receiverDefinedHere = (recv) =>
|
|
259
|
-
!!recv && fileEntry.symbols && fileEntry.symbols.some(s => s.name === recv);
|
|
260
|
-
|
|
261
324
|
for (const u of astUsages) {
|
|
262
325
|
// Skip if this is a definition line (already added above)
|
|
263
|
-
if (definitions.some(d => d.file === filePath &&
|
|
326
|
+
if (definitions.some(d => d.file === filePath &&
|
|
327
|
+
(d.startLine === u.line || d.nameLine === u.line))) {
|
|
264
328
|
continue;
|
|
265
329
|
}
|
|
266
330
|
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
// Filters: namespace access to external packages (DropdownMenuPrimitive.Separator).
|
|
272
|
-
if (u.receiver && !['self', 'this', 'cls', 'super'].includes(u.receiver) &&
|
|
273
|
-
fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust') {
|
|
274
|
-
const hasMethodDef = definitions.some(d => d.className);
|
|
275
|
-
if (!hasMethodDef && !receiverDefinedHere(u.receiver) && !importedFileHasDef()) {
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
331
|
+
// `usages` is the literal-name escape hatch. Unlike callers,
|
|
332
|
+
// it must never discard an AST occurrence merely because
|
|
333
|
+
// ownership or receiver dispatch cannot be proven. Consumers
|
|
334
|
+
// can use usageType/receiver plus `show` for semantic tiers.
|
|
279
335
|
|
|
280
336
|
const lineContent = lines[u.line - 1] || '';
|
|
281
337
|
|
|
@@ -317,6 +373,17 @@ function usages(index, name, options = {}) {
|
|
|
317
373
|
|
|
318
374
|
usagesList.push(usage);
|
|
319
375
|
}
|
|
376
|
+
if (!options.codeOnly) {
|
|
377
|
+
appendTextComplements(index, {
|
|
378
|
+
usagesList,
|
|
379
|
+
filePath,
|
|
380
|
+
fileEntry,
|
|
381
|
+
content,
|
|
382
|
+
lines,
|
|
383
|
+
name,
|
|
384
|
+
context: options.context || 0,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
320
387
|
continue; // Skip to next file
|
|
321
388
|
}
|
|
322
389
|
|
|
@@ -336,13 +403,13 @@ function usages(index, name, options = {}) {
|
|
|
336
403
|
return;
|
|
337
404
|
}
|
|
338
405
|
|
|
339
|
-
// Skip if the match is inside a string literal
|
|
340
|
-
if (index.isInsideStringAST(content, lineNum, line, name, filePath)) {
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
406
|
// Classify usage type (AST-based, defaults to 'reference' for unsupported languages)
|
|
345
|
-
const
|
|
407
|
+
const inText = index.isInsideStringAST(
|
|
408
|
+
content, lineNum, line, name, filePath);
|
|
409
|
+
const usageType = !options.codeOnly && inText
|
|
410
|
+
? 'text'
|
|
411
|
+
: (index.classifyUsageAST(
|
|
412
|
+
content, lineNum, name, filePath) ?? 'reference');
|
|
346
413
|
|
|
347
414
|
// BUG-4: enrich call usages with enclosing-function info.
|
|
348
415
|
let callerSym = null;
|
|
@@ -357,6 +424,9 @@ function usages(index, name, options = {}) {
|
|
|
357
424
|
content: line,
|
|
358
425
|
usageType,
|
|
359
426
|
isDefinition: false,
|
|
427
|
+
...(usageType === 'text' && {
|
|
428
|
+
textKind: 'comment-or-string',
|
|
429
|
+
}),
|
|
360
430
|
...(callerSym && {
|
|
361
431
|
callerName: callerSym.name,
|
|
362
432
|
callerStartLine: callerSym.startLine
|
|
@@ -414,16 +484,39 @@ function search(index, term, options = {}) {
|
|
|
414
484
|
let filesSkipped = 0;
|
|
415
485
|
let filesFilteredByFlag = 0;
|
|
416
486
|
const regexFlags = options.caseSensitive ? 'g' : 'gi';
|
|
417
|
-
const useRegex = options.regex
|
|
487
|
+
const useRegex = options.regex === true; // Safe default: literal text
|
|
418
488
|
let regex;
|
|
419
|
-
let
|
|
489
|
+
let linearRegex = null;
|
|
420
490
|
if (useRegex) {
|
|
491
|
+
if (NESTED_SINGLE_ATOM_REPEAT.test(term)) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`Unsafe regular expression "${term}": nested repetition is not accepted. Simplify it or use ripgrep.`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
421
496
|
try {
|
|
422
497
|
regex = new RegExp(term, regexFlags);
|
|
423
498
|
} catch (e) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
499
|
+
throw new Error(
|
|
500
|
+
`Invalid regular expression "${term}": ${e.message}`,
|
|
501
|
+
{ cause: e },
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
// Prefer the RE2-compatible DFA engine: ordinary code-search patterns
|
|
505
|
+
// stay linear-time even for ambiguous repetition such as `(a|a)*`.
|
|
506
|
+
// Advanced JavaScript-only constructs (backreferences/lookarounds)
|
|
507
|
+
// retain V8 compatibility only when the conservative fallback guard
|
|
508
|
+
// can establish that their repetition shape is safe.
|
|
509
|
+
try {
|
|
510
|
+
let re2Flags = options.caseSensitive ? 0 : RE2JS.CASE_INSENSITIVE;
|
|
511
|
+
if (/\(\?<([=!])/.test(term)) re2Flags |= RE2JS.LOOKBEHINDS;
|
|
512
|
+
linearRegex = RE2JS.compile(term, re2Flags);
|
|
513
|
+
} catch (re2Error) {
|
|
514
|
+
if (!isSafeRegex(term)) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`Unsafe regular expression "${term}": this JavaScript-only pattern cannot run in UCN's linear-time engine and its repetition shape may backtrack catastrophically. Simplify it or use ripgrep.`,
|
|
517
|
+
{ cause: re2Error },
|
|
518
|
+
);
|
|
519
|
+
}
|
|
427
520
|
}
|
|
428
521
|
} else {
|
|
429
522
|
regex = new RegExp(escapeRegExp(term), regexFlags);
|
|
@@ -458,7 +551,12 @@ function search(index, term, options = {}) {
|
|
|
458
551
|
try {
|
|
459
552
|
const parser = getParser(language);
|
|
460
553
|
const { findMatchesWithASTFilter } = require('../languages/utils');
|
|
461
|
-
const astMatches = findMatchesWithASTFilter(content, term, parser, {
|
|
554
|
+
const astMatches = findMatchesWithASTFilter(content, term, parser, {
|
|
555
|
+
codeOnly: true,
|
|
556
|
+
regex: useRegex,
|
|
557
|
+
caseSensitive: options.caseSensitive,
|
|
558
|
+
compiledRegex: linearRegex,
|
|
559
|
+
});
|
|
462
560
|
|
|
463
561
|
for (const m of astMatches) {
|
|
464
562
|
const match = {
|
|
@@ -497,8 +595,9 @@ function search(index, term, options = {}) {
|
|
|
497
595
|
|
|
498
596
|
// Fallback to regex-based search (non-codeOnly or unsupported language)
|
|
499
597
|
lines.forEach((line, idx) => {
|
|
500
|
-
regex.lastIndex = 0; // Reset
|
|
501
|
-
if (
|
|
598
|
+
regex.lastIndex = 0; // Reset V8 fallback state
|
|
599
|
+
if ((linearRegex && linearRegex.test(line)) ||
|
|
600
|
+
(!linearRegex && regex.test(line))) {
|
|
502
601
|
const lineNum = idx + 1;
|
|
503
602
|
// Skip if codeOnly and line is comment/string
|
|
504
603
|
if (options.codeOnly && index.isCommentOrStringAtPosition(content, lineNum, 0, filePath)) {
|
|
@@ -540,6 +639,7 @@ function search(index, term, options = {}) {
|
|
|
540
639
|
|
|
541
640
|
// Apply top limit (limits total matches across all files)
|
|
542
641
|
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
642
|
+
const totalMatchedFiles = results.length;
|
|
543
643
|
let truncatedMatches = 0;
|
|
544
644
|
if (options.top && options.top > 0 && totalMatches > options.top) {
|
|
545
645
|
let remaining = options.top;
|
|
@@ -559,7 +659,19 @@ function search(index, term, options = {}) {
|
|
|
559
659
|
results.push(...truncated);
|
|
560
660
|
}
|
|
561
661
|
|
|
562
|
-
results.meta = {
|
|
662
|
+
results.meta = {
|
|
663
|
+
filesScanned,
|
|
664
|
+
filesSkipped,
|
|
665
|
+
filesFilteredByFlag,
|
|
666
|
+
totalFiles: index.files.size,
|
|
667
|
+
mode: useRegex ? 'regex' : 'literal',
|
|
668
|
+
totalMatches,
|
|
669
|
+
totalMatchedFiles,
|
|
670
|
+
shownMatches: totalMatches - truncatedMatches,
|
|
671
|
+
truncatedMatches,
|
|
672
|
+
limit: options.top || null,
|
|
673
|
+
projectLanguage: index._getPredominantLanguage(),
|
|
674
|
+
};
|
|
563
675
|
return results;
|
|
564
676
|
} finally { index._endOp(); }
|
|
565
677
|
}
|
|
@@ -647,6 +759,9 @@ function structuralSearch(index, options = {}) {
|
|
|
647
759
|
const calls = getCachedCalls(index, filePath);
|
|
648
760
|
if (!calls) continue;
|
|
649
761
|
for (const call of calls) {
|
|
762
|
+
// Potential callback/value references are useful to the
|
|
763
|
+
// caller engine, but they are not invocations.
|
|
764
|
+
if (call.isFunctionReference || call.isTypeReference) continue;
|
|
650
765
|
if (nameMatcher && !nameMatcher(call.name)) continue;
|
|
651
766
|
// Field-hop receivers (`tm.service.Save()`) carry
|
|
652
767
|
// receiverRoot/receiverField instead of receiver —
|
|
@@ -681,6 +796,8 @@ function structuralSearch(index, options = {}) {
|
|
|
681
796
|
const classTypes = new Set(['class', 'struct', 'interface', 'impl', 'trait', 'record', 'enum']);
|
|
682
797
|
const typeTypes = new Set(['type', 'enum', 'interface', 'trait', 'record', 'namespace']);
|
|
683
798
|
const methodTypes = new Set(['method', 'constructor']);
|
|
799
|
+
const variableTypes = new Set(['state', 'field', 'constant', 'macro']);
|
|
800
|
+
const groupedTypes = new Set(['function', 'class', 'method', 'type', 'variable']);
|
|
684
801
|
|
|
685
802
|
for (const [symbolName, definitions] of index.symbols) {
|
|
686
803
|
if (nameMatcher && !nameMatcher(symbolName)) continue;
|
|
@@ -691,6 +808,8 @@ function structuralSearch(index, options = {}) {
|
|
|
691
808
|
if (type === 'class' && !classTypes.has(def.type)) continue;
|
|
692
809
|
if (type === 'method' && !methodTypes.has(def.type) && !def.isMethod) continue;
|
|
693
810
|
if (type === 'type' && !typeTypes.has(def.type)) continue;
|
|
811
|
+
if (type === 'variable' && !variableTypes.has(def.type)) continue;
|
|
812
|
+
if (type && !groupedTypes.has(type) && def.type !== type) continue;
|
|
694
813
|
|
|
695
814
|
// File filters
|
|
696
815
|
const fileEntry = index.files.get(def.file);
|
|
@@ -728,16 +847,21 @@ function structuralSearch(index, options = {}) {
|
|
|
728
847
|
|
|
729
848
|
// Exported filter
|
|
730
849
|
if (exported) {
|
|
731
|
-
const
|
|
732
|
-
const isExp =
|
|
733
|
-
|
|
734
|
-
mods.some(m => m.startsWith('pub')) ||
|
|
735
|
-
(fileEntry && langTraits(fileEntry.language)?.exportVisibility === 'capitalization' && /^[A-Z]/.test(symbolName));
|
|
850
|
+
const { symbolIsExported } = require('./graph');
|
|
851
|
+
const isExp = fileEntry && symbolIsExported(
|
|
852
|
+
def, fileEntry, new Set(fileEntry.exports || []));
|
|
736
853
|
if (!isExp) continue;
|
|
737
854
|
}
|
|
738
855
|
|
|
739
856
|
// Unused filter (expensive — last check)
|
|
740
857
|
if (unused) {
|
|
858
|
+
// This query has call-edge semantics. Reference-live
|
|
859
|
+
// types/classes/fields are handled by `deadcode`.
|
|
860
|
+
if (!functionTypes.has(def.type)) continue;
|
|
861
|
+
// Named function expressions are consumed by their
|
|
862
|
+
// expression position — never "unused" (the deadcode
|
|
863
|
+
// twin of the bodyScopedName audit skip).
|
|
864
|
+
if (def.bodyScopedName) continue;
|
|
741
865
|
index.buildCalleeIndex();
|
|
742
866
|
// A name whose every call site is its own recursion
|
|
743
867
|
// has zero callers (fix #253c — the deadcode
|
|
@@ -767,7 +891,7 @@ function structuralSearch(index, options = {}) {
|
|
|
767
891
|
// #234, campaign G2 ×4 languages: Go main/init, Java
|
|
768
892
|
// main, Rust main/#[test] all listed — the deadcode
|
|
769
893
|
// protection, applied here).
|
|
770
|
-
const langModule = fileEntry &&
|
|
894
|
+
const langModule = fileEntry && getLanguageAdapter(fileEntry.language);
|
|
771
895
|
if (langModule?.isEntryPoint?.(def)) continue;
|
|
772
896
|
// A bare decorator application (@with_logging) invokes
|
|
773
897
|
// the decorator at import time but is recorded as a
|
|
@@ -830,6 +954,10 @@ function structuralSearch(index, options = {}) {
|
|
|
830
954
|
}).filter(([, v]) => v !== undefined && v !== null)),
|
|
831
955
|
totalMatched: total,
|
|
832
956
|
shown: results.length,
|
|
957
|
+
...(unused && {
|
|
958
|
+
unusedScope: 'callable-symbols-only',
|
|
959
|
+
unusedSafety: 'candidate-only; use deadcode before deletion',
|
|
960
|
+
}),
|
|
833
961
|
}
|
|
834
962
|
};
|
|
835
963
|
} finally { index._endOp(); }
|
|
@@ -873,7 +1001,21 @@ function example(index, name, options = {}) {
|
|
|
873
1001
|
.map(c => ({ ...c, evidenceTier: 'unverified' })),
|
|
874
1002
|
...(rawCallers.unverifiedEntries || [])
|
|
875
1003
|
.map(c => ({ ...c, evidenceTier: 'unverified' })),
|
|
876
|
-
].filter(c => !c.functionReference && c.calledAs !== 'bound');
|
|
1004
|
+
].filter(c => !c.functionReference && !c.isFunctionReference && c.calledAs !== 'bound');
|
|
1005
|
+
|
|
1006
|
+
// `about`/`context` expose conserved call lines that no engine candidate
|
|
1007
|
+
// claimed as `call-not-resolved`. `example` must preserve the same
|
|
1008
|
+
// evidence contract: when those are the only candidate sites, abstain
|
|
1009
|
+
// explicitly instead of returning a misleading "no examples" error.
|
|
1010
|
+
// Compose the text-ground account only on this empty-candidate path so
|
|
1011
|
+
// normal example selection does not pay for a second project-wide scan.
|
|
1012
|
+
if (candidates.length === 0) {
|
|
1013
|
+
const { composeAccount, callNotResolvedEntries } = require('./analysis');
|
|
1014
|
+
const callerAccount = composeAccount(index, name, rawCallers);
|
|
1015
|
+
candidates.push(...callNotResolvedEntries(index, callerAccount)
|
|
1016
|
+
.filter(c => !c.functionReference && c.calledAs !== 'bound')
|
|
1017
|
+
.map(c => ({ ...c, evidenceTier: 'unverified' })));
|
|
1018
|
+
}
|
|
877
1019
|
|
|
878
1020
|
// Dedupe by site, preferring confirmed evidence when a parser shape
|
|
879
1021
|
// reaches the same line through more than one resolution path.
|
|
@@ -1168,6 +1310,50 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1168
1310
|
if (targetRels.size === 0) targetRels = null;
|
|
1169
1311
|
}
|
|
1170
1312
|
|
|
1313
|
+
// File targets answer "which tests exercise code from this file?", not
|
|
1314
|
+
// "which tests mention the file's basename?". The old basename scan turned
|
|
1315
|
+
// `tests cli/index.js` into a project-wide search for the symbol `index`,
|
|
1316
|
+
// producing huge, unrelated answers. Pin every callable declared by the
|
|
1317
|
+
// resolved file and let the caller engine prove test call sites instead.
|
|
1318
|
+
// Import-linked tests are still handled below so class/type-only use and
|
|
1319
|
+
// module-level coverage remain visible.
|
|
1320
|
+
const fileTargetCallerSites = new Map(); // absolute test file → tiered sites
|
|
1321
|
+
if (isFilePath && targetRels) {
|
|
1322
|
+
const testFileInfo = new Map(testFiles.map(info => [info.path, info]));
|
|
1323
|
+
const fileTargetDefs = [];
|
|
1324
|
+
for (const [, fe] of index.files) {
|
|
1325
|
+
if (!targetRels.has(fe.relativePath)) continue;
|
|
1326
|
+
for (const symbol of fe.symbols || []) {
|
|
1327
|
+
if (CALLABLE_SYMBOL_KINDS.has(symbol.type)) fileTargetDefs.push(symbol);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
for (const def of fileTargetDefs) {
|
|
1331
|
+
const raw = index.findCallers(def.name, {
|
|
1332
|
+
targetDefinitions: [def],
|
|
1333
|
+
collectAccount: true,
|
|
1334
|
+
});
|
|
1335
|
+
const globallyUnique = index.find(def.name, { exact: true }).length === 1;
|
|
1336
|
+
const sites = [
|
|
1337
|
+
...raw.map(site => ({ ...site, _testEvidenceTier: 'confirmed' })),
|
|
1338
|
+
...(globallyUnique ? (raw.unverifiedEntries || []).map(site => ({
|
|
1339
|
+
...site,
|
|
1340
|
+
_testEvidenceTier: 'unverified',
|
|
1341
|
+
})) : []),
|
|
1342
|
+
];
|
|
1343
|
+
for (const site of sites) {
|
|
1344
|
+
const info = testFileInfo.get(site.file);
|
|
1345
|
+
if (!info) continue;
|
|
1346
|
+
if (info.testRanges && !lineInRanges(site.line, info.testRanges)) continue;
|
|
1347
|
+
let rows = fileTargetCallerSites.get(site.file);
|
|
1348
|
+
if (!rows) {
|
|
1349
|
+
rows = [];
|
|
1350
|
+
fileTargetCallerSites.set(site.file, rows);
|
|
1351
|
+
}
|
|
1352
|
+
rows.push(site);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1171
1357
|
const className = options.className || null;
|
|
1172
1358
|
// className scoping accepts the class plus its non-overriding
|
|
1173
1359
|
// descendants — a subclass instance without its own override dispatches
|
|
@@ -1200,22 +1386,41 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1200
1386
|
if (hits.length > 0) linkedRecords = hits;
|
|
1201
1387
|
}
|
|
1202
1388
|
}
|
|
1389
|
+
const provenFileSites = fileTargetCallerSites.get(testPath) || [];
|
|
1203
1390
|
|
|
1204
1391
|
// Fast pre-check: skip if searchTerm doesn't appear in file
|
|
1205
|
-
if (!content.includes(searchTerm) && !linkedRecords) continue;
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
//
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
//
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1392
|
+
if (!isFilePath && !content.includes(searchTerm) && !linkedRecords) continue;
|
|
1393
|
+
if (isFilePath && !linkedRecords && provenFileSites.length === 0) continue;
|
|
1394
|
+
const sourceFileLinked = !sourceFileFilter || sourceFileFilter.has(testPath);
|
|
1395
|
+
// A class-qualified query can still identify a possible test link
|
|
1396
|
+
// in source layouts that have no modeled module edge (notably
|
|
1397
|
+
// Rust macro/test fixtures). Keep it visibly unverified when the
|
|
1398
|
+
// test contains an AST reference to the exact class name; never
|
|
1399
|
+
// promote that hint to confirmed source ownership.
|
|
1400
|
+
const classIdentityHint = !sourceFileLinked && className &&
|
|
1401
|
+
(index._getCachedUsages(testPath, className) || []).some(u =>
|
|
1402
|
+
u.usageType !== 'definition' && u.usageType !== 'import');
|
|
1215
1403
|
|
|
1216
1404
|
// AST-based usage detection
|
|
1217
|
-
|
|
1218
|
-
|
|
1405
|
+
// A file path is not a symbol. Its basename must never be sent
|
|
1406
|
+
// through the usage index (e.g. `index.js` → every local `index`).
|
|
1407
|
+
const astUsages = isFilePath
|
|
1408
|
+
? []
|
|
1409
|
+
: (index._getCachedUsages(testPath, searchTerm) || []);
|
|
1410
|
+
if (astUsages.length === 0 && !linkedRecords && provenFileSites.length === 0) continue;
|
|
1411
|
+
// Compiler attributes can invoke generated builder methods across
|
|
1412
|
+
// workspace/facade boundaries that the source import graph cannot
|
|
1413
|
+
// represent (`#[arg(value_delimiter = ',')]`). Keep these explicit
|
|
1414
|
+
// AST references as unverified instead of silently dropping a real
|
|
1415
|
+
// test. Ordinary calls/references still require source ownership.
|
|
1416
|
+
const hasAttributeReference = astUsages.some(u => u.inAttribute &&
|
|
1417
|
+
(!testRanges || lineInRanges(u.line, testRanges)));
|
|
1418
|
+
if (!sourceFileLinked && !hasAttributeReference && !classIdentityHint) continue;
|
|
1419
|
+
// className scoping normally requires the class or a dispatching
|
|
1420
|
+
// descendant in the file. Generated attribute references are the
|
|
1421
|
+
// conservative exception: they carry an explicit unverified tier.
|
|
1422
|
+
if (className && ![...dispatchNames].some(dn => content.includes(dn)) &&
|
|
1423
|
+
!hasAttributeReference) continue;
|
|
1219
1424
|
|
|
1220
1425
|
// A test file that DEFINES searchTerm itself owns bare-name
|
|
1221
1426
|
// calls of it (fix #246 — the #244 affectedTests rule, tests()
|
|
@@ -1254,6 +1459,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1254
1459
|
|
|
1255
1460
|
for (const usage of astUsages) {
|
|
1256
1461
|
if (usage.usageType === 'definition') continue; // not relevant in test files
|
|
1462
|
+
if (!sourceFileLinked && !usage.inAttribute && !classIdentityHint) continue;
|
|
1257
1463
|
// Inline-test-promoted file: only lines inside the test
|
|
1258
1464
|
// ranges are test code (fix #244).
|
|
1259
1465
|
if (testRanges && !lineInRanges(usage.line, testRanges)) continue;
|
|
@@ -1271,7 +1477,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1271
1477
|
if (usage.usageType === 'import') {
|
|
1272
1478
|
matchType = 'import';
|
|
1273
1479
|
} else if (usage.usageType === 'call') {
|
|
1274
|
-
matchType = 'call';
|
|
1480
|
+
matchType = classIdentityHint ? 'unverified-call' : 'call';
|
|
1275
1481
|
} else {
|
|
1276
1482
|
// 'reference' — check if inside string literal
|
|
1277
1483
|
matchType = strPattern.test(lineContent) ? 'string-ref' :
|
|
@@ -1279,7 +1485,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1279
1485
|
}
|
|
1280
1486
|
|
|
1281
1487
|
// className scoping for calls: check receiver
|
|
1282
|
-
if (className && matchType === 'call') {
|
|
1488
|
+
if (className && (matchType === 'call' || matchType === 'unverified-call')) {
|
|
1283
1489
|
if (!_receiverMatchesClass(usage, dispatchNames, instanceTypeMap, lineContent, searchTerm)) continue;
|
|
1284
1490
|
}
|
|
1285
1491
|
|
|
@@ -1309,7 +1515,11 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1309
1515
|
matches.push({
|
|
1310
1516
|
line: usage.line,
|
|
1311
1517
|
content: lineContent.trim(),
|
|
1312
|
-
matchType
|
|
1518
|
+
matchType,
|
|
1519
|
+
...(matchType === 'unverified-call' && {
|
|
1520
|
+
evidenceTier: 'unverified',
|
|
1521
|
+
reason: 'class-reference-without-source-ownership',
|
|
1522
|
+
}),
|
|
1313
1523
|
});
|
|
1314
1524
|
}
|
|
1315
1525
|
|
|
@@ -1348,10 +1558,30 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1348
1558
|
}
|
|
1349
1559
|
}
|
|
1350
1560
|
|
|
1561
|
+
// Exact-target caller evidence for callable declarations in a file
|
|
1562
|
+
// target. This covers same-package tests (notably Go) that do not
|
|
1563
|
+
// import the implementation file, without falling back to a
|
|
1564
|
+
// basename text heuristic.
|
|
1565
|
+
for (const site of provenFileSites) {
|
|
1566
|
+
const matchType = site._testEvidenceTier === 'confirmed'
|
|
1567
|
+
? 'call'
|
|
1568
|
+
: 'unverified-call';
|
|
1569
|
+
if (matches.some(m => m.line === site.line && m.matchType === matchType)) continue;
|
|
1570
|
+
matches.push({
|
|
1571
|
+
line: site.line,
|
|
1572
|
+
content: (site.content || index.getLineContent(testPath, site.line) || '').trim(),
|
|
1573
|
+
matchType,
|
|
1574
|
+
evidenceTier: site._testEvidenceTier,
|
|
1575
|
+
...(site.resolution && { resolution: site.resolution }),
|
|
1576
|
+
...(site.reason && { reason: site.reason }),
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1351
1580
|
// Language-aware test-case detection. Under a local same-name
|
|
1352
1581
|
// shadow the term on a test line is the file's OWN helper —
|
|
1353
1582
|
// only anchor test cases to matches that survived the shadow.
|
|
1354
|
-
if (!
|
|
1583
|
+
if (!isFilePath && sourceFileLinked &&
|
|
1584
|
+
(!localShadow || matches.some(m => m.matchType !== 'import'))) {
|
|
1355
1585
|
_addTestCaseMatches(index, testPath, entry, searchTerm, className, instanceTypeMap, matches);
|
|
1356
1586
|
}
|
|
1357
1587
|
|
|
@@ -1369,7 +1599,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1369
1599
|
}
|
|
1370
1600
|
|
|
1371
1601
|
const filtered = options.callsOnly
|
|
1372
|
-
? finalMatches.filter(m =>
|
|
1602
|
+
? finalMatches.filter(m => ['call', 'unverified-call', 'test-case'].includes(m.matchType))
|
|
1373
1603
|
: finalMatches;
|
|
1374
1604
|
if (filtered.length > 0) {
|
|
1375
1605
|
results.push({
|
|
@@ -1434,7 +1664,8 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1434
1664
|
const localSameName = (info.entry.symbols || []).some(s => s.name === searchTerm);
|
|
1435
1665
|
const importsTargetName = (info.entry.importBindings || []).some(b => b.name === searchTerm);
|
|
1436
1666
|
const explicitlyScopedToLocal = !!options.file && targetDefs.some(d => d.file === site.file);
|
|
1437
|
-
if (localSameName && !importsTargetName && !explicitlyScopedToLocal &&
|
|
1667
|
+
if (localSameName && !importsTargetName && !explicitlyScopedToLocal &&
|
|
1668
|
+
!site.receiver && !site.isMethod) continue;
|
|
1438
1669
|
|
|
1439
1670
|
let fileResult = results.find(r => r.file === info.entry.relativePath);
|
|
1440
1671
|
if (!fileResult) {
|
|
@@ -1531,7 +1762,16 @@ function _buildSourceFileImporters(index, defs) {
|
|
|
1531
1762
|
// If so, add it to the queue so its importers are also discovered.
|
|
1532
1763
|
if (!visited.has(imp)) {
|
|
1533
1764
|
const fe = index.files.get(imp);
|
|
1534
|
-
|
|
1765
|
+
const currentEntry = index.files.get(current);
|
|
1766
|
+
// C/C++ #include is textual inclusion, not a module import.
|
|
1767
|
+
// Names flow through every transitive include edge regardless
|
|
1768
|
+
// of extension (`common.h` may intentionally include a .c
|
|
1769
|
+
// implementation), so no re-export declaration is required.
|
|
1770
|
+
const cFamilyIncludeChain =
|
|
1771
|
+
['c', 'cpp'].includes(currentEntry?.language) &&
|
|
1772
|
+
['c', 'cpp'].includes(fe?.language);
|
|
1773
|
+
if (fe && (cFamilyIncludeChain ||
|
|
1774
|
+
_fileReExportsSymbol(index, fe, symbolName, current))) {
|
|
1535
1775
|
visited.add(imp);
|
|
1536
1776
|
queue.push(imp);
|
|
1537
1777
|
}
|
|
@@ -1636,13 +1876,28 @@ function _buildSourceFileImporters(index, defs) {
|
|
|
1636
1876
|
* Handles: named re-exports, `module.exports = require(...)` blanket re-exports,
|
|
1637
1877
|
* `export * from ...`, and files that both import from source and export the symbol.
|
|
1638
1878
|
*/
|
|
1639
|
-
function _fileReExportsSymbol(fileEntry, symbolName, sourceAbsPath) {
|
|
1640
|
-
|
|
1641
|
-
//
|
|
1642
|
-
|
|
1879
|
+
function _fileReExportsSymbol(index, fileEntry, symbolName, sourceAbsPath) {
|
|
1880
|
+
// Python module imports become module attributes. Package __init__.py
|
|
1881
|
+
// commonly exposes its public API through `from .core import Context as
|
|
1882
|
+
// Context`, which has no separate export statement in the AST.
|
|
1883
|
+
if (fileEntry.language === 'python' && symbolName) {
|
|
1884
|
+
for (const binding of (fileEntry.importBindings || [])) {
|
|
1885
|
+
if (binding.name !== symbolName && binding.alias !== symbolName) continue;
|
|
1886
|
+
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
1887
|
+
if (rel && path.join(index.root, rel) === sourceAbsPath) return true;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
const exportNames = fileEntry.exports || [];
|
|
1892
|
+
const details = fileEntry.exportDetails || [];
|
|
1893
|
+
if (exportNames.length === 0 && details.length === 0) return false;
|
|
1894
|
+
// Check if any export matches the symbol name. fileEntry.exports is the
|
|
1895
|
+
// persisted string-name list; exportDetails carries source/type metadata.
|
|
1896
|
+
if (symbolName && (exportNames.includes(symbolName) ||
|
|
1897
|
+
details.some(exp => exp.name === symbolName || exp.alias === symbolName))) return true;
|
|
1643
1898
|
// Blanket re-exports: module.exports = require(...), export * from ...
|
|
1644
1899
|
// These have undefined or generic names but re-export everything from the imported module
|
|
1645
|
-
const hasBlanketExport =
|
|
1900
|
+
const hasBlanketExport = details.some(exp =>
|
|
1646
1901
|
!exp.name || exp.type === 'module.exports' || exp.type === 're-export' || exp.type === 'export-all'
|
|
1647
1902
|
);
|
|
1648
1903
|
if (hasBlanketExport) return true;
|
|
@@ -1798,7 +2053,7 @@ function _addTestCaseMatches(index, filePath, fileEntry, searchTerm, className,
|
|
|
1798
2053
|
// Go/Python/Java/Rust: check if any AST usage falls within a test function's range
|
|
1799
2054
|
if (!fileEntry.symbols) return;
|
|
1800
2055
|
try {
|
|
1801
|
-
const langModule =
|
|
2056
|
+
const langModule = getLanguageAdapter(lang);
|
|
1802
2057
|
if (!langModule) return;
|
|
1803
2058
|
// Prefer kinded predicate so fn main() and fn init() aren't classified
|
|
1804
2059
|
// as test cases (BUG-CX). Fall back to isEntryPoint for compat.
|