@colbymchenry/codegraph-darwin-x64 0.9.9 → 1.0.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 (296) hide show
  1. package/lib/dist/bin/codegraph.d.ts +1 -0
  2. package/lib/dist/bin/codegraph.d.ts.map +1 -1
  3. package/lib/dist/bin/codegraph.js +246 -3
  4. package/lib/dist/bin/codegraph.js.map +1 -1
  5. package/lib/dist/context/index.d.ts.map +1 -1
  6. package/lib/dist/context/index.js +7 -0
  7. package/lib/dist/context/index.js.map +1 -1
  8. package/lib/dist/db/index.d.ts.map +1 -1
  9. package/lib/dist/db/index.js +2 -1
  10. package/lib/dist/db/index.js.map +1 -1
  11. package/lib/dist/db/migrations.d.ts +1 -1
  12. package/lib/dist/db/migrations.d.ts.map +1 -1
  13. package/lib/dist/db/migrations.js +10 -1
  14. package/lib/dist/db/migrations.js.map +1 -1
  15. package/lib/dist/db/queries.d.ts +43 -0
  16. package/lib/dist/db/queries.d.ts.map +1 -1
  17. package/lib/dist/db/queries.js +103 -7
  18. package/lib/dist/db/queries.js.map +1 -1
  19. package/lib/dist/db/schema.sql +1 -0
  20. package/lib/dist/db/sqlite-adapter.d.ts +7 -0
  21. package/lib/dist/db/sqlite-adapter.d.ts.map +1 -1
  22. package/lib/dist/db/sqlite-adapter.js +3 -0
  23. package/lib/dist/db/sqlite-adapter.js.map +1 -1
  24. package/lib/dist/directory.d.ts +34 -2
  25. package/lib/dist/directory.d.ts.map +1 -1
  26. package/lib/dist/directory.js +129 -35
  27. package/lib/dist/directory.js.map +1 -1
  28. package/lib/dist/extraction/astro-extractor.d.ts +79 -0
  29. package/lib/dist/extraction/astro-extractor.d.ts.map +1 -0
  30. package/lib/dist/extraction/astro-extractor.js +320 -0
  31. package/lib/dist/extraction/astro-extractor.js.map +1 -0
  32. package/lib/dist/extraction/extraction-version.d.ts +25 -0
  33. package/lib/dist/extraction/extraction-version.d.ts.map +1 -0
  34. package/lib/dist/extraction/extraction-version.js +28 -0
  35. package/lib/dist/extraction/extraction-version.js.map +1 -0
  36. package/lib/dist/extraction/function-ref.d.ts +118 -0
  37. package/lib/dist/extraction/function-ref.d.ts.map +1 -0
  38. package/lib/dist/extraction/function-ref.js +727 -0
  39. package/lib/dist/extraction/function-ref.js.map +1 -0
  40. package/lib/dist/extraction/generated-detection.d.ts.map +1 -1
  41. package/lib/dist/extraction/generated-detection.js +3 -0
  42. package/lib/dist/extraction/generated-detection.js.map +1 -1
  43. package/lib/dist/extraction/grammars.d.ts +7 -1
  44. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  45. package/lib/dist/extraction/grammars.js +52 -4
  46. package/lib/dist/extraction/grammars.js.map +1 -1
  47. package/lib/dist/extraction/index.d.ts +34 -0
  48. package/lib/dist/extraction/index.d.ts.map +1 -1
  49. package/lib/dist/extraction/index.js +346 -62
  50. package/lib/dist/extraction/index.js.map +1 -1
  51. package/lib/dist/extraction/languages/c-cpp.d.ts +8 -0
  52. package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  53. package/lib/dist/extraction/languages/c-cpp.js +87 -28
  54. package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
  55. package/lib/dist/extraction/languages/csharp.d.ts +22 -0
  56. package/lib/dist/extraction/languages/csharp.d.ts.map +1 -1
  57. package/lib/dist/extraction/languages/csharp.js +84 -2
  58. package/lib/dist/extraction/languages/csharp.js.map +1 -1
  59. package/lib/dist/extraction/languages/dart.d.ts.map +1 -1
  60. package/lib/dist/extraction/languages/dart.js +161 -1
  61. package/lib/dist/extraction/languages/dart.js.map +1 -1
  62. package/lib/dist/extraction/languages/go.d.ts.map +1 -1
  63. package/lib/dist/extraction/languages/go.js +43 -2
  64. package/lib/dist/extraction/languages/go.js.map +1 -1
  65. package/lib/dist/extraction/languages/index.d.ts.map +1 -1
  66. package/lib/dist/extraction/languages/index.js +2 -0
  67. package/lib/dist/extraction/languages/index.js.map +1 -1
  68. package/lib/dist/extraction/languages/java.d.ts.map +1 -1
  69. package/lib/dist/extraction/languages/java.js +42 -1
  70. package/lib/dist/extraction/languages/java.js.map +1 -1
  71. package/lib/dist/extraction/languages/javascript.d.ts.map +1 -1
  72. package/lib/dist/extraction/languages/javascript.js +16 -0
  73. package/lib/dist/extraction/languages/javascript.js.map +1 -1
  74. package/lib/dist/extraction/languages/kotlin.d.ts.map +1 -1
  75. package/lib/dist/extraction/languages/kotlin.js +69 -0
  76. package/lib/dist/extraction/languages/kotlin.js.map +1 -1
  77. package/lib/dist/extraction/languages/objc.d.ts.map +1 -1
  78. package/lib/dist/extraction/languages/objc.js +42 -0
  79. package/lib/dist/extraction/languages/objc.js.map +1 -1
  80. package/lib/dist/extraction/languages/pascal.d.ts.map +1 -1
  81. package/lib/dist/extraction/languages/pascal.js +11 -0
  82. package/lib/dist/extraction/languages/pascal.js.map +1 -1
  83. package/lib/dist/extraction/languages/php.d.ts.map +1 -1
  84. package/lib/dist/extraction/languages/php.js +90 -1
  85. package/lib/dist/extraction/languages/php.js.map +1 -1
  86. package/lib/dist/extraction/languages/r.d.ts +3 -0
  87. package/lib/dist/extraction/languages/r.d.ts.map +1 -0
  88. package/lib/dist/extraction/languages/r.js +314 -0
  89. package/lib/dist/extraction/languages/r.js.map +1 -0
  90. package/lib/dist/extraction/languages/ruby.d.ts.map +1 -1
  91. package/lib/dist/extraction/languages/ruby.js +35 -0
  92. package/lib/dist/extraction/languages/ruby.js.map +1 -1
  93. package/lib/dist/extraction/languages/rust.d.ts.map +1 -1
  94. package/lib/dist/extraction/languages/rust.js +35 -2
  95. package/lib/dist/extraction/languages/rust.js.map +1 -1
  96. package/lib/dist/extraction/languages/scala.d.ts.map +1 -1
  97. package/lib/dist/extraction/languages/scala.js +61 -1
  98. package/lib/dist/extraction/languages/scala.js.map +1 -1
  99. package/lib/dist/extraction/languages/swift.d.ts.map +1 -1
  100. package/lib/dist/extraction/languages/swift.js +61 -0
  101. package/lib/dist/extraction/languages/swift.js.map +1 -1
  102. package/lib/dist/extraction/languages/typescript.d.ts +13 -0
  103. package/lib/dist/extraction/languages/typescript.d.ts.map +1 -1
  104. package/lib/dist/extraction/languages/typescript.js +38 -0
  105. package/lib/dist/extraction/languages/typescript.js.map +1 -1
  106. package/lib/dist/extraction/liquid-extractor.d.ts +7 -0
  107. package/lib/dist/extraction/liquid-extractor.d.ts.map +1 -1
  108. package/lib/dist/extraction/liquid-extractor.js +53 -9
  109. package/lib/dist/extraction/liquid-extractor.js.map +1 -1
  110. package/lib/dist/extraction/razor-extractor.d.ts +42 -0
  111. package/lib/dist/extraction/razor-extractor.d.ts.map +1 -0
  112. package/lib/dist/extraction/razor-extractor.js +285 -0
  113. package/lib/dist/extraction/razor-extractor.js.map +1 -0
  114. package/lib/dist/extraction/svelte-extractor.d.ts.map +1 -1
  115. package/lib/dist/extraction/svelte-extractor.js +6 -3
  116. package/lib/dist/extraction/svelte-extractor.js.map +1 -1
  117. package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  118. package/lib/dist/extraction/tree-sitter-helpers.js +59 -10
  119. package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
  120. package/lib/dist/extraction/tree-sitter-types.d.ts +33 -0
  121. package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  122. package/lib/dist/extraction/tree-sitter.d.ts +211 -0
  123. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  124. package/lib/dist/extraction/tree-sitter.js +1681 -49
  125. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  126. package/lib/dist/extraction/vue-extractor.d.ts +15 -0
  127. package/lib/dist/extraction/vue-extractor.d.ts.map +1 -1
  128. package/lib/dist/extraction/vue-extractor.js +94 -3
  129. package/lib/dist/extraction/vue-extractor.js.map +1 -1
  130. package/lib/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  131. package/lib/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
  132. package/lib/dist/graph/queries.d.ts.map +1 -1
  133. package/lib/dist/graph/queries.js +13 -40
  134. package/lib/dist/graph/queries.js.map +1 -1
  135. package/lib/dist/graph/traversal.d.ts.map +1 -1
  136. package/lib/dist/graph/traversal.js +16 -4
  137. package/lib/dist/graph/traversal.js.map +1 -1
  138. package/lib/dist/index.d.ts +34 -2
  139. package/lib/dist/index.d.ts.map +1 -1
  140. package/lib/dist/index.js +90 -8
  141. package/lib/dist/index.js.map +1 -1
  142. package/lib/dist/installer/index.d.ts.map +1 -1
  143. package/lib/dist/installer/index.js +52 -2
  144. package/lib/dist/installer/index.js.map +1 -1
  145. package/lib/dist/installer/instructions-template.d.ts +34 -11
  146. package/lib/dist/installer/instructions-template.d.ts.map +1 -1
  147. package/lib/dist/installer/instructions-template.js +44 -12
  148. package/lib/dist/installer/instructions-template.js.map +1 -1
  149. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  150. package/lib/dist/installer/targets/claude.js +6 -10
  151. package/lib/dist/installer/targets/claude.js.map +1 -1
  152. package/lib/dist/installer/targets/codex.js +4 -6
  153. package/lib/dist/installer/targets/codex.js.map +1 -1
  154. package/lib/dist/installer/targets/gemini.js +4 -6
  155. package/lib/dist/installer/targets/gemini.js.map +1 -1
  156. package/lib/dist/installer/targets/opencode.d.ts +9 -1
  157. package/lib/dist/installer/targets/opencode.d.ts.map +1 -1
  158. package/lib/dist/installer/targets/opencode.js +91 -40
  159. package/lib/dist/installer/targets/opencode.js.map +1 -1
  160. package/lib/dist/installer/targets/shared.d.ts +14 -0
  161. package/lib/dist/installer/targets/shared.d.ts.map +1 -1
  162. package/lib/dist/installer/targets/shared.js +16 -0
  163. package/lib/dist/installer/targets/shared.js.map +1 -1
  164. package/lib/dist/mcp/daemon.d.ts +60 -1
  165. package/lib/dist/mcp/daemon.d.ts.map +1 -1
  166. package/lib/dist/mcp/daemon.js +221 -8
  167. package/lib/dist/mcp/daemon.js.map +1 -1
  168. package/lib/dist/mcp/dynamic-boundaries.d.ts +41 -0
  169. package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -0
  170. package/lib/dist/mcp/dynamic-boundaries.js +359 -0
  171. package/lib/dist/mcp/dynamic-boundaries.js.map +1 -0
  172. package/lib/dist/mcp/index.d.ts.map +1 -1
  173. package/lib/dist/mcp/index.js +18 -9
  174. package/lib/dist/mcp/index.js.map +1 -1
  175. package/lib/dist/mcp/ppid-watchdog.d.ts +44 -0
  176. package/lib/dist/mcp/ppid-watchdog.d.ts.map +1 -0
  177. package/lib/dist/mcp/ppid-watchdog.js +27 -0
  178. package/lib/dist/mcp/ppid-watchdog.js.map +1 -0
  179. package/lib/dist/mcp/proxy.d.ts +6 -0
  180. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  181. package/lib/dist/mcp/proxy.js +153 -24
  182. package/lib/dist/mcp/proxy.js.map +1 -1
  183. package/lib/dist/mcp/server-instructions.d.ts +12 -1
  184. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  185. package/lib/dist/mcp/server-instructions.js +43 -16
  186. package/lib/dist/mcp/server-instructions.js.map +1 -1
  187. package/lib/dist/mcp/session.d.ts +2 -0
  188. package/lib/dist/mcp/session.d.ts.map +1 -1
  189. package/lib/dist/mcp/session.js +49 -2
  190. package/lib/dist/mcp/session.js.map +1 -1
  191. package/lib/dist/mcp/stdin-teardown.d.ts +27 -0
  192. package/lib/dist/mcp/stdin-teardown.d.ts.map +1 -0
  193. package/lib/dist/mcp/stdin-teardown.js +49 -0
  194. package/lib/dist/mcp/stdin-teardown.js.map +1 -0
  195. package/lib/dist/mcp/tools.d.ts +71 -0
  196. package/lib/dist/mcp/tools.d.ts.map +1 -1
  197. package/lib/dist/mcp/tools.js +703 -85
  198. package/lib/dist/mcp/tools.js.map +1 -1
  199. package/lib/dist/mcp/transport.d.ts.map +1 -1
  200. package/lib/dist/mcp/transport.js +18 -2
  201. package/lib/dist/mcp/transport.js.map +1 -1
  202. package/lib/dist/resolution/callback-synthesizer.d.ts +3 -3
  203. package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  204. package/lib/dist/resolution/callback-synthesizer.js +549 -21
  205. package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
  206. package/lib/dist/resolution/frameworks/astro.d.ts +9 -0
  207. package/lib/dist/resolution/frameworks/astro.d.ts.map +1 -0
  208. package/lib/dist/resolution/frameworks/astro.js +169 -0
  209. package/lib/dist/resolution/frameworks/astro.js.map +1 -0
  210. package/lib/dist/resolution/frameworks/expo-modules.d.ts.map +1 -1
  211. package/lib/dist/resolution/frameworks/expo-modules.js +6 -1
  212. package/lib/dist/resolution/frameworks/expo-modules.js.map +1 -1
  213. package/lib/dist/resolution/frameworks/index.d.ts +1 -0
  214. package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
  215. package/lib/dist/resolution/frameworks/index.js +5 -1
  216. package/lib/dist/resolution/frameworks/index.js.map +1 -1
  217. package/lib/dist/resolution/frameworks/java.js +6 -1
  218. package/lib/dist/resolution/frameworks/java.js.map +1 -1
  219. package/lib/dist/resolution/frameworks/python.d.ts.map +1 -1
  220. package/lib/dist/resolution/frameworks/python.js +7 -3
  221. package/lib/dist/resolution/frameworks/python.js.map +1 -1
  222. package/lib/dist/resolution/frameworks/react-native.d.ts.map +1 -1
  223. package/lib/dist/resolution/frameworks/react-native.js +53 -3
  224. package/lib/dist/resolution/frameworks/react-native.js.map +1 -1
  225. package/lib/dist/resolution/frameworks/react.d.ts.map +1 -1
  226. package/lib/dist/resolution/frameworks/react.js +15 -3
  227. package/lib/dist/resolution/frameworks/react.js.map +1 -1
  228. package/lib/dist/resolution/frameworks/svelte.js +5 -1
  229. package/lib/dist/resolution/frameworks/svelte.js.map +1 -1
  230. package/lib/dist/resolution/frameworks/vue.js +24 -27
  231. package/lib/dist/resolution/frameworks/vue.js.map +1 -1
  232. package/lib/dist/resolution/import-resolver.d.ts +10 -0
  233. package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
  234. package/lib/dist/resolution/import-resolver.js +564 -2
  235. package/lib/dist/resolution/import-resolver.js.map +1 -1
  236. package/lib/dist/resolution/index.d.ts +80 -0
  237. package/lib/dist/resolution/index.d.ts.map +1 -1
  238. package/lib/dist/resolution/index.js +457 -7
  239. package/lib/dist/resolution/index.js.map +1 -1
  240. package/lib/dist/resolution/name-matcher.d.ts +61 -0
  241. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  242. package/lib/dist/resolution/name-matcher.js +590 -14
  243. package/lib/dist/resolution/name-matcher.js.map +1 -1
  244. package/lib/dist/resolution/types.d.ts +27 -3
  245. package/lib/dist/resolution/types.d.ts.map +1 -1
  246. package/lib/dist/resolution/workspace-packages.d.ts +48 -0
  247. package/lib/dist/resolution/workspace-packages.d.ts.map +1 -0
  248. package/lib/dist/resolution/workspace-packages.js +208 -0
  249. package/lib/dist/resolution/workspace-packages.js.map +1 -0
  250. package/lib/dist/search/query-utils.d.ts +17 -1
  251. package/lib/dist/search/query-utils.d.ts.map +1 -1
  252. package/lib/dist/search/query-utils.js +79 -10
  253. package/lib/dist/search/query-utils.js.map +1 -1
  254. package/lib/dist/sync/watcher.d.ts +124 -32
  255. package/lib/dist/sync/watcher.d.ts.map +1 -1
  256. package/lib/dist/sync/watcher.js +326 -111
  257. package/lib/dist/sync/watcher.js.map +1 -1
  258. package/lib/dist/telemetry/index.d.ts +146 -0
  259. package/lib/dist/telemetry/index.d.ts.map +1 -0
  260. package/lib/dist/telemetry/index.js +544 -0
  261. package/lib/dist/telemetry/index.js.map +1 -0
  262. package/lib/dist/types.d.ts +17 -2
  263. package/lib/dist/types.d.ts.map +1 -1
  264. package/lib/dist/types.js +3 -0
  265. package/lib/dist/types.js.map +1 -1
  266. package/lib/dist/upgrade/index.d.ts +132 -0
  267. package/lib/dist/upgrade/index.d.ts.map +1 -0
  268. package/lib/dist/upgrade/index.js +462 -0
  269. package/lib/dist/upgrade/index.js.map +1 -0
  270. package/lib/dist/utils.d.ts +30 -24
  271. package/lib/dist/utils.d.ts.map +1 -1
  272. package/lib/dist/utils.js +64 -48
  273. package/lib/dist/utils.js.map +1 -1
  274. package/lib/node_modules/.package-lock.json +1 -29
  275. package/lib/package.json +1 -2
  276. package/package.json +1 -1
  277. package/lib/node_modules/chokidar/LICENSE +0 -21
  278. package/lib/node_modules/chokidar/README.md +0 -305
  279. package/lib/node_modules/chokidar/esm/handler.d.ts +0 -90
  280. package/lib/node_modules/chokidar/esm/handler.js +0 -629
  281. package/lib/node_modules/chokidar/esm/index.d.ts +0 -215
  282. package/lib/node_modules/chokidar/esm/index.js +0 -798
  283. package/lib/node_modules/chokidar/esm/package.json +0 -1
  284. package/lib/node_modules/chokidar/handler.d.ts +0 -90
  285. package/lib/node_modules/chokidar/handler.js +0 -635
  286. package/lib/node_modules/chokidar/index.d.ts +0 -215
  287. package/lib/node_modules/chokidar/index.js +0 -804
  288. package/lib/node_modules/chokidar/package.json +0 -69
  289. package/lib/node_modules/readdirp/LICENSE +0 -21
  290. package/lib/node_modules/readdirp/README.md +0 -120
  291. package/lib/node_modules/readdirp/esm/index.d.ts +0 -108
  292. package/lib/node_modules/readdirp/esm/index.js +0 -257
  293. package/lib/node_modules/readdirp/esm/package.json +0 -1
  294. package/lib/node_modules/readdirp/index.d.ts +0 -108
  295. package/lib/node_modules/readdirp/index.js +0 -263
  296. package/lib/node_modules/readdirp/package.json +0 -70
