gitnexus 1.6.5-rc.43 → 1.6.5-rc.45

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.
@@ -270,12 +270,20 @@ const cCppExtractFunctionName = (node) => {
270
270
  }
271
271
  return { funcName, label };
272
272
  };
273
- /** Check if a C/C++ function_definition is inside a class or struct body.
273
+ /** Check if a C/C++ function_definition is inside a class or struct body
274
+ * (and NOT a friend declaration).
274
275
  * Used by cppLabelOverride to skip duplicate function captures
275
- * that are already covered by definition.method queries. */
276
+ * that are already covered by definition.method queries.
277
+ * Friend functions are free functions defined inside class bodies —
278
+ * they must NOT be skipped (ISO C++ hidden-friend idiom). */
276
279
  function isCppInsideClassOrStruct(functionNode) {
277
280
  let ancestor = functionNode?.parent ?? null;
278
281
  while (ancestor) {
282
+ // Friend declarations: the function_definition is wrapped in
283
+ // `friend_declaration` → `field_declaration_list` → class_specifier.
284
+ // These are free functions, not methods — don't skip them.
285
+ if (ancestor.type === 'friend_declaration')
286
+ return false;
279
287
  if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier')
280
288
  return true;
281
289
  ancestor = ancestor.parent;
@@ -41,10 +41,8 @@
41
41
  * def with that simple name. Locally-declared function-pointer variables
42
42
  * (e.g. `void (*g)()`) and function parameters are excluded from this path.
43
43
  *
44
- * The current implementation also short-circuits to ADL only when ordinary lookup is empty
45
- * (`findCallableBindingInScope` returned undefined). ISO C++ would
46
- * normally merge ADL candidates with ordinary-lookup candidates and
47
- * run overload resolution over the union; V1 defers that merge to V2.
44
+ * ADL candidates are merged with ordinary unqualified-lookup candidates
45
+ * in the free-call fallback before overload narrowing.
48
46
  *
49
47
  * ## Parenthesized-name suppression
50
48
  *
@@ -103,12 +101,6 @@ export interface CppAdlArgInfo {
103
101
  * containing a matching Function/Method def is contributed. */
104
102
  readonly functionRefText?: string;
105
103
  }
106
- /** Sentinel returned by `pickCppAdlCandidates` when ADL surfaces multiple
107
- * candidates that share normalized parameter types — the caller MUST
108
- * suppress (zero edges) rather than pick arbitrarily. Mirrors the
109
- * OVERLOAD_AMBIGUOUS contract from the receiver-bound path. */
110
- export declare const ADL_AMBIGUOUS: unique symbol;
111
- export type AdlResult = SymbolDefinition | typeof ADL_AMBIGUOUS | undefined;
112
104
  /** Record per-call-site argument info. Called once per call site from
113
105
  * `emitCppScopeCaptures`. */
114
106
  export declare function markCppAdlSiteArgs(filePath: string, line: number, col: number, args: readonly CppAdlArgInfo[]): void;
@@ -130,13 +122,10 @@ export declare function clearCppAdlState(): void;
130
122
  */
131
123
  export declare function populateCppAssociatedNamespaces(parsed: ParsedFile): void;
132
124
  /**
133
- * V1 ADL candidate picker. Returns:
134
- * - `SymbolDefinition` — exactly one ADL candidate (or unique survivor
135
- * after narrowing); caller emits the CALLS edge.
136
- * - `ADL_AMBIGUOUS` — multiple candidates with no disambiguator;
137
- * caller MUST suppress (zero edges).
138
- * - `undefined` — no ADL candidates; caller falls through to ordinary
139
- * `pickUniqueGlobalCallable` fallback.
125
+ * ADL candidate collector. Returns:
126
+ * - `readonly SymbolDefinition[]` — ADL candidates to merge with
127
+ * ordinary unqualified lookup candidates.
128
+ * - `undefined` — no ADL candidates.
140
129
  *
141
130
  * Fires only when:
142
131
  * - the call site is not in `noAdlSites` (parenthesized form), AND
@@ -145,10 +134,8 @@ export declare function populateCppAssociatedNamespaces(parsed: ParsedFile): voi
145
134
  */
146
135
  export declare function pickCppAdlCandidates(site: {
147
136
  readonly name: string;
148
- readonly arity?: number;
149
- readonly argumentTypes?: readonly string[];
150
137
  readonly atRange: {
151
138
  startLine: number;
152
139
  startCol: number;
153
140
  };
154
- }, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]): AdlResult;
141
+ }, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]): readonly SymbolDefinition[] | undefined;
@@ -41,10 +41,8 @@
41
41
  * def with that simple name. Locally-declared function-pointer variables
42
42
  * (e.g. `void (*g)()`) and function parameters are excluded from this path.
43
43
  *
44
- * The current implementation also short-circuits to ADL only when ordinary lookup is empty
45
- * (`findCallableBindingInScope` returned undefined). ISO C++ would
46
- * normally merge ADL candidates with ordinary-lookup candidates and
47
- * run overload resolution over the union; V1 defers that merge to V2.
44
+ * ADL candidates are merged with ordinary unqualified-lookup candidates
45
+ * in the free-call fallback before overload narrowing.
48
46
  *
49
47
  * ## Parenthesized-name suppression
50
48
  *
@@ -69,15 +67,10 @@
69
67
  * but logically share the same namespace. ADL must consider candidates
70
68
  * declared in either file.
71
69
  */
