ucn 5.5.3 → 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 +37 -12
- package/.claude/skills/ucn/references/commands.md +18 -14
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +485 -0
- package/README.md +66 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +137 -324
- package/core/account.js +44 -87
- package/core/analysis.js +274 -108
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +14 -0
- package/core/cache.js +103 -16
- package/core/call-fit.js +130 -0
- package/core/callers.js +353 -71
- package/core/capture.js +363 -0
- package/core/change-kinds.js +1107 -0
- package/core/check.js +1150 -139
- package/core/command-contracts.js +8 -7
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +455 -106
- package/core/deletion.js +421 -0
- package/core/discovery.js +104 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +5 -3
- package/core/ir.js +6 -2
- package/core/member-signatures.js +789 -0
- package/core/outgoing-check.js +116 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +101 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +29 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +19 -2
- package/core/output/search.js +14 -2
- package/core/output/tracing.js +36 -8
- package/core/output-budget.js +146 -61
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-binding-identity.js +707 -0
- package/core/python-modules.js +6 -0
- package/core/reflection.js +248 -31
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/review-items.js +75 -0
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +148 -164
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3698 -0
- package/core/tracing.js +270 -658
- package/core/verify.js +63 -22
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +466 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1498 -6
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1708 -3
- package/languages/rust.js +209 -104
- package/languages/static-js.js +88 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -5
package/core/search.js
CHANGED
|
@@ -17,6 +17,8 @@ const { isAccessorDefinition, findAccessorReferences } = require('./accessors');
|
|
|
17
17
|
const isSafeRegex = require('safe-regex2');
|
|
18
18
|
const { RE2JS } = require('re2js');
|
|
19
19
|
const { UcnError } = require('./errors');
|
|
20
|
+
const { paramValues } = require('./registry');
|
|
21
|
+
const { createTestModel, addTestCaseRows, linkFiles, testSite, testSiteKey } = require('./test-units');
|
|
20
22
|
|
|
21
23
|
// Keep the established search-input contract for the canonical catastrophic
|
|
22
24
|
// nested-single-atom family even though the RE2-compatible engine could run it
|
|
@@ -36,11 +38,8 @@ function buildGlobMatcher(pattern, caseSensitive) {
|
|
|
36
38
|
return (name) => regex.test(name);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'state', 'field', 'constant', 'macro', 'variable',
|
|
42
|
-
'interface', 'enum', 'struct', 'trait', 'record', 'namespace',
|
|
43
|
-
]);
|
|
41
|
+
// The kinds a structural search takes: the parameter table's set.
|
|
42
|
+
const STRUCTURAL_TYPES = new Set(paramValues('type', 'search'));
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
45
|
* Substring match. Case-insensitive by default.
|
|
@@ -209,14 +208,18 @@ function _applyFindFilters(index, matches, options) {
|
|
|
209
208
|
|
|
210
209
|
// Rank cheaply before adjudicating callers. A broad inventory must never
|
|
211
210
|
// pay for pinned caller resolution on rows the caller will not receive.
|
|
211
|
+
const definitionOrder = (a, b) =>
|
|
212
|
+
(b._fuzzyScore || 0) - (a._fuzzyScore || 0) ||
|
|
213
|
+
Number(isTestFile(a.relativePath || '', a.language)) -
|
|
214
|
+
Number(isTestFile(b.relativePath || '', b.language)) ||
|
|
215
|
+
codeUnitCompare(a.relativePath || '', b.relativePath || '') ||
|
|
216
|
+
a.startLine - b.startLine || codeUnitCompare(a.name, b.name);
|
|
212
217
|
const ranked = filtered.map(symbol => ({
|
|
213
218
|
symbol, counts: index.countSymbolUsages(symbol),
|
|
214
219
|
}));
|
|
215
220
|
if (options.limit > 0 && ranked.length > options.limit) {
|
|
216
221
|
ranked.sort((a, b) => b.counts.total - a.counts.total ||
|
|
217
|
-
|
|
218
|
-
a.symbol.startLine - b.symbol.startLine ||
|
|
219
|
-
codeUnitCompare(a.symbol.name, b.symbol.name));
|
|
222
|
+
definitionOrder(a.symbol, b.symbol));
|
|
220
223
|
}
|
|
221
224
|
const survivors = options.limit > 0 ? ranked.slice(0, options.limit) : ranked;
|
|
222
225
|
const withCounts = survivors.map(({ symbol: m, counts }) => {
|
|
@@ -258,7 +261,7 @@ function _applyFindFilters(index, matches, options) {
|
|
|
258
261
|
});
|
|
259
262
|
|
|
260
263
|
// Sort by usage count (most-used first)
|
|
261
|
-
withCounts.sort((a, b) => b.usageCount - a.usageCount);
|
|
264
|
+
withCounts.sort((a, b) => b.usageCount - a.usageCount || definitionOrder(a, b));
|
|
262
265
|
|
|
263
266
|
return withCounts;
|
|
264
267
|
}
|
|
@@ -1313,6 +1316,10 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1313
1316
|
index._beginOp();
|
|
1314
1317
|
try {
|
|
1315
1318
|
const results = [];
|
|
1319
|
+
const model = createTestModel(index);
|
|
1320
|
+
// Files where a title that names the term is a link (the term on a test
|
|
1321
|
+
// line is the project's symbol, not the file's own helper).
|
|
1322
|
+
const titled = new Set();
|
|
1316
1323
|
|
|
1317
1324
|
// Check if it's a file path
|
|
1318
1325
|
const isFilePath = nameOrFile.includes('/') || nameOrFile.includes('\\') ||
|
|
@@ -1325,7 +1332,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1325
1332
|
let sourceFileFilter = null;
|
|
1326
1333
|
let targetDefs = [];
|
|
1327
1334
|
if (options.file && !isFilePath) {
|
|
1328
|
-
targetDefs = index.find(nameOrFile, { exact: true, file: options.file, className: options.className });
|
|
1335
|
+
targetDefs = index.find(nameOrFile, { exact: true, file: options.file, className: options.className, skipCounts: true });
|
|
1329
1336
|
if (targetDefs.length > 0) {
|
|
1330
1337
|
sourceFileFilter = _buildSourceFileImporters(index, targetDefs);
|
|
1331
1338
|
}
|
|
@@ -1333,12 +1340,11 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1333
1340
|
// The execute handler validates before calling, so this path means
|
|
1334
1341
|
// the file matched but no exact symbol — fall through gracefully.
|
|
1335
1342
|
} else if (!isFilePath) {
|
|
1336
|
-
targetDefs = index.find(nameOrFile, { exact: true, className: options.className });
|
|
1343
|
+
targetDefs = index.find(nameOrFile, { exact: true, className: options.className, skipCounts: true });
|
|
1337
1344
|
}
|
|
1338
1345
|
// A pinned definition among same-name ones (an overload, a constructor
|
|
1339
1346
|
// beside its class and siblings, fix #430): calls are this definition's
|
|
1340
1347
|
// only when the caller engine attributes them to it.
|
|
1341
|
-
const pinnedFamily = !isFilePath && options.definition && targetDefs.length > 1;
|
|
1342
1348
|
if (!isFilePath && options.definition) targetDefs = [options.definition];
|
|
1343
1349
|
|
|
1344
1350
|
// Find all test files
|
|
@@ -1358,9 +1364,50 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1358
1364
|
}
|
|
1359
1365
|
}
|
|
1360
1366
|
|
|
1367
|
+
// A file's name does not decide what a test executes: the body of a
|
|
1368
|
+
// test a class inherits stands where its base class is, and a runner
|
|
1369
|
+
// takes tests from files of any name. A call site the caller engine
|
|
1370
|
+
// finds in such a file is a test's where the test model says a test
|
|
1371
|
+
// executes that line (core/test-units.js): the file joins the test
|
|
1372
|
+
// files, test code within those lines.
|
|
1373
|
+
const testInfoByPath = new Map(testFiles.map(info => [info.path, info]));
|
|
1374
|
+
const testInfoOf = filePath => {
|
|
1375
|
+
if (testInfoByPath.has(filePath)) return testInfoByPath.get(filePath);
|
|
1376
|
+
const entry = index.files.get(filePath);
|
|
1377
|
+
const ranges = entry ? model.testRangesOf(filePath) : [];
|
|
1378
|
+
const info = ranges.length > 0 ? { path: filePath, entry, testRanges: ranges } : null;
|
|
1379
|
+
testInfoByPath.set(filePath, info);
|
|
1380
|
+
if (info) testFiles.push(info);
|
|
1381
|
+
return info;
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1361
1384
|
const searchTerm = isFilePath
|
|
1362
1385
|
? path.basename(nameOrFile, path.extname(nameOrFile))
|
|
1363
1386
|
: nameOrFile;
|
|
1387
|
+
const callerSites = [];
|
|
1388
|
+
const excludedCallableLines = new Map();
|
|
1389
|
+
const globallyUniqueTarget = !isFilePath && targetDefs.length === 1;
|
|
1390
|
+
if (!isFilePath) for (const def of targetDefs) {
|
|
1391
|
+
const raw = index.findCallers(searchTerm, {
|
|
1392
|
+
targetDefinitions: [def], collectAccount: true, unverifiedEnrichLimit: Infinity,
|
|
1393
|
+
});
|
|
1394
|
+
callerSites.push(
|
|
1395
|
+
...raw.map(site => ({ ...site, _testEvidenceTier: 'confirmed' })),
|
|
1396
|
+
...(globallyUniqueTarget ? (raw.unverifiedEntries || []).map(site => ({
|
|
1397
|
+
...site, _testEvidenceTier: 'unverified',
|
|
1398
|
+
})) : []),
|
|
1399
|
+
);
|
|
1400
|
+
const counted = new Set();
|
|
1401
|
+
for (const site of raw.accountRaw?.excludedEntries || []) {
|
|
1402
|
+
const key = `${site.file}:${site.line}`;
|
|
1403
|
+
if (counted.has(key)) continue;
|
|
1404
|
+
counted.add(key);
|
|
1405
|
+
let lines = excludedCallableLines.get(site.file);
|
|
1406
|
+
if (!lines) { lines = new Map(); excludedCallableLines.set(site.file, lines); }
|
|
1407
|
+
lines.set(site.line, (lines.get(site.line) || 0) + 1);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
const acceptedCallableLines = new Set(callerSites.map(site => `${site.file}:${site.line}`));
|
|
1364
1411
|
|
|
1365
1412
|
// File-path target: module specifiers are string literals, so the
|
|
1366
1413
|
// basename usage scan is blind to import-linked tests in JS/TS
|
|
@@ -1388,7 +1435,6 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1388
1435
|
// module-level coverage remain visible.
|
|
1389
1436
|
const fileTargetCallerSites = new Map(); // absolute test file → tiered sites
|
|
1390
1437
|
if (isFilePath && targetRels) {
|
|
1391
|
-
const testFileInfo = new Map(testFiles.map(info => [info.path, info]));
|
|
1392
1438
|
const fileTargetDefs = [];
|
|
1393
1439
|
for (const [, fe] of index.files) {
|
|
1394
1440
|
if (!targetRels.has(fe.relativePath)) continue;
|
|
@@ -1401,7 +1447,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1401
1447
|
targetDefinitions: [def],
|
|
1402
1448
|
collectAccount: true,
|
|
1403
1449
|
});
|
|
1404
|
-
const globallyUnique = index.
|
|
1450
|
+
const globallyUnique = index.symbols.get(def.name)?.length === 1;
|
|
1405
1451
|
const sites = [
|
|
1406
1452
|
...raw.map(site => ({ ...site, _testEvidenceTier: 'confirmed' })),
|
|
1407
1453
|
...(globallyUnique ? (raw.unverifiedEntries || []).map(site => ({
|
|
@@ -1410,7 +1456,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1410
1456
|
})) : []),
|
|
1411
1457
|
];
|
|
1412
1458
|
for (const site of sites) {
|
|
1413
|
-
const info =
|
|
1459
|
+
const info = testInfoOf(site.file);
|
|
1414
1460
|
if (!info) continue;
|
|
1415
1461
|
if (info.testRanges && !lineInRanges(site.line, info.testRanges)) continue;
|
|
1416
1462
|
let rows = fileTargetCallerSites.get(site.file);
|
|
@@ -1539,7 +1585,18 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1539
1585
|
// own definition (fix #246).
|
|
1540
1586
|
if (localShadow && !usage.receiver && usage.usageType !== 'import') continue;
|
|
1541
1587
|
|
|
1542
|
-
|
|
1588
|
+
// File eligibility only discovers candidates. A call's
|
|
1589
|
+
// spelling does not establish which declaration it reaches;
|
|
1590
|
+
// exact-target caller evidence below owns every call row.
|
|
1591
|
+
if (targetDefs.length > 0 && usage.usageType === 'call') continue;
|
|
1592
|
+
const classBodyProof = !usage.receiver && usage.usageType === 'reference' &&
|
|
1593
|
+
classBodyReferenceTargets(index, testPath, entry.language, usage, targetDefs);
|
|
1594
|
+
// Method/function values are caller candidates too. A file
|
|
1595
|
+
// importing any sibling cannot resurrect a reference the
|
|
1596
|
+
// engine attributed to another declaration.
|
|
1597
|
+
if (usage.usageType !== 'import' &&
|
|
1598
|
+
excludedCallableLines.get(testPath)?.get(usage.line) === targetDefs.length &&
|
|
1599
|
+
!acceptedCallableLines.has(`${testPath}:${usage.line}`) && !classBodyProof) continue;
|
|
1543
1600
|
// A line is claimed only by a usage that is recorded: two
|
|
1544
1601
|
// same-kind tokens on one line (`a.m = b.m`) are judged
|
|
1545
1602
|
// separately, so a rejected first token never hides a
|
|
@@ -1625,6 +1682,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1625
1682
|
const nmUsages = index._getCachedUsages(testPath, nm) || [];
|
|
1626
1683
|
for (const u of nmUsages) {
|
|
1627
1684
|
if (u.usageType === 'definition' || u.usageType === 'import') continue;
|
|
1685
|
+
if (u.usageType === 'call') continue; // exact file-target evidence below
|
|
1628
1686
|
if (testRanges && !lineInRanges(u.line, testRanges)) continue;
|
|
1629
1687
|
const lineKey = `${u.line}:${u.usageType}:${nm}`;
|
|
1630
1688
|
if (seenLines.has(lineKey)) continue;
|
|
@@ -1648,9 +1706,9 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1648
1706
|
const matchType = site._testEvidenceTier === 'confirmed'
|
|
1649
1707
|
? 'call'
|
|
1650
1708
|
: 'unverified-call';
|
|
1651
|
-
if (matches.some(m => m
|
|
1709
|
+
if (matches.some(m => testSiteKey(m) === testSiteKey(site) && m.matchType === matchType)) continue;
|
|
1652
1710
|
matches.push({
|
|
1653
|
-
|
|
1711
|
+
...testSite(site),
|
|
1654
1712
|
content: (site.content || index.getLineContent(testPath, site.line) || '').trim(),
|
|
1655
1713
|
matchType,
|
|
1656
1714
|
evidenceTier: site._testEvidenceTier,
|
|
@@ -1659,12 +1717,11 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1659
1717
|
});
|
|
1660
1718
|
}
|
|
1661
1719
|
|
|
1662
|
-
//
|
|
1663
|
-
//
|
|
1664
|
-
// only anchor test cases to matches that survived the shadow.
|
|
1720
|
+
// Under a local same-name shadow the term in a test's title is
|
|
1721
|
+
// the file's OWN helper: titles link only where a match survived.
|
|
1665
1722
|
if (!isFilePath && sourceFileLinked &&
|
|
1666
1723
|
(!localShadow || matches.some(m => m.matchType !== 'import'))) {
|
|
1667
|
-
|
|
1724
|
+
titled.add(entry.relativePath);
|
|
1668
1725
|
}
|
|
1669
1726
|
|
|
1670
1727
|
// Deduplicate: if a line already has a 'call' or 'import', don't also add 'test-case'
|
|
@@ -1694,38 +1751,20 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1694
1751
|
}
|
|
1695
1752
|
}
|
|
1696
1753
|
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
//
|
|
1700
|
-
//
|
|
1701
|
-
// miss calls that `about`/`impact` already know about. For a globally
|
|
1754
|
+
// Callable test links use the caller engine's exact-target decisions,
|
|
1755
|
+
// including aliases and function values that do not spell the target's
|
|
1756
|
+
// name. The literal scan contributes imports/type/string references,
|
|
1757
|
+
// never an independent call identity. For a globally
|
|
1702
1758
|
// unique callable name, also retain account-unverified sites under the
|
|
1703
1759
|
// explicit `unverified-call` label: independent compiler/LSP evaluation
|
|
1704
1760
|
// shows these contain real interface/trait dispatch that a test-discovery
|
|
1705
1761
|
// command must not silently drop. Repeated names never receive that broad
|
|
1706
1762
|
// supplement because it would leak sibling classes/files.
|
|
1707
1763
|
if (!isFilePath && targetDefs.length > 0) {
|
|
1708
|
-
const testInfo = new Map(testFiles.map(t => [t.path, t]));
|
|
1709
|
-
const callerSites = [];
|
|
1710
1764
|
// A pinned definition's unverified sites were adjudicated against it
|
|
1711
1765
|
// alone, like a globally unique name's (fix #430).
|
|
1712
|
-
const globallyUniqueTarget = !!options.definition || index.find(searchTerm, { exact: true }).length === 1;
|
|
1713
|
-
for (const def of targetDefs) {
|
|
1714
|
-
const raw = index.findCallers(searchTerm, {
|
|
1715
|
-
targetDefinitions: [def],
|
|
1716
|
-
collectAccount: true,
|
|
1717
|
-
});
|
|
1718
|
-
callerSites.push(
|
|
1719
|
-
...raw.map(c => ({ ...c, _testEvidenceTier: 'confirmed' })),
|
|
1720
|
-
...(globallyUniqueTarget ? (raw.unverifiedEntries || []).map(c => ({
|
|
1721
|
-
...c,
|
|
1722
|
-
_testEvidenceTier: 'unverified',
|
|
1723
|
-
})) : []),
|
|
1724
|
-
);
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
1766
|
for (const site of callerSites) {
|
|
1728
|
-
const info =
|
|
1767
|
+
const info = testInfoOf(site.file);
|
|
1729
1768
|
if (!info) continue;
|
|
1730
1769
|
if (info.testRanges && !lineInRanges(site.line, info.testRanges)) continue;
|
|
1731
1770
|
// Every site here was adjudicated against the exact target
|
|
@@ -1754,12 +1793,12 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1754
1793
|
fileResult = { file: info.entry.relativePath, matches: [] };
|
|
1755
1794
|
results.push(fileResult);
|
|
1756
1795
|
}
|
|
1757
|
-
if (fileResult.matches.some(m => m
|
|
1796
|
+
if (fileResult.matches.some(m => testSiteKey(m) === testSiteKey(site) &&
|
|
1758
1797
|
['call', 'unverified-call'].includes(m.matchType))) continue;
|
|
1759
1798
|
fileResult.matches = fileResult.matches.filter(m =>
|
|
1760
1799
|
!(m.line === site.line && m.matchType === 'test-case'));
|
|
1761
1800
|
fileResult.matches.push({
|
|
1762
|
-
|
|
1801
|
+
...testSite(site),
|
|
1763
1802
|
content: (site.content || index.getLineContent(site.file, site.line) || '').trim(),
|
|
1764
1803
|
matchType: site._testEvidenceTier === 'confirmed' ? 'call' : 'unverified-call',
|
|
1765
1804
|
evidenceTier: site._testEvidenceTier,
|
|
@@ -1780,16 +1819,22 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1780
1819
|
const nestedTypeDefs = targetDefs.filter(def => def.enclosingType &&
|
|
1781
1820
|
['class', 'struct', 'interface', 'type', 'enum', 'trait', 'record', 'union'].includes(def.type));
|
|
1782
1821
|
if (accessorDefs.length > 0 || nestedTypeDefs.length > 0) {
|
|
1783
|
-
|
|
1822
|
+
// Reference-only tests have no call site to add their file to
|
|
1823
|
+
// testFiles above. Let the reference engines find their sites
|
|
1824
|
+
// first, then ask the same model which lines a test executes.
|
|
1825
|
+
// The index-only gate spares files that cannot hold test code;
|
|
1826
|
+
// filenames alone never remove a candidate.
|
|
1827
|
+
const referencePaths = new Set([...index.files.keys()].filter(filePath =>
|
|
1828
|
+
testInfoByPath.get(filePath) || model.mayHoldTests(filePath)));
|
|
1784
1829
|
const globallyUnique = index.find(searchTerm, { exact: true }).length === 1;
|
|
1785
1830
|
for (const def of [...accessorDefs, ...nestedTypeDefs]) {
|
|
1786
1831
|
const types = nestedTypeDefs.includes(def)
|
|
1787
|
-
? require('./analysis').findTypeReferences(index, searchTerm, def, { files:
|
|
1832
|
+
? require('./analysis').findTypeReferences(index, searchTerm, def, { files: referencePaths }) : null;
|
|
1788
1833
|
const typeSite = site => ({ ...site, absoluteFile: path.join(index.root, site.file) });
|
|
1789
1834
|
const refs = types ? {
|
|
1790
1835
|
confirmed: types.byFile.flatMap(group => group.sites).map(typeSite),
|
|
1791
1836
|
unverified: types.unverifiedSites.map(typeSite),
|
|
1792
|
-
} : findAccessorReferences(index, searchTerm, def, { files:
|
|
1837
|
+
} : findAccessorReferences(index, searchTerm, def, { files: referencePaths });
|
|
1793
1838
|
if (!refs) continue;
|
|
1794
1839
|
const rows = [
|
|
1795
1840
|
...refs.confirmed.map(r => ({ ...r, tierName: 'confirmed' })),
|
|
@@ -1797,7 +1842,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1797
1842
|
? refs.unverified.map(r => ({ ...r, tierName: 'unverified' })) : []),
|
|
1798
1843
|
];
|
|
1799
1844
|
for (const ref of rows) {
|
|
1800
|
-
const info =
|
|
1845
|
+
const info = testInfoOf(ref.absoluteFile);
|
|
1801
1846
|
if (!info) continue;
|
|
1802
1847
|
if (info.testRanges && !lineInRanges(ref.line, info.testRanges)) continue;
|
|
1803
1848
|
if (excludeArr.length > 0 &&
|
|
@@ -1835,6 +1880,55 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1835
1880
|
results.sort((a, b) => codeUnitCompare(a.file || '', b.file || ''));
|
|
1836
1881
|
}
|
|
1837
1882
|
|
|
1883
|
+
// The tests that hold the matches (core/test-units.js): one row per
|
|
1884
|
+
// test, then what the file is to its runner and how to run it.
|
|
1885
|
+
const absolute = new Map(testFiles.map(info => [info.entry.relativePath, info.path]));
|
|
1886
|
+
const linesByFile = new Map();
|
|
1887
|
+
for (const fileResult of results) {
|
|
1888
|
+
const testPath = absolute.get(fileResult.file);
|
|
1889
|
+
if (!testPath) continue;
|
|
1890
|
+
addTestCaseRows(model, index, testPath, isFilePath ? null : searchTerm, fileResult.matches, {
|
|
1891
|
+
counts: match => match.matchType !== 'import',
|
|
1892
|
+
titles: titled.has(fileResult.file),
|
|
1893
|
+
titleNeedsMatch: !!className,
|
|
1894
|
+
});
|
|
1895
|
+
linesByFile.set(testPath,
|
|
1896
|
+
fileResult.matches.filter(match => match.matchType !== 'import'));
|
|
1897
|
+
}
|
|
1898
|
+
// A test a class inherits runs where the class is: the file of a class
|
|
1899
|
+
// that inherits a linked test is a result of its own, with the test.
|
|
1900
|
+
const byFile = new Map(results.map(fileResult => [fileResult.file, fileResult]));
|
|
1901
|
+
for (const [testPath, linked] of linkFiles(model, index, linesByFile)) {
|
|
1902
|
+
const relativePath = index.files.get(testPath).relativePath;
|
|
1903
|
+
let fileResult = byFile.get(relativePath);
|
|
1904
|
+
if (!fileResult) {
|
|
1905
|
+
fileResult = { file: relativePath, matches: [] };
|
|
1906
|
+
byFile.set(relativePath, fileResult);
|
|
1907
|
+
results.push(fileResult);
|
|
1908
|
+
}
|
|
1909
|
+
for (const unit of linked.units) {
|
|
1910
|
+
if (!unit.inheritedFrom) continue;
|
|
1911
|
+
fileResult.matches.push({
|
|
1912
|
+
line: unit.line,
|
|
1913
|
+
content: (index.getLineContent(testPath, unit.line) || '').trim(),
|
|
1914
|
+
matchType: 'inherited-test',
|
|
1915
|
+
name: unit.name,
|
|
1916
|
+
from: unit.inheritedFrom,
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
fileResult.matches.sort((a, b) => (a.line || 0) - (b.line || 0) ||
|
|
1920
|
+
codeUnitCompare(a.matchType || '', b.matchType || '') || codeUnitCompare(a.name || '', b.name || ''));
|
|
1921
|
+
fileResult.role = linked.role;
|
|
1922
|
+
if (linked.runner) fileResult.runner = linked.runner;
|
|
1923
|
+
fileResult.tests = linked.tests;
|
|
1924
|
+
if (linked.moreTests) fileResult.moreTests = true;
|
|
1925
|
+
if (linked.conditionalTests) fileResult.conditionalTests = linked.conditionalTests;
|
|
1926
|
+
if (linked.units.length > 0) fileResult.units = linked.units;
|
|
1927
|
+
if (linked.run) fileResult.run = linked.run;
|
|
1928
|
+
else if (linked.runWithheld) fileResult.runWithheld = linked.runWithheld;
|
|
1929
|
+
}
|
|
1930
|
+
results.sort((a, b) => codeUnitCompare(a.file || '', b.file || ''));
|
|
1931
|
+
|
|
1838
1932
|
return results;
|
|
1839
1933
|
} finally { index._endOp(); }
|
|
1840
1934
|
}
|
|
@@ -2181,117 +2275,6 @@ function _receiverMatchesClass(usage, dispatchNames, instanceTypeMap, lineConten
|
|
|
2181
2275
|
return false;
|
|
2182
2276
|
}
|
|
2183
2277
|
|
|
2184
|
-
/**
|
|
2185
|
-
* Language-aware test-case detection.
|
|
2186
|
-
*
|
|
2187
|
-
* JS/TS: describe, it, test, spec calls with searchTerm in the description.
|
|
2188
|
-
* Go: TestXxx, BenchmarkXxx, ExampleXxx functions containing a usage of searchTerm.
|
|
2189
|
-
* Python: test_ functions or TestCase methods containing a usage of searchTerm.
|
|
2190
|
-
* Java: @Test-annotated methods containing a usage of searchTerm.
|
|
2191
|
-
* Rust: #[test]-attributed functions containing a usage of searchTerm.
|
|
2192
|
-
*/
|
|
2193
|
-
function _addTestCaseMatches(index, filePath, fileEntry, searchTerm, className, instanceTypeMap, matches) {
|
|
2194
|
-
const matchLines = new Set(matches.map(m => m.line));
|
|
2195
|
-
const lang = fileEntry.language;
|
|
2196
|
-
|
|
2197
|
-
if (lang === 'javascript' || lang === 'typescript' || lang === 'tsx') {
|
|
2198
|
-
// JS/TS: find describe/it/test/spec calls from getCachedCalls
|
|
2199
|
-
const calls = getCachedCalls(index, filePath);
|
|
2200
|
-
if (!calls) return;
|
|
2201
|
-
const testFrameworkCalls = new Set(['describe', 'it', 'test', 'spec']);
|
|
2202
|
-
// Word-boundary match: `describe("untargeted zone")` is not a test
|
|
2203
|
-
// case for `target` (fix #246 — raw substring matched mid-word).
|
|
2204
|
-
const termPattern = new RegExp('(^|[^\\w$])' + escapeRegExp(searchTerm) + '([^\\w$]|$)');
|
|
2205
|
-
for (const call of calls) {
|
|
2206
|
-
if (!testFrameworkCalls.has(call.name)) continue;
|
|
2207
|
-
const lineContent = index.getLineContent(filePath, call.line);
|
|
2208
|
-
// Check if searchTerm appears in the description string on this line
|
|
2209
|
-
if (termPattern.test(lineContent) && !matchLines.has(call.line)) {
|
|
2210
|
-
// className scoping: only add test-case if the test body has a
|
|
2211
|
-
// class-scoped match (call or class-receiver reference) — not just
|
|
2212
|
-
// any mention of className.
|
|
2213
|
-
if (className) {
|
|
2214
|
-
const endLine = _estimateTestBlockEnd(index, filePath, call.line);
|
|
2215
|
-
const hasClassScopedMatch = matches.some(m =>
|
|
2216
|
-
m.line >= call.line && m.line <= endLine &&
|
|
2217
|
-
m.matchType !== 'import'
|
|
2218
|
-
);
|
|
2219
|
-
if (!hasClassScopedMatch) continue;
|
|
2220
|
-
}
|
|
2221
|
-
matches.push({
|
|
2222
|
-
line: call.line,
|
|
2223
|
-
content: lineContent.trim(),
|
|
2224
|
-
matchType: 'test-case'
|
|
2225
|
-
});
|
|
2226
|
-
matchLines.add(call.line);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
} else {
|
|
2230
|
-
// Go/Python/Java/Rust: check if any AST usage falls within a test function's range
|
|
2231
|
-
if (!fileEntry.symbols) return;
|
|
2232
|
-
try {
|
|
2233
|
-
const langModule = getLanguageAdapter(lang);
|
|
2234
|
-
if (!langModule) return;
|
|
2235
|
-
// Prefer kinded predicate so fn main() and fn init() aren't classified
|
|
2236
|
-
// as test cases (BUG-CX). Fall back to isEntryPoint for compat.
|
|
2237
|
-
const classify = langModule.getEntryPointKind
|
|
2238
|
-
? (s) => langModule.getEntryPointKind(s) === 'test'
|
|
2239
|
-
: langModule.isEntryPoint;
|
|
2240
|
-
if (!classify) return;
|
|
2241
|
-
|
|
2242
|
-
// Find test symbols
|
|
2243
|
-
for (const symbol of fileEntry.symbols) {
|
|
2244
|
-
if (!classify(symbol)) continue;
|
|
2245
|
-
// Check if any non-import usage of searchTerm falls within this test function
|
|
2246
|
-
const usageInRange = matches.some(m =>
|
|
2247
|
-
m.line >= symbol.startLine && m.line <= symbol.endLine &&
|
|
2248
|
-
m.matchType !== 'import'
|
|
2249
|
-
);
|
|
2250
|
-
if (usageInRange && !matchLines.has(symbol.startLine)) {
|
|
2251
|
-
// className scoping: verify the test body has class-scoped matches
|
|
2252
|
-
if (className) {
|
|
2253
|
-
const hasClassScopedMatch = matches.some(m =>
|
|
2254
|
-
m.line >= symbol.startLine && m.line <= symbol.endLine &&
|
|
2255
|
-
m.matchType !== 'import'
|
|
2256
|
-
);
|
|
2257
|
-
if (!hasClassScopedMatch) continue;
|
|
2258
|
-
}
|
|
2259
|
-
const lineContent = index.getLineContent(filePath, symbol.startLine);
|
|
2260
|
-
matches.push({
|
|
2261
|
-
line: symbol.startLine,
|
|
2262
|
-
content: lineContent.trim(),
|
|
2263
|
-
matchType: 'test-case'
|
|
2264
|
-
});
|
|
2265
|
-
matchLines.add(symbol.startLine);
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
} catch (e) {
|
|
2269
|
-
// Skip if language module unavailable
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
/**
|
|
2275
|
-
* Estimate the end line of a test block (it/test/describe callback) by tracking
|
|
2276
|
-
* brace nesting from the start line.
|
|
2277
|
-
*/
|
|
2278
|
-
function _estimateTestBlockEnd(index, filePath, startLine) {
|
|
2279
|
-
const content = index._readFile(filePath);
|
|
2280
|
-
if (!content) return startLine + 5;
|
|
2281
|
-
const lines = content.split('\n');
|
|
2282
|
-
let depth = 0;
|
|
2283
|
-
let started = false;
|
|
2284
|
-
for (let i = startLine - 1; i < lines.length; i++) {
|
|
2285
|
-
const line = lines[i];
|
|
2286
|
-
for (const ch of line) {
|
|
2287
|
-
if (ch === '{' || ch === '(') { depth++; started = true; }
|
|
2288
|
-
else if (ch === '}' || ch === ')') { depth--; }
|
|
2289
|
-
}
|
|
2290
|
-
if (started && depth <= 0) return i + 1; // 1-based
|
|
2291
|
-
}
|
|
2292
|
-
return Math.min(startLine + 10, lines.length);
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
2278
|
/**
|
|
2296
2279
|
* Deduplicate matches: prefer more specific matchTypes on the same line.
|
|
2297
2280
|
* Priority: test-case > call > import > string-ref > reference
|
|
@@ -2300,9 +2283,10 @@ function _deduplicateMatches(matches) {
|
|
|
2300
2283
|
const byLine = new Map();
|
|
2301
2284
|
const priority = { 'test-case': 5, 'call': 4, 'import': 3, 'string-ref': 2, 'reference': 1 };
|
|
2302
2285
|
for (const m of matches) {
|
|
2303
|
-
const
|
|
2286
|
+
const key = testSiteKey(m);
|
|
2287
|
+
const existing = byLine.get(key);
|
|
2304
2288
|
if (!existing || (priority[m.matchType] || 0) > (priority[existing.matchType] || 0)) {
|
|
2305
|
-
byLine.set(
|
|
2289
|
+
byLine.set(key, m);
|
|
2306
2290
|
}
|
|
2307
2291
|
}
|
|
2308
2292
|
return [...byLine.values()].sort((a, b) => a.line - b.line);
|
package/core/shared.js
CHANGED
|
@@ -260,49 +260,6 @@ function isOverrideMarked(def) {
|
|
|
260
260
|
return false;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
// Per-language text patterns for the "blind spots" UCN's AST can't follow:
|
|
264
|
-
// eval/exec-style code execution and reflection (dynamic attribute access /
|
|
265
|
-
// dynamic dispatch). ONE source of truth so doctor's trust scan and
|
|
266
|
-
// detectCompleteness's about-footer warning count identically (field-report #2:
|
|
267
|
-
// they used to diverge — doctor 497 reflection vs footer 194, eval 3 vs 2 —
|
|
268
|
-
// because each kept its own regex set). Dynamic imports are NOT here: those are
|
|
269
|
-
// structural (fileEntry.dynamicImports), the AST-accurate count both paths share.
|
|
270
|
-
// `new Function(...)` is categorized as eval (code execution), not reflection.
|
|
271
|
-
const BLINDSPOT_TEXT_PATTERNS = {
|
|
272
|
-
reflection: {
|
|
273
|
-
python: /\b(getattr|hasattr|setattr|__import__|importlib\.import_module)\s*\(/g,
|
|
274
|
-
javascript: /\bReflect\.\w+\s*\(/g,
|
|
275
|
-
typescript: /\bReflect\.\w+\s*\(/g,
|
|
276
|
-
go: /\breflect\.\w+\s*\(/g,
|
|
277
|
-
java: /\.getDeclaredMethod\b|\.getMethod\b|\.getDeclaredField\b|Class\.forName\b/g,
|
|
278
|
-
rust: /\bAny::downcast/g,
|
|
279
|
-
},
|
|
280
|
-
eval: {
|
|
281
|
-
python: /\b(eval|exec)\s*\(/g,
|
|
282
|
-
javascript: /\beval\s*\(|\bnew\s+Function\s*\(/g,
|
|
283
|
-
typescript: /\beval\s*\(|\bnew\s+Function\s*\(/g,
|
|
284
|
-
},
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
/** True when a language has any text-blind-spot pattern (so callers can skip the file read otherwise). */
|
|
288
|
-
function hasTextBlindspots(language) {
|
|
289
|
-
return !!(BLINDSPOT_TEXT_PATTERNS.reflection[language] || BLINDSPOT_TEXT_PATTERNS.eval[language]);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Count text-detected blind spots (eval/exec, reflection) in one file's source.
|
|
294
|
-
* Returns { eval, reflection } OCCURRENCE counts (global match). Shared by doctor
|
|
295
|
-
* and detectCompleteness so both report the same numbers (field-report #2).
|
|
296
|
-
*/
|
|
297
|
-
function countTextBlindspots(content, language) {
|
|
298
|
-
const reRe = BLINDSPOT_TEXT_PATTERNS.reflection[language];
|
|
299
|
-
const evRe = BLINDSPOT_TEXT_PATTERNS.eval[language];
|
|
300
|
-
return {
|
|
301
|
-
eval: evRe ? (content.match(evRe) || []).length : 0,
|
|
302
|
-
reflection: reRe ? (content.match(reRe) || []).length : 0,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
263
|
/**
|
|
307
264
|
* Line ranges of INLINE test symbols in a source file — Rust #[test] fns and
|
|
308
265
|
* #[cfg(test)] module members. A production file promoted to "test file"
|
|
@@ -614,8 +571,6 @@ module.exports = { literalNameRegex,
|
|
|
614
571
|
looksLikeHandle,
|
|
615
572
|
isTestPath,
|
|
616
573
|
isOverrideMarked,
|
|
617
|
-
hasTextBlindspots,
|
|
618
|
-
countTextBlindspots,
|
|
619
574
|
isPathInsideRoot,
|
|
620
575
|
isMacroNamespaceDefinition,
|
|
621
576
|
};
|