ucn 5.5.2 → 5.6.0
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 +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- 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 +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -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 +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- 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-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- 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 +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -6
package/core/tracing.js
CHANGED
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
|
|
22
22
|
const path = require('path');
|
|
23
23
|
const { escapeRegExp, codeUnitCompare, inlineTestRanges, lineInRanges, classDispatchNames } = require('./shared');
|
|
24
|
+
const { paramDefault } = require('./registry');
|
|
24
25
|
const { isTestFile } = require('./discovery');
|
|
25
26
|
const { getCachedCalls } = require('./callers');
|
|
26
27
|
const { getLanguageAdapter } = require('../languages');
|
|
28
|
+
const { createTestModel, addTestCaseRows, linkFiles, commandsFor } = require('./test-units');
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Contract-mode caller expansion for the tree commands. Memoizes the full
|
|
@@ -147,7 +149,7 @@ function trace(index, name, options = {}) {
|
|
|
147
149
|
index._beginOp();
|
|
148
150
|
try {
|
|
149
151
|
// Sanitize depth: use default for null/undefined, clamp negative to 0
|
|
150
|
-
const rawDepth = options.depth ??
|
|
152
|
+
const rawDepth = options.depth ?? paramDefault('depth', 'trace');
|
|
151
153
|
const maxDepth = Math.max(0, rawDepth);
|
|
152
154
|
const direction = options.direction || 'down'; // 'down' = callees, 'up' = callers, 'both'
|
|
153
155
|
const maxChildren = options.all ? Infinity : 10;
|
|
@@ -340,7 +342,7 @@ function trace(index, name, options = {}) {
|
|
|
340
342
|
function blast(index, name, options = {}) {
|
|
341
343
|
index._beginOp();
|
|
342
344
|
try {
|
|
343
|
-
const maxDepth = Math.max(0, options.depth ??
|
|
345
|
+
const maxDepth = Math.max(0, options.depth ?? paramDefault('depth', 'trace'));
|
|
344
346
|
const maxChildren = options.all ? Infinity : 10;
|
|
345
347
|
const includeMethods = options.includeMethods ?? true;
|
|
346
348
|
const exclude = options.exclude || [];
|
|
@@ -354,7 +356,9 @@ function blast(index, name, options = {}) {
|
|
|
354
356
|
if (!def) return null;
|
|
355
357
|
|
|
356
358
|
const visited = new Set();
|
|
357
|
-
|
|
359
|
+
// A caller's answer about one definition is the same whichever
|
|
360
|
+
// root the walk started from: a selection over many roots shares it.
|
|
361
|
+
const callerCache = options._callerCache || new Map();
|
|
358
362
|
const affectedFunctions = new Set();
|
|
359
363
|
const possiblyAffectedSet = new Set();
|
|
360
364
|
const affectedFiles = new Set();
|
|
@@ -590,7 +594,7 @@ function blast(index, name, options = {}) {
|
|
|
590
594
|
function reverseTrace(index, name, options = {}) {
|
|
591
595
|
index._beginOp();
|
|
592
596
|
try {
|
|
593
|
-
const maxDepth = Math.max(0, options.depth ??
|
|
597
|
+
const maxDepth = Math.max(0, options.depth ?? paramDefault('depth', 'trace', 'entrypoints'));
|
|
594
598
|
const maxChildren = options.all ? Infinity : 10;
|
|
595
599
|
const includeMethods = options.includeMethods ?? true;
|
|
596
600
|
const exclude = options.exclude || [];
|
|
@@ -855,6 +859,10 @@ function affectedTests(index, name, options = {}) {
|
|
|
855
859
|
depth: maxDepth,
|
|
856
860
|
file: options.file,
|
|
857
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,
|
|
858
866
|
all: true,
|
|
859
867
|
exclude: options.exclude,
|
|
860
868
|
includeMethods: options.includeMethods,
|
|
@@ -900,12 +908,22 @@ function affectedTests(index, name, options = {}) {
|
|
|
900
908
|
const excludeArr = exclude ? (Array.isArray(exclude) ? exclude : [exclude]) : [];
|
|
901
909
|
const includeMethods = options.includeMethods ?? true;
|
|
902
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
|
+
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 => {
|
|
918
|
+
const ranges = model.testRangesOf(filePath);
|
|
919
|
+
return ranges.length > 0 ? ranges : null;
|
|
920
|
+
};
|
|
903
921
|
// Test FILES containing an unverified call site of an affected name
|
|
904
922
|
// (relativePath → funcName → Set(lines)) — merged into
|
|
905
923
|
// possiblyAffectedTests after the scan (fix #244).
|
|
906
924
|
const frontierTestHits = new Map();
|
|
907
925
|
{
|
|
908
|
-
const callerCache = new Map();
|
|
926
|
+
const callerCache = options._callerCache || new Map();
|
|
909
927
|
const possibleVisited = new Set();
|
|
910
928
|
const queue = [];
|
|
911
929
|
const enqueueCaller = (c, depth) => {
|
|
@@ -954,6 +972,11 @@ function affectedTests(index, name, options = {}) {
|
|
|
954
972
|
const ranges = inlineTestRanges(cfe);
|
|
955
973
|
if (ranges.length > 0 && lineInRanges(fe.line, ranges)) isTestCaller = true;
|
|
956
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
|
+
}
|
|
957
980
|
if (isTestCaller &&
|
|
958
981
|
(excludeArr.length === 0 || index.matchesFilters(cfe.relativePath, { exclude: excludeArr }))) {
|
|
959
982
|
const affName = fe.atNode?.name || name;
|
|
@@ -991,10 +1014,26 @@ function affectedTests(index, name, options = {}) {
|
|
|
991
1014
|
// Step 3: Scan test files for all affected names using AST
|
|
992
1015
|
// Only count call and test-case matches as real coverage — not imports or bare references.
|
|
993
1016
|
const className = options.className || null;
|
|
1017
|
+
const absoluteOf = new Map(); // relative test path -> absolute
|
|
994
1018
|
const results = [];
|
|
995
1019
|
const possibleResults = [];
|
|
996
1020
|
const scanNames = [...affectedNames, ...possiblyNames];
|
|
997
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);
|
|
1029
|
+
}
|
|
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
|
+
}
|
|
1036
|
+
}
|
|
998
1037
|
for (const [filePath, fileEntry] of index.files) {
|
|
999
1038
|
let isTest = isTestFile(fileEntry.relativePath, fileEntry.language);
|
|
1000
1039
|
// Rust inline #[cfg(test)] modules: source files with #[test]-marked symbols
|
|
@@ -1009,6 +1048,17 @@ function affectedTests(index, name, options = {}) {
|
|
|
1009
1048
|
testRanges = ranges;
|
|
1010
1049
|
}
|
|
1011
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
|
+
}
|
|
1061
|
+
}
|
|
1012
1062
|
if (!isTest) continue;
|
|
1013
1063
|
if (excludeArr.length > 0 && !index.matchesFilters(fileEntry.relativePath, { exclude: excludeArr })) continue;
|
|
1014
1064
|
try {
|
|
@@ -1025,6 +1075,12 @@ function affectedTests(index, name, options = {}) {
|
|
|
1025
1075
|
// scan remains their best-effort fallback).
|
|
1026
1076
|
const engineAdjudicated = expandedNames.has(funcName);
|
|
1027
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
|
+
|
|
1028
1084
|
// Fast pre-check
|
|
1029
1085
|
if (!content.includes(funcName) && !engineFileSites) continue;
|
|
1030
1086
|
|
|
@@ -1172,9 +1228,17 @@ function affectedTests(index, name, options = {}) {
|
|
|
1172
1228
|
}
|
|
1173
1229
|
}
|
|
1174
1230
|
|
|
1175
|
-
//
|
|
1176
|
-
|
|
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
|
+
}
|
|
1177
1240
|
}
|
|
1241
|
+
if (fileMatches.size > 0) absoluteOf.set(fileEntry.relativePath, filePath);
|
|
1178
1242
|
|
|
1179
1243
|
if (fileMatches.size > 0) {
|
|
1180
1244
|
const linkedFunctions = [...fileMatches.keys()];
|
|
@@ -1283,6 +1347,68 @@ function affectedTests(index, name, options = {}) {
|
|
|
1283
1347
|
});
|
|
1284
1348
|
}
|
|
1285
1349
|
|
|
1350
|
+
// What each file is to its runner, which tests hold the links, and
|
|
1351
|
+
// how many tests that is.
|
|
1352
|
+
for (const [absFile, entry] of frontierTestHits) absoluteOf.set(entry.rel, absFile);
|
|
1353
|
+
const confirmedTests = new Set();
|
|
1354
|
+
const conditionalTests = new Set();
|
|
1355
|
+
const describeBand = (rows, confirmed) => {
|
|
1356
|
+
const linesByFile = new Map();
|
|
1357
|
+
for (const row of rows) {
|
|
1358
|
+
const absFile = absoluteOf.get(row.file);
|
|
1359
|
+
if (!absFile) continue;
|
|
1360
|
+
linesByFile.set(absFile, row.matches
|
|
1361
|
+
.filter(match => match.matchType === 'call' || match.matchType === 'test-case')
|
|
1362
|
+
.map(match => match.line));
|
|
1363
|
+
}
|
|
1364
|
+
const byFile = new Map(rows.map(row => [row.file, row]));
|
|
1365
|
+
for (const [absFile, linked] of linkFiles(model, index, linesByFile)) {
|
|
1366
|
+
const relativePath = index.files.get(absFile).relativePath;
|
|
1367
|
+
let row = byFile.get(relativePath);
|
|
1368
|
+
if (!row) {
|
|
1369
|
+
// The file of a class that inherits a linked test: the
|
|
1370
|
+
// test runs there, whatever file its body stands in.
|
|
1371
|
+
const sources = (linked.from || []).map(from => byFile.get(from)).filter(Boolean);
|
|
1372
|
+
row = {
|
|
1373
|
+
file: relativePath,
|
|
1374
|
+
linkedFunctions: [...new Set(sources.flatMap(source => source.linkedFunctions))]
|
|
1375
|
+
.sort(codeUnitCompare),
|
|
1376
|
+
matchCount: 0,
|
|
1377
|
+
matches: [],
|
|
1378
|
+
};
|
|
1379
|
+
byFile.set(relativePath, row);
|
|
1380
|
+
absoluteOf.set(relativePath, absFile);
|
|
1381
|
+
rows.push(row);
|
|
1382
|
+
}
|
|
1383
|
+
for (const unit of linked.units) {
|
|
1384
|
+
if (!unit.inheritedFrom) continue;
|
|
1385
|
+
row.matches.push({
|
|
1386
|
+
line: unit.line,
|
|
1387
|
+
content: (index.getLineContent(absFile, unit.line) || '').trim(),
|
|
1388
|
+
matchType: 'inherited-test',
|
|
1389
|
+
functionName: row.linkedFunctions[0] || name,
|
|
1390
|
+
name: unit.name,
|
|
1391
|
+
from: unit.inheritedFrom,
|
|
1392
|
+
});
|
|
1393
|
+
row.matchCount = row.matches.length;
|
|
1394
|
+
}
|
|
1395
|
+
row.role = linked.role;
|
|
1396
|
+
if (linked.runner) row.runner = linked.runner;
|
|
1397
|
+
row.testCount = linked.tests;
|
|
1398
|
+
if (linked.moreTests) row.moreTests = true;
|
|
1399
|
+
if (linked.conditionalTests) row.conditionalTests = linked.conditionalTests;
|
|
1400
|
+
if (linked.units.length > 0) row.units = linked.units;
|
|
1401
|
+
if (linked.run) row.run = linked.run;
|
|
1402
|
+
else if (linked.runWithheld) row.runWithheld = linked.runWithheld;
|
|
1403
|
+
if (confirmed) {
|
|
1404
|
+
for (const key of linked.testKeys) confirmedTests.add(key);
|
|
1405
|
+
for (const key of linked.conditionalKeys) conditionalTests.add(key);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
describeBand(results, true);
|
|
1410
|
+
describeBand(possibleResults, false);
|
|
1411
|
+
|
|
1286
1412
|
// Sort by linkage breadth then alphabetically
|
|
1287
1413
|
results.sort((a, b) => b.linkedFunctions.length - a.linkedFunctions.length || codeUnitCompare(a.file, b.file));
|
|
1288
1414
|
possibleResults.sort((a, b) => b.linkedFunctions.length - a.linkedFunctions.length || codeUnitCompare(a.file, b.file));
|
|
@@ -1295,11 +1421,16 @@ function affectedTests(index, name, options = {}) {
|
|
|
1295
1421
|
// functions (#[test] fns in Rust's #[cfg(test)] mods, Go Test*)
|
|
1296
1422
|
// live in production-path FILES but are tests themselves — the
|
|
1297
1423
|
// language's getEntryPointKind says so; they need no coverage.
|
|
1298
|
-
for (const [, fe] of index.files) {
|
|
1424
|
+
for (const [filePath, fe] of index.files) {
|
|
1299
1425
|
if (isTestFile(fe.relativePath, fe.language)) continue;
|
|
1300
1426
|
const langModule = getLanguageAdapter(fe.language);
|
|
1301
1427
|
const kindOf = langModule?.getEntryPointKind;
|
|
1302
|
-
|
|
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)))) {
|
|
1303
1434
|
return true;
|
|
1304
1435
|
}
|
|
1305
1436
|
}
|
|
@@ -1331,11 +1462,21 @@ function affectedTests(index, name, options = {}) {
|
|
|
1331
1462
|
possiblyAffected: possiblyProduction,
|
|
1332
1463
|
testFiles: results,
|
|
1333
1464
|
possiblyAffectedTests: possibleResults,
|
|
1465
|
+
// The commands that run the confirmed files, where a runner is
|
|
1466
|
+
// proven and its command is known.
|
|
1467
|
+
commands: commandsFor(model, results.filter(row => row.role === 'tests')
|
|
1468
|
+
.map(row => absoluteOf.get(row.file)).filter(Boolean)),
|
|
1334
1469
|
...(blastResult.account && { account: blastResult.account }),
|
|
1335
1470
|
treeAccount: blastResult.treeAccount,
|
|
1336
1471
|
summary: {
|
|
1337
1472
|
totalAffected: namesForCoverage.size,
|
|
1338
1473
|
totalTestFiles: results.length,
|
|
1474
|
+
totalTests: confirmedTests.size,
|
|
1475
|
+
// A file among them holds tests that are not listed: the
|
|
1476
|
+
// total is a lower bound.
|
|
1477
|
+
...(results.some(row => row.moreTests) && { moreTests: true }),
|
|
1478
|
+
...(conditionalTests.size > 0 && { conditionalTests: conditionalTests.size }),
|
|
1479
|
+
supportFiles: results.filter(row => row.role === 'support').length,
|
|
1339
1480
|
staticallyLinkedFunctions: linkedSet.size,
|
|
1340
1481
|
notStaticallyLinkedCount: notStaticallyLinked.length,
|
|
1341
1482
|
possiblyAffected: possiblyProduction.length,
|
|
@@ -1348,73 +1489,6 @@ function affectedTests(index, name, options = {}) {
|
|
|
1348
1489
|
} finally { index._endOp(); }
|
|
1349
1490
|
}
|
|
1350
1491
|
|
|
1351
|
-
/**
|
|
1352
|
-
* Add test-case matches for a function name in a test file (language-aware).
|
|
1353
|
-
* Only adds test-case when the test body has a call match (not just import/reference).
|
|
1354
|
-
*/
|
|
1355
|
-
function _addAffectedTestCases(index, filePath, fileEntry, funcName, fileMatches) {
|
|
1356
|
-
const lang = fileEntry.language;
|
|
1357
|
-
const existingMatches = fileMatches.get(funcName) || [];
|
|
1358
|
-
const existingLines = new Set(existingMatches.map(m => m.line));
|
|
1359
|
-
|
|
1360
|
-
if (lang === 'javascript' || lang === 'typescript' || lang === 'tsx') {
|
|
1361
|
-
const calls = getCachedCalls(index, filePath);
|
|
1362
|
-
if (!calls) return;
|
|
1363
|
-
const testFrameworkCalls = new Set(['describe', 'it', 'test', 'spec']);
|
|
1364
|
-
// Word-boundary match (fix #246): substring matched mid-word titles.
|
|
1365
|
-
const termPattern = new RegExp('(^|[^\\w$])' + escapeRegExp(funcName) + '([^\\w$]|$)');
|
|
1366
|
-
for (const call of calls) {
|
|
1367
|
-
if (!testFrameworkCalls.has(call.name)) continue;
|
|
1368
|
-
const lineContent = index.getLineContent(filePath, call.line);
|
|
1369
|
-
if (termPattern.test(lineContent) && !existingLines.has(call.line)) {
|
|
1370
|
-
// Only add test-case if a call match exists in the test body
|
|
1371
|
-
const endLine = _estimateTestBlockEndTracing(index, filePath, call.line);
|
|
1372
|
-
const hasCallMatch = existingMatches.some(m =>
|
|
1373
|
-
m.line >= call.line && m.line <= endLine &&
|
|
1374
|
-
m.matchType === 'call'
|
|
1375
|
-
);
|
|
1376
|
-
if (!hasCallMatch) continue;
|
|
1377
|
-
if (!fileMatches.has(funcName)) fileMatches.set(funcName, []);
|
|
1378
|
-
fileMatches.get(funcName).push({
|
|
1379
|
-
line: call.line, content: lineContent.trim(),
|
|
1380
|
-
matchType: 'test-case', functionName: funcName
|
|
1381
|
-
});
|
|
1382
|
-
existingLines.add(call.line);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
} else {
|
|
1386
|
-
if (!fileEntry.symbols) return;
|
|
1387
|
-
try {
|
|
1388
|
-
const langModule = getLanguageAdapter(lang);
|
|
1389
|
-
if (!langModule) return;
|
|
1390
|
-
// Prefer the kinded predicate so we don't mis-tag fn main() / fn init()
|
|
1391
|
-
// (runtime entries) as test cases (BUG-CX). Fall back to isEntryPoint
|
|
1392
|
-
// for backward compat with language modules that haven't been migrated.
|
|
1393
|
-
const classify = langModule.getEntryPointKind
|
|
1394
|
-
? (s) => langModule.getEntryPointKind(s) === 'test'
|
|
1395
|
-
: langModule.isEntryPoint;
|
|
1396
|
-
if (!classify) return;
|
|
1397
|
-
for (const symbol of fileEntry.symbols) {
|
|
1398
|
-
if (!classify(symbol)) continue;
|
|
1399
|
-
// Only add test-case if a call match exists in the test body
|
|
1400
|
-
const hasCallInRange = existingMatches.some(m =>
|
|
1401
|
-
m.line >= symbol.startLine && m.line <= symbol.endLine &&
|
|
1402
|
-
m.matchType === 'call'
|
|
1403
|
-
);
|
|
1404
|
-
if (hasCallInRange && !existingLines.has(symbol.startLine)) {
|
|
1405
|
-
const lineContent = index.getLineContent(filePath, symbol.startLine);
|
|
1406
|
-
if (!fileMatches.has(funcName)) fileMatches.set(funcName, []);
|
|
1407
|
-
fileMatches.get(funcName).push({
|
|
1408
|
-
line: symbol.startLine, content: lineContent.trim(),
|
|
1409
|
-
matchType: 'test-case', functionName: funcName
|
|
1410
|
-
});
|
|
1411
|
-
existingLines.add(symbol.startLine);
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
} catch (e) { /* skip */ }
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
1492
|
/**
|
|
1419
1493
|
* Build instance type map for className scoping in affectedTests.
|
|
1420
1494
|
* Same logic as _buildInstanceTypeMap in search.js.
|
|
@@ -1476,26 +1550,6 @@ function _receiverMatchesClassTracing(usage, dispatchNames, instanceTypeMap, lin
|
|
|
1476
1550
|
return false;
|
|
1477
1551
|
}
|
|
1478
1552
|
|
|
1479
|
-
/**
|
|
1480
|
-
* Estimate the end line of a test block by tracking brace/paren nesting.
|
|
1481
|
-
*/
|
|
1482
|
-
function _estimateTestBlockEndTracing(index, filePath, startLine) {
|
|
1483
|
-
const content = index._readFile(filePath);
|
|
1484
|
-
if (!content) return startLine + 5;
|
|
1485
|
-
const lines = content.split('\n');
|
|
1486
|
-
let depth = 0;
|
|
1487
|
-
let started = false;
|
|
1488
|
-
for (let i = startLine - 1; i < lines.length; i++) {
|
|
1489
|
-
const line = lines[i];
|
|
1490
|
-
for (const ch of line) {
|
|
1491
|
-
if (ch === '{' || ch === '(') { depth++; started = true; }
|
|
1492
|
-
else if (ch === '}' || ch === ')') { depth--; }
|
|
1493
|
-
}
|
|
1494
|
-
if (started && depth <= 0) return i + 1;
|
|
1495
|
-
}
|
|
1496
|
-
return Math.min(startLine + 10, lines.length);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
1553
|
function _matchPriority(matchType) {
|
|
1500
1554
|
const p = { 'test-case': 5, 'call': 4, 'import': 3, 'string-ref': 2, 'reference': 1 };
|
|
1501
1555
|
return p[matchType] || 0;
|
package/core/verify.js
CHANGED
|
@@ -1215,7 +1215,7 @@ const GIT_REF_FORMAT = /^[a-zA-Z0-9._\-~\/^@{}:]+$/; // eslint-disable-line no-
|
|
|
1215
1215
|
*/
|
|
1216
1216
|
let baseReadSerial = 0;
|
|
1217
1217
|
|
|
1218
|
-
function startBaseRead(file, base) {
|
|
1218
|
+
function startBaseRead(file, base, repoFile = file) {
|
|
1219
1219
|
if (process.platform === 'win32' || !file || !GIT_REF_FORMAT.test(base)) return null;
|
|
1220
1220
|
const out = path.join(os.tmpdir(),
|
|
1221
1221
|
`ucn-base-${process.pid}-${++baseReadSerial}-${Math.random().toString(36).slice(2, 10)}`);
|
|
@@ -1225,8 +1225,8 @@ function startBaseRead(file, base) {
|
|
|
1225
1225
|
// marks a revision that does not hold the file.
|
|
1226
1226
|
child = spawn('/bin/sh', ['-c',
|
|
1227
1227
|
'git show "$1" > "$2.part" 2>/dev/null && mv "$2.part" "$2" || { rm -f "$2.part"; : > "$2.none"; }',
|
|
1228
|
-
'ucn-base', `${base}:./${path.basename(
|
|
1229
|
-
{ cwd: path.dirname(
|
|
1228
|
+
'ucn-base', `${base}:./${path.basename(repoFile)}`, out],
|
|
1229
|
+
{ cwd: path.dirname(repoFile), stdio: 'ignore' });
|
|
1230
1230
|
} catch {
|
|
1231
1231
|
child = null;
|
|
1232
1232
|
}
|
|
@@ -1299,8 +1299,11 @@ function baseDeclarationChanges(index, name, def, base = 'HEAD', baseRead = null
|
|
|
1299
1299
|
if (baseContent === null) continue;
|
|
1300
1300
|
if (baseContent === undefined) {
|
|
1301
1301
|
try {
|
|
1302
|
-
|
|
1303
|
-
|
|
1302
|
+
// The repository's copy of the file (a snapshot exported
|
|
1303
|
+
// from the git index is not a repository).
|
|
1304
|
+
const repoFile = index.repoPathOf ? index.repoPathOf(file) : file;
|
|
1305
|
+
baseContent = execFileSync('git', ['show', `${base}:./${path.basename(repoFile)}`], {
|
|
1306
|
+
cwd: path.dirname(repoFile), encoding: 'utf-8', maxBuffer: 256 * 1024 * 1024,
|
|
1304
1307
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1305
1308
|
});
|
|
1306
1309
|
} catch {
|
|
@@ -1848,6 +1851,9 @@ function analyzeCallSite(index, call, funcName, occurrence = 0) {
|
|
|
1848
1851
|
}
|
|
1849
1852
|
|
|
1850
1853
|
return {
|
|
1854
|
+
// Where the call is in the source: the same node, whoever reads
|
|
1855
|
+
// the file next (an alias, the second call on a line).
|
|
1856
|
+
span: { start: callNode.startIndex, end: callNode.endIndex },
|
|
1851
1857
|
args,
|
|
1852
1858
|
argCount: args.length,
|
|
1853
1859
|
hasSpread: args.some(a => a.startsWith('...')) || unpackingArgs > 0,
|
|
@@ -2199,8 +2205,9 @@ function verify(index, name, options = {}) {
|
|
|
2199
2205
|
// A base the user names must be a commit: a misspelled ref would read as
|
|
2200
2206
|
// a file absent at base and check nothing against the old declaration
|
|
2201
2207
|
// (the target-less check already resolved its base through the diff).
|
|
2202
|
-
|
|
2203
|
-
|
|
2208
|
+
const repoFile = index.repoPathOf ? index.repoPathOf(def.file) : def.file;
|
|
2209
|
+
if (options.base && options.checkBaseRef) assertGitCommit(repoFile, baseRef);
|
|
2210
|
+
const baseRead = startBaseRead(def.file, baseRef, repoFile);
|
|
2204
2211
|
let sweep;
|
|
2205
2212
|
try {
|
|
2206
2213
|
sweep = contractedCallerSweep(index, name, def, {
|
|
@@ -2274,6 +2281,15 @@ function verify(index, name, options = {}) {
|
|
|
2274
2281
|
};
|
|
2275
2282
|
}
|
|
2276
2283
|
|
|
2284
|
+
// A mismatch remembers which call it is (not part of what is printed).
|
|
2285
|
+
const located = (record, analysis) => {
|
|
2286
|
+
if (analysis && analysis.span) Object.defineProperty(record, 'span', { value: analysis.span, enumerable: false });
|
|
2287
|
+
// `f.call(self, ..)`: the arguments start one slot later.
|
|
2288
|
+
if (analysis && analysis.indirectKind) {
|
|
2289
|
+
Object.defineProperty(record, 'indirect', { value: analysis.indirectKind, enumerable: false });
|
|
2290
|
+
}
|
|
2291
|
+
return record;
|
|
2292
|
+
};
|
|
2277
2293
|
const verifyLineSeen = new Map(); // 'file:line' -> per-line ordinal (fix #231)
|
|
2278
2294
|
for (const call of calls) {
|
|
2279
2295
|
const siteKey = `${call.file}:${call.line}:${call.calledAs || name}`;
|
|
@@ -2364,7 +2380,7 @@ function verify(index, name, options = {}) {
|
|
|
2364
2380
|
continue;
|
|
2365
2381
|
}
|
|
2366
2382
|
if (!countOk || call.invalidOverload) {
|
|
2367
|
-
mismatches.push({
|
|
2383
|
+
mismatches.push(located({
|
|
2368
2384
|
file: call.relativePath,
|
|
2369
2385
|
line: call.line,
|
|
2370
2386
|
expression: call.content.trim(),
|
|
@@ -2378,7 +2394,7 @@ function verify(index, name, options = {}) {
|
|
|
2378
2394
|
args: analysis.args,
|
|
2379
2395
|
patterns: patternFlagsFrom(analysis),
|
|
2380
2396
|
...carry,
|
|
2381
|
-
});
|
|
2397
|
+
}, analysis));
|
|
2382
2398
|
continue;
|
|
2383
2399
|
}
|
|
2384
2400
|
|
|
@@ -2399,7 +2415,7 @@ function verify(index, name, options = {}) {
|
|
|
2399
2415
|
...carry,
|
|
2400
2416
|
});
|
|
2401
2417
|
} else {
|
|
2402
|
-
mismatches.push({
|
|
2418
|
+
mismatches.push(located({
|
|
2403
2419
|
file: call.relativePath,
|
|
2404
2420
|
line: call.line,
|
|
2405
2421
|
expression: call.content.trim(),
|
|
@@ -2413,7 +2429,7 @@ function verify(index, name, options = {}) {
|
|
|
2413
2429
|
problem: bindingProblems.join('; '),
|
|
2414
2430
|
patterns: patternFlagsFrom(analysis),
|
|
2415
2431
|
...carry,
|
|
2416
|
-
});
|
|
2432
|
+
}, analysis));
|
|
2417
2433
|
}
|
|
2418
2434
|
continue;
|
|
2419
2435
|
}
|
package/languages/adapter.js
CHANGED
|
@@ -147,13 +147,18 @@ function createLanguageAdapter(config) {
|
|
|
147
147
|
// safeParse returns the tree the extractors above just built.
|
|
148
148
|
if (config.traits?.reflectionApi && !parsed.reflectionSites) {
|
|
149
149
|
const { safeParse } = require('./index');
|
|
150
|
-
const {
|
|
150
|
+
const { reflectionFactsInTree } = require('../core/reflection');
|
|
151
151
|
// A language reading the file through its own view (C#
|
|
152
152
|
// conditional compilation, fix #391) supplies that tree.
|
|
153
153
|
const analysisTree = typeof languageModule.analysisTree === 'function'
|
|
154
154
|
? languageModule.analysisTree(code, parser) : safeParse(parser, code);
|
|
155
|
-
const
|
|
156
|
-
if (sites.length > 0) parsed.reflectionSites = sites;
|
|
155
|
+
const facts = reflectionFactsInTree(analysisTree, config.name, code, { imports });
|
|
156
|
+
if (facts.sites.length > 0) parsed.reflectionSites = facts.sites;
|
|
157
|
+
// How often the file uses the reflection and code-execution
|
|
158
|
+
// vocabulary: what `repo` and the completeness footer count,
|
|
159
|
+
// read from the index instead of from every file's text.
|
|
160
|
+
if (facts.reflectionUses > 0) parsed.reflectionUses = facts.reflectionUses;
|
|
161
|
+
if (facts.evalUses > 0) parsed.evalUses = facts.evalUses;
|
|
157
162
|
}
|
|
158
163
|
// Computed dispatch sites (`handlers[name]()`) of the literal
|
|
159
164
|
// parse the doctor's blind-spot inventory reads (fix #436): a
|
package/languages/c-family.js
CHANGED
|
@@ -3633,6 +3633,17 @@ function memberQualifiers(declarator) {
|
|
|
3633
3633
|
return out;
|
|
3634
3634
|
}
|
|
3635
3635
|
|
|
3636
|
+
/**
|
|
3637
|
+
* `virtual R f(...) = 0;`: a member function declaration whose initializer
|
|
3638
|
+
* is the literal 0 is pure virtual. A concrete derived class must override
|
|
3639
|
+
* it. (A data member `int x = 0;` has no function declarator and never
|
|
3640
|
+
* reaches the caller.)
|
|
3641
|
+
*/
|
|
3642
|
+
function isPureVirtual(node) {
|
|
3643
|
+
const value = templatedDeclaration(node).childForFieldName('default_value');
|
|
3644
|
+
return !!value && value.type === 'number_literal' && value.text === '0';
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3636
3647
|
function memberFromNode(node, className, access, lines, mode) {
|
|
3637
3648
|
const declarator = functionDeclarator(node);
|
|
3638
3649
|
if (!declarator) return null;
|
|
@@ -3662,6 +3673,7 @@ function memberFromNode(node, className, access, lines, mode) {
|
|
|
3662
3673
|
memberType: isConstructor ? 'constructor' : 'method',
|
|
3663
3674
|
isMethod: true,
|
|
3664
3675
|
isConstructor,
|
|
3676
|
+
...(mode === 'cpp' && !isConstructor && isPureVirtual(node) && { requiredMember: true }),
|
|
3665
3677
|
...(mode === 'cpp' && memberQualifiers(declarator)),
|
|
3666
3678
|
...(enclosingNamespace(node) && {
|
|
3667
3679
|
namespace: enclosingNamespace(node),
|