@optave/codegraph 3.10.0 → 3.11.1

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 (312) hide show
  1. package/README.md +40 -33
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +91 -60
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/rules/index.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/index.js +77 -0
  7. package/dist/ast-analysis/rules/index.js.map +1 -1
  8. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  9. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitor-utils.js +83 -49
  11. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  12. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  14. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  15. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  17. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  18. package/dist/cli/commands/audit.js +1 -1
  19. package/dist/cli/commands/audit.js.map +1 -1
  20. package/dist/cli/commands/build.d.ts.map +1 -1
  21. package/dist/cli/commands/build.js +2 -0
  22. package/dist/cli/commands/build.js.map +1 -1
  23. package/dist/cli/commands/check.js +1 -1
  24. package/dist/cli/commands/check.js.map +1 -1
  25. package/dist/cli/commands/children.js +1 -1
  26. package/dist/cli/commands/children.js.map +1 -1
  27. package/dist/cli/commands/diff-impact.js +1 -1
  28. package/dist/cli/commands/diff-impact.js.map +1 -1
  29. package/dist/cli/commands/embed.d.ts.map +1 -1
  30. package/dist/cli/commands/embed.js +49 -4
  31. package/dist/cli/commands/embed.js.map +1 -1
  32. package/dist/cli/commands/roles.js +1 -1
  33. package/dist/cli/commands/roles.js.map +1 -1
  34. package/dist/cli/commands/structure.js +1 -1
  35. package/dist/cli/commands/structure.js.map +1 -1
  36. package/dist/cli/shared/options.js +1 -1
  37. package/dist/cli/shared/options.js.map +1 -1
  38. package/dist/db/connection.d.ts.map +1 -1
  39. package/dist/db/connection.js +8 -0
  40. package/dist/db/connection.js.map +1 -1
  41. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  42. package/dist/domain/analysis/dependencies.js +106 -80
  43. package/dist/domain/analysis/dependencies.js.map +1 -1
  44. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  45. package/dist/domain/analysis/fn-impact.js +77 -52
  46. package/dist/domain/analysis/fn-impact.js.map +1 -1
  47. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  48. package/dist/domain/analysis/module-map.js +132 -121
  49. package/dist/domain/analysis/module-map.js.map +1 -1
  50. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  51. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  52. package/dist/domain/graph/builder/helpers.js +47 -33
  53. package/dist/domain/graph/builder/helpers.js.map +1 -1
  54. package/dist/domain/graph/builder/incremental.d.ts +6 -6
  55. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/incremental.js +148 -99
  57. package/dist/domain/graph/builder/incremental.js.map +1 -1
  58. package/dist/domain/graph/builder/pipeline.d.ts +1 -0
  59. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  60. package/dist/domain/graph/builder/pipeline.js +23 -637
  61. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  62. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  63. package/dist/domain/graph/builder/stages/build-edges.js +141 -98
  64. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  65. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  66. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  67. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  68. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  70. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  71. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  72. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  73. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  74. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  75. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  76. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  77. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  78. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  79. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  80. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  81. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  82. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  83. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  84. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  85. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  86. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  88. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  89. package/dist/domain/graph/cycles.d.ts +6 -4
  90. package/dist/domain/graph/cycles.d.ts.map +1 -1
  91. package/dist/domain/graph/cycles.js +50 -55
  92. package/dist/domain/graph/cycles.js.map +1 -1
  93. package/dist/domain/graph/journal.d.ts.map +1 -1
  94. package/dist/domain/graph/journal.js +89 -70
  95. package/dist/domain/graph/journal.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts.map +1 -1
  97. package/dist/domain/graph/watcher.js +28 -20
  98. package/dist/domain/graph/watcher.js.map +1 -1
  99. package/dist/domain/parser.d.ts +12 -23
  100. package/dist/domain/parser.d.ts.map +1 -1
  101. package/dist/domain/parser.js +153 -80
  102. package/dist/domain/parser.js.map +1 -1
  103. package/dist/domain/search/generator.d.ts +3 -1
  104. package/dist/domain/search/generator.d.ts.map +1 -1
  105. package/dist/domain/search/generator.js +68 -45
  106. package/dist/domain/search/generator.js.map +1 -1
  107. package/dist/domain/search/models.d.ts +18 -0
  108. package/dist/domain/search/models.d.ts.map +1 -1
  109. package/dist/domain/search/models.js +72 -4
  110. package/dist/domain/search/models.js.map +1 -1
  111. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  112. package/dist/domain/search/search/hybrid.js +49 -40
  113. package/dist/domain/search/search/hybrid.js.map +1 -1
  114. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  115. package/dist/domain/search/search/semantic.js +69 -49
  116. package/dist/domain/search/search/semantic.js.map +1 -1
  117. package/dist/domain/wasm-worker-entry.js +209 -137
  118. package/dist/domain/wasm-worker-entry.js.map +1 -1
  119. package/dist/extractors/c.js +25 -6
  120. package/dist/extractors/c.js.map +1 -1
  121. package/dist/extractors/cpp.js +47 -6
  122. package/dist/extractors/cpp.js.map +1 -1
  123. package/dist/extractors/cuda.js +90 -14
  124. package/dist/extractors/cuda.js.map +1 -1
  125. package/dist/extractors/elixir.js +108 -4
  126. package/dist/extractors/elixir.js.map +1 -1
  127. package/dist/extractors/erlang.js +56 -20
  128. package/dist/extractors/erlang.js.map +1 -1
  129. package/dist/extractors/fsharp.d.ts +7 -0
  130. package/dist/extractors/fsharp.d.ts.map +1 -1
  131. package/dist/extractors/fsharp.js +94 -0
  132. package/dist/extractors/fsharp.js.map +1 -1
  133. package/dist/extractors/gleam.d.ts.map +1 -1
  134. package/dist/extractors/gleam.js +29 -33
  135. package/dist/extractors/gleam.js.map +1 -1
  136. package/dist/extractors/groovy.js +41 -1
  137. package/dist/extractors/groovy.js.map +1 -1
  138. package/dist/extractors/haskell.js +48 -4
  139. package/dist/extractors/haskell.js.map +1 -1
  140. package/dist/extractors/helpers.d.ts +79 -1
  141. package/dist/extractors/helpers.d.ts.map +1 -1
  142. package/dist/extractors/helpers.js +137 -0
  143. package/dist/extractors/helpers.js.map +1 -1
  144. package/dist/extractors/java.d.ts.map +1 -1
  145. package/dist/extractors/java.js +37 -49
  146. package/dist/extractors/java.js.map +1 -1
  147. package/dist/extractors/javascript.d.ts.map +1 -1
  148. package/dist/extractors/javascript.js +44 -44
  149. package/dist/extractors/javascript.js.map +1 -1
  150. package/dist/extractors/julia.js +198 -74
  151. package/dist/extractors/julia.js.map +1 -1
  152. package/dist/extractors/kotlin.js +4 -0
  153. package/dist/extractors/kotlin.js.map +1 -1
  154. package/dist/extractors/objc.js +184 -47
  155. package/dist/extractors/objc.js.map +1 -1
  156. package/dist/extractors/python.js +7 -4
  157. package/dist/extractors/python.js.map +1 -1
  158. package/dist/extractors/r.d.ts.map +1 -1
  159. package/dist/extractors/r.js +103 -87
  160. package/dist/extractors/r.js.map +1 -1
  161. package/dist/extractors/scala.d.ts.map +1 -1
  162. package/dist/extractors/scala.js +18 -32
  163. package/dist/extractors/scala.js.map +1 -1
  164. package/dist/extractors/solidity.d.ts.map +1 -1
  165. package/dist/extractors/solidity.js +55 -69
  166. package/dist/extractors/solidity.js.map +1 -1
  167. package/dist/extractors/verilog.js +80 -15
  168. package/dist/extractors/verilog.js.map +1 -1
  169. package/dist/features/boundaries.d.ts.map +1 -1
  170. package/dist/features/boundaries.js +49 -39
  171. package/dist/features/boundaries.js.map +1 -1
  172. package/dist/features/cfg.d.ts.map +1 -1
  173. package/dist/features/cfg.js +90 -63
  174. package/dist/features/cfg.js.map +1 -1
  175. package/dist/features/check.d.ts.map +1 -1
  176. package/dist/features/check.js +43 -34
  177. package/dist/features/check.js.map +1 -1
  178. package/dist/features/cochange.d.ts.map +1 -1
  179. package/dist/features/cochange.js +68 -56
  180. package/dist/features/cochange.js.map +1 -1
  181. package/dist/features/complexity.d.ts.map +1 -1
  182. package/dist/features/complexity.js +105 -75
  183. package/dist/features/complexity.js.map +1 -1
  184. package/dist/features/dataflow.d.ts.map +1 -1
  185. package/dist/features/dataflow.js +37 -29
  186. package/dist/features/dataflow.js.map +1 -1
  187. package/dist/features/flow.d.ts.map +1 -1
  188. package/dist/features/flow.js +31 -22
  189. package/dist/features/flow.js.map +1 -1
  190. package/dist/features/graph-enrichment.d.ts.map +1 -1
  191. package/dist/features/graph-enrichment.js +77 -70
  192. package/dist/features/graph-enrichment.js.map +1 -1
  193. package/dist/features/owners.d.ts +17 -26
  194. package/dist/features/owners.d.ts.map +1 -1
  195. package/dist/features/owners.js +120 -109
  196. package/dist/features/owners.js.map +1 -1
  197. package/dist/features/sequence.d.ts.map +1 -1
  198. package/dist/features/sequence.js +59 -54
  199. package/dist/features/sequence.js.map +1 -1
  200. package/dist/features/structure-query.d.ts.map +1 -1
  201. package/dist/features/structure-query.js +60 -60
  202. package/dist/features/structure-query.js.map +1 -1
  203. package/dist/features/structure.js +28 -36
  204. package/dist/features/structure.js.map +1 -1
  205. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  206. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  207. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  208. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  209. package/dist/graph/classifiers/roles.js +63 -59
  210. package/dist/graph/classifiers/roles.js.map +1 -1
  211. package/dist/infrastructure/config.d.ts +1 -1
  212. package/dist/infrastructure/config.d.ts.map +1 -1
  213. package/dist/infrastructure/config.js +1 -1
  214. package/dist/infrastructure/config.js.map +1 -1
  215. package/dist/mcp/tool-registry.d.ts.map +1 -1
  216. package/dist/mcp/tool-registry.js +4 -0
  217. package/dist/mcp/tool-registry.js.map +1 -1
  218. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  219. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  220. package/dist/mcp/tools/semantic-search.js +1 -0
  221. package/dist/mcp/tools/semantic-search.js.map +1 -1
  222. package/dist/presentation/cfg.d.ts.map +1 -1
  223. package/dist/presentation/cfg.js +44 -29
  224. package/dist/presentation/cfg.js.map +1 -1
  225. package/dist/presentation/flow.d.ts.map +1 -1
  226. package/dist/presentation/flow.js +58 -38
  227. package/dist/presentation/flow.js.map +1 -1
  228. package/dist/types.d.ts +16 -2
  229. package/dist/types.d.ts.map +1 -1
  230. package/grammars/tree-sitter-erlang.wasm +0 -0
  231. package/grammars/tree-sitter-fsharp.wasm +0 -0
  232. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +10 -10
  235. package/src/ast-analysis/engine.ts +145 -61
  236. package/src/ast-analysis/rules/index.ts +87 -0
  237. package/src/ast-analysis/visitor-utils.ts +86 -46
  238. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  239. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  240. package/src/cli/commands/audit.ts +1 -1
  241. package/src/cli/commands/build.ts +2 -0
  242. package/src/cli/commands/check.ts +1 -1
  243. package/src/cli/commands/children.ts +1 -1
  244. package/src/cli/commands/diff-impact.ts +1 -1
  245. package/src/cli/commands/embed.ts +54 -4
  246. package/src/cli/commands/roles.ts +1 -1
  247. package/src/cli/commands/structure.ts +1 -1
  248. package/src/cli/shared/options.ts +1 -1
  249. package/src/db/connection.ts +8 -0
  250. package/src/domain/analysis/dependencies.ts +166 -85
  251. package/src/domain/analysis/fn-impact.ts +120 -50
  252. package/src/domain/analysis/module-map.ts +175 -140
  253. package/src/domain/graph/builder/helpers.ts +85 -76
  254. package/src/domain/graph/builder/incremental.ts +223 -131
  255. package/src/domain/graph/builder/pipeline.ts +32 -785
  256. package/src/domain/graph/builder/stages/build-edges.ts +207 -142
  257. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  258. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  259. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  260. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  261. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  262. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  263. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  264. package/src/domain/graph/cycles.ts +51 -49
  265. package/src/domain/graph/journal.ts +84 -69
  266. package/src/domain/graph/watcher.ts +29 -25
  267. package/src/domain/parser.ts +170 -67
  268. package/src/domain/search/generator.ts +132 -74
  269. package/src/domain/search/models.ts +75 -4
  270. package/src/domain/search/search/hybrid.ts +53 -42
  271. package/src/domain/search/search/semantic.ts +105 -65
  272. package/src/domain/wasm-worker-entry.ts +243 -153
  273. package/src/extractors/c.ts +27 -8
  274. package/src/extractors/cpp.ts +50 -8
  275. package/src/extractors/cuda.ts +90 -16
  276. package/src/extractors/elixir.ts +103 -4
  277. package/src/extractors/erlang.ts +63 -20
  278. package/src/extractors/fsharp.ts +104 -0
  279. package/src/extractors/gleam.ts +40 -39
  280. package/src/extractors/groovy.ts +45 -1
  281. package/src/extractors/haskell.ts +45 -4
  282. package/src/extractors/helpers.ts +205 -1
  283. package/src/extractors/java.ts +42 -45
  284. package/src/extractors/javascript.ts +44 -43
  285. package/src/extractors/julia.ts +191 -77
  286. package/src/extractors/kotlin.ts +4 -0
  287. package/src/extractors/objc.ts +171 -47
  288. package/src/extractors/python.ts +5 -3
  289. package/src/extractors/r.ts +104 -82
  290. package/src/extractors/scala.ts +24 -36
  291. package/src/extractors/solidity.ts +59 -78
  292. package/src/extractors/verilog.ts +83 -15
  293. package/src/features/boundaries.ts +64 -46
  294. package/src/features/cfg.ts +145 -74
  295. package/src/features/check.ts +60 -43
  296. package/src/features/cochange.ts +95 -72
  297. package/src/features/complexity.ts +134 -79
  298. package/src/features/dataflow.ts +57 -34
  299. package/src/features/flow.ts +48 -24
  300. package/src/features/graph-enrichment.ts +105 -70
  301. package/src/features/owners.ts +186 -146
  302. package/src/features/sequence.ts +99 -69
  303. package/src/features/structure-query.ts +94 -79
  304. package/src/features/structure.ts +56 -56
  305. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  306. package/src/graph/classifiers/roles.ts +64 -54
  307. package/src/infrastructure/config.ts +1 -1
  308. package/src/mcp/tool-registry.ts +5 -0
  309. package/src/mcp/tools/semantic-search.ts +2 -0
  310. package/src/presentation/cfg.ts +48 -32
  311. package/src/presentation/flow.ts +100 -52
  312. package/src/types.ts +16 -1
