ucn 5.4.2 → 5.5.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 +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +40 -14
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/rust-modules.js - Rust module tree (fix #377).
|
|
3
|
+
*
|
|
4
|
+
* Every Rust source file belongs to exactly one module of one crate: crate
|
|
5
|
+
* roots are the files no `mod` declaration loads, and a declared file's
|
|
6
|
+
* module path is its parent's path plus the enclosing inline modules plus
|
|
7
|
+
* the declared name. Inline `mod NAME { ... }` blocks are modules too.
|
|
8
|
+
*
|
|
9
|
+
* `rustPathModule` resolves the qualifier of a path call (`crate::a::b`,
|
|
10
|
+
* `super::super`, `self::inner`, `net::conn`) from the call site's own
|
|
11
|
+
* module to the module entries (file + inline chain) that hold its items;
|
|
12
|
+
* `rustModuleItems` lists the definitions of a name declared directly in
|
|
13
|
+
* those modules. Anything the tree cannot prove (a `use` alias, an extern
|
|
14
|
+
* crate, a type, a re-export) resolves to null: the caller keeps its other
|
|
15
|
+
* evidence rules.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { rustModDeclarationFiles, rustInlineModules, rustInlineChainAt } = require('./imports');
|
|
21
|
+
|
|
22
|
+
const FUNCTION_KINDS = new Set(['function', 'method', 'constructor', 'static']);
|
|
23
|
+
|
|
24
|
+
function isRustEntry(entry) {
|
|
25
|
+
return entry?.language === 'rust';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function moduleTree(index) {
|
|
29
|
+
const memo = index._rustScopeMemo || (index._rustScopeMemo = new Map());
|
|
30
|
+
const cached = memo.get('\x01rustModuleTree');
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
const parents = new Map(); // child file -> { file, chain, name }
|
|
33
|
+
const inlineByFile = new Map();
|
|
34
|
+
const exists = candidate => isRustEntry(index.files.get(candidate));
|
|
35
|
+
for (const [file, entry] of index.files) {
|
|
36
|
+
if (!isRustEntry(entry)) continue;
|
|
37
|
+
const inline = rustInlineModules(entry);
|
|
38
|
+
inlineByFile.set(file, inline);
|
|
39
|
+
const declared = rustModDeclarationFiles(file, entry, exists, inline);
|
|
40
|
+
if (declared.size === 0) continue;
|
|
41
|
+
for (const detail of entry.importDetails || []) {
|
|
42
|
+
if (detail.type !== 'mod' || !declared.has(detail.module)) continue;
|
|
43
|
+
const chain = rustInlineChainAt(inline, detail.line);
|
|
44
|
+
for (const child of declared.get(detail.module)) {
|
|
45
|
+
if (child === file || parents.has(child)) continue;
|
|
46
|
+
parents.set(child, { file, chain, name: detail.module });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const fileModules = new Map(); // file -> { root, segs }
|
|
51
|
+
const moduleOf = (file, seen = new Set()) => {
|
|
52
|
+
const hit = fileModules.get(file);
|
|
53
|
+
if (hit) return hit;
|
|
54
|
+
const parent = parents.get(file);
|
|
55
|
+
let result;
|
|
56
|
+
if (!parent || seen.has(file)) {
|
|
57
|
+
result = { root: file, segs: [] };
|
|
58
|
+
} else {
|
|
59
|
+
seen.add(file);
|
|
60
|
+
const up = moduleOf(parent.file, seen);
|
|
61
|
+
result = { root: up.root, segs: [...up.segs, ...parent.chain, parent.name] };
|
|
62
|
+
}
|
|
63
|
+
fileModules.set(file, result);
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
const entries = new Map(); // root \0 path -> [{ file, chain }]
|
|
67
|
+
const add = (root, segs, file, chain) => {
|
|
68
|
+
const key = `${root}\0${segs.join('::')}`;
|
|
69
|
+
if (!entries.has(key)) entries.set(key, []);
|
|
70
|
+
entries.get(key).push({ file, chain });
|
|
71
|
+
};
|
|
72
|
+
for (const [file, inline] of inlineByFile) {
|
|
73
|
+
const own = moduleOf(file);
|
|
74
|
+
add(own.root, own.segs, file, []);
|
|
75
|
+
for (const mod of inline) {
|
|
76
|
+
const chain = rustInlineChainAt(inline, mod.startLine);
|
|
77
|
+
add(own.root, [...own.segs, ...chain], file, chain);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const tree = { moduleOf, entries, inlineByFile };
|
|
81
|
+
memo.set('\x01rustModuleTree', tree);
|
|
82
|
+
return tree;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Module entries a path qualifier names from (filePath, line), or null when
|
|
87
|
+
* the qualifier is not provably a module path of this crate.
|
|
88
|
+
* @returns {{ root: string, segs: string[], entries: Array<{file: string, chain: string[]}> }|null}
|
|
89
|
+
*/
|
|
90
|
+
function rustPathModule(index, filePath, line, qualifier) {
|
|
91
|
+
if (!isRustEntry(index.files.get(filePath)) || !qualifier) return null;
|
|
92
|
+
const segments = String(qualifier).split('::');
|
|
93
|
+
if (segments.some(segment => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment))) return null;
|
|
94
|
+
const tree = moduleTree(index);
|
|
95
|
+
const own = tree.moduleOf(filePath);
|
|
96
|
+
const current = [...own.segs, ...rustInlineChainAt(tree.inlineByFile.get(filePath) || [], line)];
|
|
97
|
+
const has = segs => tree.entries.has(`${own.root}\0${segs.join('::')}`);
|
|
98
|
+
let segs;
|
|
99
|
+
let i = 0;
|
|
100
|
+
if (segments[0] === 'crate') {
|
|
101
|
+
segs = [];
|
|
102
|
+
i = 1;
|
|
103
|
+
} else if (segments[0] === 'self' || segments[0] === 'super') {
|
|
104
|
+
segs = current;
|
|
105
|
+
if (segments[0] === 'self') i = 1;
|
|
106
|
+
while (segments[i] === 'super') {
|
|
107
|
+
if (segs.length === 0) return null;
|
|
108
|
+
segs = segs.slice(0, -1);
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// A leading plain name is a child module of the current module; a
|
|
113
|
+
// `use` binding, extern crate, or type is not the tree's to prove.
|
|
114
|
+
segs = current;
|
|
115
|
+
}
|
|
116
|
+
for (; i < segments.length; i++) {
|
|
117
|
+
const next = [...segs, segments[i]];
|
|
118
|
+
if (!has(next)) return null;
|
|
119
|
+
segs = next;
|
|
120
|
+
}
|
|
121
|
+
return { root: own.root, segs, entries: tree.entries.get(`${own.root}\0${segs.join('::')}`) || [] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Definitions of `name` declared as items directly in the resolved module
|
|
126
|
+
* (not inside an impl, trait, nested inline module, or function body).
|
|
127
|
+
*/
|
|
128
|
+
function rustModuleItems(index, resolved, name) {
|
|
129
|
+
if (!resolved?.entries?.length) return [];
|
|
130
|
+
const tree = moduleTree(index);
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const def of index.symbols.get(name) || []) {
|
|
133
|
+
if (def.className || def.receiver || def.type === 'module' || def.type === 'field' ||
|
|
134
|
+
def.type === 'macro' || def.type === 'impl') continue;
|
|
135
|
+
const entry = resolved.entries.find(candidate => candidate.file === def.file);
|
|
136
|
+
if (!entry) continue;
|
|
137
|
+
const chain = rustInlineChainAt(tree.inlineByFile.get(def.file) || [], def.startLine);
|
|
138
|
+
if (chain.length !== entry.chain.length || chain.some((part, k) => part !== entry.chain[k])) continue;
|
|
139
|
+
const fileEntry = index.files.get(def.file);
|
|
140
|
+
const nested = (fileEntry?.symbols || []).some(symbol => symbol !== def &&
|
|
141
|
+
FUNCTION_KINDS.has(symbol.type) && symbol.startLine <= def.startLine &&
|
|
142
|
+
symbol.endLine >= def.endLine && !(symbol.startLine === def.startLine && symbol.name === def.name));
|
|
143
|
+
if (nested) continue;
|
|
144
|
+
out.push(def);
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The crate root and module path of a source position (fix #399), or null
|
|
151
|
+
* for a file that is not Rust.
|
|
152
|
+
* @returns {{ root: string, segs: string[] }|null}
|
|
153
|
+
*/
|
|
154
|
+
function rustModulePathAt(index, filePath, line) {
|
|
155
|
+
if (!isRustEntry(index.files.get(filePath))) return null;
|
|
156
|
+
const tree = moduleTree(index);
|
|
157
|
+
const own = tree.moduleOf(filePath);
|
|
158
|
+
return { root: own.root, segs: [...own.segs, ...rustInlineChainAt(tree.inlineByFile.get(filePath) || [], line)] };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Is an item with Rust visibility `modifiers`, declared at (file, line),
|
|
163
|
+
* accessible at (siteFile, siteLine)? `pub` items are; `pub(crate)` ones
|
|
164
|
+
* within their crate; private, `pub(self)` and `pub(super)` items within
|
|
165
|
+
* the module tree under their module (or its parent). `pub(in path)` and
|
|
166
|
+
* unknown layouts are not provable (false).
|
|
167
|
+
*/
|
|
168
|
+
function rustItemAccessible(index, modifiers, file, line, siteFile, siteLine) {
|
|
169
|
+
const mods = (modifiers || []).map(m => String(m).replace(/\s+/g, ''));
|
|
170
|
+
if (mods.includes('pub')) return true;
|
|
171
|
+
const item = rustModulePathAt(index, file, line);
|
|
172
|
+
const site = rustModulePathAt(index, siteFile, siteLine);
|
|
173
|
+
if (!item || !site || item.root !== site.root) return false;
|
|
174
|
+
if (mods.includes('pub(crate)')) return true;
|
|
175
|
+
let scope = item.segs;
|
|
176
|
+
if (mods.includes('pub(super)')) {
|
|
177
|
+
if (scope.length === 0) return false;
|
|
178
|
+
scope = scope.slice(0, -1);
|
|
179
|
+
} else if (mods.some(m => m.startsWith('pub(') && m !== 'pub(self)')) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return scope.length <= site.segs.length && scope.every((seg, i) => site.segs[i] === seg);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { rustPathModule, rustModuleItems, rustModulePathAt, rustItemAccessible };
|
package/core/search.js
CHANGED
|
@@ -13,8 +13,10 @@ const { isTestFile } = require('./discovery');
|
|
|
13
13
|
const { detectLanguage, getParser, getLanguageAdapter, langTraits } = require('../languages');
|
|
14
14
|
const { getCachedCalls } = require('./callers');
|
|
15
15
|
const { extractImports } = require('./imports');
|
|
16
|
+
const { isAccessorDefinition, findAccessorReferences } = require('./accessors');
|
|
16
17
|
const isSafeRegex = require('safe-regex2');
|
|
17
18
|
const { RE2JS } = require('re2js');
|
|
19
|
+
const { UcnError } = require('./errors');
|
|
18
20
|
|
|
19
21
|
// Keep the established search-input contract for the canonical catastrophic
|
|
20
22
|
// nested-single-atom family even though the RE2-compatible engine could run it
|
|
@@ -331,7 +333,10 @@ function usages(index, name, options = {}) {
|
|
|
331
333
|
const lines = content.split('\n');
|
|
332
334
|
|
|
333
335
|
// Try AST-based detection first (with per-operation cache)
|
|
334
|
-
|
|
336
|
+
// Refactoring internals also read each bare reference's lexical
|
|
337
|
+
// scope verdict (fix #392).
|
|
338
|
+
const astUsages = index._getCachedUsages(filePath, name,
|
|
339
|
+
options.internalEvidence ? { lexicalScopes: true } : undefined);
|
|
335
340
|
if (astUsages !== null) {
|
|
336
341
|
for (const u of astUsages) {
|
|
337
342
|
// Skip if this is a definition line (already added above)
|
|
@@ -372,6 +377,11 @@ function usages(index, name, options = {}) {
|
|
|
372
377
|
...(Number.isInteger(u.column) && { column: u.column }),
|
|
373
378
|
...(u.receiverIsModule && { receiverIsModule: true }),
|
|
374
379
|
...(u.receiverLocalBinding && { receiverLocalBinding: true }),
|
|
380
|
+
...(u.namespace && { namespace: u.namespace }),
|
|
381
|
+
...(u.scopeBinding && { scopeBinding: u.scopeBinding }),
|
|
382
|
+
...(u.scopeDefLines && { scopeDefLines: u.scopeDefLines }),
|
|
383
|
+
...(u.shorthandProperty && { shorthandProperty: true }),
|
|
384
|
+
...(u.importAlias && { importAlias: true }),
|
|
375
385
|
}),
|
|
376
386
|
...(callerSym && {
|
|
377
387
|
callerName: callerSym.name,
|
|
@@ -476,13 +486,23 @@ function usages(index, name, options = {}) {
|
|
|
476
486
|
|
|
477
487
|
// Deduplicate same-file, same-line, same-usageType entries
|
|
478
488
|
// (e.g., `detectLanguage: parser.detectLanguage` has the name twice on one line)
|
|
479
|
-
|
|
489
|
+
// Refactoring internals keep the other same-line records on the kept
|
|
490
|
+
// one (`sameLine`): every token of a line is decided (fix #392).
|
|
491
|
+
const seen = new Map();
|
|
480
492
|
const deduped = [];
|
|
481
493
|
for (const u of usagesList) {
|
|
482
494
|
const key = `${u.file}:${u.line}:${u.usageType}:${u.isDefinition}`;
|
|
483
495
|
if (!seen.has(key)) {
|
|
484
|
-
seen.
|
|
496
|
+
seen.set(key, u);
|
|
485
497
|
deduped.push(u);
|
|
498
|
+
} else if (options.internalEvidence && !u.isDefinition) {
|
|
499
|
+
const kept = seen.get(key);
|
|
500
|
+
if (!kept.sameLine) {
|
|
501
|
+
Object.defineProperty(kept, 'sameLine', {
|
|
502
|
+
value: [], enumerable: false, writable: true, configurable: true,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
kept.sameLine.push(u);
|
|
486
506
|
}
|
|
487
507
|
}
|
|
488
508
|
return deduped;
|
|
@@ -510,14 +530,14 @@ function search(index, term, options = {}) {
|
|
|
510
530
|
let linearRegex = null;
|
|
511
531
|
if (useRegex) {
|
|
512
532
|
if (NESTED_SINGLE_ATOM_REPEAT.test(term)) {
|
|
513
|
-
throw new
|
|
533
|
+
throw new UcnError(
|
|
514
534
|
`Unsafe regular expression "${term}": nested repetition is not accepted. Simplify it or use ripgrep.`,
|
|
515
535
|
);
|
|
516
536
|
}
|
|
517
537
|
try {
|
|
518
538
|
regex = new RegExp(term, regexFlags);
|
|
519
539
|
} catch (e) {
|
|
520
|
-
throw new
|
|
540
|
+
throw new UcnError(
|
|
521
541
|
`Invalid regular expression "${term}": ${e.message}`,
|
|
522
542
|
{ cause: e },
|
|
523
543
|
);
|
|
@@ -533,7 +553,7 @@ function search(index, term, options = {}) {
|
|
|
533
553
|
linearRegex = RE2JS.compile(term, re2Flags);
|
|
534
554
|
} catch (re2Error) {
|
|
535
555
|
if (!isSafeRegex(term)) {
|
|
536
|
-
throw new
|
|
556
|
+
throw new UcnError(
|
|
537
557
|
`Unsafe regular expression "${term}": this JavaScript-only pattern cannot run in UCN's linear-time engine and its repetition shape may backtrack catastrophically. Simplify it or use ripgrep.`,
|
|
538
558
|
{ cause: re2Error },
|
|
539
559
|
);
|
|
@@ -1423,7 +1443,10 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1423
1443
|
// Fast pre-check: skip if searchTerm doesn't appear in file
|
|
1424
1444
|
if (!isFilePath && !content.includes(searchTerm) && !linkedRecords) continue;
|
|
1425
1445
|
if (isFilePath && !linkedRecords && provenFileSites.length === 0) continue;
|
|
1426
|
-
|
|
1446
|
+
// A test importing the name through any chain of re-exports and
|
|
1447
|
+
// renames the caller engine follows is linked too (fix #400).
|
|
1448
|
+
const sourceFileLinked = !sourceFileFilter || sourceFileFilter.has(testPath) ||
|
|
1449
|
+
_importBindingReachesTargets(index, entry, searchTerm, targetDefs);
|
|
1427
1450
|
// A class-qualified query can still identify a possible test link
|
|
1428
1451
|
// in source layouts that have no modeled module edge (notably
|
|
1429
1452
|
// Rust macro/test fixtures). Keep it visibly unverified when the
|
|
@@ -1499,9 +1522,12 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1499
1522
|
// own definition (fix #246).
|
|
1500
1523
|
if (localShadow && !usage.receiver && usage.usageType !== 'import') continue;
|
|
1501
1524
|
|
|
1525
|
+
// A line is claimed only by a usage that is recorded: two
|
|
1526
|
+
// same-kind tokens on one line (`a.m = b.m`) are judged
|
|
1527
|
+
// separately, so a rejected first token never hides a
|
|
1528
|
+
// matching second one.
|
|
1502
1529
|
const lineKey = `${usage.line}:${usage.usageType}`;
|
|
1503
1530
|
if (seenLines.has(lineKey)) continue;
|
|
1504
|
-
seenLines.add(lineKey);
|
|
1505
1531
|
|
|
1506
1532
|
const lineContent = index.getLineContent(testPath, usage.line);
|
|
1507
1533
|
|
|
@@ -1529,6 +1555,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1529
1555
|
// where the receiver matches the target class.
|
|
1530
1556
|
if (!usage.receiver && matchType !== 'unverified-reference') continue;
|
|
1531
1557
|
if (!usage.receiver && matchType === 'unverified-reference') {
|
|
1558
|
+
seenLines.add(lineKey);
|
|
1532
1559
|
matches.push({
|
|
1533
1560
|
line: usage.line,
|
|
1534
1561
|
content: lineContent.trim(),
|
|
@@ -1544,6 +1571,7 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1544
1571
|
}
|
|
1545
1572
|
}
|
|
1546
1573
|
|
|
1574
|
+
seenLines.add(lineKey);
|
|
1547
1575
|
matches.push({
|
|
1548
1576
|
line: usage.line,
|
|
1549
1577
|
content: lineContent.trim(),
|
|
@@ -1676,28 +1704,26 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1676
1704
|
const info = testInfo.get(site.file);
|
|
1677
1705
|
if (!info) continue;
|
|
1678
1706
|
if (info.testRanges && !lineInRanges(site.line, info.testRanges)) continue;
|
|
1679
|
-
//
|
|
1680
|
-
//
|
|
1681
|
-
//
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1688
|
-
// target. Use the parser's call-kind bit rather than requiring a
|
|
1689
|
-
// textual receiver: fluent chains such as `.addStaticBlock(...)`
|
|
1690
|
-
// have a call receiver but no simple receiver identifier. Rust
|
|
1691
|
-
// proc-macro attribute references are handled by the AST usage
|
|
1692
|
-
// branch above as explicitly unverified references.
|
|
1693
|
-
if (className && !site.isMethod) continue;
|
|
1707
|
+
// Every site here was adjudicated against the exact target
|
|
1708
|
+
// definition by the caller engine (fix #402), so neither the file
|
|
1709
|
+
// layout nor the call shape is a second identity test: a test
|
|
1710
|
+
// reaches the target through a fixture-injected or inherited
|
|
1711
|
+
// receiver without importing the defining file, and a
|
|
1712
|
+
// receiver-less call reaches a method through implicit `this`, a
|
|
1713
|
+
// static import, a destructured member or a macro-generated test
|
|
1714
|
+
// body, exactly when the engine says so and in the tier it says.
|
|
1715
|
+
// Unverified sites stay visibly tiered; they are not promoted.
|
|
1694
1716
|
if (excludeArr.length > 0 &&
|
|
1695
1717
|
!index.matchesFilters(info.entry.relativePath, { exclude: excludeArr })) continue;
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1718
|
+
if (site._testEvidenceTier !== 'confirmed') {
|
|
1719
|
+
// An unverified bare call in a test file that defines the
|
|
1720
|
+
// name itself most likely binds that local definition.
|
|
1721
|
+
const localSameName = (info.entry.symbols || []).some(s => s.name === searchTerm);
|
|
1722
|
+
const importsTargetName = (info.entry.importBindings || []).some(b => b.name === searchTerm);
|
|
1723
|
+
const explicitlyScopedToLocal = !!options.file && targetDefs.some(d => d.file === site.file);
|
|
1724
|
+
if (localSameName && !importsTargetName && !explicitlyScopedToLocal &&
|
|
1725
|
+
!site.receiver && !site.isMethod) continue;
|
|
1726
|
+
}
|
|
1701
1727
|
|
|
1702
1728
|
let fileResult = results.find(r => r.file === info.entry.relativePath);
|
|
1703
1729
|
if (!fileResult) {
|
|
@@ -1718,6 +1744,50 @@ function tests(index, nameOrFile, options = {}) {
|
|
|
1718
1744
|
});
|
|
1719
1745
|
}
|
|
1720
1746
|
|
|
1747
|
+
// Accessor targets (fix #398F): a property is consumed by reads and
|
|
1748
|
+
// writes, never call syntax, so the caller supplement above sees
|
|
1749
|
+
// none of `result.stdout`. Test-file accessor references whose
|
|
1750
|
+
// receiver identity the accessor engine proves are test links; the
|
|
1751
|
+
// unverified ones join only for a globally unique name, as above.
|
|
1752
|
+
const accessorDefs = targetDefs.filter(isAccessorDefinition);
|
|
1753
|
+
if (accessorDefs.length > 0) {
|
|
1754
|
+
const testPaths = new Set(testFiles.map(t => t.path));
|
|
1755
|
+
const globallyUnique = index.find(searchTerm, { exact: true }).length === 1;
|
|
1756
|
+
for (const def of accessorDefs) {
|
|
1757
|
+
const refs = findAccessorReferences(index, searchTerm, def, { files: testPaths });
|
|
1758
|
+
if (!refs) continue;
|
|
1759
|
+
const rows = [
|
|
1760
|
+
...refs.confirmed.map(r => ({ ...r, tierName: 'confirmed' })),
|
|
1761
|
+
...(globallyUnique ? refs.unverified.map(r => ({ ...r, tierName: 'unverified' })) : []),
|
|
1762
|
+
];
|
|
1763
|
+
for (const ref of rows) {
|
|
1764
|
+
const info = testInfo.get(ref.absoluteFile);
|
|
1765
|
+
if (!info) continue;
|
|
1766
|
+
if (info.testRanges && !lineInRanges(ref.line, info.testRanges)) continue;
|
|
1767
|
+
if (excludeArr.length > 0 &&
|
|
1768
|
+
!index.matchesFilters(info.entry.relativePath, { exclude: excludeArr })) continue;
|
|
1769
|
+
let fileResult = results.find(r => r.file === info.entry.relativePath);
|
|
1770
|
+
if (!fileResult) {
|
|
1771
|
+
fileResult = { file: info.entry.relativePath, matches: [] };
|
|
1772
|
+
results.push(fileResult);
|
|
1773
|
+
}
|
|
1774
|
+
const matchType = ref.tierName === 'confirmed' ? 'reference' : 'unverified-reference';
|
|
1775
|
+
if (fileResult.matches.some(m => m.line === ref.line &&
|
|
1776
|
+
['reference', 'unverified-reference', 'call', 'unverified-call'].includes(m.matchType))) continue;
|
|
1777
|
+
fileResult.matches = fileResult.matches.filter(m =>
|
|
1778
|
+
!(m.line === ref.line && m.matchType === 'test-case'));
|
|
1779
|
+
fileResult.matches.push({
|
|
1780
|
+
line: ref.line,
|
|
1781
|
+
content: ref.expression,
|
|
1782
|
+
matchType,
|
|
1783
|
+
evidenceTier: ref.tierName,
|
|
1784
|
+
...(ref.resolution && { resolution: ref.resolution }),
|
|
1785
|
+
...(ref.reason && { reason: ref.reason }),
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1721
1791
|
for (const fileResult of results) {
|
|
1722
1792
|
fileResult.matches.sort((a, b) => (a.line || 0) - (b.line || 0) ||
|
|
1723
1793
|
codeUnitCompare(a.matchType || '', b.matchType || ''));
|
|
@@ -1903,6 +1973,23 @@ function _buildSourceFileImporters(index, defs) {
|
|
|
1903
1973
|
return importers;
|
|
1904
1974
|
}
|
|
1905
1975
|
|
|
1976
|
+
/** Does an import binding of `name` in this file reach a target
|
|
1977
|
+
* definition's file under that name (fix #400)? The caller engine's
|
|
1978
|
+
* name-level export chase. */
|
|
1979
|
+
function _importBindingReachesTargets(index, fileEntry, name, targetDefs) {
|
|
1980
|
+
if (!targetDefs || targetDefs.length === 0) return false;
|
|
1981
|
+
const targetFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
1982
|
+
const { _nameBindingReaches } = require('./callers');
|
|
1983
|
+
for (const binding of fileEntry.importBindings || []) {
|
|
1984
|
+
if ((binding.alias || binding.name) !== name || binding.module == null) continue;
|
|
1985
|
+
const rel = fileEntry.moduleResolved?.[binding.module];
|
|
1986
|
+
if (!rel) continue;
|
|
1987
|
+
const abs = path.isAbsolute(rel) ? rel : path.join(index.root, rel);
|
|
1988
|
+
if (_nameBindingReaches(index, abs, binding.name, targetFiles, 8, { exactName: true }) === 'yes') return true;
|
|
1989
|
+
}
|
|
1990
|
+
return false;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1906
1993
|
/**
|
|
1907
1994
|
* Check if a file re-exports a symbol from a source file.
|
|
1908
1995
|
* Handles: named re-exports, `module.exports = require(...)` blanket re-exports,
|
|
@@ -1933,7 +2020,12 @@ function _fileReExportsSymbol(index, fileEntry, symbolName, sourceAbsPath) {
|
|
|
1933
2020
|
!exp.name || exp.type === 'module.exports' || exp.type === 're-export' || exp.type === 'export-all'
|
|
1934
2021
|
);
|
|
1935
2022
|
if (hasBlanketExport) return true;
|
|
1936
|
-
|
|
2023
|
+
// `export * from './m'` re-exports every name of the module it names
|
|
2024
|
+
// (fix #400): it carries the symbol on when that module is the file the
|
|
2025
|
+
// chain came from.
|
|
2026
|
+
return details.some(exp => exp.type === 're-export-all' && exp.source &&
|
|
2027
|
+
fileEntry.moduleResolved?.[exp.source] &&
|
|
2028
|
+
path.join(index.root, fileEntry.moduleResolved[exp.source]) === sourceAbsPath);
|
|
1937
2029
|
}
|
|
1938
2030
|
|
|
1939
2031
|
/**
|
package/core/shared.js
CHANGED
|
@@ -59,7 +59,10 @@ function codeUnitCompare(a, b) {
|
|
|
59
59
|
*/
|
|
60
60
|
function isTestPath(rp) {
|
|
61
61
|
if (!rp) return false;
|
|
62
|
-
|
|
62
|
+
// A `.h` header tests the same under the C and C++ conventions (neither
|
|
63
|
+
// has a `.spec` form, only C's patterns name `.h`): no header-language
|
|
64
|
+
// detection, which walks the project directory (fix #396).
|
|
65
|
+
const language = path.extname(rp).toLowerCase() === '.h' ? 'c' : detectLanguage(rp);
|
|
63
66
|
const { langTraits } = require('../languages');
|
|
64
67
|
const specConvention = langTraits(language)?.testFileCandidates?.('sample', '')
|
|
65
68
|
.some(candidate => candidate.includes('.spec'));
|
|
@@ -484,6 +487,20 @@ function maskBlockComments(content, language) {
|
|
|
484
487
|
return out.join('');
|
|
485
488
|
}
|
|
486
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Is a definition invoked through the macro namespace (fix #377)? A Rust
|
|
492
|
+
* `macro_rules!` macro, or a procedural macro function (`#[proc_macro]`,
|
|
493
|
+
* `#[proc_macro_attribute]`, `#[proc_macro_derive]`). C/C++ preprocessor
|
|
494
|
+
* macros are type 'macro' too; their invocations are ordinary call syntax.
|
|
495
|
+
*/
|
|
496
|
+
function isMacroNamespaceDefinition(def) {
|
|
497
|
+
if (!def) return false;
|
|
498
|
+
if (def.type === 'macro') return true;
|
|
499
|
+
const modifiers = def.modifiers || [];
|
|
500
|
+
return modifiers.includes('proc_macro') || modifiers.includes('proc_macro_attribute') ||
|
|
501
|
+
modifiers.includes('proc_macro_derive');
|
|
502
|
+
}
|
|
503
|
+
|
|
487
504
|
module.exports = { literalNameRegex,
|
|
488
505
|
pickBestDefinition,
|
|
489
506
|
addTestExclusions,
|
|
@@ -503,4 +520,5 @@ module.exports = { literalNameRegex,
|
|
|
503
520
|
hasTextBlindspots,
|
|
504
521
|
countTextBlindspots,
|
|
505
522
|
isPathInsideRoot,
|
|
523
|
+
isMacroNamespaceDefinition,
|
|
506
524
|
};
|