@backbencher/brain 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/EMBEDDINGS.md +355 -0
  3. package/LICENSE +21 -0
  4. package/README.md +415 -2
  5. package/bin/bb +13 -0
  6. package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
  7. package/dist/brain-index-health-Q5APTJQL.js +17 -0
  8. package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
  9. package/dist/brain.sql-EIM4WS4D.js +29 -0
  10. package/dist/chunk-22V37YTK.js +1758 -0
  11. package/dist/chunk-2BSKD7O3.js +40 -0
  12. package/dist/chunk-2DE27DVC.js +1314 -0
  13. package/dist/chunk-2RRBMNBQ.js +322 -0
  14. package/dist/chunk-2WXYNLBA.js +29 -0
  15. package/dist/chunk-3CTFIJJB.js +17 -0
  16. package/dist/chunk-3ZTOHHSW.js +2104 -0
  17. package/dist/chunk-4H22TLOT.js +543 -0
  18. package/dist/chunk-5BVAH26D.js +340 -0
  19. package/dist/chunk-62DHLYW6.js +3427 -0
  20. package/dist/chunk-7AN7U5IZ.js +371 -0
  21. package/dist/chunk-7DRPNVIN.js +782 -0
  22. package/dist/chunk-7MBYT777.js +174 -0
  23. package/dist/chunk-AHDWGM7Q.js +236 -0
  24. package/dist/chunk-B5GEAAYF.js +818 -0
  25. package/dist/chunk-BEIZZ73B.js +1479 -0
  26. package/dist/chunk-BQ3OVQ3P.js +125 -0
  27. package/dist/chunk-D2Q4Y47A.js +53 -0
  28. package/dist/chunk-DIYAGQJL.js +34 -0
  29. package/dist/chunk-DWKXZ6IF.js +336 -0
  30. package/dist/chunk-DYM4FQEE.js +90 -0
  31. package/dist/chunk-ENW4BTBA.js +930 -0
  32. package/dist/chunk-EWMWK7WN.js +75 -0
  33. package/dist/chunk-EY5ZZUUA.js +626 -0
  34. package/dist/chunk-GBI7V7KY.js +69 -0
  35. package/dist/chunk-GEFFO6O2.js +152 -0
  36. package/dist/chunk-HJEWO5EV.js +247 -0
  37. package/dist/chunk-HQSPJDHI.js +613 -0
  38. package/dist/chunk-KAET47ZL.js +125 -0
  39. package/dist/chunk-KCDECRJY.js +440 -0
  40. package/dist/chunk-L5TDYKYK.js +76 -0
  41. package/dist/chunk-MLKGABMK.js +9 -0
  42. package/dist/chunk-NO3U3FJQ.js +201 -0
  43. package/dist/chunk-OWISBAOF.js +61 -0
  44. package/dist/chunk-QKTRECYS.js +1119 -0
  45. package/dist/chunk-QLK6ED4Q.js +235 -0
  46. package/dist/chunk-R2GW4F6R.js +19 -0
  47. package/dist/chunk-RCMTFSEF.js +87 -0
  48. package/dist/chunk-RM3PY6TG.js +376 -0
  49. package/dist/chunk-ROMM6KMC.js +433 -0
  50. package/dist/chunk-SGGCSHAX.js +12050 -0
  51. package/dist/chunk-SK6YVHFZ.js +211 -0
  52. package/dist/chunk-SM7RFQRO.js +214 -0
  53. package/dist/chunk-UGR4QIGS.js +67 -0
  54. package/dist/chunk-UQNQZXHY.js +711 -0
  55. package/dist/chunk-UUNHHX4M.js +13 -0
  56. package/dist/chunk-VL52PL2T.js +71 -0
  57. package/dist/chunk-WIG36E67.js +196 -0
  58. package/dist/chunk-XHK3WWGB.js +7436 -0
  59. package/dist/chunk-XTQM2IP3.js +2863 -0
  60. package/dist/chunk-ZE2LG76D.js +1296 -0
  61. package/dist/chunk-ZETTGPAW.js +6950 -0
  62. package/dist/cli/index.js +37 -0
  63. package/dist/config-HZAAYDP3.js +10 -0
  64. package/dist/content-search-7XIP555Q.js +593 -0
  65. package/dist/database-76RP44JG.js +9 -0
  66. package/dist/doctor-ANGETYMN.js +395 -0
  67. package/dist/edit-engine-CG72LDZD.js +51 -0
  68. package/dist/engine-BKEOGN7N.js +11 -0
  69. package/dist/engine-VIW6NSKO.js +18 -0
  70. package/dist/fingerprint-OBLIMZLP.js +33 -0
  71. package/dist/framework-intelligence-3LORTPOX.js +860 -0
  72. package/dist/freshness-3K6VU5OT.js +174 -0
  73. package/dist/graph-engine-HBSQNPTB.js +21 -0
  74. package/dist/graph_request-Q6NKB6N2.js +350 -0
  75. package/dist/impact_request-LYP672WE.js +68 -0
  76. package/dist/index-space-IUUN5WYV.js +15 -0
  77. package/dist/index-status-7OVGYSGW.js +34 -0
  78. package/dist/index.js +159 -0
  79. package/dist/instance-FO6QZRLH.js +19 -0
  80. package/dist/language-detection-DFCAU5DZ.js +11 -0
  81. package/dist/lsp/server.js +4301 -0
  82. package/dist/mcp/server.js +132 -0
  83. package/dist/onboard-DIQZV2NW.js +161 -0
  84. package/dist/override-companion-pass-XR7MHTP7.js +117 -0
  85. package/dist/parallel-indexer-FK2XKZXY.js +88 -0
  86. package/dist/pool-worker.js +1153 -0
  87. package/dist/project-health-6XPENVAA.js +94 -0
  88. package/dist/project-index-5SDPHITE.js +57 -0
  89. package/dist/project-registry-ZJ3DVUGO.js +355 -0
  90. package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
  91. package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
  92. package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
  93. package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
  94. package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
  95. package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
  96. package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
  97. package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
  98. package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
  99. package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
  100. package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
  101. package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
  102. package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
  103. package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
  104. package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
  105. package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
  106. package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
  107. package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
  108. package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
  109. package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
  110. package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
  111. package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
  112. package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
  113. package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
  114. package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
  115. package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
  116. package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
  117. package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
  118. package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
  119. package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
  120. package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
  121. package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
  122. package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
  123. package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
  124. package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
  125. package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
  126. package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
  127. package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
  128. package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
  129. package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
  130. package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
  131. package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
  132. package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
  133. package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
  134. package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
  135. package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
  136. package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
  137. package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
  138. package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
  139. package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
  140. package/dist/regex-search-NJKHMZNO.js +239 -0
  141. package/dist/rules-ACHPB7QU.js +152 -0
  142. package/dist/scoped-path-MJYWJZQX.js +11 -0
  143. package/dist/setup-show-NT5ZHYNE.js +15 -0
  144. package/dist/setup-wizard-G5YYR5T6.js +24 -0
  145. package/dist/symbol-search-GZJIM2J2.js +16 -0
  146. package/dist/taint-KXUHHPIE.js +202 -0
  147. package/dist/taint-analyzer-N7YLZU3N.js +163 -0
  148. package/dist/tool-handlers-OJDC35DI.js +72 -0
  149. package/dist/tool-registry-BPEJNSZG.js +17 -0
  150. package/dist/tool-validation-Q57YMMBX.js +32 -0
  151. package/dist/type-inference-QMT2RMLV.js +13 -0
  152. package/dist/vector-binary-LMY4QGAN.js +19 -0
  153. package/dist/version-ABP2TUL2.js +9 -0
  154. package/dist/wasm-NMWO3WXY.js +16 -0
  155. package/docs/EMBEDDINGS.md +132 -0
  156. package/docs/README.md +178 -0
  157. package/docs/data/m10-baseline.json +602 -0
  158. package/docs/data/m10-benchmark-baseline.json +57 -0
  159. package/docs/data/m10-benchmark-last-run.json +127 -0
  160. package/docs/data/m10-final-audit.json +215 -0
  161. package/docs/data/m12-final-audit.json +138 -0
  162. package/docs/data/m12-surface-matrix.json +50 -0
  163. package/docs/data/public-tools.json +1902 -0
  164. package/docs/indexing/DEAD-ASSETS.md +88 -0
  165. package/docs/indexing/INDEXER-REVIEW.md +634 -0
  166. package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
  167. package/docs/indexing/README.md +427 -0
  168. package/docs/indexing/discovery-matrix.md +41 -0
  169. package/docs/indexing/embeddings.md +325 -0
  170. package/docs/indexing/environment-variables.md +77 -0
  171. package/docs/indexing/performance.md +323 -0
  172. package/docs/indexing/resume.md +345 -0
  173. package/docs/indexing/troubleshooting.md +391 -0
  174. package/docs/man/bb.1 +190 -0
  175. package/docs/tools/bb_analyze.md +147 -0
  176. package/docs/tools/bb_annotate.md +110 -0
  177. package/docs/tools/bb_annotations.md +106 -0
  178. package/docs/tools/bb_ask.md +101 -0
  179. package/docs/tools/bb_cross_language.md +103 -0
  180. package/docs/tools/bb_doc.md +169 -0
  181. package/docs/tools/bb_edit.md +185 -0
  182. package/docs/tools/bb_graph.md +169 -0
  183. package/docs/tools/bb_health.md +156 -0
  184. package/docs/tools/bb_impact.md +117 -0
  185. package/docs/tools/bb_index.md +108 -0
  186. package/docs/tools/bb_migrate.md +135 -0
  187. package/docs/tools/bb_patterns.md +140 -0
  188. package/docs/tools/bb_search.md +150 -0
  189. package/docs/tools/bb_security.md +100 -0
  190. package/docs/tools/bb_visualize.md +184 -0
  191. package/grammars/README.md +57 -0
  192. package/grammars/THIRD-PARTY-NOTICES.md +50 -0
  193. package/grammars/tree-sitter-c/LICENSE +21 -0
  194. package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
  195. package/grammars/tree-sitter-cpp/LICENSE +21 -0
  196. package/grammars/tree-sitter-csharp/LICENSE +21 -0
  197. package/grammars/tree-sitter-css/LICENSE +21 -0
  198. package/grammars/tree-sitter-css/README.md +21 -0
  199. package/grammars/tree-sitter-dart/LICENSE +20 -0
  200. package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
  201. package/grammars/tree-sitter-dart-upstream/README.md +18 -0
  202. package/grammars/tree-sitter-elixir/LICENSE +176 -0
  203. package/grammars/tree-sitter-elixir/README.md +11 -0
  204. package/grammars/tree-sitter-fsharp/LICENSE +21 -0
  205. package/grammars/tree-sitter-fsharp/README.md +45 -0
  206. package/grammars/tree-sitter-graphql/LICENSE +21 -0
  207. package/grammars/tree-sitter-graphql/README.md +106 -0
  208. package/grammars/tree-sitter-groovy/LICENSE +21 -0
  209. package/grammars/tree-sitter-groovy/README.md +4 -0
  210. package/grammars/tree-sitter-haskell/LICENSE +21 -0
  211. package/grammars/tree-sitter-haskell/README.md +394 -0
  212. package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
  213. package/grammars/tree-sitter-hcl/LICENSE +201 -0
  214. package/grammars/tree-sitter-hcl/README.md +39 -0
  215. package/grammars/tree-sitter-html/LICENSE +21 -0
  216. package/grammars/tree-sitter-html/README.md +21 -0
  217. package/grammars/tree-sitter-julia/LICENSE +21 -0
  218. package/grammars/tree-sitter-julia/README.md +24 -0
  219. package/grammars/tree-sitter-kotlin/LICENSE +9 -0
  220. package/grammars/tree-sitter-lua/LICENSE.md +21 -0
  221. package/grammars/tree-sitter-lua/README.md +29 -0
  222. package/grammars/tree-sitter-make/LICENSE +21 -0
  223. package/grammars/tree-sitter-matlab/LICENSE +19 -0
  224. package/grammars/tree-sitter-matlab/README.md +99 -0
  225. package/grammars/tree-sitter-nim/LICENSE +373 -0
  226. package/grammars/tree-sitter-nim/README.md +46 -0
  227. package/grammars/tree-sitter-objc/LICENSE +21 -0
  228. package/grammars/tree-sitter-objc/README.md +20 -0
  229. package/grammars/tree-sitter-perl/LICENSE +7 -0
  230. package/grammars/tree-sitter-perl/README.md +100 -0
  231. package/grammars/tree-sitter-powershell/LICENSE +21 -0
  232. package/grammars/tree-sitter-powershell/README.md +7 -0
  233. package/grammars/tree-sitter-proto/LICENSE +21 -0
  234. package/grammars/tree-sitter-proto/README.md +105 -0
  235. package/grammars/tree-sitter-r/LICENSE +9 -0
  236. package/grammars/tree-sitter-r/README.md +66 -0
  237. package/grammars/tree-sitter-ruby/LICENSE +22 -0
  238. package/grammars/tree-sitter-scss/LICENSE +21 -0
  239. package/grammars/tree-sitter-scss/README.md +17 -0
  240. package/grammars/tree-sitter-solidity/LICENSE +7 -0
  241. package/grammars/tree-sitter-sql/LICENSE +21 -0
  242. package/grammars/tree-sitter-typescript/LICENSE +21 -0
  243. package/grammars/tree-sitter-vba/LICENSE +21 -0
  244. package/grammars/tree-sitter-vba/README.md +738 -0
  245. package/grammars/tree-sitter-vbnet/LICENSE +21 -0
  246. package/grammars/tree-sitter-vbnet/README.md +322 -0
  247. package/grammars/tree-sitter-xml/LICENSE +19 -0
  248. package/grammars/tree-sitter-yaml/LICENSE +20 -0
  249. package/grammars/tree-sitter-yaml/README.md +21 -0
  250. package/package.json +184 -4
  251. package/wasm/dart.wasm +0 -0
  252. package/wasm/tree-sitter-bash.wasm +0 -0
  253. package/wasm/tree-sitter-c.wasm +0 -0
  254. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  255. package/wasm/tree-sitter-clean.wasm +0 -0
  256. package/wasm/tree-sitter-clojure.wasm +0 -0
  257. package/wasm/tree-sitter-core_schema.wasm +0 -0
  258. package/wasm/tree-sitter-cpp.wasm +0 -0
  259. package/wasm/tree-sitter-css.wasm +0 -0
  260. package/wasm/tree-sitter-dart.wasm +0 -0
  261. package/wasm/tree-sitter-elisp.wasm +0 -0
  262. package/wasm/tree-sitter-elixir.wasm +0 -0
  263. package/wasm/tree-sitter-elm.wasm +0 -0
  264. package/wasm/tree-sitter-embedded_template.wasm +0 -0
  265. package/wasm/tree-sitter-erlang.wasm +0 -0
  266. package/wasm/tree-sitter-fsharp.wasm +0 -0
  267. package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
  268. package/wasm/tree-sitter-go.wasm +0 -0
  269. package/wasm/tree-sitter-graphql.wasm +0 -0
  270. package/wasm/tree-sitter-groovy.wasm +0 -0
  271. package/wasm/tree-sitter-haskell.wasm +0 -0
  272. package/wasm/tree-sitter-hcl.wasm +0 -0
  273. package/wasm/tree-sitter-html.wasm +0 -0
  274. package/wasm/tree-sitter-java.wasm +0 -0
  275. package/wasm/tree-sitter-javascript.wasm +0 -0
  276. package/wasm/tree-sitter-json.wasm +0 -0
  277. package/wasm/tree-sitter-json_schema.wasm +0 -0
  278. package/wasm/tree-sitter-julia.wasm +0 -0
  279. package/wasm/tree-sitter-kotlin.wasm +0 -0
  280. package/wasm/tree-sitter-lua.wasm +0 -0
  281. package/wasm/tree-sitter-make.wasm +0 -0
  282. package/wasm/tree-sitter-markdown.wasm +0 -0
  283. package/wasm/tree-sitter-markdown_inline.wasm +0 -0
  284. package/wasm/tree-sitter-matlab.wasm +0 -0
  285. package/wasm/tree-sitter-nim.wasm +0 -0
  286. package/wasm/tree-sitter-objc.wasm +0 -0
  287. package/wasm/tree-sitter-ocaml.wasm +0 -0
  288. package/wasm/tree-sitter-perl.wasm +0 -0
  289. package/wasm/tree-sitter-php.wasm +0 -0
  290. package/wasm/tree-sitter-php_only.wasm +0 -0
  291. package/wasm/tree-sitter-powershell.wasm +0 -0
  292. package/wasm/tree-sitter-protobuf.wasm +0 -0
  293. package/wasm/tree-sitter-python.wasm +0 -0
  294. package/wasm/tree-sitter-ql.wasm +0 -0
  295. package/wasm/tree-sitter-r.wasm +0 -0
  296. package/wasm/tree-sitter-rescript.wasm +0 -0
  297. package/wasm/tree-sitter-ruby.wasm +0 -0
  298. package/wasm/tree-sitter-rust.wasm +0 -0
  299. package/wasm/tree-sitter-scala.wasm +0 -0
  300. package/wasm/tree-sitter-scss.wasm +0 -0
  301. package/wasm/tree-sitter-solidity.wasm +0 -0
  302. package/wasm/tree-sitter-sql.wasm +0 -0
  303. package/wasm/tree-sitter-svelte.wasm +0 -0
  304. package/wasm/tree-sitter-swift.wasm +0 -0
  305. package/wasm/tree-sitter-systemrdl.wasm +0 -0
  306. package/wasm/tree-sitter-terraform.wasm +0 -0
  307. package/wasm/tree-sitter-tlaplus.wasm +0 -0
  308. package/wasm/tree-sitter-toml.wasm +0 -0
  309. package/wasm/tree-sitter-tsx.wasm +0 -0
  310. package/wasm/tree-sitter-typescript.wasm +0 -0
  311. package/wasm/tree-sitter-vba.wasm +0 -0
  312. package/wasm/tree-sitter-vbnet.wasm +0 -0
  313. package/wasm/tree-sitter-vue.wasm +0 -0
  314. package/wasm/tree-sitter-xml.wasm +0 -0
  315. package/wasm/tree-sitter-yaml.wasm +0 -0
  316. package/wasm/tree-sitter-zig.wasm +0 -0
  317. package/wasm/tree-sitter.wasm +0 -0
  318. package/wasm/tsx.wasm +0 -0
  319. package/wasm/typescript.wasm +0 -0