72
- import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from '../../scope-resolution/passes/overload-narrowing.js';
70
+ import { isCppInlineNamespaceScope } from './inline-namespaces.js';
73
71
  const argInfoBySite = new Map();
74
72
  const noAdlSites = new Set();
75
73
  const classToNamespaceQualifiedName = new Map();
76
- /** Sentinel returned by `pickCppAdlCandidates` when ADL surfaces multiple
77
- * candidates that share normalized parameter types — the caller MUST
78
- * suppress (zero edges) rather than pick arbitrarily. Mirrors the
79
- * OVERLOAD_AMBIGUOUS contract from the receiver-bound path. */
80
- export const ADL_AMBIGUOUS = Symbol('ADL_AMBIGUOUS');
81
74
  function siteKey(filePath, line, col) {
82
75
  return `${filePath}:${line}:${col}`;
83
76
  }
@@ -124,15 +117,27 @@ export function populateCppAssociatedNamespaces(parsed) {
124
117
  classToNamespaceQualifiedName.set(def.nodeId, nsQName);
125
118
  }
126
119
  }
120
+ // Enum defs live in Namespace scopes directly (not inside Class scopes).
121
+ // Map each Enum def to its enclosing namespace so ADL on enum-typed
122
+ // arguments contributes the correct associated namespace.
123
+ for (const scope of parsed.scopes) {
124
+ if (scope.kind !== 'Namespace')
125
+ continue;
126
+ const nsQName = computeNamespaceQName(scope, scopesById);
127
+ if (nsQName === '')
128
+ continue;
129
+ for (const def of scope.ownedDefs) {
130
+ if (def.type !== 'Enum')
131
+ continue;
132
+ classToNamespaceQualifiedName.set(def.nodeId, nsQName);
133
+ }
134
+ }
127
135
  }
