ucn 3.8.22 → 3.8.25
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 +114 -11
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +960 -37
- package/core/bridge.js +1111 -0
- package/core/brief.js +408 -0
- package/core/cache.js +213 -59
- package/core/callers.js +117 -41
- package/core/check.js +200 -0
- package/core/deadcode.js +31 -2
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph-build.js +4 -4
- package/core/graph.js +31 -12
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parallel-build.js +10 -7
- package/core/parser.js +8 -2
- package/core/project.js +147 -41
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +139 -15
- package/core/shared.js +101 -5
- package/core/tracing.js +31 -12
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +10 -2
package/core/search.js
CHANGED
|
@@ -221,11 +221,15 @@ function usages(index, name, options = {}) {
|
|
|
221
221
|
let _importedHasDef = null;
|
|
222
222
|
const importedFileHasDef = () => {
|
|
223
223
|
if (_importedHasDef !== null) return _importedHasDef;
|
|
224
|
-
const importedFiles = index.importGraph.get(filePath)
|
|
225
|
-
_importedHasDef =
|
|
224
|
+
const importedFiles = index.importGraph.get(filePath);
|
|
225
|
+
_importedHasDef = false;
|
|
226
|
+
if (importedFiles) for (const imp of importedFiles) {
|
|
226
227
|
const impEntry = index.files.get(imp);
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
if (impEntry?.symbols?.some(s => s.name === name)) {
|
|
229
|
+
_importedHasDef = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
229
233
|
return _importedHasDef;
|
|
230
234
|
};
|
|
231
235
|
|
|
@@ -249,6 +253,15 @@ function usages(index, name, options = {}) {
|
|
|
249
253
|
|
|
250
254
|
const lineContent = lines[u.line - 1] || '';
|
|
251
255
|
|
|
256
|
+
// BUG-4: enrich call usages with enclosing-function info so
|
|
257
|
+
// the JSON `handle` shows the real caller, not `_topLevel`.
|
|
258
|
+
// Only resolve for call usages — definitions, imports, and
|
|
259
|
+
// bare references don't need a caller token.
|
|
260
|
+
let callerSym = null;
|
|
261
|
+
if (u.usageType === 'call') {
|
|
262
|
+
callerSym = index.findEnclosingFunction(filePath, u.line, true);
|
|
263
|
+
}
|
|
264
|
+
|
|
252
265
|
const usage = {
|
|
253
266
|
file: filePath,
|
|
254
267
|
relativePath: fileEntry.relativePath,
|
|
@@ -256,7 +269,11 @@ function usages(index, name, options = {}) {
|
|
|
256
269
|
content: lineContent,
|
|
257
270
|
usageType: u.usageType,
|
|
258
271
|
isDefinition: false,
|
|
259
|
-
...(u.receiver && { receiver: u.receiver })
|
|
272
|
+
...(u.receiver && { receiver: u.receiver }),
|
|
273
|
+
...(callerSym && {
|
|
274
|
+
callerName: callerSym.name,
|
|
275
|
+
callerStartLine: callerSym.startLine
|
|
276
|
+
})
|
|
260
277
|
};
|
|
261
278
|
|
|
262
279
|
// Add context lines if requested
|
|
@@ -301,13 +318,23 @@ function usages(index, name, options = {}) {
|
|
|
301
318
|
// Classify usage type (AST-based, defaults to 'reference' for unsupported languages)
|
|
302
319
|
const usageType = index.classifyUsageAST(content, lineNum, name, filePath) ?? 'reference';
|
|
303
320
|
|
|
321
|
+
// BUG-4: enrich call usages with enclosing-function info.
|
|
322
|
+
let callerSym = null;
|
|
323
|
+
if (usageType === 'call') {
|
|
324
|
+
callerSym = index.findEnclosingFunction(filePath, lineNum, true);
|
|
325
|
+
}
|
|
326
|
+
|
|
304
327
|
const usage = {
|
|
305
328
|
file: filePath,
|
|
306
329
|
relativePath: fileEntry.relativePath,
|
|
307
330
|
line: lineNum,
|
|
308
331
|
content: line,
|
|
309
332
|
usageType,
|
|
310
|
-
isDefinition: false
|
|
333
|
+
isDefinition: false,
|
|
334
|
+
...(callerSym && {
|
|
335
|
+
callerName: callerSym.name,
|
|
336
|
+
callerStartLine: callerSym.startLine
|
|
337
|
+
})
|
|
311
338
|
};
|
|
312
339
|
|
|
313
340
|
// Add context lines if requested
|
|
@@ -718,25 +745,50 @@ function structuralSearch(index, options = {}) {
|
|
|
718
745
|
}
|
|
719
746
|
|
|
720
747
|
/**
|
|
721
|
-
* Find the best usage example of a function
|
|
748
|
+
* Find the best usage example of a function (or, with `diverse`, one
|
|
749
|
+
* representative per distinct argument-shape cluster).
|
|
722
750
|
*
|
|
723
751
|
* @param {object} index - ProjectIndex instance
|
|
724
752
|
* @param {string} name - Function name
|
|
725
|
-
* @param {object} options - { className }
|
|
726
|
-
* @returns {object|null} Best example with score
|
|
753
|
+
* @param {object} options - { className, diverse, top }
|
|
754
|
+
* @returns {object|null} Best example with score; with diverse=true, also `clusters`
|
|
727
755
|
*/
|
|
728
756
|
function example(index, name, options = {}) {
|
|
729
757
|
index._beginOp();
|
|
730
758
|
try {
|
|
759
|
+
// MEDIUM-8: respect --include-tests. By default, test/fixture/mock files
|
|
760
|
+
// are excluded as low-signal examples. When the user passes
|
|
761
|
+
// includeTests:true, return matches from those files too.
|
|
762
|
+
const exclude = options.includeTests
|
|
763
|
+
? []
|
|
764
|
+
: ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'];
|
|
731
765
|
const usageResults = usages(index, name, {
|
|
732
766
|
codeOnly: true,
|
|
733
767
|
className: options.className,
|
|
734
|
-
exclude
|
|
768
|
+
exclude,
|
|
735
769
|
context: 5
|
|
736
770
|
});
|
|
737
771
|
|
|
738
772
|
const calls = usageResults.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
739
|
-
if (calls.length === 0)
|
|
773
|
+
if (calls.length === 0) {
|
|
774
|
+
// No matches in non-test files. Look at how many WERE excluded so we
|
|
775
|
+
// can tell the user there's something they could see with
|
|
776
|
+
// --include-tests, instead of saying "no examples found" silently.
|
|
777
|
+
if (!options.includeTests) {
|
|
778
|
+
const allUsages = usages(index, name, {
|
|
779
|
+
codeOnly: true,
|
|
780
|
+
className: options.className,
|
|
781
|
+
exclude: [],
|
|
782
|
+
context: 0,
|
|
783
|
+
});
|
|
784
|
+
const excludedCalls = allUsages.filter(u =>
|
|
785
|
+
u.usageType === 'call' && !u.isDefinition).length;
|
|
786
|
+
if (excludedCalls > 0) {
|
|
787
|
+
return { best: null, totalCalls: 0, excludedTestCalls: excludedCalls };
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
740
792
|
|
|
741
793
|
const scored = calls.map(call => {
|
|
742
794
|
let score = 0;
|
|
@@ -777,7 +829,73 @@ function example(index, name, options = {}) {
|
|
|
777
829
|
});
|
|
778
830
|
|
|
779
831
|
scored.sort((a, b) => b.score - a.score);
|
|
780
|
-
|
|
832
|
+
const best = scored[0];
|
|
833
|
+
|
|
834
|
+
// Default behavior: one best representative.
|
|
835
|
+
if (!options.diverse) {
|
|
836
|
+
return { best, totalCalls: calls.length };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// --diverse: cluster call sites by AST argument-shape and return one
|
|
840
|
+
// representative per cluster (up to `top`, default 3).
|
|
841
|
+
//
|
|
842
|
+
// Shape key = "argCount:argKind1,argKind2,..." (see classifyArgNode in
|
|
843
|
+
// verify.js for the kind set). Calls with no resolvable AST shape go into
|
|
844
|
+
// a single 'unknown' cluster so they're still surfaced.
|
|
845
|
+
//
|
|
846
|
+
// Representative selection: highest existing example score within the
|
|
847
|
+
// cluster — this reuses the quality heuristic above (typed assignment,
|
|
848
|
+
// documented, standalone, etc.) so each cluster's example is the *best*
|
|
849
|
+
// member of that shape, not just the first-seen one.
|
|
850
|
+
const top = (options.top != null && Number(options.top) > 0) ? Number(options.top) : 3;
|
|
851
|
+
const clusterMap = new Map(); // shapeKey -> { shapeKey, kinds, count, members:[] }
|
|
852
|
+
|
|
853
|
+
for (const call of scored) {
|
|
854
|
+
const shape = index._analyzeCallShape(call.file, call.line, name);
|
|
855
|
+
const key = shape ? shape.shapeKey : 'unknown';
|
|
856
|
+
if (!clusterMap.has(key)) {
|
|
857
|
+
clusterMap.set(key, {
|
|
858
|
+
shapeKey: key,
|
|
859
|
+
argKinds: shape ? shape.argKinds : null,
|
|
860
|
+
argCount: shape ? shape.argCount : null,
|
|
861
|
+
count: 0,
|
|
862
|
+
members: [],
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
const c = clusterMap.get(key);
|
|
866
|
+
c.count++;
|
|
867
|
+
c.members.push({ ...call, _argTexts: shape ? shape.argTexts : null });
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Order clusters: largest first, ties broken by shapeKey for determinism.
|
|
871
|
+
const clusterList = [...clusterMap.values()].sort((a, b) =>
|
|
872
|
+
(b.count - a.count) || a.shapeKey.localeCompare(b.shapeKey)
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
// Pick representative per cluster: highest-scoring member; ties broken
|
|
876
|
+
// by (file, line) for stable output across runs.
|
|
877
|
+
const clusters = clusterList.slice(0, top).map(cluster => {
|
|
878
|
+
const sortedMembers = [...cluster.members].sort((a, b) =>
|
|
879
|
+
(b.score - a.score) ||
|
|
880
|
+
(a.relativePath || a.file || '').localeCompare(b.relativePath || b.file || '') ||
|
|
881
|
+
(a.line || 0) - (b.line || 0)
|
|
882
|
+
);
|
|
883
|
+
const rep = sortedMembers[0];
|
|
884
|
+
return {
|
|
885
|
+
shapeKey: cluster.shapeKey,
|
|
886
|
+
argKinds: cluster.argKinds,
|
|
887
|
+
argCount: cluster.argCount,
|
|
888
|
+
count: cluster.count,
|
|
889
|
+
representative: rep,
|
|
890
|
+
};
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
return {
|
|
894
|
+
best,
|
|
895
|
+
totalCalls: calls.length,
|
|
896
|
+
clusters,
|
|
897
|
+
totalClusters: clusterList.length,
|
|
898
|
+
};
|
|
781
899
|
} finally { index._endOp(); }
|
|
782
900
|
}
|
|
783
901
|
|
|
@@ -1008,7 +1126,7 @@ function _buildSourceFileImporters(index, defs) {
|
|
|
1008
1126
|
|
|
1009
1127
|
while (queue.length > 0) {
|
|
1010
1128
|
const current = queue.shift();
|
|
1011
|
-
const directImporters = index.exportGraph?.get(current) ||
|
|
1129
|
+
const directImporters = index.exportGraph?.get(current) || new Set();
|
|
1012
1130
|
for (const imp of directImporters) {
|
|
1013
1131
|
importers.add(imp);
|
|
1014
1132
|
// Check if this importer re-exports the symbol (barrel pattern).
|
|
@@ -1244,11 +1362,17 @@ function _addTestCaseMatches(index, filePath, fileEntry, searchTerm, className,
|
|
|
1244
1362
|
if (!fileEntry.symbols) return;
|
|
1245
1363
|
try {
|
|
1246
1364
|
const langModule = getLanguageModule(lang);
|
|
1247
|
-
if (!langModule
|
|
1365
|
+
if (!langModule) return;
|
|
1366
|
+
// Prefer kinded predicate so fn main() and fn init() aren't classified
|
|
1367
|
+
// as test cases (BUG-CX). Fall back to isEntryPoint for compat.
|
|
1368
|
+
const classify = langModule.getEntryPointKind
|
|
1369
|
+
? (s) => langModule.getEntryPointKind(s) === 'test'
|
|
1370
|
+
: langModule.isEntryPoint;
|
|
1371
|
+
if (!classify) return;
|
|
1248
1372
|
|
|
1249
1373
|
// Find test symbols
|
|
1250
1374
|
for (const symbol of fileEntry.symbols) {
|
|
1251
|
-
if (!
|
|
1375
|
+
if (!classify(symbol)) continue;
|
|
1252
1376
|
// Check if any non-import usage of searchTerm falls within this test function
|
|
1253
1377
|
const usageInRange = matches.some(m =>
|
|
1254
1378
|
m.line >= symbol.startLine && m.line <= symbol.endLine &&
|
package/core/shared.js
CHANGED
|
@@ -5,29 +5,61 @@
|
|
|
5
5
|
const { isTestFile } = require('./discovery');
|
|
6
6
|
const { detectLanguage } = require('./parser');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Path-based test heuristic — matches the same patterns as `find`'s exclusion
|
|
10
|
+
* logic so that `about` and `find` agree on which files are de-emphasized.
|
|
11
|
+
*
|
|
12
|
+
* Triggers when any of `test|tests|spec|__tests__|__mocks__|fixture|mock`
|
|
13
|
+
* appears as a path segment (with word boundaries on both sides).
|
|
14
|
+
*
|
|
15
|
+
* Complement to `isTestFile` (filename pattern check) — together they catch
|
|
16
|
+
* both `foo.test.js` (filename) AND `test/agent-benchmark.js` (directory).
|
|
17
|
+
*/
|
|
18
|
+
function isTestPath(rp) {
|
|
19
|
+
if (!rp) return false;
|
|
20
|
+
return /(^|[/._-])(test|tests|spec|__tests__|__mocks__|fixture|mock)s?([/._-]|$)/i.test(rp);
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
/**
|
|
9
24
|
* Pick the best definition from multiple matches.
|
|
10
25
|
* Prefers non-test, src/lib files, larger function bodies.
|
|
26
|
+
*
|
|
27
|
+
* BUG-M4: align with `find`'s exclusion ordering so `about` and `find` pick
|
|
28
|
+
* the same primary. Adds path-based test detection (covers `test/foo.js`
|
|
29
|
+
* which `isTestFile` misses) and prefers files that are imported by others
|
|
30
|
+
* (real source) over test/fixture files.
|
|
11
31
|
*/
|
|
12
|
-
function pickBestDefinition(matches) {
|
|
32
|
+
function pickBestDefinition(matches, opts = {}) {
|
|
13
33
|
const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
|
|
34
|
+
const importGraph = opts.importGraph;
|
|
14
35
|
const scored = matches.map(m => {
|
|
15
36
|
let score = 0;
|
|
16
37
|
const rp = m.relativePath || '';
|
|
17
38
|
// Prefer class/struct/interface types (+1000) - same as resolveSymbol
|
|
18
39
|
if (typeOrder.has(m.type)) score += 1000;
|
|
19
|
-
|
|
40
|
+
// Test file penalties: -500 for filename pattern OR path segment.
|
|
41
|
+
// Both checks because `isTestFile` only matches `*.test.js`/`__tests__/`,
|
|
42
|
+
// not `test/agent-benchmark.js` which `find` excludes via path regex.
|
|
43
|
+
if (isTestFile(rp, detectLanguage(m.file)) || isTestPath(rp)) score -= 500;
|
|
20
44
|
if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
|
|
21
45
|
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
|
|
46
|
+
// Prefer files that are imported by something — real source over scripts/fixtures.
|
|
47
|
+
if (importGraph && m.file) {
|
|
48
|
+
for (const [, importedFiles] of importGraph) {
|
|
49
|
+
if (importedFiles.has(m.file)) { score += 100; break; }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
22
52
|
// Deprioritize type-only overload signatures (TypeScript function_signature)
|
|
23
53
|
if (m.isSignature) score -= 200;
|
|
24
54
|
// Tiebreaker: prefer larger function bodies (more important/complex)
|
|
25
55
|
if (m.startLine && m.endLine) {
|
|
26
56
|
score += Math.min(m.endLine - m.startLine, 100);
|
|
27
57
|
}
|
|
28
|
-
return { match: m, score };
|
|
58
|
+
return { match: m, score, rp };
|
|
29
59
|
});
|
|
30
|
-
|
|
60
|
+
// Stable sort: by score desc, then alphabetical relativePath (so two equal-score
|
|
61
|
+
// matches always pick the same one across runs).
|
|
62
|
+
scored.sort((a, b) => (b.score - a.score) || a.rp.localeCompare(b.rp));
|
|
31
63
|
return scored[0].match;
|
|
32
64
|
}
|
|
33
65
|
|
|
@@ -52,4 +84,68 @@ function escapeRegExp(text) {
|
|
|
52
84
|
// Symbol types that are not callable (used to filter class/struct/type declarations from call analysis)
|
|
53
85
|
const NON_CALLABLE_TYPES = new Set(['class', 'struct', 'interface', 'type', 'enum', 'trait', 'state', 'impl', 'field']);
|
|
54
86
|
|
|
55
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Stable symbol handle: `relativePath:line` or `relativePath:line:name`.
|
|
89
|
+
*
|
|
90
|
+
* Handles let multi-step workflows roundtrip without name disambiguation —
|
|
91
|
+
* `find` returns handles, `brief`/`impact`/`context` accept them, and the result
|
|
92
|
+
* targets the exact same symbol even when names overlap. Renames break handles
|
|
93
|
+
* (intentionally — that's a real change, not noise).
|
|
94
|
+
*
|
|
95
|
+
* Format chosen because:
|
|
96
|
+
* - colon is the existing UCN location separator (`file.js:42`)
|
|
97
|
+
* - line number is the second-most-stable ID (after path); names move within files
|
|
98
|
+
* - the optional :name disambiguates when multiple symbols share a startLine
|
|
99
|
+
* (rare but possible: e.g. `class Foo {}` and a same-line method def)
|
|
100
|
+
*/
|
|
101
|
+
function formatSymbolHandle(symbol) {
|
|
102
|
+
if (!symbol || !symbol.startLine) return null;
|
|
103
|
+
const file = symbol.relativePath || symbol.file;
|
|
104
|
+
if (!file) return null;
|
|
105
|
+
return symbol.name ? `${file}:${symbol.startLine}:${symbol.name}` : `${file}:${symbol.startLine}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse a handle string back into its components.
|
|
110
|
+
* Returns { file, line, name? } or null if input doesn't look like a handle.
|
|
111
|
+
*
|
|
112
|
+
* Strategy: scan from the right so paths containing `:` (Windows drive letters
|
|
113
|
+
* if anyone ever uses them as relative paths) survive. The line number is the
|
|
114
|
+
* rightmost run of digits between two colons or at the end.
|
|
115
|
+
*/
|
|
116
|
+
function parseSymbolHandle(input) {
|
|
117
|
+
if (!input || typeof input !== 'string') return null;
|
|
118
|
+
// Two forms:
|
|
119
|
+
// path:digits → file + line
|
|
120
|
+
// path:digits:name → file + line + name (name may contain anything)
|
|
121
|
+
// The line is always digits; the path is everything before the line; the
|
|
122
|
+
// optional name is everything after the line.
|
|
123
|
+
const m = input.match(/^(.+):(\d+)(?::(.+))?$/);
|
|
124
|
+
if (!m) return null;
|
|
125
|
+
const file = m[1];
|
|
126
|
+
const line = parseInt(m[2], 10);
|
|
127
|
+
if (!Number.isFinite(line) || line < 1) return null;
|
|
128
|
+
const handle = { file, line };
|
|
129
|
+
if (m[3] != null) handle.name = m[3];
|
|
130
|
+
return handle;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Quick predicate: does this string look like a handle (vs a plain symbol name)?
|
|
135
|
+
* Used to short-circuit handle resolution before name lookup.
|
|
136
|
+
*/
|
|
137
|
+
function looksLikeHandle(input) {
|
|
138
|
+
if (!input || typeof input !== 'string') return false;
|
|
139
|
+
return /^.+:\d+(?::.+)?$/.test(input);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
pickBestDefinition,
|
|
144
|
+
addTestExclusions,
|
|
145
|
+
escapeRegExp,
|
|
146
|
+
NON_CALLABLE_TYPES,
|
|
147
|
+
formatSymbolHandle,
|
|
148
|
+
parseSymbolHandle,
|
|
149
|
+
looksLikeHandle,
|
|
150
|
+
isTestPath,
|
|
151
|
+
};
|
package/core/tracing.js
CHANGED
|
@@ -32,7 +32,7 @@ function trace(index, name, options = {}) {
|
|
|
32
32
|
// trace defaults to includeMethods=true (execution flow should show method calls)
|
|
33
33
|
const includeMethods = options.includeMethods ?? true;
|
|
34
34
|
|
|
35
|
-
const { def, definitions, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className });
|
|
35
|
+
const { def, definitions, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
36
36
|
if (!def) {
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
@@ -159,7 +159,7 @@ function blast(index, name, options = {}) {
|
|
|
159
159
|
const includeUncertain = options.includeUncertain || false;
|
|
160
160
|
const exclude = options.exclude || [];
|
|
161
161
|
|
|
162
|
-
const { def, definitions, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className });
|
|
162
|
+
const { def, definitions, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
163
163
|
if (!def) return null;
|
|
164
164
|
|
|
165
165
|
const visited = new Set();
|
|
@@ -334,7 +334,7 @@ function reverseTrace(index, name, options = {}) {
|
|
|
334
334
|
const includeUncertain = options.includeUncertain || false;
|
|
335
335
|
const exclude = options.exclude || [];
|
|
336
336
|
|
|
337
|
-
const { def, definitions, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className });
|
|
337
|
+
const { def, definitions, warnings } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
338
338
|
if (!def) return null;
|
|
339
339
|
|
|
340
340
|
const visited = new Set();
|
|
@@ -435,13 +435,22 @@ function reverseTrace(index, name, options = {}) {
|
|
|
435
435
|
if (callerEntries.length > maxChildren) {
|
|
436
436
|
node.truncatedChildren = callerEntries.length - maxChildren;
|
|
437
437
|
// Count entry points in truncated branches so summary is accurate
|
|
438
|
+
// Use callerCache to avoid redundant findCallers calls
|
|
438
439
|
for (const { def: cDef } of callerEntries.slice(maxChildren)) {
|
|
439
|
-
const
|
|
440
|
-
if (!visited.has(
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
440
|
+
const cKey = `${cDef.file}:${cDef.startLine}`;
|
|
441
|
+
if (!visited.has(cKey)) {
|
|
442
|
+
const cCacheKey = cDef.bindingId
|
|
443
|
+
? `${cDef.name}:${cDef.bindingId}`
|
|
444
|
+
: `${cDef.name}:${cKey}`;
|
|
445
|
+
let cCallers = callerCache.get(cCacheKey);
|
|
446
|
+
if (!cCallers) {
|
|
447
|
+
cCallers = index.findCallers(cDef.name, {
|
|
448
|
+
includeMethods, includeUncertain,
|
|
449
|
+
targetDefinitions: cDef.bindingId ? [cDef] : undefined,
|
|
450
|
+
maxResults: 1, // Only need to know if any exist
|
|
451
|
+
});
|
|
452
|
+
callerCache.set(cCacheKey, cCallers);
|
|
453
|
+
}
|
|
445
454
|
if (cCallers.length === 0) {
|
|
446
455
|
entryPoints.push({ name: cDef.name, file: cDef.relativePath || path.relative(index.root, cDef.file), line: cDef.startLine });
|
|
447
456
|
}
|
|
@@ -552,8 +561,11 @@ function affectedTests(index, name, options = {}) {
|
|
|
552
561
|
for (const [filePath, fileEntry] of index.files) {
|
|
553
562
|
let isTest = isTestFile(fileEntry.relativePath, fileEntry.language);
|
|
554
563
|
// Rust inline #[cfg(test)] modules: source files with #[test]-marked symbols
|
|
564
|
+
// or symbols inside a #[cfg(test)] mod block (BUG-CY).
|
|
555
565
|
if (!isTest && fileEntry.language === 'rust') {
|
|
556
|
-
isTest = fileEntry.symbols?.some(s =>
|
|
566
|
+
isTest = fileEntry.symbols?.some(s =>
|
|
567
|
+
s.modifiers?.includes('test') || s.modifiers?.includes('cfg_test_module')
|
|
568
|
+
);
|
|
557
569
|
}
|
|
558
570
|
if (!isTest) continue;
|
|
559
571
|
if (excludeArr.length > 0 && !index.matchesFilters(fileEntry.relativePath, { exclude: excludeArr })) continue;
|
|
@@ -735,9 +747,16 @@ function _addAffectedTestCases(index, filePath, fileEntry, funcName, fileMatches
|
|
|
735
747
|
if (!fileEntry.symbols) return;
|
|
736
748
|
try {
|
|
737
749
|
const langModule = getLanguageModule(lang);
|
|
738
|
-
if (!langModule
|
|
750
|
+
if (!langModule) return;
|
|
751
|
+
// Prefer the kinded predicate so we don't mis-tag fn main() / fn init()
|
|
752
|
+
// (runtime entries) as test cases (BUG-CX). Fall back to isEntryPoint
|
|
753
|
+
// for backward compat with language modules that haven't been migrated.
|
|
754
|
+
const classify = langModule.getEntryPointKind
|
|
755
|
+
? (s) => langModule.getEntryPointKind(s) === 'test'
|
|
756
|
+
: langModule.isEntryPoint;
|
|
757
|
+
if (!classify) return;
|
|
739
758
|
for (const symbol of fileEntry.symbols) {
|
|
740
|
-
if (!
|
|
759
|
+
if (!classify(symbol)) continue;
|
|
741
760
|
// Only add test-case if a call match exists in the test body
|
|
742
761
|
const hasCallInRange = existingMatches.some(m =>
|
|
743
762
|
m.line >= symbol.startLine && m.line <= symbol.endLine &&
|