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/core/tracing.js CHANGED
@@ -20,12 +20,11 @@
20
20
  'use strict';
21
21
 
22
22
  const path = require('path');
23
- const { escapeRegExp, codeUnitCompare, inlineTestRanges, lineInRanges, classDispatchNames } = require('./shared');
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
- const nodeKey = `${funcDef.file}:${funcDef.startLine}`;
38
- const cacheKey = funcDef.bindingId
39
- ? `${funcDef.name}:${funcDef.bindingId}`
40
- : `${funcDef.name}:${nodeKey}`;
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 c of callers) {
91
- if (exclude.length > 0 && !index.matchesFilters(c.relativePath, { exclude })) continue;
92
- const hasNamedCaller = !!c.callerName;
93
- const callerName = c.callerName || '(module/anonymous scope)';
94
- const callerStartLine = hasNamedCaller ? c.callerStartLine : c.line;
95
- const callerKey = hasNamedCaller && c.callerStartLine
96
- ? `${c.callerFile}:${c.callerStartLine}`
97
- : `${c.callerFile}:${callerName}:${callerStartLine || c.line || 0}`;
98
- if (!uniqueCallers.has(callerKey)) {
99
- uniqueCallers.set(callerKey, {
100
- name: callerName,
101
- file: c.callerFile,
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
- // Stable sort by file + line
134
- callerEntries.sort((a, b) =>
135
- codeUnitCompare(a.def.file, b.def.file) || a.def.startLine - b.def.startLine
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 = `${funcDef.file}:${funcDef.startLine}`;
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 = `${funcDef.file}:${funcDef.startLine}`;
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, { includeMethods, callerCache });
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.callerFile === funcDef.file &&
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 = `${cDef.file}:${cDef.startLine}`;
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 = `${cDef.file}:${cDef.startLine}`;
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.callerFile === funcDef.file &&
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 = `${funcDef.file}:${funcDef.startLine}`;
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 = `${cDef.file}:${cDef.startLine}`;
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
- // Step 1: confirmed closure via blast (contract mode, no truncation).
850
- // The collector records, per trunk name, (a) whether the engine
851
- // searched its callers and (b) where the confirmed edges sit — the
852
- // coverage bands below must agree with that answer instead of
853
- // re-deciding it with a text scan (fix #246: the scan credited call
854
- // sites the same payload's account excluded, and missed confirmed
855
- // edges text can't see — renamed destructure aliases, callback refs).
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
- file: options.file,
861
- className: options.className,
862
- // A pin on the declaration's line: one of several same-name
863
- // definitions in a file.
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
- // Step 2: Collect confirmed-affected function names (and node keys).
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
- // Code a test executes in a file whose name is no test file's: the
915
- // lines the model says so (a test of its own, the body of a test a
916
- // class of another file inherits), or null.
917
- const modelRanges = filePath => {
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 > 0 ? ranges : null;
877
+ return ranges.length ? { entry, ranges } : null;
920
878
  };
921
- // Test FILES containing an unverified call site of an affected name
922
- // (relativePath → funcName → Set(lines)) — merged into
923
- // possiblyAffectedTests after the scan (fix #244).
924
- const frontierTestHits = new Map();
925
- {
926
- const callerCache = options._callerCache || new Map();
927
- const possibleVisited = new Set();
928
- const queue = [];
929
- const enqueueCaller = (c, depth) => {
930
- if (!c.callerName || !c.callerFile) return;
931
- if (excludeArr.length > 0 && !index.matchesFilters(c.relativePath, { exclude: excludeArr })) return;
932
- const defs = index.symbols.get(c.callerName);
933
- let cDef = defs?.find(d => d.file === c.callerFile && d.startLine === c.callerStartLine);
934
- if (!cDef) {
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
- while (queue.length > 0) {
996
- const { def: d, depth } = queue.shift();
997
- if (depth > maxDepth) continue;
998
- const k = `${d.relativePath || path.relative(index.root, d.file)}:${d.startLine}`;
999
- if (possibleVisited.has(k)) continue;
1000
- possibleVisited.add(k);
1001
- if (!confirmedKeys.has(k) && !affectedNames.has(d.name)) {
1002
- possiblyNames.add(d.name);
1003
- }
1004
- if (depth < maxDepth) {
1005
- const { confirmed, unverified } = _contractCallers(index, d, { includeMethods, callerCache });
1006
- for (const c of confirmed) enqueueCaller(c, depth + 1);
1007
- for (const c of unverified) enqueueCaller(c, depth + 1);
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
- // A name in both bands is confirmed — the possible band only adds.
1011
- for (const n of affectedNames) possiblyNames.delete(n);
946
+ // Possible descendants are seeded by blast's bounded frontier
947
+ // below, never by the extra boundary site inspection.
1012
948
  }
1013
-
1014
- // Step 3: Scan test files for all affected names using AST
1015
- // Only count call and test-case matches as real coverage — not imports or bare references.
1016
- const className = options.className || null;
1017
- const absoluteOf = new Map(); // relative test path -> absolute
1018
- const results = [];
1019
- const possibleResults = [];
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 funcName of scanNames) {
1031
- if (expandedNames.has(funcName)) continue;
1032
- for (const calling of index.getCalleeFiles(funcName) || []) {
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
- for (const [filePath, fileEntry] of index.files) {
1038
- let isTest = isTestFile(fileEntry.relativePath, fileEntry.language);
1039
- // Rust inline #[cfg(test)] modules: source files with #[test]-marked symbols
1040
- // or symbols inside a #[cfg(test)] mod block (BUG-CY). Such files
1041
- // are test code ONLY within the inline test ranges — production
1042
- // lines were counted as test matches (fix #244, false coverage).
1043
- let testRanges = null;
1044
- if (!isTest && fileEntry.language === 'rust') {
1045
- const ranges = inlineTestRanges(fileEntry);
1046
- if (ranges.length > 0) {
1047
- isTest = true;
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
- // Merge frontier test hits: a test file whose own test function IS
1302
- // the unverified caller never surfaces via the scan (the scan greps
1303
- // for calls of scanNames; the test's name is not among them) — add
1304
- // it to the possible band with its unverified call sites (fix #244).
1305
- // A file already present for OTHER names still gets this name listed
1306
- // as possibly covered (fix #246 — the file-level dedup silently
1307
- // dropped per-name possible coverage).
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
- // Check if this name is only found in test files. Inline test
1421
- // functions (#[test] fns in Rust's #[cfg(test)] mods, Go Test*)
1422
- // live in production-path FILES but are tests themselves — the
1423
- // language's getEntryPointKind says so; they need no coverage.
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 };