@optave/codegraph 3.11.0 → 3.11.2

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 (230) hide show
  1. package/README.md +38 -31
  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/visitor-utils.d.ts +3 -0
  6. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  7. package/dist/ast-analysis/visitor-utils.js +83 -49
  8. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  11. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  12. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  14. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  15. package/dist/cli/commands/embed.d.ts.map +1 -1
  16. package/dist/cli/commands/embed.js +49 -4
  17. package/dist/cli/commands/embed.js.map +1 -1
  18. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  19. package/dist/domain/analysis/dependencies.js +106 -80
  20. package/dist/domain/analysis/dependencies.js.map +1 -1
  21. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  22. package/dist/domain/analysis/fn-impact.js +77 -52
  23. package/dist/domain/analysis/fn-impact.js.map +1 -1
  24. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  25. package/dist/domain/analysis/module-map.js +132 -121
  26. package/dist/domain/analysis/module-map.js.map +1 -1
  27. package/dist/domain/graph/builder/call-resolver.d.ts +71 -0
  28. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  29. package/dist/domain/graph/builder/call-resolver.js +130 -0
  30. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  31. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  32. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/helpers.js +47 -33
  34. package/dist/domain/graph/builder/helpers.js.map +1 -1
  35. package/dist/domain/graph/builder/incremental.d.ts +6 -0
  36. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/incremental.js +214 -127
  38. package/dist/domain/graph/builder/incremental.js.map +1 -1
  39. package/dist/domain/graph/builder/pipeline.d.ts +1 -44
  40. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/pipeline.js +10 -766
  42. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  43. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/stages/build-edges.js +151 -192
  45. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  48. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  56. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  58. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  60. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  61. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  62. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  63. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  67. package/dist/domain/graph/cycles.d.ts +6 -4
  68. package/dist/domain/graph/cycles.d.ts.map +1 -1
  69. package/dist/domain/graph/cycles.js +50 -55
  70. package/dist/domain/graph/cycles.js.map +1 -1
  71. package/dist/domain/graph/journal.d.ts.map +1 -1
  72. package/dist/domain/graph/journal.js +89 -70
  73. package/dist/domain/graph/journal.js.map +1 -1
  74. package/dist/domain/graph/watcher.d.ts.map +1 -1
  75. package/dist/domain/graph/watcher.js +10 -4
  76. package/dist/domain/graph/watcher.js.map +1 -1
  77. package/dist/domain/parser.d.ts +12 -23
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +126 -79
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/search/generator.d.ts +3 -1
  82. package/dist/domain/search/generator.d.ts.map +1 -1
  83. package/dist/domain/search/generator.js +68 -45
  84. package/dist/domain/search/generator.js.map +1 -1
  85. package/dist/domain/search/models.d.ts +2 -0
  86. package/dist/domain/search/models.d.ts.map +1 -1
  87. package/dist/domain/search/models.js +37 -3
  88. package/dist/domain/search/models.js.map +1 -1
  89. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  90. package/dist/domain/search/search/hybrid.js +49 -40
  91. package/dist/domain/search/search/hybrid.js.map +1 -1
  92. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  93. package/dist/domain/search/search/semantic.js +69 -49
  94. package/dist/domain/search/search/semantic.js.map +1 -1
  95. package/dist/domain/wasm-worker-entry.js +201 -136
  96. package/dist/domain/wasm-worker-entry.js.map +1 -1
  97. package/dist/extractors/elixir.js +95 -71
  98. package/dist/extractors/elixir.js.map +1 -1
  99. package/dist/extractors/gleam.d.ts.map +1 -1
  100. package/dist/extractors/gleam.js +23 -31
  101. package/dist/extractors/gleam.js.map +1 -1
  102. package/dist/extractors/helpers.d.ts +79 -1
  103. package/dist/extractors/helpers.d.ts.map +1 -1
  104. package/dist/extractors/helpers.js +137 -0
  105. package/dist/extractors/helpers.js.map +1 -1
  106. package/dist/extractors/java.d.ts.map +1 -1
  107. package/dist/extractors/java.js +37 -49
  108. package/dist/extractors/java.js.map +1 -1
  109. package/dist/extractors/javascript.d.ts.map +1 -1
  110. package/dist/extractors/javascript.js +44 -44
  111. package/dist/extractors/javascript.js.map +1 -1
  112. package/dist/extractors/julia.js +27 -34
  113. package/dist/extractors/julia.js.map +1 -1
  114. package/dist/extractors/r.d.ts.map +1 -1
  115. package/dist/extractors/r.js +33 -58
  116. package/dist/extractors/r.js.map +1 -1
  117. package/dist/extractors/solidity.d.ts.map +1 -1
  118. package/dist/extractors/solidity.js +38 -61
  119. package/dist/extractors/solidity.js.map +1 -1
  120. package/dist/features/boundaries.d.ts.map +1 -1
  121. package/dist/features/boundaries.js +49 -39
  122. package/dist/features/boundaries.js.map +1 -1
  123. package/dist/features/cfg.d.ts.map +1 -1
  124. package/dist/features/cfg.js +90 -63
  125. package/dist/features/cfg.js.map +1 -1
  126. package/dist/features/check.d.ts.map +1 -1
  127. package/dist/features/check.js +43 -34
  128. package/dist/features/check.js.map +1 -1
  129. package/dist/features/cochange.d.ts.map +1 -1
  130. package/dist/features/cochange.js +68 -56
  131. package/dist/features/cochange.js.map +1 -1
  132. package/dist/features/complexity.d.ts.map +1 -1
  133. package/dist/features/complexity.js +105 -75
  134. package/dist/features/complexity.js.map +1 -1
  135. package/dist/features/dataflow.d.ts.map +1 -1
  136. package/dist/features/dataflow.js +37 -29
  137. package/dist/features/dataflow.js.map +1 -1
  138. package/dist/features/flow.d.ts.map +1 -1
  139. package/dist/features/flow.js +31 -22
  140. package/dist/features/flow.js.map +1 -1
  141. package/dist/features/graph-enrichment.d.ts.map +1 -1
  142. package/dist/features/graph-enrichment.js +77 -70
  143. package/dist/features/graph-enrichment.js.map +1 -1
  144. package/dist/features/owners.d.ts +17 -26
  145. package/dist/features/owners.d.ts.map +1 -1
  146. package/dist/features/owners.js +120 -109
  147. package/dist/features/owners.js.map +1 -1
  148. package/dist/features/sequence.d.ts.map +1 -1
  149. package/dist/features/sequence.js +59 -54
  150. package/dist/features/sequence.js.map +1 -1
  151. package/dist/features/structure-query.d.ts.map +1 -1
  152. package/dist/features/structure-query.js +60 -60
  153. package/dist/features/structure-query.js.map +1 -1
  154. package/dist/features/structure.d.ts.map +1 -1
  155. package/dist/features/structure.js +149 -52
  156. package/dist/features/structure.js.map +1 -1
  157. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  158. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  159. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  160. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  161. package/dist/graph/classifiers/roles.js +63 -59
  162. package/dist/graph/classifiers/roles.js.map +1 -1
  163. package/dist/infrastructure/config.d.ts +1 -1
  164. package/dist/infrastructure/config.d.ts.map +1 -1
  165. package/dist/infrastructure/config.js +1 -1
  166. package/dist/infrastructure/config.js.map +1 -1
  167. package/dist/presentation/cfg.d.ts.map +1 -1
  168. package/dist/presentation/cfg.js +44 -29
  169. package/dist/presentation/cfg.js.map +1 -1
  170. package/dist/presentation/flow.d.ts.map +1 -1
  171. package/dist/presentation/flow.js +58 -38
  172. package/dist/presentation/flow.js.map +1 -1
  173. package/dist/types.d.ts +1 -1
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-erlang.wasm +0 -0
  176. package/package.json +9 -9
  177. package/src/ast-analysis/engine.ts +145 -61
  178. package/src/ast-analysis/visitor-utils.ts +86 -46
  179. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  180. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  181. package/src/cli/commands/embed.ts +54 -4
  182. package/src/domain/analysis/dependencies.ts +166 -85
  183. package/src/domain/analysis/fn-impact.ts +120 -50
  184. package/src/domain/analysis/module-map.ts +175 -140
  185. package/src/domain/graph/builder/call-resolver.ts +181 -0
  186. package/src/domain/graph/builder/helpers.ts +85 -76
  187. package/src/domain/graph/builder/incremental.ts +321 -152
  188. package/src/domain/graph/builder/pipeline.ts +19 -957
  189. package/src/domain/graph/builder/stages/build-edges.ts +229 -275
  190. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  191. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  192. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  193. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  194. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  195. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  196. package/src/domain/graph/cycles.ts +51 -49
  197. package/src/domain/graph/journal.ts +84 -69
  198. package/src/domain/graph/watcher.ts +12 -4
  199. package/src/domain/parser.ts +143 -66
  200. package/src/domain/search/generator.ts +132 -74
  201. package/src/domain/search/models.ts +39 -3
  202. package/src/domain/search/search/hybrid.ts +53 -42
  203. package/src/domain/search/search/semantic.ts +105 -65
  204. package/src/domain/wasm-worker-entry.ts +235 -152
  205. package/src/extractors/elixir.ts +91 -64
  206. package/src/extractors/gleam.ts +33 -37
  207. package/src/extractors/helpers.ts +205 -1
  208. package/src/extractors/java.ts +42 -45
  209. package/src/extractors/javascript.ts +44 -43
  210. package/src/extractors/julia.ts +28 -35
  211. package/src/extractors/r.ts +38 -56
  212. package/src/extractors/solidity.ts +43 -71
  213. package/src/features/boundaries.ts +64 -46
  214. package/src/features/cfg.ts +145 -74
  215. package/src/features/check.ts +60 -43
  216. package/src/features/cochange.ts +95 -72
  217. package/src/features/complexity.ts +134 -79
  218. package/src/features/dataflow.ts +57 -34
  219. package/src/features/flow.ts +48 -24
  220. package/src/features/graph-enrichment.ts +105 -70
  221. package/src/features/owners.ts +186 -146
  222. package/src/features/sequence.ts +99 -69
  223. package/src/features/structure-query.ts +94 -79
  224. package/src/features/structure.ts +199 -79
  225. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  226. package/src/graph/classifiers/roles.ts +64 -54
  227. package/src/infrastructure/config.ts +1 -1
  228. package/src/presentation/cfg.ts +48 -32
  229. package/src/presentation/flow.ts +100 -52
  230. package/src/types.ts +1 -1
