ucn 5.6.0 → 5.6.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 +4 -2
- package/.claude/skills/ucn/references/commands.md +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +5 -8
- package/core/analysis.js +3 -3
- package/core/cache.js +3 -1
- package/core/call-fit.js +130 -0
- package/core/callers.js +1 -1
- package/core/capture.js +40 -8
- package/core/change-kinds.js +221 -2
- package/core/check.js +48 -15
- package/core/command-contracts.js +2 -2
- package/core/deadcode.js +94 -23
- package/core/deletion.js +29 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/index-ir.js +2 -2
- package/core/ir.js +1 -1
- package/core/outgoing-check.js +116 -0
- package/core/output/check.js +5 -2
- package/core/output/reporting.js +4 -0
- package/core/output/search.js +3 -0
- package/core/output/tracing.js +1 -1
- package/core/output-budget.js +29 -11
- package/core/python-binding-identity.js +707 -0
- package/core/reflection.js +99 -10
- package/core/registry.js +2 -2
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +61 -38
- package/core/test-units.js +116 -42
- package/core/tracing.js +193 -635
- package/core/verify.js +37 -12
- package/languages/csharp.js +4 -4
- package/languages/go.js +1 -1
- package/languages/index.js +45 -1
- package/languages/java.js +4 -4
- package/languages/javascript.js +40 -6
- package/languages/python.js +49 -15
- package/languages/rust.js +126 -112
- package/languages/static-js.js +88 -0
- package/mcp/server.js +1 -1
- package/package.json +4 -3
package/core/tracing.js
CHANGED
|
@@ -20,12 +20,11 @@
|
|
|
20
20
|
'use strict';
|
|
21
21
|
|
|
22
22
|
const path = require('path');
|
|
23
|
-
const {
|
|
23
|
+
const { codeUnitCompare, lineInRanges } = require('./shared');
|
|
24
24
|
const { paramDefault } = require('./registry');
|
|
25
25
|
const { isTestFile } = require('./discovery');
|
|
26
|
-
const { getCachedCalls } = require('./callers');
|
|
27
26
|
const { getLanguageAdapter } = require('../languages');
|
|
28
|
-
const { createTestModel, addTestCaseRows, linkFiles, commandsFor } = require('./test-units');
|
|
27
|
+
const { createTestModel, addTestCaseRows, linkFiles, commandsFor, testSite, testSiteKey } = require('./test-units');
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Contract-mode caller expansion for the tree commands. Memoizes the full
|
|
@@ -34,10 +33,10 @@ const { createTestModel, addTestCaseRows, linkFiles, commandsFor } = require('./
|
|
|
34
33
|
* frontier display and the affected-tests possible closure need them all.
|
|
35
34
|
*/
|
|
36
35
|
function _contractCallers(index, funcDef, { includeMethods, callerCache, pin }) {
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
if (funcDef.syntheticScope) return { confirmed: [], unverified: [], raw: [] };
|
|
37
|
+
const nodeKey = declarationKey(funcDef);
|
|
38
|
+
const definitionKey = `${funcDef.name}:${nodeKey}`;
|
|
39
|
+
const cacheKey = `${pin ? 'pinned:' : ''}${definitionKey}`;
|
|
41
40
|
let res = callerCache.get(cacheKey);
|
|
42
41
|
if (!res) {
|
|
43
42
|
const raw = index.findCallers(funcDef.name, {
|
|
@@ -85,56 +84,40 @@ function _aggregateExcluded(treeAccount, raw) {
|
|
|
85
84
|
* to its symbol-table definition (pseudo-definition when absent). Shared by
|
|
86
85
|
* the confirmed trunk and the expand-unverified path.
|
|
87
86
|
*/
|
|
87
|
+
function declarationKey(def) {
|
|
88
|
+
return [def.file, def.name, def.type || '', def.className || '', def.namespace || '',
|
|
89
|
+
def.sourceSpan?.start ?? def.startLine, def.sourceSpan?.end ?? def.endLine].join(':');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function callerDefinition(index, site) {
|
|
93
|
+
const candidates = (index.symbols.get(site.callerName) || []).filter(def =>
|
|
94
|
+
def.file === site.callerFile && def.startLine === site.callerStartLine);
|
|
95
|
+
const { containsCall } = require('./callable-scope');
|
|
96
|
+
const exact = candidates.filter(def => containsCall(def, testSite(site)) === true)
|
|
97
|
+
.sort((a, b) => (a.sourceSpan.end - a.sourceSpan.start) - (b.sourceSpan.end - b.sourceSpan.start));
|
|
98
|
+
if (exact.length > 0) return exact[0];
|
|
99
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
88
102
|
function _resolveCallerEntries(index, callers, exclude) {
|
|
89
103
|
const uniqueCallers = new Map();
|
|
90
|
-
for (const
|
|
91
|
-
if (exclude.length > 0 && !index.matchesFilters(
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
relativePath: c.relativePath,
|
|
103
|
-
startLine: callerStartLine,
|
|
104
|
-
endLine: hasNamedCaller ? c.callerEndLine : c.line,
|
|
105
|
-
callSites: 1,
|
|
106
|
-
reason: c.reason,
|
|
107
|
-
syntheticScope: !hasNamedCaller,
|
|
108
|
-
});
|
|
109
|
-
} else {
|
|
110
|
-
uniqueCallers.get(callerKey).callSites++;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const callerEntries = [];
|
|
115
|
-
for (const [, caller] of uniqueCallers) {
|
|
116
|
-
const defs = index.symbols.get(caller.name);
|
|
117
|
-
let callerDef = defs?.find(d => d.file === caller.file && d.startLine === caller.startLine);
|
|
118
|
-
if (!callerDef) {
|
|
119
|
-
// Pseudo-definition for callers not in symbol table
|
|
120
|
-
callerDef = {
|
|
121
|
-
name: caller.name,
|
|
122
|
-
file: caller.file,
|
|
123
|
-
relativePath: caller.relativePath,
|
|
124
|
-
startLine: caller.startLine,
|
|
125
|
-
endLine: caller.endLine,
|
|
126
|
-
type: 'function'
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
if (caller.syntheticScope) callerDef.syntheticScope = true;
|
|
130
|
-
callerEntries.push({ def: callerDef, callSites: caller.callSites, reason: caller.reason });
|
|
104
|
+
for (const site of callers) {
|
|
105
|
+
if (exclude.length > 0 && !index.matchesFilters(site.relativePath, { exclude })) continue;
|
|
106
|
+
const named = callerDefinition(index, site);
|
|
107
|
+
const def = named || {
|
|
108
|
+
name: site.callerName || '(module/anonymous scope)',
|
|
109
|
+
file: site.callerFile || site.file, relativePath: site.relativePath,
|
|
110
|
+
startLine: site.callerStartLine || site.line, endLine: site.callerEndLine || site.line,
|
|
111
|
+
type: 'function', syntheticScope: true,
|
|
112
|
+
};
|
|
113
|
+
const key = named ? declarationKey(named) : `${declarationKey(def)}:${testSiteKey(site)}`;
|
|
114
|
+
if (uniqueCallers.has(key)) uniqueCallers.get(key).callSites++;
|
|
115
|
+
else uniqueCallers.set(key, { def, callSites: 1, reason: site.reason });
|
|
131
116
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
codeUnitCompare(a.def
|
|
136
|
-
);
|
|
137
|
-
return callerEntries;
|
|
117
|
+
return [...uniqueCallers.values()].sort((a, b) =>
|
|
118
|
+
codeUnitCompare(a.def.file, b.def.file) || a.def.startLine - b.def.startLine ||
|
|
119
|
+
(a.def.sourceSpan?.start || 0) - (b.def.sourceSpan?.start || 0) ||
|
|
120
|
+
codeUnitCompare(declarationKey(a.def), declarationKey(b.def)));
|
|
138
121
|
}
|
|
139
122
|
|
|
140
123
|
/**
|
|
@@ -176,7 +159,7 @@ function trace(index, name, options = {}) {
|
|
|
176
159
|
|
|
177
160
|
const buildTree = (funcDef, currentDepth, dir) => {
|
|
178
161
|
const funcName = funcDef.name;
|
|
179
|
-
const key =
|
|
162
|
+
const key = declarationKey(funcDef);
|
|
180
163
|
if (currentDepth > maxDepth) {
|
|
181
164
|
return null;
|
|
182
165
|
}
|
|
@@ -381,7 +364,7 @@ function blast(index, name, options = {}) {
|
|
|
381
364
|
};
|
|
382
365
|
|
|
383
366
|
const buildCallerTree = (funcDef, currentDepth, chainUnverified) => {
|
|
384
|
-
const key =
|
|
367
|
+
const key = declarationKey(funcDef);
|
|
385
368
|
if (currentDepth > maxDepth) return null;
|
|
386
369
|
if (visited.has(key)) {
|
|
387
370
|
return {
|
|
@@ -413,9 +396,12 @@ function blast(index, name, options = {}) {
|
|
|
413
396
|
...(chainUnverified && { chainUnverified: true }),
|
|
414
397
|
children: []
|
|
415
398
|
};
|
|
399
|
+
if (collect && !chainUnverified) collect.onNode?.(funcDef);
|
|
416
400
|
|
|
417
401
|
if (currentDepth < maxDepth) {
|
|
418
|
-
const { confirmed, unverified, raw } = _contractCallers(index, funcDef, {
|
|
402
|
+
const { confirmed, unverified, raw } = _contractCallers(index, funcDef, {
|
|
403
|
+
includeMethods, callerCache, pin: options._pinDefinitions,
|
|
404
|
+
});
|
|
419
405
|
treeAccount.nodesExpanded++;
|
|
420
406
|
_aggregateExcluded(treeAccount, raw);
|
|
421
407
|
if (currentDepth === 0) rootRaw = raw;
|
|
@@ -430,8 +416,7 @@ function blast(index, name, options = {}) {
|
|
|
430
416
|
if (currentDepth === 0) rootFiltered += before - callers.length;
|
|
431
417
|
}
|
|
432
418
|
const recursiveCallers = callers.filter(c =>
|
|
433
|
-
c
|
|
434
|
-
c.callerStartLine === funcDef.startLine);
|
|
419
|
+
declarationKey(callerDefinition(index, c) || {}) === declarationKey(funcDef));
|
|
435
420
|
if (recursiveCallers.length > 0) {
|
|
436
421
|
callers = callers.filter(c => !recursiveCallers.includes(c));
|
|
437
422
|
node.selfRecursive = true;
|
|
@@ -479,7 +464,7 @@ function blast(index, name, options = {}) {
|
|
|
479
464
|
treeAccount.truncatedChildren += node.truncatedChildren;
|
|
480
465
|
// Count truncated callers in summary
|
|
481
466
|
for (const { def: cDef } of callerEntries.slice(maxChildren)) {
|
|
482
|
-
const tKey =
|
|
467
|
+
const tKey = declarationKey(cDef);
|
|
483
468
|
if (!visited.has(tKey)) {
|
|
484
469
|
if (chainUnverified) {
|
|
485
470
|
possiblyAffectedSet.add(tKey);
|
|
@@ -505,7 +490,7 @@ function blast(index, name, options = {}) {
|
|
|
505
490
|
}
|
|
506
491
|
}
|
|
507
492
|
for (const { def: cDef } of unvEntries.slice(maxChildren)) {
|
|
508
|
-
const tKey =
|
|
493
|
+
const tKey = declarationKey(cDef);
|
|
509
494
|
if (!visited.has(tKey)) possiblyAffectedSet.add(tKey);
|
|
510
495
|
}
|
|
511
496
|
}
|
|
@@ -519,6 +504,14 @@ function blast(index, name, options = {}) {
|
|
|
519
504
|
|
|
520
505
|
const tree = buildCallerTree(def, 0, false);
|
|
521
506
|
|
|
507
|
+
if (options._testSitesOnly) {
|
|
508
|
+
// Internal check selection needs only the declarations observed
|
|
509
|
+
// by _collect. Caller tiering above still uses collectAccount;
|
|
510
|
+
// the root account/frontier below are public presentation, not
|
|
511
|
+
// evidence that can change the confirmed traversal.
|
|
512
|
+
return { root: name, file: def.relativePath, line: def.startLine, maxDepth };
|
|
513
|
+
}
|
|
514
|
+
|
|
522
515
|
// Root text-ground account (the context/impact contract at hop 1).
|
|
523
516
|
// Ground call-lines no candidate claimed are frontier entries too —
|
|
524
517
|
// counted in the account's unverified total, listed here.
|
|
@@ -635,8 +628,7 @@ function reverseTrace(index, name, options = {}) {
|
|
|
635
628
|
if (isExpansion) treeAccount.filteredEdges += before - conf.length - unv.length;
|
|
636
629
|
}
|
|
637
630
|
const recursive = conf.filter(c =>
|
|
638
|
-
c
|
|
639
|
-
c.callerStartLine === funcDef.startLine);
|
|
631
|
+
declarationKey(callerDefinition(index, c) || {}) === declarationKey(funcDef));
|
|
640
632
|
if (recursive.length > 0) {
|
|
641
633
|
conf = conf.filter(c => !recursive.includes(c));
|
|
642
634
|
if (isExpansion) treeAccount.recursiveEdges += recursive.length;
|
|
@@ -645,7 +637,7 @@ function reverseTrace(index, name, options = {}) {
|
|
|
645
637
|
};
|
|
646
638
|
|
|
647
639
|
const buildCallerTree = (funcDef, currentDepth, chainUnverified) => {
|
|
648
|
-
const key =
|
|
640
|
+
const key = declarationKey(funcDef);
|
|
649
641
|
if (currentDepth > maxDepth) return null;
|
|
650
642
|
if (visited.has(key)) {
|
|
651
643
|
return {
|
|
@@ -705,7 +697,7 @@ function reverseTrace(index, name, options = {}) {
|
|
|
705
697
|
node.truncatedChildren = callerEntries.length - maxChildren;
|
|
706
698
|
// Count entry points in truncated branches so summary is accurate
|
|
707
699
|
for (const { def: cDef } of callerEntries.slice(maxChildren)) {
|
|
708
|
-
const cKey =
|
|
700
|
+
const cKey = declarationKey(cDef);
|
|
709
701
|
if (!visited.has(cKey)) {
|
|
710
702
|
const tiers = nodeCallers(cDef, false);
|
|
711
703
|
if (tiers.confirmed.length === 0 && tiers.unverified.length === 0 &&
|
|
@@ -846,510 +838,151 @@ function affectedTests(index, name, options = {}) {
|
|
|
846
838
|
index._beginOp();
|
|
847
839
|
try {
|
|
848
840
|
const maxDepth = Math.max(0, options.depth ?? 3);
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const expandedNames = new Set();
|
|
857
|
-
const confirmedSitesByName = new Map(); // name → Map(relPath → Set(line))
|
|
841
|
+
// Traverse production callers only to the requested depth. Every
|
|
842
|
+
// declaration reached, including a boundary declaration, then has
|
|
843
|
+
// its immediate test sites adjudicated against its own identity.
|
|
844
|
+
// Inspecting those sites never extends the production closure.
|
|
845
|
+
const callerCache = options._callerCache || new Map();
|
|
846
|
+
const definitions = new Map();
|
|
847
|
+
const keyOf = declarationKey;
|
|
858
848
|
const blastResult = index.blast(name, {
|
|
859
|
-
depth: maxDepth,
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
line: options.line,
|
|
865
|
-
_callerCache: options._callerCache,
|
|
866
|
-
all: true,
|
|
867
|
-
exclude: options.exclude,
|
|
868
|
-
includeMethods: options.includeMethods,
|
|
869
|
-
_collect: {
|
|
870
|
-
onExpand: (funcDef) => expandedNames.add(funcDef.name),
|
|
871
|
-
onConfirmed: (funcDef, c) => {
|
|
872
|
-
let byFile = confirmedSitesByName.get(funcDef.name);
|
|
873
|
-
if (!byFile) { byFile = new Map(); confirmedSitesByName.set(funcDef.name, byFile); }
|
|
874
|
-
let lines = byFile.get(c.relativePath);
|
|
875
|
-
if (!lines) { lines = new Set(); byFile.set(c.relativePath, lines); }
|
|
876
|
-
lines.add(c.line);
|
|
877
|
-
},
|
|
878
|
-
},
|
|
849
|
+
depth: maxDepth, file: options.file, className: options.className,
|
|
850
|
+
line: options.line, _callerCache: callerCache, _pinDefinitions: true,
|
|
851
|
+
all: true, exclude: options.exclude, includeMethods: options.includeMethods,
|
|
852
|
+
_collect: { onNode: def => definitions.set(keyOf(def), def) },
|
|
853
|
+
_testSitesOnly: options._testSitesOnly,
|
|
879
854
|
});
|
|
880
855
|
if (!blastResult) return null;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
// Also record each name's CLASS identity from the tree — interior
|
|
884
|
-
// names were scanned bare (post-#239), so a test of a DIFFERENT
|
|
885
|
-
// class's same-named method got credited as coverage (fix #244:
|
|
886
|
-
// `asyncio.run(...)` credited as covering Mgr.run).
|
|
887
|
-
const affectedNames = new Set();
|
|
888
|
-
const confirmedKeys = new Set();
|
|
889
|
-
const nameToClasses = new Map(); // name → Set(className|null)
|
|
890
|
-
affectedNames.add(name);
|
|
891
|
-
const collectNames = (node) => {
|
|
892
|
-
if (!node) return;
|
|
893
|
-
affectedNames.add(node.name);
|
|
894
|
-
confirmedKeys.add(`${node.file}:${node.line}`);
|
|
895
|
-
const defs = index.symbols.get(node.name);
|
|
896
|
-
const d = defs?.find(x => x.relativePath === node.file && x.startLine === node.line);
|
|
897
|
-
let set = nameToClasses.get(node.name);
|
|
898
|
-
if (!set) { set = new Set(); nameToClasses.set(node.name, set); }
|
|
899
|
-
set.add(d?.className || null);
|
|
900
|
-
for (const child of node.children || []) collectNames(child);
|
|
901
|
-
};
|
|
902
|
-
collectNames(blastResult.tree);
|
|
903
|
-
|
|
904
|
-
// Step 2b: possible closure — BFS seeded by the frontier's enclosing
|
|
905
|
-
// functions, following both edge tiers, bounded by the same depth.
|
|
906
|
-
// Names reached only this way are possibly affected, never confirmed.
|
|
856
|
+
const affectedNames = new Set([...definitions.values()].map(def => def.name));
|
|
857
|
+
const possiblyNames = new Set();
|
|
907
858
|
const exclude = options.exclude;
|
|
908
859
|
const excludeArr = exclude ? (Array.isArray(exclude) ? exclude : [exclude]) : [];
|
|
909
860
|
const includeMethods = options.includeMethods ?? true;
|
|
910
|
-
const possiblyNames = new Set();
|
|
911
|
-
// The one model of what a test is (core/test-units.js); a caller
|
|
912
|
-
// that asks about several names hands in its own.
|
|
913
861
|
const model = options._testModel || createTestModel(index);
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
|
|
917
|
-
|
|
862
|
+
// The same multi-symbol operation reuses path classification. No
|
|
863
|
+
// source/configuration fact survives the outermost operation.
|
|
864
|
+
let testPaths = index._opMemos.get('affected-test-paths');
|
|
865
|
+
if (!testPaths) {
|
|
866
|
+
testPaths = new Map();
|
|
867
|
+
index._opMemos.set('affected-test-paths', testPaths);
|
|
868
|
+
}
|
|
869
|
+
const testInfo = filePath => {
|
|
870
|
+
const entry = index.files.get(filePath);
|
|
871
|
+
if (!entry) return null;
|
|
872
|
+
if (!testPaths.has(filePath)) {
|
|
873
|
+
testPaths.set(filePath, isTestFile(entry.relativePath, entry.language));
|
|
874
|
+
}
|
|
875
|
+
if (testPaths.get(filePath)) return { entry, ranges: null };
|
|
918
876
|
const ranges = model.testRangesOf(filePath);
|
|
919
|
-
return ranges.length
|
|
877
|
+
return ranges.length ? { entry, ranges } : null;
|
|
920
878
|
};
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
cDef = {
|
|
936
|
-
name: c.callerName,
|
|
937
|
-
file: c.callerFile,
|
|
938
|
-
relativePath: c.relativePath,
|
|
939
|
-
startLine: c.callerStartLine,
|
|
940
|
-
endLine: c.callerEndLine,
|
|
941
|
-
type: 'function'
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
queue.push({ def: cDef, depth });
|
|
945
|
-
};
|
|
946
|
-
for (const fe of blastResult.unverifiedFrontier || []) {
|
|
947
|
-
enqueueCaller(fe, fe.hop);
|
|
948
|
-
// A frontier caller that is ITSELF a test function landed in
|
|
949
|
-
// neither band (fix #244, the unittest setUp idiom):
|
|
950
|
-
// isProductionName filters it from possiblyAffected, and the
|
|
951
|
-
// test scan greps for calls OF it — tests are never called.
|
|
952
|
-
// Route its FILE into possiblyAffectedTests directly.
|
|
953
|
-
// Anonymous test callbacks (`it('...', () => { x.save() })`)
|
|
954
|
-
// have no enclosing NAMED caller at all — the site's file is
|
|
955
|
-
// still test evidence, so route by the site file whenever it
|
|
956
|
-
// is a test file (fix #246; the caller-file and site file are
|
|
957
|
-
// the same file by construction when both exist).
|
|
958
|
-
const siteFile = fe.callerFile || fe.file;
|
|
959
|
-
if (siteFile) {
|
|
960
|
-
const cfe = index.files.get(siteFile);
|
|
961
|
-
if (cfe) {
|
|
962
|
-
let isTestCaller = isTestFile(cfe.relativePath, cfe.language);
|
|
963
|
-
if (!isTestCaller && fe.callerName) {
|
|
964
|
-
const defs = index.symbols.get(fe.callerName);
|
|
965
|
-
const d = defs?.find(x => x.file === fe.callerFile && x.startLine === fe.callerStartLine);
|
|
966
|
-
const kindOf = getLanguageAdapter(cfe.language)?.getEntryPointKind;
|
|
967
|
-
if (d && kindOf && kindOf(d) === 'test') isTestCaller = true;
|
|
968
|
-
}
|
|
969
|
-
if (!isTestCaller && fe.line != null) {
|
|
970
|
-
// Inline #[cfg(test)] regions of production files
|
|
971
|
-
// are test code too — but only within the ranges.
|
|
972
|
-
const ranges = inlineTestRanges(cfe);
|
|
973
|
-
if (ranges.length > 0 && lineInRanges(fe.line, ranges)) isTestCaller = true;
|
|
974
|
-
}
|
|
975
|
-
if (!isTestCaller && fe.line != null) {
|
|
976
|
-
// A line the test model says a test executes.
|
|
977
|
-
const ranges = modelRanges(siteFile);
|
|
978
|
-
if (ranges && lineInRanges(fe.line, ranges)) isTestCaller = true;
|
|
979
|
-
}
|
|
980
|
-
if (isTestCaller &&
|
|
981
|
-
(excludeArr.length === 0 || index.matchesFilters(cfe.relativePath, { exclude: excludeArr }))) {
|
|
982
|
-
const affName = fe.atNode?.name || name;
|
|
983
|
-
let entry = frontierTestHits.get(siteFile);
|
|
984
|
-
if (!entry) {
|
|
985
|
-
entry = { rel: cfe.relativePath, byName: new Map() };
|
|
986
|
-
frontierTestHits.set(siteFile, entry);
|
|
987
|
-
}
|
|
988
|
-
let hitLines = entry.byName.get(affName);
|
|
989
|
-
if (!hitLines) { hitLines = new Set(); entry.byName.set(affName, hitLines); }
|
|
990
|
-
if (fe.line != null) hitLines.add(fe.line);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
}
|
|
879
|
+
const absoluteOf = new Map();
|
|
880
|
+
const confirmedRows = new Map();
|
|
881
|
+
const possibleRows = new Map();
|
|
882
|
+
const addSite = (site, functionName, confirmed) => {
|
|
883
|
+
const filePath = site.file || site.callerFile;
|
|
884
|
+
const info = testInfo(filePath);
|
|
885
|
+
if (!info || (info.ranges && !lineInRanges(site.line, info.ranges)) ||
|
|
886
|
+
(excludeArr.length && !index.matchesFilters(info.entry.relativePath, { exclude: excludeArr }))) return;
|
|
887
|
+
absoluteOf.set(info.entry.relativePath, filePath);
|
|
888
|
+
const rows = confirmed ? confirmedRows : possibleRows;
|
|
889
|
+
let row = rows.get(info.entry.relativePath);
|
|
890
|
+
if (!row) {
|
|
891
|
+
row = { file: info.entry.relativePath, linkedFunctions: [], matches: [] };
|
|
892
|
+
rows.set(row.file, row);
|
|
994
893
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
894
|
+
if (!row.linkedFunctions.includes(functionName)) row.linkedFunctions.push(functionName);
|
|
895
|
+
if (!row.matches.some(match => testSiteKey(match) === testSiteKey(site) && match.functionName === functionName)) {
|
|
896
|
+
row.matches.push({
|
|
897
|
+
...testSite(site),
|
|
898
|
+
content: (site.content || index.getLineContent(filePath, site.line) || '').trim(),
|
|
899
|
+
matchType: 'call', functionName,
|
|
900
|
+
...(!confirmed && { evidenceTier: 'unverified', reason: site.reason || 'unverified-chain' }),
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
const finishRows = rows => [...rows.values()].map(row => {
|
|
905
|
+
const filePath = absoluteOf.get(row.file);
|
|
906
|
+
const matches = [];
|
|
907
|
+
for (const functionName of row.linkedFunctions.sort(codeUnitCompare)) {
|
|
908
|
+
const own = row.matches.filter(match => match.functionName === functionName);
|
|
909
|
+
// Imports remain useful context once an actual test site
|
|
910
|
+
// links this file. They never create a link or select units.
|
|
911
|
+
const ranges = testInfo(filePath)?.ranges;
|
|
912
|
+
for (const usage of index._getCachedUsages(filePath, functionName) || []) {
|
|
913
|
+
if (usage.usageType !== 'import' ||
|
|
914
|
+
(ranges && !lineInRanges(usage.line, ranges)) ||
|
|
915
|
+
own.some(match => match.line === usage.line)) continue;
|
|
916
|
+
own.push({ line: usage.line,
|
|
917
|
+
content: (index.getLineContent(filePath, usage.line) || '').trim(),
|
|
918
|
+
matchType: 'import', functionName });
|
|
1008
919
|
}
|
|
920
|
+
addTestCaseRows(model, index, filePath, functionName, own, {
|
|
921
|
+
counts: match => match.matchType === 'call', titleNeedsMatch: true,
|
|
922
|
+
extra: { functionName },
|
|
923
|
+
});
|
|
924
|
+
matches.push(...own);
|
|
925
|
+
}
|
|
926
|
+
row.matches = matches.sort((a, b) => a.line - b.line ||
|
|
927
|
+
codeUnitCompare(a.functionName, b.functionName) || codeUnitCompare(a.matchType, b.matchType));
|
|
928
|
+
row.matchCount = row.matches.length;
|
|
929
|
+
return row;
|
|
930
|
+
});
|
|
931
|
+
const queue = [];
|
|
932
|
+
const enqueue = (site, depth) => {
|
|
933
|
+
if (!site.callerName || !site.callerFile || depth > maxDepth) return;
|
|
934
|
+
if (excludeArr.length && !index.matchesFilters(site.relativePath, { exclude: excludeArr })) return;
|
|
935
|
+
const def = callerDefinition(index, site);
|
|
936
|
+
// Anonymous/module sites are test evidence but not declarations
|
|
937
|
+
// whose spelling can seed another production traversal.
|
|
938
|
+
if (def) queue.push({ def, depth });
|
|
939
|
+
};
|
|
940
|
+
for (const def of definitions.values()) {
|
|
941
|
+
const { confirmed, unverified } = _contractCallers(index, def, { includeMethods, callerCache, pin: true });
|
|
942
|
+
for (const site of confirmed) addSite(site, def.name, true);
|
|
943
|
+
if (!options._testSitesOnly) {
|
|
944
|
+
for (const site of unverified) addSite(site, def.name, false);
|
|
1009
945
|
}
|
|
1010
|
-
//
|
|
1011
|
-
|
|
946
|
+
// Possible descendants are seeded by blast's bounded frontier
|
|
947
|
+
// below, never by the extra boundary site inspection.
|
|
1012
948
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
const scanNames = [...affectedNames, ...possiblyNames];
|
|
1021
|
-
const dispatchNamesCache = new Map(); // `${class}:${name}` → Set(classNames)
|
|
1022
|
-
// Files whose name is no test file's that hold a call of a scanned
|
|
1023
|
-
// name: a confirmed edge of the engine, or (a name whose callers
|
|
1024
|
-
// the engine did not search) a call by that name. Only these are
|
|
1025
|
-
// asked of the test model.
|
|
1026
|
-
const callSiteFiles = new Set();
|
|
1027
|
-
for (const byFile of confirmedSitesByName.values()) {
|
|
1028
|
-
for (const relativePath of byFile.keys()) callSiteFiles.add(relativePath);
|
|
949
|
+
if (options._testSitesOnly) {
|
|
950
|
+
// check unions the confirmed physical sites of every changed
|
|
951
|
+
// declaration, then links their test units once. Preserve the
|
|
952
|
+
// synthetic case-header sites used by that final link, but do
|
|
953
|
+
// not traverse the unused possible band or project per-root
|
|
954
|
+
// units, inherited rows and commands that check discards.
|
|
955
|
+
return { testFiles: finishRows(confirmedRows) };
|
|
1029
956
|
}
|
|
1030
|
-
for (const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const entry = index.files.get(calling);
|
|
1034
|
-
if (entry) callSiteFiles.add(entry.relativePath);
|
|
1035
|
-
}
|
|
957
|
+
for (const site of blastResult.unverifiedFrontier || []) {
|
|
958
|
+
addSite(site, site.atNode?.name || name, false);
|
|
959
|
+
enqueue(site, site.hop);
|
|
1036
960
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
testRanges = ranges;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
// A file's name does not decide what a test executes: the body
|
|
1052
|
-
// of a test a class inherits stands where its base class is,
|
|
1053
|
-
// and a runner takes tests from files of any name. Such a file
|
|
1054
|
-
// is test code within the lines the test model names.
|
|
1055
|
-
if (!isTest && callSiteFiles.has(fileEntry.relativePath)) {
|
|
1056
|
-
const ranges = modelRanges(filePath);
|
|
1057
|
-
if (ranges) {
|
|
1058
|
-
isTest = true;
|
|
1059
|
-
testRanges = ranges;
|
|
1060
|
-
}
|
|
961
|
+
const visited = new Set();
|
|
962
|
+
for (let next = 0; next < queue.length; next++) {
|
|
963
|
+
const { def, depth } = queue[next];
|
|
964
|
+
const key = keyOf(def);
|
|
965
|
+
if (visited.has(key) || definitions.has(key)) continue;
|
|
966
|
+
visited.add(key);
|
|
967
|
+
possiblyNames.add(def.name);
|
|
968
|
+
const { confirmed, unverified } = _contractCallers(index, def, { includeMethods, callerCache, pin: true });
|
|
969
|
+
for (const site of [...confirmed, ...unverified]) {
|
|
970
|
+
addSite(site, def.name, false);
|
|
971
|
+
if (depth < maxDepth) enqueue(site, depth + 1);
|
|
1061
972
|
}
|
|
1062
|
-
if (!isTest) continue;
|
|
1063
|
-
if (excludeArr.length > 0 && !index.matchesFilters(fileEntry.relativePath, { exclude: excludeArr })) continue;
|
|
1064
|
-
try {
|
|
1065
|
-
const content = index._readFile(filePath);
|
|
1066
|
-
const fileMatches = new Map();
|
|
1067
|
-
|
|
1068
|
-
for (const funcName of scanNames) {
|
|
1069
|
-
// Confirmed trunk edges of this name in THIS file — the
|
|
1070
|
-
// engine's answer for the sites the text scan is about
|
|
1071
|
-
// to re-derive (fix #246).
|
|
1072
|
-
const engineFileSites = confirmedSitesByName.get(funcName)?.get(fileEntry.relativePath) || null;
|
|
1073
|
-
// Whether the engine searched this name's callers at all
|
|
1074
|
-
// (depth-limit nodes were never adjudicated — the text
|
|
1075
|
-
// scan remains their best-effort fallback).
|
|
1076
|
-
const engineAdjudicated = expandedNames.has(funcName);
|
|
1077
|
-
|
|
1078
|
-
// The engine searched this name's callers and confirmed
|
|
1079
|
-
// none in this file: a call here is not credited (below),
|
|
1080
|
-
// and an import or a bare reference links nothing by
|
|
1081
|
-
// itself, so there is nothing a scan could add.
|
|
1082
|
-
if (engineAdjudicated && !engineFileSites) continue;
|
|
1083
|
-
|
|
1084
|
-
// Fast pre-check
|
|
1085
|
-
if (!content.includes(funcName) && !engineFileSites) continue;
|
|
1086
|
-
|
|
1087
|
-
// AST-based usage detection
|
|
1088
|
-
const astUsages = index._getCachedUsages(filePath, funcName) || [];
|
|
1089
|
-
if (astUsages.length === 0 && !engineFileSites) continue;
|
|
1090
|
-
|
|
1091
|
-
// className scoping: the user's pin applies to the ROOT
|
|
1092
|
-
// symbol's own name only (fix #239, G3-go-measured) — a
|
|
1093
|
-
// bare call to a standalone wrapper (SaveAll) can never
|
|
1094
|
-
// carry the root's class receiver. INTERIOR names with a
|
|
1095
|
-
// single unambiguous class identity in the blast tree
|
|
1096
|
-
// scope to it (fix #244): a test of a different class's
|
|
1097
|
-
// same-named method is not coverage of THIS closure.
|
|
1098
|
-
// Standalone/mixed-identity names keep the bare scan.
|
|
1099
|
-
let scopeToClass = null;
|
|
1100
|
-
if (funcName === name) {
|
|
1101
|
-
scopeToClass = className || null;
|
|
1102
|
-
} else {
|
|
1103
|
-
const classes = nameToClasses.get(funcName);
|
|
1104
|
-
if (classes && classes.size === 1) {
|
|
1105
|
-
const only = classes.values().next().value;
|
|
1106
|
-
if (only) scopeToClass = only;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// A test file that DEFINES funcName itself owns bare-name
|
|
1111
|
-
// calls of it (#215/#222(3) scope physics — fix #244: a
|
|
1112
|
-
// private same-name helper let the coverage band claim
|
|
1113
|
-
// 100% while the same payload's account excluded the
|
|
1114
|
-
// site as other-definition). Exception: when the local
|
|
1115
|
-
// def IS part of the blast closure (Rust inline-test
|
|
1116
|
-
// files define the symbol AND test it), bare calls bind
|
|
1117
|
-
// the affected symbol — genuine coverage.
|
|
1118
|
-
const localDefs = (fileEntry.symbols || []).filter(s => s.name === funcName);
|
|
1119
|
-
const localShadow = localDefs.length > 0 &&
|
|
1120
|
-
localDefs.every(s => !confirmedKeys.has(`${fileEntry.relativePath}:${s.startLine}`)) &&
|
|
1121
|
-
!(fileEntry.importBindings || []).some(b => b.name === funcName);
|
|
1122
|
-
|
|
1123
|
-
// Build instance type map for className scoping (if applicable).
|
|
1124
|
-
// Scoping accepts the class plus its non-overriding
|
|
1125
|
-
// descendants — instances of a subclass that doesn't
|
|
1126
|
-
// override funcName dispatch to the target (fix #246).
|
|
1127
|
-
let instanceTypeMap = null;
|
|
1128
|
-
let dispatchNames = null;
|
|
1129
|
-
if (scopeToClass) {
|
|
1130
|
-
const dnKey = `${scopeToClass}:${funcName}`;
|
|
1131
|
-
dispatchNames = dispatchNamesCache.get(dnKey);
|
|
1132
|
-
if (!dispatchNames) {
|
|
1133
|
-
dispatchNames = classDispatchNames(index, scopeToClass, funcName);
|
|
1134
|
-
dispatchNamesCache.set(dnKey, dispatchNames);
|
|
1135
|
-
}
|
|
1136
|
-
instanceTypeMap = new Map();
|
|
1137
|
-
for (const dn of dispatchNames) {
|
|
1138
|
-
for (const [recv, cls] of _buildInstanceTypeMapForTracing(index, filePath, content, dn)) {
|
|
1139
|
-
if (!instanceTypeMap.has(recv)) instanceTypeMap.set(recv, cls);
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
const seenLines = new Set();
|
|
1145
|
-
for (const usage of astUsages) {
|
|
1146
|
-
if (usage.usageType === 'definition') continue;
|
|
1147
|
-
// Inline-test-promoted file: only lines inside the
|
|
1148
|
-
// test ranges are test code (fix #244).
|
|
1149
|
-
if (testRanges && !lineInRanges(usage.line, testRanges)) continue;
|
|
1150
|
-
// The engine confirmed this exact site as a caller
|
|
1151
|
-
// edge — its receiver physics subsume every text
|
|
1152
|
-
// heuristic below (fix #246: exact-class scoping
|
|
1153
|
-
// rejected a non-overriding subclass receiver the
|
|
1154
|
-
// engine had confirmed).
|
|
1155
|
-
const engineConfirmedHere = engineFileSites ? engineFileSites.has(usage.line) : false;
|
|
1156
|
-
// Local same-name shadow: bare calls bind the test
|
|
1157
|
-
// file's OWN definition, never the affected symbol.
|
|
1158
|
-
if (localShadow && !engineConfirmedHere && !usage.receiver && usage.usageType !== 'import') continue;
|
|
1159
|
-
const lineKey = `${usage.line}:${usage.usageType}`;
|
|
1160
|
-
if (seenLines.has(lineKey)) continue;
|
|
1161
|
-
seenLines.add(lineKey);
|
|
1162
|
-
|
|
1163
|
-
let matchType;
|
|
1164
|
-
if (usage.usageType === 'import') {
|
|
1165
|
-
matchType = 'import';
|
|
1166
|
-
} else if (usage.usageType === 'call') {
|
|
1167
|
-
matchType = 'call';
|
|
1168
|
-
} else {
|
|
1169
|
-
matchType = 'reference';
|
|
1170
|
-
}
|
|
1171
|
-
// A confirmed usage-style edge (callback reference,
|
|
1172
|
-
// method value — fix #221) IS coverage: the test
|
|
1173
|
-
// executes the symbol through the reference.
|
|
1174
|
-
if (engineConfirmedHere && matchType === 'reference') matchType = 'call';
|
|
1175
|
-
|
|
1176
|
-
if (matchType === 'call' && !engineConfirmedHere && engineAdjudicated) {
|
|
1177
|
-
// The engine searched this name's callers and did
|
|
1178
|
-
// NOT confirm this site — the same payload's
|
|
1179
|
-
// account holds it excluded-with-reason or
|
|
1180
|
-
// unverified. Crediting it as confirmed coverage
|
|
1181
|
-
// would contradict the account (fix #246);
|
|
1182
|
-
// unverified sites surface in
|
|
1183
|
-
// possiblyAffectedTests via the frontier.
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// className scoping for calls (text physics — only
|
|
1188
|
-
// depth-limit names reach this un-adjudicated)
|
|
1189
|
-
if (scopeToClass && matchType === 'call' && !engineConfirmedHere) {
|
|
1190
|
-
if (!_receiverMatchesClassTracing(usage, dispatchNames, instanceTypeMap,
|
|
1191
|
-
index.getLineContent(filePath, usage.line), funcName)) continue;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// className scoping for references: require class-associated receiver
|
|
1195
|
-
if (scopeToClass && matchType === 'reference') {
|
|
1196
|
-
if (!usage.receiver) continue;
|
|
1197
|
-
if (!dispatchNames.has(usage.receiver) &&
|
|
1198
|
-
!(instanceTypeMap && dispatchNames.has(instanceTypeMap.get(usage.receiver)))) {
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
const lineContent = index.getLineContent(filePath, usage.line);
|
|
1204
|
-
if (!fileMatches.has(funcName)) fileMatches.set(funcName, []);
|
|
1205
|
-
fileMatches.get(funcName).push({
|
|
1206
|
-
line: usage.line, content: lineContent.trim(),
|
|
1207
|
-
matchType, functionName: funcName
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
// Confirmed engine sites the text scan could not see —
|
|
1212
|
-
// renamed destructure aliases (`{ save: persist }`),
|
|
1213
|
-
// beyondText lines — are coverage too (fix #246). The
|
|
1214
|
-
// dedup map below collapses any line the loop already
|
|
1215
|
-
// matched (call outranks reference).
|
|
1216
|
-
if (engineFileSites) {
|
|
1217
|
-
for (const ln of [...engineFileSites].sort((a, b) => a - b)) {
|
|
1218
|
-
if (testRanges && !lineInRanges(ln, testRanges)) continue;
|
|
1219
|
-
const lineKey = `${ln}:call`;
|
|
1220
|
-
if (seenLines.has(lineKey)) continue;
|
|
1221
|
-
seenLines.add(lineKey);
|
|
1222
|
-
if (!fileMatches.has(funcName)) fileMatches.set(funcName, []);
|
|
1223
|
-
fileMatches.get(funcName).push({
|
|
1224
|
-
line: ln,
|
|
1225
|
-
content: (index.getLineContent(filePath, ln) || '').trim(),
|
|
1226
|
-
matchType: 'call', functionName: funcName
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// The tests that hold a call, and the titled ones that
|
|
1232
|
-
// name the function around one (core/test-units.js).
|
|
1233
|
-
if (fileMatches.has(funcName)) {
|
|
1234
|
-
addTestCaseRows(model, index, filePath, funcName, fileMatches.get(funcName), {
|
|
1235
|
-
counts: match => match.matchType === 'call',
|
|
1236
|
-
titleNeedsMatch: true,
|
|
1237
|
-
extra: { functionName: funcName },
|
|
1238
|
-
});
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
if (fileMatches.size > 0) absoluteOf.set(fileEntry.relativePath, filePath);
|
|
1242
|
-
|
|
1243
|
-
if (fileMatches.size > 0) {
|
|
1244
|
-
const linkedFunctions = [...fileMatches.keys()];
|
|
1245
|
-
const allMatches = [];
|
|
1246
|
-
for (const matches of fileMatches.values()) allMatches.push(...matches);
|
|
1247
|
-
// Deduplicate same line+function (test-case line might overlap with call line)
|
|
1248
|
-
const dedupMap = new Map();
|
|
1249
|
-
for (const m of allMatches) {
|
|
1250
|
-
const key = `${m.line}:${m.functionName}`;
|
|
1251
|
-
const existing = dedupMap.get(key);
|
|
1252
|
-
if (!existing || _matchPriority(m.matchType) > _matchPriority(existing.matchType)) {
|
|
1253
|
-
dedupMap.set(key, m);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
const deduped = [...dedupMap.values()].sort((a, b) => a.line - b.line);
|
|
1257
|
-
|
|
1258
|
-
// Only count functions with call or test-case matches as
|
|
1259
|
-
// statically linked. Import-only or reference-only records
|
|
1260
|
-
// are useful evidence, but are not an exercising path.
|
|
1261
|
-
const realLinkedAll = linkedFunctions.filter(fn => {
|
|
1262
|
-
const fnMatches = deduped.filter(m => m.functionName === fn);
|
|
1263
|
-
return fnMatches.some(m => m.matchType === 'call' || m.matchType === 'test-case');
|
|
1264
|
-
});
|
|
1265
|
-
const staticallyLinkedFunctions = realLinkedAll.filter(fn => affectedNames.has(fn));
|
|
1266
|
-
const possiblyLinkedFunctions = realLinkedAll.filter(fn => possiblyNames.has(fn));
|
|
1267
|
-
|
|
1268
|
-
if (staticallyLinkedFunctions.length > 0) {
|
|
1269
|
-
// Confirmed band: matches for confirmed-linked names
|
|
1270
|
-
const realMatches = deduped.filter(m =>
|
|
1271
|
-
affectedNames.has(m.functionName) &&
|
|
1272
|
-
(m.matchType === 'call' || m.matchType === 'test-case' ||
|
|
1273
|
-
staticallyLinkedFunctions.includes(m.functionName))
|
|
1274
|
-
);
|
|
1275
|
-
results.push({
|
|
1276
|
-
file: fileEntry.relativePath,
|
|
1277
|
-
linkedFunctions: staticallyLinkedFunctions,
|
|
1278
|
-
...(possiblyLinkedFunctions.length > 0 && { possiblyLinkedFunctions }),
|
|
1279
|
-
matchCount: realMatches.length,
|
|
1280
|
-
matches: realMatches
|
|
1281
|
-
});
|
|
1282
|
-
} else if (possiblyLinkedFunctions.length > 0) {
|
|
1283
|
-
// Possible band: file reaches the change only through
|
|
1284
|
-
// unverified chains.
|
|
1285
|
-
const possibleMatches = deduped.filter(m =>
|
|
1286
|
-
possiblyNames.has(m.functionName) &&
|
|
1287
|
-
(m.matchType === 'call' || m.matchType === 'test-case' ||
|
|
1288
|
-
possiblyLinkedFunctions.includes(m.functionName))
|
|
1289
|
-
);
|
|
1290
|
-
possibleResults.push({
|
|
1291
|
-
file: fileEntry.relativePath,
|
|
1292
|
-
linkedFunctions: possiblyLinkedFunctions,
|
|
1293
|
-
matchCount: possibleMatches.length,
|
|
1294
|
-
matches: possibleMatches
|
|
1295
|
-
});
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
} catch (e) { /* skip unreadable */ }
|
|
1299
973
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
for (const [absFile, entry] of frontierTestHits) {
|
|
1309
|
-
const inResults = results.find(r => r.file === entry.rel);
|
|
1310
|
-
const inPossible = possibleResults.find(r => r.file === entry.rel);
|
|
1311
|
-
if (inResults || inPossible) {
|
|
1312
|
-
const existing = inResults || inPossible;
|
|
1313
|
-
const have = new Set([
|
|
1314
|
-
...existing.linkedFunctions,
|
|
1315
|
-
...(existing.possiblyLinkedFunctions || []),
|
|
1316
|
-
]);
|
|
1317
|
-
const extra = [...entry.byName.keys()].filter(n => !have.has(n)).sort(codeUnitCompare);
|
|
1318
|
-
if (extra.length > 0) {
|
|
1319
|
-
if (inResults) {
|
|
1320
|
-
existing.possiblyLinkedFunctions = [
|
|
1321
|
-
...(existing.possiblyLinkedFunctions || []),
|
|
1322
|
-
...extra,
|
|
1323
|
-
].sort(codeUnitCompare);
|
|
1324
|
-
} else {
|
|
1325
|
-
existing.linkedFunctions = [...existing.linkedFunctions, ...extra].sort(codeUnitCompare);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
|
-
const linked = [...entry.byName.keys()].sort(codeUnitCompare);
|
|
1331
|
-
const matches = [];
|
|
1332
|
-
for (const fn of linked) {
|
|
1333
|
-
for (const ln of [...entry.byName.get(fn)].sort((a, b) => a - b)) {
|
|
1334
|
-
matches.push({
|
|
1335
|
-
line: ln,
|
|
1336
|
-
content: (index.getLineContent(absFile, ln) || '').trim(),
|
|
1337
|
-
matchType: 'call',
|
|
1338
|
-
functionName: fn,
|
|
1339
|
-
});
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
possibleResults.push({
|
|
1343
|
-
file: entry.rel,
|
|
1344
|
-
linkedFunctions: linked,
|
|
1345
|
-
matchCount: matches.length,
|
|
1346
|
-
matches,
|
|
1347
|
-
});
|
|
974
|
+
for (const n of affectedNames) possiblyNames.delete(n);
|
|
975
|
+
const results = finishRows(confirmedRows);
|
|
976
|
+
// Keep uncertainty even when the same file also has a confirmed
|
|
977
|
+
// link: its possible sites may belong to other test units.
|
|
978
|
+
const possibleResults = finishRows(possibleRows);
|
|
979
|
+
for (const row of results) {
|
|
980
|
+
const possible = possibleRows.get(row.file);
|
|
981
|
+
if (possible) row.possiblyLinkedFunctions = [...possible.linkedFunctions];
|
|
1348
982
|
}
|
|
1349
983
|
|
|
1350
984
|
// What each file is to its runner, which tests hold the links, and
|
|
1351
985
|
// how many tests that is.
|
|
1352
|
-
for (const [absFile, entry] of frontierTestHits) absoluteOf.set(entry.rel, absFile);
|
|
1353
986
|
const confirmedTests = new Set();
|
|
1354
987
|
const conditionalTests = new Set();
|
|
1355
988
|
const describeBand = (rows, confirmed) => {
|
|
@@ -1358,8 +991,7 @@ function affectedTests(index, name, options = {}) {
|
|
|
1358
991
|
const absFile = absoluteOf.get(row.file);
|
|
1359
992
|
if (!absFile) continue;
|
|
1360
993
|
linesByFile.set(absFile, row.matches
|
|
1361
|
-
.filter(match => match.matchType === 'call' || match.matchType === 'test-case')
|
|
1362
|
-
.map(match => match.line));
|
|
994
|
+
.filter(match => match.matchType === 'call' || match.matchType === 'test-case'));
|
|
1363
995
|
}
|
|
1364
996
|
const byFile = new Map(rows.map(row => [row.file, row]));
|
|
1365
997
|
for (const [absFile, linked] of linkFiles(model, index, linesByFile)) {
|
|
@@ -1416,26 +1048,18 @@ function affectedTests(index, name, options = {}) {
|
|
|
1416
1048
|
// Compute static-linkage stats.
|
|
1417
1049
|
// Filter out test function names from affectedNames — they are callers,
|
|
1418
1050
|
// not production symbols that need test coverage.
|
|
1419
|
-
const isProductionName = (n) => {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
for (const [filePath, fe] of index.files) {
|
|
1425
|
-
if (isTestFile(fe.relativePath, fe.language)) continue;
|
|
1426
|
-
const langModule = getLanguageAdapter(fe.language);
|
|
1427
|
-
const kindOf = langModule?.getEntryPointKind;
|
|
1428
|
-
// A declaration the test model found test code in (asked
|
|
1429
|
-
// above, where the file held a call): a test, not a
|
|
1430
|
-
// production symbol.
|
|
1431
|
-
const ranges = model.ranges.get(filePath) || [];
|
|
1432
|
-
if (fe.symbols?.some(s => s.name === n && (!kindOf || kindOf(s) !== 'test') &&
|
|
1433
|
-
!(ranges.length > 0 && lineInRanges(s.startLine, ranges)))) {
|
|
1434
|
-
return true;
|
|
1435
|
-
}
|
|
1051
|
+
const isProductionName = n => (index.symbols.get(n) || []).some(symbol => {
|
|
1052
|
+
const fe = index.files.get(symbol.file);
|
|
1053
|
+
if (!fe) return false;
|
|
1054
|
+
if (!testPaths.has(symbol.file)) {
|
|
1055
|
+
testPaths.set(symbol.file, isTestFile(fe.relativePath, fe.language));
|
|
1436
1056
|
}
|
|
1437
|
-
return false;
|
|
1438
|
-
|
|
1057
|
+
if (testPaths.get(symbol.file)) return false;
|
|
1058
|
+
const kindOf = getLanguageAdapter(fe.language)?.getEntryPointKind;
|
|
1059
|
+
if (kindOf && kindOf(symbol) === 'test') return false;
|
|
1060
|
+
const ranges = model.ranges.get(symbol.file) || [];
|
|
1061
|
+
return !lineInRanges(symbol.startLine, ranges);
|
|
1062
|
+
});
|
|
1439
1063
|
const productionNames = new Set();
|
|
1440
1064
|
for (const n of affectedNames) {
|
|
1441
1065
|
if (isProductionName(n)) productionNames.add(n);
|
|
@@ -1489,70 +1113,4 @@ function affectedTests(index, name, options = {}) {
|
|
|
1489
1113
|
} finally { index._endOp(); }
|
|
1490
1114
|
}
|
|
1491
1115
|
|
|
1492
|
-
/**
|
|
1493
|
-
* Build instance type map for className scoping in affectedTests.
|
|
1494
|
-
* Same logic as _buildInstanceTypeMap in search.js.
|
|
1495
|
-
*/
|
|
1496
|
-
function _buildInstanceTypeMapForTracing(index, filePath, content, targetClassName) {
|
|
1497
|
-
const typeMap = new Map();
|
|
1498
|
-
const calls = getCachedCalls(index, filePath);
|
|
1499
|
-
if (calls) {
|
|
1500
|
-
for (const call of calls) {
|
|
1501
|
-
if (call.isMethod && call.receiver && call.receiverType === targetClassName) {
|
|
1502
|
-
typeMap.set(call.receiver, targetClassName);
|
|
1503
|
-
}
|
|
1504
|
-
if (call.name === targetClassName && !call.isMethod) {
|
|
1505
|
-
const lineContent = index.getLineContent(filePath, call.line);
|
|
1506
|
-
const assignMatch = lineContent.match(/(?:const|let|var|)\s*(\w+)\s*:?=\s/);
|
|
1507
|
-
if (assignMatch) typeMap.set(assignMatch[1], targetClassName);
|
|
1508
|
-
}
|
|
1509
|
-
if (call.isMethod && call.receiver === targetClassName) {
|
|
1510
|
-
const lineContent = index.getLineContent(filePath, call.line);
|
|
1511
|
-
const assignMatch = lineContent.match(/(?:const|let|var|)\s*(\w+)\s*:?=\s/);
|
|
1512
|
-
if (assignMatch) typeMap.set(assignMatch[1], targetClassName);
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
const classUsages = index._getCachedUsages(filePath, targetClassName);
|
|
1517
|
-
if (classUsages) {
|
|
1518
|
-
for (const u of classUsages) {
|
|
1519
|
-
if (u.usageType === 'import' || u.usageType === 'definition') continue;
|
|
1520
|
-
const lineContent = index.getLineContent(filePath, u.line);
|
|
1521
|
-
const assignMatch = lineContent.match(/(?:const|let|var|)\s*(\w+)\s*:?=\s/);
|
|
1522
|
-
if (assignMatch && assignMatch[1] !== targetClassName) {
|
|
1523
|
-
typeMap.set(assignMatch[1], targetClassName);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
return typeMap;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
/**
|
|
1531
|
-
* Check if a usage's receiver matches any of the target dispatch class names
|
|
1532
|
-
* (the class + its non-overriding descendants — fix #246) for affectedTests.
|
|
1533
|
-
* Same logic as _receiverMatchesClass in search.js.
|
|
1534
|
-
*/
|
|
1535
|
-
function _receiverMatchesClassTracing(usage, dispatchNames, instanceTypeMap, lineContent, searchTerm) {
|
|
1536
|
-
if (usage.receiver && dispatchNames.has(usage.receiver)) return true;
|
|
1537
|
-
if (usage.receiver && instanceTypeMap && dispatchNames.has(instanceTypeMap.get(usage.receiver))) return true;
|
|
1538
|
-
if (usage.receiver) return false;
|
|
1539
|
-
if (lineContent && searchTerm) {
|
|
1540
|
-
for (const className of dispatchNames) {
|
|
1541
|
-
const pat = new RegExp(
|
|
1542
|
-
'\\b' + escapeRegExp(className) + '\\s*(?:(?:\\([^)]*\\)|\\{[^}]*\\})\\s*\\.\\s*' +
|
|
1543
|
-
escapeRegExp(searchTerm) + '\\s*\\(|' +
|
|
1544
|
-
'new\\s+' + escapeRegExp(className) + '\\s*\\([^)]*\\)\\s*\\.\\s*' +
|
|
1545
|
-
escapeRegExp(searchTerm) + '\\s*\\()'
|
|
1546
|
-
);
|
|
1547
|
-
if (pat.test(lineContent)) return true;
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
return false;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
function _matchPriority(matchType) {
|
|
1554
|
-
const p = { 'test-case': 5, 'call': 4, 'import': 3, 'string-ref': 2, 'reference': 1 };
|
|
1555
|
-
return p[matchType] || 0;
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
1116
|
module.exports = { trace, blast, reverseTrace, affectedTests };
|