128
136
  /**
129
- * V1 ADL candidate picker. Returns:
130
- * - `SymbolDefinition` — exactly one ADL candidate (or unique survivor
131
- * after narrowing); caller emits the CALLS edge.
132
- * - `ADL_AMBIGUOUS` — multiple candidates with no disambiguator;
133
- * caller MUST suppress (zero edges).
134
- * - `undefined` — no ADL candidates; caller falls through to ordinary
135
- * `pickUniqueGlobalCallable` fallback.
137
+ * ADL candidate collector. Returns:
138
+ * - `readonly SymbolDefinition[]` — ADL candidates to merge with
139
+ * ordinary unqualified lookup candidates.
140
+ * - `undefined` — no ADL candidates.
136
141
  *
137
142
  * Fires only when:
138
143
  * - the call site is not in `noAdlSites` (parenthesized form), AND
@@ -160,6 +165,8 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
160
165
  // Walk every namespace scope in every parsed file; collect callable
161
166
  // ownedDefs whose enclosing namespace matches one of the associated
162
167
  // QNames AND whose simple name matches the call's name.
168
+ // ISO C++: inline namespaces are transparent — candidates in inline
169
+ // children of an associated namespace are also ADL-reachable.
163
170
  const candidates = [];
164
171
  const seenKey = new Set();
165
172
  for (const parsed of parsedFiles) {
@@ -170,8 +177,20 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
170
177
  if (scope.kind !== 'Namespace')
171
178
  continue;
172
179
  const qName = computeNamespaceQName(scope, scopesById);
173
- if (!associatedNamespaces.has(qName))
174
- continue;
180
+ if (!associatedNamespaces.has(qName)) {
181
+ // Check if this is an inline-namespace child of an associated NS.
182
+ // ISO C++ inline namespaces are transparent for ADL: if the outer
183
+ // namespace is in the associated set, candidates in the inline child
184
+ // are also reachable.
185
+ if (!isCppInlineNamespaceScope(scope.id))
186
+ continue;
187
+ const parentScope = scope.parent !== null ? scopesById.get(scope.parent) : undefined;
188
+ if (parentScope === undefined || parentScope.kind !== 'Namespace')
189
+ continue;
190
+ const parentQName = computeNamespaceQName(parentScope, scopesById);
191
+ if (!associatedNamespaces.has(parentQName))
192
+ continue;
193
+ }
175
194
  for (const def of scope.ownedDefs) {
176
195
  if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
177
196
  continue;
@@ -189,26 +208,66 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
189
208
  candidates.push(def);
190
209
  }
191
210
  }
211
+ // ISO C++ `[basic.lookup.argdep]` §2: hidden friend functions declared
212
+ // inside a class body are visible via ADL when the class is an associated
213
+ // class. Scan Class scopes whose enclosing namespace is in the associated
214
+ // set for callable ownedDefs matching the call name. This enables the
215
+ // canonical "hidden friend" idiom:
216
+ // struct Foo { friend void swap(Foo&, Foo&) {} };
217
+ for (const scope of parsed.scopes) {
218
+ if (scope.kind !== 'Class')
219
+ continue;
220
+ // Check if ANY class def in this scope has an associated namespace.
221
+ let isAssociatedClass = false;
222
+ for (const def of scope.ownedDefs) {
223
+ if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
224
+ continue;
225
+ const nsQName = classToNamespaceQualifiedName.get(def.nodeId);
226
+ if (nsQName !== undefined && associatedNamespaces.has(nsQName)) {
227
+ isAssociatedClass = true;
228
+ break;
229
+ }
230
+ }
231
+ if (!isAssociatedClass)
232
+ continue;
233
+ // Also scan Function scopes that are direct children of this class
234
+ // scope — friend function definitions create their own Function scope
235
+ // underneath the Class scope.
236
+ for (const childScope of parsed.scopes) {
237
+ if (childScope.parent !== scope.id)
238
+ continue;
239
+ if (childScope.kind !== 'Function')
240
+ continue;
241
+ for (const def of childScope.ownedDefs) {
242
+ if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
243
+ continue;
244
+ }
245
+ const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
246
+ if (simple !== site.name)
247
+ continue;
248
+ if (seenKey.has(def.nodeId))
249
+ continue;
250
+ seenKey.add(def.nodeId);
251
+ candidates.push(def);
252
+ }
253
+ }
254
+ for (const def of scope.ownedDefs) {
255
+ if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
256
+ continue;
257
+ }
258
+ const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
259
+ if (simple !== site.name)
260
+ continue;
261
+ if (seenKey.has(def.nodeId))
262
+ continue;
263
+ seenKey.add(def.nodeId);
264
+ candidates.push(def);
265
+ }
266
+ }
192
267
  }
193
268
  if (candidates.length === 0)
194
269
  return undefined;
195
- if (candidates.length === 1)
196
- return candidates[0];
197
- // Multi-candidate: narrow then check ambiguity. Reuses the OVERLOAD_AMBIGUOUS
198
- // sentinel contract from `overload-narrowing.ts` so int/long-collision-style
199
- // ambiguity also suppresses on the ADL path.
200
- const narrowed = narrowOverloadCandidates(candidates, site.arity, site.argumentTypes);
201
- if (narrowed.length === 1)
202
- return narrowed[0];
203
- if (narrowed.length === 0)
204
- return undefined;
205
- if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity))
206
- return ADL_AMBIGUOUS;
207
- // Multiple surviving candidates that aren't normalization-ambiguous —
208
- // ISO C++ would run overload resolution; V1 lacks conversion ranking so
209
- // suppress rather than pick arbitrarily. Mirrors `pickImplicitThisOverload`'s
210
- // unique-survivor requirement (see `pick-implicit-this-overload.test.ts`).
211
- return ADL_AMBIGUOUS;
270
+ return candidates;
212
271
  }
213
272
  function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces) {
214
273
  // For template args this may be the template name itself (e.g. `vector`);
@@ -216,8 +275,8 @@ function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces)
216
275
  // V1/V2 simplification).
217
276
  addAssociatedNamespaceForClassName(arg.simpleClassName, scopes, associatedNamespaces);
218
277
  // Includes template-owner namespaces (e.g. `std` in std::vector<T>). If
219
- // that surfaces extra candidates, ADL_AMBIGUOUS suppression below prevents
220
- // arbitrary edge emission.
278
+ // that surfaces extra candidates, merged-candidate overload narrowing in
279
+ // free-call-fallback suppresses arbitrary edge emission.
221
280
  if (arg.templateNamespace.length > 0)
222
281
  associatedNamespaces.add(arg.templateNamespace);
223
282
  for (const ns of arg.templateArgNamespaces) {
@@ -316,15 +375,21 @@ function findNamespaceDefInScope(scope) {
316
375
  }
317
376
  return undefined;
318
377
  }
319
- /** Find a class-like def by simple name across the workspace. V1
320
- * still arbitrary-picks the first class on collisions (multiple classes
378
+ /** Find a class-like or enum def by simple name across the workspace.
379
+ * V1 still arbitrary-picks the first match on collisions (multiple defs
321
380
  * share the simple name), but reports the collision so callers can avoid
322
381
  * amplifying that uncertainty (for example by skipping MRO expansion).
323
- * C++ ADL strictness would require full type-driven lookup. */
382
+ * C++ ADL strictness would require full type-driven lookup.
383
+ *
384
+ * ISO C++ `[basic.lookup.argdep]` §2: enumerations contribute their
385
+ * enclosing namespace to the associated set, just like class types. */
324
386
  function findCppClassDefBySimpleName(simpleName, scopes) {
325
387
  let firstMatch;
326
388
  for (const def of scopes.defs.byId.values()) {
327
- if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
389
+ if (def.type !== 'Class' &&
390
+ def.type !== 'Struct' &&
391
+ def.type !== 'Interface' &&
392
+ def.type !== 'Enum')
328
393
  continue;
329
394
  const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
330
395
  if (simple !== simpleName)
@@ -60,8 +60,8 @@ export declare function isCppInlineNamespaceScope(scopeId: ScopeId): boolean;
60
60
  * Returns the most specific (innermost) match — for `outer::foo()`
61
61
  * where `inline namespace v1` declares `foo`, returns `v1::foo`. When
62
62
  * multiple inline-namespace children declare the same name, ISO C++
63
- * leaves the call ambiguous; V1 returns the first match in source
64
- * order (stable across runs).
63
+ * leaves the call ambiguous; returns `'ambiguous'` so the caller
64
+ * suppresses edge emission rather than picking arbitrarily (#1564).
65
65
  */
66
- export declare function resolveCppQualifiedNamespaceMember(receiverName: string, memberName: string, parsedFiles: readonly ParsedFile[], _scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
66
+ export declare function resolveCppQualifiedNamespaceMember(receiverName: string, memberName: string, parsedFiles: readonly ParsedFile[], _scopes: ScopeResolutionIndexes): SymbolDefinition | 'ambiguous' | undefined;
67
67
  export {};
@@ -26,6 +26,7 @@
26
26
  * `std::vector` qualified calls resolve to the inline-namespace
27
27
  * declaration transparently.
28
28
  */
29
+ import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from '../../scope-resolution/passes/overload-narrowing.js';
29
30
  const inlineNamespaceRangesByFile = new Map();
30
31
  const inlineNamespaceScopeIds = new Set();
31
32
  function rangeKey(r) {
@@ -80,10 +81,12 @@ export function isCppInlineNamespaceScope(scopeId) {
80
81
  * Returns the most specific (innermost) match — for `outer::foo()`
81
82
  * where `inline namespace v1` declares `foo`, returns `v1::foo`. When
82
83
  * multiple inline-namespace children declare the same name, ISO C++
83
- * leaves the call ambiguous; V1 returns the first match in source
84
- * order (stable across runs).
84
+ * leaves the call ambiguous; returns `'ambiguous'` so the caller
85
+ * suppresses edge emission rather than picking arbitrarily (#1564).
85
86
  */
86
87
  export function resolveCppQualifiedNamespaceMember(receiverName, memberName, parsedFiles, _scopes) {
88
+ const allHits = [];
89
+ const seenNodeId = new Set();
87
90
  for (const parsed of parsedFiles) {
88
91
  const scopesById = new Map();
89
92
  for (const sc of parsed.scopes)
@@ -97,27 +100,58 @@ export function resolveCppQualifiedNamespaceMember(receiverName, memberName, par
97
100
  const nsName = nsDef.qualifiedName?.split('.').pop() ?? nsDef.qualifiedName ?? '';
98
101
  if (nsName !== receiverName)
99
102
  continue;
100
- // Found a matching namespace scope in this file. Collect the
101
- // member transitively through any inline-namespace children.
102
- const hit = findMemberInNamespaceTransitive(scope, scopesById, memberName);
103
- if (hit !== undefined)
104
- return hit;
103
+ // Found a matching namespace scope in this file. Collect ALL
104
+ // members transitively through any inline-namespace children.
105
+ const hits = findMemberInNamespaceTransitive(scope, scopesById, memberName);
106
+ for (const hit of hits) {
107
+ if (seenNodeId.has(hit.nodeId))
108
+ continue;
109
+ seenNodeId.add(hit.nodeId);
110
+ allHits.push(hit);
111
+ }
105
112
  }
106
113
  }
107
- return undefined;
114
+ if (allHits.length === 0)
115
+ return undefined;
116
+ if (allHits.length === 1)
117
+ return allHits[0];
118
+ // Multi-candidate: the `resolveQualifiedReceiverMember` hook has no
119
+ // access to call-site arity or argument types, so
120
+ // `narrowOverloadCandidates` cannot actually narrow here — the call
121
+ // with `(allHits, undefined, undefined)` is effectively a pass-through.
122
+ // We retain it so that `isOverloadAmbiguousAfterNormalization` can
123
+ // still detect int/long-style normalization collisions on this path,
124
+ // but for any multi-hit case where candidates have genuinely distinct
125
+ // signatures (e.g. `foo(int)` vs `foo(double)` in different inline
126
+ // children), we conservatively suppress rather than pick arbitrarily.
127
+ // A future enhancement could thread call-site argument info through
128
+ // the `resolveQualifiedReceiverMember` contract to enable real
129
+ // narrowing here.
130
+ const narrowed = narrowOverloadCandidates(allHits, undefined, undefined);
131
+ if (narrowed.length === 1)
132
+ return narrowed[0];
133
+ if (narrowed.length === 0)
134
+ return undefined;
135
+ if (isOverloadAmbiguousAfterNormalization(narrowed, undefined))
136
+ return 'ambiguous';
137
+ // Multiple surviving candidates (distinct signatures) — conservative
138
+ // suppress because we lack call-site info to disambiguate.
139
+ return 'ambiguous';
108
140
  }
109
141
  /** Recursively search a namespace scope and any inline-namespace
110
- * descendants for a callable def with the given simple name. Non-inline
142
+ * descendants for callable defs with the given simple name. Non-inline
111
143
  * nested namespaces are NOT traversed — they require explicit
112
- * qualification (`outer::nested::foo`). */
144
+ * qualification (`outer::nested::foo`). Returns ALL matches so the
145
+ * caller can detect same-name ambiguity across inline children (#1564). */
113
146
  function findMemberInNamespaceTransitive(scope, scopesById, memberName) {
147
+ const results = [];
114
148
  // Check this scope's own ownedDefs first.
115
149
  for (const def of scope.ownedDefs) {
116
150
  if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor')
117
151
  continue;
118
152
  const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
119
153
  if (simple === memberName)
120
- return def;
154
+ results.push(def);
121
155
  }
122
156
  // Descend into inline-namespace children.
123
157
  for (const childScope of scopesById.values()) {
@@ -127,11 +161,11 @@ function findMemberInNamespaceTransitive(scope, scopesById, memberName) {
127
161
  continue;
128
162
  if (!inlineNamespaceScopeIds.has(childScope.id))
129
163
  continue;
130
- const hit = findMemberInNamespaceTransitive(childScope, scopesById, memberName);
131
- if (hit !== undefined)
132
- return hit;
164
+ const childHits = findMemberInNamespaceTransitive(childScope, scopesById, memberName);
165
+ for (const hit of childHits)
166
+ results.push(hit);
133
167
  }
134
- return undefined;
168
+ return results;
135
169
  }
136
170
  function findNamespaceDefInScope(scope) {
137
171
  for (const def of scope.ownedDefs) {
@@ -9,10 +9,9 @@ import { resolveCppImportTarget } from './import-target.js';
9
9
  import { scanCppHeaderFiles } from './header-scan.js';
10
10
  import { expandCppWildcardNames, isFileLocal, clearFileLocalNames, populateCppAnonymousNamespaceScopes, populateCppNonGloballyVisible, isCppDefGloballyVisible, } from './file-local-linkage.js';
11
11
  import { populateCppDependentBases, clearCppDependentBases, isCppDependentBaseMember, } from './two-phase-lookup.js';
12
- import { populateCppAssociatedNamespaces, clearCppAdlState, pickCppAdlCandidates, ADL_AMBIGUOUS, } from './adl.js';
12
+ import { populateCppAssociatedNamespaces, clearCppAdlState, pickCppAdlCandidates } from './adl.js';
13
13
  import { clearCppInlineNamespaces, populateCppInlineNamespaceScopes, resolveCppQualifiedNamespaceMember, } from './inline-namespaces.js';
14
14
  import { populateCppRangeBindings } from './range-bindings.js';
15
- import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from '../../scope-resolution/passes/overload-narrowing.js';
16
15
  /**
17
16
  * C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
18
17
  * the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
@@ -187,12 +186,12 @@ export const cppScopeResolver = {
187
186
  return !isCppDependentBaseMember(callerScope, candidate, scopes);
188
187
  },
189
188
  // C++ argument-dependent / Koenig lookup (U2 of plan 2026-05-13-001).
190
- // Fires after `findCallableBindingInScope` returns undefined; surfaces
191
- // candidates from the associated namespaces of class-typed arguments.
189
+ // Contributes candidates from associated namespaces of class-typed
190
+ // arguments; caller merges with ordinary unqualified lookup candidates.
192
191
  // Current boundary: class-typed value/pointer/reference args and template
193
192
  // specializations with explicit type arguments contribute associated
194
- // namespaces. Function-pointer args, base-class associated namespaces,
195
- // and full ordinary+ADL merge remain excluded.
193
+ // namespaces. Function-pointer args and full conversion-ranking remain
194
+ // excluded.
196
195
  resolveAdlCandidates: (site, callerParsed, scopes, parsedFiles) => {
197
196
  // `using ns::name;` introduces `name` into ordinary unqualified lookup.
198
197
  // For template-class method bodies, lexical scope walks can miss this
@@ -206,26 +205,31 @@ export const cppScopeResolver = {
206
205
  if (imp.localName !== site.name)
207
206
  continue;
208
207
  const member = resolveCppQualifiedNamespaceMember(imp.targetRaw, imp.importedName, parsedFiles, scopes);
209
- if (member === undefined)
208
+ if (member === undefined || member === 'ambiguous')
210
209
  continue;
211
210
  if (seenUsing.has(member.nodeId))
212
211
  continue;
213
212
  seenUsing.add(member.nodeId);
214
213
  usingNamedHits.push(member);
215
214
  }
216
- if (usingNamedHits.length > 0) {
217
- const narrowed = narrowOverloadCandidates(usingNamedHits, site.arity, site.argumentTypes);
218
- if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity))
219
- return 'ambiguous';
220
- if (narrowed.length === 1)
221
- return narrowed[0];
222
- if (narrowed.length > 1)
223
- return 'ambiguous';
215
+ const adlHits = pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles);
216
+ if (usingNamedHits.length === 0)
217
+ return adlHits;
218
+ if (adlHits === undefined || adlHits.length === 0)
219
+ return usingNamedHits;
220
+ const merged = [];
221
+ const seen = new Set();
222
+ for (const hit of usingNamedHits) {
223
+ seen.add(hit.nodeId);
224
+ merged.push(hit);
224
225
  }
225
- const result = pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles);
226
- if (result === ADL_AMBIGUOUS)
227
- return 'ambiguous';
228
- return result;
226
+ for (const hit of adlHits) {
227
+ if (seen.has(hit.nodeId))
228
+ continue;
229
+ seen.add(hit.nodeId);
230
+ merged.push(hit);
231
+ }
232
+ return merged;
229
233
  },
230
234
  // C++ qualified namespace-member resolution (U5 of plan 2026-05-13-001).
231
235
  // Handles `outer::foo()` where `outer` is a namespace (not a class).
@@ -506,16 +506,15 @@ export interface ScopeResolver {
506
506
  * Optional argument-dependent-lookup (ADL / Koenig lookup) hook for
507
507
  * languages with C++-style associated-namespace candidate addition.
508
508
  *
509
- * Runs in the free-call fallback AFTER `findCallableBindingInScope`
510
- * returns `undefined` and BEFORE `pickUniqueGlobalCallable`. The hook
511
- * inspects the call site's argument types, computes the associated
512
- * namespace set, and returns either:
513
- * - a unique `SymbolDefinition` — emit the CALLS edge to it.
514
- * - `'ambiguous'` — multiple candidates share normalized parameter
515
- * types; the caller MUST suppress (zero edges). Mirrors the
516
- * OVERLOAD_AMBIGUOUS sentinel from `overload-narrowing.ts`.
517
- * - `undefined` — no ADL candidates; caller falls through to the
518
- * global free-call fallback (`pickUniqueGlobalCallable`).
509
+ * Runs in the free-call fallback alongside ordinary unqualified lookup.
510
+ * The fallback merges ordinary candidates with ADL candidates and applies
511
+ * overload narrowing over the union.
512
+ *
513
+ * The hook inspects the call site's argument types, computes the
514
+ * associated namespace set, and returns either:
515
+ * - an array of candidate `SymbolDefinition`s to add to the
516
+ * ordinary-lookup candidate pool.
517
+ * - `undefined` when ADL contributes no candidates.
519
518
  *
520
519
  * Languages without C++-style ADL leave this undefined. The
521
520
  * cross-language contract is "additive tier" — defining the hook never
@@ -529,7 +528,7 @@ export interface ScopeResolver {
529
528
  readonly startLine: number;
530
529
  readonly startCol: number;
531
530
  };
532
- }, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => SymbolDefinition | 'ambiguous' | undefined;
531
+ }, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => readonly SymbolDefinition[] | undefined;
533
532
  /**
534
533
  * Optional resolver for qualified-receiver member calls where the
535
534
  * receiver is a namespace (not a class) and ordinary scope-chain /
@@ -543,10 +542,11 @@ export interface ScopeResolver {
543
542
  *
544
543
  * Receiver-bound-calls invokes this hook AFTER Case 1 (namespace
545
544
  * imports) and AFTER Case 2 (class-name receiver) fail to resolve.
546
- * Returns the target def, or `undefined` to fall through to the
547
- * remaining cases.
545
+ * Returns the target def, `'ambiguous'` when multiple inline-namespace
546
+ * children declare the same name (suppresses edge emission), or
547
+ * `undefined` to fall through to the remaining cases.
548
548
  */
549
- readonly resolveQualifiedReceiverMember?: (receiverName: string, memberName: string, callerScope: ScopeId, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => SymbolDefinition | undefined;
549
+ readonly resolveQualifiedReceiverMember?: (receiverName: string, memberName: string, callerScope: ScopeId, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => SymbolDefinition | 'ambiguous' | undefined;
550
550
  /**
551
551
  * Enable the receiver-bound Case 0.5 fallback for explicit `this`
552
552
  * receivers (`this->m()` / `this.m()`) that resolves against the
@@ -41,7 +41,7 @@ export declare function emitFreeCallFallback(graph: KnowledgeGraph, scopes: Scop
41
41
  readonly startLine: number;
42
42
  readonly startCol: number;
43
43
  };
44
- }, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => SymbolDefinition | 'ambiguous' | undefined;
44
+ }, callerParsed: ParsedFile, scopes: ScopeResolutionIndexes, parsedFiles: readonly ParsedFile[]) => readonly SymbolDefinition[] | undefined;
45
45
  }): number;
46
46
  /** Walk up from the call-site scope to the enclosing class scope,
47
47
  * pick a method member by name with overload narrowing on arity +
@@ -17,8 +17,8 @@
17
17
  * generalization plan.
18
18
  */
19
19
  import { resolveCallerGraphId, resolveDefGraphId } from '../graph-bridge/ids.js';
20
- import { findCallableBindingInScope, findClassBindingInScope } from '../scope/walkers.js';
21
- import { narrowOverloadCandidates } from './overload-narrowing.js';
20
+ import { findCallableBindingInScope, findCallableBindingsAndAdlBlocker, findClassBindingInScope, } from '../scope/walkers.js';
21
+ import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from './overload-narrowing.js';
22
22
  export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _referenceIndex, handledSites, model, workspaceIndex, options = {}) {
23
23
  let emitted = 0;
24
24
  const seen = new Set();
@@ -48,36 +48,74 @@ export function emitFreeCallFallback(graph, scopes, parsedFiles, nodeLookup, _re
48
48
  fnDef = pickImplicitThisOverload(site, scopes, workspaceIndex, model);
49
49
  }
50
50
  if (fnDef === undefined) {
51
- fnDef = findCallableBindingInScope(site.inScope, site.name, scopes);
52
- }
53
- // V1 ADL tier (C++ Koenig lookup, opt-in via provider.resolveAdlCandidates).
54
- // Fires only when ordinary lookup is empty — V1 limitation per
55
- // plan 2026-05-13-001 U2; ISO C++ would merge ADL with ordinary lookup
56
- // and run overload resolution over the union.
57
- //
58
- // Sentinel 'ambiguous': ADL surfaced multiple candidates with
59
- // identical normalized parameter types (mirrors OVERLOAD_AMBIGUOUS).
60
- // We mark the site handled so `emit-references` does not retry, and
61
- // continue to the next site without emitting an edge.
62
- if (fnDef === undefined && options.resolveAdlCandidates !== undefined) {
63
- const adlResult = options.resolveAdlCandidates({
64
- name: site.name,
65
- arity: site.arity,
66
- argumentTypes: site.argumentTypes,
67
- atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
68
- }, parsed, scopes, parsedFiles);
69
- if (adlResult === 'ambiguous') {
70
- handledSites.add(`${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`);
71
- continue;
51
+ if (options.resolveAdlCandidates === undefined) {
52
+ fnDef = findCallableBindingInScope(site.inScope, site.name, scopes);
72
53
  }
73
- if (adlResult !== undefined) {
74
- fnDef = adlResult;
54
+ else {
55
+ // ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when
56
+ // ordinary lookup finds a non-function name (variable, class, enum)
57
+ // or a block-scope function declaration (not via using-declaration)
58
+ // at the nearest scope where the name exists.
59
+ const { callables: ordinary, nonCallableFound, blockScopeDeclFound, } = findCallableBindingsAndAdlBlocker(site.inScope, site.name, scopes);
60
+ const adlSuppressed = nonCallableFound || blockScopeDeclFound;
61
+ const adl = adlSuppressed
62
+ ? undefined
63
+ : options.resolveAdlCandidates({
64
+ name: site.name,
65
+ arity: site.arity,
66
+ argumentTypes: site.argumentTypes,
67
+ atRange: { startLine: site.atRange.startLine, startCol: site.atRange.startCol },
68
+ }, parsed, scopes, parsedFiles);
69
+ // Preserve existing ordinary-lookup behavior when ADL contributed
70
+ // no candidates.
71
+ if (adl === undefined || adl.length === 0) {
72
+ fnDef = ordinary[0];
73
+ }
74
+ else {
75
+ const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
76
+ const merged = [];
77
+ const seen = new Set();
78
+ const push = (defs) => {
79
+ for (const d of defs) {
80
+ if (seen.has(d.nodeId))
81
+ continue;
82
+ seen.add(d.nodeId);
83
+ merged.push(d);
84
+ }
85
+ };
86
+ push(ordinary);
87
+ push(adl);
88
+ const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes);
89
+ if (narrowed.length === 1) {
90
+ fnDef = narrowed[0];
91
+ }
92
+ else if (narrowed.length === 0) {
93
+ // ADL contributed candidates, but none survived arity/type
94
+ // narrowing. Treat as handled to avoid global-name fallback
95
+ // binding to the same mismatched symbol by simple-name
96
+ // uniqueness.
97
+ handledSites.add(siteKey);
98
+ continue;
99
+ }
100
+ else if (narrowed.length > 1) {
101
+ // Suppress ambiguous overload calls (emit zero edges) when
102
+ // merged ordinary+ADL candidate sets cannot be disambiguated.
103
+ if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
104
+ handledSites.add(siteKey);
105
+ continue;
106
+ }
107
+ // Multiple survivors remain but no conversion-ranking step
108
+ // exists yet; suppress instead of picking arbitrarily.
109
+ handledSites.add(siteKey);
110
+ continue;
111
+ }
112
+ }
75
113
  }
76
114
  }
77
115
  // V1: pickUniqueGlobalCallable ignores import context — resolves to any
78
116
  // globally-unique callable. False cross-package edges are possible when
79
117
  // the caller does not import the target package. Same-package calls are
80
- // caught by findCallableBindingInScope above before reaching here.
118
+ // usually caught by nearest-scope lookup before reaching here.
81
119
  if (fnDef === undefined && options.allowGlobalFallback === true) {
82
120
  fnDef = pickUniqueGlobalCallable(site.name, model, scopes, parsed.filePath, options.isFileLocalDef, site.arity, options.isCallableVisibleFromCaller !== undefined
83
121
  ? (candidate) => options.isCallableVisibleFromCaller({
@@ -324,6 +324,12 @@ export function emitReceiverBoundCalls(graph, scopes, parsedFiles, nodeLookup, h
324
324
  // class with the same simple name.
325
325
  if (provider.resolveQualifiedReceiverMember !== undefined) {
326
326
  const memberDef = provider.resolveQualifiedReceiverMember(receiverName, memberName, site.inScope, scopes, parsedFiles);
327
+ if (memberDef === 'ambiguous') {
328
+ // Same-name ambiguity across inline-namespace children (#1564):
329
+ // suppress edge emission, mark site handled.
330
+ handledSites.add(siteKey);
331
+ continue;
332
+ }
327
333
  if (memberDef !== undefined) {
328
334
  const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, memberDef.filePath !== parsed.filePath ? 'import-resolved' : 'global', seen, 0.85, collapse);
329
335
  if (ok)
@@ -102,6 +102,36 @@ export declare function findClassBindingInScope(startScope: ScopeId, receiverNam
102
102
  * def-type predicate differs.
103
103
  */
104
104
  export declare function findCallableBindingInScope(startScope: ScopeId, callableName: string, scopes: ScopeResolutionIndexes): SymbolDefinition | undefined;
105
+ /**
106
+ * Look up all callable bindings (Function/Method/Constructor) by name
107
+ * from the nearest scope in the chain that binds `callableName`.
108
+ *
109
+ * Preserves the original scope-walk boundary used by
110
+ * `findCallableBindingInScope`: once any callable binding is found in a
111
+ * scope, outer scopes are not consulted.
112
+ */
113
+ export declare function findAllCallableBindingsInScope(startScope: ScopeId, callableName: string, scopes: ScopeResolutionIndexes): readonly SymbolDefinition[];
114
+ /**
115
+ * ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when ordinary
116
+ * unqualified lookup finds:
117
+ * - a name that is NOT a function or function template, OR
118
+ * - a block-scope function declaration that is NOT a using-declaration.
119
+ *
120
+ * Combined walker that stops at the **nearest scope** where `name` has any
121
+ * binding (callable or non-callable) and returns:
122
+ * - `callables`: Function/Method/Constructor defs found at that scope
123
+ * - `nonCallableFound`: a non-function binding was present (variable, class, etc.)
124
+ * - `blockScopeDeclFound`: a callable was found at a Function or Block scope
125
+ * (block-scope function declaration that blocks ADL)
126
+ *
127
+ * One pass, one stop — no divergence between callable collection and blocker
128
+ * detection.
129
+ */
130
+ export declare function findCallableBindingsAndAdlBlocker(startScope: ScopeId, name: string, scopes: ScopeResolutionIndexes): {
131
+ callables: readonly SymbolDefinition[];
132
+ nonCallableFound: boolean;
133
+ blockScopeDeclFound: boolean;
134
+ };
105
135
  /**
106
136
  * Populate `ownerId` on every def structurally owned by a Class
107
137
  * scope — methods (defs in Function scopes whose parent is Class)
@@ -209,32 +209,114 @@ export function findClassBindingInScope(startScope, receiverName, scopes) {
209
209
  * def-type predicate differs.
210
210
  */
211
211
  export function findCallableBindingInScope(startScope, callableName, scopes) {
212
+ return findAllCallableBindingsInScope(startScope, callableName, scopes)[0];
213
+ }
214
+ /**
215
+ * Look up all callable bindings (Function/Method/Constructor) by name
216
+ * from the nearest scope in the chain that binds `callableName`.
217
+ *
218
+ * Preserves the original scope-walk boundary used by
219
+ * `findCallableBindingInScope`: once any callable binding is found in a
220
+ * scope, outer scopes are not consulted.
221
+ */
222
+ export function findAllCallableBindingsInScope(startScope, callableName, scopes) {
212
223
  let currentId = startScope;
213
224
  const visited = new Set();
214
225
  while (currentId !== null) {
215
226
  if (visited.has(currentId))
216
- return undefined;
227
+ return [];
217
228
  visited.add(currentId);
218
229
  const scope = scopes.scopeTree.getScope(currentId);
219
230
  if (scope === undefined)
220
- return undefined;
231
+ return [];
232
+ const out = [];
233
+ const seen = new Set();
234
+ const pushCallable = (def) => {
235
+ if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor')
236
+ return;
237
+ if (seen.has(def.nodeId))
238
+ return;
239
+ seen.add(def.nodeId);
240
+ out.push(def);
241
+ };
221
242
  const localBindings = scope.bindings.get(callableName);
222
243
  if (localBindings !== undefined) {
223
244
  for (const b of localBindings) {
224
- if (b.def.type === 'Function' || b.def.type === 'Method' || b.def.type === 'Constructor') {
225
- return b.def;
226
- }
245
+ pushCallable(b.def);
227
246
  }
228
247
  }
229
248
  const importedBindings = lookupBindingsAt(currentId, callableName, scopes);
230
249
  for (const b of importedBindings) {
231
- if (b.def.type === 'Function' || b.def.type === 'Method' || b.def.type === 'Constructor') {
232
- return b.def;
250
+ pushCallable(b.def);
251
+ }
252
+ if (out.length > 0)
253
+ return out;
254
+ currentId = scope.parent;
255
+ }
256
+ return [];
257
+ }
258
+ /**
259
+ * ISO C++ `[basic.lookup.unqual]` §7: ADL is suppressed when ordinary
260
+ * unqualified lookup finds:
261
+ * - a name that is NOT a function or function template, OR
262
+ * - a block-scope function declaration that is NOT a using-declaration.
263
+ *
264
+ * Combined walker that stops at the **nearest scope** where `name` has any
265
+ * binding (callable or non-callable) and returns:
266
+ * - `callables`: Function/Method/Constructor defs found at that scope
267
+ * - `nonCallableFound`: a non-function binding was present (variable, class, etc.)
268
+ * - `blockScopeDeclFound`: a callable was found at a Function or Block scope
269
+ * (block-scope function declaration that blocks ADL)
270
+ *
271
+ * One pass, one stop — no divergence between callable collection and blocker
272
+ * detection.
273
+ */
274
+ export function findCallableBindingsAndAdlBlocker(startScope, name, scopes) {
275
+ let currentId = startScope;
276
+ const visited = new Set();
277
+ while (currentId !== null) {
278
+ if (visited.has(currentId))
279
+ return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
280
+ visited.add(currentId);
281
+ const scope = scopes.scopeTree.getScope(currentId);
282
+ if (scope === undefined)
283
+ return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
284
+ const callables = [];
285
+ const seen = new Set();
286
+ let nonCallableFound = false;
287
+ let anyBinding = false;
288
+ const process = (def) => {
289
+ anyBinding = true;
290
+ if (def.type === 'Function' || def.type === 'Method' || def.type === 'Constructor') {
291
+ if (!seen.has(def.nodeId)) {
292
+ seen.add(def.nodeId);
293
+ callables.push(def);
294
+ }
295
+ }
296
+ else {
297
+ nonCallableFound = true;
298
+ }
299
+ };
300
+ const localBindings = scope.bindings.get(name);
301
+ if (localBindings !== undefined) {
302
+ for (const b of localBindings) {
303
+ process(b.def);
233
304
  }
234
305
  }
306
+ const importedBindings = lookupBindingsAt(currentId, name, scopes);
307
+ for (const b of importedBindings) {
308
+ process(b.def);
309
+ }
310
+ if (anyBinding) {
311
+ // ISO C++: a block-scope function declaration (Function or Block scope)
312
+ // that is NOT a using-declaration blocks ADL. If we found callables at
313
+ // a function/block scope, ADL must be suppressed.
314
+ const blockScopeDeclFound = callables.length > 0 && (scope.kind === 'Function' || scope.kind === 'Block');
315
+ return { callables, nonCallableFound, blockScopeDeclFound };
316
+ }
235
317
  currentId = scope.parent;
236
318
  }
237
- return undefined;
319
+ return { callables: [], nonCallableFound: false, blockScopeDeclFound: false };
238
320
  }
239
321
  /**
240
322
  * Populate `ownerId` on every def structurally owned by a Class
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.5-rc.43",
3
+ "version": "1.6.5-rc.45",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",