@@ -1,11 +1,13 @@
1
- import type {
2
- Call,
3
- ExtractorOutput,
4
- SubDeclaration,
5
- TreeSitterNode,
6
- TreeSitterTree,
7
- } from '../types.js';
8
- import { findChild, nodeEndLine, stripQuotes } from './helpers.js';
1
+ import type { ExtractorOutput, SubDeclaration, TreeSitterNode, TreeSitterTree } from '../types.js';
2
+ import {
3
+ findChild,
4
+ findFirstChildOfTypes,
5
+ nodeEndLine,
6
+ nodeStartLine,
7
+ pushCall,
8
+ pushImport,
9
+ stripQuotes,
10
+ } from './helpers.js';
9
11
 
10
12
  /**
11
13
  * Extract symbols from Gleam files.
@@ -74,7 +76,7 @@ function handleFunction(node: TreeSitterNode, ctx: ExtractorOutput): void {
74
76
  ctx.definitions.push({
75
77
  name: nameNode.text,
76
78
  kind: 'function',
77
- line: node.startPosition.row + 1,
79
+ line: nodeStartLine(node),
78
80
  endLine: nodeEndLine(node),
79
81
  visibility,
80
82
  children: params.length > 0 ? params : undefined,
@@ -90,7 +92,7 @@ function handleExternalFunction(node: TreeSitterNode, ctx: ExtractorOutput): voi
90
92
  ctx.definitions.push({
91
93
  name: nameNode.text,
92
94
  kind: 'function',
93
- line: node.startPosition.row + 1,
95
+ line: nodeStartLine(node),
94
96
  endLine: nodeEndLine(node),
95
97
  visibility: isPublic(node) ? 'public' : 'private',
96
98
  children: params.length > 0 ? params : undefined,
@@ -107,10 +109,7 @@ function handleTypeDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
107
109
  const child = node.child(i);
108
110
  if (!child) continue;
109
111
  if (child.type === 'data_constructor' || child.type === 'type_constructor') {
110
- const ctorName = child.childForFieldName('name') || findChild(child, 'constructor_name');
111
- if (ctorName) {
112
- children.push({ name: ctorName.text, kind: 'property', line: child.startPosition.row + 1 });
113
- }
112
+ pushConstructor(child, children);
114
113
  }
115
114
  // Recurse into constructors block
116
115
  if (child.type === 'data_constructors' || child.type === 'type_constructors') {
@@ -118,14 +117,7 @@ function handleTypeDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
118
117
  const ctor = child.child(j);
119
118
  if (!ctor) continue;
120
119
  if (ctor.type === 'data_constructor' || ctor.type === 'type_constructor') {
121
- const ctorName = ctor.childForFieldName('name') || findChild(ctor, 'constructor_name');
122
- if (ctorName) {
123
- children.push({
124
- name: ctorName.text,
125
- kind: 'property',
126
- line: ctor.startPosition.row + 1,
127
- });
128
- }
120
+ pushConstructor(ctor, children);
129
121
  }
130
122
  }
131
123
  }
@@ -134,13 +126,20 @@ function handleTypeDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
134
126
  ctx.definitions.push({
135
127
  name: nameNode.text,
136
128
  kind: 'type',
137
- line: node.startPosition.row + 1,
129
+ line: nodeStartLine(node),
138
130
  endLine: nodeEndLine(node),
139
131
  visibility: isPublic(node) ? 'public' : 'private',
140
132
  children: children.length > 0 ? children : undefined,
141
133
  });
142
134
  }
143
135
 
136
+ function pushConstructor(ctorNode: TreeSitterNode, out: SubDeclaration[]): void {
137
+ const ctorName = ctorNode.childForFieldName('name') || findChild(ctorNode, 'constructor_name');
138
+ if (ctorName) {
139
+ out.push({ name: ctorName.text, kind: 'property', line: nodeStartLine(ctorNode) });
140
+ }
141
+ }
142
+
144
143
  function handleTypeAlias(node: TreeSitterNode, ctx: ExtractorOutput): void {
145
144
  const nameNode = node.childForFieldName('name') || findChild(node, 'type_name');
146
145
  if (!nameNode) return;
@@ -148,7 +147,7 @@ function handleTypeAlias(node: TreeSitterNode, ctx: ExtractorOutput): void {
148
147
  ctx.definitions.push({
149
148
  name: nameNode.text,
150
149
  kind: 'type',
151
- line: node.startPosition.row + 1,
150
+ line: nodeStartLine(node),
152
151
  endLine: nodeEndLine(node),
153
152
  visibility: isPublic(node) ? 'public' : 'private',
154
153
  });
@@ -161,7 +160,7 @@ function handleConstant(node: TreeSitterNode, ctx: ExtractorOutput): void {
161
160
  ctx.definitions.push({
162
161
  name: nameNode.text,
163
162
  kind: 'variable',
164
- line: node.startPosition.row + 1,
163
+ line: nodeStartLine(node),
165
164
  endLine: nodeEndLine(node),
166
165
  visibility: isPublic(node) ? 'public' : 'private',
167
166
  });
@@ -169,7 +168,7 @@ function handleConstant(node: TreeSitterNode, ctx: ExtractorOutput): void {
169
168
 
170
169
  function handleImport(node: TreeSitterNode, ctx: ExtractorOutput): void {
171
170
  const moduleNode =
172
- node.childForFieldName('module') || findChild(node, 'module') || findChild(node, 'string');
171
+ node.childForFieldName('module') || findFirstChildOfTypes(node, ['module', 'string']);
173
172
  if (!moduleNode) return;
174
173
 
175
174
  const source = stripQuotes(moduleNode.text);
@@ -193,11 +192,9 @@ function handleImport(node: TreeSitterNode, ctx: ExtractorOutput): void {
193
192
  names.push(alias.text);
194
193
  }
195
194
 
196
- ctx.imports.push({
197
- source,
198
- names: names.length > 0 ? names : [source.split('/').pop() || source],
199
- line: node.startPosition.row + 1,
200
- });
195
+ // `pushImport` falls back to the source basename when `names` is empty,
196
+ // preserving the previous `source.split('/').pop() || source` default.
197
+ pushImport(ctx, node, source, names);
201
198
  }
202
199
 
203
200
  function handleCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
@@ -205,16 +202,15 @@ function handleCall(node: TreeSitterNode, ctx: ExtractorOutput): void {
205
202
  if (!funcNode) return;
206
203
 
207
204
  if (funcNode.type === 'identifier' || funcNode.type === 'variable') {
208
- ctx.calls.push({ name: funcNode.text, line: node.startPosition.row + 1 });
205
+ pushCall(ctx, node, funcNode.text);
209
206
  } else if (funcNode.type === 'field_access' || funcNode.type === 'module_select') {
210
207
  const field = funcNode.childForFieldName('field') || funcNode.childForFieldName('label');
211
208
  // Prefer the `record` field; fall back to first named child to skip
212
209
  // anonymous punctuation tokens (the `.` between record and field).
213
210
  const record = funcNode.childForFieldName('record') || funcNode.namedChild(0);
214
211
  if (field) {
215
- const call: Call = { name: field.text, line: node.startPosition.row + 1 };
216
- if (record && record !== field) call.receiver = record.text;
217
- ctx.calls.push(call);
212
+ const receiver = record && record !== field ? record.text : undefined;
213
+ pushCall(ctx, node, field.text, receiver !== undefined ? { receiver } : {});
218
214
  }
219
215
  }
220
216
  }
@@ -231,11 +227,11 @@ function extractParams(funcNode: TreeSitterNode): SubDeclaration[] {
231
227
  if (param.type === 'function_parameter' || param.type === 'parameter') {
232
228
  const nameNode = param.childForFieldName('name') || findChild(param, 'identifier');
233
229
  if (nameNode) {
234
- params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
230
+ params.push({ name: nameNode.text, kind: 'parameter', line: nodeStartLine(param) });
235
231
  }
236
232
  }
237
233
  if (param.type === 'identifier') {
238
- params.push({ name: param.text, kind: 'parameter', line: param.startPosition.row + 1 });
234
+ params.push({ name: param.text, kind: 'parameter', line: nodeStartLine(param) });
239
235
  }
240
236
  }
241
237
  return params;
@@ -1,4 +1,11 @@
1
- import type { SubDeclaration, TreeSitterNode, TypeMapEntry } from '../types.js';
1
+ import type {
2
+ Call,
3
+ ExtractorOutput,
4
+ Import,
5
+ SubDeclaration,
6
+ TreeSitterNode,
7
+ TypeMapEntry,
8
+ } from '../types.js';
2
9
 
3
10
  /**
4
11
  * Maximum recursion depth for tree-sitter AST walkers.
@@ -6,6 +13,11 @@ import type { SubDeclaration, TreeSitterNode, TypeMapEntry } from '../types.js';
6
13
  */
