ucn 5.4.2 → 5.5.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 +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 +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- 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 +529 -31
- package/core/callers.js +12228 -921
- 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 +472 -66
- 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 +3026 -0
- package/core/rust-modules.js +203 -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 +1185 -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 +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/core/provenance-facts.js
CHANGED
|
@@ -6,6 +6,7 @@ const { NON_CALLABLE_TYPES } = require('./shared');
|
|
|
6
6
|
const { declarationIdentity, identityKey, propertyReadMember } = require('./provenance');
|
|
7
7
|
const { captureOverload, overloadMemberGroup } = require('./provenance-overload');
|
|
8
8
|
const { splitParentList } = require('./graph-build');
|
|
9
|
+
const { distinctOwnerDefinitions } = require('./class-identity');
|
|
9
10
|
|
|
10
11
|
const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'record', 'enum', 'type', 'impl']);
|
|
11
12
|
const ownerName = definition => definition.className || (definition.receiver || '').replace(/^\*/, '');
|
|
@@ -24,6 +25,72 @@ function occurrenceIdentity(file, call, siteId) {
|
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Callable same-name definitions owned by a type, in symbol-table order.
|
|
30
|
+
* Within an operation the name's definitions are grouped once (fix #382):
|
|
31
|
+
* an exact HOT ranking resolves every site of a name once per same-name
|
|
32
|
+
* definition, and a filter over thousands of definitions per site made the
|
|
33
|
+
* ranking quadratic.
|
|
34
|
+
*/
|
|
35
|
+
function callableMembersOwnedBy(index, name, owner) {
|
|
36
|
+
const definitions = index.symbols.get(name) || [];
|
|
37
|
+
const memo = index._opMemo?.('callableMembersByOwner', () => new WeakMap());
|
|
38
|
+
if (!memo) {
|
|
39
|
+
return definitions.filter(d => !NON_CALLABLE_TYPES.has(d.type) && ownerName(d) === owner);
|
|
40
|
+
}
|
|
41
|
+
let byOwner = memo.get(definitions);
|
|
42
|
+
if (!byOwner) {
|
|
43
|
+
byOwner = new Map();
|
|
44
|
+
for (const d of definitions) {
|
|
45
|
+
if (NON_CALLABLE_TYPES.has(d.type)) continue;
|
|
46
|
+
const key = ownerName(d);
|
|
47
|
+
let list = byOwner.get(key);
|
|
48
|
+
if (!list) {
|
|
49
|
+
list = [];
|
|
50
|
+
byOwner.set(key, list);
|
|
51
|
+
}
|
|
52
|
+
list.push(d);
|
|
53
|
+
}
|
|
54
|
+
memo.set(definitions, byOwner);
|
|
55
|
+
}
|
|
56
|
+
return byOwner.get(owner) || [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Same-name definitions declared in one file, in symbol-table order. */
|
|
60
|
+
function definitionsInFile(index, name, file) {
|
|
61
|
+
const definitions = index.symbols.get(name) || [];
|
|
62
|
+
const memo = index._opMemo?.('definitionsByFile', () => new WeakMap());
|
|
63
|
+
if (!memo) return definitions.filter(d => d.file === file);
|
|
64
|
+
let byFile = memo.get(definitions);
|
|
65
|
+
if (!byFile) {
|
|
66
|
+
byFile = new Map();
|
|
67
|
+
for (const d of definitions) {
|
|
68
|
+
let list = byFile.get(d.file);
|
|
69
|
+
if (!list) {
|
|
70
|
+
list = [];
|
|
71
|
+
byFile.set(d.file, list);
|
|
72
|
+
}
|
|
73
|
+
list.push(d);
|
|
74
|
+
}
|
|
75
|
+
memo.set(definitions, byFile);
|
|
76
|
+
}
|
|
77
|
+
return byFile.get(file) || [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The first same-name definition in a file starting at `line`, if any. */
|
|
81
|
+
function definitionAt(index, name, file, line) {
|
|
82
|
+
const inFile = definitionsInFile(index, name, file);
|
|
83
|
+
const memo = index._opMemo?.('definitionsByLine', () => new WeakMap());
|
|
84
|
+
if (!memo || inFile.length < 8) return inFile.find(d => d.startLine === line);
|
|
85
|
+
let byLine = memo.get(inFile);
|
|
86
|
+
if (!byLine) {
|
|
87
|
+
byLine = new Map();
|
|
88
|
+
for (const d of inFile) if (!byLine.has(d.startLine)) byLine.set(d.startLine, d);
|
|
89
|
+
memo.set(inFile, byLine);
|
|
90
|
+
}
|
|
91
|
+
return byLine.get(line);
|
|
92
|
+
}
|
|
93
|
+
|
|
27
94
|
/** Follow paired AST import/export names; an arbitrary file import is no hop. */
|
|
28
95
|
function namedDeclaration(index, file, name, accepts, line, seen = new Set()) {
|
|
29
96
|
const key = `${file}\0${name}`;
|
|
@@ -45,7 +112,7 @@ function namedDeclaration(index, file, name, accepts, line, seen = new Set()) {
|
|
|
45
112
|
}, ...result.chain] };
|
|
46
113
|
}
|
|
47
114
|
}
|
|
48
|
-
let local = (index
|
|
115
|
+
let local = definitionsInFile(index, name, file).filter(d => accepts(d));
|
|
49
116
|
if (line != null) {
|
|
50
117
|
local = local.filter(d => !d.lexicalScopeStartLine ||
|
|
51
118
|
(line >= d.lexicalScopeStartLine && line <= d.lexicalScopeEndLine));
|
|
@@ -101,6 +168,23 @@ function namedDeclaration(index, file, name, accepts, line, seen = new Set()) {
|
|
|
101
168
|
return distinct.size === 1 ? distinct.values().next().value : null;
|
|
102
169
|
}
|
|
103
170
|
|
|
171
|
+
// Distinct owners of a member name. Depends only on the symbol table, so
|
|
172
|
+
// one operation (a findCallers pass visits every call site of the name)
|
|
173
|
+
// computes it once per name; the memo dies with the operation's content cache.
|
|
174
|
+
const ownerCountMemo = new WeakMap(); // op content cache -> Map<name, count>
|
|
175
|
+
function ownerCountOf(index, name) {
|
|
176
|
+
const scope = index._opContentCache;
|
|
177
|
+
let memo = scope ? ownerCountMemo.get(scope) : null;
|
|
178
|
+
if (scope && !memo) { memo = new Map(); ownerCountMemo.set(scope, memo); }
|
|
179
|
+
const cached = memo?.get(name);
|
|
180
|
+
if (cached !== undefined) return cached;
|
|
181
|
+
const count = new Set((index.symbols.get(name) || [])
|
|
182
|
+
.filter(d => !NON_CALLABLE_TYPES.has(d.type) && ownerName(d))
|
|
183
|
+
.map(d => `${d.file}\0${ownerName(d)}\0${d.namespace || ''}`)).size;
|
|
184
|
+
memo?.set(name, count);
|
|
185
|
+
return count;
|
|
186
|
+
}
|
|
187
|
+
|
|
104
188
|
/** Collect independent facts; a failed lookup never manufactures a negative. */
|
|
105
189
|
function confirmationFacts(index, file, call, targets, options = {}) {
|
|
106
190
|
const entry = index.files.get(file);
|
|
@@ -114,9 +198,7 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
114
198
|
receiverTypeSource: options.receiverTypeSource || call.receiverTypeSource || 'unknown',
|
|
115
199
|
receiverOrigin: options.receiverOrigin || call.receiverTypeEvidence || null,
|
|
116
200
|
...(call.isFunctionReference && { valueReference: true }),
|
|
117
|
-
ownerCount:
|
|
118
|
-
.filter(d => !NON_CALLABLE_TYPES.has(d.type) && ownerName(d))
|
|
119
|
-
.map(d => `${d.file}\0${ownerName(d)}\0${d.namespace || ''}`)).size,
|
|
201
|
+
ownerCount: ownerCountOf(index, call.name),
|
|
120
202
|
...((options.originFile || call.receiverTypeFlowFile) && {
|
|
121
203
|
receiverTypeFlowFile: path.relative(index.root, options.originFile || call.receiverTypeFlowFile),
|
|
122
204
|
}),
|
|
@@ -141,13 +223,13 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
141
223
|
}
|
|
142
224
|
let type = facts.receiverType;
|
|
143
225
|
if (options.sameClass) {
|
|
144
|
-
const enclosing = index.findEnclosingFunction(file, call.line, true);
|
|
226
|
+
const enclosing = index.findEnclosingFunction(file, call.line, true, call);
|
|
145
227
|
type = enclosing?.className || enclosing?.receiver?.replace(/^\*/, '');
|
|
146
228
|
facts.receiverTypeSource = 'same-class';
|
|
147
229
|
facts.receiverOrigin = { source: 'same-class', declaration: declarationIdentity(enclosing) };
|
|
148
230
|
}
|
|
149
231
|
if (language === 'csharp' && call.receiverCastThis) {
|
|
150
|
-
const enclosing = index.findEnclosingFunction(file, call.line, true);
|
|
232
|
+
const enclosing = index.findEnclosingFunction(file, call.line, true, call);
|
|
151
233
|
if (enclosing?.className) {
|
|
152
234
|
facts.receiverCastThis = { enclosing: declarationIdentity(enclosing), interfaceType: type };
|
|
153
235
|
type = enclosing.className;
|
|
@@ -188,10 +270,13 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
188
270
|
// already-pinned declaration file may be another crate/module, where
|
|
189
271
|
// replaying `crate::Type` or an import alias would change its meaning.
|
|
190
272
|
const receiverContext = qualifier && facts.receiverTypeSource === 'annotation' ? file : originFile;
|
|
191
|
-
|
|
273
|
+
// A type name resolves where it was written: a field typed by
|
|
274
|
+
// `self.x = Local()` names the class visible at that write (fix #381).
|
|
275
|
+
const typeLine = Number.isInteger(options.receiverTypeLine) ? options.receiverTypeLine : call.line;
|
|
276
|
+
let resolved = resolveType(type, receiverContext, receiverContext === file ? typeLine : undefined, qualifier);
|
|
192
277
|
if (resolved && receiverContext !== originFile && resolved.declaration.file !== originFile) resolved = null;
|
|
193
278
|
if (!resolved && language === 'rust') {
|
|
194
|
-
const enclosing = index.findEnclosingFunction(file, call.line, true);
|
|
279
|
+
const enclosing = index.findEnclosingFunction(file, call.line, true, call);
|
|
195
280
|
const bounds = enclosing?.genericBounds?.[type];
|
|
196
281
|
const boundTypes = (bounds || []).map(bound => resolveType(bound, file, call.line))
|
|
197
282
|
.filter(bound => bound?.declaration.type === 'trait' &&
|
|
@@ -224,8 +309,11 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
224
309
|
const key = identityKey(identity);
|
|
225
310
|
if (visited.has(key) || visited.size >= 16) return null;
|
|
226
311
|
visited.add(key);
|
|
227
|
-
const
|
|
228
|
-
|
|
312
|
+
const ownerDefinition = owner.file && TYPE_KINDS.has(owner.type) ? owner : null;
|
|
313
|
+
const members = callableMembersOwnedBy(index, call.name, owner.name).filter(d => {
|
|
314
|
+
// A member of a same-name class declared in another scope of
|
|
315
|
+
// the file is not this owner's member (fix #389).
|
|
316
|
+
if (ownerDefinition && distinctOwnerDefinitions(index, d, ownerDefinition, true)) return false;
|
|
229
317
|
if (language === 'csharp' && d.explicitInterface &&
|
|
230
318
|
(!facts.receiverCastThis || d.explicitInterface !== facts.receiverCastThis.interfaceType)) return false;
|
|
231
319
|
if ((d.namespace || null) !== (owner.namespace || null)) return false;
|
|
@@ -248,6 +336,27 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
248
336
|
facts.lookupUnsupported = 'external-parent-member';
|
|
249
337
|
return null;
|
|
250
338
|
}
|
|
339
|
+
// A C++ using-declaration in the class body (`using Base::name;`,
|
|
340
|
+
// fix #401) brings the base's members of the name into this lookup
|
|
341
|
+
// set: the arguments choose among both. The witness only states a
|
|
342
|
+
// member of this class, so a choice it cannot express (a base
|
|
343
|
+
// member, or no choice) leaves the lookup unproven.
|
|
344
|
+
if (language === 'cpp' && members.length > 0 && parents.length > 0) {
|
|
345
|
+
const usingBases = require('./callers')._cppMemberUsingBases(index, owner.name, call.name, owner.file);
|
|
346
|
+
const brought = usingBases && usingBases.size > 0 ? parents.filter(parent => usingBases.has('*') ||
|
|
347
|
+
usingBases.has(String(parent.name).replace(/<.*$/s, '').split('::').pop())) : [];
|
|
348
|
+
if (brought.length > 0) {
|
|
349
|
+
const baseMembers = brought.flatMap(parent =>
|
|
350
|
+
callableMembersOwnedBy(index, call.name, parent.name).filter(d => d.file === parent.file));
|
|
351
|
+
let choice;
|
|
352
|
+
try {
|
|
353
|
+
choice = options.selectOverload ? options.selectOverload(index, call, [...members, ...baseMembers], language) : null;
|
|
354
|
+
} catch {
|
|
355
|
+
choice = null;
|
|
356
|
+
}
|
|
357
|
+
if (!choice?.match || members.length !== 1 || choice.match !== members[0]) return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
251
360
|
const step = { owner: identity, members: members.map(declarationIdentity), parents: parents.map(declarationIdentity),
|
|
252
361
|
...(derefTarget && { derefTarget }) };
|
|
253
362
|
const getter = facts.valueReference && propertyReadMember(step.members);
|
|
@@ -269,12 +378,47 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
269
378
|
receiver: declarationIdentity(resolved.declaration),
|
|
270
379
|
steps: [...steps, step], selected: declarationIdentity(members[0]),
|
|
271
380
|
};
|
|
381
|
+
// Configuration alternatives of one member (fix #385: `#ifdef _WIN32
|
|
382
|
+
// bool f() ... #else bool f() ... #endif` in one class body) are the
|
|
383
|
+
// member in each build configuration: the lookup selects the item.
|
|
384
|
+
if (members.length > 1) {
|
|
385
|
+
const { _isConfigurationAlternative } = require('./callers');
|
|
386
|
+
if (members.slice(1).every(member => _isConfigurationAlternative(index, members[0], member))) {
|
|
387
|
+
return {
|
|
388
|
+
receiver: declarationIdentity(resolved.declaration),
|
|
389
|
+
steps: [...steps, step], selected: declarationIdentity(members[0]),
|
|
390
|
+
configurationItem: members.map(declarationIdentity),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
272
394
|
if (members.length > 1 && options.selectOverload) {
|
|
273
|
-
|
|
395
|
+
// The choice is made on the index itself first (its memos stay
|
|
396
|
+
// on); only a selected member needs the replayable witness.
|
|
397
|
+
let choice;
|
|
398
|
+
try {
|
|
399
|
+
choice = options.selectOverload(index, call, members, language);
|
|
400
|
+
} catch {
|
|
401
|
+
choice = null;
|
|
402
|
+
}
|
|
403
|
+
const overload = choice?.match
|
|
404
|
+
? captureOverload(index, call, members, language, options.selectOverload) : null;
|
|
274
405
|
if (overload?.selected) return {
|
|
275
406
|
receiver: declarationIdentity(resolved.declaration),
|
|
276
407
|
steps: [...steps, { ...step, overload }], selected: overload.selected,
|
|
277
408
|
};
|
|
409
|
+
// Every member the lookup found is known but the arguments cannot
|
|
410
|
+
// choose one (fix #396). Name lookup stops at the declaring class,
|
|
411
|
+
// so the callee is one of its members of that name: the witness
|
|
412
|
+
// (`memberSet`) proves that a target outside them is not the
|
|
413
|
+
// callee without replaying the overload choice. The hops before
|
|
414
|
+
// the declaring class declare no member of the name.
|
|
415
|
+
if (!choice?.match && choice?.ambiguous === true && steps.every(prior => prior.members.length === 0)) {
|
|
416
|
+
return {
|
|
417
|
+
receiver: declarationIdentity(resolved.declaration),
|
|
418
|
+
steps: [...steps, step],
|
|
419
|
+
selected: declarationIdentity(members[0]), memberSet: true,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
278
422
|
}
|
|
279
423
|
if (members.length || parents.length !== 1) return null;
|
|
280
424
|
return walk(parents[0], [...steps, step]);
|
|
@@ -284,4 +428,4 @@ function confirmationFacts(index, file, call, targets, options = {}) {
|
|
|
284
428
|
return facts;
|
|
285
429
|
}
|
|
286
430
|
|
|
287
|
-
module.exports = { confirmationFacts, namedDeclaration, occurrenceIdentity };
|
|
431
|
+
module.exports = { confirmationFacts, namedDeclaration, occurrenceIdentity, definitionsInFile, definitionAt };
|
|
@@ -30,8 +30,21 @@ function factIndex(reads, live) {
|
|
|
30
30
|
if (!Object.hasOwn(reads, key)) throw new Error(`Missing overload fact: ${key}`);
|
|
31
31
|
return decode(reads[key]);
|
|
32
32
|
};
|
|
33
|
+
// A map-like read answers with one object per key, as the live index's
|
|
34
|
+
// Map.get does (fix #388: decoding the same file entry again for every
|
|
35
|
+
// read dominated warm `find` on Java projects).
|
|
36
|
+
const mapReads = new Map();
|
|
37
|
+
const readMapValue = (key, compute) => {
|
|
38
|
+
if (mapReads.has(key)) return mapReads.get(key);
|
|
39
|
+
const value = read(key, compute);
|
|
40
|
+
mapReads.set(key, value);
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
33
43
|
return new Proxy({}, {
|
|
34
44
|
get(_, property) {
|
|
45
|
+
// Callers skip their per-operation memos on this facade: every
|
|
46
|
+
// read must be recorded for the replay (fix #396).
|
|
47
|
+
if (property === 'isProvenanceFactIndex') return true;
|
|
35
48
|
if (local.has(property)) return local.get(property);
|
|
36
49
|
if (String(property).includes('Cache')) {
|
|
37
50
|
const cache = new Map(); local.set(property, cache); return cache;
|
|
@@ -39,7 +52,7 @@ function factIndex(reads, live) {
|
|
|
39
52
|
if (['symbols', 'files', 'importGraph', 'extendsGraph', 'extendedByGraph'].includes(property)) {
|
|
40
53
|
return {
|
|
41
54
|
get(key) {
|
|
42
|
-
return
|
|
55
|
+
return readMapValue(`${property}.get:${JSON.stringify(key)}`, () => {
|
|
43
56
|
const value = live[property]?.get(key);
|
|
44
57
|
if (property !== 'files' || !value) return value;
|
|
45
58
|
return { language: value.language, relativePath: value.relativePath,
|
|
@@ -62,11 +75,25 @@ function factIndex(reads, live) {
|
|
|
62
75
|
});
|
|
63
76
|
}
|
|
64
77
|
|
|
78
|
+
// Index definitions are immutable records: one encoding each (fix #396:
|
|
79
|
+
// every excluded receiver site of a many-overload name encoded the whole
|
|
80
|
+
// candidate set again). The encoding is read-only witness data.
|
|
81
|
+
const ENCODED_DEFINITIONS = new WeakMap();
|
|
82
|
+
function encodeDefinition(definition) {
|
|
83
|
+
if (!definition || typeof definition !== 'object') return encode(definition);
|
|
84
|
+
let encoded = ENCODED_DEFINITIONS.get(definition);
|
|
85
|
+
if (!encoded) {
|
|
86
|
+
encoded = encode(definition);
|
|
87
|
+
ENCODED_DEFINITIONS.set(definition, encoded);
|
|
88
|
+
}
|
|
89
|
+
return encoded;
|
|
90
|
+
}
|
|
91
|
+
|
|
65
92
|
function captureOverload(index, call, candidates, language, select) {
|
|
66
93
|
const reads = {};
|
|
67
94
|
try {
|
|
68
95
|
const result = select(factIndex(reads, index), call, candidates, language);
|
|
69
|
-
return { call: encode(call), candidates:
|
|
96
|
+
return { call: encode(call), candidates: candidates.map(encodeDefinition), language, reads,
|
|
70
97
|
selected: declarationIdentity(result.match),
|
|
71
98
|
outcome: result.match ? 'selected' : result.ambiguous ? 'ambiguous' : 'no-fit' };
|
|
72
99
|
} catch { return null; }
|
|
@@ -115,4 +142,4 @@ function overloadMemberGroup(steps) {
|
|
|
115
142
|
return group;
|
|
116
143
|
}
|
|
117
144
|
|
|
118
|
-
module.exports = { captureOverload, validateOverload, overloadMemberGroup };
|
|
145
|
+
module.exports = { captureOverload, validateOverload, overloadMemberGroup, factIndex };
|
package/core/provenance.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// A source offset. Facts produced inside a Rust macro expansion (fix #374)
|
|
4
|
+
// carry fractional offsets within the invocation: positions without a
|
|
5
|
+
// source token of their own, still ordered and paired.
|
|
6
|
+
const isOffset = value => Number.isFinite(value) && value >= 0;
|
|
7
|
+
|
|
3
8
|
const { codeUnitCompare } = require('./shared');
|
|
4
9
|
|
|
5
10
|
const TYPE_SOURCE_RULES = Object.freeze({
|
|
@@ -29,21 +34,35 @@ function declarationIdentity(definition) {
|
|
|
29
34
|
};
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
function validIdentity(identity) {
|
|
38
|
+
return !!identity && !!identity.file && Number.isInteger(identity.startLine) &&
|
|
39
|
+
!!identity.name && !!identity.kind;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A declaration identity's key, or null when the identity is incomplete.
|
|
44
|
+
* Fields are joined with NUL (no path, name or kind contains one); an
|
|
45
|
+
* absent optional field is the empty string.
|
|
46
|
+
*/
|
|
32
47
|
function identityKey(identity) {
|
|
33
|
-
if (!
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
identity.
|
|
37
|
-
identity.
|
|
38
|
-
identity.namespace || null, identity.enclosingType || null,
|
|
39
|
-
identity.lexicalScopeStartLine || null, identity.lexicalScopeEndLine || null,
|
|
40
|
-
identity.bindingId || null,
|
|
41
|
-
]);
|
|
48
|
+
if (!validIdentity(identity)) return null;
|
|
49
|
+
return `${identity.file}\0${identity.startLine}\0${identity.endLine ?? ''}\0${identity.name}\0` +
|
|
50
|
+
`${identity.kind}\0${identity.className || ''}\0${identity.namespace || ''}\0` +
|
|
51
|
+
`${identity.enclosingType || ''}\0${identity.lexicalScopeStartLine || ''}\0` +
|
|
52
|
+
`${identity.lexicalScopeEndLine || ''}\0${identity.bindingId || ''}`;
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
/** Do two complete identities name one declaration (field by field)? */
|
|
44
56
|
function sameDeclaration(left, right) {
|
|
45
|
-
|
|
46
|
-
return
|
|
57
|
+
if (!validIdentity(left) || !validIdentity(right)) return false;
|
|
58
|
+
return left.file === right.file && left.startLine === right.startLine &&
|
|
59
|
+
(left.endLine ?? null) === (right.endLine ?? null) && left.name === right.name &&
|
|
60
|
+
left.kind === right.kind && (left.className || null) === (right.className || null) &&
|
|
61
|
+
(left.namespace || null) === (right.namespace || null) &&
|
|
62
|
+
(left.enclosingType || null) === (right.enclosingType || null) &&
|
|
63
|
+
(left.lexicalScopeStartLine || null) === (right.lexicalScopeStartLine || null) &&
|
|
64
|
+
(left.lexicalScopeEndLine || null) === (right.lexicalScopeEndLine || null) &&
|
|
65
|
+
(left.bindingId || null) === (right.bindingId || null);
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
function propertyReadMember(members) {
|
|
@@ -65,13 +84,13 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
65
84
|
const verdictFor = declaration => targets.some(t => sameDeclaration(t, declaration))
|
|
66
85
|
? { verdict: 'establishes-target' }
|
|
67
86
|
: { verdict: 'establishes-other', declaration };
|
|
68
|
-
if (!targets.length || targets.some(t => !
|
|
87
|
+
if (!targets.length || targets.some(t => !validIdentity(t))) return incomplete('missing-target-identity');
|
|
69
88
|
if (facts.receiverOrigin?.externalFactory) {
|
|
70
89
|
const factory = facts.receiverOrigin.externalFactory;
|
|
71
90
|
const empty = value => Array.isArray(value) && !value.length;
|
|
72
|
-
const span = origin =>
|
|
91
|
+
const span = origin => isOffset(origin?.start) && isOffset(origin?.end) && origin.end > origin.start;
|
|
73
92
|
if (facts.language !== 'python' || facts.receiverTypeSource !== 'flow' ||
|
|
74
|
-
!
|
|
93
|
+
!validIdentity(factory.owner) || !validIdentity(factory.enclosing) ||
|
|
75
94
|
factory.owner.file !== facts.site?.file || factory.enclosing.file !== factory.owner.file ||
|
|
76
95
|
factory.enclosing.className !== factory.owner.name || !factory.field ||
|
|
77
96
|
facts.site.line < factory.enclosing.startLine || facts.site.line > factory.enclosing.endLine ||
|
|
@@ -108,7 +127,7 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
108
127
|
!['annotation', 'constructor', 'flow'].includes(binding.origin?.source) ||
|
|
109
128
|
(binding.referenceAnnotation && (binding.origin.source !== 'annotation' ||
|
|
110
129
|
!binding.referenceAnnotation.trim().startsWith('&'))) ||
|
|
111
|
-
!
|
|
130
|
+
!isOffset(binding.assignment?.start) || !isOffset(binding.assignment?.end)) {
|
|
112
131
|
return incomplete('invalid-copied-receiver-binding');
|
|
113
132
|
}
|
|
114
133
|
}
|
|
@@ -140,9 +159,10 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
140
159
|
const producer = facts.receiverOrigin.moduleProducer;
|
|
141
160
|
const call = producer.call, declaration = producer.declaration;
|
|
142
161
|
if (facts.language !== 'rust' || !call?.receiver || !call.name || !call.file ||
|
|
143
|
-
!
|
|
144
|
-
!
|
|
145
|
-
declaration.name !== call.name ||
|
|
162
|
+
!isOffset(call.start) || !isOffset(call.end) ||
|
|
163
|
+
!validIdentity(declaration) || declaration.className || !declaration.returnType ||
|
|
164
|
+
declaration.name !== call.name ||
|
|
165
|
+
declaration.file !== (producer.module?.reexportedFrom || producer.module?.file)) {
|
|
146
166
|
return incomplete('invalid-module-producer-declaration');
|
|
147
167
|
}
|
|
148
168
|
const segments = call.receiver.split('::');
|
|
@@ -153,8 +173,12 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
153
173
|
const specifier = binding ? [binding.module, ...segments.slice(1)].join('::') : call.receiver;
|
|
154
174
|
const fileParts = producer.module.file.split('/');
|
|
155
175
|
const base = fileParts.pop().replace(/\.rs$/, '');
|
|
176
|
+
// A crate-name specifier (`cursive::default()`) owns the crate root
|
|
177
|
+
// file, whose basename is lib/main (fix #369).
|
|
178
|
+
const crateRoot = producer.module.crateRoot === true && !specifier.includes('::') &&
|
|
179
|
+
(base === 'lib' || base === 'main');
|
|
156
180
|
if (specifier !== producer.module.specifier ||
|
|
157
|
-
specifier.split('::').at(-1) !== (base === 'mod' ? fileParts.pop() : base)) {
|
|
181
|
+
(!crateRoot && specifier.split('::').at(-1) !== (base === 'mod' ? fileParts.pop() : base))) {
|
|
158
182
|
return inconsistent('module-producer-path-mismatch');
|
|
159
183
|
}
|
|
160
184
|
}
|
|
@@ -198,8 +222,8 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
198
222
|
pattern.contract?.kind === (pattern.variant === 'Some' ? 'Option' : 'Result') &&
|
|
199
223
|
Array.isArray(pattern.shadowDeclarations) && !pattern.shadowDeclarations.length &&
|
|
200
224
|
Array.isArray(pattern.shadowBindings) && !pattern.shadowBindings.length &&
|
|
201
|
-
(pattern.source?.variable || (
|
|
202
|
-
|
|
225
|
+
(pattern.source?.variable || (isOffset(pattern.source?.start) &&
|
|
226
|
+
isOffset(pattern.source?.end) && pattern.source.end > pattern.source.start))
|
|
203
227
|
: ['unwrap', 'expect'].includes(unwrap.method);
|
|
204
228
|
if (facts.language !== 'rust' || !validProjection ||
|
|
205
229
|
!require('./rust-result-flow').validateRustWrapperContract(unwrap.contract)) {
|
|
@@ -222,7 +246,7 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
222
246
|
}
|
|
223
247
|
if (facts.lookup) {
|
|
224
248
|
const { receiver, steps, selected } = facts.lookup;
|
|
225
|
-
if (!
|
|
249
|
+
if (!validIdentity(receiver) || !validIdentity(selected) || !Array.isArray(steps) || !steps.length) {
|
|
226
250
|
return incomplete('missing-receiver-lookup');
|
|
227
251
|
}
|
|
228
252
|
if (!sameDeclaration(receiver, steps[0].owner)) return inconsistent('lookup-origin-mismatch');
|
|
@@ -240,19 +264,19 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
240
264
|
let resolvedAlias = typeDeclaration;
|
|
241
265
|
for (const alias of aliases) {
|
|
242
266
|
if (!sameDeclaration(alias.declaration, resolvedAlias) ||
|
|
243
|
-
!alias.aliasOf || !alias.name || !
|
|
267
|
+
!alias.aliasOf || !alias.name || !validIdentity(alias.target)) return inconsistent('receiver-alias-chain-mismatch');
|
|
244
268
|
const head = String(alias.aliasOf).replace(/^[*&\s]+/, '').split(/[<[]/, 1)[0].trim().split(/::|\./).pop();
|
|
245
269
|
if (head !== alias.name.split(/::|\./).pop() || head !== alias.target.name) return inconsistent('receiver-alias-target-mismatch');
|
|
246
270
|
resolvedAlias = alias.target;
|
|
247
271
|
}
|
|
248
272
|
if (!sameDeclaration(resolvedAlias, receiver)) return inconsistent('receiver-alias-end-mismatch');
|
|
249
273
|
const bound = facts.receiverGenericBound;
|
|
250
|
-
if (bound && (!
|
|
274
|
+
if (bound && (!validIdentity(bound.declaration) || bound.parameter !== facts.receiverType ||
|
|
251
275
|
!bound.bounds?.includes(typeDeclaration.name) || !sameDeclaration(bound.selected, typeDeclaration))) {
|
|
252
276
|
return inconsistent('receiver-generic-bound-mismatch');
|
|
253
277
|
}
|
|
254
278
|
const castThis = facts.receiverCastThis;
|
|
255
|
-
if (castThis && (!
|
|
279
|
+
if (castThis && (!validIdentity(castThis.enclosing) ||
|
|
256
280
|
castThis.enclosing.className !== receiver.name || castThis.enclosing.file !== receiver.file ||
|
|
257
281
|
castThis.interfaceType !== facts.receiverType)) return inconsistent('receiver-this-cast-mismatch');
|
|
258
282
|
const overload = facts.lookup.overload;
|
|
@@ -262,9 +286,18 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
262
286
|
return inconsistent('selected-member-outside-overload-group');
|
|
263
287
|
}
|
|
264
288
|
const ambiguousOverload = overload?.outcome === 'ambiguous';
|
|
289
|
+
const configurationItem = !overload && Array.isArray(facts.lookup.configurationItem)
|
|
290
|
+
? facts.lookup.configurationItem : null;
|
|
291
|
+
if (configurationItem && configurationItem.some(item => !validIdentity(item))) {
|
|
292
|
+
return incomplete('missing-configuration-item');
|
|
293
|
+
}
|
|
294
|
+
// Every same-name member of the declaring class, the arguments
|
|
295
|
+
// choosing none (fix #396): the callee is one of them.
|
|
296
|
+
const memberSet = !overload && !configurationItem && facts.lookup.memberSet === true;
|
|
297
|
+
let memberSetMembers = null;
|
|
265
298
|
for (let i = 0; i < steps.length; i++) {
|
|
266
299
|
const step = steps[i];
|
|
267
|
-
if (!
|
|
300
|
+
if (!validIdentity(step.owner) || !Array.isArray(step.members) || !Array.isArray(step.parents)) {
|
|
268
301
|
return incomplete('missing-member-lookup-facts');
|
|
269
302
|
}
|
|
270
303
|
const named = step.members.filter(member => member.name === selected.name);
|
|
@@ -279,6 +312,19 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
279
312
|
if (!step.parents.some(parent => sameDeclaration(parent, steps[i + 1].owner))) {
|
|
280
313
|
return inconsistent('unproven-inheritance-hop');
|
|
281
314
|
}
|
|
315
|
+
} else if (configurationItem) {
|
|
316
|
+
// One member in several build configurations: the witness
|
|
317
|
+
// names exactly the step's same-name members.
|
|
318
|
+
if (named.length !== configurationItem.length ||
|
|
319
|
+
!named.every(member => configurationItem.some(item => sameDeclaration(item, member))) ||
|
|
320
|
+
!configurationItem.some(item => sameDeclaration(item, selected))) {
|
|
321
|
+
return inconsistent('configuration-item-mismatch');
|
|
322
|
+
}
|
|
323
|
+
} else if (memberSet) {
|
|
324
|
+
if (named.length < 2 || !named.some(member => sameDeclaration(member, selected))) {
|
|
325
|
+
return inconsistent('member-set-mismatch');
|
|
326
|
+
}
|
|
327
|
+
memberSetMembers = named;
|
|
282
328
|
} else if (!overload && (named.length !== 1 || !sameDeclaration(named[0], selected))) {
|
|
283
329
|
const propertyRead = facts.valueReference && step.propertyRead &&
|
|
284
330
|
sameDeclaration(propertyReadMember(named), selected);
|
|
@@ -300,6 +346,12 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
300
346
|
if (facts.receiverOrigin.source !== facts.receiverTypeSource) {
|
|
301
347
|
return inconsistent('receiver-source-mismatch');
|
|
302
348
|
}
|
|
349
|
+
if (memberSetMembers) {
|
|
350
|
+
if (targets.some(target => memberSetMembers.some(member => sameDeclaration(target, member)))) {
|
|
351
|
+
return incomplete('ambiguous-or-missing-member');
|
|
352
|
+
}
|
|
353
|
+
return { verdict: 'establishes-other', declarations: memberSetMembers };
|
|
354
|
+
}
|
|
303
355
|
if (ambiguousOverload) {
|
|
304
356
|
// Every overload is known, but the argument shape cannot choose
|
|
305
357
|
// one. This proves no particular target. It can still establish
|
|
@@ -310,10 +362,15 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
310
362
|
}
|
|
311
363
|
return { verdict: 'establishes-other', declarations };
|
|
312
364
|
}
|
|
365
|
+
if (configurationItem) {
|
|
366
|
+
return targets.some(target => configurationItem.some(item => sameDeclaration(target, item)))
|
|
367
|
+
? { verdict: 'establishes-target' }
|
|
368
|
+
: { verdict: 'establishes-other', declarations: configurationItem };
|
|
369
|
+
}
|
|
313
370
|
return verdictFor(selected);
|
|
314
371
|
}
|
|
315
372
|
if (facts.binding) {
|
|
316
|
-
if (!
|
|
373
|
+
if (!validIdentity(facts.binding.declaration) || !facts.binding.referenceId ||
|
|
317
374
|
facts.binding.referenceId !== facts.binding.declaration.bindingId) {
|
|
318
375
|
return incomplete('missing-binding-witness');
|
|
319
376
|
}
|
|
@@ -350,7 +407,7 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
350
407
|
}
|
|
351
408
|
}
|
|
352
409
|
const last = chain.at(-1);
|
|
353
|
-
if (!
|
|
410
|
+
if (!validIdentity(last.declaration) || last.declaration.file !== last.toFile ||
|
|
354
411
|
last.declaration.name !== last.importedName) return incomplete('missing-import-declaration');
|
|
355
412
|
return verdictFor(last.declaration);
|
|
356
413
|
}
|
|
@@ -385,9 +442,26 @@ function createProvenance(evidence, resolution) {
|
|
|
385
442
|
if (evidence.moduleOwnedPath) rules.push('module-owned');
|
|
386
443
|
if (evidence.hasBindingId) rules.push('binding');
|
|
387
444
|
if (evidence.hasSingleOwnerEvidence) rules.push('single-owner');
|
|
388
|
-
if (evidence.hasImportEvidence)
|
|
445
|
+
if (evidence.hasImportEvidence) {
|
|
446
|
+
// A C/C++ include closure that needs a basename-resolved include
|
|
447
|
+
// edge (no compile database or include path proves it) is the
|
|
448
|
+
// weaker include evidence (fix #361).
|
|
449
|
+
rules.push(evidence.includeBasename ? 'include-basename'
|
|
450
|
+
: facts.importChain ? 'import-chain' : 'import-supported');
|
|
451
|
+
}
|
|
389
452
|
if (evidence.hasReceiverEvidence) rules.push('receiver-binding');
|
|
390
453
|
if (evidence.hasSamePackageEvidence) rules.push('same-package');
|
|
454
|
+
// A call target produced by expanding a project macro invocation
|
|
455
|
+
// (fix #362) leads the rule list: the name exists only after token
|
|
456
|
+
// substitution, whatever scope evidence then bound it.
|
|
457
|
+
if (evidence.macroExpansion) {
|
|
458
|
+
rules.unshift('macro-expansion');
|
|
459
|
+
facts.macroExpansion = {
|
|
460
|
+
macro: evidence.macroExpansion.macro,
|
|
461
|
+
definition: evidence.macroExpansion.definition,
|
|
462
|
+
origin: evidence.macroExpansion.origin,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
391
465
|
if (!rules.length) rules.push(evidence.reason || resolution || 'unknown');
|
|
392
466
|
const provenance = { rule: rules[0], rules: [...new Set(rules)], facts };
|
|
393
467
|
if (facts.targets?.length) {
|
|
@@ -418,6 +492,6 @@ function summarizeProvenance(sites) {
|
|
|
418
492
|
}
|
|
419
493
|
|
|
420
494
|
module.exports = {
|
|
421
|
-
TYPE_SOURCE_RULES, declarationIdentity, identityKey, sameDeclaration, propertyReadMember,
|
|
495
|
+
TYPE_SOURCE_RULES, declarationIdentity, identityKey, validIdentity, sameDeclaration, propertyReadMember,
|
|
422
496
|
validateConfirmation, validateCallMismatch, createProvenance, summarizeProvenance,
|
|
423
497
|
};
|