ucn 3.8.23 → 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 +936 -32
- package/core/bridge.js +1111 -0
- package/core/brief.js +408 -0
- package/core/cache.js +105 -5
- package/core/callers.js +72 -18
- package/core/check.js +200 -0
- 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.js +24 -2
- 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/parser.js +8 -2
- package/core/project.js +39 -3
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +130 -10
- package/core/shared.js +101 -5
- package/core/tracing.js +16 -6
- 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 +9 -1
package/core/search.js
CHANGED
|
@@ -253,6 +253,15 @@ function usages(index, name, options = {}) {
|
|
|
253
253
|
|
|
254
254
|
const lineContent = lines[u.line - 1] || '';
|
|
255
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
|
+
|
|
256
265
|
const usage = {
|
|
257
266
|
file: filePath,
|
|
258
267
|
relativePath: fileEntry.relativePath,
|
|
@@ -260,7 +269,11 @@ function usages(index, name, options = {}) {
|
|
|
260
269
|
content: lineContent,
|
|
261
270
|
usageType: u.usageType,
|
|
262
271
|
isDefinition: false,
|
|
263
|
-
...(u.receiver && { receiver: u.receiver })
|
|
272
|
+
...(u.receiver && { receiver: u.receiver }),
|
|
273
|
+
...(callerSym && {
|
|
274
|
+
callerName: callerSym.name,
|
|
275
|
+
callerStartLine: callerSym.startLine
|
|
276
|
+
})
|
|
264
277
|
};
|
|
265
278
|
|
|
266
279
|
// Add context lines if requested
|
|
@@ -305,13 +318,23 @@ function usages(index, name, options = {}) {
|
|
|
305
318
|
// Classify usage type (AST-based, defaults to 'reference' for unsupported languages)
|
|
306
319
|
const usageType = index.classifyUsageAST(content, lineNum, name, filePath) ?? 'reference';
|
|
307
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
|
+
|
|
308
327
|
const usage = {
|
|
309
328
|
file: filePath,
|
|
310
329
|
relativePath: fileEntry.relativePath,
|
|
311
330
|
line: lineNum,
|
|
312
331
|
content: line,
|
|
313
332
|
usageType,
|
|
314
|
-
isDefinition: false
|
|
333
|
+
isDefinition: false,
|
|
334
|
+
...(callerSym && {
|
|
335
|
+
callerName: callerSym.name,
|
|
336
|
+
callerStartLine: callerSym.startLine
|
|
337
|
+
})
|
|
315
338
|
};
|
|
316
339
|
|
|
317
340
|
// Add context lines if requested
|
|
@@ -722,25 +745,50 @@ function structuralSearch(index, options = {}) {
|
|
|
722
745
|
}
|
|
723
746
|
|
|
724
747
|
/**
|
|
725
|
-
* 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).
|
|
726
750
|
*
|
|
727
751
|
* @param {object} index - ProjectIndex instance
|
|
728
752
|
* @param {string} name - Function name
|
|
729
|
-
* @param {object} options - { className }
|
|
730
|
-
* @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`
|
|
731
755
|
*/
|
|
732
756
|
function example(index, name, options = {}) {
|
|
733
757
|
index._beginOp();
|
|
734
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'];
|
|
735
765
|
const usageResults = usages(index, name, {
|
|
736
766
|
codeOnly: true,
|
|
737
767
|
className: options.className,
|
|
738
|
-
exclude
|
|
768
|
+
exclude,
|
|
739
769
|
context: 5
|
|
740
770
|
});
|
|
741
771
|
|
|
742
772
|
const calls = usageResults.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
743
|
-
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
|
+
}
|
|
744
792
|
|
|
745
793
|
const scored = calls.map(call => {
|
|
746
794
|
let score = 0;
|
|
@@ -781,7 +829,73 @@ function example(index, name, options = {}) {
|
|
|
781
829
|
});
|
|
782
830
|
|
|
783
831
|
scored.sort((a, b) => b.score - a.score);
|
|
784
|
-
|
|
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
|
+
};
|
|
785
899
|
} finally { index._endOp(); }
|
|
786
900
|
}
|
|
787
901
|
|
|
@@ -1248,11 +1362,17 @@ function _addTestCaseMatches(index, filePath, fileEntry, searchTerm, className,
|
|
|
1248
1362
|
if (!fileEntry.symbols) return;
|
|
1249
1363
|
try {
|
|
1250
1364
|
const langModule = getLanguageModule(lang);
|
|
1251
|
-
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;
|
|
1252
1372
|
|
|
1253
1373
|
// Find test symbols
|
|
1254
1374
|
for (const symbol of fileEntry.symbols) {
|
|
1255
|
-
if (!
|
|
1375
|
+
if (!classify(symbol)) continue;
|
|
1256
1376
|
// Check if any non-import usage of searchTerm falls within this test function
|
|
1257
1377
|
const usageInRange = matches.some(m =>
|
|
1258
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();
|
|
@@ -561,8 +561,11 @@ function affectedTests(index, name, options = {}) {
|
|
|
561
561
|
for (const [filePath, fileEntry] of index.files) {
|
|
562
562
|
let isTest = isTestFile(fileEntry.relativePath, fileEntry.language);
|
|
563
563
|
// Rust inline #[cfg(test)] modules: source files with #[test]-marked symbols
|
|
564
|
+
// or symbols inside a #[cfg(test)] mod block (BUG-CY).
|
|
564
565
|
if (!isTest && fileEntry.language === 'rust') {
|
|
565
|
-
isTest = fileEntry.symbols?.some(s =>
|
|
566
|
+
isTest = fileEntry.symbols?.some(s =>
|
|
567
|
+
s.modifiers?.includes('test') || s.modifiers?.includes('cfg_test_module')
|
|
568
|
+
);
|
|
566
569
|
}
|
|
567
570
|
if (!isTest) continue;
|
|
568
571
|
if (excludeArr.length > 0 && !index.matchesFilters(fileEntry.relativePath, { exclude: excludeArr })) continue;
|
|
@@ -744,9 +747,16 @@ function _addAffectedTestCases(index, filePath, fileEntry, funcName, fileMatches
|
|
|
744
747
|
if (!fileEntry.symbols) return;
|
|
745
748
|
try {
|
|
746
749
|
const langModule = getLanguageModule(lang);
|
|
747
|
-
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;
|
|
748
758
|
for (const symbol of fileEntry.symbols) {
|
|
749
|
-
if (!
|
|
759
|
+
if (!classify(symbol)) continue;
|
|
750
760
|
// Only add test-case if a call match exists in the test body
|
|
751
761
|
const hasCallInRange = existingMatches.some(m =>
|
|
752
762
|
m.line >= symbol.startLine && m.line <= symbol.endLine &&
|