@optave/codegraph 3.10.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +13 -13
  2. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  3. package/dist/ast-analysis/rules/index.js +77 -0
  4. package/dist/ast-analysis/rules/index.js.map +1 -1
  5. package/dist/cli/commands/audit.js +1 -1
  6. package/dist/cli/commands/audit.js.map +1 -1
  7. package/dist/cli/commands/build.d.ts.map +1 -1
  8. package/dist/cli/commands/build.js +2 -0
  9. package/dist/cli/commands/build.js.map +1 -1
  10. package/dist/cli/commands/check.js +1 -1
  11. package/dist/cli/commands/check.js.map +1 -1
  12. package/dist/cli/commands/children.js +1 -1
  13. package/dist/cli/commands/children.js.map +1 -1
  14. package/dist/cli/commands/diff-impact.js +1 -1
  15. package/dist/cli/commands/diff-impact.js.map +1 -1
  16. package/dist/cli/commands/roles.js +1 -1
  17. package/dist/cli/commands/roles.js.map +1 -1
  18. package/dist/cli/commands/structure.js +1 -1
  19. package/dist/cli/commands/structure.js.map +1 -1
  20. package/dist/cli/shared/options.js +1 -1
  21. package/dist/cli/shared/options.js.map +1 -1
  22. package/dist/db/connection.d.ts.map +1 -1
  23. package/dist/db/connection.js +8 -0
  24. package/dist/db/connection.js.map +1 -1
  25. package/dist/domain/graph/builder/incremental.d.ts +0 -6
  26. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  27. package/dist/domain/graph/builder/incremental.js +6 -23
  28. package/dist/domain/graph/builder/incremental.js.map +1 -1
  29. package/dist/domain/graph/builder/pipeline.d.ts +44 -0
  30. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  31. package/dist/domain/graph/builder/pipeline.js +181 -39
  32. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  33. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  34. package/dist/domain/graph/builder/stages/build-edges.js +8 -2
  35. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  36. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  38. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  39. package/dist/domain/graph/watcher.d.ts.map +1 -1
  40. package/dist/domain/graph/watcher.js +23 -18
  41. package/dist/domain/graph/watcher.js.map +1 -1
  42. package/dist/domain/parser.d.ts.map +1 -1
  43. package/dist/domain/parser.js +27 -1
  44. package/dist/domain/parser.js.map +1 -1
  45. package/dist/domain/search/models.d.ts +16 -0
  46. package/dist/domain/search/models.d.ts.map +1 -1
  47. package/dist/domain/search/models.js +35 -1
  48. package/dist/domain/search/models.js.map +1 -1
  49. package/dist/domain/wasm-worker-entry.js +8 -1
  50. package/dist/domain/wasm-worker-entry.js.map +1 -1
  51. package/dist/extractors/c.js +25 -6
  52. package/dist/extractors/c.js.map +1 -1
  53. package/dist/extractors/cpp.js +47 -6
  54. package/dist/extractors/cpp.js.map +1 -1
  55. package/dist/extractors/cuda.js +90 -14
  56. package/dist/extractors/cuda.js.map +1 -1
  57. package/dist/extractors/elixir.js +83 -3
  58. package/dist/extractors/elixir.js.map +1 -1
  59. package/dist/extractors/erlang.js +56 -20
  60. package/dist/extractors/erlang.js.map +1 -1
  61. package/dist/extractors/fsharp.d.ts +7 -0
  62. package/dist/extractors/fsharp.d.ts.map +1 -1
  63. package/dist/extractors/fsharp.js +94 -0
  64. package/dist/extractors/fsharp.js.map +1 -1
  65. package/dist/extractors/gleam.js +6 -2
  66. package/dist/extractors/gleam.js.map +1 -1
  67. package/dist/extractors/groovy.js +41 -1
  68. package/dist/extractors/groovy.js.map +1 -1
  69. package/dist/extractors/haskell.js +48 -4
  70. package/dist/extractors/haskell.js.map +1 -1
  71. package/dist/extractors/julia.js +172 -41
  72. package/dist/extractors/julia.js.map +1 -1
  73. package/dist/extractors/kotlin.js +4 -0
  74. package/dist/extractors/kotlin.js.map +1 -1
  75. package/dist/extractors/objc.js +184 -47
  76. package/dist/extractors/objc.js.map +1 -1
  77. package/dist/extractors/python.js +7 -4
  78. package/dist/extractors/python.js.map +1 -1
  79. package/dist/extractors/r.js +93 -52
  80. package/dist/extractors/r.js.map +1 -1
  81. package/dist/extractors/scala.d.ts.map +1 -1
  82. package/dist/extractors/scala.js +18 -32
  83. package/dist/extractors/scala.js.map +1 -1
  84. package/dist/extractors/solidity.js +18 -9
  85. package/dist/extractors/solidity.js.map +1 -1
  86. package/dist/extractors/verilog.js +80 -15
  87. package/dist/extractors/verilog.js.map +1 -1
  88. package/dist/mcp/tool-registry.d.ts.map +1 -1
  89. package/dist/mcp/tool-registry.js +4 -0
  90. package/dist/mcp/tool-registry.js.map +1 -1
  91. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  92. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  93. package/dist/mcp/tools/semantic-search.js +1 -0
  94. package/dist/mcp/tools/semantic-search.js.map +1 -1
  95. package/dist/types.d.ts +15 -1
  96. package/dist/types.d.ts.map +1 -1
  97. package/grammars/tree-sitter-erlang.wasm +0 -0
  98. package/grammars/tree-sitter-fsharp.wasm +0 -0
  99. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  100. package/grammars/tree-sitter-gleam.wasm +0 -0
  101. package/package.json +10 -10
  102. package/src/ast-analysis/rules/index.ts +87 -0
  103. package/src/cli/commands/audit.ts +1 -1
  104. package/src/cli/commands/build.ts +2 -0
  105. package/src/cli/commands/check.ts +1 -1
  106. package/src/cli/commands/children.ts +1 -1
  107. package/src/cli/commands/diff-impact.ts +1 -1
  108. package/src/cli/commands/roles.ts +1 -1
  109. package/src/cli/commands/structure.ts +1 -1
  110. package/src/cli/shared/options.ts +1 -1
  111. package/src/db/connection.ts +8 -0
  112. package/src/domain/graph/builder/incremental.ts +6 -41
  113. package/src/domain/graph/builder/pipeline.ts +222 -37
  114. package/src/domain/graph/builder/stages/build-edges.ts +9 -2
  115. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  116. package/src/domain/graph/watcher.ts +21 -23
  117. package/src/domain/parser.ts +27 -1
  118. package/src/domain/search/models.ts +36 -1
  119. package/src/domain/wasm-worker-entry.ts +8 -1
  120. package/src/extractors/c.ts +27 -8
  121. package/src/extractors/cpp.ts +50 -8
  122. package/src/extractors/cuda.ts +90 -16
  123. package/src/extractors/elixir.ts +75 -3
  124. package/src/extractors/erlang.ts +63 -20
  125. package/src/extractors/fsharp.ts +104 -0
  126. package/src/extractors/gleam.ts +7 -2
  127. package/src/extractors/groovy.ts +45 -1
  128. package/src/extractors/haskell.ts +45 -4
  129. package/src/extractors/julia.ts +164 -43
  130. package/src/extractors/kotlin.ts +4 -0
  131. package/src/extractors/objc.ts +171 -47
  132. package/src/extractors/python.ts +5 -3
  133. package/src/extractors/r.ts +88 -48
  134. package/src/extractors/scala.ts +24 -36
  135. package/src/extractors/solidity.ts +17 -8
  136. package/src/extractors/verilog.ts +83 -15
  137. package/src/mcp/tool-registry.ts +5 -0
  138. package/src/mcp/tools/semantic-search.ts +2 -0
  139. package/src/types.ts +15 -0