@@ -43,9 +43,13 @@ exports.extractFromSource = extractFromSource;
43
43
  const path = __importStar(require("path"));
44
44
  const grammars_1 = require("./grammars");
45
45
  const tree_sitter_helpers_1 = require("./tree-sitter-helpers");
46
+ const function_ref_1 = require("./function-ref");
47
+ const generated_detection_1 = require("./generated-detection");
46
48
  const languages_1 = require("./languages");
47
49
  const liquid_extractor_1 = require("./liquid-extractor");
50
+ const razor_extractor_1 = require("./razor-extractor");
48
51
  const svelte_extractor_1 = require("./svelte-extractor");
52
+ const astro_extractor_1 = require("./astro-extractor");
49
53
  const dfm_extractor_1 = require("./dfm-extractor");
50
54
  const vue_extractor_1 = require("./vue-extractor");
51
55
  const mybatis_extractor_1 = require("./mybatis-extractor");
@@ -130,6 +134,78 @@ function extractName(node, source, extractor) {
130
134
  }
131
135
  return '<anonymous>';
132
136
  }
137
+ /**
138
+ * Resolve a Scala type node to its base type NAME for name-matching — unwrapping
139
+ * `generic_type` (`Monoid[Int]` → `Monoid`), taking the last segment of a
140
+ * qualified `stable_type_identifier` (`cats.Functor` → `Functor`), and falling
141
+ * back to a descendant `type_identifier`. Returns null for non-type nodes.
142
+ * Shared by Scala inheritance and type-reference extraction.
143
+ */
144
+ function scalaBaseTypeName(node, source) {
145
+ if (!node)
146
+ return null;
147
+ switch (node.type) {
148
+ case 'type_identifier':
149
+ case 'identifier':
150
+ return (0, tree_sitter_helpers_1.getNodeText)(node, source);
151
+ case 'generic_type':
152
+ // `<base> type_arguments` — the base type is the first named child.
153
+ return scalaBaseTypeName(node.namedChild(0), source);
154
+ case 'stable_type_identifier':
155
+ case 'stable_identifier': {
156
+ // Qualified `a.b.C` — match on the simple (last) segment.
157
+ const ids = node.namedChildren.filter((c) => c.type === 'type_identifier' || c.type === 'identifier');
158
+ const last = ids[ids.length - 1];
159
+ return last ? (0, tree_sitter_helpers_1.getNodeText)(last, source) : null;
160
+ }
161
+ default: {
162
+ const id = node.namedChildren.find((c) => c.type === 'type_identifier');
163
+ return id ? (0, tree_sitter_helpers_1.getNodeText)(id, source) : null;
164
+ }
165
+ }
166
+ }
167
+ /**
168
+ * PHP type-position wrapper node kinds (a type-hint is `named_type`,
169
+ * `?Foo` is `optional_type`, `A|B` is `union_type`, `A&B` is
170
+ * `intersection_type`). Used to find the type subtree inside a parameter /
171
+ * property / return position before walking it for class references.
172
+ */
173
+ const PHP_TYPE_NODES = new Set([
174
+ 'named_type', 'optional_type', 'nullable_type',
175
+ 'union_type', 'intersection_type', 'disjunctive_normal_form_type',
176
+ 'primitive_type',
177
+ ]);
178
+ /**
179
+ * Member-access node kinds whose receiver, when it's a capitalized
180
+ * type/enum/class name, is a real dependency — `Enum.value`, `Type.CONST`,
181
+ * `Foo::BAR`. These VALUE reads (as opposed to `Type.method()` calls, already
182
+ * handled) produced no edge, so a type used only via a static member or enum
183
+ * value looked like nothing depended on it. See {@link extractStaticMemberRef}.
184
+ */
185
+ const MEMBER_ACCESS_TYPES = new Set([
186
+ 'field_access', // java (`Foo.BAR`)
187
+ 'member_access_expression', // c# (`Foo.Bar`)
188
+ 'navigation_expression', // kotlin / swift (`Foo.bar`)
189
+ 'field_expression', // scala (`Foo.bar`)
190
+ 'class_constant_access_expression', // php (`Foo::CONST`, `Foo::class`)
191
+ 'scoped_property_access_expression', // php (`Foo::$bar`)
192
+ 'qualified_identifier', // c++ (`Foo::bar`)
193
+ ]);
194
+ /**
195
+ * Languages whose types are Capitalized by convention, so a capitalized
196
+ * member-access receiver is reliably a type (not a local/variable). The
197
+ * static-member/value-read pass is gated to these — the ones where it was the
198
+ * confirmed residual frontier (enum-value / static-field reads). TS/JS/Python
199
+ * are deliberately excluded, and a measured A/B confirms the call: extending the
200
+ * pass to them adds ZERO coverage — in import-based languages you must `import` a
201
+ * type before any `Type.MEMBER` read, so the import edge already covers it (the
202
+ * static read is pure duplication) — while adding real graph noise (+1813 edges /
203
+ * +2448 `references` on excalidraw, the retrieval-perf benchmark, all pointing at
204
+ * already-covered types). Don't re-add `member_expression`/`attribute` here.
205
+ */
206
+ const STATIC_MEMBER_LANGS = new Set([
207
+ 'java', 'csharp', 'kotlin', 'swift', 'scala', 'dart', 'php', 'cpp',
208
+ ]);
133
209
  /**
134
210
  * Tree-sitter node kinds that represent constructor invocations
135
211
  * (`new Foo()` and friends). Used by extractInstantiation to emit
@@ -139,6 +215,9 @@ const INSTANTIATION_KINDS = new Set([
139
215
  'new_expression', // typescript / javascript / tsx / jsx
140
216
  'object_creation_expression', // java / c#
141
217
  'instance_creation_expression', // some grammars
218
+ 'composite_literal', // go — `Widget{...}` / `pkga.Widget{...}`
219
+ 'struct_expression', // rust — `Widget { n: 1 }` / `m::Widget { .. }`
220
+ 'instance_expression', // scala — `new Monoid[Int] { ... }`
142
221
  ]);
143
222
  /**
144
223
  * TreeSitterExtractor - Main extraction class
@@ -155,11 +234,17 @@ class TreeSitterExtractor {
155
234
  extractor = null;
156
235
  nodeStack = []; // Stack of parent node IDs
157
236
  methodIndex = null; // lookup key → node ID for Pascal defProc lookup
237
+ // Function-as-value capture (#756): per-language spec + candidates collected
238
+ // during the walk, gated & flushed into unresolvedReferences at end-of-file
239
+ // (see flushFnRefCandidates).
240
+ fnRefSpec;
241
+ fnRefCandidates = [];
158
242
  constructor(filePath, source, language) {
159
243
  this.filePath = filePath;
160
244
  this.source = source;
161
245
  this.language = language || (0, grammars_1.detectLanguage)(filePath, source);
162
246
  this.extractor = languages_1.EXTRACTORS[this.language] || null;
247
+ this.fnRefSpec = function_ref_1.FN_REF_SPECS[this.language];
163
248
  }
164
249
  /**
165
250
  * Parse and extract from the source code
@@ -200,6 +285,14 @@ class TreeSitterExtractor {
200
285
  };
201
286
  }
202
287
  try {
288
+ // Optional pre-parse source transform (offset-preserving) to work around
289
+ // grammar gaps — e.g. C# blanks conditional-compilation directive lines
290
+ // the grammar mis-parses inside enum bodies (#237). We reassign
291
+ // this.source so downstream getNodeText reads the same bytes the parser
292
+ // saw (identical outside the blanked directive lines).
293
+ if (this.extractor?.preParse) {
294
+ this.source = this.extractor.preParse(this.source);
295
+ }
203
296
  this.tree = parser.parse(this.source) ?? null;
204
297
  if (!this.tree) {
205
298
  throw new Error('Parser returned null tree');
@@ -230,6 +323,9 @@ class TreeSitterExtractor {
230
323
  if (packageNodeId)
231
324
  this.nodeStack.push(packageNodeId);
232
325
  this.visitNode(this.tree.rootNode);
326
+ // Gate + flush function-as-value candidates (#756) while the file's
327
+ // nodes and import refs are complete and the file node is still pushed.
328
+ this.flushFnRefCandidates();
233
329
  if (packageNodeId)
234
330
  this.nodeStack.pop();
235
331
  this.nodeStack.pop();
@@ -267,6 +363,157 @@ class TreeSitterExtractor {
267
363
  durationMs: Date.now() - startTime,
268
364
  };
269
365
  }
366
+ /**
367
+ * Function-as-value capture (#756): if this node is one of the language's
368
+ * value-position containers (call arguments, assignment RHS, struct/object
369
+ * initializer, array/table literal), collect candidate function names from
370
+ * it. Candidates are gated & flushed at end-of-file (flushFnRefCandidates).
371
+ */
372
+ maybeCaptureFnRefs(node, nodeType) {
373
+ const spec = this.fnRefSpec;
374
+ if (!spec)
375
+ return;
376
+ const rule = spec.dispatch.get(nodeType);
377
+ if (!rule || this.nodeStack.length === 0)
378
+ return;
379
+ const fromNodeId = this.nodeStack[this.nodeStack.length - 1];
380
+ if (!fromNodeId)
381
+ return;
382
+ for (const cand of (0, function_ref_1.captureFnRefCandidates)(node, rule, spec, this.source)) {
383
+ this.fnRefCandidates.push({ ...cand, fromNodeId });
384
+ }
385
+ }
386
+ /**
387
+ * Candidates-only scan of a subtree the main walkers won't traverse
388
+ * (top-level variable initializers). No extraction side effects. Halts at
389
+ * nested function definitions: their bodies are walked — and their
390
+ * candidates attributed — by extractFunction's own body walk.
391
+ */
392
+ scanFnRefSubtree(node, depth) {
393
+ if (!this.fnRefSpec || depth > 12)
394
+ return;
395
+ const nodeType = node.type;
396
+ if (depth > 0 && (this.extractor?.functionTypes.includes(nodeType) ||
397
+ nodeType === 'arrow_function' ||
398
+ nodeType === 'function_expression' ||
399
+ nodeType === 'lambda_literal' ||
400
+ nodeType === 'lambda_expression')) {
401
+ return;
402
+ }
403
+ this.maybeCaptureFnRefs(node, nodeType);
404
+ for (let i = 0; i < node.namedChildCount; i++) {
405
+ const child = node.namedChild(i);
406
+ if (child)
407
+ this.scanFnRefSubtree(child, depth + 1);
408
+ }
409
+ }
410
+ /**
411
+ * Gate captured function-as-value candidates and push survivors as
412
+ * `function_ref` unresolved references.
413
+ *
414
+ * The gate bounds volume and protects precision: a candidate survives only
415
+ * if its name matches a function/method DEFINED IN THIS FILE or a name this
416
+ * file imports/references. Everything else (locals, params, fields passed
417
+ * as arguments) is dropped before it ever reaches the database. Resolution
418
+ * then matches survivors against function/method nodes only
419
+ * (matchFunctionRef) and emits `references` edges — which callers/impact
420
+ * already traverse.
421
+ *
422
+ * Known v1 limit, deliberate: a C/C++ callback registered in a DIFFERENT
423
+ * translation unit than its definition (extern, no symbol imports to match)
424
+ * is not captured. Same-file registration — the dominant C pattern (static
425
+ * callback + same-file ops struct) — is.
426
+ */
427
+ flushFnRefCandidates() {
428
+ if (this.fnRefCandidates.length === 0)
429
+ return;
430
+ const candidates = this.fnRefCandidates;
431
+ this.fnRefCandidates = [];
432
+ // Generated/minified files (vendored jquery.min.js and friends): their
433
+ // function-as-value edges are noise — single-letter minified symbols
434
+ // resolve everywhere. Same policy as the callback synthesizer.
435
+ if ((0, generated_detection_1.isGeneratedFile)(this.filePath))
436
+ return;
437
+ const definedHere = new Set();
438
+ for (const n of this.nodes) {
439
+ if (n.kind === 'function' || n.kind === 'method')
440
+ definedHere.add(n.name);
441
+ }
442
+ // Import-binding names only (all binding emitters push kind 'imports').
443
+ // Deliberately NOT 'references': those carry type-annotation and
444
+ // interface-member names, which let local variables that share a type
445
+ // member's name slip through the gate (excalidraw A/B finding). A dotted
446
+ // import (JVM `import com.example.OtherClass`) also contributes its LAST
447
+ // segment — the simple name Java/Kotlin code uses in `OtherClass::method`
448
+ // references.
449
+ const SIMPLE_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
450
+ // JVM imports are dotted (`com.example.OtherClass`); PHP `use` imports
451
+ // are backslashed (`App\Services\Mailer`). Both contribute their last
452
+ // segment — the simple name code uses to reference them.
453
+ const QUALIFIED_IMPORT = /^[A-Za-z_$][A-Za-z0-9_$.\\]*[.\\]([A-Za-z_$][A-Za-z0-9_$]*)$/;
454
+ const importedNames = new Set();
455
+ for (const r of this.unresolvedReferences) {
456
+ if (r.referenceKind !== 'imports')
457
+ continue;
458
+ if (SIMPLE_NAME.test(r.referenceName)) {
459
+ importedNames.add(r.referenceName);
460
+ }
461
+ else {
462
+ const qualified = r.referenceName.match(QUALIFIED_IMPORT);
463
+ if (qualified)
464
+ importedNames.add(qualified[1]);
465
+ }
466
+ }
467
+ const ungated = this.fnRefSpec?.ungatedModes;
468
+ const addressOfOnly = this.fnRefSpec?.addressOfOnly === true;
469
+ const seen = new Set();
470
+ for (const c of candidates) {
471
+ const atFileScope = c.fromNodeId.startsWith('file:');
472
+ // C++ (addressOfOnly): a BARE identifier qualifies only inside a
473
+ // file-scope initializer table. Everywhere else — args, assignments,
474
+ // local braced-init lists like `{begin, size}` — only explicit `&`
475
+ // forms count (fmt A/B finding: generic names `begin`/`out`/`size`
476
+ // collide with locals and members).
477
+ if (addressOfOnly &&
478
+ !c.explicitRef &&
479
+ !(atFileScope && (c.mode === 'value' || c.mode === 'list'))) {
480
+ continue;
481
+ }
482
+ // Gate policy by candidate shape:
483
+ // - `this.<member>`: ALWAYS flush — the member may be inherited from a
484
+ // class in another file (definedHere can't see it), volume is
485
+ // naturally bounded by real `this.X` expressions, and resolution is
486
+ // strictly class-scoped (own members or the validated supertype
487
+ // pass), so nothing fuzzy can leak.
488
+ // - `Scope::member` (C++ member-pointers, Java/Kotlin type-qualified
489
+ // method refs, PHP `'Cls::m'`): ALWAYS flush — the explicit-ref
490
+ // syntax is self-selecting, the referenced type often needs NO
491
+ // import (Java/Kotlin same-package, Kotlin companions), and
492
+ // resolution is scope-suffix-anchored + unique-or-drop, so a
493
+ // same-named member on another class can't match.
494
+ // - C-family file-scope initializers skip the gate entirely
495
+ // (constant-expression context — see FnRefSpec.ungatedModes).
496
+ // - everything else: name ∈ same-file functions/methods ∪ imports.
497
+ if (!c.name.startsWith('this.') && !c.name.includes('::')) {
498
+ const skipGate = (ungated?.has(c.mode) === true && atFileScope) ||
499
+ c.skipGate === true; // PHP HOF-position string callables (see FnRefCandidate.skipGate)
500
+ if (!skipGate && !definedHere.has(c.name) && !importedNames.has(c.name)) {
501
+ continue;
502
+ }
503
+ }
504
+ const key = `${c.fromNodeId}|${c.name}`;
505
+ if (seen.has(key))
506
+ continue;
507
+ seen.add(key);
508
+ this.unresolvedReferences.push({
509
+ fromNodeId: c.fromNodeId,
510
+ referenceName: c.name,
511
+ referenceKind: 'function_ref',
512
+ line: c.line,
513
+ column: c.column,
514
+ });
515
+ }
516
+ }
270
517
  /**
271
518
  * Visit a node and extract information
272
519
  */
