gitnexus 1.6.6-rc.124 → 1.6.6-rc.125

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.
@@ -49,13 +49,18 @@
49
49
  *
50
50
  * ## State lifecycle
51
51
  *
52
- * Three module-level maps populated per pipeline invocation, cleared via
53
- * `clearCppAdlState()` (called from `clearFileLocalNames`):
52
+ * Five pieces of module-level state populated per pipeline invocation, all
53
+ * reset together by `clearCppAdlState()` (called from
54
+ * `cppScopeResolver.loadResolutionConfig`, alongside `clearFileLocalNames` —
55
+ * NOT from `clearFileLocalNames` itself), grouped by when they fill:
54
56
  *
55
57
  * - `argInfoBySite` — per-call-site argument shape (capture-time)
56
58
  * - `noAdlSites` — call sites with parenthesized function (capture-time)
57
59
  * - `classToNamespaceQualifiedName` — class def → its enclosing namespace
58
60
  * qualified name (`populateCppAssociatedNamespaces` time)
61
+ * - `adlIndex` / `adlIndexSource` — the lazily-built candidate index and the
62
+ * `parsedFiles` reference it was built from (first-`pickCppAdlCandidates`
63
+ * time; see `ensureAdlIndex`)
59
64
  *
60
65
  * The class→namespace map uses qualified names (not scope IDs) because
61
66
  * C++ namespaces are open: `namespace N { ... }` in file A and
@@ -94,14 +99,59 @@ export interface CppAdlArgInfo {
94
99
  * referenced Function/Method def's parameter and return types. */
95
100
  readonly functionRefText?: string;
96
101
  }
102
+ /**
103
+ * ADL candidate index — built **once** per pipeline run from
104
+ * `(scopes, parsedFiles)` and reused by every call site.
105
+ *
106
+ * The legacy `pickCppAdlCandidates` re-scanned all parsed files (rebuilding a
107
+ * per-file `scopesById` map each time), all workspace defs (for the
108
+ * class-by-simple-name lookup), and used an O(scopes²) child-scope walk for
109
+ * hidden friends — once **per unresolved call site**. With hundreds of
110
+ * thousands of unresolved C++ sites that made the scope-resolution emit phase
111
+ * super-linear (observed ~6.7h on a large repo). This index moves all of that
112
+ * work to a single pass; per-site cost drops to O(associated namespaces).
113
+ */
114
+ export interface AdlCandidateIndex {
115
+ /** simple name → class-like defs (Class/Struct/Interface/Enum), preserving
116
+ * `scopes.defs.byId` iteration order so first-match / ambiguous semantics
117
+ * match the legacy linear scan. */
118
+ readonly classDefsBySimple: Map<string, SymbolDefinition[]>;
119
+ /** namespace QName → simple name → callable defs owned by that namespace,
120
+ * with inline-namespace transparency (inline-ns defs are also registered
121
+ * under the parent namespace's QName). */
122
+ readonly nsCandidates: Map<string, Map<string, SymbolDefinition[]>>;
123
+ /** associated-class enclosing-namespace QName → simple name → hidden-friend
124
+ * and class-member callable defs. */
125
+ readonly friendCandidates: Map<string, Map<string, SymbolDefinition[]>>;
126
+ /** namespace QName (own) → simple name → Function/Method defs, for the
127
+ * qualified function-reference ADL path. */
128
+ readonly nsFunctionsByQName: Map<string, Map<string, SymbolDefinition[]>>;
129
+ /** simple name → Function/Method defs across all namespaces, for the
130
+ * unqualified function-reference ADL path. */
131
+ readonly nsFunctionsBySimple: Map<string, SymbolDefinition[]>;
132
+ /** nodeId → visitation sequence number, used to merge per-namespace buckets
133
+ * back into the exact legacy candidate order (file-major; namespace defs
134
+ * before friend/member defs within a file). */
135
+ readonly seqByNodeId: Map<string, number>;
136
+ }
137
+ /**
138
+ * Return the nodeIds present in the index's candidate buckets
139
+ * (`nsCandidates` + `friendCandidates`) but missing from `seqByNodeId`, each
140
+ * reported once. Empty array means the seq-coverage invariant holds — which it
141
+ * must, since `buildAdlIndex` assigns a seq to every callable def in the same
142
+ * block that buckets it. Exported for the dev-gated guard in `buildAdlIndex`
143
+ * and its unit test.
144
+ */
145
+ export declare function validateAdlSeqCoverage(idx: AdlCandidateIndex): string[];
97
146
  /** Record per-call-site argument info. Called once per call site from
98
147
  * `emitCppScopeCaptures`. */