@@ -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
+ }
@@ -122,7 +122,7 @@ function handlePyFunctionDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
122
122
  const parentClass = findPythonParentClass(node);
123
123
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
124
124
  const kind = parentClass ? 'method' : 'function';
125
- const fnChildren = extractPythonParameters(node);
125
+ const fnChildren = extractPythonParameters(node, parentClass !== null);
126
126
  ctx.definitions.push({
127
127
  name: fullName,
128
128
  kind,
@@ -238,7 +238,7 @@ function handlePyImportFrom(node: TreeSitterNode, ctx: ExtractorOutput): void {
238
238
 
239
239
  // ── Python-specific helpers ─────────────────────────────────────────────────
240
240
 
241
- function extractPythonParameters(fnNode: TreeSitterNode): SubDeclaration[] {
241
+ function extractPythonParameters(fnNode: TreeSitterNode, isMethod: boolean): SubDeclaration[] {
242
242
  const params: SubDeclaration[] = [];
243
243
  const paramsNode = fnNode.childForFieldName('parameters') || findChild(fnNode, 'parameters');
244
244
  if (!paramsNode) return params;
@@ -246,7 +246,9 @@ function extractPythonParameters(fnNode: TreeSitterNode): SubDeclaration[] {
246
246
  const child = paramsNode.child(i);
247
247
  if (!child) continue;
248
248
  const param = extractSinglePyParam(child);
249
- if (param) params.push(param);
249
+ if (!param) continue;
250
+ if (isMethod && (param.name === 'self' || param.name === 'cls')) continue;
251
+ params.push(param);
250
252
  }
251
253
  return params;
252
254
  }
@@ -1,5 +1,13 @@
1
1
  import type { ExtractorOutput, SubDeclaration, TreeSitterNode, TreeSitterTree } from '../types.js';
2
- import { findChild, nodeEndLine } from './helpers.js';
2
+ import {
3
+ findChild,
4
+ findFirstChildOfTypes,
5
+ nodeEndLine,
6
+ nodeStartLine,
7
+ pushCall,
8
+ pushImport,
9
+ stripQuotes,
10
+ } from './helpers.js';
3
11
 
4
12
  /**
5
13
  * Extract symbols from R files.
@@ -58,7 +66,7 @@ function handleBinaryOp(node: TreeSitterNode, ctx: ExtractorOutput): void {
58
66
  ctx.definitions.push({
59
67
  name: lhs.text,
60
68
  kind: 'function',
61
- line: node.startPosition.row + 1,
69
+ line: nodeStartLine(node),
62
70
  endLine: nodeEndLine(node),
63
71
  children: params.length > 0 ? params : undefined,
64
72
  });
@@ -68,7 +76,7 @@ function handleBinaryOp(node: TreeSitterNode, ctx: ExtractorOutput): void {
68
76
  ctx.definitions.push({
69
77
  name: lhs.text,
70
78
  kind: 'variable',
71
- line: node.startPosition.row + 1,
79
+ line: nodeStartLine(node),
72
80
  endLine: nodeEndLine(node),
73
81
  });
74
82
  }
@@ -87,14 +95,14 @@ function extractRParams(funcDef: TreeSitterNode): SubDeclaration[] {
87
95
  // parameter node has name and possibly default value
88
96
  const nameNode = child.childForFieldName('name') || findChild(child, 'identifier');
89
97
  if (nameNode) {
90
- params.push({ name: nameNode.text, kind: 'parameter', line: child.startPosition.row + 1 });
98
+ params.push({ name: nameNode.text, kind: 'parameter', line: nodeStartLine(child) });
91
99
  } else if (child.text && child.text !== ',' && child.text !== '(' && child.text !== ')') {
92
100
  // Some grammars have the param as plain text
93
- params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 });
101
+ params.push({ name: child.text, kind: 'parameter', line: nodeStartLine(child) });
94
102
  }
95
103
  }
96
104
  if (child.type === 'identifier') {
97
- params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 });
105
+ params.push({ name: child.text, kind: 'parameter', line: nodeStartLine(child) });
98
106
  }
99
107
  }
100
108
  return params;
@@ -125,29 +133,35 @@ function handleCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
125
133
  return;
126
134
  }
127
135
 
128
- if (funcName === 'setGeneric' || funcName === 'setMethod') {
136
+ if (funcName === 'setGeneric') {
129
137
  handleSetGeneric(node, ctx);
130
138
  return;
131
139
  }
132
140
 
141
+ if (funcName === 'setMethod') {
142
+ handleSetMethod(node, ctx);
143
+ return;
144
+ }
145
+
133
146
  // Regular call
134
147
  if (funcNode.type === 'identifier') {
135
- ctx.calls.push({ name: funcName, line: node.startPosition.row + 1 });
148
+ pushCall(ctx, node, funcName);
136
149
  } else if (funcNode.type === 'namespace_operator') {
137
150
  // pkg::func
138
151
  const parts = funcName.split('::');
139
152
  if (parts.length >= 2) {
140
- ctx.calls.push({
141
- name: parts[parts.length - 1]!,
153
+ pushCall(ctx, node, parts[parts.length - 1]!, {
142
154
  receiver: parts.slice(0, -1).join('::'),
143
- line: node.startPosition.row + 1,
144
155
  });
145
156
  }
146
157
  }
147
158
  }
148
159
 
149
160
  function handleLibraryCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
150
- // Find the package name in arguments
161
+ // Find the package name in arguments. For named arguments like
162
+ // `library(package = dplyr)`, prefer the field-named `value` child of the
163
+ // `argument` node so we extract `dplyr` (the value), not `package` (the
164
+ // parameter name). Keeps native (Rust) and WASM extractors in parity.
151
165
  for (let i = 0; i < node.childCount; i++) {
152
166
  const child = node.child(i);
153
167
  if (!child) continue;
@@ -156,32 +170,38 @@ function handleLibraryCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
156
170
  const arg = child.child(j);
157
171
  if (!arg) continue;
158
172
  if (arg.type === 'identifier') {
159
- ctx.imports.push({
160
- source: arg.text,
161
- names: [arg.text],
162
- line: node.startPosition.row + 1,
163
- });
173
+ pushImport(ctx, node, arg.text, [arg.text]);
164
174
  return;
165
175
  }
166
176
  if (arg.type === 'string' || arg.type === 'string_content') {
167
- const text = arg.text.replace(/^["']|["']$/g, '');
168
- ctx.imports.push({
169
- source: text,
170
- names: [text],
171
- line: node.startPosition.row + 1,
172
- });
177
+ const text = stripQuotes(arg.text);
178
+ pushImport(ctx, node, text, [text]);
173
179
  return;
174
180
  }
175
181
  // Argument might be wrapped
176
182
  if (arg.type === 'argument') {
177
- const id = findChild(arg, 'identifier') || findChild(arg, 'string');
178
- if (id) {
179
- const text = id.text.replace(/^["']|["']$/g, '');
180
- ctx.imports.push({
181
- source: text,
182
- names: [text],
183
- line: node.startPosition.row + 1,
184
- });
183
+ // Prefer the `value` field (correct for named arguments).
184
+ const valueNode = arg.childForFieldName('value');
185
+ let pick: TreeSitterNode | null = null;
186
+ if (valueNode && (valueNode.type === 'string' || valueNode.type === 'identifier')) {
187
+ pick = valueNode;
188
+ } else {
189
+ // Fallback: skip the parameter-name child if the grammar exposes
190
+ // it via the `name` field, then pick the first string/identifier.
191
+ const nameNode = arg.childForFieldName('name');
192
+ for (let k = 0; k < arg.childCount; k++) {
193
+ const inner = arg.child(k);
194
+ if (!inner) continue;
195
+ if (nameNode && inner.id === nameNode.id) continue;
196
+ if (inner.type === 'string' || inner.type === 'identifier') {
197
+ pick = inner;
198
+ break;
199
+ }
200
+ }
201
+ }
202
+ if (pick) {
203
+ const text = stripQuotes(pick.text);
204
+ pushImport(ctx, node, text, [text]);
185
205
  return;
186
206
  }
187
207
  }
@@ -191,63 +211,65 @@ function handleLibraryCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
191
211
  }
192
212
 
193
213
  function handleSourceCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
194
- for (let i = 0; i < node.childCount; i++) {
195
- const child = node.child(i);
196
- if (!child || child.type !== 'arguments') continue;
197
- for (let j = 0; j < child.childCount; j++) {
198
- const arg = child.child(j);
199
- if (!arg) continue;
200
- if (arg.type === 'string') {
201
- const text = arg.text.replace(/^["']|["']$/g, '');
202
- ctx.imports.push({
203
- source: text,
204
- names: ['source'],
205
- line: node.startPosition.row + 1,
206
- });
207
- return;
208
- }
209
- }
210
- }
214
+ // source() only accepts string literals `source(varname)` is not an import.
215
+ const path = firstStringArgument(node);
216
+ if (path === null) return;
217
+ pushImport(ctx, node, path, ['source']);
211
218
  }
212
219
 
213
220
  function handleSetClass(node: TreeSitterNode, ctx: ExtractorOutput): void {
214
- for (let i = 0; i < node.childCount; i++) {
215
- const child = node.child(i);
216
- if (!child || child.type !== 'arguments') continue;
217
- for (let j = 0; j < child.childCount; j++) {
218
- const arg = child.child(j);
219
- if (!arg) continue;
220
- if (arg.type === 'string') {
221
- const name = arg.text.replace(/^["']|["']$/g, '');
222
- ctx.definitions.push({
223
- name,
224
- kind: 'class',
225
- line: node.startPosition.row + 1,
226
- endLine: nodeEndLine(node),
227
- });
228
- return;
229
- }
230
- }
231
- }
221
+ const name = firstStringArgument(node);
222
+ if (name === null) return;
223
+ ctx.definitions.push({
224
+ name,
225
+ kind: 'class',
226
+ line: nodeStartLine(node),
227
+ endLine: nodeEndLine(node),
228
+ });
232
229
  }
233
230
 
234
231
  function handleSetGeneric(node: TreeSitterNode, ctx: ExtractorOutput): void {
235
- for (let i = 0; i < node.childCount; i++) {
236
- const child = node.child(i);
237
- if (!child || child.type !== 'arguments') continue;
238
- for (let j = 0; j < child.childCount; j++) {
239
- const arg = child.child(j);
240
- if (!arg) continue;
241
- if (arg.type === 'string') {
242
- const name = arg.text.replace(/^["']|["']$/g, '');
243
- ctx.definitions.push({
244
- name,
245
- kind: 'function',
246
- line: node.startPosition.row + 1,
247
- endLine: nodeEndLine(node),
248
- });
249
- return;
250
- }
232
+ const name = firstStringArgument(node);
233
+ if (name === null) return;
234
+ ctx.definitions.push({
235
+ name,
236
+ kind: 'function',
237
+ line: nodeStartLine(node),
238
+ endLine: nodeEndLine(node),
239
+ });
240
+ }
241
+
242
+ // setMethod("greet", "Person", function(x) ...) registers an implementation of
243
+ // the generic `greet` — it is not a new top-level definition. Emitting a
244
+ // definition here produced two `function` nodes with the same name (one from
245
+ // setGeneric, one from setMethod) and broke resolution. Emit a call edge to
246
+ // the generic instead; the method body's calls are still picked up by the
247
+ // recursive walk of the anonymous function argument.
248
+ function handleSetMethod(node: TreeSitterNode, ctx: ExtractorOutput): void {
249
+ const name = firstStringArgument(node);
250
+ if (name === null) return;
251
+ pushCall(ctx, node, name);
252
+ }
253
+
254
+ // tree-sitter-r wraps each positional argument in an `argument` node that
255
+ // contains the actual `string` (or `identifier`) child, so the inner string
256
+ // must be unwrapped — checking `child.type === 'string'` directly misses it.
257
+ // Mirrors `first_argument_value` in the Rust extractor for parity.
258
+ function firstStringArgument(node: TreeSitterNode): string | null {
259
+ const args = findFirstChildOfTypes(node, ['arguments']);
260
+ if (!args) return null;
261
+ for (let j = 0; j < args.childCount; j++) {
262
+ const arg = args.child(j);
263
+ if (!arg) continue;
264
+ if (arg.type === 'string') {
265
+ return stripQuotes(arg.text);
266
+ }
267
+ if (arg.type === 'argument') {
268
+ const valueNode = arg.childForFieldName('value');
269
+ if (valueNode && valueNode.type === 'string') return stripQuotes(valueNode.text);
270
+ const innerStr = findFirstChildOfTypes(arg, ['string']);
271
+ if (innerStr) return stripQuotes(innerStr.text);
251
272
  }
252
273
  }
274
+ return null;
253
275
  }