gitnexus 1.6.5-rc.41 → 1.6.5-rc.43

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.
@@ -21,8 +21,25 @@
21
21
  * - `audit::Event& r`, `audit::Event&& rr`
22
22
  * - `std::vector<audit::Event>` (template namespace + template-arg namespaces)
23
23
  *
24
- * Function-pointer arguments, base-class associated namespaces, and the rest
25
- * of the full closure are still deliberately excluded.
24
+ * V2 additionally walks class ancestors (via MRO), so base-class enclosing
25
+ * namespaces also contribute associated namespaces.
26
+ *
27
+ * **GitNexus approximation (not strict ISO C++ ADL):** passing a qualified
28
+ * function reference like `utils::worker` contributes `utils` to the associated
29
+ * set, enabling resolution of unqualified calls like `with_callback(utils::worker)`
30
+ * to `utils::with_callback`. Under ISO C++ `[basic.lookup.argdep]`, associated
31
+ * entities for function-type arguments come from the **parameter types and return
32
+ * type** of each function in the overload set — NOT the function's enclosing
33
+ * namespace. For `void worker()`, the standard-compliant associated set is empty.
34
+ * GitNexus instead contributes the enclosing namespace of any Function/Method
35
+ * def whose simple name matches, because it enables the dominant real-world ADL
36
+ * pattern at reasonable precision cost.
37
+ *
38
+ * For qualified refs (e.g. `utils::worker`) the namespace is confirmed via a
39
+ * workspace lookup (only contributed when a Function/Method named `worker` exists
40
+ * in `utils`). For unqualified refs the workspace is searched for any Function
41
+ * def with that simple name. Locally-declared function-pointer variables
42
+ * (e.g. `void (*g)()`) and function parameters are excluded from this path.
26
43
  *
27
44
  * The current implementation also short-circuits to ADL only when ordinary lookup is empty
28
45
  * (`findCallableBindingInScope` returned undefined). ISO C++ would
@@ -58,6 +75,7 @@ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexe
58
75
  * Per-argument shape information collected at capture time. ADL fires for
59
76
  * arguments where `simpleClassName !== ''`, including class pointers and
60
77
  * references whose declarator chain resolves to a named class type.
78
+ * Free-function reference arguments use `functionRefText`.
61
79
  */
62
80
  export interface CppAdlArgInfo {
63
81
  /** Simple class-like type name (last segment of qualified name); empty
@@ -75,6 +93,15 @@ export interface CppAdlArgInfo {
75
93
  /** Enclosing namespaces extracted from explicit type template arguments,
76
94
  * recursively bounded. */
77
95
  readonly templateArgNamespaces: readonly string[];
96
+ /** When set, the arg is a potential free-function reference (not a locally-
97
+ * declared function-pointer variable or function parameter). Contains the
98
+ * identifier text as written in source (e.g. `"utils::worker"` or
99
+ * `"worker"`). GitNexus approximation: the function's enclosing namespace
100
+ * is contributed to the ADL associated set. For qualified refs a workspace
101
+ * lookup confirms a Function/Method with that simple name exists in the
102
+ * namespace before contributing; for unqualified refs every namespace
103
+ * containing a matching Function/Method def is contributed. */
104
+ readonly functionRefText?: string;
78
105
  }
