ucn 5.5.1 → 5.5.3

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 (97) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -9
  2. package/.claude/skills/ucn/references/commands.md +40 -18
  3. package/.claude/skills/ucn/references/resolution-rules.md +126 -20
  4. package/.claude/skills/ucn/references/trust-contract.md +5 -3
  5. package/README.md +32 -21
  6. package/cli/index.js +2 -1
  7. package/core/accessors.js +341 -54
  8. package/core/account.js +74 -20
  9. package/core/analysis.js +257 -125
  10. package/core/ast-analysis.js +12 -3
  11. package/core/bridge.js +15 -65
  12. package/core/brief.js +8 -4
  13. package/core/build-worker.js +2 -1
  14. package/core/cache.js +137 -6
  15. package/core/call-name-cache.js +134 -0
  16. package/core/callable-scope.js +21 -0
  17. package/core/callers.js +4726 -940
  18. package/core/check.js +22 -5
  19. package/core/class-identity.js +399 -12
  20. package/core/client-receivers.js +189 -0
  21. package/core/constructor-calls.js +831 -0
  22. package/core/constructor-hierarchy.js +147 -0
  23. package/core/constructor-protocol.js +196 -0
  24. package/core/contract-membership.js +346 -140
  25. package/core/cpp-conversion-constraints.js +123 -0
  26. package/core/cpp-conversion-syntax.js +216 -0
  27. package/core/cpp-conversions.js +414 -0
  28. package/core/cpp-scope.js +245 -44
  29. package/core/deadcode.js +61 -16
  30. package/core/decorator-binding.js +137 -3
  31. package/core/enum-reference-syntax.js +134 -0
  32. package/core/execute.js +17 -1
  33. package/core/external-macros.js +42 -10
  34. package/core/go-packages.js +171 -0
  35. package/core/graph-build.js +2 -0
  36. package/core/graph.js +75 -59
  37. package/core/ignore-rules.js +2 -1
  38. package/core/imports.js +145 -19
  39. package/core/index-ir.js +41 -4
  40. package/core/inprocess-clients.js +41 -15
  41. package/core/ir.js +27 -6
  42. package/core/macro-expansion.js +38 -2
  43. package/core/out-of-class-members.js +182 -0
  44. package/core/output/analysis-ext.js +4 -2
  45. package/core/output/analysis.js +89 -20
  46. package/core/output/graph.js +12 -3
  47. package/core/output/lines.js +4 -3
  48. package/core/output/public.js +2 -0
  49. package/core/output/reporting.js +12 -1
  50. package/core/output/shared.js +2 -0
  51. package/core/output/tracing.js +28 -22
  52. package/core/output-budget.js +2 -2
  53. package/core/parallel-build.js +52 -1
  54. package/core/project.js +217 -112
  55. package/core/provenance-constructor-base.js +161 -0
  56. package/core/provenance-facts.js +257 -27
  57. package/core/provenance-overload.js +458 -54
  58. package/core/provenance-type.js +487 -0
  59. package/core/provenance.js +234 -15
  60. package/core/public-command.js +17 -2
  61. package/core/query-bodies.js +159 -0
  62. package/core/reflection.js +270 -2
  63. package/core/rename-token-syntax.js +111 -0
  64. package/core/reporting.js +13 -7
  65. package/core/request-helpers.js +63 -7
  66. package/core/rust-macro-expansion.js +151 -19
  67. package/core/rust-modules.js +1 -1
  68. package/core/rust-names.js +607 -0
  69. package/core/search.js +70 -17
  70. package/core/shared.js +98 -1
  71. package/core/source-memo.js +201 -0
  72. package/core/text-usage-syntax.js +38 -0
  73. package/core/tracing.js +7 -1
  74. package/core/type-denotation.js +124 -37
  75. package/core/type-references.js +615 -246
  76. package/core/value-references.js +2777 -0
  77. package/core/verify.js +1097 -281
  78. package/languages/adapter.js +28 -4
  79. package/languages/c-family.js +1118 -164
  80. package/languages/constructor-bases.js +105 -0
  81. package/languages/cpp-platform.js +26 -0
  82. package/languages/cpp-site-syntax.js +83 -0
  83. package/languages/csharp-scope.js +439 -0
  84. package/languages/csharp.js +428 -68
  85. package/languages/go.js +385 -207
  86. package/languages/html.js +4 -2
  87. package/languages/index.js +397 -2
  88. package/languages/java-platform.js +226 -0
  89. package/languages/java-unicode.js +269 -0
  90. package/languages/java.js +461 -117
  91. package/languages/javascript.js +418 -66
  92. package/languages/lexical-scope.js +190 -23
  93. package/languages/python.js +404 -54
  94. package/languages/rust.js +510 -82
  95. package/languages/utils.js +67 -8
  96. package/mcp/server.js +8 -2
  97. package/package.json +3 -3
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const { langTraits, getLanguageAdapter, getParser } = require('../languages');
4
+ const { declarationIdentity, sameDeclaration } = require('./provenance');
5
+ const { occurrenceIdentity } = require('./provenance-facts');
6
+ const { captureTypeLookup, validateTypeLookup } = require('./provenance-type');
7
+ const { SourceMemo } = require('./source-memo');
8
+
9
+ // Omitted-initializer call syntax of a source, by language.
10
+ const sourceCalls = new SourceMemo({ maxEntries: 128, maxWeight: 32 * 1024 * 1024,
11
+ weigh: calls => JSON.stringify(calls).length * 2 });
12
+
13
+ const implicitRecord = call => ({
14
+ name: call.name, qualifier: call.constructorQualifier || null,
15
+ argCount: call.argCount, site: occurrenceIdentity('', call),
16
+ base: JSON.stringify(call.implicitBase), enclosing: enclosingKey(call.enclosingFunction),
17
+ header: call.implicitBase?.header && { ...call.implicitBase.header },
18
+ });
19
+ // The constructor an implicit call runs in, by name and extent: the
20
+ // attribution a whole-file extraction adds (a class name) is not part of it.
21
+ const enclosingKey = enclosing => enclosing ? JSON.stringify([enclosing.name, enclosing.startLine, enclosing.endLine]) : null;
22
+
23
+ // Cache syntax, never a binding verdict. External macro recovery has an
24
+ // additional input and consultation side effects, so keep that path fresh.
25
+ const reusableSyntax = language => !langTraits(language)?.textualIncludes ||
26
+ !require('../languages/c-family').hasExternalMacroContext();
27
+
28
+ function implicitSourceCalls(language, source) {
29
+ const reusable = reusableSyntax(language);
30
+ const saved = reusable ? sourceCalls.get(language, source) : undefined;
31
+ if (saved) return saved;
32
+ const calls = getLanguageAdapter(language).findCallsInCode(source, getParser(language))
33
+ .filter(call => call.constructorSyntax === 'implicit').map(implicitRecord);
34
+ if (reusable) sourceCalls.set(language, source, calls);
35
+ return calls;
36
+ }
37
+
38
+ /**
39
+ * The omitted-initializer calls of the class at `base`'s lines (fix #421:
40
+ * the first replay over a large file extracted every call of the file).
41
+ * The adapter reads them from the same trees as a whole-file extraction.
42
+ */
43
+ const ownerCalls = new SourceMemo({ maxEntries: 128, maxWeight: 8 * 1024 * 1024,
44
+ weigh: byOwner => JSON.stringify([...byOwner.values()]).length * 2 });
45
+ function implicitOwnerSourceCalls(language, source, base) {
46
+ const adapter = getLanguageAdapter(language);
47
+ if (typeof adapter.implicitBaseCallsOf !== 'function' || !Number.isInteger(base?.ownerLine)) return null;
48
+ const reusable = reusableSyntax(language);
49
+ let byOwner = reusable ? ownerCalls.get(language, source) : undefined;
50
+ if (!byOwner) {
51
+ byOwner = new Map();
52
+ if (reusable) ownerCalls.set(language, source, byOwner);
53
+ }
54
+ const key = `${base.ownerLine}:${base.ownerEndLine}`;
55
+ if (!byOwner.has(key)) {
56
+ byOwner.set(key, adapter.implicitBaseCallsOf(source, getParser(language), base.ownerLine, base.ownerEndLine)
57
+ .filter(call => call.constructorSyntax === 'implicit').map(implicitRecord));
58
+ }
59
+ return byOwner.get(key);
60
+ }
61
+
62
+ function headerSite(call, file) {
63
+ const header = call.implicitBase?.header;
64
+ return header && { file, line: header.line, column: header.column,
65
+ start: header.start, end: header.end };
66
+ }
67
+
68
+ function captureImplicitBase(index, file, call, owner) {
69
+ const entry = index.files.get(file), language = entry?.language;
70
+ if (!langTraits(language)?.implicitBaseConstruction || call.constructorSyntax !== 'implicit') return null;
71
+ const header = headerSite(call, entry.relativePath);
72
+ if (!header) return null;
73
+ const binding = captureTypeLookup(index, { file, relativePath: entry.relativePath,
74
+ line: header.line, column: header.column, name: call.name,
75
+ qualifier: call.constructorQualifier || null, arity: call.implicitBase.arity,
76
+ position: call.implicitBase.header, construction: true }, owner);
77
+ if (!binding) return null;
78
+ const proof = { language, source: index._readFile(file), site: occurrenceIdentity(entry.relativePath, call),
79
+ name: call.name, qualifier: call.constructorQualifier || null,
80
+ base: call.implicitBase, enclosing: call.enclosingFunction, binding };
81
+ seedImplicitReplay(index, file, entry, call, proof);
82
+ return proof;
83
+ }
84
+
85
+ // A capture's first replay (fix #421, the #418 rule for other witnesses):
86
+ // the live record was extracted from this same content, so the replay's
87
+ // re-derivation finds exactly it when the file holds one such record. A
88
+ // copied or changed proof is re-derived in full.
89
+ const SEEDED_IMPLICIT = new WeakMap();
90
+ const proofKey = proof => JSON.stringify([proof.language, proof.name, proof.qualifier, proof.site, proof.base,
91
+ enclosingKey(proof.enclosing)]);
92
+
93
+ function seedImplicitReplay(index, file, entry, call, proof) {
94
+ if (index.isProvenanceFactIndex || !require('./source-memo').indexedContent(index, file, entry, proof.source)) return;
95
+ const base = JSON.stringify(call.implicitBase), enclosing = enclosingKey(call.enclosingFunction);
96
+ const same = (require('./callers').getCachedCalls(index, file) || []).filter(other =>
97
+ other.constructorSyntax === 'implicit' && other.name === call.name && other.argCount === 0 &&
98
+ (other.constructorQualifier || null) === proof.qualifier &&
99
+ ['line', 'column', 'start', 'end'].every(key => occurrenceIdentity(entry.relativePath, other)[key] === proof.site[key]) &&
100
+ JSON.stringify(other.implicitBase) === base && enclosingKey(other.enclosingFunction) === enclosing);
101
+ if (same.length === 1) SEEDED_IMPLICIT.set(proof, { key: proofKey(proof), source: proof.source,
102
+ header: call.implicitBase.header && { ...call.implicitBase.header } });
103
+ }
104
+
105
+ /** Re-derive the omitted initializer from raw source, then replay its header binding. */
106
+ function validateImplicitBase(proof, owner, site) {
107
+ if (!proof || !sameDeclaration(proof.binding?.owner, owner) ||
108
+ typeof proof.source !== 'string' || !langTraits(proof.language)?.implicitBaseConstruction ||
109
+ !site || ['file', 'line', 'column', 'start', 'end'].some(key => proof.site?.[key] !== site[key])) return false;
110
+ try {
111
+ const matching = calls => calls.filter(call => call.name === proof.name &&
112
+ call.qualifier === proof.qualifier && call.argCount === 0 &&
113
+ ['line', 'column', 'start', 'end'].every(key => call.site[key] === site[key]) &&
114
+ call.base === JSON.stringify(proof.base) && call.enclosing === enclosingKey(proof.enclosing));
115
+ const seeded = SEEDED_IMPLICIT.get(proof);
116
+ // The class's own calls decide; a call the whole-file extraction
117
+ // places elsewhere (a macro body) is read the whole-file way.
118
+ let matches = seeded && seeded.source === proof.source && seeded.key === proofKey(proof)
119
+ ? [{ header: seeded.header }]
120
+ : matching(implicitOwnerSourceCalls(proof.language, proof.source, proof.base) || []);
121
+ if (matches.length !== 1) matches = matching(implicitSourceCalls(proof.language, proof.source));
122
+ const header = matches[0]?.header;
123
+ return matches.length === 1 && validateTypeLookup(proof.binding, owner, header && {
124
+ file: site.file, line: header.line, column: header.column, start: header.start, end: header.end,
125
+ });
126
+ } catch (error) { return require('./provenance-overload').refusedProof(error, false); }
127
+ }
128
+
129
+ function implicitBaseBinding(index, file, call, candidates) {
130
+ const config = langTraits(index.files.get(file)?.language)?.implicitBaseConstruction;
131
+ if (!config || !call.implicitBase) return null;
132
+ const found = candidates.map(owner => ({ owner, proof: captureImplicitBase(index, file, call, owner) }))
133
+ .filter(entry => entry.proof);
134
+ if (found.length !== 1) return null;
135
+ const { owner, proof } = found[0];
136
+ return { owner, nonClass: !config.baseKinds.includes(owner.type), provenance: {
137
+ rule: 'implicit-base-binding', rules: ['implicit-base-binding'], facts: {
138
+ language: proof.language, site: proof.site, targets: [declarationIdentity(owner)], implicitBaseBinding: proof,
139
+ },
140
+ } };
141
+ }
142
+
143
+ /** The indexed source body, distinct from the constructor it initializes. */
144
+ function implicitSourceDefinition(index, file, call) {
145
+ const base = call.implicitBase, scope = base?.scope;
146
+ if (!base || !scope) return null;
147
+ const { ownConstructors } = require('./constructor-calls');
148
+ const owners = (index.symbols.get(base.owner) || []).filter(def => def.file === file &&
149
+ ['class', 'struct', 'record'].includes(def.type) && def.startLine === base.ownerLine && def.endLine === base.ownerEndLine &&
150
+ (!Object.hasOwn(scope, 'namespace') || (def.namespace || null) === (scope.namespace || null)) &&
151
+ (def.enclosingType || null) === (scope.enclosingType || null) &&
152
+ !!def.anonymousNamespace === !!scope.anonymousNamespace);
153
+ if (owners.length !== 1) return null;
154
+ if (base.generated) return owners[0];
155
+ const members = ownConstructors(index, owners[0]).filter(def => !def.modifiers?.includes('static') &&
156
+ def.startLine === call.enclosingFunction?.startLine && def.endLine === call.enclosingFunction?.endLine &&
157
+ def.params === base.params);
158
+ return members.length === 1 ? members[0] : null;
159
+ }
160
+
161
+ module.exports = { implicitBaseBinding, validateImplicitBase, implicitSourceDefinition };
@@ -2,11 +2,11 @@
2
2
 