@@ -83,17 +83,49 @@ function handleModuleDef(node: TreeSitterNode, ctx: ExtractorOutput): string | n
83
83
  return nameNode.text;
84
84
  }
85
85
 
86
+ function qualifyName(base: string, currentModule: string | null): string {
87
+ // For qualified names (`function Base.show ... end` inside `module Foo`,
88
+ // or short-form `Foo.bar(x, y) = x + y` inside `module Outer`), the LHS
89
+ // is a `scoped_identifier` already containing the qualifier — skip the
90
+ // module prefix to avoid producing `Foo.Base.show` / `Outer.Foo.bar`.
91
+ if (currentModule && !base.includes('.')) return `${currentModule}.${base}`;
92
+ return base;
93
+ }
94
+
95
+ /**
96
+ * Extract the call_expression from a function/macro definition's signature.
97
+ *
98
+ * tree-sitter-julia wraps the signature in a `signature` node whose direct
99
+ * children include the `call_expression` for the function name and parameters.
100
+ * `findChild` only inspects direct children, so we unwrap one level explicitly.
101
+ * Without this step, `findChild(node, 'call_expression')` on a
102
+ * `function_definition` would match the *body's* first call_expression
103
+ * (e.g. `println(...)` inside the body) instead of the signature.
104
+ *
105
+ * Grammar assumption: every `function_definition` / `macro_definition` emits a
106
+ * `signature` child in the current tree-sitter-julia grammar. The fallback to
107
+ * `findChild(node, 'call_expression')` exists only as a defensive measure for
108
+ * grammar drift — if it ever fires on a real definition, that fallback would
109
+ * silently match the first body call_expression and mis-record the function
110
+ * name. Callers must therefore treat a missing `signature` as a parser/grammar
111
+ * mismatch worth investigating, not as a routine code path.
112
+ */
113
+ function signatureCall(node: TreeSitterNode): TreeSitterNode | null {
114
+ const sig = findChild(node, 'signature');
115
+ if (sig) return findChild(sig, 'call_expression');
116
+ return findChild(node, 'call_expression');
117
+ }
118
+
86
119
  function handleFunctionDef(
87
120
  node: TreeSitterNode,
88
121
  ctx: ExtractorOutput,
89
122
  currentModule: string | null,
90
123
  ): void {
91
- // function_definition may have a call_expression child as the signature
92
- const callSig = findChild(node, 'call_expression');
124
+ const callSig = signatureCall(node);
93
125
  if (callSig) {
94
126
  const funcNameNode = callSig.child(0);
95
127
  if (funcNameNode) {
96
- const name = currentModule ? `${currentModule}.${funcNameNode.text}` : funcNameNode.text;
128
+ const name = qualifyName(funcNameNode.text, currentModule);
97
129
  const params = extractJuliaParams(callSig);
98
130
  ctx.definitions.push({
99
131
  name,
@@ -110,9 +142,8 @@ function handleFunctionDef(
110
142
  const nameNode = node.childForFieldName('name') || findChild(node, 'identifier');
111
143
  if (!nameNode) return;
112
144
 
113
- const name = currentModule ? `${currentModule}.${nameNode.text}` : nameNode.text;
114
145
  ctx.definitions.push({
115
- name,
146
+ name: qualifyName(nameNode.text, currentModule),
116
147
  kind: 'function',
117
148
  line: node.startPosition.row + 1,
118
149
  endLine: nodeEndLine(node),
@@ -133,11 +164,10 @@ function handleAssignment(
133
164
  const funcNameNode = lhs.child(0);
134
165
  if (!funcNameNode) return;
135
166
 
136
- const name = currentModule ? `${currentModule}.${funcNameNode.text}` : funcNameNode.text;
137
167
  const params = extractJuliaParams(lhs);
138
168
 
139
169
  ctx.definitions.push({
140
- name,
170
+ name: qualifyName(funcNameNode.text, currentModule),
141
171
  kind: 'function',
142
172
  line: node.startPosition.row + 1,
143
173
  endLine: nodeEndLine(node),
@@ -146,16 +176,74 @@ function handleAssignment(
146
176
  }
147
177
  }
148
178
 
179
+ /**
180
+ * Locate the base-name identifier within a `type_head` node.
181
+ *
182
+ * Handles plain identifiers, `Name <: Super` binary expressions, and
183
+ * parameterized forms like `Name{T}` / `Name{T} <: Super{T,1}` by recursing
184
+ * into wrapper kinds the Julia grammar actually emits for type heads
185
+ * (binary expressions, parametrized type expressions, parameterized
186
+ * identifiers). Returns `null` when no identifier can be located — callers
187
+ * should skip emitting a definition in that case.
188
+ *
189
+ * Note: `type_parameter_list` / `type_argument_list` are intentionally
190
+ * excluded — Julia's grammar uses `curly_expression` for `{T}` constructs,
191
+ * not those node kinds. Including them would risk recursing into a
192
+ * type-parameter list and returning a type variable (e.g. `T`) instead of
193
+ * the struct name if `findBaseName` were ever called on a node lacking a
194
+ * direct `identifier` child.
195
+ */
196
+ const TYPE_HEAD_WRAPPERS: ReadonlySet<string> = new Set([
197
+ 'binary_expression',
198
+ 'parametrized_type_expression',
199
+ 'parameterized_identifier',
200
+ ]);
201
+
202
+ function findBaseName(node: TreeSitterNode): TreeSitterNode | null {
203
+ if (node.type === 'identifier') return node;
204
+ const direct = findChild(node, 'identifier');
205
+ if (direct) return direct;
206
+ for (let i = 0; i < node.childCount; i++) {
207
+ const child = node.child(i);
208
+ if (!child) continue;
209
+ if (TYPE_HEAD_WRAPPERS.has(child.type)) {
210
+ const found = findBaseName(child);
211
+ if (found) return found;
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+
149
217
  function handleStructDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
150
218
  // struct_definition: struct type_head fields... end
219
+ // type_head wraps the name and optional supertype. The name may be a
220
+ // bare `identifier`, a parameterized form (e.g. `Vec{T}`), or either
221
+ // of those nested inside a `binary_expression` (`Name <: Super`).
151
222
  const typeHead = findChild(node, 'type_head');
152
- const nameNode = typeHead
153
- ? (findChild(typeHead, 'identifier') ?? typeHead)
154
- : findChild(node, 'identifier');
223
+ if (!typeHead) return;
224
+
225
+ let nameNode: TreeSitterNode | null;
226
+ let supertypeNode: TreeSitterNode | null = null;
227
+
228
+ const binary = findChild(typeHead, 'binary_expression');
229
+ if (binary) {
230
+ // Walk into each side of the binary expression to find the base-name
231
+ // identifier — handles parameterized forms like `Vec{T} <: AbstractArray{T,1}`.
232
+ const sides: TreeSitterNode[] = [];
233
+ for (let i = 0; i < binary.childCount; i++) {
234
+ const c = binary.child(i);
235
+ if (c && c.type !== 'operator') sides.push(c);
236
+ }
237
+ nameNode = sides[0] ? findBaseName(sides[0]) : null;
238
+ supertypeNode = sides[1] ? findBaseName(sides[1]) : null;
239
+ } else {
240
+ nameNode = findBaseName(typeHead);
241
+ }
242
+
155
243
  if (!nameNode) return;
244
+ const structName = nameNode.text;
156
245
 
157
246
  const children: SubDeclaration[] = [];
158
- // Fields are typed_expression children of struct_definition
159
247
  for (let i = 0; i < node.childCount; i++) {
160
248
  const child = node.child(i);
161
249
  if (!child) continue;
@@ -168,33 +256,24 @@ function handleStructDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
168
256
  line: child.startPosition.row + 1,
169
257
  });
170
258
  }
171
- }
172
- // Plain identifier fields (no type annotation)
173
- if (child.type === 'identifier' && child !== nameNode && typeHead && child !== typeHead) {
259
+ } else if (child.type === 'identifier') {
260
+ // Plain identifier fields (no type annotation) appear as direct
261
+ // identifier children of struct_definition. The type_head is a
262
+ // separate node so there is nothing to filter out here.
174
263
  children.push({ name: child.text, kind: 'property', line: child.startPosition.row + 1 });
175
264
  }
176
265
  }
177
266
 
178
- // Check for supertype in type_head (Point <: AbstractPoint)
179
- if (typeHead) {
180
- const subtypeExpr = findChild(typeHead, 'subtype_expression');
181
- if (subtypeExpr) {
182
- // Find the supertype identifier
183
- for (let i = 0; i < subtypeExpr.childCount; i++) {
184
- const child = subtypeExpr.child(i);
185
- if (child?.type === 'identifier' && i > 0) {
186
- ctx.classes.push({
187
- name: nameNode.text,
188
- extends: child.text,
189
- line: node.startPosition.row + 1,
190
- });
191
- }
192
- }
193
- }
267
+ if (supertypeNode) {
268
+ ctx.classes.push({
269
+ name: structName,
270
+ extends: supertypeNode.text,
271
+ line: node.startPosition.row + 1,
272
+ });
194
273
  }
195
274
 
196
275
  ctx.definitions.push({
197
- name: nameNode.text,
276
+ name: structName,
198
277
  kind: 'struct',
199
278
  line: node.startPosition.row + 1,
200
279
  endLine: nodeEndLine(node),
@@ -203,7 +282,14 @@ function handleStructDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
203
282
  }
204
283
 
205
284
  function handleAbstractDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
206
- const nameNode = node.childForFieldName('name') || findChild(node, 'identifier');
285
+ // abstract_definition: `abstract type` type_head `end`
286
+ // The identifier is nested inside `type_head` — possibly wrapped in a
287
+ // `Name <: Super` binary_expression or a `Name{T,...}` parameterized form.
288
+ // Mirror handleStructDef and skip rather than emit a garbled name when no
289
+ // base identifier can be located.
290
+ const typeHead = findChild(node, 'type_head');
291
+ if (!typeHead) return;
292
+ const nameNode = findBaseName(typeHead);
207
293
  if (!nameNode) return;
208
294
 
209
295
  ctx.definitions.push({
@@ -219,10 +305,17 @@ function handleMacroDef(
219
305
  ctx: ExtractorOutput,
220
306
  currentModule: string | null,
221
307
  ): void {
222
- const nameNode = node.childForFieldName('name') || findChild(node, 'identifier');
308
+ // macro_definition: `macro` signature/call_expression body `end`.
309
+ // The name lives in the same shape as a function signature — unwrap via
310
+ // signatureCall so we don't pick up an identifier from the body (e.g.
311
+ // `macro mymac(x) x end` would otherwise resolve to `@x`).
312
+ const callSig = signatureCall(node);
313
+ const nameNode =
314
+ callSig?.child(0) ?? node.childForFieldName('name') ?? findChild(node, 'identifier');
223
315
  if (!nameNode) return;
224
316
 
225
- const name = currentModule ? `${currentModule}.@${nameNode.text}` : `@${nameNode.text}`;
317
+ const base = nameNode.text;
318
+ const name = currentModule ? `${currentModule}.@${base}` : `@${base}`;
226
319
  ctx.definitions.push({
227
320
  name,
228
321
  kind: 'function',
@@ -232,19 +325,40 @@ function handleMacroDef(
232
325
  }
233
326
 
234
327
  function handleImport(node: TreeSitterNode, ctx: ExtractorOutput): void {
328
+ // tree-sitter-julia shapes:
329
+ // `using LinearAlgebra` → using_statement [ using, identifier ]
330
+ // `import Foo.Bar` → import_statement [ import, scoped_identifier ]
331
+ // `import Base: show` → import_statement [ import, selected_import[Base, show] ]
332
+ // `import Foo.Bar: baz` → import_statement [ import, selected_import[scoped_identifier, baz] ]
235
333
  const names: string[] = [];
236
334
  let source = '';
237
335
 
238
336
  for (let i = 0; i < node.childCount; i++) {
239
337
  const child = node.child(i);
240
338
  if (!child) continue;
241
- if (
242
- child.type === 'identifier' ||
243
- child.type === 'scoped_identifier' ||
244
- child.type === 'selected_import'
245
- ) {
246
- if (!source) source = child.text;
247
- names.push(child.text.split('.').pop() || child.text);
339
+ if (child.type === 'identifier' || child.type === 'scoped_identifier') {
340
+ const txt = child.text;
341
+ if (!source) source = txt;
342
+ names.push(txt.split('.').pop() || txt);
343
+ } else if (child.type === 'selected_import') {
344
+ // First identifier-bearing node is the source module; the rest are
345
+ // imported names. The module may itself be a `scoped_identifier`
346
+ // (e.g. `import Foo.Bar: baz`) — handle it alongside bare
347
+ // `identifier` and use the trailing segment as the display name,
348
+ // mirroring the outer loop.
349
+ let first = true;
350
+ for (let j = 0; j < child.childCount; j++) {
351
+ const part = child.child(j);
352
+ if (!part) continue;
353
+ if (part.type !== 'identifier' && part.type !== 'scoped_identifier') continue;
354
+ const txt = part.text;
355
+ if (first) {
356
+ if (!source) source = txt;
357
+ first = false;
358
+ } else {
359
+ names.push(txt.split('.').pop() || txt);
360
+ }
361
+ }
248
362
  }
249
363
  }
250
364
 
@@ -260,8 +374,15 @@ function handleImport(node: TreeSitterNode, ctx: ExtractorOutput): void {
260
374
  function handleCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
261
375
  // Don't record if parent is assignment LHS (that's a function definition)
262
376
  if (node.parent?.type === 'assignment' && node === node.parent.child(0)) return;
263
- // Don't record if parent is function_definition (that's a signature)
264
- if (node.parent?.type === 'function_definition') return;
377
+ // Skip when this call is the signature of a function/macro definition.
378
+ // tree-sitter-julia wraps the signature in a `signature` node whose parent
379
+ // is `function_definition` or `macro_definition`. Body calls (e.g.
380
+ // `println(name)` inside `function greet ... end`) appear as descendants of
381
+ // the body, not as direct children of `signature`, so they are unaffected.
382
+ if (node.parent?.type === 'signature') {
383
+ const grand = node.parent.parent;
384
+ if (grand?.type === 'function_definition' || grand?.type === 'macro_definition') return;
385
+ }
265
386
 
266
387
  const funcNode = node.child(0);
267
388
  if (!funcNode) return;
@@ -147,11 +147,13 @@ function collectKotlinMethods(node: TreeSitterNode, className: string, ctx: Extr
147
147
  if (!child || child.type !== 'function_declaration') continue;
148
148
  const methName = findChild(child, 'simple_identifier');
149
149
  if (methName) {
150
+ const params = extractKotlinParameters(child);
150
151
  ctx.definitions.push({
151
152
  name: `${className}.${methName.text}`,
152
153
  kind: 'method',
153
154
  line: child.startPosition.row + 1,
154
155
  endLine: child.endPosition.row + 1,
156
+ children: params.length > 0 ? params : undefined,
155
157
  visibility: extractModifierVisibility(child),
156
158
  });
157
159
  }
@@ -214,11 +216,13 @@ function handleKotlinObjectDecl(node: TreeSitterNode, ctx: ExtractorOutput): voi
214
216
  if (child && child.type === 'function_declaration') {
215
217
  const methName = findChild(child, 'simple_identifier');
216
218
  if (methName) {
219
+ const params = extractKotlinParameters(child);
217
220
  ctx.definitions.push({
218
221
  name: `${nameNode.text}.${methName.text}`,
219
222
  kind: 'method',
220
223
  line: child.startPosition.row + 1,
221
224
  endLine: child.endPosition.row + 1,
225
+ children: params.length > 0 ? params : undefined,
222
226
  visibility: extractModifierVisibility(child),
223
227
  });
224
228
  }
@@ -55,6 +55,11 @@ function walkObjCNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
55
55
  case 'preproc_import':
56
56
  handleImport(node, ctx);
57
57
  break;
58
+ // tree-sitter-objc v3 emits `module_import` for `@import Foundation;`
59
+ // statements. Older grammar revisions used `import_declaration`, so we
60
+ // accept both for forward/backward compatibility and keep behaviour
61
+ // aligned with `handle_at_import` on the Rust side.
62
+ case 'module_import':
58
63
  case 'import_declaration':
59
64
  handleAtImport(node, ctx);
60
65
  break;
@@ -87,29 +92,46 @@ function handleClassInterface(node: TreeSitterNode, ctx: ExtractorOutput): void
87
92
  const nameNode = node.childForFieldName('name') || findObjCDeclName(node);
88
93
  if (!nameNode) return;
89
94
  const name = nameNode.text;
95
+ // Categories declared as `@interface Foo (Cat)` arrive as `class_interface`
96
+ // with a `category` field (rather than the `category_interface` node type).
97
+ // Qualify the display name with `(Cat)` so symbols stay grouped per category
98
+ // and match the Rust extractor.
99
+ const category = node.childForFieldName('category');
100
+ const displayName = category ? `${name}(${category.text})` : name;
90
101
 
91
102
  const members = collectClassMembers(node);
92
103
  ctx.definitions.push({
93
- name,
104
+ name: displayName,
94
105
  kind: 'class',
95
106
  line: node.startPosition.row + 1,
96
107
  endLine: nodeEndLine(node),
97
108
  children: members.length > 0 ? members : undefined,
98
109
  });
99
110
 
100
- // Superclass
111
+ // Superclass — keyed on the bare class name (categories don't have a superclass).
101
112
  const superclass = node.childForFieldName('superclass');
102
113
  if (superclass) {
103
114
  ctx.classes.push({ name, extends: superclass.text, line: node.startPosition.row + 1 });
104
115
  }
105
116
 
106
- // Protocols
107
- const protocols = findChild(node, 'protocol_qualifiers');
117
+ // Adopted protocols. tree-sitter-objc v3 wraps the adopted-protocol list in
118
+ // `parameterized_arguments` (not `protocol_qualifiers`, which was the v2
119
+ // grammar shape). Each child is wrapped in `type_name > type_identifier`;
120
+ // fall back to a bare `identifier`/`type_identifier` for older grammars.
121
+ const protocols = findChild(node, 'parameterized_arguments');
108
122
  if (protocols) {
109
123
  for (let i = 0; i < protocols.childCount; i++) {
110
124
  const proto = protocols.child(i);
111
- if (proto && proto.type === 'identifier') {
112
- ctx.classes.push({ name, implements: proto.text, line: node.startPosition.row + 1 });
125
+ if (!proto) continue;
126
+ let protoName: string | null = null;
127
+ if (proto.type === 'type_name') {
128
+ const inner = findChild(proto, 'type_identifier') || findChild(proto, 'identifier');
129
+ if (inner) protoName = inner.text;
130
+ } else if (proto.type === 'identifier' || proto.type === 'type_identifier') {
131
+ protoName = proto.text;
132
+ }
133
+ if (protoName) {
134
+ ctx.classes.push({ name, implements: protoName, line: node.startPosition.row + 1 });
113
135
  }
114
136
  }
115
137
  }
@@ -118,9 +140,14 @@ function handleClassInterface(node: TreeSitterNode, ctx: ExtractorOutput): void
118
140
  function handleClassImplementation(node: TreeSitterNode, ctx: ExtractorOutput): void {
119
141
  const nameNode = node.childForFieldName('name') || findObjCDeclName(node);
120
142
  if (!nameNode) return;
143
+ // Categories declared as `@implementation Foo (Cat)` arrive as
144
+ // `class_implementation` with a `category` field. Mirror the Rust extractor
145
+ // and qualify the display name with `(Cat)`.
146
+ const category = node.childForFieldName('category');
147
+ const displayName = category ? `${nameNode.text}(${category.text})` : nameNode.text;
121
148
 
122
149
  ctx.definitions.push({
123
- name: nameNode.text,
150
+ name: displayName,
124
151
  kind: 'class',
125
152
  line: node.startPosition.row + 1,
126
153
  endLine: nodeEndLine(node),
@@ -285,7 +312,20 @@ function handleTypedef(node: TreeSitterNode, ctx: ExtractorOutput): void {
285
312
  // ── Call handlers ─────────────────────────────────────────────────────────
286
313
 
287
314
  function handleCCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
288
- const funcNode = node.childForFieldName('function');
315
+ // tree-sitter-objc does not expose a `function` field on `call_expression`,
316
+ // so the named-field lookup almost always misses. Fall back to the first
317
+ // `identifier` / `field_expression` child to mirror `handle_c_call_expr` in
318
+ // `crates/codegraph-core/src/extractors/objc.rs` and keep engine parity.
319
+ let funcNode = node.childForFieldName('function');
320
+ if (!funcNode) {
321
+ for (let i = 0; i < node.childCount; i++) {
322
+ const child = node.child(i);
323
+ if (child && (child.type === 'identifier' || child.type === 'field_expression')) {
324
+ funcNode = child;
325
+ break;
326
+ }
327
+ }
328
+ }
289
329
  if (!funcNode) return;
290
330
  const call: Call = { name: '', line: node.startPosition.row + 1 };
291
331
  if (funcNode.type === 'field_expression') {
@@ -302,10 +342,33 @@ function handleCCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
302
342
  function handleMessageExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
303
343
  // [receiver selector:arg ...]
304
344
  const receiver = node.childForFieldName('receiver');
305
- const selector = node.childForFieldName('selector');
306
- if (!selector) return;
307
345
 
308
- const call: Call = { name: selector.text, line: node.startPosition.row + 1 };
346
+ // tree-sitter-objc v3 does not expose a `selector` field on
347
+ // `message_expression`; instead every keyword identifier has the `method`
348
+ // field. Assemble the selector by joining `method` children with `:`,
349
+ // appending a trailing `:` when the message has at least one colon
350
+ // (keyword form). Mirrors `build_message_selector` in
351
+ // `crates/codegraph-core/src/extractors/objc.rs`.
352
+ const parts: string[] = [];
353
+ let hasColon = false;
354
+ for (let i = 0; i < node.childCount; i++) {
355
+ const child = node.child(i);
356
+ if (!child) continue;
357
+ const fieldName = node.fieldNameForChild(i);
358
+ if (fieldName === 'method') parts.push(child.text);
359
+ if (child.type === ':') hasColon = true;
360
+ }
361
+ let name: string;
362
+ if (parts.length > 0) {
363
+ name = hasColon ? `${parts.join(':')}:` : parts.join(':');
364
+ } else {
365
+ // Fallback: some grammar revisions expose a `selector` field.
366
+ const selector = node.childForFieldName('selector');
367
+ if (!selector) return;
368
+ name = selector.text;
369
+ }
370
+
371
+ const call: Call = { name, line: node.startPosition.row + 1 };
309
372
  if (receiver) call.receiver = receiver.text;
310
373
  ctx.calls.push(call);
311
374
  }
@@ -313,29 +376,25 @@ function handleMessageExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
313
376
  // ── Helpers ───────────────────────────────────────────────────────────────
314
377
 
315
378
  function buildSelector(methodNode: TreeSitterNode): string | null {
316
- const selector = methodNode.childForFieldName('selector');
317
- if (selector) return selector.text;
318
-
319
- // Build selector from keyword children: initWith:name:
379
+ // tree-sitter-objc v3 does not expose a `selector` field; the selector is
380
+ // assembled from the leading `identifier` keywords. Multi-keyword forms
381
+ // look like `setName:(...)x age:(...)y` and appear as flat
382
+ // `identifier` + `method_parameter` children directly under the method
383
+ // node (not wrapped in `keyword_selector`). Mirrors `build_selector` in
384
+ // `crates/codegraph-core/src/extractors/objc.rs`.
320
385
  const parts: string[] = [];
386
+ let hasParams = false;
321
387
  for (let i = 0; i < methodNode.childCount; i++) {
322
388
  const child = methodNode.child(i);
323
389
  if (!child) continue;
324
- if (child.type === 'keyword_selector') {
325
- for (let j = 0; j < child.childCount; j++) {
326
- const kw = child.child(j);
327
- if (kw && kw.type === 'keyword_declarator') {
328
- const kwName = kw.childForFieldName('keyword');
329
- if (kwName) parts.push(kwName.text);
330
- }
331
- }
332
- }
333
- if (child.type === 'identifier' && i === 1) {
334
- // Simple unary selector
335
- return child.text;
390
+ if (child.type === 'identifier') {
391
+ parts.push(child.text);
392
+ } else if (child.type === 'method_parameter') {
393
+ hasParams = true;
336
394
  }
337
395
  }
338
- return parts.length > 0 ? `${parts.join(':')}:` : null;
396
+ if (parts.length === 0) return null;
397
+ return hasParams ? `${parts.join(':')}:` : parts.join(':');
339
398
  }
340
399
 
341
400
  function findObjCParentClass(node: TreeSitterNode): string | null {
@@ -349,7 +408,14 @@ function findObjCParentClass(node: TreeSitterNode): string | null {
349
408
  current.type === 'category_implementation'
350
409
  ) {
351
410
  const nameNode = current.childForFieldName('name') || findObjCDeclName(current);
352
- return nameNode ? nameNode.text : null;
411
+ if (!nameNode) return null;
412
+ // Categories: include `(Cat)` so methods are grouped per category.
413
+ // Two categories on the same class can declare same-named methods, so
414
+ // qualifying the parent name keeps the symbols disambiguated. Mirrors
415
+ // `find_objc_parent_class` in `crates/codegraph-core/src/extractors/objc.rs`.
416
+ const category = current.childForFieldName('category');
417
+ if (category) return `${nameNode.text}(${category.text})`;
418
+ return nameNode.text;
353
419
  }
354
420
  current = current.parent;
355
421
  }
@@ -381,32 +447,65 @@ function collectClassMembers(classNode: TreeSitterNode): SubDeclaration[] {
381
447
  }
382
448
  }
383
449
  if (child.type === 'property_declaration') {
384
- const propName = child.childForFieldName('name');
450
+ const propName = extractPropertyName(child);
385
451
  if (propName) {
386
- members.push({ name: propName.text, kind: 'property', line: child.startPosition.row + 1 });
452
+ members.push({ name: propName, kind: 'property', line: child.startPosition.row + 1 });
387
453
  }
388
454
  }
389
455
  }
390
456
  return members;
391
457
  }
392
458
 
459
+ /**
460
+ * Extract the property name from `@property (...) Type *foo;`. The v3 grammar
461
+ * does not expose `name` as a named field on `property_declaration`; instead
462
+ * the identifier nests under `struct_declaration > struct_declarator >
463
+ * [pointer_declarator >] identifier`. Mirrors `extract_property_name` in
464
+ * `crates/codegraph-core/src/extractors/objc.rs`.
465
+ */
466
+ function extractPropertyName(propNode: TreeSitterNode): string | null {
467
+ const structDecl = findChild(propNode, 'struct_declaration');
468
+ if (!structDecl) return null;
469
+ for (let i = 0; i < structDecl.childCount; i++) {
470
+ const child = structDecl.child(i);
471
+ if (!child || child.type !== 'struct_declarator') continue;
472
+ const id = findIdentifierDeep(child);
473
+ if (id) return id.text;
474
+ }
475
+ return null;
476
+ }
477
+
478
+ function findIdentifierDeep(node: TreeSitterNode): TreeSitterNode | null {
479
+ if (node.type === 'identifier') return node;
480
+ for (let i = 0; i < node.childCount; i++) {
481
+ const child = node.child(i);
482
+ if (!child) continue;
483
+ const found = findIdentifierDeep(child);
484
+ if (found) return found;
485
+ }
486
+ return null;
487
+ }
488
+
393
489
  function extractMethodParams(methodNode: TreeSitterNode): SubDeclaration[] {
490
+ // The v3 grammar emits flat `method_parameter` children under the method
491
+ // node; the parameter name is the last `identifier` inside each
492
+ // `method_parameter`. Mirrors `extract_method_params` in
493
+ // `crates/codegraph-core/src/extractors/objc.rs`.
394
494
  const params: SubDeclaration[] = [];
395
495
  for (let i = 0; i < methodNode.childCount; i++) {
396
496
  const child = methodNode.child(i);
397
- if (!child || child.type !== 'keyword_selector') continue;
497
+ if (!child || child.type !== 'method_parameter') continue;
498
+ let nameNode: TreeSitterNode | null = null;
398
499
  for (let j = 0; j < child.childCount; j++) {
399
- const kw = child.child(j);
400
- if (kw && kw.type === 'keyword_declarator') {
401
- const nameNode = kw.childForFieldName('name');
402
- if (nameNode) {
403
- params.push({
404
- name: nameNode.text,
405
- kind: 'parameter',
406
- line: nameNode.startPosition.row + 1,
407
- });
408
- }
409
- }
500
+ const inner = child.child(j);
501
+ if (inner && inner.type === 'identifier') nameNode = inner;
502
+ }
503
+ if (nameNode) {
504
+ params.push({
505
+ name: nameNode.text,
506
+ kind: 'parameter',
507
+ line: nameNode.startPosition.row + 1,
508
+ });
410
509
  }
411
510
  }
412
511
  return params;
@@ -420,12 +519,37 @@ function extractCParams(paramListNode: TreeSitterNode | null): SubDeclaration[]
420
519
  if (!param || param.type !== 'parameter_declaration') continue;
421
520
  const nameNode = param.childForFieldName('declarator');
422
521
  if (nameNode) {
423
- const name =
424
- nameNode.type === 'identifier'
425
- ? nameNode.text
426
- : (findChild(nameNode, 'identifier')?.text ?? nameNode.text);
522
+ const name = unwrapObjCDeclaratorName(nameNode);
427
523
  params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
428
524
  }
429
525
  }
430
526
  return params;
431
527
  }
528
+
529
+ const OBJC_DECLARATOR_WRAPPERS = new Set([
530
+ 'pointer_declarator',
531
+ 'reference_declarator',
532
+ 'array_declarator',
533
+ 'parenthesized_declarator',
534
+ 'function_declarator',
535
+ ]);
536
+
537
+ /**
538
+ * Drill through pointer/array/reference/parenthesized/function declarator
539
+ * wrappers to recover the bare parameter identifier. Without this the WASM
540
+ * extractor emitted raw declarator text (e.g. `*argv[]` or `callback(int x)`)
541
+ * while native unwrapped to `argv` / `callback`, producing cross-engine
542
+ * `contains` divergence on C-style parameters such as
543
+ * `int main(int argc, const char *argv[])` and function-type parameters such
544
+ * as `void process(int callback(int))`.
545
+ */
546
+ function unwrapObjCDeclaratorName(node: TreeSitterNode): string {
547
+ let current: TreeSitterNode | null = node;
548
+ while (current && OBJC_DECLARATOR_WRAPPERS.has(current.type)) {
549
+ current = current.childForFieldName('declarator');
550
+ }
551
+ if (current?.type === 'identifier' || current?.type === 'field_identifier') {
552
+ return current.text;
553
+ }
554
+ return current?.text ?? node.text;
555
+ }