79
106
  /** Sentinel returned by `pickCppAdlCandidates` when ADL surfaces multiple
80
107
  * candidates that share normalized parameter types — the caller MUST
@@ -21,8 +21,25 @@
21
21
  * - `audit::Event& r`, `audit::Event&& rr`
22
22
  * - `std::vector<audit::Event>` (template namespace + template-arg namespaces)
23
23
  *
24
- * Function-pointer arguments, base-class associated namespaces, and the rest
25
- * of the full closure are still deliberately excluded.
24
+ * V2 additionally walks class ancestors (via MRO), so base-class enclosing
25
+ * namespaces also contribute associated namespaces.
26
+ *
27
+ * **GitNexus approximation (not strict ISO C++ ADL):** passing a qualified
28
+ * function reference like `utils::worker` contributes `utils` to the associated
29
+ * set, enabling resolution of unqualified calls like `with_callback(utils::worker)`
30
+ * to `utils::with_callback`. Under ISO C++ `[basic.lookup.argdep]`, associated
31
+ * entities for function-type arguments come from the **parameter types and return
32
+ * type** of each function in the overload set — NOT the function's enclosing
33
+ * namespace. For `void worker()`, the standard-compliant associated set is empty.
34
+ * GitNexus instead contributes the enclosing namespace of any Function/Method
35
+ * def whose simple name matches, because it enables the dominant real-world ADL
36
+ * pattern at reasonable precision cost.
37
+ *
38
+ * For qualified refs (e.g. `utils::worker`) the namespace is confirmed via a
39
+ * workspace lookup (only contributed when a Function/Method named `worker` exists
40
+ * in `utils`). For unqualified refs the workspace is searched for any Function
41
+ * def with that simple name. Locally-declared function-pointer variables
42
+ * (e.g. `void (*g)()`) and function parameters are excluded from this path.
26
43
  *
27
44
  * The current implementation also short-circuits to ADL only when ordinary lookup is empty
28
45
  * (`findCallableBindingInScope` returned undefined). ISO C++ would
@@ -129,10 +146,14 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
129
146
  const args = argInfoBySite.get(key);
130
147
  if (args === undefined || args.length === 0)
131
148
  return undefined;
132
- // Collect associated namespace QNames from every participating class-typed arg.
149
+ // Collect associated namespace QNames from every participating class-typed arg
150
+ // and from function-reference args.
133
151
  const associatedNamespaces = new Set();
134
152
  for (const arg of args) {
135
153
  collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces);
154
+ if (arg.functionRefText !== undefined) {
155
+ collectFunctionRefNamespaces(arg.functionRefText, parsedFiles, associatedNamespaces);
156
+ }
136
157
  }
137
158
  if (associatedNamespaces.size === 0)
138
159
  return undefined;
@@ -210,12 +231,23 @@ function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces)
210
231
  function addAssociatedNamespaceForClassName(simpleClassName, scopes, associatedNamespaces) {
211
232
  if (simpleClassName.length === 0)
212
233
  return;
213
- const classDef = findCppClassDefBySimpleName(simpleClassName, scopes);
214
- if (classDef === undefined)
234
+ const classLookup = findCppClassDefBySimpleName(simpleClassName, scopes);
235
+ if (classLookup === undefined)
215
236
  return;
237
+ const { classDef, ambiguous } = classLookup;
216
238
  const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId);
217
239
  if (nsQName !== undefined)
218
240
  associatedNamespaces.add(nsQName);
241
+ // Preserve V1 collision behavior for the direct class namespace, but avoid
242
+ // amplifying a same-simple-name collision by walking an arbitrary class's
243
+ // full MRO chain.
244
+ if (ambiguous)
245
+ return;
246
+ for (const ancestorDefId of scopes.methodDispatch.mroFor(classDef.nodeId)) {
247
+ const ancestorNsQName = classToNamespaceQualifiedName.get(ancestorDefId);
248
+ if (ancestorNsQName !== undefined)
249
+ associatedNamespaces.add(ancestorNsQName);
250
+ }
219
251
  }
220
252
  /** Walk upward from a Class scope, finding the innermost enclosing
221
253
  * Namespace scope, and return that namespace's qualified name (dot-
@@ -285,16 +317,97 @@ function findNamespaceDefInScope(scope) {
285
317
  return undefined;
286
318
  }
287
319
  /** Find a class-like def by simple name across the workspace. V1
288
- * arbitrary-pick on collisions (multiple classes share the simple name);
289
- * C++ ADL strictness would require full type-driven lookup, but V1
290
- * trades that for simplicity. */
320
+ * still arbitrary-picks the first class on collisions (multiple classes
321
+ * share the simple name), but reports the collision so callers can avoid
322
+ * amplifying that uncertainty (for example by skipping MRO expansion).
323
+ * C++ ADL strictness would require full type-driven lookup. */
291
324
  function findCppClassDefBySimpleName(simpleName, scopes) {
325
+ let firstMatch;
292
326
  for (const def of scopes.defs.byId.values()) {
293
327
  if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
294
328
  continue;
295
329
  const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
296
- if (simple === simpleName)
297
- return def;
330
+ if (simple !== simpleName)
331
+ continue;
332
+ if (firstMatch === undefined) {
333
+ firstMatch = def;
334
+ continue;
335
+ }
336
+ return { classDef: firstMatch, ambiguous: true };
337
+ }
338
+ if (firstMatch === undefined)
339
+ return undefined;
340
+ return { classDef: firstMatch, ambiguous: false };
341
+ }
342
+ /**
343
+ * Contribute associated namespaces for a function-reference argument.
344
+ *
345
+ * - **Qualified refs** (`utils::worker`, `outer::inner::fn`): the namespace
346
+ * is extracted from the qualifier text (converting `::` to `.` for dot-joined
347
+ * QName matching). A workspace lookup then **verifies** that a Function or
348
+ * Method def named `worker` (the simple name after the last `::`) actually
349
+ * exists in the extracted namespace. This prevents false positives from
350
+ * namespace-qualified variables, enum values, and static data members, which
351
+ * also produce `qualified_identifier` AST nodes in tree-sitter-cpp (the
352
+ * AST node type alone does not distinguish functions from non-function names).
353
+ * - **Unqualified refs** (`worker`): the workspace is searched for any
354
+ * Function/Method def whose simple name matches. Every distinct enclosing
355
+ * namespace found is added — overloads across the same namespace produce
356
+ * a single entry; GitNexus does not select a specific overload at this stage.
357
+ */
358
+ function collectFunctionRefNamespaces(refText, parsedFiles, out) {
359
+ const colonIdx = refText.lastIndexOf('::');
360
+ if (colonIdx !== -1) {
361
+ // Qualified ref: extract namespace prefix and normalise :: → dot notation.
362
+ const nsText = refText.slice(0, colonIdx).replace(/::/g, '.');
363
+ if (nsText === '')
364
+ return;
365
+ const simpleName = refText.slice(colonIdx + 2);
366
+ // Verify that a Function/Method named `simpleName` exists in `nsText`.
367
+ // Without this guard every `a::b` qualified_identifier arg (variable,
368
+ // enum value, static member, type alias) would blindly contribute `a`
369
+ // to the associated set and risk a false-positive CALLS edge.
370
+ for (const parsed of parsedFiles) {
371
+ const scopesById = new Map();
372
+ for (const sc of parsed.scopes)
373
+ scopesById.set(sc.id, sc);
374
+ for (const scope of parsed.scopes) {
375
+ if (scope.kind !== 'Namespace')
376
+ continue;
377
+ if (computeNamespaceQName(scope, scopesById) !== nsText)
378
+ continue;
379
+ for (const def of scope.ownedDefs) {
380
+ if (def.type !== 'Function' && def.type !== 'Method')
381
+ continue;
382
+ const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
383
+ if (simple === simpleName) {
384
+ out.add(nsText);
385
+ return; // Namespace confirmed; no need to scan further files.
386
+ }
387
+ }
388
+ }
389
+ }
390
+ return;
391
+ }
392
+ // Unqualified: search all namespace scopes for a Function def with this
393
+ // simple name and contribute its enclosing namespace.
394
+ for (const parsed of parsedFiles) {
395
+ const scopesById = new Map();
396
+ for (const sc of parsed.scopes)
397
+ scopesById.set(sc.id, sc);
398
+ for (const scope of parsed.scopes) {
399
+ if (scope.kind !== 'Namespace')
400
+ continue;
401
+ for (const def of scope.ownedDefs) {
402
+ if (def.type !== 'Function' && def.type !== 'Method')
403
+ continue;
404
+ const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
405
+ if (simple !== refText)
406
+ continue;
407
+ const nsQName = computeNamespaceQName(scope, scopesById);
408
+ if (nsQName !== '')
409
+ out.add(nsQName);
410
+ }
411
+ }
298
412
  }
299
- return undefined;
300
413
  }
