ucn 5.4.1 → 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.
Files changed (87) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +18 -12
  6. package/cli/index.js +42 -16
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +522 -31
  16. package/core/callers.js +12053 -1047
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis-ext.js +6 -6
  38. package/core/output/analysis.js +18 -2
  39. package/core/output/brief.js +3 -0
  40. package/core/output/doctor.js +4 -0
  41. package/core/output/endpoints.js +15 -6
  42. package/core/output/find.js +11 -2
  43. package/core/output/lines.js +8 -2
  44. package/core/output/refactoring.js +121 -14
  45. package/core/output/reporting.js +31 -3
  46. package/core/output/shared.js +14 -0
  47. package/core/output-budget.js +316 -122
  48. package/core/parallel-build.js +124 -47
  49. package/core/parser.js +3 -2
  50. package/core/project.js +457 -60
  51. package/core/provenance-facts.js +156 -12
  52. package/core/provenance-overload.js +30 -3
  53. package/core/provenance.js +104 -30
  54. package/core/pytest-fixtures.js +434 -0
  55. package/core/python-fixture-flow.js +17 -3
  56. package/core/python-modules.js +173 -0
  57. package/core/reflection.js +877 -0
  58. package/core/registry.js +1 -1
  59. package/core/reporting.js +234 -52
  60. package/core/request-helpers.js +561 -0
  61. package/core/route-graph.js +1922 -0
  62. package/core/route-lists.js +2414 -0
  63. package/core/rust-macro-expansion.js +2113 -0
  64. package/core/rust-modules.js +185 -0
  65. package/core/search.js +121 -29
  66. package/core/shared.js +19 -1
  67. package/core/test-name-bindings.js +256 -0
  68. package/core/type-denotation.js +1161 -0
  69. package/core/type-references.js +3229 -0
  70. package/core/verify.js +3360 -672
  71. package/languages/adapter.js +31 -5
  72. package/languages/c-family.js +4061 -212
  73. package/languages/c-macro-invocations.js +380 -0
  74. package/languages/c-preprocessor.js +254 -0
  75. package/languages/csharp.js +1406 -49
  76. package/languages/go.js +1243 -70
  77. package/languages/index.js +728 -6
  78. package/languages/java.js +473 -61
  79. package/languages/javascript.js +1270 -197
  80. package/languages/lexical-scope.js +1280 -0
  81. package/languages/python.js +1557 -72
  82. package/languages/rust-macro-rules.js +1020 -0
  83. package/languages/rust-value-flow.js +416 -0
  84. package/languages/rust.js +2237 -115
  85. package/languages/utils.js +298 -4
  86. package/mcp/server.js +25 -11
  87. package/package.json +1 -1
@@ -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.symbols.get(name) || []).filter(d => d.file === file && accepts(d));
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: new Set((index.symbols.get(call.name) || [])
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
- let resolved = resolveType(type, receiverContext, receiverContext === file ? call.line : undefined, qualifier);
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 members = (index.symbols.get(call.name) || []).filter(d => {
228
- if (NON_CALLABLE_TYPES.has(d.type) || ownerName(d) !== owner.name) return false;
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
- const overload = captureOverload(index, call, members, language, options.selectOverload);
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 read(`${property}.get:${JSON.stringify(key)}`, () => {
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: encode(candidates), language, reads,
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 };
@@ -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 (!identity || !identity.file || !Number.isInteger(identity.startLine) ||
34
- !identity.name || !identity.kind) return null;
35
- return JSON.stringify([
36
- identity.file, identity.startLine, identity.endLine ?? null,
37
- identity.name, identity.kind, identity.className || null,
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
- const key = identityKey(left);
46
- return key !== null && key === identityKey(right);
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 => !identityKey(t))) return incomplete('missing-target-identity');
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 => Number.isInteger(origin?.start) && Number.isInteger(origin?.end) && origin.end > origin.start;
91
+ const span = origin => isOffset(origin?.start) && isOffset(origin?.end) && origin.end > origin.start;
73
92
  if (facts.language !== 'python' || facts.receiverTypeSource !== 'flow' ||
74
- !identityKey(factory.owner) || !identityKey(factory.enclosing) ||
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
- !Number.isInteger(binding.assignment?.start) || !Number.isInteger(binding.assignment?.end)) {
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
- !Number.isInteger(call.start) || !Number.isInteger(call.end) ||
144
- !identityKey(declaration) || declaration.className || !declaration.returnType ||
145
- declaration.name !== call.name || declaration.file !== producer.module?.file) {
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 || (Number.isInteger(pattern.source?.start) &&
202
- Number.isInteger(pattern.source?.end) && pattern.source.end > pattern.source.start))
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 (!identityKey(receiver) || !identityKey(selected) || !Array.isArray(steps) || !steps.length) {
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 || !identityKey(alias.target)) return inconsistent('receiver-alias-chain-mismatch');
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 && (!identityKey(bound.declaration) || bound.parameter !== facts.receiverType ||
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 && (!identityKey(castThis.enclosing) ||
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 (!identityKey(step.owner) || !Array.isArray(step.members) || !Array.isArray(step.parents)) {
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 (!identityKey(facts.binding.declaration) || !facts.binding.referenceId ||
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 (!identityKey(last.declaration) || last.declaration.file !== last.toFile ||
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) rules.push(facts.importChain ? 'import-chain' : 'import-supported');
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
  };