3
3
  const path = require('path');
4
4
  const { langTraits } = require('../languages');
5
- const { NON_CALLABLE_TYPES } = require('./shared');
5
+ const { NON_CALLABLE_TYPES, isCallableMember } = 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
+ const { distinctOwnerDefinitions, completeForwardDeclaration } = require('./class-identity');
10
10
 
11
11
  const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'record', 'enum', 'type', 'impl']);
12
12
  const ownerName = definition => definition.className || (definition.receiver || '').replace(/^\*/, '');
@@ -36,13 +36,13 @@ function callableMembersOwnedBy(index, name, owner) {
36
36
  const definitions = index.symbols.get(name) || [];
37
37
  const memo = index._opMemo?.('callableMembersByOwner', () => new WeakMap());
38
38
  if (!memo) {
39
- return definitions.filter(d => !NON_CALLABLE_TYPES.has(d.type) && ownerName(d) === owner);
39
+ return definitions.filter(d => isCallableMember(d) && ownerName(d) === owner);
40
40
  }
41
41
  let byOwner = memo.get(definitions);
42
42
  if (!byOwner) {
43
43
  byOwner = new Map();
44
44
  for (const d of definitions) {
45
- if (NON_CALLABLE_TYPES.has(d.type)) continue;
45
+ if (!isCallableMember(d)) continue;
46
46
  const key = ownerName(d);
47
47
  let list = byOwner.get(key);
48
48
  if (!list) {
@@ -185,6 +185,65 @@ function ownerCountOf(index, name) {
185
185
  return count;
186
186
  }
187
187
 
188
+ /**
189
+ * Every part of a C# `partial` type (one class across declarations, fix
190
+ * #428): same namespace, enclosing type, name and generic arity, each
191
+ * declared `partial`, in (file, line) order; null for any other type.
192
+ */
193
+ function partialTypeParts(index, definition) {
194
+ if (!Array.isArray(definition.modifiers) || !definition.modifiers.includes('partial') ||
195
+ index.files.get(definition.file)?.language !== 'csharp') return null;
196
+ const parts = (index.symbols.get(definition.name) || []).filter(d => d.type === definition.type &&
197
+ Array.isArray(d.modifiers) && d.modifiers.includes('partial') &&
198
+ (d.namespace || null) === (definition.namespace || null) &&
199
+ (d.enclosingType || null) === (definition.enclosingType || null) &&
200
+ (d.typeArity || 0) === (definition.typeArity || 0) &&
201
+ index.files.get(d.file)?.language === 'csharp');
202
+ if (parts.length < 2) return null;
203
+ return parts.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : a.startLine - b.startLine);
204
+ }
205
+
206
+ /**
207
+ * Does the Rust name model (core/rust-names.js) bind a bare name at the call
208
+ * to something other than `declaration` (fix #428)? An undecided model
209
+ * answer keeps the witness.
210
+ */
211
+ function rustNameDisagrees(index, file, call, declaration, ns) {
212
+ if (!langTraits(index.files.get(file)?.language)?.itemScopeResolution || call.isMethod || call.receiver) return false;
213
+ const named = require('./rust-names').resolveRustName(index, file, call.line, call.resolvedName || call.name, ns,
214
+ call.scopeBinding || null);
215
+ if (!named?.defs?.length) return !!named?.external;
216
+ return !named.defs.includes(declaration);
217
+ }
218
+
219
+ /**
220
+ * Does a call select its only candidate without reading any fact (fix
221
+ * #428): no type arguments, no spread, no typed argument, exactly the
222
+ * candidate's fixed, non-generic, non-extension parameter list?
223
+ */
224
+ function soleMemberFits(call, definition) {
225
+ const params = definition.paramsStructured;
226
+ return !call.methodTypeArgs && !call.argSpread && Number.isInteger(call.argCount) &&
227
+ (!Array.isArray(call.argKinds) || call.argKinds.every(kind => kind === 'expr')) &&
228
+ !definition.generics && !definition.isExtensionMethod && Array.isArray(params) &&
229
+ params.length === call.argCount &&
230
+ params.every(param => param && !param.rest && !param.optional && param.default === undefined);
231
+ }
232
+
233
+ /** The generic arity of each top-level entry of a base list (`A, B<T, U>` -> [0, 2]). */
234
+ function writtenArities(clause) {
235
+ const out = [];
236
+ let depth = 0;
237
+ let count = 0;
238
+ for (const ch of `${clause},`) {
239
+ if (ch === '<') { if (depth === 0) count = 1; depth++; }
240
+ else if (ch === '>') depth = Math.max(0, depth - 1);
241
+ else if (ch === ',' && depth === 1) count++;
242
+ else if (ch === ',' && depth === 0) { out.push(count); count = 0; }
243
+ }
244
+ return out;
245
+ }
246
+
188
247
  /** Collect independent facts; a failed lookup never manufactures a negative. */
189
248
  function confirmationFacts(index, file, call, targets, options = {}) {
190
249
  const entry = index.files.get(file);
@@ -217,16 +276,55 @@ function confirmationFacts(index, file, call, targets, options = {}) {
217
276
  };
218
277
  }
219
278
  if (!methodReceiver) {
279
+ // Explicit construction looks up a type. Its same-named constructor
280
+ // declarations are members, not competing import bindings.
281
+ const constructsType = (call.isConstructor || langTraits(language)?.classesCallableWithoutNew) && targets.length > 0 &&
282
+ targets.every(target => TYPE_KINDS.has(target.type));
220
283
  const result = namedDeclaration(index, file, call.resolvedName || call.name,
221
- d => !NON_CALLABLE_TYPES.has(d.type) || call.isConstructor, call.line);
222
- if (result?.chain.length) facts.importChain = result.chain;
284
+ d => constructsType ? TYPE_KINDS.has(d.type)
285
+ : !NON_CALLABLE_TYPES.has(d.type) || call.isConstructor, call.line);
286
+ // Rust binds a bare name by its own item and `use` scopes (fix
287
+ // #422): an import chain naming another item than the language's
288
+ // lookup is no witness of it (fix #428).
289
+ if (result?.chain.length && rustNameDisagrees(index, file, call, result.declaration, constructsType ? 'type' : 'value')) {
290
+ facts.lookupGap = 'rust-name-scope';
291
+ } else if (result?.chain.length) facts.importChain = result.chain;
223
292
  }
224
293
  let type = facts.receiverType;
294
+ let anonymousQualifier = null;
225
295
  if (options.sameClass) {
226
296
  const enclosing = index.findEnclosingFunction(file, call.line, true, call);
227
297
  type = enclosing?.className || enclosing?.receiver?.replace(/^\*/, '');
228
298
  facts.receiverTypeSource = 'same-class';
229
299
  facts.receiverOrigin = { source: 'same-class', declaration: declarationIdentity(enclosing) };
300
+ // `super.m()` / `super().m()` / `base.M()` look the member up from
301
+ // the enclosing class's bases, never its own members (fix #428): the
302
+ // witness starts there too. In an anonymous class body `super` names
303
+ // the instantiated base type (fix #394), looked up as written.
304
+ const superReceiver = langTraits(language)?.superReceiver;
305
+ if (superReceiver && call.isMethod && call.receiver === superReceiver && !call.constructorSyntax) {
306
+ if (call.receiverSuperType) type = call.receiverSuperType;
307
+ else facts.receiverOrigin.superLookup = true;
308
+ }
309
+ // A bare call in an anonymous class body (`new Base() { .. m() .. }`,
310
+ // fix #401) looks the member up from the instantiated base (fix #428).
311
+ if (!call.receiver && call.anonymousSuperType) {
312
+ const parts = String(call.anonymousSuperType).replace(/<.*$/s, '').trim().split('.');
313
+ type = parts.pop();
314
+ if (parts.length) anonymousQualifier = parts.join('.');
315
+ facts.receiverOrigin.anonymousSuperType = call.anonymousSuperType;
316
+ }
317
+ }
318
+ // A static member call is also a reference to the type it is written
319
+ // through (`JsonParser.parseString(..)` for a class query, fix #428):
320
+ // its witness is the type the qualifier names, not a member lookup.
321
+ if (call.isTypeReference && call.receiver && options.resolveType) {
322
+ const written = String(call.receiver).split('.').pop();
323
+ const qualified = String(call.receiver).includes('.') ? String(call.receiver).split('.').slice(0, -1).join('.') : null;
324
+ const named = options.resolveType(written, file, call.line, qualified || call.receiverTypeNamespace || undefined);
325
+ if (named) facts.typeReference = { written: call.receiver, declaration: declarationIdentity(named) };
326
+ else facts.lookupGap = 'type-qualifier-unresolved';
327
+ return facts;
230
328
  }
231
329
  if (language === 'csharp' && call.receiverCastThis) {
232
330
  const enclosing = index.findEnclosingFunction(file, call.line, true, call);
@@ -239,32 +337,52 @@ function confirmationFacts(index, file, call, targets, options = {}) {
239
337
  facts.incomplete = ['receiverType'];
240
338
  return facts;
241
339
  }
340
+ // A C# extension invocation binds a static member of another class
341
+ // through the receiver's `this` parameter: no member lookup on the
342
+ // receiver's type describes it, and this collector has no extension
343
+ // witness yet (a rule not yet validated, fix #428).
344
+ if (options.extensionMethod) {
345
+ facts.lookupGap = 'extension-method';
346
+ return facts;
347
+ }
242
348
  if (facts.receiverTypeSource === 'unknown') facts.incomplete = ['receiverTypeSource'];
243
349
  const originFile = options.originFile || call.receiverTypeFlowFile || file;
244
- const resolveType = (name, context, atLine, qualifier) => {
350
+ // A written generic arity names one of the same-name types where arity is
351
+ // identity (C# `MemberMap` vs `MemberMap<TClass, TMember>`, fix #428).
352
+ const arityFits = (d, arity) => !Number.isInteger(arity) || !langTraits(language)?.genericArityIsIdentity ||
353
+ (d.typeArity || 0) === arity;
354
+ const resolveType = (name, context, atLine, qualifier, arity) => {
245
355
  // A qualified annotation cannot bind to a same-named local type.
246
356
  // Apply the qualifier only to the receiver's first lookup: aliases
247
357
  // and parent annotations are resolved in their own declaration scope.
248
358
  if (qualifier && options.resolveType &&
249
359
  langTraits(language)?.typeSystem !== 'structural') {
250
- const definition = options.resolveType(name, context, atLine, qualifier);
251
- return definition ? { declaration: definition, chain: [] } : null;
360
+ const definition = options.resolveType(name, context, atLine, qualifier, arity);
361
+ return definition ? {
362
+ declaration: completeForwardDeclaration(index, definition, file), chain: [] } : null;
252
363
  }
253
364
  const named = namedDeclaration(index, context, name, d =>
254
- (TYPE_KINDS.has(d.type) && d.type !== 'impl') ||
365
+ ((TYPE_KINDS.has(d.type) && d.type !== 'impl') ||
255
366
  (['javascript', 'typescript', 'tsx'].includes(language) &&
256
367
  d.type === 'function' && !d.className &&
257
- ['constructor', 'flow'].includes(facts.receiverTypeSource)), atLine);
258
- if (named) return named;
368
+ ['constructor', 'flow'].includes(facts.receiverTypeSource))) && arityFits(d, arity), atLine);
369
+ if (named) return { ...named,
370
+ declaration: completeForwardDeclaration(index, named.declaration, file) };
259
371
  // Nominal package/type lookup is delegated to the engine's language
260
372
  // rules, then pinned to a unique declaration in the resolved scope.
261
373
  if (langTraits(language)?.typeSystem !== 'structural' && options.resolveType) {
262
- const definition = options.resolveType(name, context, atLine);
263
- if (definition) return { declaration: definition, chain: [] };
374
+ const definition = options.resolveType(name, context, atLine, undefined, arity);
375
+ if (definition) return {
376
+ declaration: completeForwardDeclaration(index, definition, file), chain: [] };
264
377
  }
265
378
  return null;
266
379
  };
267
- const qualifier = call.receiverTypeNamespace || call.receiverTypeQualifier;
380
+ // A Rust path receiver (`crate::m::T::new()`) names the type its module
381
+ // path declares (fix #428).
382
+ const rustPath = langTraits(language)?.itemScopeResolution && facts.receiverTypeSource === 'type-qualified' &&
383
+ String(call.receiver || '').includes('::') && String(call.receiver).split('::').pop() === type
384
+ ? String(call.receiver).split('::').slice(0, -1).join('::') : null;
385
+ const qualifier = anonymousQualifier || rustPath || call.receiverTypeNamespace || call.receiverTypeQualifier;
268
386
  if (qualifier) facts.receiverTypeQualifier = qualifier;
269
387
  // A parser annotation's qualifier belongs to the consuming file. The
270
388
  // already-pinned declaration file may be another crate/module, where
@@ -304,13 +422,54 @@ function confirmationFacts(index, file, call, targets, options = {}) {
304
422
  if (aliases.length) facts.receiverAliases = aliases;
305
423
  facts.receiverResolvedIn = declarationIdentity(resolved.declaration);
306
424
  const visited = new Set();
307
- const walk = (owner, steps) => {
425
+ // A bare (implicit-this) call whose class hierarchy declares no member
426
+ // of the name looks it up in the lexically enclosing type next (JLS
427
+ // 15.12.1, C# member lookup through enclosing type declarations): the
428
+ // innermost declaration of this file enclosing the type that started
429
+ // the current lexical level (fix #428).
430
+ const lexicalOuterOf = chain => {
431
+ if (call.receiver || facts.receiverTypeSource !== 'same-class' || facts.receiverOrigin?.superLookup) return null;
432
+ let start = 0;
433
+ for (let i = chain.length - 1; i > 0; i--) if (chain[i].enclosingOf) { start = i; break; }
434
+ if (chain.slice(start).some(step => step.members.length > 0)) return null;
435
+ const inner = chain[start].owner;
436
+ if (!inner.enclosingType) return null;
437
+ const segments = String(inner.enclosingType).split('.');
438
+ const name = segments.pop();
439
+ const prefix = segments.length ? segments.join('.') : null;
440
+ const outer = (index.files.get(path.resolve(index.root, inner.file))?.symbols || []).filter(d =>
441
+ TYPE_KINDS.has(d.type) && d.type !== 'impl' && d.name === name && (d.enclosingType || null) === prefix &&
442
+ (d.namespace || null) === (inner.namespace || null) &&
443
+ d.startLine <= inner.startLine && (d.endLine ?? d.startLine) >= inner.endLine)
444
+ .sort((a, b) => b.startLine - a.startLine)[0];
445
+ return outer ? { definition: outer, inner } : null;
446
+ };
447
+ // Choices among same-name members the overload replay cannot state yet
448
+ // (report-only, fix #428): Rust probes inherent impls before trait impls
449
+ // and selects among one type's trait impls by argument type (fixes #384,
450
+ // #399); C++ member pairs that differ in `const` or a ref-qualifier are
451
+ // chosen by the object expression (fix #427).
452
+ const memberChoiceGap = members => {
453
+ if (langTraits(language)?.typeQualifiedCallStyle === 'path' &&
454
+ new Set(members.map(member => (member.traitImpl
455
+ ? `trait:${member.traitName || ''}:${member.implSelfRef || ''}` : 'inherent') +
456
+ `:${(member.ownerSelfArgs || []).join(',')}`)).size > 1) return 'impl-selection';
457
+ if (new Set(members.map(member => `${!!member.memberConst}:${member.refQualifier || ''}`)).size > 1) {
458
+ return 'object-qualified-overload';
459
+ }
460
+ return null;
461
+ };
462
+ const walk = (owner, steps, extra = null) => {
308
463
  const identity = declarationIdentity(owner);
309
464
  const key = identityKey(identity);
310
465
  if (visited.has(key) || visited.size >= 16) return null;
311
466
  visited.add(key);
312
467
  const ownerDefinition = owner.file && TYPE_KINDS.has(owner.type) ? owner : null;
313
- const members = callableMembersOwnedBy(index, call.name, owner.name).filter(d => {
468
+ // The parts of a C# `partial` type are one class (fix #428): members
469
+ // and bases declared in any part belong to it.
470
+ const partialParts = ownerDefinition ? partialTypeParts(index, ownerDefinition) : null;
471
+ const partFiles = partialParts ? new Set(partialParts.map(part => part.file)) : null;
472
+ let members = callableMembersOwnedBy(index, call.name, owner.name).filter(d => {
314
473
  // A member of a same-name class declared in another scope of
315
474
  // the file is not this owner's member (fix #389).
316
475
  if (ownerDefinition && distinctOwnerDefinitions(index, d, ownerDefinition, true)) return false;
@@ -318,21 +477,48 @@ function confirmationFacts(index, file, call, targets, options = {}) {
318
477
  (!facts.receiverCastThis || d.explicitInterface !== facts.receiverCastThis.interfaceType)) return false;
319
478
  if ((d.namespace || null) !== (owner.namespace || null)) return false;
320
479
  if ((d.enclosingType || null) !== (owner.enclosingType || null)) return false;
321
- return d.file === owner.file || (language === 'go' &&
322
- path.dirname(d.file) === path.dirname(owner.file));
480
+ return d.file === owner.file || !!partFiles?.has(d.file) || (language === 'go' &&
481
+ path.dirname(d.file) === path.dirname(owner.file)) ||
482
+ // A Rust impl block anywhere in the crate whose self type
483
+ // names this definition declares its members (fix #428).
484
+ // A member of a same-name type declared in its own file (a
485
+ // function-local struct) belongs to that type.
486
+ (!!ownerDefinition && langTraits(language)?.itemScopeResolution &&
487
+ !require('./class-identity').ownerDefinitionOf(index, d) &&
488
+ !!require('./rust-names').resolveRustName(index, d.file, d.startLine, owner.name, 'type')
489
+ ?.defs?.includes(ownerDefinition));
323
490
  });
491
+ // A super lookup passes over the enclosing class's own members.
492
+ const superLookup = steps.length === 0 && facts.receiverOrigin?.superLookup === true
493
+ ? { skipped: members.map(declarationIdentity) } : null;
494
+ if (superLookup) members = [];
324
495
  // Use the AST spelling before the inheritance graph normalizes aliases
325
496
  // and qualifiers. A terminal name alone loses its declaring scope.
326
- const rawOwner = index.files.get(owner.file)?.symbols.find(d =>
327
- d.name === owner.name && d.startLine === owner.startLine && d.type === owner.type) || owner;
328
- const parentNames = rawOwner.extends ? splitParentList(rawOwner.extends) : [];
497
+ const rawOf = part => index.files.get(part.file)?.symbols.find(d =>
498
+ d.name === part.name && d.startLine === part.startLine && d.type === part.type) || part;
499
+ const rawOwner = rawOf(owner);
500
+ const written = [];
501
+ for (const part of partialParts || [owner]) {
502
+ const raw = part === owner ? rawOwner : rawOf(part);
503
+ const arities = raw.extends ? writtenArities(raw.extends) : [];
504
+ (raw.extends ? splitParentList(raw.extends) : []).forEach((name, i) => written.push({ name, part, arity: arities[i] }));
505
+ }
329
506
  const derefTarget = language === 'rust' ? rawOwner.derefTarget : null;
330
- const parents = [...parentNames, ...(derefTarget ? [derefTarget] : [])]
331
- .map(name => resolveType(name, owner.file, owner.startLine)).filter(Boolean).map(r => r.declaration);
507
+ const resolvedParents = [...written, ...(derefTarget ? [{ name: derefTarget, part: owner }] : [])]
508
+ .map(entry => resolveType(entry.name, entry.part.file, entry.part.startLine, undefined, entry.arity));
509
+ const resolvedCount = resolvedParents.filter(Boolean).length;
510
+ // Two parts may name one base: it is one parent.
511
+ const parentKeys = new Set();
512
+ const parents = resolvedParents.filter(Boolean).map(r => r.declaration).filter(parent => {
513
+ const parentKey = identityKey(declarationIdentity(parent));
514
+ if (parentKeys.has(parentKey)) return false;
515
+ parentKeys.add(parentKey);
516
+ return true;
517
+ });
332
518
  // The member may be inherited from an external declaration. This
333
519
  // collector has no platform-member lookup; report the coverage gap
334
520
  // separately from an ambiguous or inconsistent project lookup.
335
- if (!members.length && parents.length < parentNames.length) {
521
+ if (!members.length && resolvedCount < written.length) {
336
522
  facts.lookupUnsupported = 'external-parent-member';
337
523
  return null;
338
524
  }
@@ -354,10 +540,24 @@ function confirmationFacts(index, file, call, targets, options = {}) {
354
540
  } catch {
355
541
  choice = null;
356
542
  }
543
+ // A base member brought in is chosen by a lookup this
544
+ // witness cannot state yet: report-only (fix #428).
545
+ if (choice?.match && baseMembers.includes(choice.match)) {
546
+ facts.lookupGap = 'using-declaration-member';
547
+ return null;
548
+ }
357
549
  if (!choice?.match || members.length !== 1 || choice.match !== members[0]) return null;
358
550
  }
359
551
  }
552
+ const delegation = call.constructorSyntax === 'delegation' && call.receiver === 'super' &&
553
+ steps.length === 0 && facts.receiverOrigin?.source === 'same-class'
554
+ ? { enclosing: facts.receiverOrigin.declaration, members: members.map(declarationIdentity) } : null;
555
+ if (delegation) members = [];
360
556
  const step = { owner: identity, members: members.map(declarationIdentity), parents: parents.map(declarationIdentity),
557
+ ...(delegation && { constructorDelegation: delegation }),
558
+ ...(superLookup && { superLookup }),
559
+ ...(extra?.enclosingOf && { enclosingOf: extra.enclosingOf }),
560
+ ...(partialParts && { partialParts: partialParts.map(declarationIdentity) }),
361
561
  ...(derefTarget && { derefTarget }) };
362
562
  const getter = facts.valueReference && propertyReadMember(step.members);
363
563
  if (getter) return { receiver: declarationIdentity(resolved.declaration),
@@ -368,7 +568,21 @@ function confirmationFacts(index, file, call, targets, options = {}) {
368
568
  if (parents.length === 1) return walk(parents[0], chain);
369
569
  if (parents.length > 1) return null;
370
570
  const group = overloadMemberGroup(chain);
371
- if (!group?.length) return null;
571
+ if (!group?.length) {
572
+ const outer = lexicalOuterOf(chain);
573
+ return outer ? walk(outer.definition, chain, { enclosingOf: outer.inner }) : null;
574
+ }
575
+ // One member of the name in the whole chain, a call of exactly its
576
+ // fixed parameter count with no typed argument and no type
577
+ // arguments: overload selection reads nothing (fix #428), the
578
+ // lookup ends at the declaring step.
579
+ if (group.length === 1 && soleMemberFits(call, group[0])) {
580
+ const declaring = chain.findIndex(chainStep => chainStep.members.length > 0);
581
+ if (declaring >= 0 && chain[declaring].members.length === 1) {
582
+ return { receiver: declarationIdentity(resolved.declaration), steps: chain.slice(0, declaring + 1),
583
+ selected: declarationIdentity(group[0]) };
584
+ }
585
+ }
372
586
  const overload = captureOverload(index, call, group, language, options.selectOverload);
373
587
  if (!overload) return null;
374
588
  return { receiver: declarationIdentity(resolved.declaration), steps: chain,
@@ -406,6 +620,8 @@ function confirmationFacts(index, file, call, targets, options = {}) {
406
620
  receiver: declarationIdentity(resolved.declaration),
407
621
  steps: [...steps, { ...step, overload }], selected: overload.selected,
408
622
  };
623
+ const gap = memberChoiceGap(members);
624
+ if (gap) facts.lookupGap = gap;
409
625
  // Every member the lookup found is known but the arguments cannot
410
626
  // choose one (fix #396). Name lookup stops at the declaring class,
411
627
  // so the callee is one of its members of that name: the witness
@@ -419,6 +635,20 @@ function confirmationFacts(index, file, call, targets, options = {}) {
419
635
  selected: declarationIdentity(members[0]), memberSet: true,
420
636
  };
421
637
  }
638
+ if (gap) return null;
639
+ }
640
+ const gap = members.length > 1 ? memberChoiceGap(members) : null;
641
+ if (gap) {
642
+ facts.lookupGap = gap;
643
+ return null;
644
+ }
645
+ // A Rust type also takes the methods of traits it implements, a
646
+ // trait's provided methods included: no member of the name among its
647
+ // impls is a lookup this witness cannot finish (report-only, fix #428).
648
+ if (!members.length && !parents.length && langTraits(language)?.itemScopeResolution && !superLookup &&
649
+ ['struct', 'enum', 'union'].includes(owner.type)) {
650
+ facts.lookupGap = 'trait-method-probe';
651
+ return null;
422
652
  }
423
653
  if (members.length || parents.length !== 1) return null;
424
654
  return walk(parents[0], [...steps, step]);
@@ -428,4 +658,4 @@ function confirmationFacts(index, file, call, targets, options = {}) {
428
658
  return facts;
429
659
  }
430
660
 
431
- module.exports = { confirmationFacts, namedDeclaration, occurrenceIdentity, definitionsInFile, definitionAt };
661
+ module.exports = { confirmationFacts, namedDeclaration, occurrenceIdentity, definitionsInFile, definitionAt, partialTypeParts };