99
148
  export declare function markCppAdlSiteArgs(filePath: string, line: number, col: number, args: readonly CppAdlArgInfo[]): void;
100
149
  /** Mark a call site as ADL-suppressed (function child wrapped in
101
150
  * `parenthesized_expression`, e.g. `(f)(s)`). */
102
151
  export declare function markCppAdlSiteNoAdl(filePath: string, line: number, col: number): void;
103
- /** Clear ADL state. Called from `clearFileLocalNames` so all C++ resolver
104
- * per-pipeline state is reset together. */
152
+ /** Clear ADL state. Called from `cppScopeResolver.loadResolutionConfig`
153
+ * (alongside `clearFileLocalNames`) so all C++ resolver per-pipeline state is
154
+ * reset together at the start of each resolution pass. */
105
155
  export declare function clearCppAdlState(): void;
106
156
  /**
107
157
  * Walk `parsed.scopes` to record each Class def's enclosing namespace
@@ -49,13 +49,18 @@
49
49
  *
50
50
  * ## State lifecycle
51
51
  *
52
- * Three module-level maps populated per pipeline invocation, cleared via
53
- * `clearCppAdlState()` (called from `clearFileLocalNames`):
52
+ * Five pieces of module-level state populated per pipeline invocation, all
53
+ * reset together by `clearCppAdlState()` (called from
54
+ * `cppScopeResolver.loadResolutionConfig`, alongside `clearFileLocalNames` —
55
+ * NOT from `clearFileLocalNames` itself), grouped by when they fill:
54
56
  *
55
57
  * - `argInfoBySite` — per-call-site argument shape (capture-time)
56
58
  * - `noAdlSites` — call sites with parenthesized function (capture-time)
57
59
  * - `classToNamespaceQualifiedName` — class def → its enclosing namespace
58
60
  * qualified name (`populateCppAssociatedNamespaces` time)
61
+ * - `adlIndex` / `adlIndexSource` — the lazily-built candidate index and the
62
+ * `parsedFiles` reference it was built from (first-`pickCppAdlCandidates`
63
+ * time; see `ensureAdlIndex`)
59
64
  *
60
65
  * The class→namespace map uses qualified names (not scope IDs) because
61
66
  * C++ namespaces are open: `namespace N { ... }` in file A and
@@ -68,9 +73,216 @@ import { isCppInlineNamespaceScope } from './inline-namespaces.js';
68
73
  const argInfoBySite = new Map();
69
74
  const noAdlSites = new Set();
70
75
  const classToNamespaceQualifiedName = new Map();
76
+ let adlIndex;
77
+ let adlIndexSource;
71
78
  function siteKey(filePath, line, col) {
72
79
  return `${filePath}:${line}:${col}`;
73
80
  }
81
+ /** Last segment of a dotted qualified name (matches legacy inline expression). */
82
+ function adlSimpleName(def) {
83
+ return def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
84
+ }
85
+ function isAdlCallableType(type) {
86
+ return type === 'Function' || type === 'Method' || type === 'Constructor';
87
+ }
88
+ function pushNested(map, outerKey, innerKey, def) {
89
+ let inner = map.get(outerKey);
90
+ if (inner === undefined) {
91
+ inner = new Map();
92
+ map.set(outerKey, inner);
93
+ }
94
+ let arr = inner.get(innerKey);
95
+ if (arr === undefined) {
96
+ arr = [];
97
+ inner.set(innerKey, arr);
98
+ }
99
+ arr.push(def);
100
+ }
101
+ function pushFlat(map, key, def) {
102
+ let arr = map.get(key);
103
+ if (arr === undefined) {
104
+ arr = [];
105
+ map.set(key, arr);
106
+ }
107
+ arr.push(def);
108
+ }
109
+ /** Build the ADL candidate index in a single pass over the workspace. The
110
+ * visitation order (file-major; per file, all namespace scopes before all
111
+ * class scopes; ownedDefs in declaration order) mirrors the legacy push
112
+ * order so `seqByNodeId` reconstructs identical candidate ordering. */
113
+ function buildAdlIndex(scopes, parsedFiles) {
114
+ const idx = {
115
+ classDefsBySimple: new Map(),
116
+ nsCandidates: new Map(),
117
+ friendCandidates: new Map(),
118
+ nsFunctionsByQName: new Map(),
119
+ nsFunctionsBySimple: new Map(),
120
+ seqByNodeId: new Map(),
121
+ };
122
+ // (1) class-like defs by simple name — preserve byId order so arr[0] is the
123
+ // legacy `firstMatch` and `arr.length > 1` is the legacy `ambiguous`.
124
+ for (const def of scopes.defs.byId.values()) {
125
+ if (def.type !== 'Class' &&
126
+ def.type !== 'Struct' &&
127
+ def.type !== 'Interface' &&
128
+ def.type !== 'Enum')
129
+ continue;
130
+ pushFlat(idx.classDefsBySimple, adlSimpleName(def), def);
131
+ }
132
+ let seq = 0;
133
+ for (const parsed of parsedFiles) {
134
+ const scopesById = new Map();
135
+ for (const sc of parsed.scopes)
136
+ scopesById.set(sc.id, sc);
137
+ // parent → children, built once (replaces the legacy O(scopes²) walk).
138
+ const childrenByParent = new Map();
139
+ for (const sc of parsed.scopes) {
140
+ if (sc.parent === null)
141
+ continue;
142
+ let kids = childrenByParent.get(sc.parent);
143
+ if (kids === undefined) {
144
+ kids = [];
145
+ childrenByParent.set(sc.parent, kids);
146
+ }
147
+ kids.push(sc);
148
+ }
149
+ // PASS A — namespace-owned candidates (+ function-reference indexes).
150
+ for (const scope of parsed.scopes) {
151
+ if (scope.kind !== 'Namespace')
152
+ continue;
153
+ const qName = computeNamespaceQName(scope, scopesById);
154
+ // Registration keys reproduce the legacy membership test: own QName
155
+ // always; for an inline namespace child of a Namespace, also the parent
156
+ // QName (ISO C++ inline-namespace transparency for ADL).
157
+ const keys = [];
158
+ if (qName !== '')
159
+ keys.push(qName);
160
+ if (isCppInlineNamespaceScope(scope.id)) {
161
+ const parentScope = scope.parent !== null ? scopesById.get(scope.parent) : undefined;
162
+ if (parentScope !== undefined && parentScope.kind === 'Namespace') {
163
+ const parentQName = computeNamespaceQName(parentScope, scopesById);
164
+ if (parentQName !== '' && parentQName !== qName)
165
+ keys.push(parentQName);
166
+ }
167
+ }
168
+ for (const def of scope.ownedDefs) {
169
+ if (def.type === 'Function' || def.type === 'Method') {
170
+ const sn = adlSimpleName(def);
171
+ pushFlat(idx.nsFunctionsBySimple, sn, def);
172
+ if (qName !== '')
173
+ pushNested(idx.nsFunctionsByQName, qName, sn, def);
174
+ }
175
+ if (!isAdlCallableType(def.type))
176
+ continue;
177
+ const s = seq++;
178
+ idx.seqByNodeId.set(def.nodeId, s);
179
+ const sn = adlSimpleName(def);
180
+ for (const key of keys)
181
+ pushNested(idx.nsCandidates, key, sn, def);
182
+ }
183
+ }
184
+ // PASS B — hidden-friend + class-member candidates for associated classes.
185
+ for (const scope of parsed.scopes) {
186
+ if (scope.kind !== 'Class')
187
+ continue;
188
+ // Enclosing-namespace QName(s) of the class def(s) in this scope.
189
+ const classNsKeys = new Set();
190
+ for (const def of scope.ownedDefs) {
191
+ if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
192
+ continue;
193
+ const nsQName = classToNamespaceQualifiedName.get(def.nodeId);
194
+ if (nsQName !== undefined)
195
+ classNsKeys.add(nsQName);
196
+ }
197
+ if (classNsKeys.size === 0)
198
+ continue;
199
+ // Friend functions: callable defs in child Function scopes.
200
+ for (const childScope of childrenByParent.get(scope.id) ?? []) {
201
+ if (childScope.kind !== 'Function')
202
+ continue;
203
+ for (const def of childScope.ownedDefs) {
204
+ if (!isAdlCallableType(def.type))
205
+ continue;
206
+ const s = seq++;
207
+ idx.seqByNodeId.set(def.nodeId, s);
208
+ const sn = adlSimpleName(def);
209
+ for (const key of classNsKeys)
210
+ pushNested(idx.friendCandidates, key, sn, def);
211
+ }
212
+ }
213
+ // Class-member callables.
214
+ for (const def of scope.ownedDefs) {
215
+ if (!isAdlCallableType(def.type))
216
+ continue;
217
+ const s = seq++;
218
+ idx.seqByNodeId.set(def.nodeId, s);
219
+ const sn = adlSimpleName(def);
220
+ for (const key of classNsKeys)
221
+ pushNested(idx.friendCandidates, key, sn, def);
222
+ }
223
+ }
224
+ }
225
+ // Dev/test-only invariant guard: every def bucketed into nsCandidates/
226
+ // friendCandidates must have a seqByNodeId entry, otherwise the `?? 0`
227
+ // fallback in pickCppAdlCandidates could collapse two seq-0 candidates and
228
+ // silently drop a CALLS edge. Gated like the rest of the resolver's opt-in
229
+ // validation (see contract/scope-resolver.ts and reconcile-ownership.ts):
230
+ // active in dev/test, off in production and when VALIDATE_SEMANTIC_MODEL=0.
231
+ if (process.env.NODE_ENV !== 'production' && process.env.VALIDATE_SEMANTIC_MODEL !== '0') {
232
+ const missing = validateAdlSeqCoverage(idx);
233
+ if (missing.length > 0) {
234
+ throw new Error(`[cpp-adl] seq-coverage invariant violated: ${missing.length} candidate def(s) ` +
235
+ `bucketed without a seqByNodeId entry (e.g. ${missing.slice(0, 5).join(', ')}). ` +
236
+ `Every def pushed into nsCandidates/friendCandidates must be seq-assigned in the ` +
237
+ `same build block — see pickCppAdlCandidates' \`?? 0\` fallback.`);
238
+ }
239
+ }
240
+ return idx;
241
+ }
242
+ /**
243
+ * Return the nodeIds present in the index's candidate buckets
244
+ * (`nsCandidates` + `friendCandidates`) but missing from `seqByNodeId`, each
245
+ * reported once. Empty array means the seq-coverage invariant holds — which it
246
+ * must, since `buildAdlIndex` assigns a seq to every callable def in the same
247
+ * block that buckets it. Exported for the dev-gated guard in `buildAdlIndex`
248
+ * and its unit test.
249
+ */
250
+ export function validateAdlSeqCoverage(idx) {
251
+ const missing = new Set();
252
+ for (const buckets of [idx.nsCandidates, idx.friendCandidates]) {
253
+ for (const bySimple of buckets.values()) {
254
+ for (const defs of bySimple.values()) {
255
+ for (const def of defs) {
256
+ if (!idx.seqByNodeId.has(def.nodeId))
257
+ missing.add(def.nodeId);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ return [...missing];
263
+ }
264
+ /** Build the ADL index on first use of a given `parsedFiles` set; reuse it for
265
+ * all subsequent call sites in the same pipeline run. Reset by
266
+ * `clearCppAdlState`.
267
+ *
268
+ * Staleness is keyed on `parsedFiles` reference identity ONLY, but the index
269
+ * is a function of THREE inputs: `parsedFiles` (namespace/friend candidates),
270
+ * `scopes` (`classDefsBySimple`, read from `scopes.defs.byId`), and the
271
+ * module-level `classToNamespaceQualifiedName` (friend-candidate keys). This
272
+ * is sound for the current pipeline because all three are built together once
273
+ * per `runScopeResolution` pass and `clearCppAdlState` runs in
274
+ * `loadResolutionConfig` at the start of every pass. Callers MUST call
275
+ * `clearCppAdlState` between any two passes that change `scopes` or
276
+ * `classToNamespaceQualifiedName` while reusing the same `parsedFiles` array
277
+ * reference — otherwise a stale index would be served. (No such caller exists
278
+ * today; widening the guard to also key on `scopes` is deferred until one
279
+ * does.) */
280
+ function ensureAdlIndex(scopes, parsedFiles) {
281
+ if (adlIndex !== undefined && adlIndexSource === parsedFiles)
282
+ return;
283
+ adlIndex = buildAdlIndex(scopes, parsedFiles);
284
+ adlIndexSource = parsedFiles;
285
+ }
74
286
  /** Record per-call-site argument info. Called once per call site from
75
287
  * `emitCppScopeCaptures`. */
76
288
  export function markCppAdlSiteArgs(filePath, line, col, args) {
@@ -81,12 +293,15 @@ export function markCppAdlSiteArgs(filePath, line, col, args) {
81
293
  export function markCppAdlSiteNoAdl(filePath, line, col) {
82
294
  noAdlSites.add(siteKey(filePath, line, col));
83
295
  }
84
- /** Clear ADL state. Called from `clearFileLocalNames` so all C++ resolver
85
- * per-pipeline state is reset together. */
296
+ /** Clear ADL state. Called from `cppScopeResolver.loadResolutionConfig`
297
+ * (alongside `clearFileLocalNames`) so all C++ resolver per-pipeline state is
298
+ * reset together at the start of each resolution pass. */
86
299
  export function clearCppAdlState() {
87
300
  argInfoBySite.clear();
88
301
  noAdlSites.clear();
89
302
  classToNamespaceQualifiedName.clear();
303
+ adlIndex = undefined;
304
+ adlIndexSource = undefined;
90
305
  }
91
306
  /**
92
307
  * Walk `parsed.scopes` to record each Class def's enclosing namespace
@@ -148,123 +363,54 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
148
363
  const args = argInfoBySite.get(key);
149
364
  if (args === undefined || args.length === 0)
150
365
  return undefined;
366
+ // Build the workspace-wide ADL candidate index once; reuse for every site.
367
+ ensureAdlIndex(scopes, parsedFiles);
151
368
  // Collect associated namespace QNames from every participating class-typed arg
152
369
  // and from function-reference args.
153
370
  const associatedNamespaces = new Set();
154
371
  for (const arg of args) {
155
372
  collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces);
156
373
  if (arg.functionRefText !== undefined) {
157
- collectFunctionTypeAssociatedNamespaces(arg.functionRefText, scopes, parsedFiles, associatedNamespaces);
374
+ collectFunctionTypeAssociatedNamespaces(arg.functionRefText, scopes, associatedNamespaces);
158
375
  }
159
376
  }
160
377
  if (associatedNamespaces.size === 0)
161
378
  return undefined;
162
- // Walk every namespace scope in every parsed file; collect callable
163
- // ownedDefs whose enclosing namespace matches one of the associated
164
- // QNames AND whose simple name matches the call's name.
165
- // ISO C++: inline namespaces are transparent — candidates in inline
166
- // children of an associated namespace are also ADL-reachable.
167
- const candidates = [];
379
+ // Gather candidates from the prebuilt index instead of re-scanning every
380
+ // parsed file. For each associated namespace, pull:
381
+ // - namespace-owned callables (`nsCandidates`, includes inline-namespace
382
+ // transparency), AND
383
+ // - hidden-friend / class-member callables of associated classes
384
+ // (`friendCandidates`, ISO C++ `[basic.lookup.argdep]` §2).
385
+ // Dedup by nodeId and sort by visitation sequence so the candidate list is
386
+ // byte-for-byte identical to the legacy file-major scan order.
387
+ const idx = adlIndex;
388
+ if (idx === undefined)
389
+ return undefined;
390
+ const bySeq = new Map();
168
391
  const seenKey = new Set();
169
- for (const parsed of parsedFiles) {
170
- const scopesById = new Map();
171
- for (const sc of parsed.scopes)
172
- scopesById.set(sc.id, sc);
173
- for (const scope of parsed.scopes) {
174
- if (scope.kind !== 'Namespace')
392
+ const collectFrom = (buckets) => {
393
+ for (const ns of associatedNamespaces) {
394
+ const matches = buckets.get(ns)?.get(site.name);
395
+ if (matches === undefined)
175
396
  continue;
176
- const qName = computeNamespaceQName(scope, scopesById);
177
- if (!associatedNamespaces.has(qName)) {
178
- // Check if this is an inline-namespace child of an associated NS.
179
- // ISO C++ inline namespaces are transparent for ADL: if the outer
180
- // namespace is in the associated set, candidates in the inline child
181
- // are also reachable.
182
- if (!isCppInlineNamespaceScope(scope.id))
183
- continue;
184
- const parentScope = scope.parent !== null ? scopesById.get(scope.parent) : undefined;
185
- if (parentScope === undefined || parentScope.kind !== 'Namespace')
186
- continue;
187
- const parentQName = computeNamespaceQName(parentScope, scopesById);
188
- if (!associatedNamespaces.has(parentQName))
189
- continue;
190
- }
191
- for (const def of scope.ownedDefs) {
192
- if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
193
- continue;
194
- }
195
- const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
196
- if (simple !== site.name)
197
- continue;
198
- // Dedup by nodeId — using normalized parameter-types as the key
199
- // would collapse `process(int)`/`process(long)`-style overloads
200
- // (both normalize to `['int']`) before
201
- // `isOverloadAmbiguousAfterNormalization` can detect them.
202
- if (seenKey.has(def.nodeId))
203
- continue;
204
- seenKey.add(def.nodeId);
205
- candidates.push(def);
206
- }
207
- }
208
- // ISO C++ `[basic.lookup.argdep]` §2: hidden friend functions declared
209
- // inside a class body are visible via ADL when the class is an associated
210
- // class. Scan Class scopes whose enclosing namespace is in the associated
211
- // set for callable ownedDefs matching the call name. This enables the
212
- // canonical "hidden friend" idiom:
213
- // struct Foo { friend void swap(Foo&, Foo&) {} };
214
- for (const scope of parsed.scopes) {
215
- if (scope.kind !== 'Class')
216
- continue;
217
- // Check if ANY class def in this scope has an associated namespace.
218
- let isAssociatedClass = false;
219
- for (const def of scope.ownedDefs) {
220
- if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
221
- continue;
222
- const nsQName = classToNamespaceQualifiedName.get(def.nodeId);
223
- if (nsQName !== undefined && associatedNamespaces.has(nsQName)) {
224
- isAssociatedClass = true;
225
- break;
226
- }
227
- }
228
- if (!isAssociatedClass)
229
- continue;
230
- // Also scan Function scopes that are direct children of this class
231
- // scope — friend function definitions create their own Function scope
232
- // underneath the Class scope.
233
- for (const childScope of parsed.scopes) {
234
- if (childScope.parent !== scope.id)
235
- continue;
236
- if (childScope.kind !== 'Function')
237
- continue;
238
- for (const def of childScope.ownedDefs) {
239
- if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
240
- continue;
241
- }
242
- const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
243
- if (simple !== site.name)
244
- continue;
245
- if (seenKey.has(def.nodeId))
246
- continue;
247
- seenKey.add(def.nodeId);
248
- candidates.push(def);
249
- }
250
- }
251
- for (const def of scope.ownedDefs) {
252
- if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor') {
253
- continue;
254
- }
255
- const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
256
- if (simple !== site.name)
257
- continue;
397
+ for (const def of matches) {
258
398
  if (seenKey.has(def.nodeId))
259
399
  continue;
260
400
  seenKey.add(def.nodeId);
261
- candidates.push(def);
401
+ // `?? 0` is unreachable: every bucketed def is seq-assigned in the same
402
+ // block that buckets it in buildAdlIndex (PASS A / PASS B). The dev-gated
403
+ // validateAdlSeqCoverage guard in buildAdlIndex fails loudly if that ever
404
+ // breaks, rather than letting two seq-0 defs collide and drop a candidate.
405
+ bySeq.set(idx.seqByNodeId.get(def.nodeId) ?? 0, def);
262
406
  }
263
407
  }
264
- }
265
- if (candidates.length === 0)
408
+ };
409
+ collectFrom(idx.nsCandidates);
410
+ collectFrom(idx.friendCandidates);
411
+ if (bySeq.size === 0)
266
412
  return undefined;
267
- return candidates;
413
+ return [...bySeq.entries()].sort((a, b) => a[0] - b[0]).map(([, def]) => def);
268
414
  }
269
415
  function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces) {
270
416
  // For template args this may be the template name itself (e.g. `vector`);
@@ -287,7 +433,7 @@ function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces)
287
433
  function addAssociatedNamespaceForClassName(simpleClassName, scopes, associatedNamespaces) {
288
434
  if (simpleClassName.length === 0)
289
435
  return;
290
- const classLookup = findCppClassDefBySimpleName(simpleClassName, scopes);
436
+ const classLookup = findCppClassDefBySimpleName(simpleClassName);
291
437
  if (classLookup === undefined)
292
438
  return;
293
439
  const { classDef, ambiguous } = classLookup;
@@ -380,32 +526,25 @@ function findNamespaceDefInScope(scope) {
380
526
  *
381
527
  * ISO C++ `[basic.lookup.argdep]` §2: enumerations contribute their
382
528
  * enclosing namespace to the associated set, just like class types. */
383
- function findCppClassDefBySimpleName(simpleName, scopes) {
384
- let firstMatch;
385
- for (const def of scopes.defs.byId.values()) {
386
- if (def.type !== 'Class' &&
387
- def.type !== 'Struct' &&
388
- def.type !== 'Interface' &&
389
- def.type !== 'Enum')
390
- continue;
391
- const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
392
- if (simple !== simpleName)
393
- continue;
394
- if (firstMatch === undefined) {
395
- firstMatch = def;
396
- continue;
397
- }
398
- return { classDef: firstMatch, ambiguous: true };
399
- }
400
- if (firstMatch === undefined)
529
+ function findCppClassDefBySimpleName(simpleName) {
530
+ // `classDefsBySimple` preserves `scopes.defs.byId` order, so `[0]` is the
531
+ // legacy first-match and `length > 1` is the legacy `ambiguous` flag.
532
+ const matches = adlIndex?.classDefsBySimple.get(simpleName);
533
+ if (matches === undefined)
534
+ return undefined;
535
+ const first = matches[0];
536
+ if (first === undefined)
401
537
  return undefined;
402
- return { classDef: firstMatch, ambiguous: false };
538
+ return { classDef: first, ambiguous: matches.length > 1 };
403
539
  }
404
540
  /**
405
541
  * Contribute associated namespaces for a function-reference argument by walking
406
542
  * the referenced overload set's parameter and return types.
407
543
  */
408
- function collectFunctionTypeAssociatedNamespaces(refText, scopes, parsedFiles, out) {
544
+ function collectFunctionTypeAssociatedNamespaces(refText, scopes, out) {
545
+ const idx = adlIndex;
546
+ if (idx === undefined)
547
+ return;
409
548
  const colonIdx = refText.lastIndexOf('::');
410
549
  if (colonIdx !== -1) {
411
550
  // Qualified ref: extract namespace prefix and normalise :: → dot notation.
@@ -413,27 +552,14 @@ function collectFunctionTypeAssociatedNamespaces(refText, scopes, parsedFiles, o
413
552
  if (nsText === '')
414
553
  return;
415
554
  const simpleName = refText.slice(colonIdx + 2);
416
- // Verify that a Function/Method named `simpleName` exists in `nsText`.
417
- // Without this guard every `a::b` qualified_identifier arg (variable,
418
- // enum value, static member, type alias) would blindly contribute `a`
419
- // to the associated set and risk a false-positive CALLS edge.
420
- for (const parsed of parsedFiles) {
421
- const scopesById = new Map();
422
- for (const sc of parsed.scopes)
423
- scopesById.set(sc.id, sc);
424
- for (const scope of parsed.scopes) {
425
- if (scope.kind !== 'Namespace')
426
- continue;
427
- if (computeNamespaceQName(scope, scopesById) !== nsText)
428
- continue;
429
- for (const def of scope.ownedDefs) {
430
- if (def.type !== 'Function' && def.type !== 'Method')
431
- continue;
432
- const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
433
- if (simple === simpleName)
434
- collectAssociatedNamespacesForFunctionDef(def, scopes, out);
435
- }
436
- }
555
+ // Only Function/Method defs named `simpleName` in `nsText` contribute
556
+ // (the index already restricts to those types); this guards against an
557
+ // `a::b` arg that names a variable / enum value / type alias blindly
558
+ // contributing `a` to the associated set (false-positive CALLS edge).
559
+ const matches = idx.nsFunctionsByQName.get(nsText)?.get(simpleName);
560
+ if (matches !== undefined) {
561
+ for (const def of matches)
562
+ collectAssociatedNamespacesForFunctionDef(def, scopes, out);
437
563
  }
438
564
  return;
439
565
  }
@@ -441,19 +567,10 @@ function collectFunctionTypeAssociatedNamespaces(refText, scopes, parsedFiles, o
441
567
  // the previous V1 lookup scope. The stricter part of this PR is what each
442
568
  // overload contributes: only namespaces from parameter/return types, never
443
569
  // the function's own enclosing namespace.
444
- for (const parsed of parsedFiles) {
445
- for (const scope of parsed.scopes) {
446
- if (scope.kind !== 'Namespace')
447
- continue;
448
- for (const def of scope.ownedDefs) {
449
- if (def.type !== 'Function' && def.type !== 'Method')
450
- continue;
451
- const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
452
- if (simple !== refText)
453
- continue;
454
- collectAssociatedNamespacesForFunctionDef(def, scopes, out);
455
- }
456
- }
570
+ const matches = idx.nsFunctionsBySimple.get(refText);
571
+ if (matches !== undefined) {
572
+ for (const def of matches)
573
+ collectAssociatedNamespacesForFunctionDef(def, scopes, out);
457
574
  }
458
575
  }
459
576
  function collectAssociatedNamespacesForFunctionDef(def, scopes, out) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.124",
3
+ "version": "1.6.6-rc.125",
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",