@@ -0,0 +1,2285 @@
1
+ import {
2
+ findDeclaringParent,
3
+ localEdgeLabel,
4
+ localEdgeName
5
+ } from "./chunk-OWISBAOF.js";
6
+ import {
7
+ ASTEngine
8
+ } from "./chunk-3ZTOHHSW.js";
9
+ import "./chunk-BQ3OVQ3P.js";
10
+ import "./chunk-SK6YVHFZ.js";
11
+ import "./chunk-2BSKD7O3.js";
12
+ import "./chunk-KAET47ZL.js";
13
+ import "./chunk-KCDECRJY.js";
14
+ import "./chunk-MLKGABMK.js";
15
+
16
+ // src/ast/real-groovy-analyzer.ts
17
+ var INTERPOLATION_OWNED_BY_MEMBER_WALK = /* @__PURE__ */ new Set([
18
+ "field_access",
19
+ "method_invocation",
20
+ "juxt_function_call",
21
+ "array_access",
22
+ "object_creation_expression"
23
+ ]);
24
+ var GROOVY_BUILTINS = /* @__PURE__ */ new Set([
25
+ // Groovy language builtins
26
+ "println",
27
+ "print",
28
+ "printf",
29
+ "sprintf",
30
+ "assert",
31
+ "sleep",
32
+ "synchronized",
33
+ // JVM Object methods
34
+ "toString",
35
+ "hashCode",
36
+ "equals",
37
+ "getClass",
38
+ "notify",
39
+ "notifyAll",
40
+ "wait",
41
+ "clone",
42
+ "finalize",
43
+ // Common java.* entry points
44
+ "System.out.println",
45
+ "System.out.print",
46
+ "System.out.printf",
47
+ "System.err.println",
48
+ "System.err.print",
49
+ "System.exit",
50
+ "System.gc",
51
+ "System.currentTimeMillis",
52
+ "System.nanoTime",
53
+ "System.getProperty",
54
+ "System.getenv",
55
+ "System.identityHashCode",
56
+ "System.arraycopy",
57
+ "System.lineSeparator"
58
+ ]);
59
+ var GROOVY_DECLARATION_TYPES = /* @__PURE__ */ new Set([
60
+ "method_declaration",
61
+ "constructor_declaration",
62
+ "function_definition",
63
+ "class_declaration",
64
+ "trait_declaration",
65
+ "enum_declaration"
66
+ ]);
67
+ var TOP_LEVEL_TYPE_NODES = /* @__PURE__ */ new Set([
68
+ "class_declaration",
69
+ "interface_declaration",
70
+ "trait_declaration",
71
+ "enum_declaration",
72
+ "record_declaration",
73
+ "annotation_type_declaration"
74
+ ]);
75
+ var RealGroovyAnalyzer = class _RealGroovyAnalyzer {
76
+ analyze(rootNode, content, filePath, context) {
77
+ const symbols = [];
78
+ const relationships = [];
79
+ const imports = [];
80
+ const exports = [];
81
+ const errors = [];
82
+ const symbolMap = /* @__PURE__ */ new Map();
83
+ const classHierarchy = /* @__PURE__ */ new Map();
84
+ const overrideTasks = [];
85
+ const scopeStack = [];
86
+ const stack = [{ node: rootNode }];
87
+ const walk = {
88
+ content,
89
+ symbols,
90
+ relationships,
91
+ imports,
92
+ symbolMap,
93
+ classHierarchy,
94
+ overrideTasks,
95
+ scopeStack,
96
+ parentContext: void 0,
97
+ newContext: void 0,
98
+ pushedScope: false,
99
+ currentPackage: ""
100
+ };
101
+ while (stack.length > 0) {
102
+ const item = stack.pop();
103
+ const { node, parentContext, isExit } = item;
104
+ if (isExit) {
105
+ if (parentContext) {
106
+ if (scopeStack.length > 0 && scopeStack[scopeStack.length - 1] === parentContext) {
107
+ scopeStack.pop();
108
+ }
109
+ }
110
+ continue;
111
+ }
112
+ if (node.isMissing || node.hasError) {
113
+ if (node.type === "ERROR" || node.type === "MISSING") {
114
+ errors.push({
115
+ message: `Syntax ${node.type} detected.`,
116
+ range: this.nodeToRange(node),
117
+ severity: "warning"
118
+ });
119
+ }
120
+ }
121
+ walk.parentContext = parentContext;
122
+ walk.newContext = parentContext;
123
+ walk.pushedScope = false;
124
+ const handler = this.nodeHandlers.get(node.type);
125
+ if (handler) handler(node, walk);
126
+ if (walk.pushedScope) {
127
+ stack.push({ node, parentContext: walk.newContext, isExit: true });
128
+ }
129
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
130
+ const child = node.namedChild(i);
131
+ if (child) {
132
+ stack.push({ node: child, parentContext: walk.newContext });
133
+ }
134
+ }
135
+ }
136
+ this.recoverDegradedClassShapes(rootNode, symbols, relationships, symbolMap, walk.currentPackage);
137
+ this.resolveOverrides(overrideTasks, classHierarchy, symbolMap, relationships, context?.overrideCompanion);
138
+ const primaryType = symbols.find((s) => {
139
+ if (s.ownerName || s.scope) return false;
140
+ return s.kind === ASTEngine.SymbolKind.Class || s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Enum;
141
+ });
142
+ if (primaryType) {
143
+ for (const rel of relationships) {
144
+ const isImportMention = rel.type === ASTEngine.RelationshipType.References && rel.metadata?.isGroovyImport === true;
145
+ if (isImportMention && (rel.source === walk.currentPackage || rel.source === "(script)" || rel.source === "root")) {
146
+ rel.source = primaryType.name;
147
+ }
148
+ }
149
+ }
150
+ for (const s of symbols) {
151
+ const _owner = s.ownerName || s.scope;
152
+ if (_owner) {
153
+ relationships.push({
154
+ source: _owner,
155
+ target: s.name,
156
+ type: ASTEngine.RelationshipType.Contains,
157
+ confidence: 1,
158
+ // M59 (#825): the member's DECLARATION line, and the same line the
159
+ // member's own range carries — a symbol and the edge naming it must
160
+ // never disagree (java/scala contract). Never 0 (#820/#821).
161
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
162
+ metadata: { isContained: true }
163
+ });
164
+ }
165
+ }
166
+ this.collectFieldAccessUses(rootNode, relationships, symbols);
167
+ this.collectDeclaredTypeRefs(rootNode, relationships);
168
+ const fw = this.detectGroovyFrameworks(content, symbols, imports);
169
+ for (let i = 0; i < symbols.length; i++) {
170
+ const s = symbols[i];
171
+ const nodeType = String(s.metadata?.nodeType ?? "");
172
+ const isTopLevelType = TOP_LEVEL_TYPE_NODES.has(nodeType);
173
+ const isTopLevelFunction = nodeType === "method_declaration" || nodeType === "function_definition";
174
+ if (!s.metadata?.isAnonymousClass && !s.metadata?.isPhantom && !s.ownerName && (isTopLevelType || isTopLevelFunction)) {
175
+ exports.push({
176
+ name: s.name,
177
+ kind: s.kind,
178
+ range: s.range,
179
+ visibility: s.visibility,
180
+ metadata: { exportForm: isTopLevelFunction ? "public_top_level_function" : "public_top_level_type" }
181
+ });
182
+ }
183
+ }
184
+ let exportsWithNoPackage = 0;
185
+ let exportsNotImportable = 0;
186
+ if (exports.length > 0) {
187
+ const published = /* @__PURE__ */ new Set();
188
+ for (let i = 0; i < exports.length; i++) {
189
+ const e = exports[i];
190
+ const target = e.name;
191
+ if (!target) continue;
192
+ if (published.has(target)) continue;
193
+ published.add(target);
194
+ if (!walk.currentPackage) {
195
+ exportsWithNoPackage++;
196
+ continue;
197
+ }
198
+ if (e.visibility === "private" || e.visibility === "protected") {
199
+ exportsNotImportable++;
200
+ continue;
201
+ }
202
+ const exportForm = String(e.metadata?.exportForm ?? "public_top_level_type");
203
+ relationships.push({
204
+ source: walk.currentPackage,
205
+ target,
206
+ type: ASTEngine.RelationshipType.Exports,
207
+ confidence: 1,
208
+ // M59 (#825): the exported DECLARATION's own line — the name is
209
+ // written there, not at whatever construct encloses it. Absent rather
210
+ // than 0 when the symbol carried no range (#823).
211
+ ...e.range?.startLine ? { line_number: e.range.startLine } : {},
212
+ metadata: {
213
+ // The vocabulary is the java lane's, and it is the adjudicated
214
+ // one (java/exported-types, task-9, 2026-10-18): `exported_type` was
215
+ // superseded by `<lang>_exportable_unit` + `exportForm` when java's
216
+ // export edge was folded into ONE post-pass, and the surviving
217
+ // channel is the one with the RICHER metadata — `kind` + `dottedName`
218
+ // + the `is<Lang>Export` flag. groovy carries the same shape, and
219
+ // groovy/exports-publication-edge pins it.
220
+ relationshipType: "groovy_exportable_unit",
221
+ exportForm,
222
+ isGroovyExport: true,
223
+ kind: String(e.kind ?? ""),
224
+ // The join key, stated because it is not obvious: an `exports` edge
225
+ // is `package -> simpleName`, so `source + "." + target`
226
+ // reconstructs the dotted path an `imports` edge targets and the
227
+ // two labels meet without a resolver (java's key, same meaning).
228
+ dottedName: `${walk.currentPackage}.${target}`
229
+ }
230
+ });
231
+ }
232
+ }
233
+ for (const sym of symbols) {
234
+ const decs = sym.decorators;
235
+ if (!decs || decs.length === 0) continue;
236
+ const lines = sym.metadata?.decoratorLines;
237
+ for (let i = 0; i < decs.length; i++) {
238
+ const line = lines?.[i];
239
+ relationships.push({
240
+ source: sym.name,
241
+ target: decs[i],
242
+ type: ASTEngine.RelationshipType.Decorates,
243
+ confidence: 1,
244
+ ...typeof line === "number" ? { line_number: line } : {},
245
+ metadata: {
246
+ isDecorator: true,
247
+ decoratorLine: typeof line === "number" ? line : null,
248
+ isGroovyDecorator: true
249
+ }
250
+ });
251
+ }
252
+ }
253
+ this.classifyEvidenceTier(relationships);
254
+ return {
255
+ symbols,
256
+ relationships,
257
+ imports,
258
+ exports,
259
+ errors,
260
+ metadata: {
261
+ // Real cyclomatic complexity — was the synthetic heuristic
262
+ // symbols.length + relationships.length, which is not a complexity
263
+ // metric at all (same defect class previously fixed on scala).
264
+ complexity: this.calculateGroovyMethodComplexity(rootNode),
265
+ cyclomaticComplexity: this.calculateGroovyMethodComplexity(rootNode),
266
+ linesOfCode: content.split("\n").length,
267
+ // Refusals, surfaced. A declaration whose name came back EMPTY (a MISSING node
268
+ // from error recovery) is dropped rather than published, because an empty name is
269
+ // not a name -- but a silent drop is a hole exactly like the one it closes, so the
270
+ // count travels with the result. Non-zero means the file had a parse that did not
271
+ // happen, which is worth knowing even when the symbol list looks clean.
272
+ emptyNameRefusals: walk.emptyNameRefusals ?? 0,
273
+ currentPackage: walk.currentPackage,
274
+ // DERIV_EXPORTS_GROOVY3: exportable units this file HAS but did not
275
+ // publish as an edge, because it declares no package and so has no
276
+ // truthful exporter to name. Zero means every exportable unit in the
277
+ // file reached the graph; non-zero is the count of export surface a
278
+ // reader of the relationship table cannot see, and it is reported
279
+ // rather than absorbed. Measured over the corpus: 266 of 2,817 files.
280
+ exportsWithNoPackage,
281
+ exportsNotImportable,
282
+ framework: fw.framework,
283
+ detectedFrameworks: fw.detected,
284
+ analysisVersion: "5.0.0-groovy-pure-ast"
285
+ }
286
+ };
287
+ }
288
+ // ── #896: node-type dispatch table ────────────────────────────────────────
289
+ // `analyze` walks (push/visit/pop); these handlers extract. Each is a named
290
+ // method taking the walk state, so a handler can be exercised on its own —
291
+ // which is what the review asked for and what the if/else chain prevented.
292
+ // The misparse guards (§ juxt_function_call / field_declaration) are named
293
+ // handlers here for the same reason: they stay individually testable, and
294
+ // #841 rounds 3/14/22 measured them as load-bearing, so they are kept.
295
+ nodeHandlersCache;
296
+ get nodeHandlers() {
297
+ if (!this.nodeHandlersCache) {
298
+ const m = /* @__PURE__ */ new Map();
299
+ const reg = (types, h) => {
300
+ for (const t of types) m.set(t, h);
301
+ };
302
+ reg(["package_declaration"], (n, s) => this.handlePackageDeclaration(n, s));
303
+ reg(["import_declaration"], (n, s) => this.handleImportDeclaration(n, s));
304
+ reg(["class_declaration", "interface_declaration", "trait_declaration", "enum_declaration", "record_declaration", "annotation_type_declaration"], (n, s) => this.handleTypeDeclaration(n, s));
305
+ reg(["method_declaration", "constructor_declaration", "function_definition"], (n, s) => this.handleMethodDeclaration(n, s));
306
+ reg(["field_declaration", "constant_declaration"], (n, s) => this.handleFieldDeclaration(n, s));
307
+ reg(["enum_constant"], (n, s) => this.handleEnumConstant(n, s));
308
+ reg(["method_invocation"], (n, s) => this.handleMethodInvocation(n, s));
309
+ reg(["explicit_constructor_invocation"], (n, s) => this.handleExplicitConstructorInvocation(n, s));
310
+ reg(["juxt_function_call"], (n, s) => this.handleJuxtFunctionCall(n, s));
311
+ reg(["leading_dot_call"], (n, s) => this.handleLeadingDotCall(n, s));
312
+ reg(["object_creation_expression"], (n, s) => this.handleObjectCreationExpression(n, s));
313
+ this.nodeHandlersCache = m;
314
+ }
315
+ return this.nodeHandlersCache;
316
+ }
317
+ handlePackageDeclaration(node, s) {
318
+ const type = node.type;
319
+ const nameNode = this.findChild(node, (n) => n.type === "scoped_identifier" || n.type === "identifier");
320
+ if (nameNode) s.currentPackage = nameNode.text;
321
+ }
322
+ handleImportDeclaration(node, s) {
323
+ const { relationships, imports } = s;
324
+ const type = node.type;
325
+ const nameNode = this.findChild(node, (n) => n.type === "scoped_identifier" || n.type === "identifier");
326
+ if (nameNode) {
327
+ const moduleName = nameNode.text;
328
+ let isStatic = false;
329
+ let isWildcard = false;
330
+ let hasAlias = false;
331
+ for (let i = 0; i < node.childCount; i++) {
332
+ const c = node.child(i);
333
+ if (!c) continue;
334
+ if (c.type === "static") isStatic = true;
335
+ else if (c.type === "asterisk") isWildcard = true;
336
+ else if (c.type === "as") hasAlias = true;
337
+ }
338
+ imports.push({
339
+ module: moduleName,
340
+ items: [],
341
+ isDefault: false,
342
+ range: this.nodeToRange(node),
343
+ isNamespace: isWildcard,
344
+ metadata: { isWildcard, isStatic }
345
+ });
346
+ const importMeta = { isGroovyImport: true, isWildcard, isStatic, hasAlias };
347
+ relationships.push({
348
+ source: s.currentPackage || "(script)",
349
+ target: moduleName,
350
+ type: ASTEngine.RelationshipType.References,
351
+ confidence: 0.9,
352
+ // M59 (#825): the line the imported MODULE is named on (java's rule).
353
+ ...this.lineField(nameNode, node),
354
+ metadata: importMeta
355
+ });
356
+ relationships.push({
357
+ source: s.currentPackage || "(script)",
358
+ target: moduleName,
359
+ type: ASTEngine.RelationshipType.Imports,
360
+ confidence: 0.9,
361
+ ...this.lineField(nameNode, node),
362
+ metadata: {
363
+ ...importMeta,
364
+ isImport: true,
365
+ relationshipType: isStatic ? isWildcard ? "static_on_demand" : "static_member" : isWildcard ? "on_demand_type" : "single_type"
366
+ }
367
+ });
368
+ relationships.push({
369
+ source: s.currentPackage || "(script)",
370
+ target: moduleName,
371
+ type: ASTEngine.RelationshipType.Imports,
372
+ confidence: 1,
373
+ ...this.lineField(nameNode, node),
374
+ metadata: {
375
+ relationshipType: isStatic ? "static_member_import" : isWildcard ? "type_on_demand_import" : "single_type_import",
376
+ isGroovyImport: true,
377
+ isWildcard,
378
+ isStatic,
379
+ fileLevel: true
380
+ }
381
+ });
382
+ }
383
+ }
384
+ handleTypeDeclaration(node, s) {
385
+ const { symbols, relationships, symbolMap, classHierarchy, scopeStack } = s;
386
+ const type = node.type;
387
+ const nameNode = this.findChild(node, (n) => n.type === "identifier");
388
+ if (nameNode) {
389
+ const name = nameNode.text;
390
+ if (name.trim() === "") {
391
+ s.emptyNameRefusals = (s.emptyNameRefusals ?? 0) + 1;
392
+ return;
393
+ }
394
+ const kind = type === "interface_declaration" || type === "trait_declaration" ? ASTEngine.SymbolKind.Interface : type === "enum_declaration" ? ASTEngine.SymbolKind.Enum : ASTEngine.SymbolKind.Class;
395
+ const fqn = this.buildFqn(s.currentPackage, scopeStack, name);
396
+ const visibility = this.extractVisibility(node);
397
+ const parents = this.extractInheritance(node, type);
398
+ classHierarchy.set(fqn, parents);
399
+ const classAnnotationRefs = this.extractAnnotationRefs(node);
400
+ const classAnnotations = classAnnotationRefs.map((a) => a.name);
401
+ if (type === "class_declaration") {
402
+ const superClass = this.findChild(node, (n) => n.type === "superclass");
403
+ if (superClass) {
404
+ const sc = this.findChild(superClass, (n) => n.type === "type_identifier" || n.type === "identifier" || n.type === "generic_type" || n.type === "scoped_type_identifier");
405
+ if (sc) {
406
+ relationships.push({
407
+ source: name,
408
+ target: this.baseTypeName(sc),
409
+ type: ASTEngine.RelationshipType.Extends,
410
+ confidence: 1,
411
+ // M59 (#825): the SUPERtype's own line, never the class header's.
412
+ ...this.lineField(sc, node),
413
+ metadata: { relationshipType: "extends" }
414
+ });
415
+ }
416
+ }
417
+ } else if (type === "interface_declaration") {
418
+ const ext = this.findChild(node, (n) => n.type === "extends_interfaces");
419
+ if (ext) {
420
+ for (const tid of this.collectTypeNodes(ext)) {
421
+ relationships.push({
422
+ source: name,
423
+ target: this.baseTypeName(tid),
424
+ type: ASTEngine.RelationshipType.Extends,
425
+ confidence: 1,
426
+ // M59 (#825): each interface parent is named on its OWN line.
427
+ ...this.lineField(tid, node),
428
+ metadata: { relationshipType: "extends_interface" }
429
+ });
430
+ }
431
+ }
432
+ } else if (type === "trait_declaration") {
433
+ const superClass = this.findChild(node, (n) => n.type === "superclass");
434
+ if (superClass) {
435
+ const sc = this.findChild(superClass, (n) => n.type === "type_identifier" || n.type === "identifier" || n.type === "generic_type" || n.type === "scoped_type_identifier");
436
+ if (sc) {
437
+ relationships.push({
438
+ source: name,
439
+ target: this.baseTypeName(sc),
440
+ type: ASTEngine.RelationshipType.Extends,
441
+ confidence: 1,
442
+ ...this.lineField(sc, node),
443
+ metadata: { relationshipType: "extends_trait" }
444
+ });
445
+ }
446
+ }
447
+ }
448
+ const superInterfaces = this.findChild(node, (n) => n.type === "super_interfaces");
449
+ if (superInterfaces) {
450
+ for (const tid of this.collectTypeNodes(superInterfaces)) {
451
+ relationships.push({
452
+ source: name,
453
+ target: this.baseTypeName(tid),
454
+ type: ASTEngine.RelationshipType.Implements,
455
+ confidence: 1,
456
+ // M59 (#825): each interface is named on its OWN line.
457
+ ...this.lineField(tid, node),
458
+ metadata: { relationshipType: "implements" }
459
+ });
460
+ }
461
+ }
462
+ const sym = {
463
+ name,
464
+ kind,
465
+ comments: (() => {
466
+ const d = this.extractGroovyDoc(node);
467
+ return d ? [d] : [];
468
+ })(),
469
+ genericParameters: (() => {
470
+ const tp = this.extractGroovyTypeParameters(node);
471
+ return tp.length > 0 ? tp : void 0;
472
+ })(),
473
+ // M59 (#825): annotations skipped — the class is written on its own line.
474
+ range: this.symbolRange(node),
475
+ visibility,
476
+ decorators: classAnnotations,
477
+ ownerName: scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0,
478
+ ownerKind: scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].kind : void 0,
479
+ metadata: { nodeType: type, annotations: classAnnotations, isRecord: type === "record_declaration", decoratorLines: classAnnotationRefs.map((a) => a.line) }
480
+ };
481
+ symbols.push(sym);
482
+ symbolMap.set(fqn, sym);
483
+ symbolMap.set(name, sym);
484
+ if (type === "record_declaration") {
485
+ const fp = this.findChild(node, (n) => n.type === "formal_parameters");
486
+ if (fp) {
487
+ for (let i = 0; i < fp.namedChildCount; i++) {
488
+ const p = fp.namedChild(i);
489
+ if (!p || p.type !== "formal_parameter") continue;
490
+ const pName = this.findChild(p, (n) => n.type === "identifier");
491
+ const pType = this.findChild(p, (n) => n.type === "type_identifier" || n.type === "integral_type" || n.type === "floating_point_type" || n.type === "boolean_type" || n.type === "generic_type" || n.type === "array_type" || n.type === "scoped_type_identifier");
492
+ if (pName) {
493
+ symbols.push({
494
+ name: pName.text,
495
+ kind: ASTEngine.SymbolKind.Property,
496
+ range: this.symbolRange(p),
497
+ returnType: pType ? this.baseTypeName(pType) : void 0,
498
+ visibility: "public",
499
+ ownerName: name,
500
+ ownerKind: kind,
501
+ metadata: { nodeType: "record_component" }
502
+ });
503
+ }
504
+ }
505
+ }
506
+ }
507
+ s.newContext = {
508
+ node,
509
+ name,
510
+ kind,
511
+ fqn,
512
+ symbolRef: sym,
513
+ visibility,
514
+ isInterface: type === "interface_declaration" || type === "trait_declaration",
515
+ parents
516
+ };
517
+ scopeStack.push(s.newContext);
518
+ s.pushedScope = true;
519
+ }
520
+ }
521
+ handleMethodDeclaration(node, s) {
522
+ const { symbols, symbolMap, overrideTasks, scopeStack } = s;
523
+ const type = node.type;
524
+ const isConstructorRaw = type === "constructor_declaration";
525
+ const nameNode = this.findChild(node, (n) => n.type === "identifier" || n.type === "string_literal");
526
+ if (nameNode) {
527
+ const rawName = nameNode.type === "string_literal" ? this.stripQuotes(nameNode.text) : nameNode.text;
528
+ if (rawName.trim() === "") {
529
+ s.emptyNameRefusals = (s.emptyNameRefusals ?? 0) + 1;
530
+ return;
531
+ }
532
+ const name = "constructor";
533
+ const modifiers = this.extractModifiers(node);
534
+ const visibility = this.extractVisibility(node);
535
+ const annotationRefs = this.extractAnnotationRefs(node);
536
+ const annotations = annotationRefs.map((a) => a.name);
537
+ const enclosingScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
538
+ const enclosingName = enclosingScope && (enclosingScope.kind === ASTEngine.SymbolKind.Class || enclosingScope.kind === ASTEngine.SymbolKind.Enum || enclosingScope.kind === ASTEngine.SymbolKind.Interface) ? enclosingScope.name : void 0;
539
+ const isMisnamedCtor = isConstructorRaw && enclosingName !== void 0 && rawName !== enclosingName;
540
+ const isConstructor = isConstructorRaw && !isMisnamedCtor;
541
+ const memberName = rawName;
542
+ const parameters = [];
543
+ const paramList = this.findChild(node, (n) => n.type === "formal_parameters");
544
+ if (paramList) {
545
+ for (let i = 0; i < paramList.namedChildCount; i++) {
546
+ const p = paramList.namedChild(i);
547
+ if (!p || p.type !== "formal_parameter" && p.type !== "spread_parameter") continue;
548
+ const isSpread = p.type === "spread_parameter";
549
+ let pName = this.findChild(p, (n) => n.type === "identifier");
550
+ if (isSpread) {
551
+ const decl = this.findChild(p, (n) => n.type === "variable_declarator");
552
+ pName = decl ? this.findChild(decl, (n) => n.type === "identifier") : null;
553
+ }
554
+ const pType = this.findChild(p, (n) => n.type === "type_identifier" || n.type === "integral_type" || n.type === "floating_point_type" || n.type === "boolean_type" || n.type === "void_type" || n.type === "generic_type" || n.type === "array_type" || n.type === "scoped_type_identifier");
555
+ parameters.push({
556
+ name: pName?.text ?? "anonymous",
557
+ type: pType ? this.baseTypeName(pType) + (isSpread ? "[]" : "") : void 0
558
+ });
559
+ }
560
+ }
561
+ const signature = `(${parameters.map((p) => p.type ?? "").join(",")})`;
562
+ const ownerName = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0;
563
+ const ownerKind = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].kind : void 0;
564
+ const fqn = this.buildFqn(s.currentPackage, scopeStack, memberName);
565
+ const hasBody = !!this.findChild(node, (n) => n.type === "block" || n.type === "closure" || n.type === "constructor_body");
566
+ const isAbstract = !hasBody || !!s.newContext?.isInterface && !hasBody;
567
+ const declLine = (node.childForFieldName("name") ?? nameNode ?? this.declarationLineNode(node)).startPosition.row + 1;
568
+ if (annotations.indexOf("Override") !== -1 && s.newContext) {
569
+ overrideTasks.push({
570
+ fqn,
571
+ methodName: memberName,
572
+ signature,
573
+ ownerFqn: s.newContext.fqn,
574
+ explicit: true,
575
+ line: declLine
576
+ });
577
+ } else if (s.newContext && !isConstructor && modifiers.indexOf("static") === -1 && visibility !== "private") {
578
+ overrideTasks.push({
579
+ fqn,
580
+ methodName: memberName,
581
+ signature,
582
+ ownerFqn: s.newContext.fqn,
583
+ explicit: false,
584
+ line: declLine
585
+ });
586
+ }
587
+ const sym = {
588
+ name: memberName,
589
+ kind: isConstructor ? ASTEngine.SymbolKind.Constructor : ASTEngine.SymbolKind.Method,
590
+ comments: (() => {
591
+ const d = this.extractGroovyDoc(node);
592
+ return d ? [d] : [];
593
+ })(),
594
+ complexity: this.calculateGroovyMethodComplexity(node),
595
+ // The M59 (#825) anchor for a method is its NAME, and it is its name
596
+ // ALWAYS — the annotation branch that used to sit here is gone.
597
+ //
598
+ // It existed because the symbol's range was being read as the
599
+ // DECLARATION's line, and the hand-audited truth recorded the
600
+ // ANNOTATION's line for an annotated method (`@Override` L16 /
601
+ // `format` L17 → truth 16). With the name as the anchor, the line is
602
+ // the line the method is SPELLED on, which is the contract the M59 note
603
+ // states ("the line the thing is spelled on, not the declaration's
604
+ // opening line") — and it is what the whole live family does
605
+ // (script/m14/groovy-family-line-anchor-probe.ts: java/kotlin/scala all
606
+ // report the declaration's line for an annotated method, groovy
607
+ // reported the annotation's).
608
+ //
609
+ // THIS LANE'S OWN FIXTURES pin it and forbid the other:
610
+ // groovy/lines-825 contains[Wrapped->use] @26, @25 FORBIDDEN
611
+ // groovy/anonymous-body-overrides overrides[anonymous_TypeCodec.bsonType->bsonType] @23
612
+ // groovy/ctor-field-split-lines contains[Split->Split] @19 (the NAME's line)
613
+ //
614
+ // The tree-sitter-groovy field-absorb case is unchanged by this and is
615
+ // still the reason the NAME is the anchor: `final Distance distance` +
616
+ // `Sphere(...)` parse as one method_declaration starting at the field, so
617
+ // the node start is the field's line while the name is written 7 lines
618
+ // lower (measured: grails-core Sphere.groovy).
619
+ range: this.symbolRange(
620
+ node,
621
+ node.childForFieldName("name") ?? nameNode
622
+ ),
623
+ visibility,
624
+ genericParameters: (() => {
625
+ const tp = this.extractGroovyTypeParameters(node);
626
+ return tp.length > 0 ? tp : void 0;
627
+ })(),
628
+ parameters,
629
+ decorators: annotations,
630
+ ownerName,
631
+ ownerKind,
632
+ metadata: { nodeType: type, modifiers, annotations, isAbstract, decoratorLines: annotationRefs.map((a) => a.line) }
633
+ };
634
+ symbols.push(sym);
635
+ symbolMap.set(fqn, sym);
636
+ const rawKey = ownerName ? `${ownerName}.${memberName}${signature}` : `${memberName}${signature}`;
637
+ symbolMap.set(rawKey, sym);
638
+ s.newContext = {
639
+ node,
640
+ name: memberName,
641
+ kind: isConstructor ? ASTEngine.SymbolKind.Constructor : ASTEngine.SymbolKind.Method,
642
+ fqn,
643
+ symbolRef: sym,
644
+ visibility,
645
+ isInterface: false,
646
+ parents: []
647
+ };
648
+ scopeStack.push(s.newContext);
649
+ s.pushedScope = true;
650
+ }
651
+ }
652
+ handleFieldDeclaration(node, s) {
653
+ const { symbols, scopeStack } = s;
654
+ const type = node.type;
655
+ const modifiers = this.extractModifiers(node);
656
+ const visibility = this.extractVisibility(node);
657
+ const isStatic = modifiers.indexOf("static") !== -1;
658
+ const isConstant = modifiers.indexOf("final") !== -1 && isStatic;
659
+ const fieldAnnotationRefs = this.extractAnnotationRefs(node);
660
+ const fieldAnnotations = fieldAnnotationRefs.map((a) => a.name);
661
+ const ownerName = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0;
662
+ const ownerKind = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].kind : void 0;
663
+ for (let i = 0; i < node.namedChildCount; i++) {
664
+ const child = node.namedChild(i);
665
+ if (child?.type !== "variable_declarator") continue;
666
+ const idNode = this.findChild(child, (n) => n.type === "identifier");
667
+ if (!idNode || !ownerName) continue;
668
+ if (idNode.text === "def") continue;
669
+ if (this.isDuplicateParseField(node, s.content)) continue;
670
+ const fieldName = idNode.text.trim();
671
+ if (fieldName === "") {
672
+ s.emptyNameRefusals = (s.emptyNameRefusals ?? 0) + 1;
673
+ continue;
674
+ }
675
+ symbols.push({
676
+ name: fieldName,
677
+ kind: isConstant ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Property,
678
+ range: this.symbolRange(child),
679
+ visibility,
680
+ ownerName,
681
+ ownerKind,
682
+ decorators: fieldAnnotations,
683
+ metadata: { nodeType: type, modifiers, isConstant, annotations: fieldAnnotations, decoratorLines: fieldAnnotationRefs.map((a) => a.line) }
684
+ });
685
+ }
686
+ }
687
+ handleEnumConstant(node, s) {
688
+ const { symbols, scopeStack } = s;
689
+ const type = node.type;
690
+ const nameNode = this.findChild(node, (n) => n.type === "identifier");
691
+ const ownerName = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0;
692
+ const enumMemberName = nameNode?.text.trim() ?? "";
693
+ if (enumMemberName !== "" && ownerName) {
694
+ symbols.push({
695
+ name: enumMemberName,
696
+ kind: ASTEngine.SymbolKind.EnumMember,
697
+ range: this.symbolRange(node),
698
+ visibility: "public",
699
+ ownerName,
700
+ ownerKind: ASTEngine.SymbolKind.Enum,
701
+ metadata: { nodeType: type }
702
+ });
703
+ }
704
+ }
705
+ handleMethodInvocation(node, s) {
706
+ const { relationships } = s;
707
+ const type = node.type;
708
+ const nameNode = node.childForFieldName("name");
709
+ if (nameNode) {
710
+ const methodName = nameNode.text;
711
+ const objectNode = node.childForFieldName("object");
712
+ const callExpr = this.buildGroovyQualifiedCall(objectNode, methodName);
713
+ const sourceName = s.parentContext?.name ?? "(script)";
714
+ if (!this.isGroovyBuiltin(callExpr)) {
715
+ if (s.parentContext?.symbolRef) {
716
+ if (!s.parentContext.symbolRef.qualified_calls) s.parentContext.symbolRef.qualified_calls = [];
717
+ s.parentContext.symbolRef.qualified_calls.push(callExpr);
718
+ }
719
+ relationships.push({
720
+ source: sourceName,
721
+ target: methodName,
722
+ type: ASTEngine.RelationshipType.Calls,
723
+ confidence: 0.7,
724
+ // M59 (#825): the method NAME's line, not the receiver's — a
725
+ // multi-line chain opens on the receiver (scala's measured defect).
726
+ ...this.lineField(nameNode, node),
727
+ metadata: { groovyCallExpression: callExpr }
728
+ });
729
+ }
730
+ }
731
+ }
732
+ handleExplicitConstructorInvocation(node, s) {
733
+ const { relationships } = s;
734
+ const type = node.type;
735
+ const first = node.namedChild(0);
736
+ if (first && (first.type === "super" || first.type === "this")) {
737
+ relationships.push({
738
+ source: s.parentContext?.name ?? "(script)",
739
+ target: first.type,
740
+ type: ASTEngine.RelationshipType.Calls,
741
+ confidence: 1,
742
+ ...this.lineField(first, node),
743
+ metadata: { relationshipType: "constructor_invocation" }
744
+ });
745
+ }
746
+ }
747
+ handleJuxtFunctionCall(node, s) {
748
+ const { relationships } = s;
749
+ const type = node.type;
750
+ const nameNode = node.childForFieldName("name");
751
+ if (nameNode && !this.isGroovyBuiltin(nameNode.text)) {
752
+ if (this.looksLikeTypedClosureMisparse(node, nameNode, s.content)) {
753
+ } else {
754
+ const leafNode = nameNode.type === "field_access" ? nameNode.childForFieldName("field") ?? nameNode : nameNode;
755
+ relationships.push({
756
+ source: s.parentContext?.name ?? "(script)",
757
+ target: leafNode.text,
758
+ type: ASTEngine.RelationshipType.Calls,
759
+ confidence: 0.7,
760
+ ...this.lineField(leafNode, node),
761
+ // The callee expression AS WRITTEN (the sibling method_invocation
762
+ // branch's payload) — kept so the dotted form is recoverable by a
763
+ // consumer even though the edge itself names the bare leaf.
764
+ metadata: { relationshipType: "juxt_call", groovyCallExpression: nameNode.text }
765
+ });
766
+ }
767
+ }
768
+ }
769
+ handleLeadingDotCall(node, s) {
770
+ const { relationships } = s;
771
+ const type = node.type;
772
+ const nameNode = node.childForFieldName("name");
773
+ if (nameNode && !this.isGroovyBuiltin(nameNode.text)) {
774
+ const args = node.childForFieldName("arguments") ?? this.findChild(node, (n) => n.type === "argument_list");
775
+ if (args) {
776
+ relationships.push({
777
+ source: s.parentContext?.name ?? "(script)",
778
+ target: nameNode.text,
779
+ type: ASTEngine.RelationshipType.Calls,
780
+ confidence: 0.7,
781
+ ...this.lineField(nameNode, node),
782
+ metadata: { relationshipType: "leading_dot_call" }
783
+ });
784
+ } else {
785
+ relationships.push({
786
+ source: s.parentContext?.name ?? "(script)",
787
+ target: nameNode.text,
788
+ type: ASTEngine.RelationshipType.Uses,
789
+ confidence: 0.6,
790
+ ...this.lineField(nameNode, node),
791
+ metadata: { isFieldAccess: true }
792
+ });
793
+ }
794
+ }
795
+ }
796
+ handleObjectCreationExpression(node, s) {
797
+ const { symbols, relationships, symbolMap, classHierarchy, scopeStack } = s;
798
+ const type = node.type;
799
+ const typeNode = this.findChild(node, (n) => n.type === "type_identifier" || n.type === "generic_type" || n.type === "scoped_type_identifier" || n.type === "identifier");
800
+ if (typeNode) {
801
+ const source = s.parentContext?.name || s.parentContext?.symbolRef?.name || "(script)";
802
+ relationships.push({
803
+ source,
804
+ target: this.baseTypeName(typeNode),
805
+ type: ASTEngine.RelationshipType.Instantiates,
806
+ confidence: 0.95,
807
+ // M59 (#825): the constructed TYPE's own line.
808
+ ...this.lineField(typeNode, node),
809
+ metadata: { relationshipType: "instantiates", isNew: true }
810
+ });
811
+ const classBody = this.findChild(node, (n) => n.type === "class_body");
812
+ if (classBody && s.parentContext) {
813
+ const superName = this.baseTypeName(typeNode);
814
+ const anonName = superName ? `anonymous_${superName}` : "anonymous_class";
815
+ const anonFqn = s.parentContext.fqn ? `${s.parentContext.fqn}.${anonName}` : anonName;
816
+ const anonSym = {
817
+ name: anonName,
818
+ kind: ASTEngine.SymbolKind.Class,
819
+ range: this.symbolRange(node),
820
+ visibility: s.parentContext.visibility || "public",
821
+ ownerName: s.parentContext.name,
822
+ ownerKind: s.parentContext.kind,
823
+ // `synthesized: true` is a DECLARATION, not an exemption request. The name is
824
+ // invented from the supertype because an anonymous class has no spelling, and
825
+ // the M60 surface audit's "name not present in source" / "ghost target" rows
826
+ // cannot tell that from a name the analyzer made up and passed off. Declaring
827
+ // it moves both rows into the audit's own "named by contract, not by source
828
+ // spelling" category -- counted, disclosed, and still failing for any
829
+ // UNDECLARED synthesized name, which is the case that actually matters.
830
+ metadata: { nodeType: type, isAnonymousClass: true, synthesized: true, superTypes: superName ? [superName] : [] }
831
+ };
832
+ symbols.push(anonSym);
833
+ symbolMap.set(anonFqn, anonSym);
834
+ classHierarchy.set(anonFqn, superName ? [superName] : []);
835
+ s.newContext = {
836
+ node: classBody,
837
+ name: anonName,
838
+ kind: ASTEngine.SymbolKind.Class,
839
+ fqn: anonFqn,
840
+ symbolRef: anonSym,
841
+ visibility: s.parentContext.visibility,
842
+ isInterface: false,
843
+ parents: superName ? [superName] : []
844
+ };
845
+ scopeStack.push(s.newContext);
846
+ s.pushedScope = true;
847
+ }
848
+ }
849
+ }
850
+ /**
851
+ * Framework detection from imports + annotation short names. The mapping is
852
+ * MEASURED against the grails-core corpus (top imported packages):
853
+ * the "grails" / "org.grails" / "org.apache.grails", "spock.lang" and
854
+ * "org.springframework" import families dominate real Groovy code.
855
+ * result.framework flows to
856
+ * brain_file.framework_type which patterns/rules.ts ranks on — Groovy
857
+ * files previously got NULL there, hiding Grails projects from pattern
858
+ * analysis. Primary selection follows a fixed priority so the same file
859
+ * always yields the same primary.
860
+ */
861
+ /**
862
+ * M14 #299 - evidence-tier classification for EVERY emitted relationship.
863
+ * Tiers: structural (grammar clauses, conf 1.0) | usage_contextual (calls,
864
+ * field access, companion-apply idioms) | import_based | reference_qualified
865
+ * | inferred (sub-1.0 resolutions). Applied centrally at end of analyze()
866
+ * so no emission site can forget it.
867
+ */
868
+ classifyEvidenceTier(relationships) {
869
+ for (const r of relationships) {
870
+ const md = r.metadata ?? (r.metadata = {});
871
+ if (md.evidenceClass) continue;
872
+ const t = String(r.type ?? r.relationshipType ?? "");
873
+ const conf = typeof r.confidence === "number" ? r.confidence : 1;
874
+ if (t === "contains" || t === "exports") {
875
+ md.evidenceClass = "structural";
876
+ continue;
877
+ }
878
+ if (t === "imports") {
879
+ md.evidenceClass = "import_based";
880
+ continue;
881
+ }
882
+ if (t === "references") {
883
+ md.evidenceClass = md.isGroovyImport || md.isImport ? "import_based" : "reference_qualified";
884
+ continue;
885
+ }
886
+ if (t === "imports") {
887
+ md.evidenceClass = "import_based";
888
+ continue;
889
+ }
890
+ if (t === "extends" || t === "implements") {
891
+ md.evidenceClass = conf >= 1 ? "structural" : "inferred";
892
+ continue;
893
+ }
894
+ if (t === "overrides") {
895
+ md.evidenceClass = conf >= 1 ? "structural" : conf >= 0.85 ? "usage_contextual" : "inferred";
896
+ continue;
897
+ }
898
+ if (t === "instantiates") {
899
+ md.evidenceClass = md.isNew || conf >= 0.9 ? "structural" : "usage_contextual";
900
+ continue;
901
+ }
902
+ if (t === "decorates") {
903
+ md.evidenceClass = "structural";
904
+ continue;
905
+ }
906
+ md.evidenceClass = "usage_contextual";
907
+ }
908
+ }
909
+ detectGroovyFrameworks(content, symbols, imports) {
910
+ const frameworks = /* @__PURE__ */ new Set();
911
+ for (let i = 0; i < imports.length; i++) {
912
+ const mod = imports[i].module || "";
913
+ if (mod.startsWith("grails.") || mod.startsWith("org.grails.") || mod.startsWith("org.apache.grails.")) frameworks.add("grails");
914
+ else if (mod.startsWith("org.springframework.")) frameworks.add("spring");
915
+ else if (mod.startsWith("spock.lang.")) frameworks.add("spock");
916
+ else if (mod.startsWith("javax.persistence.") || mod.startsWith("jakarta.persistence.")) frameworks.add("jpa");
917
+ else if (mod.startsWith("org.hibernate.")) frameworks.add("hibernate");
918
+ else if (mod.startsWith("org.junit.") || mod.startsWith("org.spockframework.")) frameworks.add("junit");
919
+ }
920
+ for (let i = 0; i < symbols.length; i++) {
921
+ const anns = symbols[i].metadata?.annotations ?? [];
922
+ for (let j = 0; j < anns.length; j++) {
923
+ const a = anns[j];
924
+ if (a === "Service" || a === "RestController" || a === "Controller" || a === "Repository" || a === "Component") frameworks.add("spring");
925
+ }
926
+ }
927
+ for (const priority of ["grails", "spring", "hibernate", "jpa", "spock", "junit"]) {
928
+ if (frameworks.has(priority)) return { framework: priority, detected: [...frameworks] };
929
+ }
930
+ return { framework: void 0, detected: [...frameworks] };
931
+ }
932
+ resolveOverrides(tasks, classHierarchy, symbolMap, relationships, companion) {
933
+ let suffixIndex = null;
934
+ for (let ti = 0; ti < tasks.length; ti++) {
935
+ const task = tasks[ti];
936
+ let targetFqn = null;
937
+ let confidence = 1;
938
+ let reasonCode = "exact-in-file";
939
+ if (!suffixIndex) {
940
+ suffixIndex = /* @__PURE__ */ new Map();
941
+ for (const fqn of classHierarchy.keys()) {
942
+ const bare = fqn.slice(fqn.lastIndexOf(".") + 1);
943
+ const arr = suffixIndex.get(bare);
944
+ if (arr) arr.push(fqn);
945
+ else suffixIndex.set(bare, [fqn]);
946
+ }
947
+ }
948
+ const queue = [];
949
+ const ownerH = classHierarchy.get(task.ownerFqn);
950
+ if (ownerH) {
951
+ for (let i = 0; i < ownerH.length; i++) queue.push(ownerH[i]);
952
+ }
953
+ const seen = /* @__PURE__ */ new Set();
954
+ let head = 0;
955
+ while (head < queue.length && !targetFqn) {
956
+ const parent = queue[head++];
957
+ if (seen.has(parent)) continue;
958
+ seen.add(parent);
959
+ const candidates = suffixIndex.get(parent) ?? [parent];
960
+ for (const cand of candidates) {
961
+ if (!classHierarchy.has(cand)) continue;
962
+ const ownerBare = cand.slice(cand.lastIndexOf(".") + 1);
963
+ const potential = `${ownerBare}.${task.methodName}${task.signature}`;
964
+ if (symbolMap.has(potential)) {
965
+ targetFqn = `${cand}.${task.methodName}${task.signature}`;
966
+ break;
967
+ }
968
+ }
969
+ if (targetFqn) break;
970
+ for (const cand of candidates) {
971
+ const pH = classHierarchy.get(cand);
972
+ if (pH) {
973
+ for (let i = 0; i < pH.length; i++) queue.push(pH[i]);
974
+ }
975
+ }
976
+ }
977
+ if (!targetFqn && task.explicit && companion && ownerH && ownerH.length > 0) {
978
+ const declaring = findDeclaringParent(
979
+ task.methodName,
980
+ ownerH,
981
+ companion,
982
+ (t) => classHierarchy.get(t) ?? []
983
+ );
984
+ if (declaring) {
985
+ targetFqn = `${declaring}.${task.methodName}${task.signature}`;
986
+ confidence = 0.85;
987
+ reasonCode = "companion-index";
988
+ }
989
+ }
990
+ if (!targetFqn && task.explicit && ownerH && ownerH.length > 0) {
991
+ targetFqn = `${ownerH[0]}.${task.methodName}${task.signature}`;
992
+ confidence = 0.5;
993
+ reasonCode = "first-parent-guess";
994
+ }
995
+ if (targetFqn) {
996
+ const localSource = localEdgeLabel(task.fqn);
997
+ const targetLabel = localEdgeLabel(targetFqn);
998
+ if (localSource === targetLabel) continue;
999
+ relationships.push({
1000
+ source: localSource,
1001
+ // THE TARGET IS THE DECLARED MEMBER, NOT `Owner.member` — this lane resolved 0 of 253 with
1002
+ // the dotted spelling, and the label helper it was using is the one the four-lane fix now
1003
+ // pairs with `localEdgeName`.
1004
+ target: localEdgeName(targetFqn),
1005
+ type: ASTEngine.RelationshipType.Overrides,
1006
+ confidence,
1007
+ // M59 (#825): the overriding declaration's line — captured while the
1008
+ // node was in hand, not resolved later by name (namesakes collide).
1009
+ ...task.line ? { line_number: task.line } : {},
1010
+ metadata: {
1011
+ relationshipType: "method_override",
1012
+ isGroovyOverride: true,
1013
+ reasonCode,
1014
+ // THE OWNER IS CARRIED, not dropped — see the cpp site's note on why the four-lane fix
1015
+ // needed it and did not have it.
1016
+ overriddenOwner: targetLabel.includes(".") ? targetLabel.slice(0, targetLabel.lastIndexOf(".")) : ""
1017
+ }
1018
+ });
1019
+ }
1020
+ }
1021
+ }
1022
+ /**
1023
+ * Declared-type reference graph (M14). References edges from the enclosing
1024
+ * method/class to every named type in a declaration position. Collection
1025
+ * descends but stops at value boundaries (variable_declarator values,
1026
+ * closures, blocks) so initializer contents are never mistaken for the
1027
+ * declared type clause.
1028
+ */
1029
+ collectDeclaredTypeRefs(rootNode, relationships) {
1030
+ const emitted = /* @__PURE__ */ new Set();
1031
+ const TYPE_STOP = /* @__PURE__ */ new Set(["variable_declarator", "closure", "block", "closure_expression"]);
1032
+ const isPlainTypeName = (t) => {
1033
+ if (t.length === 0) return false;
1034
+ for (let i = 0; i < t.length; i++) {
1035
+ const c = t.charCodeAt(i);
1036
+ const ok = c >= 97 && c <= 122 || // a-z
1037
+ c >= 65 && c <= 90 || // A-Z
1038
+ c >= 48 && c <= 57 || // 0-9
1039
+ c === 95 || // _
1040
+ c === 36 || // $ (a legal groovy/java identifier character)
1041
+ c === 46;
1042
+ if (!ok) return false;
1043
+ }
1044
+ return true;
1045
+ };
1046
+ const collectTypeNames = (n, out) => {
1047
+ if (n.type === "type_identifier") {
1048
+ if (n.text !== "def" && n.text !== "as" && isPlainTypeName(n.text) && !out.some((o) => o.name === n.text)) out.push({ name: n.text, node: n });
1049
+ return;
1050
+ }
1051
+ if (n.type === "record_pattern") {
1052
+ const t = n.namedChild(0);
1053
+ if (t && t.type === "identifier" && t.text !== "this" && !out.some((o) => o.name === t.text)) out.push({ name: t.text, node: t });
1054
+ return;
1055
+ }
1056
+ if (n.type === "type_arguments") {
1057
+ for (let i = 0; i < n.namedChildCount; i++) {
1058
+ const c = n.namedChild(i);
1059
+ if (c) collectTypeNames(c, out);
1060
+ }
1061
+ return;
1062
+ }
1063
+ if (n.type === "scoped_type_identifier") {
1064
+ const text = n.text;
1065
+ if (!isPlainTypeName(text)) return;
1066
+ if (!out.some((o) => o.name === text)) out.push({ name: text, node: n });
1067
+ const args = this.findChild(n, (c) => c.type === "type_arguments");
1068
+ if (args) collectTypeNames(args, out);
1069
+ return;
1070
+ }
1071
+ if (TYPE_STOP.has(n.type)) return;
1072
+ for (let i = 0; i < n.namedChildCount; i++) {
1073
+ const c = n.namedChild(i);
1074
+ if (c) collectTypeNames(c, out);
1075
+ }
1076
+ };
1077
+ const resolveSource = (start) => {
1078
+ let p = GROOVY_DECLARATION_TYPES.has(start.type) ? start : start.parent;
1079
+ while (p) {
1080
+ if (p.type === "method_declaration" || p.type === "constructor_declaration" || p.type === "function_definition") {
1081
+ const name = this.findChild(p, (c) => c.type === "identifier");
1082
+ if (name) return name.text;
1083
+ }
1084
+ if (p.type === "class_declaration" || p.type === "trait_declaration" || p.type === "enum_declaration") {
1085
+ const name = this.findChild(p, (c) => c.type === "identifier");
1086
+ if (name) return name.text;
1087
+ }
1088
+ p = p.parent;
1089
+ }
1090
+ return "(script)";
1091
+ };
1092
+ const emit = (decl, roots, kind, relType = "declared_type") => {
1093
+ const source = resolveSource(decl);
1094
+ for (const r of roots) {
1095
+ const names = [];
1096
+ collectTypeNames(r, names);
1097
+ for (const t of names) {
1098
+ const key = `${source}|${t.name}|${kind}`;
1099
+ if (emitted.has(key)) continue;
1100
+ emitted.add(key);
1101
+ relationships.push({
1102
+ source,
1103
+ target: t.name,
1104
+ type: ASTEngine.RelationshipType.References,
1105
+ confidence: 0.95,
1106
+ // M59 (#825): the line the TYPE is named on. A wrapped generic
1107
+ // (`List<\n Map<String, Int>>`) opens on an earlier line than its
1108
+ // arguments, so the enclosing type expression's line is wrong for
1109
+ // every nested name (scala's measured defect class).
1110
+ ...this.lineField(t.node, decl),
1111
+ metadata: { relationshipType: relType, declaredOn: decl.type }
1112
+ });
1113
+ }
1114
+ }
1115
+ };
1116
+ const visited = /* @__PURE__ */ new Set();
1117
+ const walk = (n) => {
1118
+ if (visited.has(n.id)) return;
1119
+ visited.add(n.id);
1120
+ switch (n.type) {
1121
+ case "binary_expression": {
1122
+ const asType = n.childForFieldName("type");
1123
+ if (asType && n.childForFieldName("left")) {
1124
+ emit(n, [asType], "as_coercion", "cast_type");
1125
+ break;
1126
+ }
1127
+ break;
1128
+ }
1129
+ case "field_declaration":
1130
+ case "formal_parameter":
1131
+ case "local_variable_declaration": {
1132
+ if (n.type === "local_variable_declaration") {
1133
+ let asKw = null;
1134
+ let asDecl = null;
1135
+ for (let i = 0; i < n.namedChildCount; i++) {
1136
+ const c = n.namedChild(i);
1137
+ if (!c) continue;
1138
+ if ((c.type === "type_identifier" || c.type === "identifier") && c.text === "as") asKw = c;
1139
+ else if (c.type === "variable_declarator") asDecl = c;
1140
+ }
1141
+ if (asKw && asDecl) {
1142
+ const names = [];
1143
+ const tstack = [asDecl];
1144
+ while (tstack.length > 0) {
1145
+ const m = tstack.pop();
1146
+ if (m.type === "type_identifier" || m.type === "identifier") {
1147
+ if (m.text !== "as" && m.text !== "def" && !names.some((o) => o.name === m.text)) names.push({ name: m.text, node: m });
1148
+ } else {
1149
+ for (let i = 0; i < m.namedChildCount; i++) {
1150
+ const c = m.namedChild(i);
1151
+ if (c) tstack.push(c);
1152
+ }
1153
+ }
1154
+ }
1155
+ const source = resolveSource(n);
1156
+ for (const t of names) {
1157
+ const key = `${source}|${t.name}|as_cast`;
1158
+ if (emitted.has(key)) continue;
1159
+ emitted.add(key);
1160
+ relationships.push({
1161
+ source,
1162
+ target: t.name,
1163
+ type: ASTEngine.RelationshipType.References,
1164
+ confidence: 0.95,
1165
+ ...this.lineField(t.node, n),
1166
+ metadata: { relationshipType: "cast_type", declaredOn: "as_cast" }
1167
+ });
1168
+ }
1169
+ break;
1170
+ }
1171
+ }
1172
+ const roots = [];
1173
+ for (let i = 0; i < n.namedChildCount; i++) {
1174
+ const c = n.namedChild(i);
1175
+ if (!c || TYPE_STOP.has(c.type)) continue;
1176
+ if (c.type.endsWith("type") || c.type === "type_identifier" || c.type === "scoped_type_identifier") roots.push(c);
1177
+ }
1178
+ if (roots.length) emit(n, roots, n.type);
1179
+ break;
1180
+ }
1181
+ case "throws": {
1182
+ const kids = [];
1183
+ for (let i = 0; i < n.namedChildCount; i++) {
1184
+ const c = n.namedChild(i);
1185
+ if (c) kids.push(c);
1186
+ }
1187
+ emit(n, kids, "throws", "throws_type");
1188
+ break;
1189
+ }
1190
+ case "catch_formal_parameter": {
1191
+ const catchType = this.findChild(n, (c) => c.type === "catch_type");
1192
+ if (catchType) emit(n, [catchType], "catch_formal_parameter", "declared_type");
1193
+ break;
1194
+ }
1195
+ case "instanceof_expression": {
1196
+ for (let i = 0; i < n.namedChildCount; i++) {
1197
+ const c = n.namedChild(i);
1198
+ if (c) emit(n, [c], n.type, "instanceof_expression");
1199
+ }
1200
+ break;
1201
+ }
1202
+ case "cast_expression": {
1203
+ const first = n.namedChild(0);
1204
+ if (first) emit(n, [first], n.type, "cast_type");
1205
+ break;
1206
+ }
1207
+ case "switch_label": {
1208
+ for (let i = 0; i < n.namedChildCount; i++) {
1209
+ const c = n.namedChild(i);
1210
+ if (c && c.type === "pattern") emit(n, [c], "switch_pattern", "case_pattern");
1211
+ }
1212
+ break;
1213
+ }
1214
+ case "method_reference": {
1215
+ const first = n.namedChild(0);
1216
+ if (first && first.type === "identifier" && first.text !== "this") {
1217
+ const source = resolveSource(n);
1218
+ const key = `${source}|${first.text}|method_reference`;
1219
+ if (!emitted.has(key)) {
1220
+ emitted.add(key);
1221
+ relationships.push({
1222
+ source,
1223
+ target: first.text,
1224
+ type: ASTEngine.RelationshipType.References,
1225
+ confidence: 0.9,
1226
+ ...this.lineField(first, n),
1227
+ metadata: { relationshipType: "method_reference" }
1228
+ });
1229
+ }
1230
+ }
1231
+ break;
1232
+ }
1233
+ case "juxt_function_call": {
1234
+ const first = n.namedChild(0);
1235
+ if (first && first.text === "as") {
1236
+ for (let i = 0; i < n.namedChildCount; i++) {
1237
+ const c = n.namedChild(i);
1238
+ if (c && c.type === "argument_list") emit(n, [c], "as_expression", "cast_type");
1239
+ }
1240
+ }
1241
+ break;
1242
+ }
1243
+ case "method_declaration": {
1244
+ const roots = [];
1245
+ for (let i = 0; i < n.namedChildCount; i++) {
1246
+ const c = n.namedChild(i);
1247
+ if (!c) continue;
1248
+ if (c.type === "identifier") break;
1249
+ if (c.type === "modifiers") continue;
1250
+ if (c.type.endsWith("_type") || c.type === "type_identifier" || c.type === "scoped_type_identifier") roots.push(c);
1251
+ else break;
1252
+ }
1253
+ if (roots.length) emit(n, roots, "return_type", "return_type");
1254
+ break;
1255
+ }
1256
+ }
1257
+ for (let i = 0; i < n.namedChildCount; i++) {
1258
+ const c = n.namedChild(i);
1259
+ if (c) walk(c);
1260
+ }
1261
+ };
1262
+ walk(rootNode);
1263
+ }
1264
+ collectFieldAccessUses(rootNode, relationships, symbols) {
1265
+ const stack = [
1266
+ { node: rootNode, owner: null, method: null }
1267
+ ];
1268
+ const seen = /* @__PURE__ */ new Set();
1269
+ const boundCache = /* @__PURE__ */ new Map();
1270
+ const boundNamesOf = (scope) => {
1271
+ const cached = boundCache.get(scope.id);
1272
+ if (cached) return cached;
1273
+ const out = /* @__PURE__ */ new Set();
1274
+ const push = (n) => {
1275
+ if (n && n.type === "identifier") out.add(n.text);
1276
+ };
1277
+ const work = [];
1278
+ for (let i = 0; i < scope.namedChildCount; i++) {
1279
+ const c = scope.namedChild(i);
1280
+ if (c) work.push(c);
1281
+ }
1282
+ while (work.length > 0) {
1283
+ const n = work.pop();
1284
+ if (n.type === "formal_parameter" || n.type === "catch_formal_parameter") push(n.childForFieldName("name"));
1285
+ else if (n.type === "variable_declarator") push(n.childForFieldName("name"));
1286
+ for (let i = 0; i < n.namedChildCount; i++) {
1287
+ const c = n.namedChild(i);
1288
+ if (c) work.push(c);
1289
+ }
1290
+ }
1291
+ if (scope.type === "closure") out.add("it");
1292
+ boundCache.set(scope.id, out);
1293
+ return out;
1294
+ };
1295
+ const locallyBoundNames = (n) => {
1296
+ const acc = /* @__PURE__ */ new Set();
1297
+ let p = n.parent;
1298
+ let guard = 0;
1299
+ while (p && guard++ < 24) {
1300
+ if (p.type === "method_declaration" || p.type === "constructor_declaration" || p.type === "function_definition" || p.type === "lambda_expression" || p.type === "closure" || p.type === "program") {
1301
+ for (const t of boundNamesOf(p)) acc.add(t);
1302
+ }
1303
+ if (p.type === "class_declaration" || p.type === "interface_declaration" || p.type === "trait_declaration" || p.type === "enum_declaration") break;
1304
+ p = p.parent;
1305
+ }
1306
+ return acc;
1307
+ };
1308
+ while (stack.length > 0) {
1309
+ const item = stack.pop();
1310
+ const { node, owner, method } = item;
1311
+ if (node.type === "class_declaration" || node.type === "interface_declaration" || node.type === "trait_declaration" || node.type === "enum_declaration") {
1312
+ const nameNode = this.findChild(node, (n) => n.type === "identifier");
1313
+ const nextOwner = nameNode ? nameNode.text : owner;
1314
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
1315
+ const child = node.namedChild(i);
1316
+ if (child) stack.push({ node: child, owner: nextOwner, method });
1317
+ }
1318
+ continue;
1319
+ }
1320
+ let nextMethod = method;
1321
+ if (node.type === "method_declaration" || node.type === "constructor_declaration" || node.type === "function_definition") {
1322
+ const nameNode = this.findChild(node, (n) => n.type === "identifier");
1323
+ const declaredName = nameNode?.text?.trim() ? nameNode.text : void 0;
1324
+ const isRealCtor = node.type === "constructor_declaration" && (owner === void 0 || declaredName === owner);
1325
+ nextMethod = isRealCtor ? "constructor" : declaredName ?? method;
1326
+ }
1327
+ if (node.type === "field_access") {
1328
+ const parts = node.text.split(".").map((p) => p.endsWith("?") || p.endsWith("*") ? p.slice(0, -1) : p);
1329
+ if (parts.length >= 2) {
1330
+ const first = this.fieldAccessReceiverName(node);
1331
+ const readOwner = method ?? owner;
1332
+ const source = first === "this" || first === "super" ? owner ?? first : readOwner ?? first ?? owner ?? "(script)";
1333
+ const pType = node.parent?.type;
1334
+ let isWrite = false;
1335
+ if (pType === "assignment_expression") {
1336
+ const lhs = node.parent.namedChild(0);
1337
+ isWrite = !!lhs && lhs.startIndex === node.startIndex && lhs.endIndex === node.endIndex;
1338
+ }
1339
+ const parentField = node.parent?.type === "juxt_function_call" ? node.parent.childForFieldName("name") : null;
1340
+ const isCalleePosition = !!parentField && parentField.startIndex === node.startIndex && parentField.endIndex === node.endIndex;
1341
+ const inConstructorBody = method === "constructor";
1342
+ const thisLhsInConstructor = inConstructorBody && isWrite && (first === "this" || first === "super");
1343
+ const leafNode = node.childForFieldName("field");
1344
+ const leafName = leafNode ? leafNode.type === "string_literal" || leafNode.type === "character_literal" ? this.stripQuotes(leafNode.text) : leafNode.text : parts[parts.length - 1];
1345
+ const key = `${source}.${leafName}`;
1346
+ if ((!isWrite || thisLhsInConstructor) && !isCalleePosition && !seen.has(key)) {
1347
+ seen.add(key);
1348
+ relationships.push({
1349
+ source,
1350
+ target: leafName,
1351
+ type: ASTEngine.RelationshipType.Uses,
1352
+ // 0.6 for an ordinary read, but 1.0 for a `this.x = …` in a constructor. The 0.6 is
1353
+ // a real claim-strength discount for an INFERRED read; this edge is not inferred.
1354
+ // `this.<field>` names a member of the enclosing type by construction, the type is
1355
+ // known statically, and the ground truth cites the exact source line for all four such
1356
+ // rows (User->id, User->name, Robot->name, Counter->count). At 0.6 the harness
1357
+ // classified the observed fact as state "unknown" -- anything below 0.7 is
1358
+ // emitted-but-not-claimed (script/m12 relationshipFacts) -- while the truth row is
1359
+ // "resolved", so a correct edge scored as a false NEGATIVE.
1360
+ confidence: thisLhsInConstructor ? 1 : 0.6,
1361
+ // M59 (#825): the `field` field of the access node names the
1362
+ // member — measured populated in the pinned grammar
1363
+ // (`repo.items` -> [identifier, identifier], field=identifier),
1364
+ // so a multi-line receiver chain does not drag the read onto the
1365
+ // receiver's line.
1366
+ ...this.lineField(node.childForFieldName("field"), node),
1367
+ metadata: { isFieldAccess: true }
1368
+ });
1369
+ }
1370
+ }
1371
+ }
1372
+ if (node.type === "array_access") {
1373
+ const parent = node.parent;
1374
+ const lhs = parent && parent.type === "assignment_expression" ? parent.namedChild(0) : null;
1375
+ const isWrite = !!lhs && lhs.startIndex === node.startIndex && lhs.endIndex === node.endIndex;
1376
+ const first = node.namedChild(0);
1377
+ if (!isWrite && first && first.type === "identifier") {
1378
+ const source = method ?? owner ?? first.text;
1379
+ const key = `arr:${source}.${first.text}`;
1380
+ if (!seen.has(key)) {
1381
+ seen.add(key);
1382
+ relationships.push({
1383
+ source,
1384
+ target: first.text,
1385
+ type: ASTEngine.RelationshipType.Uses,
1386
+ confidence: 0.6,
1387
+ ...this.lineField(first, node),
1388
+ metadata: { isArrayAccess: true }
1389
+ });
1390
+ }
1391
+ }
1392
+ }
1393
+ if (node.type === "interpolated_identifier" || node.type === "string_interpolation") {
1394
+ const source = method ?? owner;
1395
+ const candidates = [];
1396
+ if (node.type === "interpolated_identifier") {
1397
+ const parentType = node.parent?.type ?? "";
1398
+ if (parentType === "juxt_function_call" || parentType === "field_access") continue;
1399
+ candidates.push(node);
1400
+ } else {
1401
+ const collect = (n) => {
1402
+ if (n.type === "identifier") {
1403
+ const pt = n.parent?.type ?? "";
1404
+ if (!INTERPOLATION_OWNED_BY_MEMBER_WALK.has(pt)) candidates.push(n);
1405
+ return;
1406
+ }
1407
+ for (let i = 0; i < n.namedChildCount; i++) {
1408
+ const c = n.namedChild(i);
1409
+ if (c) collect(c);
1410
+ }
1411
+ };
1412
+ for (let i = 0; i < node.namedChildCount; i++) {
1413
+ const c = node.namedChild(i);
1414
+ if (c) collect(c);
1415
+ }
1416
+ }
1417
+ for (const n of candidates) {
1418
+ const name = n.text.startsWith("$") ? n.text.slice(1) : n.text;
1419
+ if (!name || !source) continue;
1420
+ if (locallyBoundNames(n).has(name)) continue;
1421
+ const key = `interp:${source}.${name}`;
1422
+ if (seen.has(key)) continue;
1423
+ seen.add(key);
1424
+ relationships.push({
1425
+ source,
1426
+ target: name,
1427
+ type: ASTEngine.RelationshipType.Uses,
1428
+ confidence: 0.9,
1429
+ ...this.lineField(n, n),
1430
+ metadata: { isInterpolationRead: true }
1431
+ });
1432
+ }
1433
+ }
1434
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
1435
+ const child = node.namedChild(i);
1436
+ if (child) stack.push({ node: child, owner, method: nextMethod });
1437
+ }
1438
+ }
1439
+ }
1440
+ /**
1441
+ * M60 ontology pass: the NAMEABLE receiver of a `field_access`, or `undefined`
1442
+ * when the receiver is an expression rather than a name.
1443
+ *
1444
+ * Nameable means the receiver's own text is an identifier the reader can look up:
1445
+ * `a` -> "a" `a.b` -> "a" `a.b.c` -> "a"
1446
+ * `this` -> "this" `super` -> "super" `i?.j` -> "i"
1447
+ * `k*.l` -> "k" (safe-nav and spread receivers are plain identifiers)
1448
+ *
1449
+ * NOT nameable, and deliberately `undefined` rather than a text fragment:
1450
+ * `arr[0].f` receiver is an `array_access` -> undefined
1451
+ * `new File(x).g` receiver is an `object_creation_expression`
1452
+ * `foo().h` receiver is a `method_invocation`
1453
+ * `(m).n` receiver is a `parenthesized_expression`
1454
+ *
1455
+ * Returning the raw text for those is what produced the measured defect: the
1456
+ * `node.text.split(".")[0]` this replaces yielded `arr[0]`, `new File(x` and
1457
+ * `(m)` as relationship SOURCES — strings no symbol carries and no consumer can
1458
+ * resolve. `undefined` is the honest answer, and the call site then applies the
1459
+ * rule the walk already used for an unresolvable receiver: attribute the read to
1460
+ * the enclosing method.
1461
+ *
1462
+ * Probe-verified node shapes (`object`/`field` are populated for every one,
1463
+ * including `?.` and `*.`): script/m14/tmp/gv-probe6.mts, and the corpus count is
1464
+ * `script/m60/groovy-r7-census.ts`'s `malformedNameEndpoints`.
1465
+ */
1466
+ fieldAccessReceiverName(node) {
1467
+ let cur = node.childForFieldName("object");
1468
+ while (cur) {
1469
+ if (cur.type === "identifier" || cur.type === "this" || cur.type === "super") return cur.text;
1470
+ if (cur.type === "field_access") {
1471
+ cur = cur.childForFieldName("object");
1472
+ continue;
1473
+ }
1474
+ return void 0;
1475
+ }
1476
+ return void 0;
1477
+ }
1478
+ isGroovyBuiltin(call) {
1479
+ if (GROOVY_BUILTINS.has(call)) return true;
1480
+ const firstDot = call.indexOf(".");
1481
+ const root = firstDot > 0 ? call.slice(0, firstDot) : call;
1482
+ const STDLIB_ROOTS = /* @__PURE__ */ new Set([
1483
+ "System",
1484
+ "String",
1485
+ "Integer",
1486
+ "Long",
1487
+ "Double",
1488
+ "Float",
1489
+ "Boolean",
1490
+ "Byte",
1491
+ "Short",
1492
+ "Character",
1493
+ "Math",
1494
+ "StrictMath",
1495
+ "Object",
1496
+ "Class",
1497
+ "Thread",
1498
+ "Runnable",
1499
+ "Runtime",
1500
+ "Process",
1501
+ "ProcessBuilder",
1502
+ "Throwable",
1503
+ "Error",
1504
+ "Exception",
1505
+ "StackTraceElement",
1506
+ "Enum",
1507
+ "Void",
1508
+ "Compiler",
1509
+ "Package",
1510
+ "ArrayList",
1511
+ "LinkedList",
1512
+ "HashMap",
1513
+ "HashSet",
1514
+ "TreeMap",
1515
+ "TreeSet",
1516
+ "LinkedHashMap",
1517
+ "LinkedHashSet",
1518
+ "ArrayDeque",
1519
+ "PriorityQueue",
1520
+ "Collections",
1521
+ "Arrays",
1522
+ "Objects",
1523
+ "Optional",
1524
+ "Comparator",
1525
+ "List",
1526
+ "Map",
1527
+ "Set",
1528
+ "Collection",
1529
+ "Iterator",
1530
+ "Iterable",
1531
+ "Calendar",
1532
+ "Date",
1533
+ "UUID",
1534
+ "Locale",
1535
+ "Currency",
1536
+ "File",
1537
+ "Path",
1538
+ "Paths",
1539
+ "Files",
1540
+ "BigDecimal",
1541
+ "BigInteger",
1542
+ "AtomicInteger",
1543
+ "AtomicLong",
1544
+ "StringBuilder",
1545
+ "StringBuffer",
1546
+ "Pattern",
1547
+ "Matcher",
1548
+ "Random",
1549
+ "Logger",
1550
+ "Level",
1551
+ "OutputStream",
1552
+ "InputStream",
1553
+ "Reader",
1554
+ "Writer",
1555
+ "URL",
1556
+ "URI",
1557
+ "Socket",
1558
+ "ServerSocket"
1559
+ ]);
1560
+ if (STDLIB_ROOTS.has(root)) return true;
1561
+ return false;
1562
+ }
1563
+ /**
1564
+ * Groovydoc extraction: block_comment starting with '/**' immediately
1565
+ * preceding the declaration. Previously groovy symbols carried NO
1566
+ * comments (M14 #383).
1567
+ */
1568
+ /**
1569
+ * Per-method cyclomatic complexity (1 + decision points) - java/kotlin
1570
+ * parity (M14 #301 prerequisite). Previously always absent.
1571
+ */
1572
+ buildGroovyQualifiedCall(objectNode, methodName) {
1573
+ if (!objectNode) return methodName;
1574
+ if (objectNode.type === "method_invocation") {
1575
+ const innerName = objectNode.childForFieldName("name");
1576
+ const innerObject = objectNode.childForFieldName("object");
1577
+ const base = this.buildGroovyQualifiedCall(innerObject, innerName?.text ?? "");
1578
+ return `${base}.${methodName}`;
1579
+ }
1580
+ return `${objectNode.text}.${methodName}`;
1581
+ }
1582
+ calculateGroovyMethodComplexity(node) {
1583
+ let complexity = 1;
1584
+ const stack = [node];
1585
+ while (stack.length > 0) {
1586
+ const n = stack.pop();
1587
+ if (n.type === "if_statement" || n.type === "for_statement" || n.type === "enhanced_for_statement" || // for-in loops parsed as this, not for_statement
1588
+ n.type === "while_statement" || n.type === "do_statement" || // do-while (NOT "do_while")
1589
+ n.type === "switch_block_statement_group" || // per-case granularity (family contract)
1590
+ n.type === "catch_clause" || // NOT "catch_block"
1591
+ n.type === "ternary_expression") {
1592
+ complexity++;
1593
+ } else if (n.type === "binary_expression") {
1594
+ const op = n.childForFieldName("operator")?.text;
1595
+ if (op === "&&" || op === "||" || op === "?:") complexity++;
1596
+ }
1597
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
1598
+ const child = n.namedChild(i);
1599
+ if (child) stack.push(child);
1600
+ }
1601
+ }
1602
+ return complexity;
1603
+ }
1604
+ extractGroovyDoc(node) {
1605
+ const parent = node.parent;
1606
+ if (!parent) return void 0;
1607
+ let selfIdx = -1;
1608
+ for (let i = 0; i < parent.namedChildCount; i++) {
1609
+ const c = parent.namedChild(i);
1610
+ if (c && c.startIndex === node.startIndex && c.endIndex === node.endIndex) {
1611
+ selfIdx = i;
1612
+ break;
1613
+ }
1614
+ }
1615
+ for (let i = selfIdx - 1; i >= 0; i--) {
1616
+ const sibling = parent.namedChild(i);
1617
+ if (!sibling) continue;
1618
+ if (sibling.type === "block_comment" && sibling.text.startsWith("/**")) return sibling.text;
1619
+ break;
1620
+ }
1621
+ return void 0;
1622
+ }
1623
+ extractInheritance(node, type) {
1624
+ const parents = [];
1625
+ if (type === "class_declaration" || type === "trait_declaration") {
1626
+ const superClass = this.findChild(node, (n) => n.type === "superclass");
1627
+ if (superClass) {
1628
+ const sc = this.findChild(superClass, (n) => n.type === "type_identifier" || n.type === "identifier" || n.type === "generic_type" || n.type === "scoped_type_identifier");
1629
+ if (sc) parents.push(this.baseTypeName(sc));
1630
+ }
1631
+ } else if (type === "interface_declaration") {
1632
+ const ext = this.findChild(node, (n) => n.type === "extends_interfaces");
1633
+ if (ext) {
1634
+ for (const tid of this.collectTypeNodes(ext)) {
1635
+ parents.push(this.baseTypeName(tid));
1636
+ }
1637
+ }
1638
+ }
1639
+ const superInterfaces = this.findChild(node, (n) => n.type === "super_interfaces");
1640
+ if (superInterfaces) {
1641
+ for (const tid of this.collectTypeNodes(superInterfaces)) {
1642
+ parents.push(this.baseTypeName(tid));
1643
+ }
1644
+ }
1645
+ return parents;
1646
+ }
1647
+ /** Collect type nodes within a type_list/super_interfaces/etc (DFS).
1648
+ * scoped_type_identifier is accepted AS a complete type and NOT descended
1649
+ * into — its nested type_identifiers are package/class segments, never
1650
+ * independent types (collecting them fabricated garbage edges). */
1651
+ collectTypeNodes(node) {
1652
+ const out = [];
1653
+ const stack = [node];
1654
+ while (stack.length > 0) {
1655
+ const n = stack.pop();
1656
+ for (let i = 0; i < n.namedChildCount; i++) {
1657
+ const c = n.namedChild(i);
1658
+ if (!c) continue;
1659
+ if (c.type === "type_identifier" || c.type === "generic_type" || c.type === "scoped_type_identifier") {
1660
+ out.push(c);
1661
+ } else {
1662
+ stack.push(c);
1663
+ }
1664
+ }
1665
+ }
1666
+ return out;
1667
+ }
1668
+ stripQuotes(text) {
1669
+ if (text.length >= 2) {
1670
+ const first = text[0];
1671
+ const last = text[text.length - 1];
1672
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
1673
+ return text.slice(1, -1);
1674
+ }
1675
+ }
1676
+ return text;
1677
+ }
1678
+ baseTypeName(node) {
1679
+ if (node.type === "generic_type") {
1680
+ const id = this.findChild(node, (n) => n.type === "type_identifier" || n.type === "identifier");
1681
+ return id ? id.text : node.text;
1682
+ }
1683
+ return node.text;
1684
+ }
1685
+ extractModifiers(node) {
1686
+ const mods = [];
1687
+ const modifiers = this.findChild(node, (n) => n.type === "modifiers");
1688
+ if (modifiers) {
1689
+ for (let i = 0; i < modifiers.childCount; i++) {
1690
+ const c = modifiers.child(i);
1691
+ if (!c || !c.isNamed) {
1692
+ if (c) mods.push(c.text);
1693
+ continue;
1694
+ }
1695
+ if (c.type === "annotation" || c.type === "marker_annotation") continue;
1696
+ mods.push(c.text);
1697
+ }
1698
+ }
1699
+ return mods;
1700
+ }
1701
+ /**
1702
+ * Generic type-parameter extraction from type_parameters nodes.
1703
+ * Previously groovy symbols carried NO generic information (#383 fix).
1704
+ */
1705
+ extractGroovyTypeParameters(node) {
1706
+ const tpNode = node.namedChildren.find((c) => c.type === "type_parameters");
1707
+ if (!tpNode) return [];
1708
+ const out = [];
1709
+ for (let i = 0; i < tpNode.namedChildCount; i++) {
1710
+ const c = tpNode.namedChild(i);
1711
+ if (!c) continue;
1712
+ if (c.type.includes("identifier")) {
1713
+ out.push(c.text.trim());
1714
+ } else if (c.type === "type_parameter") {
1715
+ for (let j = 0; j < c.namedChildCount; j++) {
1716
+ const gc = c.namedChild(j);
1717
+ if (gc && gc.type.includes("identifier")) {
1718
+ out.push(gc.text.trim());
1719
+ break;
1720
+ }
1721
+ }
1722
+ }
1723
+ }
1724
+ return out;
1725
+ }
1726
+ extractVisibility(node) {
1727
+ const mods = this.extractModifiers(node);
1728
+ if (mods.indexOf("private") !== -1) return "private";
1729
+ if (mods.indexOf("protected") !== -1) return "protected";
1730
+ return "public";
1731
+ }
1732
+ extractAnnotations(node) {
1733
+ return this.extractAnnotationRefs(node).map((a) => a.name);
1734
+ }
1735
+ /**
1736
+ * THE ANNOTATIONS, WITH THE LINE EACH IS WRITTEN ON — 2026-10-22.
1737
+ *
1738
+ * The `decorates` edge is the family shape for "a decorator is applied to a
1739
+ * declaration" (src/ast/types.ts: the direction is the DECORATED DECLARATION
1740
+ * → THE DECORATOR, and the family's emitters are javascript
1741
+ * `src/ast/real-javascript-analyzer.ts:3003` and python
1742
+ * `src/ast/real-python-analyzer.ts:6112`):
1743
+ *
1744
+ * source = the decorated declaration's name
1745
+ * target = the decorator's NAME LEAF
1746
+ * line_number = the DECORATOR's own line
1747
+ * metadata = { isDecorator: true, decoratorLine }
1748
+ *
1749
+ * ⇒ SO THE NAME IS THE LEAF, NOT THE WRITTEN PATH: the same rule this
1750
+ * method already applies to the symbol's `decorators` list (the last dotted
1751
+ * segment), and the same rule the lane's own fixture pins
1752
+ * (groovy/fq-annotations: `expectDecorators` says the class `FqService`
1753
+ * "contains" `Service` for `@Service`, and the short-name normalizer is what
1754
+ * the fixture's own rationale records: "The short-name normalizer (last
1755
+ * dotted segment) must feed both"). ⇒ The EDGE and the SYMBOL LIST therefore
1756
+ * agree, which is what lets a consumer join them.
1757
+ *
1758
+ * ⇒ AND THE LINE IS THE ANNOTATION'S OWN LINE, not the declaration's: the
1759
+ * decorator is spelled there, and the family's emitters put the decorator's
1760
+ * line on the row (javascript: `decoratorLine`).
1761
+ */
1762
+ extractAnnotationRefs(node) {
1763
+ const anns = [];
1764
+ const modifiers = this.findChild(node, (n) => n.type === "modifiers");
1765
+ if (modifiers) {
1766
+ for (let i = 0; i < modifiers.namedChildCount; i++) {
1767
+ const c = modifiers.namedChild(i);
1768
+ if (c && (c.type === "annotation" || c.type === "marker_annotation")) {
1769
+ const nameNode = this.findChild(c, (n) => n.type === "identifier" || n.type === "scoped_identifier");
1770
+ if (nameNode) {
1771
+ const text = nameNode.text;
1772
+ const dot = text.lastIndexOf(".");
1773
+ anns.push({
1774
+ name: dot === -1 ? text : text.slice(dot + 1),
1775
+ line: nameNode.startPosition.row + 1
1776
+ });
1777
+ }
1778
+ }
1779
+ }
1780
+ }
1781
+ return anns;
1782
+ }
1783
+ buildFqn(currentPackage, scopeStack, name) {
1784
+ if (scopeStack.length > 0) {
1785
+ return `${scopeStack[scopeStack.length - 1].fqn}.${name}`;
1786
+ }
1787
+ return currentPackage ? `${currentPackage}.${name}` : name;
1788
+ }
1789
+ /**
1790
+ * #841 round 14: does this juxt_function_call carry the typed-closure-
1791
+ * parameter misparse shape? Measured on the pinned grammar, not assumed:
1792
+ * `{ Test task -> … }` → name=Test, args="task"
1793
+ * `{ String key, Object value → }` → name=String, args="key, Object"
1794
+ * `{ Map<String,File> m -> … }` → name=Map, args="<String,File> m" (generic)
1795
+ *
1796
+ * The decisive evidence is the SOURCE, measured over the whole groovy corpus
1797
+ * (962 such juxt nodes, every one verified): what follows the node is at most
1798
+ * further parameter names (the multi-param node stops before the last name —
1799
+ * `String key, Object` then ` value`), commas, dots, generics, brackets and
1800
+ * whitespace, and then the closure arrow `->`. A REAL juxtaposed call is
1801
+ * never followed by an arrow — the arrow can only make the preceding text a
1802
+ * closure parameter list (Groovy has no other `->`). The tree compositions
1803
+ * (lambda sibling, arrow sibling, first child of closure/block) are the
1804
+ * fallback when no source text is supplied — the same 962 sites fall into
1805
+ * exactly those three compositions, with 139 of them ending before the last
1806
+ * parameter name (which is why a sibling-only test under-covers by ~5×: it
1807
+ * suppressed 115 of the 608 fabricated edges; the source test covers the
1808
+ * rest).
1809
+ */
1810
+ /** The JS `\s` set, as a char test — kept exact so the char walks below are
1811
+ * behaviour-identical to the regexes they replace (M49.R2: the AST regex
1812
+ * census counted those three literals as review-class debt). */
1813
+ static isRegexSpace(c, code) {
1814
+ if (c === " " || c === " " || c === "\n" || c === "\r" || c === "\v" || c === "\f") return true;
1815
+ return code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
1816
+ }
1817
+ /** `/^[\s]*$/` as a char walk. */
1818
+ static isWhitespaceOnly(text) {
1819
+ for (let i = 0; i < text.length; i++) {
1820
+ if (!_RealGroovyAnalyzer.isRegexSpace(text[i], text.charCodeAt(i))) return false;
1821
+ }
1822
+ return true;
1823
+ }
1824
+ /**
1825
+ * `/^[A-Z][A-Za-z0-9_]*(\.[A-Z][A-Za-z0-9_]*)*$/` as a char walk: a
1826
+ * type-cased name, optionally dotted, every segment starting uppercase and
1827
+ * otherwise word characters. Lowercase juxt callees (`echo`, `sh`) never
1828
+ * match, so ordinary Jenkins-style juxtaposition is untouched.
1829
+ */
1830
+ static isTypeCasedDottedName(text) {
1831
+ if (text.length === 0) return false;
1832
+ let i = 0;
1833
+ while (i < text.length) {
1834
+ const first = text[i];
1835
+ if (!(first >= "A" && first <= "Z")) return false;
1836
+ i++;
1837
+ while (i < text.length && text[i] !== ".") {
1838
+ const c = text[i];
1839
+ if (!(c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c >= "0" && c <= "9" || c === "_")) return false;
1840
+ i++;
1841
+ }
1842
+ if (i < text.length) {
1843
+ i++;
1844
+ if (i >= text.length) return false;
1845
+ }
1846
+ }
1847
+ return true;
1848
+ }
1849
+ /** Identifier start character (the regexes' `[A-Za-z_]`). */
1850
+ static isIdentStart(c) {
1851
+ return c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c === "_";
1852
+ }
1853
+ /** Identifier continuation character (the regexes' `[A-Za-z0-9_]`). */
1854
+ static isWordChar(c) {
1855
+ return c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c >= "0" && c <= "9" || c === "_";
1856
+ }
1857
+ /**
1858
+ * `/^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*`-style list as a char
1859
+ * walk: comma-separated items, each an identifier optionally dotted, with
1860
+ * optional whitespace around the commas only (`Type name`, `a.b.C`,
1861
+ * `x, y.Z`). Mirrors the regex exactly — leading and trailing whitespace are
1862
+ * not accepted (the call site trims first), and a trailing comma fails.
1863
+ */
1864
+ static isDottedIdentifierList(text) {
1865
+ let i = 0;
1866
+ const spaced = () => {
1867
+ while (i < text.length && _RealGroovyAnalyzer.isRegexSpace(text[i], text.charCodeAt(i))) i++;
1868
+ };
1869
+ const dotted = () => {
1870
+ if (!_RealGroovyAnalyzer.isIdentStart(text[i])) return false;
1871
+ i++;
1872
+ while (i < text.length && _RealGroovyAnalyzer.isWordChar(text[i])) i++;
1873
+ while (i < text.length && text[i] === ".") {
1874
+ i++;
1875
+ if (!_RealGroovyAnalyzer.isIdentStart(text[i])) return false;
1876
+ i++;
1877
+ while (i < text.length && _RealGroovyAnalyzer.isWordChar(text[i])) i++;
1878
+ }
1879
+ return true;
1880
+ };
1881
+ if (!dotted()) return false;
1882
+ while (i < text.length) {
1883
+ const save = i;
1884
+ spaced();
1885
+ if (i >= text.length) {
1886
+ i = save;
1887
+ break;
1888
+ }
1889
+ if (text[i] !== ",") return false;
1890
+ i++;
1891
+ spaced();
1892
+ if (!dotted()) return false;
1893
+ }
1894
+ return i >= text.length;
1895
+ }
1896
+ /** `/^[\s,.<>\[\]A-Za-z0-9_]*$/` as a char walk: parameter-list material only. */
1897
+ static isParamMaterialOnly(text) {
1898
+ for (let i = 0; i < text.length; i++) {
1899
+ const c = text[i];
1900
+ if (_RealGroovyAnalyzer.isRegexSpace(c, text.charCodeAt(i))) continue;
1901
+ if (c === "," || c === "." || c === "<" || c === ">" || c === "[" || c === "]") continue;
1902
+ if (c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c >= "0" && c <= "9" || c === "_") continue;
1903
+ return false;
1904
+ }
1905
+ return true;
1906
+ }
1907
+ looksLikeTypedClosureMisparse(juxtNode, nameNode, content) {
1908
+ const nameText = nameNode.text.trim();
1909
+ if (!_RealGroovyAnalyzer.isTypeCasedDottedName(nameText)) return false;
1910
+ const argsNode = juxtNode.childForFieldName("args");
1911
+ if (!argsNode) return false;
1912
+ if (!_RealGroovyAnalyzer.isDottedIdentifierList(argsNode.text.trim())) return false;
1913
+ if (content != null) {
1914
+ const tail = content.slice(juxtNode.endIndex, juxtNode.endIndex + 160);
1915
+ const arrow = tail.indexOf("->");
1916
+ if (arrow >= 0) {
1917
+ const between = tail.slice(0, arrow);
1918
+ if (_RealGroovyAnalyzer.isParamMaterialOnly(between)) return true;
1919
+ }
1920
+ }
1921
+ const parent = juxtNode.parent;
1922
+ if (!parent) return false;
1923
+ if (juxtNode.nextSibling?.type === "->") return true;
1924
+ const siblings = [];
1925
+ for (let i = 0; i < parent.namedChildCount; i++) {
1926
+ const c = parent.namedChild(i);
1927
+ if (c) siblings.push(c);
1928
+ }
1929
+ const idx = siblings.findIndex((s) => s.id === juxtNode.id);
1930
+ if (idx === -1) return false;
1931
+ const next = siblings[idx + 1];
1932
+ if (next != null && next.type === "lambda_expression") return true;
1933
+ if (idx === 0 && (parent.type === "closure" || parent.type === "block")) return true;
1934
+ return false;
1935
+ }
1936
+ /**
1937
+ * #841 round 14 (analyzer-side recovery): when a typed closure parameter
1938
+ * list collides inside a chained call, tree-sitter's recovery emits the
1939
+ * class as ROOT-LEVEL fragments — the `class` keyword token, the name
1940
+ * `identifier`, the `super_interfaces` node, and the member declarations
1941
+ * as siblings — instead of a class_declaration node. The walk therefore
1942
+ * never pushes a class scope, and the file loses its class symbol, its
1943
+ * implements edges, and every contains edge: methods extract but ride
1944
+ * ownerless/"(script)" (measured: 0 classes and 0 contains edges on
1945
+ * 160–470-line build-logic plugin files). The grammar fix for the
1946
+ * underlying construct is closed off by measurement (§7f–§7q), so this
1947
+ * pass reconstructs the ownership graph AFTER the walk:
1948
+ * 1. the class symbol, from the root `class` token + name fragment;
1949
+ * 2. implements edges + classHierarchy from the surviving
1950
+ * super_interfaces fragment;
1951
+ * 3. ownerName/ownerKind on the surviving member symbols inside the
1952
+ * body region (the existing contains loop then emits class → member);
1953
+ * 4. method symbols synthesized for degraded method headers (modifiers +
1954
+ * type + identifier + formal_parameters + `{` fragments) so the
1955
+ * methods whose wrapper was dropped rejoin the graph;
1956
+ * 5. "(script)"-sourced calls/uses edges inside the region re-parented
1957
+ * to the class.
1958
+ * Runs only on trees with a root-level ERROR and the class fragment
1959
+ * shape, so clean trees cannot enter (the shapes exist only where the
1960
+ * parser failed). Parse errors are NOT suppressed — the tree keeps its
1961
+ * ERROR nodes; only the ownership graph is restored.
1962
+ */
1963
+ recoverDegradedClassShapes(rootNode, symbols, relationships, symbolMap, currentPackage) {
1964
+ if (!rootNode.hasError) return;
1965
+ const allKids = rootNode.children;
1966
+ const kids = [];
1967
+ for (const c of allKids) {
1968
+ if (c) kids.push(c);
1969
+ }
1970
+ if (!kids.some((k) => k.type === "ERROR")) return;
1971
+ for (let i = 0; i < kids.length; i++) {
1972
+ if (kids[i].type !== "class") continue;
1973
+ const nameNode = kids[i + 1];
1974
+ if (!nameNode || nameNode.type !== "identifier") continue;
1975
+ const name = nameNode.text;
1976
+ const classToken = kids[i];
1977
+ const startLine = classToken.startPosition.row + 1;
1978
+ let j = i + 2;
1979
+ const headerFragments = [];
1980
+ while (j < kids.length && kids[j].type !== "{") {
1981
+ headerFragments.push(kids[j]);
1982
+ j++;
1983
+ }
1984
+ if (j >= kids.length) continue;
1985
+ const openBrace = kids[j];
1986
+ let closeIdx = -1;
1987
+ for (let k = kids.length - 1; k > j; k--) {
1988
+ if (kids[k].type === "}") {
1989
+ closeIdx = k;
1990
+ break;
1991
+ }
1992
+ }
1993
+ const endLine = closeIdx >= 0 ? kids[closeIdx].endPosition.row + 1 : rootNode.endPosition.row + 1;
1994
+ const bodyChildren = kids.slice(j + 1, closeIdx >= 0 ? closeIdx : kids.length);
1995
+ if (symbols.some((s) => s.name === name && s.range?.startLine === startLine)) continue;
1996
+ const typeFragments = /* @__PURE__ */ new Set(["generic_type", "type_identifier", "scoped_type_identifier", "void_type", "integral_type", "floating_point_type", "boolean_type", "array_type"]);
1997
+ const memberKinds = /* @__PURE__ */ new Set([ASTEngine.SymbolKind.Method, ASTEngine.SymbolKind.Constructor, ASTEngine.SymbolKind.Property]);
1998
+ const members = symbols.filter((s) => !s.ownerName && !s.metadata?.isAnonymousClass && memberKinds.has(s.kind) && s.range?.startLine != null && s.range.startLine >= startLine && s.range.startLine <= endLine);
1999
+ const hasFieldDecl = bodyChildren.some((k) => k.type === "field_declaration");
2000
+ let hasDegradedHeader = false;
2001
+ for (let b = 0; b < bodyChildren.length - 1 && !hasDegradedHeader; b++) {
2002
+ let p = b;
2003
+ if (bodyChildren[b].type === "modifiers") p = b + 1;
2004
+ const typeFrag = bodyChildren[p];
2005
+ if (!typeFrag || !typeFragments.has(typeFrag.type)) continue;
2006
+ const nameFrag = bodyChildren[p + 1];
2007
+ if (!nameFrag || nameFrag.type !== "identifier") continue;
2008
+ const fp = bodyChildren[p + 2];
2009
+ const brace = bodyChildren[p + 3];
2010
+ if (fp && fp.type === "formal_parameters" && brace && brace.type === "{") hasDegradedHeader = true;
2011
+ }
2012
+ if (members.length === 0 && !hasDegradedHeader && !hasFieldDecl) continue;
2013
+ let visibility = "public";
2014
+ let annotations = [];
2015
+ const before = kids[i - 1];
2016
+ if (before && before.type === "modifiers") {
2017
+ for (let m = 0; m < before.namedChildCount; m++) {
2018
+ const mc = before.namedChild(m);
2019
+ if (!mc) continue;
2020
+ const t = mc.text ?? "";
2021
+ if (t === "private") visibility = "private";
2022
+ else if (t === "protected") visibility = "protected";
2023
+ if (mc.type === "annotation" || mc.type === "marker_annotation") {
2024
+ const annName = this.findChild(mc, (n) => n.type === "identifier" || n.type === "scoped_identifier");
2025
+ if (annName) {
2026
+ const text = annName.text;
2027
+ const dot = text.lastIndexOf(".");
2028
+ annotations.push(dot === -1 ? text : text.slice(dot + 1));
2029
+ }
2030
+ }
2031
+ }
2032
+ }
2033
+ for (const frag of headerFragments) {
2034
+ if (frag.type !== "super_interfaces") continue;
2035
+ for (const tid of this.collectTypeNodes(frag)) {
2036
+ relationships.push({
2037
+ source: name,
2038
+ target: this.baseTypeName(tid),
2039
+ type: ASTEngine.RelationshipType.Implements,
2040
+ confidence: 1,
2041
+ ...this.lineField(tid, frag),
2042
+ metadata: { relationshipType: "implements" }
2043
+ });
2044
+ }
2045
+ }
2046
+ const fqn = currentPackage ? `${currentPackage}.${name}` : name;
2047
+ const sym = {
2048
+ name,
2049
+ kind: ASTEngine.SymbolKind.Class,
2050
+ comments: [],
2051
+ range: {
2052
+ // The measured body extent: from the opening brace (start overridden
2053
+ // to the class keyword's line, #825 rule) to the closing brace token
2054
+ // when one was found — NOT the whole root node (fragments are
2055
+ // root-level children, so the root's range is the entire file).
2056
+ ...this.nodeToRange(closeIdx >= 0 ? kids[closeIdx] : openBrace.parent ?? rootNode),
2057
+ startLine,
2058
+ startColumn: classToken.startPosition.column + 1
2059
+ },
2060
+ visibility,
2061
+ decorators: annotations.length > 0 ? annotations : void 0,
2062
+ metadata: { nodeType: "class_declaration_recovered", annotations }
2063
+ };
2064
+ symbols.push(sym);
2065
+ symbolMap.set(fqn, sym);
2066
+ symbolMap.set(name, sym);
2067
+ for (const s of members) {
2068
+ s.ownerName = name;
2069
+ s.ownerKind = ASTEngine.SymbolKind.Class;
2070
+ if (!s.visibility) s.visibility = "public";
2071
+ }
2072
+ for (let b = 0; b < bodyChildren.length - 1; b++) {
2073
+ let p = b;
2074
+ const mods = bodyChildren[b].type === "modifiers" ? bodyChildren[b] : null;
2075
+ if (mods) p = b + 1;
2076
+ const typeFrag = bodyChildren[p];
2077
+ if (!typeFrag || !typeFragments.has(typeFrag.type)) continue;
2078
+ const nameFrag = bodyChildren[p + 1];
2079
+ if (!nameFrag || nameFrag.type !== "identifier") continue;
2080
+ const mName = nameFrag.text;
2081
+ const mLine = nameFrag.startPosition.row + 1;
2082
+ if (symbols.some((s) => s.name === mName && s.range?.startLine === mLine)) continue;
2083
+ let q = p + 2;
2084
+ let paramNode = null;
2085
+ const fp = bodyChildren[q];
2086
+ if (fp && fp.type === "formal_parameters") {
2087
+ paramNode = fp;
2088
+ q++;
2089
+ }
2090
+ const bodyBrace = bodyChildren[q];
2091
+ if (!bodyBrace || bodyBrace.type !== "{") continue;
2092
+ const parameters = [];
2093
+ if (paramNode) {
2094
+ for (let pc = 0; pc < paramNode.namedChildCount; pc++) {
2095
+ const par = paramNode.namedChild(pc);
2096
+ if (!par || par.type !== "formal_parameter" && par.type !== "spread_parameter") continue;
2097
+ const isSpread = par.type === "spread_parameter";
2098
+ let pName = this.findChild(par, (n) => n.type === "identifier");
2099
+ if (isSpread) {
2100
+ const decl = this.findChild(par, (n) => n.type === "variable_declarator");
2101
+ pName = decl ? this.findChild(decl, (n) => n.type === "identifier") : null;
2102
+ }
2103
+ const pType = this.findChild(par, (n) => n.type === "type_identifier" || n.type === "integral_type" || n.type === "floating_point_type" || n.type === "boolean_type" || n.type === "void_type" || n.type === "generic_type" || n.type === "array_type" || n.type === "scoped_type_identifier");
2104
+ parameters.push({
2105
+ name: pName?.text ?? "anonymous",
2106
+ type: pType ? this.baseTypeName(pType) + (isSpread ? "[]" : "") : void 0
2107
+ });
2108
+ }
2109
+ }
2110
+ let mVisibility = "public";
2111
+ if (mods) {
2112
+ for (let m2 = 0; m2 < mods.namedChildCount; m2++) {
2113
+ const mc2 = mods.namedChild(m2);
2114
+ const t2 = mc2?.text ?? "";
2115
+ if (t2 === "private") mVisibility = "private";
2116
+ else if (t2 === "protected") mVisibility = "protected";
2117
+ }
2118
+ }
2119
+ symbols.push({
2120
+ name: mName,
2121
+ kind: ASTEngine.SymbolKind.Method,
2122
+ comments: [],
2123
+ parameters: parameters.length > 0 ? parameters : void 0,
2124
+ returnType: typeFragments.has(typeFrag.type) ? this.baseTypeName(typeFrag) : void 0,
2125
+ range: {
2126
+ ...this.nodeToRange(typeFrag),
2127
+ // The method is written on its NAME's line (#825 rule).
2128
+ startLine: mLine,
2129
+ startColumn: nameFrag.startPosition.column + 1
2130
+ },
2131
+ visibility: mVisibility,
2132
+ ownerName: name,
2133
+ ownerKind: ASTEngine.SymbolKind.Class,
2134
+ metadata: { nodeType: "method_declaration_recovered" }
2135
+ });
2136
+ b = q;
2137
+ }
2138
+ for (const rel of relationships) {
2139
+ if (rel.source !== "(script)") continue;
2140
+ if (rel.type !== ASTEngine.RelationshipType.Calls && rel.type !== ASTEngine.RelationshipType.Uses) continue;
2141
+ const line = rel.line_number;
2142
+ if (line == null || line < startLine || line > endLine) continue;
2143
+ rel.source = name;
2144
+ }
2145
+ break;
2146
+ }
2147
+ }
2148
+ findChild(node, predicate) {
2149
+ const count = node.namedChildCount;
2150
+ for (let i = 0; i < count; i++) {
2151
+ const child = node.namedChild(i);
2152
+ if (child && predicate(child)) return child;
2153
+ }
2154
+ return null;
2155
+ }
2156
+ findAllChildren(node, predicate) {
2157
+ const out = [];
2158
+ const count = node.namedChildCount;
2159
+ for (let i = 0; i < count; i++) {
2160
+ const child = node.namedChild(i);
2161
+ if (child && predicate(child)) out.push(child);
2162
+ }
2163
+ return out;
2164
+ }
2165
+ nodeToRange(node) {
2166
+ return {
2167
+ startLine: node.startPosition.row + 1,
2168
+ // 1-based columns, consistent with java/kotlin/scala (M14 consistency gate)
2169
+ startColumn: node.startPosition.column + 1,
2170
+ endLine: node.endPosition.row + 1,
2171
+ endColumn: node.endPosition.column + 1,
2172
+ startIndex: node.startIndex,
2173
+ endIndex: node.endIndex
2174
+ };
2175
+ }
2176
+ /**
2177
+ * M59 (#825): the `line_number` field for a relationship, taken from the node
2178
+ * that NAMES the relationship's target — never from the construct that merely
2179
+ * contains it. Returns nothing at all when no node is available: #820/#821,
2180
+ * a stored 0 is a claim that the relationship occurs on line 0.
2181
+ */
2182
+ lineField(nameNode, fallback) {
2183
+ const n = nameNode ?? fallback;
2184
+ return n ? { line_number: n.startPosition.row + 1 } : {};
2185
+ }
2186
+ /**
2187
+ * M59 (#825): the node whose start is the DECLARATION's own line.
2188
+ *
2189
+ * An annotation is a modifier — `@Service` decorates the class, it is not
2190
+ * where the class is written. Measured on the pinned grammar, not assumed:
2191
+ * `class_declaration` for `@Service\nclass Svc` has children
2192
+ * `[modifiers@5, identifier@6, class_body@6]`, so the class symbol's line was
2193
+ * the ANNOTATION line. Same shape and same rule as java's
2194
+ * `declarationLineNode` and scala's `declarationLineNode` — one rule for the
2195
+ * whole JVM family rather than three dialects of it.
2196
+ */
2197
+ /** True when the field node is the same declaration as a sibling method, parsed twice (M59 #825 follow-up: `static Sphere valueOf` + the `final Distance distance`/ctor split). Proof, not heuristic: two distinct same-line declarations must be `;`-separated, so a sibling method starting after the field with only whitespace between them is the same tokens parsed twice — the full-span method wins. */
2198
+ isDuplicateParseField(node, content) {
2199
+ const parent = node.parent;
2200
+ if (!parent) return false;
2201
+ for (let i = 0; i < parent.namedChildCount; i++) {
2202
+ const s = parent.namedChild(i);
2203
+ if (!s || s.id === node.id) continue;
2204
+ if (s.type !== "method_declaration" && s.type !== "constructor_declaration" && s.type !== "function_definition") continue;
2205
+ if (s.startPosition.row !== node.startPosition.row) continue;
2206
+ if (s.startIndex <= node.endIndex) continue;
2207
+ if (_RealGroovyAnalyzer.isWhitespaceOnly(content.slice(node.endIndex, s.startIndex))) return true;
2208
+ }
2209
+ return false;
2210
+ }
2211
+ // A symbol is anchored at the DECLARATION, not at the annotation above it.
2212
+ //
2213
+ // THIS RULE WAS REVERSED ON 2026-10-22, and the reversal is measured, not
2214
+ // preferred. The old rule ("anchor at the FIRST ANNOTATION") was adopted
2215
+ // against a hand-audited truth file that records the annotation's line:
2216
+ // @Slf4j L18 / class Counter truth 18, was 19
2217
+ // @Override L16 / String format(…) truth 16, was 17
2218
+ // and it claimed the family convention — "the java analyzer anchors a symbol
2219
+ // at its annotation line via `annotationLine`, so groovy was the outlier".
2220
+ //
2221
+ // BOTH CLAIMS ARE MEASURED FALSE TODAY (script/m14/groovy-family-line-anchor-probe.ts,
2222
+ // the same annotated shape through all four JVM analyzers, the annotation on
2223
+ // the line BEFORE each declaration):
2224
+ //
2225
+ // java class@2 label@4 use@6 ← the DECLARATION's line
2226
+ // kotlin class@2 label@4 use@6 ← the DECLARATION's line
2227
+ // scala class@2 label@4 use@6 ← the DECLARATION's line
2228
+ // groovy class@1 label@4 use@5 ← the ANNOTATION's line
2229
+ //
2230
+ // java's `annotationLine` is not what java's SYMBOL range uses (java anchors
2231
+ // the symbol on the declaration and uses the annotation line for a
2232
+ // DIFFERENT edge), and the truth file the old rule cited is stale for the
2233
+ // whole JVM family — the same file is stale for the constructor name (see
2234
+ // the constructor comment) and `bun script/m12/verify-analyzer.ts java` /
2235
+ // `kotlin` report the constructor FN/FP pair at HEAD.
2236
+ //
2237
+ // THIS LANE'S OWN m14 FIXTURES record the DECLARATION's line and FORBID the
2238
+ // annotation's line, so the old anchor is pinned as the pre-fix shape:
2239
+ // groovy/lines-825 pins `contains[Wrapped->use] @26` and FORBIDS @25 (the
2240
+ // `@Deprecated` line), `contains[Wrapped->label] @23`, and the class at 19;
2241
+ // groovy/anonymous-body-overrides pins `overrides[anonymous_TypeCodec.bsonType->bsonType] @23`
2242
+ // and `contains[anonymous_TypeCodec->bsonType] @23` where the `@Override` is
2243
+ // on 22.
2244
+ //
2245
+ // A `modifiers` child is skipped in BOTH cases — annotation-carrying or not —
2246
+ // so `private String label` and `final int hex` stay on their own line, which
2247
+ // is what the already-correct property rows require.
2248
+ /** True when this declaration carries at least one annotation (directly or in `modifiers`). */
2249
+ declarationHasAnnotation(node) {
2250
+ for (let i = 0; i < node.namedChildCount; i++) {
2251
+ const c = node.namedChild(i);
2252
+ if (!c) continue;
2253
+ if (c.type === "annotation" || c.type === "marker_annotation") return true;
2254
+ if (c.type === "modifiers" && this.findChild(c, (n) => n.type === "annotation" || n.type === "marker_annotation")) return true;
2255
+ }
2256
+ return false;
2257
+ }
2258
+ declarationLineNode(node) {
2259
+ for (let i = 0; i < node.namedChildCount; i++) {
2260
+ const c = node.namedChild(i);
2261
+ if (!c) continue;
2262
+ if (c.type === "modifiers") {
2263
+ continue;
2264
+ }
2265
+ if (c.type === "annotation" || c.type === "marker_annotation") continue;
2266
+ if (c.type === "line_comment" || c.type === "block_comment") continue;
2267
+ return c;
2268
+ }
2269
+ return node;
2270
+ }
2271
+ /** The range of a SYMBOL (not of an import, an error or a raw node), anchored per `declarationLineNode`. */
2272
+ symbolRange(node, nameNode) {
2273
+ const decl = nameNode ?? this.declarationLineNode(node);
2274
+ const base = this.nodeToRange(node);
2275
+ return {
2276
+ ...base,
2277
+ startLine: decl.startPosition.row + 1,
2278
+ startColumn: decl.startPosition.column + 1,
2279
+ startIndex: decl.startIndex
2280
+ };
2281
+ }
2282
+ };
2283
+ export {
2284
+ RealGroovyAnalyzer
2285
+ };