@@ -279,8 +526,14 @@ class TreeSitterExtractor {
279
526
  if (this.extractor.visitNode) {
280
527
  const ctx = this.makeExtractorContext();
281
528
  const handled = this.extractor.visitNode(node, ctx);
282
- if (handled)
529
+ if (handled) {
530
+ // The hook consumed this subtree, so the walkers below never descend
531
+ // into it — scan it for function-as-value candidates (#756). Scala's
532
+ // hook handles val/var definitions (`val table = Seq(targetCb)`), for
533
+ // example. The scan is capture-only and halts at nested functions.
534
+ this.scanFnRefSubtree(node, 0);
283
535
  return;
536
+ }
284
537
  }
285
538
  // Pascal-specific AST handling
286
539
  if (this.language === 'pascal') {
@@ -288,6 +541,10 @@ class TreeSitterExtractor {
288
541
  if (skipChildren)
289
542
  return;
290
543
  }
544
+ // Function-as-value capture (#756) — independent of the dispatch ladder
545
+ // below (the captured container types have no other handler there), so it
546
+ // can never shadow or be shadowed by an extraction branch.
547
+ this.maybeCaptureFnRefs(node, nodeType);
291
548
  // Check for function declarations
292
549
  // For Python/Ruby, function_definition inside a class should be treated as method
293
550
  if (this.extractor.functionTypes.includes(nodeType)) {
@@ -329,8 +586,31 @@ class TreeSitterExtractor {
329
586
  }
330
587
  // Check for method declarations (only if not already handled by functionTypes)
331
588
  else if (this.extractor.methodTypes.includes(nodeType)) {
332
- this.extractMethod(node);
333
- skipChildren = true; // extractMethod visits children via visitFunctionBody
589
+ // TS/JS class fields parse as a methodTypes node; only function-valued
590
+ // fields are methods a plain field (`public fonts: Fonts;`) is a
591
+ // property (#808). classifyMethodNode is absent for other languages.
592
+ if (this.extractor.classifyMethodNode?.(node) === 'property') {
593
+ const propNode = this.extractProperty(node);
594
+ // Walk the initializer so its calls/instantiations attribute to the
595
+ // property (`history = createHistory()` → history calls
596
+ // createHistory). The old field-as-method path never walked these
597
+ // (resolveBody only resolves function bodies), so this is additive.
598
+ const valueNode = (0, tree_sitter_helpers_1.getChildByField)(node, 'value');
599
+ if (propNode && valueNode) {
600
+ this.nodeStack.push(propNode.id);
601
+ this.visitFunctionBody(valueNode, '');
602
+ this.nodeStack.pop();
603
+ }
604
+ // A field initializer can also register callbacks
605
+ // (`static handlers = { click: onClick }`) — scan it for
606
+ // function-as-value candidates (capture-only, halts at functions).
607
+ this.scanFnRefSubtree(node, 0);
608
+ skipChildren = true;
609
+ }
610
+ else {
611
+ this.extractMethod(node);
612
+ skipChildren = true; // extractMethod visits children via visitFunctionBody
613
+ }
334
614
  }
335
615
  // Check for interface/protocol/trait declarations
336
616
  else if (this.extractor.interfaceTypes.includes(nodeType)) {
@@ -356,19 +636,70 @@ class TreeSitterExtractor {
356
636
  // Check for class properties (e.g. C# property_declaration)
357
637
  else if (this.extractor.propertyTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
358
638
  this.extractProperty(node);
639
+ // Property initializers aren't walked — scan for function-as-value
640
+ // candidates (#756): Scala `val table = Seq(targetCb)` in an object,
641
+ // Kotlin `val cb = ::handler` class properties.
642
+ this.scanFnRefSubtree(node, 0);
359
643
  skipChildren = true;
360
644
  }
361
645
  // Check for class fields (e.g. Java field_declaration, C# field_declaration)
362
646
  else if (this.extractor.fieldTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
363
647
  this.extractField(node);
648
+ // Field initializers aren't walked — scan for function-as-value
649
+ // candidates (#756): Java `List<IntConsumer> table = List.of(Main::cb)`,
650
+ // C# `List<Action<int>> table = new() { TargetCb }`.
651
+ this.scanFnRefSubtree(node, 0);
364
652
  skipChildren = true;
365
653
  }
366
654
  // Check for variable declarations (const, let, var, etc.)
367
655
  // Only extract top-level variables (not inside functions/methods)
368
656
  else if (this.extractor.variableTypes.includes(nodeType) && !this.isInsideClassLikeNode()) {
369
657
  this.extractVariable(node);
658
+ // extractVariable doesn't walk every initializer shape (object literals
659
+ // are deliberately skipped; Python/Ruby don't walk at all), so scan the
660
+ // declaration subtree for function-as-value candidates — `const routes =
661
+ // { home: renderHome }`, `handlers = {"recv": target_cb}`. The scan halts
662
+ // at nested function definitions (their bodies are walked — and
663
+ // attributed — separately) and flush-time dedup absorbs any overlap with
664
+ // initializers extractVariable DOES walk.
665
+ this.scanFnRefSubtree(node, 0);
370
666
  skipChildren = true; // extractVariable handles children
371
667
  }
668
+ // Swift stored properties inside a type. Swift instance properties aren't
669
+ // extracted as their own nodes, but a property's PROPERTY WRAPPER
670
+ // (`@Argument`/`@Published`/`@State`/custom) and declared type ARE
671
+ // dependencies — attribute them to the enclosing type so the wrapper/type
672
+ // files get dependents. Don't skipChildren: an initializer's calls still
673
+ // matter. (Other languages extract properties via property/field types.)
674
+ else if (this.language === 'swift' &&
675
+ nodeType === 'property_declaration' &&
676
+ this.isInsideClassLikeNode()) {
677
+ const ownerId = this.nodeStack[this.nodeStack.length - 1];
678
+ if (ownerId) {
679
+ this.extractDecoratorsFor(node, ownerId);
680
+ this.extractVariableTypeAnnotation(node, ownerId);
681
+ // Fluent / SwiftUI property-wrapper attributes often reference a model or
682
+ // type by metatype in their ARGUMENTS — `@Siblings(through: Pivot.self,
683
+ // …)`, `@Group(…)`. extractDecoratorsFor captures the wrapper type
684
+ // (`Siblings`); this pulls the TYPE out of the argument expressions
685
+ // (`Pivot.self` → a dependency on Pivot), so a model reached ONLY through
686
+ // a relationship (a many-to-many pivot/join model) isn't left orphaned.
687
+ // extractStaticMemberRef self-filters to `Type.member` navigation, so the
688
+ // `\.$keypath` arguments and the wrapper `user_type` are skipped.
689
+ const modifiers = node.namedChildren.find((c) => c.type === 'modifiers');
690
+ if (modifiers) {
691
+ const walkAttrArgs = (n) => {
692
+ this.extractStaticMemberRef(n);
693
+ for (let i = 0; i < n.namedChildCount; i++) {
694
+ const c = n.namedChild(i);
695
+ if (c)
696
+ walkAttrArgs(c);
697
+ }
698
+ };
699
+ walkAttrArgs(modifiers);
700
+ }
701
+ }
702
+ }
372
703
  // `export_statement` itself is not extracted — the walker descends
373
704
  // into children, where the inner declaration (lexical_declaration,
374
705
  // function_declaration, class_declaration, etc.) is dispatched to
@@ -386,6 +717,21 @@ class TreeSitterExtractor {
386
717
  else if (this.extractor.importTypes.includes(nodeType)) {
387
718
  this.extractImport(node);
388
719
  }
720
+ // Re-export from another module — `export { X } from './y'` (TS/JS). A
721
+ // re-export is a dependency on the source module just like an import, but
722
+ // the export_statement is otherwise only descended into (no declaration to
723
+ // extract), so a barrel that ONLY re-exports produced zero edges and showed
724
+ // 0 dependents. Link each re-exported name to its definition. Children are
725
+ // still visited (a non-re-export `export const X = …` has no `source` and
726
+ // falls through to its normal declaration extraction).
727
+ else if (nodeType === 'export_statement' &&
728
+ (this.language === 'typescript' || this.language === 'tsx' ||
729
+ this.language === 'javascript' || this.language === 'jsx') &&
730
+ (0, tree_sitter_helpers_1.getChildByField)(node, 'source')) {
731
+ const parentId = this.nodeStack[this.nodeStack.length - 1];
732
+ if (parentId)
733
+ this.emitReExportRefs(node, parentId);
734
+ }
389
735
  // Check for function calls
390
736
  else if (this.extractor.callTypes.includes(nodeType)) {
391
737
  this.extractCall(node);
@@ -477,6 +823,14 @@ class TreeSitterExtractor {
477
823
  updatedAt: Date.now(),
478
824
  ...extra,
479
825
  };
826
+ // Persist extra symbol-level modifiers (e.g. Kotlin `expect`/`actual`) onto
827
+ // the node's decorators list so the resolver can pair multiplatform
828
+ // declarations with their implementations. Merged, not overwritten, so a
829
+ // language that also captures real annotations keeps both.
830
+ const mods = this.extractor?.extractModifiers?.(node);
831
+ if (mods && mods.length > 0) {
832
+ newNode.decorators = [...(newNode.decorators ?? []), ...mods];
833
+ }
480
834
  this.nodes.push(newNode);
481
835
  // Add containment edge from parent
482
836
  if (this.nodeStack.length > 0) {
@@ -616,8 +970,19 @@ class TreeSitterExtractor {
616
970
  }
617
971
  }
618
972
  }
619
- if (name === '<anonymous>')
620
- return; // Skip anonymous functions
973
+ if (name === '<anonymous>') {
974
+ // Don't emit a node for the anonymous wrapper itself, but still visit its
975
+ // body: AMD/RequireJS and CommonJS module wrappers (`define([], function(){…})`,
976
+ // `(function(){…})()`) hold named inner functions and calls that would
977
+ // otherwise be lost — the dispatcher set skipChildren, so nothing else
978
+ // descends into this subtree. (#528)
979
+ const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
980
+ ?? (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.bodyField);
981
+ if (body) {
982
+ this.visitFunctionBody(body, '');
983
+ }
984
+ return;
985
+ }
621
986
  // Check for misparse artifacts (e.g. C++ macros causing "namespace detail" functions)
622
987
  // Skip the node but still visit the body for calls and structural nodes
623
988
  if (this.extractor.isMisparsedFunction?.(name, node)) {
@@ -634,6 +999,7 @@ class TreeSitterExtractor {
634
999
  const isExported = this.extractor.isExported?.(node, this.source);
635
1000
  const isAsync = this.extractor.isAsync?.(node);
636
1001
  const isStatic = this.extractor.isStatic?.(node);
1002
+ const returnType = this.extractor.getReturnType?.(node, this.source);
637
1003
  const funcNode = this.createNode('function', name, node, {
638
1004
  docstring,
639
1005
  signature,
@@ -641,6 +1007,7 @@ class TreeSitterExtractor {
641
1007
  isExported,
642
1008
  isAsync,
643
1009
  isStatic,
1010
+ returnType,
644
1011
  });
645
1012
  if (!funcNode)
646
1013
  return;
@@ -678,6 +1045,8 @@ class TreeSitterExtractor {
678
1045
  return;
679
1046
  // Extract extends/implements
680
1047
  this.extractInheritance(node, classNode.id);
1048
+ // C# primary-constructor parameter dependencies (`class Svc(IRepo r, …)`).
1049
+ this.extractCsharpPrimaryCtorParamRefs(node, classNode.id);
681
1050
  // Extract decorators applied to the class (`@Foo class X {}`).
682
1051
  this.extractDecoratorsFor(node, classNode.id);
683
1052
  // Push to stack and visit body
@@ -738,12 +1107,14 @@ class TreeSitterExtractor {
738
1107
  const visibility = this.extractor.getVisibility?.(node);
739
1108
  const isAsync = this.extractor.isAsync?.(node);
740
1109
  const isStatic = this.extractor.isStatic?.(node);
1110
+ const returnType = this.extractor.getReturnType?.(node, this.source);
741
1111
  const extraProps = {
742
1112
  docstring,
743
1113
  signature,
744
1114
  visibility,
745
1115
  isAsync,
746
1116
  isStatic,
1117
+ returnType,
747
1118
  };
748
1119
  if (receiverType) {
749
1120
  extraProps.qualifiedName = `${receiverType}::${name}`;
@@ -817,8 +1188,10 @@ class TreeSitterExtractor {
817
1188
  if (!this.extractor)
818
1189
  return;
819
1190
  // Skip forward declarations and type references (no body = not a definition)
1191
+ // — EXCEPT C# positional records (`record struct M(decimal Amount);`),
1192
+ // complete definitions with no body block. (#831)
820
1193
  const body = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.bodyField);
821
- if (!body)
1194
+ if (!body && node.type !== 'record_declaration')
822
1195
  return;
823
1196
  const name = extractName(node, this.source, this.extractor);
824
1197
  const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
@@ -833,15 +1206,21 @@ class TreeSitterExtractor {
833
1206
  return;
834
1207
  // Extract inheritance (e.g. Swift: struct HTTPMethod: RawRepresentable)
835
1208
  this.extractInheritance(node, structNode.id);
836
- // Push to stack for field extraction
837
- this.nodeStack.push(structNode.id);
838
- for (let i = 0; i < body.namedChildCount; i++) {
839
- const child = body.namedChild(i);
840
- if (child) {
841
- this.visitNode(child);
1209
+ // C# primary-constructor parameter dependencies (`struct P(int x)`, and
1210
+ // `record struct M(decimal Amount)` which the grammar nests here).
1211
+ this.extractCsharpPrimaryCtorParamRefs(node, structNode.id);
1212
+ // Push to stack for field extraction (bodiless positional records have
1213
+ // no members to visit)
1214
+ if (body) {
1215
+ this.nodeStack.push(structNode.id);
1216
+ for (let i = 0; i < body.namedChildCount; i++) {
1217
+ const child = body.namedChild(i);
1218
+ if (child) {
1219
+ this.visitNode(child);
1220
+ }
842
1221
  }
1222
+ this.nodeStack.pop();
843
1223
  }
844
- this.nodeStack.pop();
845
1224
  }
846
1225
  /**
847
1226
  * Extract an enum
@@ -914,22 +1293,35 @@ class TreeSitterExtractor {
914
1293
  */
915
1294
  extractProperty(node) {
916
1295
  if (!this.extractor)
917
- return;
1296
+ return null;
918
1297
  const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
919
1298
  const visibility = this.extractor.getVisibility?.(node);
920
1299
  const isStatic = this.extractor.isStatic?.(node) ?? false;
921
1300
  const hookName = this.extractor.extractPropertyName?.(node, this.source);
1301
+ // JS `field_definition` names its key the `property` field (TS uses
1302
+ // `name`) — try both before the generic identifier scan (#808).
922
1303
  const nameNode = hookName
923
1304
  ? null
924
- : (0, tree_sitter_helpers_1.getChildByField)(node, 'name') || node.namedChildren.find(c => c.type === 'identifier');
1305
+ : (0, tree_sitter_helpers_1.getChildByField)(node, 'name') ||
1306
+ (0, tree_sitter_helpers_1.getChildByField)(node, 'property') ||
1307
+ node.namedChildren.find(c => c.type === 'identifier');
925
1308
  const name = hookName ?? (nameNode ? (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source) : null);
926
1309
  if (!name)
927
- return;
928
- // Get property type from the type child (first named child that isn't modifier or identifier)
929
- const typeNode = node.namedChildren.find(c => c.type !== 'modifier' && c.type !== 'modifiers'
930
- && c.type !== 'identifier' && c.type !== 'accessor_list'
931
- && c.type !== 'accessors' && c.type !== 'equals_value_clause');
932
- const typeText = typeNode ? (0, tree_sitter_helpers_1.getNodeText)(typeNode, this.source) : undefined;
1310
+ return null;
1311
+ // Get property type. TS/JS field definitions carry an explicit `type`
1312
+ // field (a `type_annotation`); their other named children are the name
1313
+ // and the initializer VALUE, which the generic finder below would
1314
+ // wrongly pick so fields use the type field only (#808). Other
1315
+ // languages (C# property_declaration) keep the generic scan.
1316
+ const isTsJsField = node.type === 'public_field_definition' || node.type === 'field_definition';
1317
+ const typeNode = isTsJsField
1318
+ ? (0, tree_sitter_helpers_1.getChildByField)(node, 'type')
1319
+ : node.namedChildren.find(c => c.type !== 'modifier' && c.type !== 'modifiers'
1320
+ && c.type !== 'identifier' && c.type !== 'accessor_list'
1321
+ && c.type !== 'accessors' && c.type !== 'equals_value_clause');
1322
+ const typeText = typeNode
1323
+ ? (0, tree_sitter_helpers_1.getNodeText)(typeNode, this.source).replace(/^:\s*/, '')
1324
+ : undefined;
933
1325
  const signature = typeText ? `${typeText} ${name}` : name;
934
1326
  const propNode = this.createNode('property', name, node, {
935
1327
  docstring,
@@ -946,6 +1338,7 @@ class TreeSitterExtractor {
946
1338
  // `type_annotation` children; the C# branch walks the `type` field.
947
1339
  this.extractTypeAnnotations(node, propNode.id);
948
1340
  }
1341
+ return propNode;
949
1342
  }
950
1343
  /**
951
1344
  * Extract a class field declaration (e.g. Java field_declaration, C# field_declaration).
@@ -1263,16 +1656,34 @@ class TreeSitterExtractor {
1263
1656
  const specs = node.namedChildren.filter(c => c.type === 'var_spec' || c.type === 'const_spec');
1264
1657
  for (const spec of specs) {
1265
1658
  const nameNode = spec.namedChild(0);
1659
+ let varNode = null;
1266
1660
  if (nameNode && nameNode.type === 'identifier') {
1267
1661
  const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
1268
1662
  const valueNode = spec.namedChildCount > 1 ? spec.namedChild(spec.namedChildCount - 1) : null;
1269
1663
  const initValue = valueNode ? (0, tree_sitter_helpers_1.getNodeText)(valueNode, this.source).slice(0, 100) : undefined;
1270
1664
  const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
1271
- this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, {
1665
+ varNode = this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, {
1272
1666
  docstring,
1273
1667
  signature: initSignature,
1274
1668
  });
1275
1669
  }
1670
+ // Walk the initializer so composite literals and calls in a
1671
+ // package-level `var Query Binding = queryBinding{}` (a registry of
1672
+ // implementations) or `var c = pkg.New()` are extracted as
1673
+ // instantiates/calls dependencies — the body walker only covers
1674
+ // initializers inside functions, not these top-level declarations.
1675
+ // Scope the walk to the declared symbol so a call inside an anonymous
1676
+ // func_literal initializer — a cobra `RunE: func(){…}` handler, a
1677
+ // goroutine or callback closure — attributes to the var instead of
1678
+ // leaking to the file node (which reads as "no caller"), issue #693.
1679
+ const valueField = (0, tree_sitter_helpers_1.getChildByField)(spec, 'value');
1680
+ if (valueField) {
1681
+ if (varNode)
1682
+ this.nodeStack.push(varNode.id);
1683
+ this.visitFunctionBody(valueField, varNode?.id ?? '');
1684
+ if (varNode)
1685
+ this.nodeStack.pop();
1686
+ }
1276
1687
  }
1277
1688
  // Handle short_var_declaration (:=)
1278
1689
  if (node.type === 'short_var_declaration') {
@@ -1410,6 +1821,13 @@ class TreeSitterExtractor {
1410
1821
  const typeChild = (0, tree_sitter_helpers_1.getChildByField)(node, 'type');
1411
1822
  if (typeChild)
1412
1823
  this.extractInheritance(typeChild, interfaceNode.id);
1824
+ // Go: extract the interface's method specs as `method` nodes so implicit
1825
+ // interface satisfaction (a struct's method set ⊇ the interface's) and
1826
+ // impl-navigation can see the contract. Go has no `implements` keyword, so
1827
+ // without the interface's method set there's nothing to match against.
1828
+ if (this.language === 'go' && typeChild) {
1829
+ this.extractGoInterfaceMethods(typeChild, interfaceNode.id);
1830
+ }
1413
1831
  return true;
1414
1832
  }
1415
1833
  const typeAliasNode = this.createNode('type_alias', name, node, {
@@ -1429,11 +1847,39 @@ class TreeSitterExtractor {
1429
1847
  // an unrelated class method picked by path-proximity (#359).
1430
1848
  if (this.language === 'typescript' || this.language === 'tsx') {
1431
1849
  this.extractTsTypeAliasMembers(value, typeAliasNode);
1850
+ // `type List = [ Service<'name', Req, Resp>, … ]` — surface each
1851
+ // entry's string-literal name as a searchable member (issue #634).
1852
+ this.extractTsTupleContractNames(value, typeAliasNode);
1432
1853
  }
1433
1854
  }
1434
1855
  }
1435
1856
  return false;
1436
1857
  }
1858
+ /**
1859
+ * Extract the method specs of a Go `interface_type` body as `method` nodes
1860
+ * contained by the interface (e.g. `Marshal`, `Unmarshal` of a `Core`
1861
+ * interface). tree-sitter-go names these `method_elem` (newer) or
1862
+ * `method_spec` (older). Embedded interfaces (`Reader` inside `ReadWriter`)
1863
+ * are `type_identifier`s, not methods, and are left to inheritance extraction.
1864
+ */
1865
+ extractGoInterfaceMethods(interfaceType, ifaceId) {
1866
+ this.nodeStack.push(ifaceId);
1867
+ for (let i = 0; i < interfaceType.namedChildCount; i++) {
1868
+ const m = interfaceType.namedChild(i);
1869
+ if (!m || (m.type !== 'method_elem' && m.type !== 'method_spec'))
1870
+ continue;
1871
+ const nameNode = (0, tree_sitter_helpers_1.getChildByField)(m, 'name') ?? m.namedChild(0);
1872
+ if (!nameNode)
1873
+ continue;
1874
+ const mname = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
1875
+ if (mname) {
1876
+ this.createNode('method', mname, m, {
1877
+ signature: this.extractor?.getSignature?.(m, this.source),
1878
+ });
1879
+ }
1880
+ }
1881
+ this.nodeStack.pop();
1882
+ }
1437
1883
  /**
1438
1884
  * Surface the members of a TypeScript `type X = { ... }` (or intersection
1439
1885
  * thereof) as `property` / `method` nodes under the type-alias node. Only
@@ -1490,6 +1936,82 @@ class TreeSitterExtractor {
1490
1936
  }
1491
1937
  this.nodeStack.pop();
1492
1938
  }
1939
+ /**
1940
+ * Surface the string-literal "names" of a TypeScript service/contract
1941
+ * registry written as a tuple of generic instantiations:
1942
+ *
1943
+ * type MyServiceList = [
1944
+ * Service<'query_apply_record', Req, Resp>,
1945
+ * Service<'apply_confirm', Req, Resp>,
1946
+ * ];
1947
+ *
1948
+ * Each `Service<'name', …>` tags an entry with a string-literal name that a
1949
+ * dynamic factory (`createService<MyServiceList>()`) turns into a callable
1950
+ * property (`api.query_apply_record(…)`). Static extraction otherwise never
1951
+ * sees that name — it's a type argument, not a declaration — so
1952
+ * `codegraph query query_apply_record` returned nothing (issue #634). We emit
1953
+ * each name as a `method` node under the type alias (qualifiedName
1954
+ * `MyServiceList::query_apply_record`) so it's searchable and resolvable as a
1955
+ * symbol. (A call through the proxy, `api.query_apply_record(…)`, still
1956
+ * resolves to the imported `api` binding — the receiver's type isn't known —
1957
+ * so this fixes discoverability, not the per-method call edge.)
1958
+ *
1959
+ * Scope is deliberately narrow to avoid noise: only a string literal that is
1960
+ * a DIRECT type argument of a `generic_type` that is itself a DIRECT element
1961
+ * of a `tuple_type`. This excludes utility types (`Pick`/`Omit`/`Record` are
1962
+ * never written as tuples) and string args nested deeper
1963
+ * (`Service<'a', Pick<U, 'id'>>` yields only `a`, never `id`). Names must be
1964
+ * valid identifiers, which also rules out route paths / arbitrary strings.
1965
+ */
1966
+ extractTsTupleContractNames(value, typeAliasNode) {
1967
+ const tuples = [];
1968
+ const collectTuples = (n, depth) => {
1969
+ if (depth > 6)
1970
+ return; // a type expression is shallow; cap defensively
1971
+ if (n.type === 'tuple_type')
1972
+ tuples.push(n);
1973
+ for (let i = 0; i < n.namedChildCount; i++) {
1974
+ const c = n.namedChild(i);
1975
+ if (c)
1976
+ collectTuples(c, depth + 1);
1977
+ }
1978
+ };
1979
+ collectTuples(value, 0);
1980
+ if (tuples.length === 0)
1981
+ return;
1982
+ this.nodeStack.push(typeAliasNode.id);
1983
+ for (const tuple of tuples) {
1984
+ for (let i = 0; i < tuple.namedChildCount; i++) {
1985
+ const entry = tuple.namedChild(i);
1986
+ if (!entry || entry.type !== 'generic_type')
1987
+ continue;
1988
+ const typeArgs = (0, tree_sitter_helpers_1.getChildByField)(entry, 'type_arguments');
1989
+ if (!typeArgs)
1990
+ continue;
1991
+ for (let j = 0; j < typeArgs.namedChildCount; j++) {
1992
+ const arg = typeArgs.namedChild(j);
1993
+ if (!arg || arg.type !== 'literal_type')
1994
+ continue;
1995
+ // literal_type wraps the actual literal; only a string is a name.
1996
+ const strNode = arg.namedChild(0);
1997
+ if (!strNode || strNode.type !== 'string')
1998
+ continue;
1999
+ const name = (0, tree_sitter_helpers_1.getNodeText)(strNode, this.source)
2000
+ .trim()
2001
+ .replace(/^['"`]/, '')
2002
+ .replace(/['"`]$/, '');
2003
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name))
2004
+ continue;
2005
+ const signature = (0, tree_sitter_helpers_1.getNodeText)(entry, this.source).replace(/\s+/g, ' ').trim().slice(0, 120);
2006
+ this.createNode('method', name, entry, {
2007
+ signature,
2008
+ qualifiedName: `${typeAliasNode.name}::${name}`,
2009
+ });
2010
+ }
2011
+ }
2012
+ }
2013
+ this.nodeStack.pop();
2014
+ }
1493
2015
  /**
1494
2016
  * `foo: () => T` → property_signature whose type_annotation contains a
1495
2017
  * `function_type`. Treat that as a method-shaped contract member, since
@@ -1541,6 +2063,48 @@ class TreeSitterExtractor {
1541
2063
  });
1542
2064
  }
1543
2065
  }
2066
+ // Link each imported binding to its definition so imported-but-not-
2067
+ // called/typed symbols still record a cross-file dependency (TS/JS only).
2068
+ if (this.language === 'typescript' || this.language === 'tsx' ||
2069
+ this.language === 'javascript' || this.language === 'jsx') {
2070
+ const parentId = this.nodeStack[this.nodeStack.length - 1];
2071
+ if (parentId)
2072
+ this.emitImportBindingRefs(node, parentId);
2073
+ }
2074
+ // Python `from module import X, Y` — link each imported name to its
2075
+ // definition (covers `__init__.py` re-export barrels, which are just
2076
+ // `from .sub import X`). Same recall gap as TS: a name imported and
2077
+ // used in a non-call position created no dependency edge.
2078
+ if (this.language === 'python' && node.type === 'import_from_statement') {
2079
+ const parentId = this.nodeStack[this.nodeStack.length - 1];
2080
+ if (parentId)
2081
+ this.emitPyFromImportRefs(node, parentId);
2082
+ }
2083
+ // Rust `use crate::m::Item;` / `pub use self::sub::Item;` — link each
2084
+ // imported leaf to its definition. Covers `pub use` re-export hubs
2085
+ // (a `mod.rs` re-exporting submodule items, e.g. tokio's `fs/mod.rs`)
2086
+ // and items imported but used in non-call/non-type positions.
2087
+ if (this.language === 'rust' && node.type === 'use_declaration') {
2088
+ const parentId = this.nodeStack[this.nodeStack.length - 1];
2089
+ if (parentId)
2090
+ this.emitRustUseBindingRefs(node, parentId);
2091
+ }
2092
+ // PHP `use Foo\Bar\Baz;` — link to the namespace-qualified definition so
2093
+ // an imported-but-DI-injected contract (Laravel's pattern) records a
2094
+ // cross-file dependency. Grouped imports are handled in their own branch.
2095
+ if (this.language === 'php' && node.type === 'namespace_use_declaration') {
2096
+ const parentId = this.nodeStack[this.nodeStack.length - 1];
2097
+ if (parentId)
2098
+ this.emitPhpUseRefs(node, parentId);
2099
+ }
2100
+ // Ruby `require "lib/foo"` / `require_relative "../foo"` — resolve to the
2101
+ // required FILE so a file pulled in only by `require` (config-loaded
2102
+ // components, gems that don't autoload) records a cross-file dependency.
2103
+ if (this.language === 'ruby' && node.type === 'call') {
2104
+ const parentId = this.nodeStack[this.nodeStack.length - 1];
2105
+ if (parentId)
2106
+ this.emitRubyRequireRefs(node, parentId);
2107
+ }
1544
2108
  return;
1545
2109
  }
1546
2110
  // Hook returned null — fall through to multi-import inline handlers only
@@ -1550,12 +2114,31 @@ class TreeSitterExtractor {
1550
2114
  // Multi-import cases that create multiple nodes (can't be expressed with single-return hook)
1551
2115
  // Python import_statement: import os, sys (creates one import per module)
1552
2116
  if (this.language === 'python' && node.type === 'import_statement') {
2117
+ const importParentId = this.nodeStack[this.nodeStack.length - 1];
2118
+ // A bare `import a.b.c` of an internal module (the standard Django
2119
+ // `AppConfig.ready(): import myapp.signals` registration pattern, and any
2120
+ // `import pkg.mod` used for its side effects) had no edge to the module
2121
+ // file — only `from x import y` was linked. Push an `imports` ref (like
2122
+ // Go) so the resolver maps the dotted path to its file. Stdlib/external
2123
+ // modules naturally don't resolve (no `os.py` file node in the repo).
2124
+ const pushModuleRef = (dotted) => {
2125
+ if (!importParentId)
2126
+ return;
2127
+ this.unresolvedReferences.push({
2128
+ fromNodeId: importParentId,
2129
+ referenceName: (0, tree_sitter_helpers_1.getNodeText)(dotted, this.source),
2130
+ referenceKind: 'imports',
2131
+ line: dotted.startPosition.row + 1,
2132
+ column: dotted.startPosition.column,
2133
+ });
2134
+ };
1553
2135
  for (let i = 0; i < node.namedChildCount; i++) {
1554
2136
  const child = node.namedChild(i);
1555
2137
  if (child?.type === 'dotted_name') {
1556
2138
  this.createNode('import', (0, tree_sitter_helpers_1.getNodeText)(child, this.source), node, {
1557
2139
  signature: importText,
1558
2140
  });
2141
+ pushModuleRef(child);
1559
2142
  }
1560
2143
  else if (child?.type === 'aliased_import') {
1561
2144
  const dottedName = child.namedChildren.find(c => c.type === 'dotted_name');
@@ -1563,6 +2146,7 @@ class TreeSitterExtractor {
1563
2146
  this.createNode('import', (0, tree_sitter_helpers_1.getNodeText)(dottedName, this.source), node, {
1564
2147
  signature: importText,
1565
2148
  });
2149
+ pushModuleRef(dottedName);
1566
2150
  }
1567
2151
  }
1568
2152
  }
@@ -1623,6 +2207,9 @@ class TreeSitterExtractor {
1623
2207
  this.createNode('import', fullPath, node, {
1624
2208
  signature: importText,
1625
2209
  });
2210
+ const parentId = this.nodeStack[this.nodeStack.length - 1];
2211
+ if (parentId)
2212
+ this.pushPhpUseRef(fullPath, parentId, node);
1626
2213
  }
1627
2214
  }
1628
2215
  return;
@@ -1636,6 +2223,285 @@ class TreeSitterExtractor {
1636
2223
  signature: importText,
1637
2224
  });
1638
2225
  }
2226
+ /**
2227
+ * Emit one `imports` reference per named/default import binding (TS/JS family),
2228
+ * attributed to the file node — so the resolver links each imported symbol to
2229
+ * the file that DEFINES it.
2230
+ *
2231
+ * Importing a symbol IS a dependency, but extraction only emits references for
2232
+ * calls, instantiations, type annotations, and inheritance. A symbol that's
2233
+ * imported and then only re-exported (`export { X } from './x'`), placed in a
2234
+ * registry array (`[expressResolver, …]`), passed as an argument, or used in
2235
+ * JSX produced NO cross-file edge at all — so the providing file showed a
2236
+ * false "0 dependents" and was invisible to blast-radius / `affected`. The
2237
+ * resolver maps the local name (alias-aware) to the provider's definition and
2238
+ * creates a cross-file `imports` edge; `getFileDependents` picks it up, while
2239
+ * `getImpactRadius` keeps it as a bounded leaf (the importing file node).
2240
+ *
2241
+ * Namespace imports (`import * as NS`) bind a whole module: `NS.member` calls
2242
+ * resolve on their own, but a namespace used ONLY via a value-member read
2243
+ * (`NS.SOME_CONST`) would leave no edge — so we also emit the namespace local
2244
+ * name, which the resolver links to the module FILE as a dependency backstop.
2245
+ */
2246
+ emitImportBindingRefs(node, fromNodeId) {
2247
+ const clause = node.namedChildren.find((c) => c.type === 'import_clause');
2248
+ if (!clause)
2249
+ return; // side-effect import (`import './x'`) — no bindings
2250
+ const pushRef = (nameNode) => {
2251
+ if (!nameNode)
2252
+ return;
2253
+ const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
2254
+ if (!name)
2255
+ return;
2256
+ this.unresolvedReferences.push({
2257
+ fromNodeId,
2258
+ referenceName: name,
2259
+ referenceKind: 'imports',
2260
+ line: nameNode.startPosition.row + 1,
2261
+ column: nameNode.startPosition.column,
2262
+ });
2263
+ };
2264
+ for (const child of clause.namedChildren) {
2265
+ if (child.type === 'identifier') {
2266
+ // default import: `import Foo from './x'`
2267
+ pushRef(child);
2268
+ }
2269
+ else if (child.type === 'named_imports') {
2270
+ // `import { A, B as C } from './x'` — link the LOCAL name (alias if any)
2271
+ for (const spec of child.namedChildren) {
2272
+ if (spec.type !== 'import_specifier')
2273
+ continue;
2274
+ pushRef((0, tree_sitter_helpers_1.getChildByField)(spec, 'alias') ?? (0, tree_sitter_helpers_1.getChildByField)(spec, 'name') ?? spec.namedChild(0));
2275
+ }
2276
+ }
2277
+ else if (child.type === 'namespace_import') {
2278
+ // `import * as NS from './x'` — emit NS so the module-import backstop can
2279
+ // record the file dependency even if NS is only used by value-member read.
2280
+ pushRef(child.namedChildren.find((c) => c.type === 'identifier') ?? child.namedChild(0));
2281
+ }
2282
+ }
2283
+ }
2284
+ /**
2285
+ * Emit one `imports` reference per re-exported binding of a
2286
+ * `export { A, B as C } from './y'` statement, attributed to the file node —
2287
+ * so a barrel that re-exports from another module records a dependency on it.
2288
+ *
2289
+ * Links the SOURCE-side name (`A`, the `name` field — not the local alias
2290
+ * `C`), since that is what the source module defines. `export * from './y'`
2291
+ * has no named bindings to attribute and `export { default as X }` can't be
2292
+ * name-matched, so both are skipped.
2293
+ */
2294
+ emitReExportRefs(node, fromNodeId) {
2295
+ const clause = node.namedChildren.find((c) => c.type === 'export_clause');
2296
+ if (!clause)
2297
+ return; // `export * from './y'` — no named bindings
2298
+ for (const spec of clause.namedChildren) {
2299
+ if (spec.type !== 'export_specifier')
2300
+ continue;
2301
+ const nameNode = (0, tree_sitter_helpers_1.getChildByField)(spec, 'name') ?? spec.namedChild(0);
2302
+ if (!nameNode)
2303
+ continue;
2304
+ const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
2305
+ if (!name || name === 'default')
2306
+ continue;
2307
+ this.unresolvedReferences.push({
2308
+ fromNodeId,
2309
+ referenceName: name,
2310
+ referenceKind: 'imports',
2311
+ line: nameNode.startPosition.row + 1,
2312
+ column: nameNode.startPosition.column,
2313
+ });
2314
+ }
2315
+ }
2316
+ /**
2317
+ * Emit one `imports` reference per binding of a Rust `use` declaration —
2318
+ * `use crate::m::Item`, `use crate::m::{A, B as C}`, `pub use self::sub::Item`.
2319
+ * Emits the FULL path (e.g. `self::sub::Item`, not just `Item`) so the resolver
2320
+ * can resolve the module prefix to a file and find the leaf symbol there —
2321
+ * disambiguating common-name re-exports (`pub use self::read::read`, where the
2322
+ * leaf `read` collides with many same-named symbols). Falls back to name-match
2323
+ * on the leaf when the path can't be resolved. `use ...::*` has no leaf binding.
2324
+ */
2325
+ emitRustUseBindingRefs(node, fromNodeId) {
2326
+ const paths = [];
2327
+ const join = (prefix, seg) => (prefix ? `${prefix}::${seg}` : seg);
2328
+ const collect = (n, prefix) => {
2329
+ switch (n.type) {
2330
+ case 'identifier':
2331
+ paths.push({ text: join(prefix, (0, tree_sitter_helpers_1.getNodeText)(n, this.source)), node: n });
2332
+ break;
2333
+ case 'scoped_identifier': {
2334
+ // Full scoped path (`a::b::C`); combine with any outer group prefix.
2335
+ const full = (0, tree_sitter_helpers_1.getNodeText)(n, this.source).trim();
2336
+ paths.push({ text: prefix ? `${prefix}::${full}` : full, node: n });
2337
+ break;
2338
+ }
2339
+ case 'scoped_use_list': {
2340
+ // `path::{ ... }` — the group's path becomes the prefix for each item.
2341
+ const pathNode = (0, tree_sitter_helpers_1.getChildByField)(n, 'path');
2342
+ const seg = pathNode ? (0, tree_sitter_helpers_1.getNodeText)(pathNode, this.source).trim() : '';
2343
+ const newPrefix = seg ? join(prefix, seg) : prefix;
2344
+ const list = (0, tree_sitter_helpers_1.getChildByField)(n, 'list') ?? n.namedChildren.find((c) => c.type === 'use_list');
2345
+ if (list)
2346
+ collect(list, newPrefix);
2347
+ break;
2348
+ }
2349
+ case 'use_list':
2350
+ for (let i = 0; i < n.namedChildCount; i++) {
2351
+ const c = n.namedChild(i);
2352
+ if (c)
2353
+ collect(c, prefix);
2354
+ }
2355
+ break;
2356
+ case 'use_as_clause': {
2357
+ // `Path as Alias` → link the source path (the definition), not the alias.
2358
+ const p = (0, tree_sitter_helpers_1.getChildByField)(n, 'path') ?? n.namedChild(0);
2359
+ if (p)
2360
+ collect(p, prefix);
2361
+ break;
2362
+ }
2363
+ // use_wildcard → no specific binding to link.
2364
+ }
2365
+ };
2366
+ for (let i = 0; i < node.namedChildCount; i++) {
2367
+ const c = node.namedChild(i);
2368
+ if (c)
2369
+ collect(c, '');
2370
+ }
2371
+ for (const p of paths) {
2372
+ // The leaf must be a real name (skip a path that is only `self`/`super`/`crate`).
2373
+ const leaf = p.text.split('::').pop();
2374
+ if (!leaf || leaf === 'self' || leaf === 'super' || leaf === 'crate' || leaf === '*')
2375
+ continue;
2376
+ this.unresolvedReferences.push({
2377
+ fromNodeId,
2378
+ referenceName: p.text,
2379
+ referenceKind: 'imports',
2380
+ line: p.node.startPosition.row + 1,
2381
+ column: p.node.startPosition.column,
2382
+ });
2383
+ }
2384
+ }
2385
+ /**
2386
+ * Emit an `imports` reference for a single PHP `use Foo\Bar\Baz;` (grouped
2387
+ * imports `use Foo\{A, B}` are handled where their per-item nodes are created).
2388
+ * The reference targets the namespace-qualified `Foo\Bar::Baz` form classes are
2389
+ * stored under (see the PHP `namespace` capture), so it resolves to the RIGHT
2390
+ * definition — Laravel has many same-named contracts (`Factory`, `Dispatcher`,
2391
+ * `Guard`) across namespaces that a bare-name match can't disambiguate.
2392
+ */
2393
+ emitPhpUseRefs(node, fromNodeId) {
2394
+ const clause = node.namedChildren.find((c) => c.type === 'namespace_use_clause');
2395
+ if (!clause)
2396
+ return;
2397
+ const qn = clause.namedChildren.find((c) => c.type === 'qualified_name')
2398
+ ?? clause.namedChildren.find((c) => c.type === 'name');
2399
+ if (qn)
2400
+ this.pushPhpUseRef((0, tree_sitter_helpers_1.getNodeText)(qn, this.source), fromNodeId, node);
2401
+ }
2402
+ /**
2403
+ * Ruby `require`/`require_relative` → an `imports` ref to the required FILE.
2404
+ * `require "sidekiq/fetch"` is load-path-relative (matched by file-path suffix
2405
+ * via {@link matchByFilePath}); `require_relative "../foo"` is resolved against
2406
+ * this file's directory. Bare gem/stdlib requires (`require "json"`, no slash)
2407
+ * are skipped — they're external. The path form (a `/` + `.rb`) makes the ref
2408
+ * resolve to the file node, so a file pulled in only by `require` — not by a
2409
+ * resolved constant/call — still records a cross-file dependency.
2410
+ */
2411
+ emitRubyRequireRefs(node, fromNodeId) {
2412
+ const method = node.namedChildren.find((c) => c.type === 'identifier');
2413
+ const mname = method ? (0, tree_sitter_helpers_1.getNodeText)(method, this.source) : '';
2414
+ if (mname !== 'require' && mname !== 'require_relative')
2415
+ return;
2416
+ const argList = node.namedChildren.find((c) => c.type === 'argument_list');
2417
+ const str = argList?.namedChildren.find((c) => c.type === 'string');
2418
+ const content = str?.namedChildren.find((c) => c.type === 'string_content');
2419
+ if (!content)
2420
+ return;
2421
+ const req = (0, tree_sitter_helpers_1.getNodeText)(content, this.source).trim();
2422
+ if (!req)
2423
+ return;
2424
+ let refPath;
2425
+ if (mname === 'require_relative') {
2426
+ const slash = this.filePath.lastIndexOf('/');
2427
+ const dir = slash >= 0 ? this.filePath.slice(0, slash) : '';
2428
+ refPath = path.posix.normalize(dir ? `${dir}/${req}` : req);
2429
+ }
2430
+ else {
2431
+ refPath = req; // load-path require — suffix-matched against the file path
2432
+ }
2433
+ if (!refPath.includes('/'))
2434
+ return; // bare gem/stdlib require — external
2435
+ if (!refPath.endsWith('.rb'))
2436
+ refPath += '.rb';
2437
+ this.unresolvedReferences.push({
2438
+ fromNodeId,
2439
+ referenceName: refPath,
2440
+ referenceKind: 'imports',
2441
+ line: node.startPosition.row + 1,
2442
+ column: node.startPosition.column,
2443
+ });
2444
+ }
2445
+ /** Convert a PHP FQN `Foo\Bar\Baz` to the stored `Foo\Bar::Baz` and emit an `imports` ref. */
2446
+ pushPhpUseRef(fqn, fromNodeId, node) {
2447
+ const clean = fqn.replace(/^\\/, '');
2448
+ const lastSep = clean.lastIndexOf('\\');
2449
+ if (lastSep < 0)
2450
+ return; // global-namespace class — already matches by simple name
2451
+ this.unresolvedReferences.push({
2452
+ fromNodeId,
2453
+ referenceName: `${clean.slice(0, lastSep)}::${clean.slice(lastSep + 1)}`,
2454
+ referenceKind: 'imports',
2455
+ line: node.startPosition.row + 1,
2456
+ column: node.startPosition.column,
2457
+ });
2458
+ }
2459
+ /**
2460
+ * Emit one `imports` reference per name imported in a Python
2461
+ * `from module import A, B as C` statement, attributed to the file node — so
2462
+ * the resolver links each imported name to the module that DEFINES it.
2463
+ *
2464
+ * Same recall gap as TS: extraction only emitted references for calls,
2465
+ * instantiations, and inheritance, so a name imported and then used in a
2466
+ * non-call position (a list/dict literal, a default argument, a decorator
2467
+ * target, or simply re-exported through an `__init__.py` barrel) produced no
2468
+ * cross-file edge — the providing module showed a false "0 dependents". Links
2469
+ * the LOCAL name (alias when present, since that's what the resolver's import
2470
+ * mapping keys on); `from module import *` has no names to attribute.
2471
+ */
2472
+ emitPyFromImportRefs(node, fromNodeId) {
2473
+ const moduleNameNode = (0, tree_sitter_helpers_1.getChildByField)(node, 'module_name');
2474
+ for (const child of node.namedChildren) {
2475
+ // Skip the `from <module>` part itself and `import *`.
2476
+ if (moduleNameNode &&
2477
+ child.startIndex === moduleNameNode.startIndex &&
2478
+ child.endIndex === moduleNameNode.endIndex)
2479
+ continue;
2480
+ if (child.type === 'wildcard_import')
2481
+ continue;
2482
+ let nameNode = null;
2483
+ if (child.type === 'aliased_import') {
2484
+ nameNode = (0, tree_sitter_helpers_1.getChildByField)(child, 'alias') ?? (0, tree_sitter_helpers_1.getChildByField)(child, 'name') ?? child.namedChild(0);
2485
+ }
2486
+ else if (child.type === 'dotted_name') {
2487
+ nameNode = child;
2488
+ }
2489
+ if (!nameNode)
2490
+ continue;
2491
+ const raw = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
2492
+ // Imported names are simple identifiers; defensively take the last segment.
2493
+ const local = raw.includes('.') ? raw.split('.').pop() : raw;
2494
+ if (!local)
2495
+ continue;
2496
+ this.unresolvedReferences.push({
2497
+ fromNodeId,
2498
+ referenceName: local,
2499
+ referenceKind: 'imports',
2500
+ line: nameNode.startPosition.row + 1,
2501
+ column: nameNode.startPosition.column,
2502
+ });
2503
+ }
2504
+ }
1639
2505
  /**
1640
2506
  * Extract a function call
1641
2507
  */
@@ -1659,6 +2525,57 @@ class TreeSitterExtractor {
1659
2525
  // single-dot receiver regex fails. Pull out the immediate field after `this.`
1660
2526
  // so the receiver is the field name (`userbo`), which the resolver can then
1661
2527
  // look up in the enclosing class's field declarations.
2528
+ // PHP static-factory fluent chain: `Cls::for($x)->method()` — the receiver
2529
+ // is itself a static call, so resolution must infer the method's class
2530
+ // from what `Cls::for` RETURNS (its `: self` / `: static` / `: Type`),
2531
+ // #608 (mirrors the C++ chain fix in #645). Encode `<Cls::factory>().<method>`;
2532
+ // the `().` marker lets the PHP resolver split it. The receiver text
2533
+ // (`Cls::for('x')`) carries the args, so without this it degrades to an
2534
+ // unresolvable string and the call edge is dropped.
2535
+ if (methodName && this.language === 'php' && objectField.type === 'scoped_call_expression') {
2536
+ const innerScope = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'scope');
2537
+ const innerName = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'name');
2538
+ if (innerScope && innerName) {
2539
+ calleeName = `${(0, tree_sitter_helpers_1.getNodeText)(innerScope, this.source)}::${(0, tree_sitter_helpers_1.getNodeText)(innerName, this.source)}().${methodName}`;
2540
+ }
2541
+ else {
2542
+ calleeName = methodName;
2543
+ }
2544
+ if (calleeName) {
2545
+ this.unresolvedReferences.push({
2546
+ fromNodeId: callerId,
2547
+ referenceName: calleeName,
2548
+ referenceKind: 'calls',
2549
+ line: node.startPosition.row + 1,
2550
+ column: node.startPosition.column,
2551
+ });
2552
+ }
2553
+ return;
2554
+ }
2555
+ // Java static-factory / fluent chain: `Foo.getInstance().bar()` — the
2556
+ // receiver is itself a method call, so resolution must infer bar's class
2557
+ // from what `Foo.getInstance` RETURNS (its declared return type), the
2558
+ // #645/#608 mechanism. Encode `<inner-receiver>.<inner-method>().<method>`;
2559
+ // the `().` marker lets the Java chain resolver split it, and normalizing to
2560
+ // empty parens drops any factory args (`Foo.create(cfg).bar()`) that would
2561
+ // otherwise leave a `(cfg)` in the receiver text and break the split.
2562
+ if (methodName &&
2563
+ this.language === 'java' &&
2564
+ objectField.type === 'method_invocation') {
2565
+ const innerObj = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'object');
2566
+ const innerName = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'name');
2567
+ if (innerObj && innerName) {
2568
+ calleeName = `${(0, tree_sitter_helpers_1.getNodeText)(innerObj, this.source)}.${(0, tree_sitter_helpers_1.getNodeText)(innerName, this.source)}().${methodName}`;
2569
+ this.unresolvedReferences.push({
2570
+ fromNodeId: callerId,
2571
+ referenceName: calleeName,
2572
+ referenceKind: 'calls',
2573
+ line: node.startPosition.row + 1,
2574
+ column: node.startPosition.column,
2575
+ });
2576
+ return;
2577
+ }
2578
+ }
1662
2579
  let receiverName;
1663
2580
  if (objectField.type === 'field_access') {
1664
2581
  const inner = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'object');
@@ -1704,15 +2621,77 @@ class TreeSitterExtractor {
1704
2621
  }
1705
2622
  }
1706
2623
  if (methodKeywords.length > 0) {
1707
- const methodName = methodKeywords.length === 1
1708
- ? methodKeywords[0]
1709
- : methodKeywords.map((k) => `${k}:`).join('');
2624
+ // A selector keyword takes a `:` when it has an argument. A SINGLE
2625
+ // keyword can be unary (`[c reset]` → `reset`) OR take one argument
2626
+ // (`[c storeImage:k]` `storeImage:`) — distinguished by whether the
2627
+ // message has a `:` token. Without this, every single-argument message
2628
+ // (the most common form: `addObject:`, `storeImage:`, …) was named
2629
+ // without the colon and never matched its `storeImage:` method.
2630
+ let hasColon = false;
2631
+ for (let i = 0; i < node.childCount; i++) {
2632
+ if (node.child(i)?.type === ':') {
2633
+ hasColon = true;
2634
+ break;
2635
+ }
2636
+ }
2637
+ const methodName = hasColon
2638
+ ? methodKeywords.map((k) => `${k}:`).join('')
2639
+ : methodKeywords[0];
1710
2640
  const receiverField = (0, tree_sitter_helpers_1.getChildByField)(node, 'receiver');
1711
2641
  const SKIP_RECEIVERS = new Set(['self', 'super']);
1712
2642
  if (receiverField && receiverField.type !== 'message_expression') {
1713
2643
  const receiverName = (0, tree_sitter_helpers_1.getNodeText)(receiverField, this.source);
1714
2644
  if (receiverName && !SKIP_RECEIVERS.has(receiverName)) {
1715
2645
  calleeName = `${receiverName}.${methodName}`;
2646
+ // A CLASS-message receiver (`[SDImageCache alloc]`,
2647
+ // `[SDImageCache sharedCache]`) is a capitalized class name. The
2648
+ // call resolves the method (`alloc`/`sharedCache`), but the CLASS
2649
+ // itself — whose @interface lives in the header — would otherwise
2650
+ // never be referenced. Emit a `references` edge to it so a class
2651
+ // used only via class messages (alloc/init, singletons, factories)
2652
+ // and its header record a dependent.
2653
+ if (/^[A-Z][A-Za-z0-9_]*$/.test(receiverName)) {
2654
+ this.unresolvedReferences.push({
2655
+ fromNodeId: callerId,
2656
+ referenceName: receiverName,
2657
+ referenceKind: 'references',
2658
+ line: receiverField.startPosition.row + 1,
2659
+ column: receiverField.startPosition.column,
2660
+ });
2661
+ }
2662
+ }
2663
+ else {
2664
+ calleeName = methodName;
2665
+ }
2666
+ }
2667
+ else if (receiverField && receiverField.type === 'message_expression' && /^\w+$/.test(methodName)) {
2668
+ // Chained message send `[[Foo create] doIt]` — the receiver is itself a
2669
+ // class message. Recover the inner `Class.selector` and encode
2670
+ // `Class.selector().doIt` so resolution infers doIt's class from what
2671
+ // `Class.selector` RETURNS (#645/#608). Only a CLASS-factory chain
2672
+ // (capitalized inner receiver); a unary outer selector is required
2673
+ // because the chain resolver's method part is `\w+` (no `:`). An
2674
+ // instance chain (`[[obj foo] bar]`, lowercase inner) stays bare.
2675
+ const innerRecv = (0, tree_sitter_helpers_1.getChildByField)(receiverField, 'receiver');
2676
+ const innerRecvName = innerRecv ? (0, tree_sitter_helpers_1.getNodeText)(innerRecv, this.source) : '';
2677
+ if (innerRecv?.type === 'identifier' && /^[A-Z]/.test(innerRecvName)) {
2678
+ const innerKw = [];
2679
+ for (let i = 0; i < receiverField.namedChildCount; i++) {
2680
+ if (receiverField.fieldNameForNamedChild(i) === 'method') {
2681
+ const kw = receiverField.namedChild(i);
2682
+ if (kw)
2683
+ innerKw.push((0, tree_sitter_helpers_1.getNodeText)(kw, this.source));
2684
+ }
2685
+ }
2686
+ let innerColon = false;
2687
+ for (let i = 0; i < receiverField.childCount; i++) {
2688
+ if (receiverField.child(i)?.type === ':') {
2689
+ innerColon = true;
2690
+ break;
2691
+ }
2692
+ }
2693
+ const innerSelector = innerColon ? innerKw.map((k) => `${k}:`).join('') : innerKw[0];
2694
+ calleeName = innerSelector ? `${innerRecvName}.${innerSelector}().${methodName}` : methodName;
1716
2695
  }
1717
2696
  else {
1718
2697
  calleeName = methodName;
@@ -1762,6 +2741,67 @@ class TreeSitterExtractor {
1762
2741
  calleeName = methodName;
1763
2742
  }
1764
2743
  }
2744
+ else if ((this.language === 'cpp' ||
2745
+ this.language === 'c' ||
2746
+ this.language === 'kotlin' ||
2747
+ this.language === 'swift' ||
2748
+ this.language === 'rust' ||
2749
+ this.language === 'go' ||
2750
+ this.language === 'scala') &&
2751
+ receiver &&
2752
+ receiver.type === 'call_expression') {
2753
+ // Receiver that is itself a call — `Foo::instance().bar()`,
2754
+ // `openSession()->run()`, `mgr.view().render()` (C/C++),
2755
+ // `Foo.getInstance().bar()` (Kotlin) / `Foo.make().draw()` (Swift),
2756
+ // `Foo::new().bar()` (Rust), or `New().Method()` (Go). Keep the inner
2757
+ // call so resolution can infer bar()'s class from what the inner call
2758
+ // RETURNS (#645/#608). Encode as `<innerCallee>().<method>`; the `().`
2759
+ // marker never appears in an ordinary ref, so the resolver can detect
2760
+ // and split it. Other languages keep the bare-name behavior below.
2761
+ let innerCallee;
2762
+ let reencode;
2763
+ if (this.language === 'kotlin' || this.language === 'swift') {
2764
+ // tree-sitter-kotlin/swift expose the inner callee as the
2765
+ // call_expression's first named child (a navigation_expression
2766
+ // `Foo.getInstance`, or a bare identifier for a free/constructor call).
2767
+ const innerNav = receiver.namedChild(0);
2768
+ innerCallee = innerNav ? (0, tree_sitter_helpers_1.getNodeText)(innerNav, this.source).replace(/\s+/g, '') : '';
2769
+ // Only re-encode a CLASS / companion-factory / constructor chain,
2770
+ // whose receiver chain starts with a capitalized type
2771
+ // (`Foo.getInstance().bar()`, `Foo().bar()`). An instance chain
2772
+ // (`list.filter{}.map{}`) has a lowercase receiver whose type we
2773
+ // can't recover here — re-encoding it would only drop the edge (no
2774
+ // chain resolution, no bare-name fallback), regressing recall in
2775
+ // fluent codebases. Leave those to the bare-name path.
2776
+ reencode = /^[A-Z]/.test(innerCallee);
2777
+ }
2778
+ else {
2779
+ const innerFn = (0, tree_sitter_helpers_1.getChildByField)(receiver, 'function');
2780
+ innerCallee = innerFn
2781
+ ? (0, tree_sitter_helpers_1.getNodeText)(innerFn, this.source).replace(/->/g, '.').replace(/\s+/g, '')
2782
+ : '';
2783
+ // Rust: only re-encode an associated-function chain
2784
+ // (`Foo::new().bar()`), whose inner callee is a path/`scoped_identifier`.
2785
+ // Go: only a bare package-level factory chain (`New().Method()`),
2786
+ // whose inner callee is an `identifier`. An instance chain
2787
+ // (`x.foo().bar()` Rust, `obj.Method().Other()` Go) keeps bare-name —
2788
+ // the resolver can't recover a variable's type, so re-encoding would
2789
+ // only drop the edge. C/C++ re-encode any inner.
2790
+ if (this.language === 'rust')
2791
+ reencode = innerFn?.type === 'scoped_identifier';
2792
+ else if (this.language === 'go')
2793
+ reencode = innerFn?.type === 'identifier';
2794
+ // Scala: only a companion-factory / case-class-apply chain whose
2795
+ // receiver chain starts with a capitalized type (`Foo.create().bar()`,
2796
+ // `Foo(args).bar()`). An instance chain (`list.map().filter()`) has a
2797
+ // lowercase receiver whose type we can't recover — leave it bare.
2798
+ else if (this.language === 'scala')
2799
+ reencode = /^[A-Z]/.test(innerCallee);
2800
+ else
2801
+ reencode = !!innerCallee;
2802
+ }
2803
+ calleeName = reencode ? `${innerCallee}().${methodName}` : methodName;
2804
+ }
1765
2805
  else {
1766
2806
  calleeName = methodName;
1767
2807
  }
@@ -1771,11 +2811,39 @@ class TreeSitterExtractor {
1771
2811
  // Scoped call: Module::function()
1772
2812
  calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
1773
2813
  }
2814
+ else if (this.language === 'csharp' && func.type === 'member_access_expression') {
2815
+ // C# member call `recv.Method(...)`. When the receiver is itself a call
2816
+ // — a chained factory `Foo.Create(args).Bar()` — encode `inner().Bar`
2817
+ // with normalized empty parens so resolution can infer Bar's class from
2818
+ // what `Foo.Create` RETURNS (#645/#608). A non-call receiver keeps the
2819
+ // full member-access text (the existing `recv.Method` behavior).
2820
+ const recv = (0, tree_sitter_helpers_1.getChildByField)(func, 'expression');
2821
+ const nameNode = (0, tree_sitter_helpers_1.getChildByField)(func, 'name');
2822
+ const methodName = nameNode ? (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source) : '';
2823
+ if (recv && recv.type === 'invocation_expression' && methodName) {
2824
+ const innerFunc = (0, tree_sitter_helpers_1.getChildByField)(recv, 'function');
2825
+ const innerCallee = innerFunc ? (0, tree_sitter_helpers_1.getNodeText)(innerFunc, this.source).replace(/\s+/g, '') : '';
2826
+ calleeName = innerCallee ? `${innerCallee}().${methodName}` : methodName;
2827
+ }
2828
+ else {
2829
+ calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
2830
+ }
2831
+ }
1774
2832
  else {
1775
2833
  calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
1776
2834
  }
1777
2835
  }
1778
2836
  }
2837
+ // Parenthesized type conversions — Go `(*T)(x)` / `(T)(x)` (and a
2838
+ // parenthesized callee generally) parse as a call whose "function" is a
2839
+ // parenthesized type/expression, so the callee text is the un-resolvable
2840
+ // literal `(*T)`. Normalize to the inner name so it resolves to `T` (a real
2841
+ // dependency on the converted-to type) instead of dropping on the floor.
2842
+ if (calleeName) {
2843
+ const conv = calleeName.match(/^\(\s*\*?\s*([A-Za-z_][\w.]*)\s*\)$/);
2844
+ if (conv && conv[1])
2845
+ calleeName = conv[1];
2846
+ }
1779
2847
  if (calleeName) {
1780
2848
  this.unresolvedReferences.push({
1781
2849
  fromNodeId: callerId,
@@ -1809,6 +2877,46 @@ class TreeSitterExtractor {
1809
2877
  node.namedChild(0);
1810
2878
  if (!ctor)
1811
2879
  return;
2880
+ // Go composite literals: `Widget{...}` (same package) and `pkga.Widget{...}`
2881
+ // (cross-package). Only a directly-named struct type is a meaningful
2882
+ // instantiation target — skip slice/map/array literals (`[]T{}`,
2883
+ // `map[K]V{}`) whose `type` field is a composite type, not a named type.
2884
+ // Unlike `new ns.Foo()`, KEEP the package qualifier (`pkga.Widget`) so the
2885
+ // Go cross-package resolver can disambiguate it to the right package's type.
2886
+ if (node.type === 'composite_literal') {
2887
+ if (ctor.type !== 'type_identifier' && ctor.type !== 'qualified_type')
2888
+ return;
2889
+ let goType = (0, tree_sitter_helpers_1.getNodeText)(ctor, this.source).trim();
2890
+ const brIdx = goType.indexOf('['); // strip Go generic args: `Box[T]{}` -> `Box`
2891
+ if (brIdx > 0)
2892
+ goType = goType.slice(0, brIdx).trim();
2893
+ if (goType) {
2894
+ this.unresolvedReferences.push({
2895
+ fromNodeId: fromId,
2896
+ referenceName: goType,
2897
+ referenceKind: 'instantiates',
2898
+ line: node.startPosition.row + 1,
2899
+ column: node.startPosition.column,
2900
+ });
2901
+ }
2902
+ return;
2903
+ }
2904
+ // Scala: `new Monoid[Int] { ... }` — the constructor is a `generic_type`
2905
+ // (or qualified `stable_type_identifier`) using `[...]` type args, which the
2906
+ // generic `<...>` strip below misses. Unwrap to the base type name.
2907
+ if (node.type === 'instance_expression') {
2908
+ const name = scalaBaseTypeName(ctor, this.source);
2909
+ if (name) {
2910
+ this.unresolvedReferences.push({
2911
+ fromNodeId: fromId,
2912
+ referenceName: name,
2913
+ referenceKind: 'instantiates',
2914
+ line: node.startPosition.row + 1,
2915
+ column: node.startPosition.column,
2916
+ });
2917
+ }
2918
+ return;
2919
+ }
1812
2920
  let className = (0, tree_sitter_helpers_1.getNodeText)(ctor, this.source);
1813
2921
  // Strip type-argument suffix first: `new Map<K, V>()` would
1814
2922
  // otherwise produce className 'Map<K, V>' (the constructor
@@ -1834,6 +2942,75 @@ class TreeSitterExtractor {
1834
2942
  });
1835
2943
  }
1836
2944
  }
2945
+ /**
2946
+ * Static-member / value-read pass. A type/enum/class used only via a member
2947
+ * VALUE — `Enum.value`, `Type.CONST`, `Colors.red`, `Foo::BAR` — recorded no
2948
+ * edge, because the body walker only handled CALLS (`Type.method()`). So a
2949
+ * type referenced only by an enum value or a static field looked like nothing
2950
+ * depended on it (the residual frontier across Dart/Java/C#/Swift/Kotlin/PHP).
2951
+ * Emit a `references` edge to the capitalized receiver. Gated to languages
2952
+ * where types are Capitalized by convention, and skipped when the access is a
2953
+ * call's callee (the call extractor already links the method).
2954
+ */
2955
+ extractStaticMemberRef(node) {
2956
+ if (!STATIC_MEMBER_LANGS.has(this.language))
2957
+ return;
2958
+ if (this.nodeStack.length === 0)
2959
+ return;
2960
+ const ownerId = this.nodeStack[this.nodeStack.length - 1];
2961
+ if (!ownerId)
2962
+ return;
2963
+ // Dart structures member access as an `identifier` + a sibling `selector`,
2964
+ // not a single node. A value-read selector (no `argument_part`) whose
2965
+ // previous sibling is a capitalized identifier is `Enum.value`.
2966
+ if (this.language === 'dart') {
2967
+ if (node.type !== 'selector')
2968
+ return;
2969
+ if (node.namedChildren.some((c) => c.type === 'argument_part'))
2970
+ return;
2971
+ const prev = node.previousNamedSibling;
2972
+ if (prev?.type === 'identifier' && /^[A-Z][A-Za-z0-9_]*$/.test(prev.text)) {
2973
+ this.pushStaticMemberRef(prev.text, ownerId, prev);
2974
+ }
2975
+ return;
2976
+ }
2977
+ if (!MEMBER_ACCESS_TYPES.has(node.type))
2978
+ return;
2979
+ // Skip `Type.method()` — the access is the callee of a call, already linked.
2980
+ const parent = node.parent;
2981
+ if (parent && this.extractor.callTypes.includes(parent.type)) {
2982
+ const callee = (0, tree_sitter_helpers_1.getChildByField)(parent, 'function') ??
2983
+ (0, tree_sitter_helpers_1.getChildByField)(parent, 'method') ??
2984
+ parent.namedChild(0);
2985
+ if (callee && callee.startIndex === node.startIndex)
2986
+ return;
2987
+ }
2988
+ // The receiver must be a SIMPLE capitalized identifier — `Type.X`, not the
2989
+ // nested `a.B.c` (whose own head member-access is visited separately) nor a
2990
+ // lowercase `obj.field` / `pkg.func`.
2991
+ const recv = (0, tree_sitter_helpers_1.getChildByField)(node, 'object') ??
2992
+ (0, tree_sitter_helpers_1.getChildByField)(node, 'expression') ??
2993
+ (0, tree_sitter_helpers_1.getChildByField)(node, 'scope') ??
2994
+ node.namedChild(0);
2995
+ if (!recv)
2996
+ return;
2997
+ const t = recv.type;
2998
+ if (t === 'identifier' || t === 'type_identifier' || t === 'simple_identifier' ||
2999
+ t === 'name' || t === 'scoped_type_identifier') {
3000
+ const text = (0, tree_sitter_helpers_1.getNodeText)(recv, this.source);
3001
+ if (/^[A-Z][A-Za-z0-9_]*$/.test(text))
3002
+ this.pushStaticMemberRef(text, ownerId, recv);
3003
+ }
3004
+ }
3005
+ pushStaticMemberRef(name, ownerId, node) {
3006
+ this.unresolvedReferences.push({
3007
+ fromNodeId: ownerId,
3008
+ referenceName: name,
3009
+ referenceKind: 'references',
3010
+ line: node.startPosition.row + 1,
3011
+ column: node.startPosition.column,
3012
+ });
3013
+ }
1837
3014
  /**
1838
3015
  * Find a `class_body` child of an `object_creation_expression` — the
1839
3016
  * marker for an anonymous class (`new T() { ... }`). Returns the body
@@ -1924,11 +3101,13 @@ class TreeSitterExtractor {
1924
3101
  if (!n)
1925
3102
  return;
1926
3103
  // `marker_annotation` is Java's grammar for arg-less annotations
1927
- // (`@Override`, `@Deprecated`); without including it, every
1928
- // such Java annotation would be silently skipped.
3104
+ // (`@Override`, `@Deprecated`); `attribute` is Swift's grammar for
3105
+ // attributes and PROPERTY WRAPPERS (`@objc`, `@Argument`, `@Published`,
3106
+ // `@State`). Without these, those usages would be silently skipped.
1929
3107
  if (n.type !== 'decorator' &&
1930
3108
  n.type !== 'annotation' &&
1931
- n.type !== 'marker_annotation') {
3109
+ n.type !== 'marker_annotation' &&
3110
+ n.type !== 'attribute') {
1932
3111
  return;
1933
3112
  }
1934
3113
  // Find the leading identifier: skip the `@` punct, unwrap
@@ -1948,7 +3127,9 @@ class TreeSitterExtractor {
1948
3127
  if (child.type === 'identifier' ||
1949
3128
  child.type === 'member_expression' ||
1950
3129
  child.type === 'scoped_identifier' ||
1951
- child.type === 'navigation_expression') {
3130
+ child.type === 'navigation_expression' ||
3131
+ child.type === 'user_type' || // swift attribute → user_type (`@Argument`)
3132
+ child.type === 'type_identifier') {
1952
3133
  target = child;
1953
3134
  break;
1954
3135
  }
@@ -1956,9 +3137,13 @@ class TreeSitterExtractor {
1956
3137
  if (!target)
1957
3138
  return;
1958
3139
  let name = (0, tree_sitter_helpers_1.getNodeText)(target, this.source);
3140
+ const lt = name.indexOf('<'); // strip generic args: `@Argument<T>` → `Argument`
3141
+ if (lt > 0)
3142
+ name = name.slice(0, lt);
1959
3143
  const lastDot = Math.max(name.lastIndexOf('.'), name.lastIndexOf('::'));
1960
3144
  if (lastDot >= 0)
1961
3145
  name = name.slice(lastDot + 1).replace(/^[:.]/, '');
3146
+ name = name.trim();
1962
3147
  if (!name)
1963
3148
  return;
1964
3149
  this.unresolvedReferences.push({
@@ -1972,7 +3157,17 @@ class TreeSitterExtractor {
1972
3157
  // 1. Decorators that are direct children of the declaration
1973
3158
  // (method/property style, also some grammars for class).
1974
3159
  for (let i = 0; i < declNode.namedChildCount; i++) {
1975
- consider(declNode.namedChild(i));
3160
+ const child = declNode.namedChild(i);
3161
+ consider(child);
3162
+ // Java/Kotlin/C# put annotations INSIDE a `modifiers` node
3163
+ // (`@MyAnno public class X` → class_declaration → modifiers → annotation),
3164
+ // so descend into it — otherwise every annotation usage is silently
3165
+ // dropped and annotation types show zero dependents.
3166
+ if (child && child.type === 'modifiers') {
3167
+ for (let j = 0; j < child.namedChildCount; j++) {
3168
+ consider(child.namedChild(j));
3169
+ }
3170
+ }
1976
3171
  }
1977
3172
  // 2. Decorators that are PRECEDING siblings of the declaration
1978
3173
  // inside the parent's children (TypeScript class style).
@@ -2020,11 +3215,80 @@ class TreeSitterExtractor {
2020
3215
  * tree-sitter to interpret the namespace block as a function_definition,
2021
3216
  * hiding real class/struct/enum nodes inside the "function body".
2022
3217
  */
3218
+ /**
3219
+ * Rocket route-registration macros — `routes![a::b::handler, c::d::other]`
3220
+ * and `catchers![not_found]`. Tree-sitter leaves a macro body as a flat
3221
+ * `token_tree` of raw tokens (`identifier`, `::`, `,`), so the handler paths
3222
+ * are never seen as references and each handler fn looks like it has no caller
3223
+ * — it's mounted by Rocket at runtime, not called by in-repo code, so its file
3224
+ * shows 0 dependents. Walk the token tree, reconstruct each comma-separated
3225
+ * path, and emit a `references` edge; the Rust path resolver
3226
+ * (`resolveRustPathReference`) then links it to the handler fn. The handler
3227
+ * names are explicit in source, so this is precise static extraction, not a
3228
+ * heuristic — no false edges (resolution still validates each path).
3229
+ */
3230
+ extractRustRouteMacro(node) {
3231
+ if (this.language !== 'rust')
3232
+ return;
3233
+ const macroName = node.namedChild(0);
3234
+ if (!macroName)
3235
+ return;
3236
+ const name = (0, tree_sitter_helpers_1.getNodeText)(macroName, this.source);
3237
+ if (name !== 'routes' && name !== 'catchers')
3238
+ return;
3239
+ const tokenTree = node.namedChildren.find((c) => c.type === 'token_tree');
3240
+ if (!tokenTree)
3241
+ return;
3242
+ const fromId = this.nodeStack[this.nodeStack.length - 1];
3243
+ if (!fromId)
3244
+ return;
3245
+ // The token tree is a flat stream: `[ id :: id :: id , id … ]`. Group runs
3246
+ // of `identifier` tokens (the `::` joiners are anonymous) into one path; a
3247
+ // `,` (or the closing `]`) ends a path.
3248
+ let parts = [];
3249
+ let line = 0;
3250
+ let column = 0;
3251
+ const flush = () => {
3252
+ if (parts.length > 0) {
3253
+ this.unresolvedReferences.push({
3254
+ fromNodeId: fromId,
3255
+ referenceName: parts.join('::'),
3256
+ referenceKind: 'references',
3257
+ line,
3258
+ column,
3259
+ });
3260
+ parts = [];
3261
+ }
3262
+ };
3263
+ for (let i = 0; i < tokenTree.childCount; i++) {
3264
+ const t = tokenTree.child(i);
3265
+ if (!t)
3266
+ continue;
3267
+ if (t.type === 'identifier') {
3268
+ if (parts.length === 0) {
3269
+ line = t.startPosition.row + 1;
3270
+ column = t.startPosition.column;
3271
+ }
3272
+ parts.push((0, tree_sitter_helpers_1.getNodeText)(t, this.source));
3273
+ }
3274
+ else if (t.type === ',') {
3275
+ flush();
3276
+ }
3277
+ }
3278
+ flush();
3279
+ }
2023
3280
  visitFunctionBody(body, _functionId) {
2024
3281
  if (!this.extractor)
2025
3282
  return;
2026
3283
  const visitForCallsAndStructure = (node) => {
2027
3284
  const nodeType = node.type;
3285
+ // Function-as-value capture (#756) — function bodies are walked here,
3286
+ // not in visitNode, so the capture hook must fire in both walkers.
3287
+ this.maybeCaptureFnRefs(node, nodeType);
3288
+ // Rocket route-registration macros (`routes![…]` / `catchers![…]`): the
3289
+ // handler paths live in a raw token tree the call walker can't see.
3290
+ if (nodeType === 'macro_invocation')
3291
+ this.extractRustRouteMacro(node);
2028
3292
  if (this.extractor.callTypes.includes(nodeType)) {
2029
3293
  this.extractCall(node);
2030
3294
  }
@@ -2058,6 +3322,24 @@ class TreeSitterExtractor {
2058
3322
  }
2059
3323
  }
2060
3324
  }
3325
+ // Static-member / value-read: `Enum.value`, `Type.CONST`, `Foo::BAR`.
3326
+ this.extractStaticMemberRef(node);
3327
+ // Local variable type annotations inside a body — `const items: Foo[] = []`,
3328
+ // `const x: SomeType = svc.load()`. We deliberately do NOT create nodes for
3329
+ // locals (that would explode the graph — the data-flow frontier we leave
3330
+ // uncovered), but the TYPE a local is annotated with is a real dependency of
3331
+ // the enclosing function, so attribute a `references` edge to it. Without
3332
+ // this, a function that uses a type ONLY in its body (very common — e.g. a
3333
+ // resolver building `const nodes: Node[] = []`) produced no edge to that
3334
+ // type, so impact / `affected` missed the dependency entirely. We fall
3335
+ // through to the default recursion below so the initializer's calls (and any
3336
+ // nested declarators) are still walked.
3337
+ if (nodeType === 'variable_declarator' &&
3338
+ this.TYPE_ANNOTATION_LANGUAGES.has(this.language)) {
3339
+ const ownerId = this.nodeStack[this.nodeStack.length - 1];
3340
+ if (ownerId)
3341
+ this.extractVariableTypeAnnotation(node, ownerId);
3342
+ }
2061
3343
  // Nested NAMED functions inside a body — function declarations and named
2062
3344
  // function expressions like `.on('mount', function onmount(){})` — become
2063
3345
  // their own nodes so the graph can link to them (callback handlers, local
@@ -2159,6 +3441,62 @@ class TreeSitterExtractor {
2159
3441
  child.type === 'base_clause' || // PHP class extends
2160
3442
  child.type === 'extends_interfaces' // Java interface extends
2161
3443
  ) {
3444
+ // Scala: `extends A[X] with B with C` packs EVERY supertype into the
3445
+ // one extends_clause (separated by `with`), each a `generic_type` /
3446
+ // `type_identifier` / `stable_type_identifier`. The generic path below
3447
+ // takes only namedChild(0) and keeps the full text (`A[X]`), so a
3448
+ // parameterized supertype — every typeclass in cats/algebra — never
3449
+ // matched and `with`-mixed traits past the first were dropped. Iterate
3450
+ // all supertypes and unwrap each to its base type name.
3451
+ if (this.language === 'scala') {
3452
+ for (const target of child.namedChildren) {
3453
+ const name = scalaBaseTypeName(target, this.source);
3454
+ if (name) {
3455
+ this.unresolvedReferences.push({
3456
+ fromNodeId: classId,
3457
+ referenceName: name,
3458
+ referenceKind: 'extends',
3459
+ line: target.startPosition.row + 1,
3460
+ column: target.startPosition.column,
3461
+ });
3462
+ }
3463
+ }
3464
+ continue;
3465
+ }
3466
+ // Dart: `class C extends Base with M1, M2` — the `superclass` node holds
3467
+ // the extends type as a direct `type_identifier` AND a `mixins` child
3468
+ // listing the `with` mixins (and `class C with M` has ONLY mixins, no
3469
+ // extends type). The generic `namedChild(0)` path would read the
3470
+ // `mixins` node itself as the superclass and drop every mixin — yet
3471
+ // mixins are Dart's core composition mechanism (Flutter is built on
3472
+ // them). Emit `extends` for the base and `implements` for each mixin.
3473
+ if (this.language === 'dart' && child.type === 'superclass') {
3474
+ for (const t of child.namedChildren) {
3475
+ if (t.type === 'mixins') {
3476
+ for (const m of t.namedChildren) {
3477
+ if (m.type === 'type_identifier') {
3478
+ this.unresolvedReferences.push({
3479
+ fromNodeId: classId,
3480
+ referenceName: (0, tree_sitter_helpers_1.getNodeText)(m, this.source),
3481
+ referenceKind: 'implements',
3482
+ line: m.startPosition.row + 1,
3483
+ column: m.startPosition.column,
3484
+ });
3485
+ }
3486
+ }
3487
+ }
3488
+ else if (t.type === 'type_identifier') {
3489
+ this.unresolvedReferences.push({
3490
+ fromNodeId: classId,
3491
+ referenceName: (0, tree_sitter_helpers_1.getNodeText)(t, this.source),
3492
+ referenceKind: 'extends',
3493
+ line: t.startPosition.row + 1,
3494
+ column: t.startPosition.column,
3495
+ });
3496
+ }
3497
+ }
3498
+ continue;
3499
+ }
2162
3500
  // Extract parent class/interface names
2163
3501
  // Java uses type_list wrapper: superclass -> type_identifier, extends_interfaces -> type_list -> type_identifier
2164
3502
  const typeList = child.namedChildren.find((c) => c.type === 'type_list');
@@ -2438,7 +3776,15 @@ class TreeSitterExtractor {
2438
3776
  * Languages that support type annotations (TypeScript, etc.)
2439
3777
  */
2440
3778
  TYPE_ANNOTATION_LANGUAGES = new Set([
2441
- 'typescript', 'tsx', 'dart', 'kotlin', 'swift', 'rust', 'go', 'java', 'csharp',
3779
+ 'typescript', 'tsx', 'dart', 'kotlin', 'swift', 'rust', 'go', 'java', 'csharp', 'scala', 'php',
3780
+ ]);
3781
+ /**
3782
+ * PHP pseudo-types and `self`/`static`/`parent` that aren't project symbols.
3783
+ * (Scalar primitives parse as `primitive_type` and are skipped structurally.)
3784
+ */
3785
+ PHP_PSEUDO_TYPES = new Set([
3786
+ 'self', 'static', 'parent', 'mixed', 'object', 'iterable', 'callable', 'void',
3787
+ 'null', 'false', 'true', 'never', 'array', 'int', 'float', 'string', 'bool',
2442
3788
  ]);
2443
3789
  /**
2444
3790
  * Built-in/primitive type names that shouldn't create references
@@ -2454,6 +3800,9 @@ class TreeSitterExtractor {
2454
3800
  // Go
2455
3801
  'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64',
2456
3802
  'float32', 'float64', 'complex64', 'complex128', 'rune', 'error',
3803
+ // Scala (capitalized primitives + ubiquitous stdlib aliases)
3804
+ 'Int', 'Long', 'Short', 'Byte', 'Float', 'Double', 'Boolean', 'Char', 'Unit',
3805
+ 'String', 'Any', 'AnyRef', 'AnyVal', 'Nothing', 'Null',
2457
3806
  ]);
2458
3807
  /**
2459
3808
  * Extract type references from type annotations on a function/method/field node.
@@ -2474,16 +3823,67 @@ class TreeSitterExtractor {
2474
3823
  this.extractCsharpTypeRefs(node, nodeId);
2475
3824
  return;
2476
3825
  }
2477
- // Extract parameter type annotations
2478
- const params = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.paramsField || 'parameters');
2479
- if (params) {
2480
- this.extractTypeRefsFromSubtree(params, nodeId);
3826
+ // PHP type-hints are `named_type`/`optional_type`/`union_type` wrapping a
3827
+ // `name`/`qualified_name` never `type_identifier` so the generic walker
3828
+ // below emits nothing for them. Dispatch to a PHP-aware path that walks only
3829
+ // type positions (parameter / return / property types), so type-hinted
3830
+ // dependencies (the constructor-injected contracts that dominate Laravel) are
3831
+ // recorded and a `variable_name` like `$events` never mis-emits as a ref.
3832
+ if (this.language === 'php') {
3833
+ this.extractPhpTypeRefs(node, nodeId);
3834
+ return;
3835
+ }
3836
+ // Dart: a `method_signature` wraps the real `function_signature` (where the
3837
+ // params and return type live), and the return type is a bare
3838
+ // `type_identifier` child, not a `type` field — so getChildByField below
3839
+ // finds neither. Walk the inner signature: param names / the method name are
3840
+ // `identifier` (not `type_identifier`), so only types surface.
3841
+ if (this.language === 'dart') {
3842
+ let sig = node;
3843
+ if (node.type === 'method_signature') {
3844
+ sig = node.namedChildren.find((c) => c.type === 'function_signature' ||
3845
+ c.type === 'getter_signature' ||
3846
+ c.type === 'setter_signature' ||
3847
+ c.type === 'constructor_signature' ||
3848
+ c.type === 'factory_constructor_signature') ?? node;
3849
+ }
3850
+ this.extractTypeRefsFromSubtree(sig, nodeId);
3851
+ return;
3852
+ }
3853
+ // Extract parameter type annotations. Scala curries — `def f(a)(implicit
3854
+ // M: TC)` has MULTIPLE `parameters` siblings, and the typeclass is almost
3855
+ // always in the trailing implicit list — so walk every parameter list, not
3856
+ // just getChildByField's first match.
3857
+ if (this.language === 'scala') {
3858
+ for (const pc of node.namedChildren) {
3859
+ if (pc.type === 'parameters')
3860
+ this.extractTypeRefsFromSubtree(pc, nodeId);
3861
+ }
3862
+ }
3863
+ else {
3864
+ const params = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.paramsField || 'parameters');
3865
+ if (params) {
3866
+ this.extractTypeRefsFromSubtree(params, nodeId);
3867
+ }
2481
3868
  }
2482
3869
  // Extract return type annotation
2483
3870
  const returnType = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.returnField || 'return_type');
2484
3871
  if (returnType) {
2485
3872
  this.extractTypeRefsFromSubtree(returnType, nodeId);
2486
3873
  }
3874
+ // Scala context bounds / type-parameter bounds: `def f[A: Monoid]`,
3875
+ // `[F[_]: Monad]`, `[A <: Foo]` carry the bound type inside `type_parameters`.
3876
+ // This is THE pervasive way a typeclass is required in Scala, yet the bound
3877
+ // never appears in the value parameters. Param NAMES are `identifier` (not
3878
+ // `type_identifier`), so only the bound types surface. Scala-only: in other
3879
+ // languages a `type_parameters` child holds declaration names as
3880
+ // `type_identifier` (TS `<T>`), which would wrongly surface as refs.
3881
+ if (this.language === 'scala') {
3882
+ const typeParams = node.namedChildren.find((c) => c.type === 'type_parameters');
3883
+ if (typeParams) {
3884
+ this.extractTypeRefsFromSubtree(typeParams, nodeId);
3885
+ }
3886
+ }
2487
3887
  // Extract direct type annotation (for class fields like `model: ITextModel`)
2488
3888
  const typeAnnotation = node.namedChildren.find((c) => c.type === 'type_annotation');
2489
3889
  if (typeAnnotation) {
@@ -2503,8 +3903,11 @@ class TreeSitterExtractor {
2503
3903
  * `tuple_type`, …) — none of which are `type_identifier`. Closes #381.
2504
3904
  */
2505
3905
  extractCsharpTypeRefs(node, nodeId) {
2506
- // Return type / property type the field is named `type`.
2507
- const directType = (0, tree_sitter_helpers_1.getChildByField)(node, 'type');
3906
+ // A property's type is under the `type` field; a method/constructor's RETURN
3907
+ // type is under `returns` (tree-sitter-c-sharp 0.23.x — older builds used
3908
+ // `type` for both). A node carries only one of the two, so checking both
3909
+ // covers return types and property types without conflating them.
3910
+ const directType = (0, tree_sitter_helpers_1.getChildByField)(node, 'type') ?? (0, tree_sitter_helpers_1.getChildByField)(node, 'returns');
2508
3911
  if (directType)
2509
3912
  this.walkCsharpTypePosition(directType, nodeId);
2510
3913
  // Field declarations wrap declarators in a `variable_declaration`
@@ -2534,6 +3937,32 @@ class TreeSitterExtractor {
2534
3937
  }
2535
3938
  }
2536
3939
  }
3940
+ /**
3941
+ * Record the dependencies declared by a C# PRIMARY CONSTRUCTOR
3942
+ * (`class Svc(IRepo repo, [FromKeyedServices("k")] ICache cache) { … }`,
3943
+ * C# 12+). The parameter list hangs off the class/struct/record declaration
3944
+ * as an unnamed-field `parameter_list` child (not the `parameters` field a
3945
+ * method uses), so it's found by node type. Each parameter's declared type
3946
+ * becomes a `references` edge from the owning type — these are exactly the
3947
+ * services a DI-registered type depends on, so impact/blast-radius and
3948
+ * "who depends on this contract" now see them. No-op when there's no primary
3949
+ * constructor. (#237)
3950
+ */
3951
+ extractCsharpPrimaryCtorParamRefs(node, ownerId) {
3952
+ if (this.language !== 'csharp')
3953
+ return;
3954
+ const paramList = node.namedChildren.find((c) => c.type === 'parameter_list');
3955
+ if (!paramList)
3956
+ return;
3957
+ for (let i = 0; i < paramList.namedChildCount; i++) {
3958
+ const param = paramList.namedChild(i);
3959
+ if (!param || param.type !== 'parameter')
3960
+ continue;
3961
+ const paramType = (0, tree_sitter_helpers_1.getChildByField)(param, 'type');
3962
+ if (paramType)
3963
+ this.walkCsharpTypePosition(paramType, ownerId);
3964
+ }
3965
+ }
2537
3966
  /**
2538
3967
  * Walk a C# subtree that is KNOWN to be in a type position
2539
3968
  * (return type, parameter type, property type, field type, generic
@@ -2596,6 +4025,65 @@ class TreeSitterExtractor {
2596
4025
  this.walkCsharpTypePosition(child, fromNodeId);
2597
4026
  }
2598
4027
  }
4028
+ /**
4029
+ * Extract PHP type references from a method/function/property declaration.
4030
+ * Walks ONLY type positions: each parameter's type child (inside
4031
+ * `formal_parameters`), the return type, and a property's type — all
4032
+ * `named_type` / `optional_type` / `union_type` / … direct children. Parameter
4033
+ * and property NAMES are `variable_name` (`$x`), never type nodes, so they
4034
+ * can't be mis-emitted.
4035
+ */
4036
+ extractPhpTypeRefs(node, nodeId) {
4037
+ const params = node.namedChildren.find((c) => c.type === 'formal_parameters');
4038
+ if (params) {
4039
+ for (const p of params.namedChildren) {
4040
+ // simple_parameter / property_promotion_parameter / variadic_parameter
4041
+ for (const c of p.namedChildren) {
4042
+ if (PHP_TYPE_NODES.has(c.type))
4043
+ this.walkPhpTypePosition(c, nodeId);
4044
+ }
4045
+ }
4046
+ }
4047
+ // Return type (method/function) and property type are TYPE nodes that are
4048
+ // DIRECT children of the declaration.
4049
+ for (const c of node.namedChildren) {
4050
+ if (PHP_TYPE_NODES.has(c.type))
4051
+ this.walkPhpTypePosition(c, nodeId);
4052
+ }
4053
+ }
4054
+ /** Walk a PHP subtree KNOWN to be in a type position; emit class/interface refs. */
4055
+ walkPhpTypePosition(node, fromNodeId) {
4056
+ if (node.type === 'primitive_type')
4057
+ return; // int/string/void/…
4058
+ if (node.type === 'name') {
4059
+ const name = (0, tree_sitter_helpers_1.getNodeText)(node, this.source);
4060
+ if (name && !this.PHP_PSEUDO_TYPES.has(name)) {
4061
+ this.unresolvedReferences.push({
4062
+ fromNodeId, referenceName: name, referenceKind: 'references',
4063
+ line: node.startPosition.row + 1, column: node.startPosition.column,
4064
+ });
4065
+ }
4066
+ return;
4067
+ }
4068
+ if (node.type === 'qualified_name') {
4069
+ // `App\Contracts\Logger` → match on the trailing simple name (what the
4070
+ // class node is stored as, and what a `use` import brings into scope).
4071
+ const last = (0, tree_sitter_helpers_1.getNodeText)(node, this.source).split('\\').pop() ?? '';
4072
+ if (last && !this.PHP_PSEUDO_TYPES.has(last)) {
4073
+ this.unresolvedReferences.push({
4074
+ fromNodeId, referenceName: last, referenceKind: 'references',
4075
+ line: node.startPosition.row + 1, column: node.startPosition.column,
4076
+ });
4077
+ }
4078
+ return;
4079
+ }
4080
+ // optional_type / nullable_type / union_type / intersection_type / named_type → recurse
4081
+ for (let i = 0; i < node.namedChildCount; i++) {
4082
+ const child = node.namedChild(i);
4083
+ if (child)
4084
+ this.walkPhpTypePosition(child, fromNodeId);
4085
+ }
4086
+ }
2599
4087
  /**
2600
4088
  * Extract type references from a variable's type annotation.
2601
4089
  */
@@ -2927,9 +4415,29 @@ class TreeSitterExtractor {
2927
4415
  }
2928
4416
  }
2929
4417
  }
2930
- const parentId = this.methodIndex.get(fullNameKey) ||
2931
- this.methodIndex.get(shortNameKey) ||
2932
- this.nodeStack[this.nodeStack.length - 1];
4418
+ let parentId = this.methodIndex.get(fullNameKey) ||
4419
+ this.methodIndex.get(shortNameKey);
4420
+ // No existing node? This is an implementation-only **free** procedure/function
4421
+ // (`procedure Helper; begin … end;` with no interface declaration and not a
4422
+ // class method). Create a function node so its body's calls attribute to it,
4423
+ // not to the enclosing file/module. A method (`TClass.Method`, a dotted name)
4424
+ // always has a node from its class declaration, so this only fires for free
4425
+ // routines — and the methodIndex lookup above already covers interface-declared
4426
+ // free routines, so there's no duplicate.
4427
+ if (!parentId && !fullName.includes('.')) {
4428
+ const fnNode = this.createNode('function', fullName, declProc, {
4429
+ signature: this.extractor?.getSignature?.(declProc, this.source),
4430
+ visibility: this.extractor?.getVisibility?.(declProc),
4431
+ });
4432
+ if (fnNode) {
4433
+ parentId = fnNode.id;
4434
+ this.methodIndex.set(fullNameKey, fnNode.id);
4435
+ if (!this.methodIndex.has(shortNameKey))
4436
+ this.methodIndex.set(shortNameKey, fnNode.id);
4437
+ }
4438
+ }
4439
+ if (!parentId)
4440
+ parentId = this.nodeStack[this.nodeStack.length - 1];
2933
4441
  if (!parentId)
2934
4442
  return;
2935
4443
  // Visit the block for calls
@@ -2955,10 +4463,41 @@ class TreeSitterExtractor {
2955
4463
  return;
2956
4464
  let calleeName = '';
2957
4465
  if (firstChild.type === 'exprDot') {
2958
- // Qualified call: Obj.Method(...)
2959
- const identifiers = firstChild.namedChildren.filter((c) => c.type === 'identifier');
2960
- if (identifiers.length > 0) {
2961
- calleeName = identifiers.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source)).join('.');
4466
+ // Chained static-factory call: `TFoo.GetInstance().DoIt()` — the exprDot's
4467
+ // receiver is itself an `exprCall`, so the bare identifier list would
4468
+ // collapse to just `DoIt` and mis-resolve to a same-named method on an
4469
+ // unrelated class. Encode `TFoo.GetInstance().DoIt` so resolution infers
4470
+ // DoIt's class from what `TFoo.GetInstance` RETURNS (#645/#608). Only a
4471
+ // capitalized class-factory chain; a unary outer method.
4472
+ const innerCall = firstChild.namedChildren.find((c) => c.type === 'exprCall');
4473
+ const outerId = firstChild.namedChildren.filter((c) => c.type === 'identifier').pop();
4474
+ const method = outerId ? (0, tree_sitter_helpers_1.getNodeText)(outerId, this.source) : '';
4475
+ if (innerCall && method && /^\w+$/.test(method)) {
4476
+ const innerFirst = innerCall.namedChild(0);
4477
+ let innerCallee = '';
4478
+ if (innerFirst?.type === 'exprDot') {
4479
+ innerCallee = innerFirst.namedChildren
4480
+ .filter((c) => c.type === 'identifier')
4481
+ .map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
4482
+ .join('.');
4483
+ }
4484
+ else if (innerFirst?.type === 'identifier') {
4485
+ innerCallee = (0, tree_sitter_helpers_1.getNodeText)(innerFirst, this.source);
4486
+ }
4487
+ // Gate on the Delphi type-naming convention — `TFoo` classes / `IFoo`
4488
+ // interfaces — so a class-factory chain re-encodes but a capitalized
4489
+ // VARIABLE/parameter chain (Pascal capitalizes locals too: `Curve.X().Y()`,
4490
+ // `Self.X().Y()`) stays bare and keeps its existing bare-name resolution.
4491
+ calleeName = innerCallee && /^[TI][A-Z]/.test(innerCallee)
4492
+ ? `${innerCallee}().${method}`
4493
+ : method;
4494
+ }
4495
+ else {
4496
+ // Qualified call: Obj.Method(...)
4497
+ const identifiers = firstChild.namedChildren.filter((c) => c.type === 'identifier');
4498
+ if (identifiers.length > 0) {
4499
+ calleeName = identifiers.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source)).join('.');
4500
+ }
2962
4501
  }
2963
4502
  }
2964
4503
  else if (firstChild.type === 'identifier') {
@@ -2979,6 +4518,72 @@ class TreeSitterExtractor {
2979
4518
  this.visitPascalBlock(args);
2980
4519
  }
2981
4520
  }
4521
+ /**
4522
+ * Extract a PAREN-LESS Pascal method/procedure call (`Obj.Method;`,
4523
+ * `TFoo.GetInstance.DoIt;`). Pascal lets a no-arg method drop its parens, so it
4524
+ * parses as a bare `exprDot` (not an `exprCall`). A bare `exprDot` is
4525
+ * syntactically identical to a field/property access, so this is only ever
4526
+ * called for a STATEMENT-level exprDot (caller-gated): a bare `Obj.Field;`
4527
+ * statement is a no-op, so a statement-level dot expression is a call. (An
4528
+ * exprDot in assignment LHS/RHS or a condition is left alone — there it really
4529
+ * can be a field/property read.)
4530
+ */
4531
+ extractPascalParenlessCall(node) {
4532
+ if (this.nodeStack.length === 0)
4533
+ return;
4534
+ const callerId = this.nodeStack[this.nodeStack.length - 1];
4535
+ if (!callerId)
4536
+ return;
4537
+ const receiver = node.namedChild(0);
4538
+ const outerId = node.namedChildren.filter((c) => c.type === 'identifier').pop();
4539
+ const method = outerId ? (0, tree_sitter_helpers_1.getNodeText)(outerId, this.source) : '';
4540
+ if (!method)
4541
+ return;
4542
+ let calleeName = '';
4543
+ // Chained: the receiver is itself a call — a paren-less `TFoo.GetInstance` (an
4544
+ // inner exprDot) or a paren'd `TFoo.GetInstance()` (an exprCall). Encode the
4545
+ // chain `TFoo.GetInstance().DoIt` so resolution infers DoIt's class from what
4546
+ // the factory RETURNS (#645/#608), gated on the Delphi `TFoo`/`IFoo` type
4547
+ // convention; a capitalized VARIABLE chain stays a bare method name.
4548
+ if ((receiver?.type === 'exprDot' || receiver?.type === 'exprCall') && /^\w+$/.test(method)) {
4549
+ const innerCalleeNode = receiver.type === 'exprCall' ? receiver.namedChild(0) : receiver;
4550
+ const innerCallee = !innerCalleeNode
4551
+ ? ''
4552
+ : innerCalleeNode.type === 'identifier'
4553
+ ? (0, tree_sitter_helpers_1.getNodeText)(innerCalleeNode, this.source)
4554
+ : innerCalleeNode.namedChildren
4555
+ .filter((c) => c.type === 'identifier')
4556
+ .map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
4557
+ .join('.');
4558
+ if (innerCallee && /^[TI][A-Z]/.test(innerCallee)) {
4559
+ calleeName = `${innerCallee}().${method}`;
4560
+ // The T/I-prefixed inner is itself a real call — record it too.
4561
+ if (receiver.type === 'exprCall')
4562
+ this.extractPascalCall(receiver);
4563
+ else
4564
+ this.extractPascalParenlessCall(receiver);
4565
+ }
4566
+ else {
4567
+ calleeName = method; // non-class receiver: a bare method ref (no field-access ref)
4568
+ }
4569
+ }
4570
+ else {
4571
+ // Simple: `Obj.Method` → the dotted name (resolves via the receiver / bare name).
4572
+ calleeName = node.namedChildren
4573
+ .filter((c) => c.type === 'identifier')
4574
+ .map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
4575
+ .join('.');
4576
+ }
4577
+ if (calleeName) {
4578
+ this.unresolvedReferences.push({
4579
+ fromNodeId: callerId,
4580
+ referenceName: calleeName,
4581
+ referenceKind: 'calls',
4582
+ line: node.startPosition.row + 1,
4583
+ column: node.startPosition.column,
4584
+ });
4585
+ }
4586
+ }
2982
4587
  /**
2983
4588
  * Recursively visit a Pascal block/statement tree for call expressions
2984
4589
  */
@@ -2987,15 +4592,32 @@ class TreeSitterExtractor {
2987
4592
  const child = node.namedChild(i);
2988
4593
  if (!child)
2989
4594
  continue;
4595
+ // Function-as-value capture (#756): Pascal bodies are walked here, not
4596
+ // in visitNode/visitForCallsAndStructure, so the capture hook fires here
4597
+ // — assignment RHS is the Delphi event-wiring idiom (`OnFire := Handler`).
4598
+ this.maybeCaptureFnRefs(child, child.type);
2990
4599
  if (child.type === 'exprCall') {
2991
4600
  this.extractPascalCall(child);
4601
+ // The walker doesn't descend into a call's arguments — dispatch the
4602
+ // argument container directly (`RegisterHandler(TargetCb)` / `(@Cb)`).
4603
+ const args = child.namedChildren.find((c) => c.type === 'exprArgs');
4604
+ if (args)
4605
+ this.maybeCaptureFnRefs(args, 'exprArgs');
2992
4606
  }
2993
4607
  else if (child.type === 'exprDot') {
2994
- // Check if exprDot contains an exprCall
2995
- for (let j = 0; j < child.namedChildCount; j++) {
2996
- const grandchild = child.namedChild(j);
2997
- if (grandchild?.type === 'exprCall') {
2998
- this.extractPascalCall(grandchild);
4608
+ // A STATEMENT-level bare exprDot is a paren-less call (`Obj.Free;`,
4609
+ // `TFoo.GetInstance.DoIt;`). Anywhere else (assignment side, condition,
4610
+ // expression) a bare exprDot is ambiguous with a field/property access,
4611
+ // so there we only descend for paren'd inner calls.
4612
+ if (node.type === 'statement') {
4613
+ this.extractPascalParenlessCall(child);
4614
+ }
4615
+ else {
4616
+ for (let j = 0; j < child.namedChildCount; j++) {
4617
+ const grandchild = child.namedChild(j);
4618
+ if (grandchild?.type === 'exprCall') {
4619
+ this.extractPascalCall(grandchild);
4620
+ }
2999
4621
  }
3000
4622
  }
3001
4623
  }
@@ -3027,11 +4649,21 @@ function extractFromSource(filePath, source, language, frameworkNames) {
3027
4649
  const extractor = new vue_extractor_1.VueExtractor(filePath, source);
3028
4650
  result = extractor.extract();
3029
4651
  }
4652
+ else if (detectedLanguage === 'astro') {
4653
+ // Use custom extractor for Astro (frontmatter + template delegation)
4654
+ const extractor = new astro_extractor_1.AstroExtractor(filePath, source);
4655
+ result = extractor.extract();
4656
+ }
3030
4657
  else if (detectedLanguage === 'liquid') {
3031
4658
  // Use custom extractor for Liquid
3032
4659
  const extractor = new liquid_extractor_1.LiquidExtractor(filePath, source);
3033
4660
  result = extractor.extract();
3034
4661
  }
4662
+ else if (detectedLanguage === 'razor') {
4663
+ // Use custom extractor for ASP.NET Razor (.cshtml) / Blazor (.razor) markup
4664
+ const extractor = new razor_extractor_1.RazorExtractor(filePath, source);
4665
+ result = extractor.extract();
4666
+ }
3035
4667
  else if (detectedLanguage === 'xml') {
3036
4668
  // Custom extractor for MyBatis mapper XML. Non-mapper XML returns just a
3037
4669
  // file node so the watcher tracks it without emitting symbols.