ucn 4.2.3 → 5.0.2
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 +438 -305
- 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 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- 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 +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- 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 +212 -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 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- 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 +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/search.js
CHANGED
|
@@ -10,9 +10,17 @@
|
|
|
10
10
|
const path = require('path');
|
|
11
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,148 +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
|
-
const _importedHasDef = new Map();
|
|
224
|
-
const importedFileHasDef = (receiver) => {
|
|
225
|
-
const cacheKey = receiver || '';
|
|
226
|
-
if (_importedHasDef.has(cacheKey)) return _importedHasDef.get(cacheKey);
|
|
227
|
-
const importedFiles = index.importGraph.get(filePath);
|
|
228
|
-
let found = false;
|
|
229
|
-
if (importedFiles) for (const imp of importedFiles) {
|
|
230
|
-
const impEntry = index.files.get(imp);
|
|
231
|
-
if (impEntry?.symbols?.some(s => s.name === name)) {
|
|
232
|
-
found = true;
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
// A module namespace may expose the target through a
|
|
236
|
-
// re-export chain (`import httpx; httpx.URL(...)`,
|
|
237
|
-
// where httpx/__init__.py re-exports URL). Direct-file
|
|
238
|
-
// symbol checks silently dropped these compiler-true
|
|
239
|
-
// usages. Reuse the caller engine's conservative
|
|
240
|
-
// name-ownership chase: yes confirms; unknown stays
|
|
241
|
-
// visible in this raw-usage inventory; only a proven
|
|
242
|
-
// no is filtering evidence.
|
|
243
|
-
if (targetFiles.size > 0 &&
|
|
244
|
-
_nameBindingReaches(index, imp, name, targetFiles) !== 'no') {
|
|
245
|
-
found = true;
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Named namespace surfaces need one more ownership axis:
|
|
251
|
-
// `import { util } from './core'; util.helper()` where
|
|
252
|
-
// core exports `* as util` from the helper's module. The
|
|
253
|
-
// imported file does not itself define `helper`; entering
|
|
254
|
-
// the namespace is what reaches the owning file.
|
|
255
|
-
if (!found && receiver && targetFiles.size > 0) {
|
|
256
|
-
const queue = [];
|
|
257
|
-
let unknown = false;
|
|
258
|
-
for (const b of (fileEntry.importBindings || [])) {
|
|
259
|
-
if (b.name !== receiver && b.alias !== receiver) continue;
|
|
260
|
-
const rel = fileEntry.moduleResolved?.[b.module];
|
|
261
|
-
if (rel) queue.push({ file: path.join(index.root, rel), attr: b.name, depth: 0 });
|
|
262
|
-
else if (String(b.module).startsWith('.')) unknown = true;
|
|
263
|
-
}
|
|
264
|
-
const seen = new Set();
|
|
265
|
-
while (!found && queue.length > 0) {
|
|
266
|
-
const cur = queue.shift();
|
|
267
|
-
const state = `${cur.file}\0${cur.attr || ''}`;
|
|
268
|
-
if (seen.has(state)) continue;
|
|
269
|
-
seen.add(state);
|
|
270
|
-
if (targetFiles.has(cur.file)) { found = true; break; }
|
|
271
|
-
if (cur.depth >= 4) { unknown = true; continue; }
|
|
272
|
-
const fe = index.files.get(cur.file);
|
|
273
|
-
if (!fe) { unknown = true; continue; }
|
|
274
|
-
for (const e of (fe.exportDetails || [])) {
|
|
275
|
-
if (!e.source) continue;
|
|
276
|
-
let nextAttr;
|
|
277
|
-
if (e.type === 're-export-all' && e.alias === cur.attr) {
|
|
278
|
-
nextAttr = null; // entered the namespace
|
|
279
|
-
} else if (e.type === 're-export-all' && !e.alias && cur.attr) {
|
|
280
|
-
nextAttr = cur.attr; // transparent barrel
|
|
281
|
-
} else if (e.type === 're-export' &&
|
|
282
|
-
(e.alias || e.name) === cur.attr) {
|
|
283
|
-
nextAttr = e.name;
|
|
284
|
-
} else {
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
const rel = fe.moduleResolved?.[e.source];
|
|
288
|
-
if (rel) {
|
|
289
|
-
queue.push({
|
|
290
|
-
file: path.join(index.root, rel),
|
|
291
|
-
attr: nextAttr,
|
|
292
|
-
depth: cur.depth + 1,
|
|
293
|
-
});
|
|
294
|
-
} else if (String(e.source).startsWith('.')) {
|
|
295
|
-
unknown = true;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// Dynamic/CJS export objects cannot prove that a
|
|
299
|
-
// namespace property is unrelated. Keep the usage
|
|
300
|
-
// visible rather than manufacturing a false miss.
|
|
301
|
-
if ((fe.exportDetails || []).some(e =>
|
|
302
|
-
e.type === 'exports' || e.type === 'module.exports') ||
|
|
303
|
-
(fe.moduleAssignedNames || []).includes(cur.attr)) {
|
|
304
|
-
unknown = true;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if (!found && unknown) found = true;
|
|
308
|
-
}
|
|
309
|
-
_importedHasDef.set(cacheKey, found);
|
|
310
|
-
return found;
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// A qualified usage whose RECEIVER is a symbol defined in this
|
|
314
|
-
// file is never external-package noise — `Geometry.area(3, 4)`
|
|
315
|
-
// in the file declaring namespace Geometry was dropped by the
|
|
316
|
-
// receiver filter while find's usageCounts counted it (fix
|
|
317
|
-
// #241). Keyed on the receiver, not the target name: a file
|
|
318
|
-
// defining its own `Separator` while using external
|
|
319
|
-
// `Ns.Separator` must still filter the latter (bug #23).
|
|
320
|
-
const receiverDefinedHere = (recv) =>
|
|
321
|
-
!!recv && fileEntry.symbols && fileEntry.symbols.some(s => s.name === recv);
|
|
322
|
-
|
|
323
324
|
for (const u of astUsages) {
|
|
324
325
|
// Skip if this is a definition line (already added above)
|
|
325
|
-
if (definitions.some(d => d.file === filePath &&
|
|
326
|
+
if (definitions.some(d => d.file === filePath &&
|
|
327
|
+
(d.startLine === u.line || d.nameLine === u.line))) {
|
|
326
328
|
continue;
|
|
327
329
|
}
|
|
328
330
|
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
// Owners are CALLABLE member defs only — an enum variant
|
|
334
|
-
// member also carries className, and counting it as an
|
|
335
|
-
// owner would let `Boundary::Grid` survive a struct-Grid
|
|
336
|
-
// query via its own enum. Lowercase receivers are module
|
|
337
|
-
// paths (`render::draw` reaches the free fn); `Self` is
|
|
338
|
-
// the enclosing type — both keep escape-hatch visibility.
|
|
339
|
-
if (fileEntry.language === 'rust' && u.scopedReference &&
|
|
340
|
-
u.receiver && /^[A-Z]/.test(u.receiver) && u.receiver !== 'Self') {
|
|
341
|
-
const methodOwners = new Set(definitions
|
|
342
|
-
.filter(d => d.className && CALLABLE_SYMBOL_KINDS.has(d.type))
|
|
343
|
-
.map(d => d.className));
|
|
344
|
-
if (!methodOwners.has(u.receiver)) continue;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Filter member expressions with unrelated receivers in JS/TS/Python.
|
|
348
|
-
// Keeps: standalone usages, self/this/cls/super, method calls on known types,
|
|
349
|
-
// qualified usages whose receiver this file defines,
|
|
350
|
-
// and module access (output.fn()) when the imported file defines the name.
|
|
351
|
-
// Filters: namespace access to external packages (DropdownMenuPrimitive.Separator).
|
|
352
|
-
if (u.receiver && !['self', 'this', 'cls', 'super'].includes(u.receiver) &&
|
|
353
|
-
fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust') {
|
|
354
|
-
const hasMethodDef = definitions.some(d => d.className);
|
|
355
|
-
const sameFileMember = definitions.some(d =>
|
|
356
|
-
d.file === filePath && d.memberAssigned);
|
|
357
|
-
if (!hasMethodDef && !sameFileMember && !receiverDefinedHere(u.receiver) &&
|
|
358
|
-
!importedFileHasDef(u.receiver)) {
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
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.
|
|
362
335
|
|
|
363
336
|
const lineContent = lines[u.line - 1] || '';
|
|
364
337
|
|
|
@@ -400,6 +373,17 @@ function usages(index, name, options = {}) {
|
|
|
400
373
|
|
|
401
374
|
usagesList.push(usage);
|
|
402
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
|
+
}
|
|
403
387
|
continue; // Skip to next file
|
|
404
388
|
}
|
|
405
389
|
|
|
@@ -419,13 +403,13 @@ function usages(index, name, options = {}) {
|
|
|
419
403
|
return;
|
|
420
404
|
}
|
|
421
405
|
|
|
422
|
-
// Skip if the match is inside a string literal
|
|
423
|
-
if (index.isInsideStringAST(content, lineNum, line, name, filePath)) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
406
|
// Classify usage type (AST-based, defaults to 'reference' for unsupported languages)
|
|
428
|
-
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');
|
|
429
413
|
|
|
430
414
|
// BUG-4: enrich call usages with enclosing-function info.
|
|
431
415
|
let callerSym = null;
|
|
@@ -440,6 +424,9 @@ function usages(index, name, options = {}) {
|
|
|
440
424
|
content: line,
|
|
441
425
|
usageType,
|
|
442
426
|
isDefinition: false,
|
|
427
|
+
...(usageType === 'text' && {
|
|
428
|
+
textKind: 'comment-or-string',
|
|
429
|
+
}),
|
|
443
430
|
...(callerSym && {
|
|
444
431
|
callerName: callerSym.name,
|
|
445
432
|
callerStartLine: callerSym.startLine
|
|
@@ -497,16 +484,39 @@ function search(index, term, options = {}) {
|
|
|
497
484
|
let filesSkipped = 0;
|
|
498
485
|
let filesFilteredByFlag = 0;
|
|
499
486
|
const regexFlags = options.caseSensitive ? 'g' : 'gi';
|
|
500
|
-
const useRegex = options.regex
|
|
487
|
+
const useRegex = options.regex === true; // Safe default: literal text
|
|
501
488
|
let regex;
|
|
502
|
-
let
|
|
489
|
+
let linearRegex = null;
|
|
503
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
|
+
}
|
|
504
496
|
try {
|
|
505
497
|
regex = new RegExp(term, regexFlags);
|
|
506
498
|
} catch (e) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
}
|
|
510
520
|
}
|
|
511
521
|
} else {
|
|
512
522
|
regex = new RegExp(escapeRegExp(term), regexFlags);
|
|
@@ -541,7 +551,12 @@ function search(index, term, options = {}) {
|
|
|
541
551
|
try {
|
|
542
552
|
const parser = getParser(language);
|
|
543
553
|
const { findMatchesWithASTFilter } = require('../languages/utils');
|
|
544
|
-
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
|
+
});
|
|
545
560
|
|
|
546
561
|
for (const m of astMatches) {
|
|
547
562
|
const match = {
|
|
@@ -580,8 +595,9 @@ function search(index, term, options = {}) {
|
|
|
580
595
|
|
|
581
596
|
// Fallback to regex-based search (non-codeOnly or unsupported language)
|
|
582
597
|
lines.forEach((line, idx) => {
|
|
583
|
-
regex.lastIndex = 0; // Reset
|
|
584
|
-
if (
|
|
598
|
+
regex.lastIndex = 0; // Reset V8 fallback state
|
|
599
|
+
if ((linearRegex && linearRegex.test(line)) ||
|
|
600
|
+
(!linearRegex && regex.test(line))) {
|
|
585
601
|
const lineNum = idx + 1;
|
|
586
602
|
// Skip if codeOnly and line is comment/string
|
|
587
603
|
if (options.codeOnly && index.isCommentOrStringAtPosition(content, lineNum, 0, filePath)) {
|
|
@@ -623,6 +639,7 @@ function search(index, term, options = {}) {
|
|
|
623
639
|
|
|
624
640
|
// Apply top limit (limits total matches across all files)
|
|
625
641
|
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
642
|
+
const totalMatchedFiles = results.length;
|
|
626
643
|
let truncatedMatches = 0;
|
|
627
644
|
if (options.top && options.top > 0 && totalMatches > options.top) {
|
|
628
645
|
let remaining = options.top;
|
|
@@ -642,7 +659,19 @@ function search(index, term, options = {}) {
|
|
|
642
659
|
results.push(...truncated);
|
|
643
660
|
}
|
|
644
661
|
|
|
645
|
-
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
|
+
};
|
|
646
675
|
return results;
|
|
647
676
|
} finally { index._endOp(); }
|
|
648
677
|
}
|
|
@@ -730,6 +759,9 @@ function structuralSearch(index, options = {}) {
|
|
|
730
759
|
const calls = getCachedCalls(index, filePath);
|
|
731
760
|
if (!calls) continue;
|
|
732
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;
|
|
733
765
|
if (nameMatcher && !nameMatcher(call.name)) continue;
|
|
734
766
|
// Field-hop receivers (`tm.service.Save()`) carry
|
|
735
767
|
// receiverRoot/receiverField instead of receiver —
|
|
@@ -764,6 +796,8 @@ function structuralSearch(index, options = {}) {
|
|
|
764
796
|
const classTypes = new Set(['class', 'struct', 'interface', 'impl', 'trait', 'record', 'enum']);
|
|
765
797
|
const typeTypes = new Set(['type', 'enum', 'interface', 'trait', 'record', 'namespace']);
|
|
766
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']);
|
|
767
801
|
|
|
768
802
|
for (const [symbolName, definitions] of index.symbols) {
|
|
769
803
|
if (nameMatcher && !nameMatcher(symbolName)) continue;
|
|
@@ -774,6 +808,8 @@ function structuralSearch(index, options = {}) {
|
|
|
774
808
|
if (type === 'class' && !classTypes.has(def.type)) continue;
|
|
775
809
|
if (type === 'method' && !methodTypes.has(def.type) && !def.isMethod) continue;
|
|
776
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;
|
|
777
813
|
|
|
778
814
|
// File filters
|
|
779
815
|
const fileEntry = index.files.get(def.file);
|
|
@@ -811,16 +847,17 @@ function structuralSearch(index, options = {}) {
|
|
|
811
847
|
|
|
812
848
|
// Exported filter
|
|
813
849
|
if (exported) {
|
|
814
|
-
const
|
|
815
|
-
const isExp =
|
|
816
|
-
|
|
817
|
-
mods.some(m => m.startsWith('pub')) ||
|
|
818
|
-
(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 || []));
|
|
819
853
|
if (!isExp) continue;
|
|
820
854
|
}
|
|
821
855
|
|
|
822
856
|
// Unused filter (expensive — last check)
|
|
823
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;
|
|
824
861
|
// Named function expressions are consumed by their
|
|
825
862
|
// expression position — never "unused" (the deadcode
|
|
826
863
|
// twin of the bodyScopedName audit skip).
|
|
@@ -854,7 +891,7 @@ function structuralSearch(index, options = {}) {
|
|
|
854
891
|
// #234, campaign G2 ×4 languages: Go main/init, Java
|
|
855
892
|
// main, Rust main/#[test] all listed — the deadcode
|
|
856
893
|
// protection, applied here).
|
|
857
|
-
const langModule = fileEntry &&
|
|
894
|
+
const langModule = fileEntry && getLanguageAdapter(fileEntry.language);
|
|
858
895
|
if (langModule?.isEntryPoint?.(def)) continue;
|
|
859
896
|
// A bare decorator application (@with_logging) invokes
|
|
860
897
|
// the decorator at import time but is recorded as a
|
|
@@ -917,6 +954,10 @@ function structuralSearch(index, options = {}) {
|
|
|
917
954
|
}).filter(([, v]) => v !== undefined && v !== null)),
|
|
918
955
|
totalMatched: total,
|
|
919
956
|
shown: results.length,
|
|
957
|
+
...(unused && {
|
|
958
|
+
unusedScope: 'callable-symbols-only',
|
|
959
|
+
unusedSafety: 'candidate-only; use deadcode before deletion',
|
|
960
|
+
}),
|
|
920
961
|
}
|
|
921
962
|
};
|
|
922
963
|
} finally { index._endOp(); }
|
|
@@ -1269,6 +1310,50 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1269
1310
|
if (targetRels.size === 0) targetRels = null;
|
|
1270
1311
|
}
|
|
1271
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
|
+
|
|
1272
1357
|
const className = options.className || null;
|
|
1273
1358
|
// className scoping accepts the class plus its non-overriding
|
|
1274
1359
|
// descendants — a subclass instance without its own override dispatches
|
|
@@ -1301,14 +1386,28 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1301
1386
|
if (hits.length > 0) linkedRecords = hits;
|
|
1302
1387
|
}
|
|
1303
1388
|
}
|
|
1389
|
+
const provenFileSites = fileTargetCallerSites.get(testPath) || [];
|
|
1304
1390
|
|
|
1305
1391
|
// Fast pre-check: skip if searchTerm doesn't appear in file
|
|
1306
|
-
if (!content.includes(searchTerm) && !linkedRecords) continue;
|
|
1392
|
+
if (!isFilePath && !content.includes(searchTerm) && !linkedRecords) continue;
|
|
1393
|
+
if (isFilePath && !linkedRecords && provenFileSites.length === 0) continue;
|
|
1307
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');
|
|
1308
1403
|
|
|
1309
1404
|
// AST-based usage detection
|
|
1310
|
-
|
|
1311
|
-
|
|
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;
|
|
1312
1411
|
// Compiler attributes can invoke generated builder methods across
|
|
1313
1412
|
// workspace/facade boundaries that the source import graph cannot
|
|
1314
1413
|
// represent (`#[arg(value_delimiter = ',')]`). Keep these explicit
|
|
@@ -1316,7 +1415,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1316
1415
|
// test. Ordinary calls/references still require source ownership.
|
|
1317
1416
|
const hasAttributeReference = astUsages.some(u => u.inAttribute &&
|
|
1318
1417
|
(!testRanges || lineInRanges(u.line, testRanges)));
|
|
1319
|
-
if (!sourceFileLinked && !hasAttributeReference) continue;
|
|
1418
|
+
if (!sourceFileLinked && !hasAttributeReference && !classIdentityHint) continue;
|
|
1320
1419
|
// className scoping normally requires the class or a dispatching
|
|
1321
1420
|
// descendant in the file. Generated attribute references are the
|
|
1322
1421
|
// conservative exception: they carry an explicit unverified tier.
|
|
@@ -1360,7 +1459,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1360
1459
|
|
|
1361
1460
|
for (const usage of astUsages) {
|
|
1362
1461
|
if (usage.usageType === 'definition') continue; // not relevant in test files
|
|
1363
|
-
if (!sourceFileLinked && !usage.inAttribute) continue;
|
|
1462
|
+
if (!sourceFileLinked && !usage.inAttribute && !classIdentityHint) continue;
|
|
1364
1463
|
// Inline-test-promoted file: only lines inside the test
|
|
1365
1464
|
// ranges are test code (fix #244).
|
|
1366
1465
|
if (testRanges && !lineInRanges(usage.line, testRanges)) continue;
|
|
@@ -1378,7 +1477,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1378
1477
|
if (usage.usageType === 'import') {
|
|
1379
1478
|
matchType = 'import';
|
|
1380
1479
|
} else if (usage.usageType === 'call') {
|
|
1381
|
-
matchType = 'call';
|
|
1480
|
+
matchType = classIdentityHint ? 'unverified-call' : 'call';
|
|
1382
1481
|
} else {
|
|
1383
1482
|
// 'reference' — check if inside string literal
|
|
1384
1483
|
matchType = strPattern.test(lineContent) ? 'string-ref' :
|
|
@@ -1386,7 +1485,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1386
1485
|
}
|
|
1387
1486
|
|
|
1388
1487
|
// className scoping for calls: check receiver
|
|
1389
|
-
if (className && matchType === 'call') {
|
|
1488
|
+
if (className && (matchType === 'call' || matchType === 'unverified-call')) {
|
|
1390
1489
|
if (!_receiverMatchesClass(usage, dispatchNames, instanceTypeMap, lineContent, searchTerm)) continue;
|
|
1391
1490
|
}
|
|
1392
1491
|
|
|
@@ -1416,7 +1515,11 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1416
1515
|
matches.push({
|
|
1417
1516
|
line: usage.line,
|
|
1418
1517
|
content: lineContent.trim(),
|
|
1419
|
-
matchType
|
|
1518
|
+
matchType,
|
|
1519
|
+
...(matchType === 'unverified-call' && {
|
|
1520
|
+
evidenceTier: 'unverified',
|
|
1521
|
+
reason: 'class-reference-without-source-ownership',
|
|
1522
|
+
}),
|
|
1420
1523
|
});
|
|
1421
1524
|
}
|
|
1422
1525
|
|
|
@@ -1455,10 +1558,29 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1455
1558
|
}
|
|
1456
1559
|
}
|
|
1457
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
|
+
|
|
1458
1580
|
// Language-aware test-case detection. Under a local same-name
|
|
1459
1581
|
// shadow the term on a test line is the file's OWN helper —
|
|
1460
1582
|
// only anchor test cases to matches that survived the shadow.
|
|
1461
|
-
if (sourceFileLinked &&
|
|
1583
|
+
if (!isFilePath && sourceFileLinked &&
|
|
1462
1584
|
(!localShadow || matches.some(m => m.matchType !== 'import'))) {
|
|
1463
1585
|
_addTestCaseMatches(index, testPath, entry, searchTerm, className, instanceTypeMap, matches);
|
|
1464
1586
|
}
|
|
@@ -1477,7 +1599,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1477
1599
|
}
|
|
1478
1600
|
|
|
1479
1601
|
const filtered = options.callsOnly
|
|
1480
|
-
? finalMatches.filter(m =>
|
|
1602
|
+
? finalMatches.filter(m => ['call', 'unverified-call', 'test-case'].includes(m.matchType))
|
|
1481
1603
|
: finalMatches;
|
|
1482
1604
|
if (filtered.length > 0) {
|
|
1483
1605
|
results.push({
|
|
@@ -1542,7 +1664,8 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1542
1664
|
const localSameName = (info.entry.symbols || []).some(s => s.name === searchTerm);
|
|
1543
1665
|
const importsTargetName = (info.entry.importBindings || []).some(b => b.name === searchTerm);
|
|
1544
1666
|
const explicitlyScopedToLocal = !!options.file && targetDefs.some(d => d.file === site.file);
|
|
1545
|
-
if (localSameName && !importsTargetName && !explicitlyScopedToLocal &&
|
|
1667
|
+
if (localSameName && !importsTargetName && !explicitlyScopedToLocal &&
|
|
1668
|
+
!site.receiver && !site.isMethod) continue;
|
|
1546
1669
|
|
|
1547
1670
|
let fileResult = results.find(r => r.file === info.entry.relativePath);
|
|
1548
1671
|
if (!fileResult) {
|
|
@@ -1639,7 +1762,16 @@ function _buildSourceFileImporters(index, defs) {
|
|
|
1639
1762
|
// If so, add it to the queue so its importers are also discovered.
|
|
1640
1763
|
if (!visited.has(imp)) {
|
|
1641
1764
|
const fe = index.files.get(imp);
|
|
1642
|
-
|
|
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))) {
|
|
1643
1775
|
visited.add(imp);
|
|
1644
1776
|
queue.push(imp);
|
|
1645
1777
|
}
|
|
@@ -1921,7 +2053,7 @@ function _addTestCaseMatches(index, filePath, fileEntry, searchTerm, className,
|
|
|
1921
2053
|
// Go/Python/Java/Rust: check if any AST usage falls within a test function's range
|
|
1922
2054
|
if (!fileEntry.symbols) return;
|
|
1923
2055
|
try {
|
|
1924
|
-
const langModule =
|
|
2056
|
+
const langModule = getLanguageAdapter(lang);
|
|
1925
2057
|
if (!langModule) return;
|
|
1926
2058
|
// Prefer kinded predicate so fn main() and fn init() aren't classified
|
|
1927
2059
|
// as test cases (BUG-CX). Fall back to isEntryPoint for compat.
|