7
14
  export const MAX_WALK_DEPTH = 200;
8
15
 
16
+ /** Convert a tree-sitter node's start row to a 1-based source line. */
17
+ export function nodeStartLine(node: TreeSitterNode): number {
18
+ return node.startPosition.row + 1;
19
+ }
20
+
9
21
  export function nodeEndLine(node: TreeSitterNode): number {
10
22
  return node.endPosition.row + 1;
11
23
  }
@@ -18,6 +30,56 @@ export function findChild(node: TreeSitterNode, type: string): TreeSitterNode |
18
30
  return null;
19
31
  }
20
32
 
33
+ /**
34
+ * Find the first child whose type is in `types`. Useful when several grammar
35
+ * variants name the same conceptual node differently (e.g. `string` vs
36
+ * `string_literal`). Returns the first match in document order, or null.
37
+ */
38
+ export function findFirstChildOfTypes(
39
+ node: TreeSitterNode,
40
+ types: readonly string[],
41
+ ): TreeSitterNode | null {
42
+ for (let i = 0; i < node.childCount; i++) {
43
+ const child = node.child(i);
44
+ if (child && types.includes(child.type)) return child;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Iterate the direct children of `node` in document order, skipping nulls and
51
+ * tokens whose type appears in `skipTypes`. Mirrors the common
52
+ * `for (let i = 0; i < node.childCount; i++) { const c = node.child(i); if (...) continue; ... }`
53
+ * idiom while letting callers filter out grammar punctuation (`,`, `(`, `{`, etc.).
54
+ */
55
+ export function* iterChildren(
56
+ node: TreeSitterNode,
57
+ skipTypes: ReadonlySet<string> = EMPTY_SKIP_SET,
58
+ ): Generator<TreeSitterNode> {
59
+ for (let i = 0; i < node.childCount; i++) {
60
+ const child = node.child(i);
61
+ if (!child) continue;
62
+ if (skipTypes.has(child.type)) continue;
63
+ yield child;
64
+ }
65
+ }
66
+
67
+ const EMPTY_SKIP_SET: ReadonlySet<string> = new Set();
68
+
69
+ /** Common punctuation tokens — handy as a `skipTypes` set for `iterChildren`. */
70
+ export const PUNCTUATION_TOKENS: ReadonlySet<string> = new Set([
71
+ ',',
72
+ ';',
73
+ '(',
74
+ ')',
75
+ '[',
76
+ ']',
77
+ '{',
78
+ '}',
79
+ ':',
80
+ '.',
81
+ ]);
82
+
21
83
  /**
22
84
  * Merge a type-map entry, keeping the higher-confidence one.
23
85
  * Shared across all language extractors that build type maps for call resolution.
@@ -197,3 +259,145 @@ export function extractModifierVisibility(
197
259
  }
198
260
  return undefined;
199
261
  }
262
+
263
+ // ── Output-push helpers ────────────────────────────────────────────────────
264
+ //
265
+ // Most extractors finish with `ctx.calls.push({ name, line: node.startPosition.row + 1 })`
266
+ // or `ctx.imports.push({ source, names, line: node.startPosition.row + 1 })`.
267
+ // Centralising the construction keeps `line` derivation consistent and removes
268
+ // the ~108 hand-rolled `startPosition.row + 1` literals scattered across
269
+ // language extractors.
270
+
271
+ /**
272
+ * Append a `Call` to the extractor output. `line` defaults to the start line of
273
+ * `node`; pass `extra` for `receiver` / `dynamic` flags.
274
+ */
275
+ export function pushCall(
276
+ ctx: ExtractorOutput,
277
+ node: TreeSitterNode,
278
+ name: string,
279
+ extra: { receiver?: string; dynamic?: boolean } = {},
280
+ ): void {
281
+ if (!name) return;
282
+ const call: Call = { name, line: nodeStartLine(node) };
283
+ if (extra.receiver !== undefined) call.receiver = extra.receiver;
284
+ if (extra.dynamic !== undefined) call.dynamic = extra.dynamic;
285
+ ctx.calls.push(call);
286
+ }
287
+
288
+ /**
289
+ * Append an `Import` to the extractor output. `line` defaults to the start
290
+ * line of `node`. If `names` is empty, the source basename (split on `/`) is
291
+ * used as a single-name fallback — matching the convention in gleam, julia,
292
+ * and similar module-path imports.
293
+ */
294
+ export function pushImport(
295
+ ctx: ExtractorOutput,
296
+ node: TreeSitterNode,
297
+ source: string,
298
+ names: string[],
299
+ flags: Partial<Omit<Import, 'source' | 'names' | 'line'>> = {},
300
+ ): void {
301
+ if (!source) return;
302
+ const resolved = names.length > 0 ? names : [lastPathSegment(source, '/') || source];
303
+ const entry: Import = { source, names: resolved, line: nodeStartLine(node) };
304
+ Object.assign(entry, flags);
305
+ ctx.imports.push(entry);
306
+ }
307
+
308
+ // ── Parameter extraction ───────────────────────────────────────────────────
309
+
310
+ /**
311
+ * Options for {@link extractSimpleParameters}.
312
+ */
313
+ export interface ExtractParametersOptions {
314
+ /** Tree-sitter types that mark a single parameter node (e.g. `formal_parameter`). */
315
+ paramTypes: readonly string[];
316
+ /**
317
+ * Field name on each parameter that holds the bound identifier. Defaults to
318
+ * `'name'`. Pass `null` to use the parameter node itself when its type is in
319
+ * `paramTypes` and it has no `name` field (e.g. R's bare `identifier`).
320
+ */
321
+ nameField?: string | null;
322
+ /**
323
+ * If true, when `nameField` lookup fails fall back to the first `identifier`
324
+ * child of the parameter. Useful for Gleam / Solidity-style grammars.
325
+ */
326
+ fallbackToIdentifier?: boolean;
327
+ /**
328
+ * Optional type-map sink. When provided, the parameter's `type` field text
329
+ * (if present) is recorded with the given confidence.
330
+ */
331
+ typeMap?: Map<string, TypeMapEntry>;
332
+ /** Confidence used when writing into `typeMap`. Defaults to `0.9`. */
333
+ typeMapConfidence?: number;
334
+ /**
335
+ * Optional callback to derive the type text from the parameter's `type`
336
+ * field node. Defaults to `node.text`. Use this for languages where the
337
+ * `type` field is wrapped (e.g. Java `generic_type` → first child).
338
+ */
339
+ resolveType?: (typeNode: TreeSitterNode) => string | undefined;
340
+ }
341
+
342
+ /**
343
+ * Extract parameters from a parameter-list node using a uniform pattern.
344
+ *
345
+ * This collapses the boilerplate in `extract*Params` helpers across
346
+ * Java/Julia/Gleam/Solidity/R/etc. — each one walks the parameter list,
347
+ * matches a parameter node type, reads the `name` field, and pushes a
348
+ * `SubDeclaration` with `kind: 'parameter'`.
349
+ */
350
+ export function extractSimpleParameters(
351
+ paramListNode: TreeSitterNode | null,
352
+ options: ExtractParametersOptions,
353
+ ): SubDeclaration[] {
354
+ const params: SubDeclaration[] = [];
355
+ if (!paramListNode) return params;
356
+ const { paramTypes, nameField = 'name', fallbackToIdentifier = false } = options;
357
+
358
+ for (let i = 0; i < paramListNode.childCount; i++) {
359
+ const param = paramListNode.child(i);
360
+ if (!param || !paramTypes.includes(param.type)) continue;
361
+ const nameNode = resolveParamName(param, nameField, fallbackToIdentifier);
362
+ if (!nameNode) continue;
363
+ params.push({ name: nameNode.text, kind: 'parameter', line: nodeStartLine(param) });
364
+ recordParamType(param, nameNode.text, options);
365
+ }
366
+ return params;
367
+ }
368
+
369
+ /** Record a parameter's declared type into the type-map sink, if configured. */
370
+ function recordParamType(
371
+ param: TreeSitterNode,
372
+ paramName: string,
373
+ options: ExtractParametersOptions,
374
+ ): void {
375
+ const { typeMap, resolveType, typeMapConfidence = 0.9 } = options;
376
+ if (!typeMap) return;
377
+ const typeNode = param.childForFieldName('type');
378
+ if (!typeNode) return;
379
+ const typeText = resolveType ? resolveType(typeNode) : typeNode.text;
380
+ if (!typeText) return;
381
+ setTypeMapEntry(typeMap, paramName, typeText, typeMapConfidence);
382
+ }
383
+
384
+ /**
385
+ * Resolve the identifier node that names a parameter. Used by
386
+ * {@link extractSimpleParameters}; exposed so language-specific extractors
387
+ * can reuse the same lookup logic in custom loops.
388
+ */
389
+ export function resolveParamName(
390
+ paramNode: TreeSitterNode,
391
+ nameField: string | null,
392
+ fallbackToIdentifier: boolean,
393
+ ): TreeSitterNode | null {
394
+ if (nameField === null) {
395
+ return paramNode;
396
+ }
397
+ const named = paramNode.childForFieldName(nameField);
398
+ if (named) return named;
399
+ if (fallbackToIdentifier) {
400
+ return findChild(paramNode, 'identifier');
401
+ }
402
+ return null;
403
+ }
@@ -1,5 +1,4 @@
1
1
  import type {
2
- Call,
3
2
  ExtractorOutput,
4
3
  SubDeclaration,
5
4
  TreeSitterNode,
@@ -9,10 +8,14 @@ import type {
9
8
  import {
10
9
  extractBodyMembers,
11
10
  extractModifierVisibility,
11
+ extractSimpleParameters,
12
12
  findChild,
13
13
  findParentNode,
14
14
  lastPathSegment,
15
15
  nodeEndLine,
16
+ nodeStartLine,
17
+ pushCall,
18
+ pushImport,
16
19
  } from './helpers.js';
17
20
 
18
21
  /**
@@ -78,7 +81,7 @@ function handleJavaClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
78
81
  ctx.definitions.push({
79
82
  name: nameNode.text,
80
83
  kind: 'class',
81
- line: node.startPosition.row + 1,
84
+ line: nodeStartLine(node),
82
85
  endLine: nodeEndLine(node),
83
86
  children: classChildren.length > 0 ? classChildren : undefined,
84
87
  });
@@ -87,7 +90,7 @@ function handleJavaClassDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
87
90
 
88
91
  const interfaces = node.childForFieldName('interfaces');
89
92
  if (interfaces) {
90
- extractJavaInterfaces(interfaces, nameNode.text, node.startPosition.row + 1, ctx);
93
+ extractJavaInterfaces(interfaces, nameNode.text, nodeStartLine(node), ctx);
91
94
  }
92
95
  }
93
96
 
@@ -101,7 +104,7 @@ function extractJavaSuperclass(
101
104
  if (!superclass) return;
102
105
  const superName = findJavaSuperTypeName(superclass);
103
106
  if (superName) {
104
- ctx.classes.push({ name: className, extends: superName, line: node.startPosition.row + 1 });
107
+ ctx.classes.push({ name: className, extends: superName, line: nodeStartLine(node) });
105
108
  }
106
109
  }
107
110
 
@@ -163,7 +166,7 @@ function handleJavaInterfaceDecl(node: TreeSitterNode, ctx: ExtractorOutput): vo
163
166
  ctx.definitions.push({
164
167
  name: nameNode.text,
165
168
  kind: 'interface',
166
- line: node.startPosition.row + 1,
169
+ line: nodeStartLine(node),
167
170
  endLine: nodeEndLine(node),
168
171
  });
169
172
  const body = node.childForFieldName('body');
@@ -184,8 +187,8 @@ function extractJavaInterfaceMethods(
184
187
  ctx.definitions.push({
185
188
  name: `${ifaceName}.${methName.text}`,
186
189
  kind: 'method',
187
- line: child.startPosition.row + 1,
188
- endLine: child.endPosition.row + 1,
190
+ line: nodeStartLine(child),
191
+ endLine: nodeEndLine(child),
189
192
  });
190
193
  }
191
194
  }
@@ -199,7 +202,7 @@ function handleJavaEnumDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
199
202
  ctx.definitions.push({
200
203
  name: nameNode.text,
201
204
  kind: 'enum',
202
- line: node.startPosition.row + 1,
205
+ line: nodeStartLine(node),
203
206
  endLine: nodeEndLine(node),
204
207
  children: enumChildren.length > 0 ? enumChildren : undefined,
205
208
  });
@@ -216,7 +219,7 @@ function handleJavaMethodDecl(node: TreeSitterNode, ctx: ExtractorOutput): void
216
219
  ctx.definitions.push({
217
220
  name: fullName,
218
221
  kind: 'method',
219
- line: node.startPosition.row + 1,
222
+ line: nodeStartLine(node),
220
223
  endLine: nodeEndLine(node),
221
224
  children: params.length > 0 ? params : undefined,
222
225
  visibility: extractModifierVisibility(node),
@@ -232,7 +235,7 @@ function handleJavaConstructorDecl(node: TreeSitterNode, ctx: ExtractorOutput):
232
235
  ctx.definitions.push({
233
236
  name: fullName,
234
237
  kind: 'method',
235
- line: node.startPosition.row + 1,
238
+ line: nodeStartLine(node),
236
239
  endLine: nodeEndLine(node),
237
240
  children: params.length > 0 ? params : undefined,
238
241
  visibility: extractModifierVisibility(node),
@@ -245,12 +248,7 @@ function handleJavaImportDecl(node: TreeSitterNode, ctx: ExtractorOutput): void
245
248
  if (child && (child.type === 'scoped_identifier' || child.type === 'identifier')) {
246
249
  const fullPath = child.text;
247
250
  const lastName = lastPathSegment(fullPath, '.');
248
- ctx.imports.push({
249
- source: fullPath,
250
- names: [lastName],
251
- line: node.startPosition.row + 1,
252
- javaImport: true,
253
- });
251
+ pushImport(ctx, node, fullPath, [lastName], { javaImport: true });
254
252
  }
255
253
  if (child && child.type === 'asterisk') {
256
254
  const lastImport = ctx.imports[ctx.imports.length - 1];
@@ -263,21 +261,25 @@ function handleJavaMethodInvocation(node: TreeSitterNode, ctx: ExtractorOutput):
263
261
  const nameNode = node.childForFieldName('name');
264
262
  if (!nameNode) return;
265
263
  const obj = node.childForFieldName('object');
266
- const call: Call = { name: nameNode.text, line: node.startPosition.row + 1 };
267
- if (obj) call.receiver = obj.text;
268
- ctx.calls.push(call);
264
+ pushCall(ctx, node, nameNode.text, obj ? { receiver: obj.text } : {});
269
265
  }
270
266
 
271
267
  function handleJavaLocalVarDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
272
268
  const typeNode = node.childForFieldName('type');
273
269
  if (!typeNode) return;
274
- const typeName = typeNode.type === 'generic_type' ? typeNode.child(0)?.text : typeNode.text;
270
+ const typeName = resolveJavaTypeText(typeNode);
275
271
  if (!typeName) return;
276
272
  for (let i = 0; i < node.childCount; i++) {
277
273
  const child = node.child(i);
278
274
  if (child?.type === 'variable_declarator') {
279
275
  const nameNode = child.childForFieldName('name');
280
- if (nameNode) ctx.typeMap?.set(nameNode.text, { type: typeName, confidence: 0.9 });
276
+ // Use direct Map.set (last-wins) for local variable declarations.
277
+ // Local variable types are method-scoped and should override any
278
+ // prior entry (e.g. a same-named constructor parameter). Using
279
+ // setTypeMapEntry (first-wins on tie) would let a constructor
280
+ // parameter type block a local variable's more-specific concrete type.
281
+ if (nameNode && ctx.typeMap)
282
+ ctx.typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 });
281
283
  }
282
284
  }
283
285
  }
@@ -285,8 +287,17 @@ function handleJavaLocalVarDecl(node: TreeSitterNode, ctx: ExtractorOutput): voi
285
287
  function handleJavaObjectCreation(node: TreeSitterNode, ctx: ExtractorOutput): void {
286
288
  const typeNode = node.childForFieldName('type');
287
289
  if (!typeNode) return;
288
- const typeName = typeNode.type === 'generic_type' ? typeNode.child(0)?.text : typeNode.text;
289
- if (typeName) ctx.calls.push({ name: typeName, line: node.startPosition.row + 1 });
290
+ const typeName = resolveJavaTypeText(typeNode);
291
+ if (typeName) pushCall(ctx, node, typeName);
292
+ }
293
+
294
+ /**
295
+ * Resolve a Java type node's text, unwrapping `generic_type` to its base name.
296
+ * Used wherever we need the bare type identifier (local var decls, object
297
+ * creation, parameter types).
298
+ */
299
+ function resolveJavaTypeText(typeNode: TreeSitterNode): string | undefined {
300
+ return typeNode.type === 'generic_type' ? typeNode.child(0)?.text : typeNode.text;
290
301
  }
291
302
 
292
303
  const JAVA_PARENT_TYPES = [
@@ -300,31 +311,17 @@ function findJavaParentClass(node: TreeSitterNode): string | null {
300
311
 
301
312
  // ── Child extraction helpers ────────────────────────────────────────────────
302
313
 
314
+ const JAVA_PARAM_TYPES = ['formal_parameter', 'spread_parameter'] as const;
315
+
303
316
  function extractJavaParameters(
304
317
  paramListNode: TreeSitterNode | null,
305
318
  typeMap?: Map<string, TypeMapEntry>,
306
319
  ): SubDeclaration[] {
307
- const params: SubDeclaration[] = [];
308
- if (!paramListNode) return params;
309
- for (let i = 0; i < paramListNode.childCount; i++) {
310
- const param = paramListNode.child(i);
311
- if (!param) continue;
312
- if (param.type === 'formal_parameter' || param.type === 'spread_parameter') {
313
- const nameNode = param.childForFieldName('name');
314
- if (nameNode) {
315
- params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
316
- if (typeMap) {
317
- const typeNode = param.childForFieldName('type');
318
- if (typeNode) {
319
- const typeName =
320
- typeNode.type === 'generic_type' ? typeNode.child(0)?.text : typeNode.text;
321
- if (typeName) typeMap.set(nameNode.text, { type: typeName, confidence: 0.9 });
322
- }
323
- }
324
- }
325
- }
326
- }
327
- return params;
320
+ return extractSimpleParameters(paramListNode, {
321
+ paramTypes: JAVA_PARAM_TYPES,
322
+ typeMap,
323
+ resolveType: resolveJavaTypeText,
324
+ });
328
325
  }
329
326
 
330
327
  function extractClassFields(classNode: TreeSitterNode): SubDeclaration[] {
@@ -350,7 +347,7 @@ function extractFieldDeclarators(member: TreeSitterNode, fields: SubDeclaration[
350
347
  fields.push({
351
348
  name: nameNode.text,
352
349
  kind: 'property',
353
- line: member.startPosition.row + 1,
350
+ line: nodeStartLine(member),
354
351
  visibility: vis,
355
352
  });
356
353
  }