@@ -4,7 +4,7 @@ import { getTreeSitterBufferSize } from '../../constants.js';
4
4
  import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
5
5
  import { splitCppInclude, splitCppUsingDecl } from './import-decomposer.js';
6
6
  import { computeCppDeclarationArity, computeCppCallArity } from './arity-metadata.js';
7
- import { markFileLocal } from './file-local-linkage.js';
7
+ import { markCppAnonymousNamespaceRange, markFileLocal } from './file-local-linkage.js';
8
8
  import { markCppDependentBase } from './two-phase-lookup.js';
9
9
  import { markCppAdlSiteArgs, markCppAdlSiteNoAdl } from './adl.js';
10
10
  import { markCppInlineNamespaceRange } from './inline-namespaces.js';
@@ -150,20 +150,33 @@ export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
150
150
  // namespace's source range so `populateCppInlineNamespaceScopes`
151
151
  // (during populateOwners) can match it back to the corresponding
152
152
  // Namespace scope.
153
- if (grouped['@declaration.namespace'] !== undefined) {
154
- const anchor = grouped['@declaration.namespace'];
155
- const nsNode = findNodeAtRange(tree.rootNode, anchor.range, 'namespace_definition');
156
- if (nsNode !== null && isInlineNamespace(nsNode)) {
153
+ // `@declaration.namespace` fires only for NAMED namespaces (the query
154
+ // requires a `name: (namespace_identifier)` child). Use the unconditional
155
+ // `@scope.namespace` capture so the anonymous-namespace branch also runs.
156
+ const namespaceScopeAnchor = grouped['@declaration.namespace'] ?? grouped['@scope.namespace'];
157
+ if (namespaceScopeAnchor !== undefined) {
158
+ const nsNode = findNodeAtRange(tree.rootNode, namespaceScopeAnchor.range, 'namespace_definition');
159
+ if (nsNode !== null) {
157
160
  // Range coords stored in the shared Range shape use 1-based
158
161
  // line numbers (see `ast-helpers.ts` rangeForNode where
159
162
  // `startPosition.row + 1` is applied). Match that convention so
160
- // `populateCppInlineNamespaceScopes` can join against `Scope.range`.
161
- markCppInlineNamespaceRange(filePath, {
163
+ // the populators can join against `Scope.range`.
164
+ const nsRange = {
162
165
  startLine: nsNode.startPosition.row + 1,
163
166
  startCol: nsNode.startPosition.column,
164
167
  endLine: nsNode.endPosition.row + 1,
165
168
  endCol: nsNode.endPosition.column,
166
- });
169
+ };
170
+ if (isInlineNamespace(nsNode)) {
171
+ markCppInlineNamespaceRange(filePath, nsRange);
172
+ }
173
+ // Anonymous namespace: `namespace_definition` with no `name` field.
174
+ // Recorded so `expandCppWildcardNames` can propagate its members
175
+ // to including TUs even though their names are also `markFileLocal`'d
176
+ // (which blocks the global free-call fallback's cross-file path).
177
+ if ((nsNode.childForFieldName?.('name') ?? null) === null) {
178
+ markCppAnonymousNamespaceRange(filePath, nsRange);
179
+ }
167
180
  }
168
181
  }
169
182
  // ── ADL (Koenig lookup) per-site recording ──────────────────────
@@ -704,15 +717,93 @@ function classifyAdlArg(argNode) {
704
717
  argNode.type === 'nullptr') {
705
718
  return EMPTY_ADL_ARG;
706
719
  }
720
+ // Qualified expression (a::b) — may be a function, variable, enum value,
721
+ // or static member. Record as a potential function reference; resolution
722
+ // time verifies via workspace lookup that a Function/Method with this simple
723
+ // name exists in the extracted namespace before contributing to the set.
724
+ if (argNode.type === 'qualified_identifier') {
725
+ return {
726
+ simpleClassName: '',
727
+ templateSimpleClassName: '',
728
+ templateNamespace: '',
729
+ templateArgClassNames: [],
730
+ templateArgNamespaces: [],
731
+ functionRefText: argNode.text,
732
+ };
733
+ }
707
734
  // Variable reference — look up its declared type (preserving pointer /
708
735
  // reference / qualified-name shape; the existing arity-narrowing helper
709
736
  // strips this info).
710
737
  if (argNode.type === 'identifier') {
711
- return lookupAdlIdentifierType(argNode);
738
+ const result = lookupAdlIdentifierType(argNode);
739
+ if (result === null) {
740
+ // Not found in the local compound_statement scope — could be a
741
+ // free-function reference (unqualified name, namespace scope).
742
+ return {
743
+ simpleClassName: '',
744
+ templateSimpleClassName: '',
745
+ templateNamespace: '',
746
+ templateArgClassNames: [],
747
+ templateArgNamespaces: [],
748
+ functionRefText: argNode.text,
749
+ };
750
+ }
751
+ return result;
712
752
  }
713
753
  // Other shapes (calls, member access, operators) — V1 unsupported.
714
754
  return EMPTY_ADL_ARG;
715
755
  }
756
+ /**
757
+ * Returns `true` when `varName` appears as a parameter name in the nearest
758
+ * enclosing `function_definition` or `function_declarator` that contains
759
+ * `identNode`. Parameters live in `parameter_list` (a sibling of the
760
+ * `compound_statement`), so the `compound_statement`-local declaration scan
761
+ * in `lookupAdlIdentifierType` would not find them — causing them to be
762
+ * mistakenly classified as potential free-function references.
763
+ *
764
+ * In tree-sitter-cpp a `function_definition` does NOT expose `parameters`
765
+ * as a direct named field; parameters live inside the nested
766
+ * `function_declarator`. For `function_declarator` nodes the `parameters`
767
+ * field IS direct. Both cases are handled below.
768
+ */
769
+ function isIdentifierAFunctionParameter(identNode, varName) {
770
+ let node = identNode.parent;
771
+ let safety = 64;
772
+ while (node !== null && safety-- > 0) {
773
+ let params = null;
774
+ if (node.type === 'function_declarator') {
775
+ // parameters is a direct field on function_declarator.
776
+ params = node.childForFieldName('parameters');
777
+ }
778
+ else if (node.type === 'function_definition') {
779
+ // function_definition carries parameters inside its `declarator` field
780
+ // (which is a function_declarator). Walk through it.
781
+ const decl = node.childForFieldName('declarator');
782
+ if (decl !== null && decl.type === 'function_declarator') {
783
+ params = decl.childForFieldName('parameters');
784
+ }
785
+ }
786
+ if (params !== null) {
787
+ for (let i = 0; i < params.namedChildCount; i++) {
788
+ const param = params.namedChild(i);
789
+ if (param === null)
790
+ continue;
791
+ const declNode = param.childForFieldName('declarator');
792
+ if (declNode === null)
793
+ continue;
794
+ const leafName = extractDeclaratorLeafName(declNode);
795
+ if (leafName === varName)
796
+ return true;
797
+ }
798
+ // Only check the immediately enclosing function — do not climb further.
799
+ break;
800
+ }
801
+ if (node.type === 'translation_unit')
802
+ break;
803
+ node = node.parent;
804
+ }
805
+ return false;
806
+ }
716
807
  function lookupAdlIdentifierType(identNode) {
717
808
  const varName = identNode.text;
718
809
  let scope = identNode.parent;
@@ -722,7 +813,15 @@ function lookupAdlIdentifierType(identNode) {
722
813
  scope = scope.parent;
723
814
  }
724
815
  if (scope === null)
816
+ return null;
817
+ // Function parameters live in the enclosing function's `parameter_list`,
818
+ // NOT inside the `compound_statement`, so the declaration scan below would
819
+ // never find them and would return `null` — incorrectly triggering the
820
+ // free-function-reference path. Check the parameter_list first.
821
+ if (isIdentifierAFunctionParameter(identNode, varName)) {
725
822
  return EMPTY_ADL_ARG;
823
+ }
824
+ let foundAsLocalFunctionPointer = false;
726
825
  for (let i = 0; i < scope.childCount; i++) {
727
826
  const stmt = scope.child(i);
728
827
  if (stmt === null || stmt.type !== 'declaration')
@@ -751,6 +850,9 @@ function lookupAdlIdentifierType(identNode) {
751
850
  if (inner.type === 'pointer_declarator') {
752
851
  if (findFirstDescendantOfType(inner, 'function_declarator') !== null) {
753
852
  isFunctionPointer = true;
853
+ // Extract the name from within the function-pointer declarator chain
854
+ // so `foundAsLocalFunctionPointer` can detect a matching declaration.
855
+ nameText = extractDeclaratorLeafName(inner);
754
856
  break;
755
857
  }
756
858
  const next = inner.childForFieldName('declarator');
@@ -783,12 +885,22 @@ function lookupAdlIdentifierType(identNode) {
783
885
  }
784
886
  if (inner.type === 'function_declarator') {
785
887
  isFunctionPointer = true;
888
+ // Extract the name from the inner declarator (e.g. `(*g)` in `void (*g)()`).
889
+ const innerDecl = inner.childForFieldName('declarator');
890
+ if (innerDecl !== null)
891
+ nameText = extractDeclaratorLeafName(innerDecl);
786
892
  break;
787
893
  }
788
894
  // Reached the leaf — usually `identifier`. Take its text.
789
895
  nameText = inner.text;
790
896
  break;
791
897
  }
898
+ if (nameText === varName && isFunctionPointer) {
899
+ // Explicitly declared as a function-pointer variable — must not be
900
+ // treated as a free-function reference by the caller.
901
+ foundAsLocalFunctionPointer = true;
902
+ continue;
903
+ }
792
904
  if (isFunctionPointer || nameText !== varName)
793
905
  continue;
794
906
  const simpleClassName = extractAdlSimpleTypeName(typeNode);
@@ -801,7 +913,22 @@ function lookupAdlIdentifierType(identNode) {
801
913
  templateArgNamespaces,
802
914
  };
803
915
  }
804
- return EMPTY_ADL_ARG;
916
+ // If the identifier was found in local scope as a function-pointer variable,
917
+ // return EMPTY_ADL_ARG so the caller does NOT treat it as a free-function
918
+ // reference. Otherwise return null to indicate "not in local scope".
919
+ //
920
+ // Known limitation (Finding 4): variables whose type is a typedef/using alias
921
+ // for a function-pointer type are NOT detected here. For example:
922
+ // using Callback = void (*)();
923
+ // Callback g;
924
+ // foo(g); // `g`'s declarator is `identifier` with type `Callback`
925
+ // The declarator has no `pointer_declarator` wrapper, so `isFunctionPointer`
926
+ // stays false and `extractAdlSimpleTypeName` returns `"Callback"`. ADL then
927
+ // looks for a class named `Callback`; if none exists, this degrades to
928
+ // EMPTY_ADL_ARG (class not found → no namespace contributed). If a class
929
+ // named `Callback` does exist, a spurious namespace contribution could occur.
930
+ // Risk is low in practice; a future fix should resolve the typedef/alias chain.
931
+ return foundAsLocalFunctionPointer ? EMPTY_ADL_ARG : null;
805
932
  }
806
933
  /** Extract the simple class-like type name from a `type:` field node.
807
934
  * Returns '' for primitives and any other
@@ -950,6 +1077,29 @@ function extractNamespaceFromQualifiedText(text) {
950
1077
  return '';
951
1078
  return normalizeCppNamespaceQName(cleaned.slice(0, idx));
952
1079
  }
1080
+ /**
1081
+ * Walk a declarator node chain, unwrapping pointer/reference/function/
1082
+ * parenthesized wrappers, and return the text of the innermost identifier.
1083
+ * Returns `null` when no identifier is found within `safety` steps.
1084
+ * Used by `lookupAdlIdentifierType` to extract the variable name from
1085
+ * function-pointer declarator trees such as `(*g)()` in `void (*g)()`.
1086
+ */
1087
+ function extractDeclaratorLeafName(node) {
1088
+ let cur = node;
1089
+ let safety = 16;
1090
+ while (safety-- > 0) {
1091
+ if (cur.type === 'identifier' || cur.type === 'type_identifier')
1092
+ return cur.text;
1093
+ // Common wrapper nodes — follow the 'declarator' field when present.
1094
+ const next = cur.childForFieldName('declarator') ??
1095
+ // parenthesized_declarator: single named child
1096
+ (cur.type === 'parenthesized_declarator' ? cur.namedChild(0) : null);
1097
+ if (next === null)
1098
+ return null;
1099
+ cur = next;
1100
+ }
1101
+ return null;
1102
+ }
953
1103
  /**
954
1104
  * Check if a C++ function_definition or declaration has `static` storage class.
955
1105
  */
@@ -1,10 +1,34 @@
1
1
  import type { ParsedFile, ScopeId } from '../../../../_shared/index.js';
2
+ interface RangeKeyShape {
3
+ readonly startLine: number;
4
+ readonly startCol: number;
5
+ readonly endLine: number;
6
+ readonly endCol: number;
7
+ }
2
8
  /** Record a symbol name as file-local (static or anonymous namespace). */
3
9
  export declare function markFileLocal(filePath: string, name: string): void;
4
10
  /** Check whether a symbol name has file-local linkage in the given file. */
5
11
  export declare function isFileLocal(filePath: string, name: string): boolean;
12
+ /** Capture-time: record an anonymous `namespace_definition` source range. */
13
+ export declare function markCppAnonymousNamespaceRange(filePath: string, range: RangeKeyShape): void;
14
+ /** Predicate consumed by `populateCppNonGloballyVisible` and
15
+ * `expandCppWildcardNames` to exempt anonymous-namespace scopes from
16
+ * the cross-file unqualified-lookup exclusion that applies to ordinary
17
+ * named namespaces. */
18
+ export declare function isCppAnonymousNamespaceScope(scopeId: ScopeId): boolean;
6
19
  /** Clear tracked file-local names (call at start of each resolution pass). */
7
20
  export declare function clearFileLocalNames(): void;
21
+ /** Resolve recorded anonymous-namespace source ranges to `ScopeId`s.
22
+ * Must run inside `populateOwners` BEFORE `populateCppNonGloballyVisible`
23
+ * consults the resolved set. */
24
+ export declare function populateCppAnonymousNamespaceScopes(parsed: {
25
+ readonly filePath: string;
26
+ readonly scopes: readonly {
27
+ readonly id: ScopeId;
28
+ readonly kind: string;
29
+ readonly range: RangeKeyShape;
30
+ }[];
31
+ }): void;
8
32
  /**
9
33
  * Populate per-file "not globally visible" nodeIds by walking the parsed
10
34
  * file's scopes. Run as part of the `populateOwners` hook so every C++
@@ -91,3 +115,4 @@ export declare function isCppDefGloballyVisible(filePath: string, nodeId: string
91
115
  * members never enter `localDefs` unqualified.
92
116
  */
93
117
  export declare function expandCppWildcardNames(targetModuleScope: ScopeId, parsedFiles: readonly ParsedFile[]): readonly string[];
118
+ export {};
@@ -31,6 +31,24 @@ const fileLocalNames = new Map();
31
31
  * this set encodes).
32
32
  */
33
33
  const nonGloballyVisibleNodeIds = new Map();
34
+ /**
35
+ * Per-file set of source-range keys identifying `namespace { ... }` blocks.
36
+ * Resolved to `ScopeId`s in `populateCppAnonymousNamespaceScopes` and
37
+ * consumed via `isCppAnonymousNamespaceScope`.
38
+ *
39
+ * Anonymous namespaces have file-local linkage but, unlike `static`, their
40
+ * members propagate to any TU that `#include`s the declaring file — each
41
+ * including TU gets its own internal-linkage copy. So for wildcard import
42
+ * expansion (`expandCppWildcardNames`) we treat anonymous-namespace owned
43
+ * defs as if declared at the enclosing scope. Cross-file unqualified
44
+ * lookup that does NOT go through `#include` is still blocked by the
45
+ * `isFileLocal` mark recorded on the def's name.
46
+ */
47
+ const anonymousNamespaceRangesByFile = new Map();
48
+ const anonymousNamespaceScopeIds = new Set();
49
+ function rangeKey(r) {
50
+ return `${r.startLine}:${r.startCol}:${r.endLine}:${r.endCol}`;
51
+ }
34
52
  /** Record a symbol name as file-local (static or anonymous namespace). */
35
53
  export function markFileLocal(filePath, name) {
36
54
  let names = fileLocalNames.get(filePath);
@@ -44,10 +62,43 @@ export function markFileLocal(filePath, name) {
44
62
  export function isFileLocal(filePath, name) {
45
63
  return fileLocalNames.get(filePath)?.has(name) ?? false;
46
64
  }
65
+ /** Capture-time: record an anonymous `namespace_definition` source range. */
66
+ export function markCppAnonymousNamespaceRange(filePath, range) {
67
+ let set = anonymousNamespaceRangesByFile.get(filePath);
68
+ if (set === undefined) {
69
+ set = new Set();
70
+ anonymousNamespaceRangesByFile.set(filePath, set);
71
+ }
72
+ set.add(rangeKey(range));
73
+ }
74
+ /** Predicate consumed by `populateCppNonGloballyVisible` and
75
+ * `expandCppWildcardNames` to exempt anonymous-namespace scopes from
76
+ * the cross-file unqualified-lookup exclusion that applies to ordinary
77
+ * named namespaces. */
78
+ export function isCppAnonymousNamespaceScope(scopeId) {
79
+ return anonymousNamespaceScopeIds.has(scopeId);
80
+ }
47
81
  /** Clear tracked file-local names (call at start of each resolution pass). */
48
82
  export function clearFileLocalNames() {
49
83
  fileLocalNames.clear();
50
84
  nonGloballyVisibleNodeIds.clear();
85
+ anonymousNamespaceRangesByFile.clear();
86
+ anonymousNamespaceScopeIds.clear();
87
+ }
88
+ /** Resolve recorded anonymous-namespace source ranges to `ScopeId`s.
89
+ * Must run inside `populateOwners` BEFORE `populateCppNonGloballyVisible`
90
+ * consults the resolved set. */
91
+ export function populateCppAnonymousNamespaceScopes(parsed) {
92
+ const ranges = anonymousNamespaceRangesByFile.get(parsed.filePath);
93
+ if (ranges === undefined || ranges.size === 0)
94
+ return;
95
+ for (const scope of parsed.scopes) {
96
+ if (scope.kind !== 'Namespace')
97
+ continue;
98
+ if (ranges.has(rangeKey(scope.range))) {
99
+ anonymousNamespaceScopeIds.add(scope.id);
100
+ }
101
+ }
51
102
  }
52
103
  /**
53
104
  * Populate per-file "not globally visible" nodeIds by walking the parsed
@@ -75,6 +126,14 @@ export function populateCppNonGloballyVisible(parsed) {
75
126
  // unqualified lookup can still see their callable defs.
76
127
  if (scope.kind === 'Namespace' && isCppInlineNamespaceScope(scope.id))
77
128
  continue;
129
+ // Anonymous namespaces give internal linkage but their contents are
130
+ // visible at the enclosing scope within the same TU and propagate to
131
+ // any TU that `#include`s the declaring file. The `isFileLocal` mark
132
+ // (recorded on the def's name in this file) still blocks cross-file
133
+ // unqualified lookup that does not go through #include, so dropping
134
+ // the structural visibility exclusion here is safe.
135
+ if (scope.kind === 'Namespace' && anonymousNamespaceScopeIds.has(scope.id))
136
+ continue;
78
137
  for (const def of scope.ownedDefs) {
79
138
  set.add(def.nodeId);
80
139
  }
@@ -173,15 +232,27 @@ export function expandCppWildcardNames(targetModuleScope, parsedFiles) {
173
232
  // including TU. When the owning scope is unknown we default to
174
233
  // include (preserves prior behavior for any def whose structural
175
234
  // ownership wasn't recorded in `Scope.ownedDefs`).
235
+ //
236
+ // Anonymous namespaces are exempt: their members propagate to the
237
+ // enclosing scope of any TU that #includes the declaring file (each
238
+ // including TU gets its own internal-linkage copy per ISO C++).
176
239
  const ownerScope = ownerScopeByNodeId.get(def.nodeId);
240
+ const ownerIsAnonymousNamespace = ownerScope !== undefined &&
241
+ ownerScope.kind === 'Namespace' &&
242
+ anonymousNamespaceScopeIds.has(ownerScope.id);
177
243
  if (ownerScope !== undefined &&
244
+ !ownerIsAnonymousNamespace &&
178
245
  (ownerScope.kind === 'Namespace' || ownerScope.kind === 'Class')) {
179
246
  continue;
180
247
  }
181
248
  const name = simpleName(def);
182
249
  if (name === '')
183
250
  continue;
184
- if (isFileLocal(target.filePath, name))
251
+ // Same exemption for the `isFileLocal` mark — anonymous-namespace
252
+ // names are recorded as file-local to suppress the global free-call
253
+ // fallback's cross-file leak, but they MUST still propagate through
254
+ // wildcard import expansion to including TUs.
255
+ if (!ownerIsAnonymousNamespace && isFileLocal(target.filePath, name))
185
256
  continue;
186
257
  if (seen.has(name))
187
258
  continue;
@@ -7,7 +7,7 @@ import { cppArityCompatibility } from './arity.js';
7
7
  import { cppMergeBindings } from './merge-bindings.js';
8
8
  import { resolveCppImportTarget } from './import-target.js';
9
9
  import { scanCppHeaderFiles } from './header-scan.js';
10
- import { expandCppWildcardNames, isFileLocal, clearFileLocalNames, populateCppNonGloballyVisible, isCppDefGloballyVisible, } from './file-local-linkage.js';
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
12
  import { populateCppAssociatedNamespaces, clearCppAdlState, pickCppAdlCandidates, ADL_AMBIGUOUS, } from './adl.js';
13
13
  import { clearCppInlineNamespaces, populateCppInlineNamespaceScopes, resolveCppQualifiedNamespaceMember, } from './inline-namespaces.js';
@@ -61,10 +61,11 @@ export const cppScopeResolver = {
61
61
  buildMro: (graph, parsedFiles, nodeLookup) => buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
62
62
  populateOwners: (parsed) => {
63
63
  populateClassOwnedMembers(parsed);
64
- // Resolve inline-namespace ranges (recorded at capture time) to
65
- // ScopeIds BEFORE `populateCppNonGloballyVisible` runs, so the
66
- // inline-namespace exemption sees the populated Set.
64
+ // Resolve inline- and anonymous-namespace ranges (recorded at capture
65
+ // time) to ScopeIds BEFORE `populateCppNonGloballyVisible` runs, so
66
+ // both exemptions see the populated Sets.
67
67
  populateCppInlineNamespaceScopes(parsed);
68
+ populateCppAnonymousNamespaceScopes(parsed);
68
69
  // Track namespace-nested and class-nested defs so the global free-call
69
70
  // fallback and wildcard expansion can suppress them as unqualified
70
71
  // cross-file callables.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.5-rc.41",
3
+ "version": "1.6.5-rc.43",
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",