@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,3293 @@
1
+ import {
2
+ findDeclaringParent
3
+ } from "./chunk-OWISBAOF.js";
4
+ import {
5
+ ASTEngine
6
+ } from "./chunk-3ZTOHHSW.js";
7
+ import "./chunk-BQ3OVQ3P.js";
8
+ import "./chunk-SK6YVHFZ.js";
9
+ import "./chunk-2BSKD7O3.js";
10
+ import "./chunk-KAET47ZL.js";
11
+ import "./chunk-KCDECRJY.js";
12
+ import "./chunk-MLKGABMK.js";
13
+
14
+ // src/ast/real-scala-analyzer.ts
15
+ var COMPOUND_ASSIGN_OPS = /* @__PURE__ */ new Set(["+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", ">>>="]);
16
+ function scalaOperatorIsAssignment(op) {
17
+ if (!op || !op.endsWith("=")) return false;
18
+ if (COMPOUND_ASSIGN_OPS.has(op)) return true;
19
+ if (op.endsWith("==") || op.endsWith("<=") || op.endsWith(">=")) return false;
20
+ if (op.includes("=:=") || op.includes("<:<")) return false;
21
+ if (op === "=>" || op === "<-" || op === "<:" || op === ">:" || op === "=:") return false;
22
+ return true;
23
+ }
24
+ function stripScalaQuoting(text) {
25
+ if (!text.includes("`")) return text;
26
+ let out = "";
27
+ let i = 0;
28
+ while (i < text.length) {
29
+ if (text.charCodeAt(i) === 96) {
30
+ const close = text.indexOf("`", i + 1);
31
+ if (close === -1) {
32
+ out += text.slice(i);
33
+ break;
34
+ }
35
+ out += text.slice(i + 1, close);
36
+ i = close + 1;
37
+ } else {
38
+ out += text[i];
39
+ i++;
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ function sigHasTypeVar(sig) {
45
+ const n = sig.length;
46
+ let i = 0;
47
+ while (i < n) {
48
+ const c = sig[i];
49
+ if (c === "(" || c === "," || c === ":" || c === "|" || c === "[" || c === "<" || c === "&" || i === 0) {
50
+ let j = i;
51
+ while (j < n && (sig[j] === "(" || sig[j] === "[" || sig[j] === " " || sig[j] === ":" || sig[j] === ",")) j++;
52
+ if (j < n && sig[j] >= "A" && sig[j] <= "Z") {
53
+ let end = j + 1;
54
+ while (end < n && (sig[end] >= "A" && sig[end] <= "Z" || sig[end] >= "a" && sig[end] <= "z" || sig[end] >= "0" && sig[end] <= "9" || sig[end] === "_")) end++;
55
+ if (end === j + 1) return true;
56
+ i = end;
57
+ continue;
58
+ }
59
+ i = j > i ? j : i + 1;
60
+ continue;
61
+ }
62
+ i++;
63
+ }
64
+ return false;
65
+ }
66
+ function isSelfOverride(declaring, ownerName) {
67
+ return !!declaring && !!ownerName && declaring === ownerName;
68
+ }
69
+ var SCALA_JVM_PLATFORM_ROOTS = /* @__PURE__ */ new Set(["java", "javax", "sun"]);
70
+ var SCALA_CLAIM_KEYS_ON_EDGE = [
71
+ "localBinding",
72
+ "remoteRef",
73
+ "importProjectFile",
74
+ "languageNamespace",
75
+ "sourcePathClaim",
76
+ "importOutsideProject",
77
+ "baseBeyondRunScope",
78
+ "isExternal",
79
+ "isExternalTarget",
80
+ "isStdlib",
81
+ "stdlibModule",
82
+ "isExternalBase",
83
+ "external",
84
+ "externalRef"
85
+ ];
86
+ var SCALA_RESERVED = /* @__PURE__ */ new Set([
87
+ "abstract",
88
+ "case",
89
+ "catch",
90
+ "class",
91
+ "def",
92
+ "do",
93
+ "else",
94
+ "extends",
95
+ "false",
96
+ "final",
97
+ "finally",
98
+ "for",
99
+ "if",
100
+ "implicit",
101
+ "import",
102
+ "lazy",
103
+ "macro",
104
+ "match",
105
+ "new",
106
+ "null",
107
+ "object",
108
+ "override",
109
+ "package",
110
+ "private",
111
+ "protected",
112
+ "return",
113
+ "sealed",
114
+ "super",
115
+ "this",
116
+ "throw",
117
+ "trait",
118
+ "true",
119
+ "try",
120
+ "type",
121
+ "val",
122
+ "var",
123
+ "while",
124
+ "with",
125
+ "yield",
126
+ // Scala 3 reserved additions
127
+ "given",
128
+ "enum",
129
+ "export"
130
+ ]);
131
+ function isKeywordUsedAsName(name) {
132
+ return SCALA_RESERVED.has(name);
133
+ }
134
+ var RealScalaAnalyzer = class _RealScalaAnalyzer {
135
+ // S1.21: stable per-file counter for anonymous given names (see handleGiven).
136
+ // Reset at the top of every analyze() so each file counts from 1.
137
+ givenCounter = 0;
138
+ // M60: per-file anonymous-CLASS name disambiguation, same contract as
139
+ // givenCounter above. `anonymous_<SuperName>` is only unique within its supertype,
140
+ // and a file routinely declares several anonymous classes of the SAME supertype.
141
+ // Measured on the full scala3 corpus: 3,330 anonymous symbols across 1,503 files,
142
+ // of which **402 files declare a DUPLICATE name** (517 collisions).
143
+ //
144
+ // The cost was not cosmetic. `CheckCaptures.scala` declares FOUR distinct
145
+ // `anonymous_TypeTraverser` classes (lines 161, 363, 384, 2308), and all their
146
+ // members were published under ONE owner: 8 contains rows drawn from 4 different
147
+ // classes, 7 of them attributed to the wrong one, with `traverse` appearing four
148
+ // times under the same owner and collapsing to a single row after the harness's
149
+ // (label, target) dedupe. One node where the source has four.
150
+ //
151
+ // The FIRST occurrence keeps the bare name, so every existing pin on
152
+ // `anonymous_Runnable -> run` still holds; later ones take `#2`, `#3`, ... in
153
+ // source order, which is rename-stable under upstream line insertions the same way
154
+ // given_N is.
155
+ anonNameUse = /* @__PURE__ */ new Map();
156
+ analyze(rootNode, content, filePath, context) {
157
+ const symbols = [];
158
+ const imports = [];
159
+ const relationships = [];
160
+ const exports = [];
161
+ this.givenCounter = 0;
162
+ this.anonNameUse = /* @__PURE__ */ new Map();
163
+ const patternsObj = {};
164
+ const frameworksObj = {};
165
+ let hasTypes = false;
166
+ let cyclomaticComplexity = 0;
167
+ const metadata = {
168
+ linesOfCode: 0,
169
+ complexity: 0,
170
+ architecturalScore: 85,
171
+ codeQuality: "excellent",
172
+ hasTypeInference: false,
173
+ hasFrameworkIntelligence: true,
174
+ analysisVersion: "scala-2.0",
175
+ detectedFrameworks: [],
176
+ patterns: []
177
+ };
178
+ if (!rootNode) {
179
+ return {
180
+ symbols,
181
+ imports,
182
+ relationships,
183
+ exports,
184
+ metadata,
185
+ errors: []
186
+ };
187
+ }
188
+ if (rootNode.hasError) {
189
+ let reportedNodes = 0;
190
+ const errStack = [rootNode];
191
+ while (errStack.length > 0) {
192
+ const n = errStack.pop();
193
+ if (n.type === "ERROR" || n.isMissing) {
194
+ reportedNodes++;
195
+ if (!metadata.errors) metadata.errors = [];
196
+ metadata.errors.push({
197
+ message: `Syntax ${n.type} detected. The Brain Engine is bypassing this broken node.`,
198
+ range: {
199
+ startLine: n.startPosition.row + 1,
200
+ startColumn: n.startPosition.column + 1,
201
+ endLine: n.endPosition.row + 1,
202
+ endColumn: n.endPosition.column + 1,
203
+ startIndex: n.startIndex,
204
+ endIndex: n.endIndex
205
+ },
206
+ severity: "warning"
207
+ });
208
+ }
209
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
210
+ const child = n.namedChild(i);
211
+ if (child) errStack.push(child);
212
+ }
213
+ }
214
+ if (reportedNodes === 0) {
215
+ if (!metadata.errors) metadata.errors = [];
216
+ metadata.errors.push({
217
+ message: "Syntax error detected. The parser flagged this file (rootNode.hasError) but recovered no ERROR or MISSING node to name, so no specific node is bypassed.",
218
+ range: {
219
+ startLine: 1,
220
+ startColumn: 1,
221
+ endLine: 1,
222
+ endColumn: 1,
223
+ startIndex: 0,
224
+ endIndex: 0
225
+ },
226
+ severity: "warning"
227
+ });
228
+ }
229
+ }
230
+ let loc = 1;
231
+ for (let i = 0; i < content.length; i++) {
232
+ if (content[i] === "\n") loc++;
233
+ }
234
+ metadata.linesOfCode = loc;
235
+ const typeIndex = /* @__PURE__ */ new Map();
236
+ const typeIndexByKind = /* @__PURE__ */ new Map();
237
+ const typesByKindPos = /* @__PURE__ */ new Map();
238
+ const kindKey = (name, kind) => `${kind}:${name}`;
239
+ const indexType = (s) => {
240
+ if (!typeIndex.has(s.name)) typeIndex.set(s.name, s);
241
+ const kk = kindKey(s.name, s.kind);
242
+ if (!typeIndexByKind.has(kk)) typeIndexByKind.set(kk, s);
243
+ let arr = typesByKindPos.get(kk);
244
+ if (!arr) {
245
+ arr = [];
246
+ typesByKindPos.set(kk, arr);
247
+ }
248
+ arr.push(s);
249
+ };
250
+ const resolveOwner = (s) => {
251
+ if (s.ownerKind) {
252
+ const kk = kindKey(s.ownerName ?? "", s.ownerKind);
253
+ const exact = typeIndexByKind.get(kk);
254
+ if (exact) {
255
+ const all = typesByKindPos.get(kk);
256
+ if (!all || all.length < 2) return exact;
257
+ const line = s.range?.startLine ?? 0;
258
+ for (const cand of all) {
259
+ const st = cand.range?.startLine ?? 0;
260
+ const en = cand.range?.endLine ?? st;
261
+ if (line >= st && line <= en) return cand;
262
+ }
263
+ return exact;
264
+ }
265
+ }
266
+ return typeIndex.get(s.ownerName ?? "");
267
+ };
268
+ const stack = [{ node: rootNode, parent: null }];
269
+ while (stack.length > 0) {
270
+ const popped = stack.pop();
271
+ if (!popped) continue;
272
+ const { node, parent } = popped;
273
+ let currentParent = parent;
274
+ if (node.type === "package_clause") {
275
+ const pkgId = this.findChild(node, "package_identifier");
276
+ const pkgChildren = [];
277
+ const walkPkg = (n) => {
278
+ if (n.type === "identifier") pkgChildren.push(n.text);
279
+ for (let i = 0; i < n.namedChildCount; i++) {
280
+ const ch = n.namedChild(i);
281
+ if (ch) walkPkg(ch);
282
+ }
283
+ };
284
+ if (pkgId) walkPkg(pkgId);
285
+ if (pkgChildren.length > 0) {
286
+ metadata.packageName = pkgChildren.join(".");
287
+ patternsObj["Packages"] = true;
288
+ }
289
+ } else if (node.type === "class_definition") {
290
+ const cls = this.handleClass(node, symbols, relationships, exports, patternsObj, parent);
291
+ if (cls) {
292
+ currentParent = cls;
293
+ indexType(cls);
294
+ }
295
+ } else if (node.type === "object_definition") {
296
+ const obj = this.handleObject(node, symbols, relationships, exports, patternsObj, parent);
297
+ if (obj) {
298
+ currentParent = obj;
299
+ indexType(obj);
300
+ }
301
+ } else if (node.type === "trait_definition") {
302
+ const tr = this.handleTrait(node, symbols, relationships, exports, patternsObj, parent);
303
+ if (tr) {
304
+ currentParent = tr;
305
+ indexType(tr);
306
+ }
307
+ } else if (node.type === "function_definition" || node.type === "function_declaration") {
308
+ const fn = this.handleFunction(node, symbols, relationships, exports, patternsObj, parent);
309
+ if (fn) currentParent = fn;
310
+ } else if (node.type === "val_definition" || node.type === "var_definition" || node.type === "val_declaration" || node.type === "var_declaration") {
311
+ this.handleVariable(node, symbols, exports, parent);
312
+ } else if (node.type === "type_definition") {
313
+ this.handleTypeAlias(node, symbols, parent, exports);
314
+ } else if (node.type === "import_declaration") {
315
+ this.handleImport(node, imports);
316
+ } else if (node.type === "enum_definition") {
317
+ const en = this.handleEnum(node, symbols, relationships, exports, patternsObj, parent);
318
+ if (en) {
319
+ currentParent = en;
320
+ indexType(en);
321
+ }
322
+ } else if (node.type === "given_definition") {
323
+ const g = this.handleGiven(node, symbols, relationships, exports, patternsObj, parent);
324
+ if (g) {
325
+ currentParent = g;
326
+ indexType(g);
327
+ }
328
+ } else if (node.type === "extension_definition") {
329
+ patternsObj["Extension Methods (Scala 3)"] = true;
330
+ } else if (node.type === "export_declaration") {
331
+ patternsObj["Exports (Scala 3)"] = true;
332
+ this.handleExportClause(node, relationships, currentParent);
333
+ } else if (node.type === "match_expression") {
334
+ patternsObj["Case Classes & Pattern Matching"] = true;
335
+ } else if (node.type === "for_expression") {
336
+ patternsObj["For Comprehensions"] = true;
337
+ } else if (node.type === "type_identifier" || node.type === "type") {
338
+ hasTypes = true;
339
+ } else if (node.type === "annotation") {
340
+ const short = this.annotationShortName(node);
341
+ if (short === "Test" || short === "main") patternsObj["Annotations"] = true;
342
+ if (short === "BeanProperty") patternsObj["Java Interop"] = true;
343
+ if (short === "tailrec") patternsObj["Tail Recursion"] = true;
344
+ if (short === "specialized") patternsObj["Specialized Generics"] = true;
345
+ } else if (node.type === "catch_clause") {
346
+ patternsObj["Error Handling"] = true;
347
+ } else if (node.type === "lambda_expression") {
348
+ patternsObj["Lambdas"] = true;
349
+ } else if (node.type === "self_type") {
350
+ patternsObj["Self Types"] = true;
351
+ } else if (node.type === "context_bound") {
352
+ patternsObj["Type Classes (Context Bounds)"] = true;
353
+ } else if (node.type === "infix_expression" || node.type === "infix_type") {
354
+ patternsObj["Infix Notation"] = true;
355
+ } else if (node.type === "interpolated_string_expression" || node.type === "interpolated_string") {
356
+ patternsObj["String Interpolation"] = true;
357
+ } else if (node.type === "derives_clause") {
358
+ patternsObj["Derives (Typeclass Derivation)"] = true;
359
+ } else if (node.type === "package_object") {
360
+ patternsObj["Package Objects"] = true;
361
+ } else if (node.type === "throw_expression" || node.type === "throw") {
362
+ const thrown = node.namedChild(0);
363
+ if (thrown) {
364
+ let target;
365
+ if (thrown.type === "instance_expression") {
366
+ const typeNode = thrown.namedChild(0);
367
+ if (typeNode) {
368
+ target = typeNode.type === "generic_type" ? typeNode.namedChild(0)?.text ?? typeNode.text : typeNode.text;
369
+ }
370
+ } else if (thrown.type === "identifier") {
371
+ target = thrown.text;
372
+ } else if (thrown.type === "call_expression") {
373
+ const callee = thrown.childForFieldName("function");
374
+ if (callee) {
375
+ const kids = [];
376
+ for (let i = 0; i < callee.namedChildCount; i++) {
377
+ const c = callee.namedChild(i);
378
+ if (c && c.type === "identifier") kids.push(c.text);
379
+ }
380
+ target = kids.length > 0 ? kids[kids.length - 1] : callee.text.split("[")[0];
381
+ }
382
+ } else if (thrown.type === "field_expression") {
383
+ target = thrown.text;
384
+ }
385
+ if (target) {
386
+ relationships.push({
387
+ // S1.7: ONE top-level sentinel — throws join calls/refs on
388
+ // "(script)" (was "global", the third disconnected root node).
389
+ source: parent?.name || "(script)",
390
+ target,
391
+ type: ASTEngine.RelationshipType.References,
392
+ // S1.4 (M49 #715): explicit confidence — undefined is treated
393
+ // as 1 by the classifier but filtered inconsistently by
394
+ // downstream >= 0.7 consumers. A throw target is structural.
395
+ confidence: 0.95,
396
+ // M59 (#825): the throw target is named at the throw expression.
397
+ line_number: (thrown ?? node).startPosition.row + 1,
398
+ metadata: { scalaSpecific: true, isThrow: true, relationshipType: "throw_target" }
399
+ });
400
+ }
401
+ }
402
+ } else if (node.type === "opaque_modifier") {
403
+ patternsObj["Opaque Types (Scala 3)"] = true;
404
+ } else if (node.type === "inline_modifier" || node.type === "transparent_modifier") {
405
+ patternsObj["Inline Methods (Scala 3)"] = true;
406
+ } else if (node.type === "using") {
407
+ patternsObj["Using Clauses (Scala 3)"] = true;
408
+ } else if (node.type === "implicit") {
409
+ patternsObj["Implicit Parameters & Conversions"] = true;
410
+ } else if (node.type === "lazy") {
411
+ patternsObj["Lazy Initialization"] = true;
412
+ } else if (node.type === "override") {
413
+ patternsObj["Override/Polymorphism"] = true;
414
+ }
415
+ if (node.type === "call_expression") {
416
+ this.handleCall(node, relationships, patternsObj, parent);
417
+ } else if (node.type === "instance_expression") {
418
+ this.handleInstantiation(node, relationships, parent);
419
+ const templateBody = this.findChild(node, "template_body");
420
+ if (templateBody) {
421
+ const typeNode = node.namedChild(0);
422
+ const superName = this.stripTypeArgs(this.scalaAnonymousSupertypeName(typeNode));
423
+ const anonBase = superName ? `anonymous_${superName}` : "anonymous_object";
424
+ const seen = this.anonNameUse.get(anonBase) ?? 0;
425
+ this.anonNameUse.set(anonBase, seen + 1);
426
+ const anonName = seen === 0 ? anonBase : `${anonBase}#${seen + 1}`;
427
+ const anonSym = {
428
+ name: anonName,
429
+ kind: ASTEngine.SymbolKind.Class,
430
+ range: {
431
+ startLine: node.startPosition.row + 1,
432
+ startColumn: node.startPosition.column + 1,
433
+ endLine: node.endPosition.row + 1,
434
+ endColumn: node.endPosition.column + 1,
435
+ startIndex: node.startIndex,
436
+ endIndex: node.endIndex
437
+ },
438
+ visibility: "public",
439
+ ownerName: parent?.name,
440
+ ownerKind: parent?.kind,
441
+ metadata: { isAnonymousObject: true, superTypes: superName ? [superName] : [], scalaSpecific: true }
442
+ };
443
+ symbols.push(anonSym);
444
+ indexType(anonSym);
445
+ currentParent = anonSym;
446
+ }
447
+ }
448
+ if (node.type === "if_expression" || node.type === "while_expression" || node.type === "do_while_expression" || node.type === "for_expression") {
449
+ cyclomaticComplexity++;
450
+ } else if (node.type === "case_clause") {
451
+ cyclomaticComplexity++;
452
+ } else if (node.type === "infix_expression") {
453
+ const op = node.childForFieldName("operator")?.text;
454
+ if (op === "&&" || op === "||") cyclomaticComplexity++;
455
+ }
456
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
457
+ const child = node.namedChild(i);
458
+ if (child) {
459
+ stack.push({ node: child, parent: currentParent });
460
+ }
461
+ }
462
+ }
463
+ this.detectFrameworks(imports, frameworksObj);
464
+ this.detectCompanionObjects(symbols, patternsObj);
465
+ metadata.hasTypeInference = hasTypes;
466
+ metadata.complexity = cyclomaticComplexity + 1;
467
+ metadata.cyclomaticComplexity = cyclomaticComplexity + 1;
468
+ for (const fw in frameworksObj) metadata.detectedFrameworks.push(fw);
469
+ for (const pat in patternsObj) metadata.patterns.push(pat);
470
+ {
471
+ const extByOwnerName = /* @__PURE__ */ new Map();
472
+ for (const s of symbols) {
473
+ const recv = s.metadata?.extensionReceiver;
474
+ if (!recv) continue;
475
+ const own = s.ownerName ?? "";
476
+ let byName = extByOwnerName.get(own);
477
+ if (!byName) {
478
+ byName = /* @__PURE__ */ new Map();
479
+ extByOwnerName.set(own, byName);
480
+ }
481
+ const list = byName.get(s.name);
482
+ if (list) list.push(s);
483
+ else byName.set(s.name, [s]);
484
+ }
485
+ const receiverLeaf = (t) => {
486
+ let s = t;
487
+ const cut = s.indexOf("[");
488
+ if (cut > 0) s = s.slice(0, cut);
489
+ const dot = s.lastIndexOf(".");
490
+ if (dot >= 0) s = s.slice(dot + 1);
491
+ return s.trim();
492
+ };
493
+ for (const byName of extByOwnerName.values()) {
494
+ for (const group of byName.values()) {
495
+ if (group.length < 2) continue;
496
+ const receivers = new Set(group.map((s) => String(s.metadata.extensionReceiver)));
497
+ if (receivers.size < 2) continue;
498
+ for (const s of group) {
499
+ const leaf = receiverLeaf(String(s.metadata.extensionReceiver));
500
+ const renamed = leaf.length > 0 ? `${s.name}@${leaf}` : `${s.name}@ext${group.indexOf(s) + 1}`;
501
+ for (const r of relationships) {
502
+ if (r.metadata?.relationshipType !== "scala_extension_method") continue;
503
+ if (r.source !== s.name) continue;
504
+ if (r.target !== s.metadata.extensionReceiver) continue;
505
+ r.source = renamed;
506
+ }
507
+ s.name = renamed;
508
+ s.id = s.ownerName ? `${s.ownerName}.${renamed}` : renamed;
509
+ }
510
+ }
511
+ }
512
+ }
513
+ const sigText = (m) => "(" + (m.parameters ?? []).map((p) => this.compactType(p.type || "unknown")).join(",") + ")";
514
+ const hasTypeVar = (sig) => sigHasTypeVar(sig);
515
+ const methodSigsByOwner = /* @__PURE__ */ new Map();
516
+ const nonPrivateMethodsByOwner = /* @__PURE__ */ new Map();
517
+ for (const s of symbols) {
518
+ if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName || s.visibility === "private") continue;
519
+ let byName = nonPrivateMethodsByOwner.get(s.ownerName);
520
+ if (!byName) {
521
+ byName = /* @__PURE__ */ new Map();
522
+ nonPrivateMethodsByOwner.set(s.ownerName, byName);
523
+ }
524
+ let arr = byName.get(s.name);
525
+ if (!arr) {
526
+ arr = [];
527
+ byName.set(s.name, arr);
528
+ }
529
+ arr.push(s);
530
+ }
531
+ const sigTextElided = (m) => {
532
+ const ps = m.parameters ?? [];
533
+ let end = ps.length;
534
+ while (end > 0 && ps[end - 1].isImplicit) end--;
535
+ return sigText({ ...m, parameters: ps.slice(0, end) });
536
+ };
537
+ for (const s of symbols) {
538
+ if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName || s.metadata?.isPhantom) continue;
539
+ let byName = methodSigsByOwner.get(s.ownerName);
540
+ if (!byName) {
541
+ byName = /* @__PURE__ */ new Map();
542
+ methodSigsByOwner.set(s.ownerName, byName);
543
+ }
544
+ let sigs = byName.get(s.name);
545
+ if (!sigs) {
546
+ sigs = /* @__PURE__ */ new Set();
547
+ byName.set(s.name, sigs);
548
+ }
549
+ sigs.add(sigText(s));
550
+ sigs.add(sigTextElided(s));
551
+ }
552
+ const findDeclaringAncestor = (s) => {
553
+ const ownerSym = resolveOwner(s);
554
+ if (!ownerSym) return null;
555
+ const sig = sigText(s);
556
+ const seen = /* @__PURE__ */ new Set();
557
+ const queue = [...ownerSym.metadata?.superTypes ?? []];
558
+ while (queue.length > 0) {
559
+ const parent = queue.shift();
560
+ if (seen.has(parent)) continue;
561
+ seen.add(parent);
562
+ const parentMethods = methodSigsByOwner.get(parent);
563
+ const parentSigs = parentMethods?.get(s.name);
564
+ const ownerMethods = nonPrivateMethodsByOwner.get(parent)?.get(s.name) ?? [];
565
+ if (parentSigs?.has(sig)) {
566
+ const nonPrivate = ownerMethods.some((bm) => sigText(bm) === sig || sigTextElided(bm) === sig);
567
+ if (nonPrivate) return parent;
568
+ }
569
+ if (!parentSigs?.has(sig) && parentSigs && parentSigs.size > 0 && ([...parentSigs].some(hasTypeVar) || hasTypeVar(sig))) {
570
+ if (ownerMethods.length > 0) return parent;
571
+ }
572
+ const pSym = typeIndex.get(parent);
573
+ if (pSym?.metadata?.superTypes) {
574
+ for (const p of pSym.metadata.superTypes) queue.push(p);
575
+ }
576
+ }
577
+ return null;
578
+ };
579
+ for (const s of symbols) {
580
+ const _owner = s.ownerName;
581
+ if (_owner) {
582
+ relationships.push({
583
+ source: _owner,
584
+ target: s.name,
585
+ type: ASTEngine.RelationshipType.Contains,
586
+ confidence: 1,
587
+ // M59 (#825): a containment edge has no "call site", but it does have
588
+ // a navigable site — the member's own declaration line. Taken from the
589
+ // symbol's range rather than a syntax node because this loop runs over
590
+ // the symbol table, and omitted entirely when the symbol has no range
591
+ // (never written as 0 — #820).
592
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
593
+ metadata: { isContained: true }
594
+ });
595
+ }
596
+ if (s.kind === ASTEngine.SymbolKind.Method && s.ownerName && !s.metadata?.isPhantom && s.metadata?.isOverride) {
597
+ const declaringInFile = findDeclaringAncestor(s);
598
+ if (declaringInFile && !isSelfOverride(declaringInFile, s.ownerName)) {
599
+ relationships.push({
600
+ source: `${s.ownerName}.${s.name}`,
601
+ target: `${declaringInFile}.${s.name}`,
602
+ type: ASTEngine.RelationshipType.Overrides,
603
+ confidence: 1,
604
+ // M59 (#825): the override occurs at the OVERRIDING declaration, so
605
+ // the line is the overriding method's own — not the base's.
606
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
607
+ // S1.3 (M49 #715): override edges carry the parameter signature
608
+ // payload — the source/target keep the family convention
609
+ // (Owner.name → Parent.name), but the SIG travels in metadata so
610
+ // overloads foo(Int)/foo(String) are distinguishable downstream.
611
+ metadata: { isOverride: true, overrideParent: declaringInFile, reasonCode: "exact-in-file", overrideSig: sigText(s) }
612
+ });
613
+ continue;
614
+ }
615
+ const ownerSym = resolveOwner(s);
616
+ const parents = ownerSym?.metadata?.superTypes ?? [];
617
+ let overrideParent = parents.length > 0 ? parents[0] : "";
618
+ let confidence = 0.5;
619
+ let reasonCode = "first-supertype-guess";
620
+ if (context?.overrideCompanion && parents.length > 0) {
621
+ const declaring = findDeclaringParent(
622
+ s.name,
623
+ parents,
624
+ context.overrideCompanion,
625
+ (t) => typeIndex.get(t)?.metadata?.superTypes ?? []
626
+ );
627
+ if (declaring) {
628
+ overrideParent = declaring;
629
+ confidence = 0.9;
630
+ reasonCode = "companion-index";
631
+ }
632
+ }
633
+ if (overrideParent && !isSelfOverride(overrideParent, s.ownerName)) {
634
+ relationships.push({
635
+ source: `${s.ownerName}.${s.name}`,
636
+ target: `${overrideParent}.${s.name}`,
637
+ type: ASTEngine.RelationshipType.Overrides,
638
+ confidence,
639
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
640
+ metadata: { isOverride: true, overrideParent, reasonCode, overrideSig: sigText(s) }
641
+ });
642
+ }
643
+ }
644
+ }
645
+ for (const s of symbols) {
646
+ if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName) continue;
647
+ if (s.metadata?.isPhantom || s.metadata?.isOverride || s.visibility === "private") continue;
648
+ const declaringInFile = findDeclaringAncestor(s);
649
+ if (declaringInFile && !isSelfOverride(declaringInFile, s.ownerName)) {
650
+ relationships.push({
651
+ source: `${s.ownerName}.${s.name}`,
652
+ target: `${declaringInFile}.${s.name}`,
653
+ type: ASTEngine.RelationshipType.Overrides,
654
+ confidence: 1,
655
+ ...s.range?.startLine ? { line_number: s.range.startLine } : {},
656
+ metadata: { isOverride: true, implicitOverride: true, overrideParent: declaringInFile, reasonCode: "exact-in-file", overrideSig: sigText(s) }
657
+ });
658
+ }
659
+ }
660
+ for (const imp of imports) {
661
+ if (!imp.module || imp.module.length === 0) continue;
662
+ relationships.push({
663
+ // Import references are sourced at "root".
664
+ //
665
+ // This reverses S1.7, which moved imports from "root" onto "(script)" so that
666
+ // the whole top level would share ONE sentinel. That unification is real, but this
667
+ // lane's hand-audited truth is sourced at "root" -- all six `references` rows it
668
+ // expects cite an import and every one of them reads
669
+ // root -> com.example.base.BaseService (import com.example.base.BaseService)
670
+ // root -> a.b (import a.b.{C, D => Renamed})
671
+ // root -> x.y.Z (import x.y.Z as W -- full module path)
672
+ // so the choice of label is part of the contract the differential checks, and
673
+ // "(script)" made every one of them an unmatched false negative.
674
+ //
675
+ // It also removes the rewrite below: an import referenced FROM the file's primary
676
+ // type (`UserService -> com.example.base.BaseService`) was neither "root" nor
677
+ // "(script)", so it was a false positive as well. Truth expects the import at the
678
+ // file level, not attributed to whichever type happens to be declared first.
679
+ //
680
+ // RESOLVED 2026-10-02 during branch consolidation: the pinned hand-audited truth
681
+ // at validation/m12/ground-truth/scala.json carries all six `references` rows with
682
+ // sourceId "root" (verified by direct query), and that truth file is identical on
683
+ // both sides of this merge. The evidence therefore decides: "root" is correct and
684
+ // the local S1.7 "(script)" sentinel would have failed all six.
685
+ source: "root",
686
+ target: imp.module || "",
687
+ type: ASTEngine.RelationshipType.References,
688
+ confidence: 0.9,
689
+ // M59 (#825): an import reference occurs where its TARGET is written —
690
+ // the module's last segment (handleImport records that line), falling
691
+ // back to the statement's start. Omitted, never zeroed, when neither is
692
+ // known (#820).
693
+ ...imp.moduleLine || imp.range?.startLine ? { line_number: imp.moduleLine || imp.range.startLine } : {},
694
+ metadata: { isImportDep: true }
695
+ });
696
+ }
697
+ for (const imp of imports) {
698
+ if (!imp.module || imp.module.length === 0) continue;
699
+ const excluded = /* @__PURE__ */ new Set();
700
+ for (const sp of imp.specifiers ?? []) if (sp?.excluded && typeof sp.imported === "string") excluded.add(sp.imported);
701
+ const impMeta = imp.metadata ?? {};
702
+ const groupList = Array.isArray(impMeta.importGroups) && impMeta.importGroups.length > 0 ? impMeta.importGroups : [{ prefix: imp.module, names: (imp.items ?? []).filter((s) => typeof s === "string" && s.length > 0) }];
703
+ const targets = [];
704
+ for (const g of groupList) {
705
+ const base = typeof g?.prefix === "string" ? g.prefix : "";
706
+ if (base.length === 0) continue;
707
+ const names = (Array.isArray(g?.names) ? g.names : []).filter((n) => typeof n === "string" && n.length > 0 && n !== "*" && !excluded.has(n));
708
+ if (names.length === 0) {
709
+ targets.push(base);
710
+ continue;
711
+ }
712
+ const alreadyFull = names.every((n) => base.endsWith(`.${n}`) || base === n);
713
+ if (alreadyFull) targets.push(base);
714
+ else for (const n of names) targets.push(`${base}.${n}`);
715
+ }
716
+ const emittedHere = /* @__PURE__ */ new Set();
717
+ for (const t of targets) {
718
+ if (t.length === 0 || emittedHere.has(t)) continue;
719
+ emittedHere.add(t);
720
+ relationships.push({
721
+ source: "(file)",
722
+ target: t,
723
+ type: ASTEngine.RelationshipType.Imports,
724
+ confidence: 1,
725
+ ...imp.range?.startLine ? { line_number: imp.range.startLine } : {},
726
+ // `isImport` is what script/m14/completeness-inventory.ts section B reads
727
+ // to pin the kind, so the label is claimed twice over — by the enum and
728
+ // by the marker — exactly as the go lane's edge does.
729
+ metadata: { isImport: true, isScalaImport: true, packageName: t }
730
+ });
731
+ }
732
+ }
733
+ const publishedExports = /* @__PURE__ */ new Set();
734
+ for (const e of exports) {
735
+ if (!e.name || publishedExports.has(e.name)) continue;
736
+ publishedExports.add(e.name);
737
+ relationships.push({
738
+ source: "(file)",
739
+ target: e.name,
740
+ type: ASTEngine.RelationshipType.Exports,
741
+ confidence: 1,
742
+ ...e.range?.startLine ? { line_number: e.range.startLine } : {},
743
+ metadata: { isScalaExport: true, exportKind: String(e.kind ?? "") }
744
+ });
745
+ }
746
+ this.collectScalaReExports(rootNode, relationships);
747
+ this.emitExportSurface(rootNode, exports, relationships);
748
+ this.collectDeclaredTypeRefs(rootNode, relationships, symbols);
749
+ this.collectExpressionUses(rootNode, relationships);
750
+ this.classifyEvidenceTier(relationships);
751
+ this.certifyJvmPlatformTargets(rootNode, relationships, symbols);
752
+ return {
753
+ symbols,
754
+ relationships,
755
+ imports,
756
+ exports,
757
+ metadata,
758
+ errors: metadata.errors || []
759
+ };
760
+ }
761
+ /**
762
+ * The MODULE a Scala file belongs to: its full package path.
763
+ *
764
+ * Read from the `package_clause` nodes themselves rather than from
765
+ * `metadata.packageName`, which the walk overwrites per clause. Scala writes a
766
+ * package header two ways and this corpus uses the chained one exclusively:
767
+ *
768
+ * package scala.tools.nsc <- one dotted clause
769
+ * package typechecker <- a second clause CHAINED onto it
770
+ *
771
+ * The full path is the two joined with "."; a single braced clause
772
+ * (`package a.b { … }`) needs no join. Measured on 600 corpus files
773
+ * (script/m14/tmp/scala-package-identity.mts): 599 files chained-only, 1
774
+ * mixed, 0 braced-only, and `metadata.packageName` equals the FULL path in
775
+ * only the 292 single-clause files — on the other 308 it holds the last
776
+ * fragment (`typechecker`, `nsc`), which is not a package any reference site
777
+ * spells.
778
+ *
779
+ * Only TOP-LEVEL clauses are joined. A clause nested in a braced block is
780
+ * already written as its full path (`package reflect { … }` inside
781
+ * `package scala { … }` still spells `scala.reflect`), and joining it again
782
+ * would double the prefix.
783
+ *
784
+ * FILE_MODULE is the fallback for a file with no package clause at all — the
785
+ * default package, where the compilation unit IS the module. It is a sentinel
786
+ * and not a symbol, declared as such: zig's `(file)` and this lane's own
787
+ * `(script)`/`root` sentinels are the same kind of thing, and the alternative
788
+ * — publishing an empty source — is the defect the M60/SDJ-11 work removed.
789
+ */
790
+ static FILE_MODULE = "(file)";
791
+ scalaModuleName(rootNode) {
792
+ const parts = [];
793
+ for (let i = 0; i < rootNode.namedChildCount; i++) {
794
+ const c = rootNode.namedChild(i);
795
+ if (!c || c.type !== "package_clause") continue;
796
+ let id = null;
797
+ for (let j = 0; j < c.namedChildCount; j++) {
798
+ const x = c.namedChild(j);
799
+ if (x && x.type === "package_identifier") {
800
+ id = x;
801
+ break;
802
+ }
803
+ }
804
+ if (!id) continue;
805
+ const segs = [];
806
+ const collect = (n) => {
807
+ if (n.type === "identifier") segs.push(n.text);
808
+ for (let k = 0; k < n.namedChildCount; k++) {
809
+ const y = n.namedChild(k);
810
+ if (y) collect(y);
811
+ }
812
+ };
813
+ collect(id);
814
+ const seg = segs.join(".");
815
+ if (seg.length > 0) parts.push(stripScalaQuoting(seg));
816
+ }
817
+ if (parts.length === 0) return _RealScalaAnalyzer.FILE_MODULE;
818
+ return parts.join(".");
819
+ }
820
+ /**
821
+ * `exports` — this module's public surface, as relationships.
822
+ *
823
+ * Two producers, one dedupe (see the call site for the measurement behind
824
+ * both):
825
+ *
826
+ * A. IMPLICIT, from the `ExportInfo[]` the walk already builds: every public
827
+ * TOP-LEVEL class/trait/object/enum/function/val/var/named-given. The
828
+ * array is the lane's existing definition of the surface — it is what the
829
+ * fixture gate's F3 export-uniqueness check reads — so the edges are
830
+ * derived from it rather than from a second, divergent rule that could
831
+ * drift from it.
832
+ *
833
+ * B. EXPLICIT, from Scala 3 `export_declaration`. The exported NAME is the
834
+ * leaf that is actually introduced here: a selector, or the final segment
835
+ * of a plain path. Two forms publish NO edge and the reason is the
836
+ * language's, not caution:
837
+ *
838
+ * `export a.b.*` re-exports an unknown set of names — the clause
839
+ * names a set, not a name, so an edge per member
840
+ * would mean inventing members this file never
841
+ * writes.
842
+ * `export a.b.given` re-exports the GIVEN INSTANCES resolved for
843
+ * `a.b`'s type — a set defined by implicit
844
+ * resolution in another compilation unit.
845
+ *
846
+ * Both are recorded in the edge metadata (`exportForm`) rather than
847
+ * dropped, so a reader can see that the file re-exports a wildcard and
848
+ * that the analyzer declines to enumerate it.
849
+ *
850
+ * `confidence: 1.0` and `evidenceClass: structural` (the tier classifier
851
+ * already routes `exports` there): the edge is read off a declaration, and
852
+ * nothing about it is inferred.
853
+ */
854
+ emitExportSurface(rootNode, exports, relationships) {
855
+ const moduleName = this.scalaModuleName(rootNode);
856
+ const surfaceIndexes = /* @__PURE__ */ new Set();
857
+ const SURFACE_DECLS = /* @__PURE__ */ new Set([
858
+ "class_definition",
859
+ "trait_definition",
860
+ "object_definition",
861
+ "enum_definition",
862
+ "val_definition",
863
+ "var_definition",
864
+ "val_declaration",
865
+ "var_declaration",
866
+ "function_definition",
867
+ "given_definition"
868
+ ]);
869
+ const collectSurface = (scope) => {
870
+ for (let i = 0; i < scope.namedChildCount; i++) {
871
+ const c = scope.namedChild(i);
872
+ if (!c) continue;
873
+ if (SURFACE_DECLS.has(c.type)) {
874
+ surfaceIndexes.add(this.declarationLineNode(c).startIndex);
875
+ continue;
876
+ }
877
+ if (c.type === "package_clause" || c.type === "package_object") {
878
+ for (let j = 0; j < c.namedChildCount; j++) {
879
+ const b = c.namedChild(j);
880
+ if (b && (b.type === "block" || b.type === "template_body")) collectSurface(b);
881
+ }
882
+ }
883
+ }
884
+ };
885
+ collectSurface(rootNode);
886
+ const seen = /* @__PURE__ */ new Set();
887
+ const surfaceChannelDuplicated = moduleName === _RealScalaAnalyzer.FILE_MODULE;
888
+ const key = (name) => `${moduleName} ${name}`;
889
+ for (const e of exports) {
890
+ if (surfaceChannelDuplicated) continue;
891
+ const name = stripScalaQuoting(String(e.name ?? ""));
892
+ if (name.length === 0) continue;
893
+ if (e.range?.startIndex !== void 0 && !surfaceIndexes.has(e.range.startIndex)) continue;
894
+ if (seen.has(key(name))) continue;
895
+ seen.add(key(name));
896
+ relationships.push({
897
+ source: moduleName,
898
+ target: name,
899
+ type: ASTEngine.RelationshipType.Exports,
900
+ confidence: 1,
901
+ // M59 (#825): the surface is declared where the name is declared.
902
+ ...e.range?.startLine ? { line_number: e.range.startLine } : {},
903
+ metadata: {
904
+ exportForm: "public-top-level-declaration",
905
+ symbolKind: String(e.kind ?? "")
906
+ }
907
+ });
908
+ }
909
+ const visit = (n) => {
910
+ if (n.type === "export_declaration") {
911
+ for (const t of this.scalaExportClauseTargets(n)) {
912
+ const k = key(t.name);
913
+ if (seen.has(k)) continue;
914
+ seen.add(k);
915
+ relationships.push({
916
+ source: moduleName,
917
+ target: t.name,
918
+ type: ASTEngine.RelationshipType.Exports,
919
+ confidence: 1,
920
+ line_number: n.startPosition.row + 1,
921
+ metadata: { exportForm: t.form }
922
+ });
923
+ }
924
+ }
925
+ for (let i = 0; i < n.namedChildCount; i++) {
926
+ const c = n.namedChild(i);
927
+ if (c) visit(c);
928
+ }
929
+ };
930
+ visit(rootNode);
931
+ }
932
+ /**
933
+ * The names one Scala 3 `export` clause introduces into this file, read from
934
+ * the grammar's own node types (probe:
935
+ * script/m14/tmp/scala-scala3-export-clause.mts dumps all six forms):
936
+ *
937
+ * export a.b.c -> export_declaration > identifier "." identifier
938
+ * "." identifier => leaf `c`
939
+ * export a.b.{c, d} -> namespace_selectors > identifier => `c`, `d`
940
+ * export a.b.{c => d, e} -> namespace_selectors > arrow_renamed_identifier
941
+ * > identifier "=>" identifier => `d`
942
+ * (the name INTRODUCED here is `d`; `c` is
943
+ * only reachable under it)
944
+ * export a.b.{given C} -> namespace_selectors > given type_identifier
945
+ * => `C`
946
+ * export a.b.* -> namespace_wildcard => no name to publish
947
+ * export a.b.given -> namespace_wildcard `given` => no name to publish
948
+ *
949
+ * Each entry carries the FORM it came from, so the edge metadata says which
950
+ * spelling produced the name.
951
+ */
952
+ scalaExportClauseTargets(node) {
953
+ const out = [];
954
+ const path = [];
955
+ let wildcard = false;
956
+ let sawSelectors = false;
957
+ for (let i = 0; i < node.namedChildCount; i++) {
958
+ const c = node.namedChild(i);
959
+ if (!c) continue;
960
+ if (c.type === "identifier") {
961
+ path.push(c.text);
962
+ continue;
963
+ }
964
+ if (c.type === "namespace_selectors") {
965
+ sawSelectors = true;
966
+ for (let j = 0; j < c.namedChildCount; j++) {
967
+ const s = c.namedChild(j);
968
+ if (!s) continue;
969
+ if (s.type === "identifier") {
970
+ out.push({ name: stripScalaQuoting(s.text), form: "scala3-export-selector" });
971
+ } else if (s.type === "arrow_renamed_identifier") {
972
+ let last = null;
973
+ for (let k = 0; k < s.namedChildCount; k++) {
974
+ const id = s.namedChild(k);
975
+ if (id && id.type === "identifier") last = id.text;
976
+ }
977
+ if (last) out.push({ name: stripScalaQuoting(last), form: "scala3-export-renamed-selector" });
978
+ } else if (s.type === "type_identifier") {
979
+ out.push({ name: stripScalaQuoting(s.text), form: "scala3-export-given-selector" });
980
+ } else if (s.type === "generic_type") {
981
+ out.push({ name: this.scalaTypeHeadName(s), form: "scala3-export-given-selector" });
982
+ }
983
+ }
984
+ continue;
985
+ }
986
+ if (c.type === "namespace_wildcard") {
987
+ wildcard = true;
988
+ }
989
+ }
990
+ if (out.length === 0 && !wildcard && !sawSelectors && path.length > 0) {
991
+ out.push({ name: stripScalaQuoting(path[path.length - 1]), form: "scala3-export-clause" });
992
+ }
993
+ return out.filter((t) => t.name.length > 0);
994
+ }
995
+ detectFrameworks(imports, frameworksObj) {
996
+ for (let i = 0; i < imports.length; i++) {
997
+ const source = imports[i].module;
998
+ if (!source) continue;
999
+ if (source.indexOf("play.api") !== -1) frameworksObj["Play Framework"] = true;
1000
+ if (source.indexOf("akka") !== -1) frameworksObj["Akka"] = true;
1001
+ if (source.indexOf("org.apache.spark") !== -1) frameworksObj["Apache Spark"] = true;
1002
+ const isRoot = (root) => source === root || source.startsWith(root + ".");
1003
+ if (isRoot("cats")) frameworksObj["Cats Effect"] = true;
1004
+ if (isRoot("zio")) frameworksObj["ZIO"] = true;
1005
+ if (isRoot("org.http4s")) frameworksObj["http4s"] = true;
1006
+ if (isRoot("slick") || isRoot("org.slick")) frameworksObj["Slick"] = true;
1007
+ if (isRoot("fs2")) frameworksObj["FS2"] = true;
1008
+ if (isRoot("org.scalatest")) frameworksObj["ScalaTest"] = true;
1009
+ if (isRoot("org.springframework.boot")) frameworksObj["Spring Boot"] = true;
1010
+ if (isRoot("org.springframework")) frameworksObj["Spring Framework"] = true;
1011
+ if (isRoot("com.fasterxml.jackson")) frameworksObj["Jackson"] = true;
1012
+ if (isRoot("com.github.plokhotnyuk.jsoniter_scala")) frameworksObj["jsoniter-scala"] = true;
1013
+ if (isRoot("com.softwaremill.sttp")) frameworksObj["sttp"] = true;
1014
+ }
1015
+ }
1016
+ detectCompanionObjects(symbols, patternsObj) {
1017
+ const classNames = {};
1018
+ const objectNames = {};
1019
+ for (let i = 0; i < symbols.length; i++) {
1020
+ const sym = symbols[i];
1021
+ if (sym.kind === ASTEngine.SymbolKind.Class || sym.metadata?.isTrait) {
1022
+ classNames[sym.name] = true;
1023
+ } else if (sym.metadata?.isObject) {
1024
+ objectNames[sym.name] = true;
1025
+ }
1026
+ }
1027
+ for (let i = 0; i < symbols.length; i++) {
1028
+ const sym = symbols[i];
1029
+ if ((sym.kind === ASTEngine.SymbolKind.Class || sym.metadata?.isTrait) && objectNames[sym.name]) {
1030
+ if (!sym.metadata) sym.metadata = {};
1031
+ sym.metadata.hasCompanionObject = true;
1032
+ patternsObj["Companion Objects"] = true;
1033
+ } else if (sym.metadata?.isObject && classNames[sym.name]) {
1034
+ if (!sym.metadata) sym.metadata = {};
1035
+ sym.metadata.isCompanionObject = true;
1036
+ patternsObj["Companion Objects"] = true;
1037
+ }
1038
+ if (sym.metadata?.isCaseClass) {
1039
+ if (!sym.metadata) sym.metadata = {};
1040
+ sym.metadata.hasCompanionObject = true;
1041
+ }
1042
+ }
1043
+ }
1044
+ extractIdentifier(node, field) {
1045
+ const child = node.childForFieldName(field);
1046
+ if (child) return child.text;
1047
+ for (let i = 0; i < node.childCount; i++) {
1048
+ const c = node.child(i);
1049
+ if (c && c.isNamed && (c.type === "identifier" || c.type === "type_identifier")) {
1050
+ return c.text;
1051
+ }
1052
+ }
1053
+ return null;
1054
+ }
1055
+ /** Collect modifiers (sealed/abstract/final/case/implicit/override/etc). */
1056
+ collectModifiers(node) {
1057
+ const mods = /* @__PURE__ */ new Set();
1058
+ for (let i = 0; i < node.childCount; i++) {
1059
+ const c = node.child(i);
1060
+ if (!c) continue;
1061
+ if (c.type === "modifiers") {
1062
+ for (let j = 0; j < c.childCount; j++) {
1063
+ const m = c.child(j);
1064
+ if (m) mods.add(m.type);
1065
+ }
1066
+ } else {
1067
+ mods.add(c.type);
1068
+ }
1069
+ }
1070
+ return mods;
1071
+ }
1072
+ /**
1073
+ * SCD5: the short name of ONE annotation node (@Deprecated →
1074
+ * "Deprecated", @org.x.Transactional → "Transactional"). The walk calls
1075
+ * this on the annotation node itself; collectAnnotations (below) reuses it
1076
+ * for every annotation child of a definition.
1077
+ */
1078
+ annotationShortName(ann) {
1079
+ for (let k = 0; k < ann.namedChildCount; k++) {
1080
+ const nc = ann.namedChild(k);
1081
+ if (nc && (nc.type === "identifier" || nc.type === "type_identifier" || nc.type === "stable_type_identifier")) {
1082
+ const text = nc.text;
1083
+ const dot = text.lastIndexOf(".");
1084
+ return dot === -1 ? text : text.slice(dot + 1);
1085
+ }
1086
+ }
1087
+ return null;
1088
+ }
1089
+ /**
1090
+ * Annotation short names from a definition's modifiers (@Deprecated →
1091
+ * "Deprecated", @org.x.Transactional → "Transactional"). Previously
1092
+ * annotations were consumed only as pattern-flag substrings and symbols
1093
+ * carried no decorator data at all, unlike every sibling analyzer.
1094
+ */
1095
+ collectAnnotations(node) {
1096
+ const out = [];
1097
+ for (let i = 0; i < node.childCount; i++) {
1098
+ const c = node.child(i);
1099
+ if (!c) continue;
1100
+ if (c.type !== "annotation" && c.type !== "modifiers") continue;
1101
+ const container = c.type === "annotation" ? null : c;
1102
+ const sources = container ? [container] : [c];
1103
+ for (const src of sources) {
1104
+ for (let j = 0; j < src.childCount; j++) {
1105
+ const ann = container ? src.child(j) : src;
1106
+ if (!ann || ann.type !== "annotation") continue;
1107
+ const short = this.annotationShortName(ann);
1108
+ if (short) out.push(short);
1109
+ if (!container) break;
1110
+ }
1111
+ }
1112
+ }
1113
+ return out;
1114
+ }
1115
+ /**
1116
+ * M59 (#825): the node whose START is the declaration's own line.
1117
+ *
1118
+ * tree-sitter-scala gives a definition the line of its LEADING ANNOTATION —
1119
+ * measured, not assumed: for
1120
+ *
1121
+ * @tailrec ← row 4
1122
+ * def loop(n: Int): Int = ← row 5
1123
+ *
1124
+ * the `function_definition` node starts at row 4 while its `name` field is at
1125
+ * row 5. Stamping the node's start put the `contains` edge (and the symbol's
1126
+ * own line) on the annotation, so the line no longer contained the declared
1127
+ * name — 4 of 13 remaining scala oracle failures on a 60-file sample, and the
1128
+ * shape that would otherwise be copied by java/kotlin/groovy.
1129
+ *
1130
+ * An annotation is a modifier: it decorates the declaration, it is not where
1131
+ * the declaration is written. Skipping leading annotations (and `modifiers`
1132
+ * wrappers) gives the declaration keyword line, which is what a tool must
1133
+ * point at and what the tier contract can check against the source.
1134
+ *
1135
+ * COMMENTS are skipped for the same reason, and that is not hypothetical —
1136
+ * measured on cats-effect's QueueBenchmark.scala, where
1137
+ *
1138
+ * @Param(Array("32768")) // must be a power of 2
1139
+ * var size: Int = _
1140
+ *
1141
+ * gives `var_definition` the named children `annotation, comment, identifier,
1142
+ * type_identifier, wildcard`: skipping only the annotation selected the
1143
+ * trailing COMMENT, so the symbol and its `contains` edge landed on the
1144
+ * annotation's line (47, column 26) instead of the declaration's (48, column
1145
+ * 6). A comment is text ABOUT the declaration, never the declaration.
1146
+ */
1147
+ declarationLineNode(node) {
1148
+ const nameNode = node.childForFieldName?.("name");
1149
+ if (nameNode) return nameNode;
1150
+ for (let i = 0; i < node.namedChildCount; i++) {
1151
+ const c = node.namedChild(i);
1152
+ if (!c) continue;
1153
+ if (c.type === "annotation" || c.type === "modifiers" || c.type === "opaque_modifier" || c.type === "inline_modifier") continue;
1154
+ if (c.type === "comment" || c.type === "block_comment") continue;
1155
+ return c;
1156
+ }
1157
+ return node;
1158
+ }
1159
+ getVisibility(node) {
1160
+ let modifiersNode = null;
1161
+ for (let i = 0; i < node.childCount; i++) {
1162
+ const c = node.child(i);
1163
+ if (c && c.type === "modifiers") {
1164
+ modifiersNode = c;
1165
+ break;
1166
+ }
1167
+ }
1168
+ if (modifiersNode) {
1169
+ for (let i = 0; i < modifiersNode.childCount; i++) {
1170
+ const mod = modifiersNode.child(i);
1171
+ if (mod && mod.type === "access_modifier") {
1172
+ if (mod.text.includes("[")) return "package";
1173
+ const inner = mod.child(0);
1174
+ if (inner && (inner.type === "private" || inner.type === "protected")) return inner.type;
1175
+ }
1176
+ if (mod && (mod.type === "private" || mod.type === "protected")) return mod.type;
1177
+ }
1178
+ }
1179
+ return "public";
1180
+ }
1181
+ isPublic(node) {
1182
+ return this.getVisibility(node) === "public";
1183
+ }
1184
+ isCaseClass(node) {
1185
+ for (let i = 0; i < node.childCount; i++) {
1186
+ const c = node.child(i);
1187
+ if (c && c.type === "case") return true;
1188
+ }
1189
+ return false;
1190
+ }
1191
+ /**
1192
+ * ScalaDoc extraction: a multiline_comment starting with '/**' immediately
1193
+ * preceding the declaration (previous sibling). Mirrors the kotlin KDoc
1194
+ * approach; previously scala symbols carried NO comments at all.
1195
+ */
1196
+ extractScalaDoc(node) {
1197
+ const sibling = node.previousNamedSibling;
1198
+ if (sibling && sibling.type === "block_comment" && sibling.text.startsWith("/**")) return sibling.text;
1199
+ return void 0;
1200
+ }
1201
+ /**
1202
+ * Generic type-parameter extraction from type_parameters nodes
1203
+ * (`class Store[T]`, `def map[R](...)`). Previously scala symbols carried
1204
+ * NO generic information at all - silent absence (M14 #383 fix).
1205
+ */
1206
+ extractScalaTypeParameters(node) {
1207
+ const tpNode = node.namedChildren.find((c) => c.type === "type_parameters");
1208
+ if (!tpNode) return [];
1209
+ const out = [];
1210
+ for (let i = 0; i < tpNode.namedChildCount; i++) {
1211
+ const c = tpNode.namedChild(i);
1212
+ if (c && this.isTypeishNodeName(c.type)) {
1213
+ out.push(c.text.trim());
1214
+ }
1215
+ }
1216
+ return out;
1217
+ }
1218
+ buildScalaCallPath(fnNode) {
1219
+ const parts = [];
1220
+ let cur = fnNode;
1221
+ while (cur) {
1222
+ if (cur.type === "field_expression") {
1223
+ parts.unshift(cur.namedChild(cur.namedChildCount - 1)?.text ?? "");
1224
+ cur = cur.namedChild(0);
1225
+ } else if (cur.type === "call_expression") {
1226
+ cur = cur.namedChild(0);
1227
+ } else {
1228
+ parts.unshift(cur.text);
1229
+ break;
1230
+ }
1231
+ }
1232
+ return parts.filter(Boolean).join(".");
1233
+ }
1234
+ /**
1235
+ * Per-function cyclomatic complexity (1 + decision points) - java/kotlin
1236
+ * parity (M14 #301 prerequisite).
1237
+ */
1238
+ calculateScalaFunctionComplexity(node) {
1239
+ let complexity = 1;
1240
+ const stack = [node];
1241
+ while (stack.length > 0) {
1242
+ const n = stack.pop();
1243
+ if (n.type === "if_expression" || n.type === "while_expression" || n.type === "do_while_expression" || n.type === "for_expression") {
1244
+ complexity++;
1245
+ } else if (n.type === "case_clause") {
1246
+ complexity++;
1247
+ } else if (n.type === "infix_expression") {
1248
+ const op = n.childForFieldName("operator")?.text;
1249
+ if (op === "&&" || op === "||") complexity++;
1250
+ }
1251
+ for (let i = n.namedChildCount - 1; i >= 0; i--) {
1252
+ const child = n.namedChild(i);
1253
+ if (child) stack.push(child);
1254
+ }
1255
+ }
1256
+ return complexity;
1257
+ }
1258
+ /**
1259
+ * M14 #299 - evidence-tier classification for EVERY emitted relationship.
1260
+ * Tiers: structural (grammar clauses, conf 1.0) | usage_contextual (calls,
1261
+ * field access, companion-apply idioms) | import_based | reference_qualified
1262
+ * | inferred (sub-1.0 resolutions). Applied centrally at end of analyze()
1263
+ * so no emission site can forget it.
1264
+ */
1265
+ /**
1266
+ * Declared-type reference graph (M14). References edges from the enclosing
1267
+ * function/class to every named type in a declaration position. Scala's
1268
+ * @throws[IO] annotation parses as generic_type("throws", [IO]) — the
1269
+ * "throws" name is skipped, its arguments become throws_type edges.
1270
+ */
1271
+ collectDeclaredTypeRefs(rootNode, relationships, declaredSymbols = []) {
1272
+ const emitted = /* @__PURE__ */ new Set();
1273
+ const declaredNames = /* @__PURE__ */ new Set();
1274
+ for (const s of declaredSymbols) if (s?.name) declaredNames.add(stripScalaQuoting(String(s.name)));
1275
+ const collectTypeNames = (n, out) => {
1276
+ const push = (name0) => {
1277
+ const name = stripScalaQuoting(name0);
1278
+ if (!name || name === "throws") return;
1279
+ if (out.some((x) => x.name === name)) return;
1280
+ out.push({ name, line: n.startPosition.row + 1 });
1281
+ };
1282
+ if (n.type === "type_identifier") {
1283
+ if (this.isScalaPlaceholderFragment(n)) return;
1284
+ const bare = stripScalaQuoting(n.text);
1285
+ if ((bare === "?" || bare === "_") && !declaredNames.has(bare)) return;
1286
+ push(n.text);
1287
+ return;
1288
+ }
1289
+ if (n.type === "stable_type_identifier") {
1290
+ push(n.text);
1291
+ return;
1292
+ }
1293
+ for (let i = 0; i < n.namedChildCount; i++) {
1294
+ const c = n.namedChild(i);
1295
+ if (c) collectTypeNames(c, out);
1296
+ }
1297
+ };
1298
+ const resolveSource = (start) => {
1299
+ let p = start.type === "function_definition" || start.type === "function_declaration" ? start : start.parent;
1300
+ while (p) {
1301
+ if (p.type === "function_definition" || p.type === "function_declaration") {
1302
+ const name = this.scalaDeclaredNameOf(p);
1303
+ if (name) return name;
1304
+ }
1305
+ if (p.type === "class_definition" || p.type === "object_definition" || p.type === "trait_definition" || p.type === "enum_definition") {
1306
+ const name = this.scalaDeclaredNameOf(p);
1307
+ if (name) return name;
1308
+ }
1309
+ p = p.parent;
1310
+ }
1311
+ return "(script)";
1312
+ };
1313
+ const emit = (decl, roots, kind, relType = "declared_type", sourceOverride, leafOnly = false) => {
1314
+ const source = sourceOverride ?? resolveSource(decl);
1315
+ for (const r of roots) {
1316
+ if (!r) continue;
1317
+ const names = [];
1318
+ collectTypeNames(r, names);
1319
+ for (const { name: raw, line: nameLine } of names) {
1320
+ const t = leafOnly ? raw.includes(".") ? raw.slice(raw.lastIndexOf(".") + 1) : raw : raw;
1321
+ if (!t) continue;
1322
+ const key = `${source}|${t}|${kind}`;
1323
+ if (emitted.has(key)) continue;
1324
+ emitted.add(key);
1325
+ relationships.push({
1326
+ source,
1327
+ target: t,
1328
+ type: ASTEngine.RelationshipType.References,
1329
+ confidence: 0.95,
1330
+ // M59 (#825): a type reference occurs where the type is NAMED, so
1331
+ // the line is the type node's own — this one site covers
1332
+ // declared_type / return_type / pattern_type / type_alias /
1333
+ // cast_target / instanceof_check / given_type / tuple_type /
1334
+ // annotation_type, which together were 1,129 line-less edges on a
1335
+ // 60-file scala sample. Note what it is NOT: the declaration's line.
1336
+ // `def f: Map[K, V]` written over several lines references Map on
1337
+ // the line Map is written on. `nameLine` (not `r`'s start) is what
1338
+ // makes that true when the type expression itself is multi-line.
1339
+ line_number: nameLine,
1340
+ metadata: { relationshipType: relType, declaredOn: decl.type }
1341
+ });
1342
+ }
1343
+ }
1344
+ };
1345
+ const walk = (n) => {
1346
+ switch (n.type) {
1347
+ case "given_definition": {
1348
+ const roots = [];
1349
+ for (let i = 0; i < n.namedChildCount; i++) {
1350
+ const c = n.namedChild(i);
1351
+ if (c && this.isTypePositionNode(c.type)) roots.push(c);
1352
+ }
1353
+ if (roots.length) emit(n, roots, "given_definition", "given_type");
1354
+ break;
1355
+ }
1356
+ case "class_parameter":
1357
+ case "parameter": {
1358
+ emit(n, [n], n.type);
1359
+ break;
1360
+ }
1361
+ case "val_definition":
1362
+ case "var_definition":
1363
+ case "val_declaration":
1364
+ case "var_declaration": {
1365
+ const roots = [];
1366
+ for (let i = 0; i < n.namedChildCount; i++) {
1367
+ const c = n.namedChild(i);
1368
+ if (c && this.isTypePositionNode(c.type)) roots.push(c);
1369
+ }
1370
+ if (roots.length) emit(n, roots, n.type);
1371
+ break;
1372
+ }
1373
+ case "annotation": {
1374
+ const gt = this.findChild(n, "generic_type");
1375
+ if (gt && gt.namedChild(0)?.text === "throws") {
1376
+ const argsNode = this.findChild(gt, "type_arguments");
1377
+ if (argsNode) emit(n, [argsNode], "throws_annotation", "throws_type");
1378
+ }
1379
+ break;
1380
+ }
1381
+ case "function_definition":
1382
+ case "function_declaration": {
1383
+ let sawParams = false;
1384
+ const hasParamsClause = !!this.findChild(n, "parameters");
1385
+ for (let i = 0; i < n.namedChildCount; i++) {
1386
+ const c = n.namedChild(i);
1387
+ if (!c) continue;
1388
+ if (c.type === "parameters") {
1389
+ sawParams = true;
1390
+ continue;
1391
+ }
1392
+ if (c.type === "generic_type" || c.type === "type_identifier" || c.type === "function_type" || c.type === "tuple_type" || c.type === "stable_type_identifier") {
1393
+ if (!sawParams && c.namedChild(0)?.text === "throws") {
1394
+ const argsNode = this.findChild(c, "type_arguments");
1395
+ if (argsNode) emit(n, [argsNode], "throws_annotation", "throws_type");
1396
+ continue;
1397
+ }
1398
+ emit(n, [c], c.type === "tuple_type" ? "tuple_type" : sawParams || !hasParamsClause ? "return_type" : "annotation_type", sawParams || !hasParamsClause ? "return_type" : "declared_type");
1399
+ }
1400
+ }
1401
+ break;
1402
+ }
1403
+ case "call_expression": {
1404
+ const fn = n.childForFieldName?.("function");
1405
+ const fnText = fn ? fn.text : "";
1406
+ if (fnText.endsWith("asInstanceOf") || fnText.endsWith("isInstanceOf")) {
1407
+ const args = n.childForFieldName?.("arguments") || this.findChild(n, "type_arguments");
1408
+ if (args) emit(n, [args], fnText.endsWith("asInstanceOf") ? "cast_target" : "instanceof_check");
1409
+ }
1410
+ break;
1411
+ }
1412
+ case "generic_function": {
1413
+ const fe = this.findChild(n, "field_expression");
1414
+ const tail = fe ? fe.namedChild(fe.namedChildCount - 1)?.text ?? "" : "";
1415
+ if (tail === "asInstanceOf" || tail === "isInstanceOf") {
1416
+ const args = this.findChild(n, "type_arguments");
1417
+ if (args) emit(n, [args], tail === "asInstanceOf" ? "cast_target" : "instanceof_check", tail === "asInstanceOf" ? "cast_target" : "instanceof_check");
1418
+ }
1419
+ break;
1420
+ }
1421
+ case "typed_pattern": {
1422
+ const last = n.namedChild(n.namedChildCount - 1);
1423
+ if (last) emit(n, [last], "typed_pattern", "pattern_type");
1424
+ break;
1425
+ }
1426
+ case "case_class_pattern": {
1427
+ const first = n.namedChild(0);
1428
+ if (first) emit(n, [first], "case_class_pattern", "pattern_type");
1429
+ break;
1430
+ }
1431
+ case "derives_clause": {
1432
+ emit(n, [n], "derives_clause", "derives_type", void 0, true);
1433
+ break;
1434
+ }
1435
+ case "type_definition": {
1436
+ const tName = this.findChild(n, "type_identifier") ?? this.findChild(n, "identifier");
1437
+ if (tName) {
1438
+ const rhs = [];
1439
+ for (let i = 0; i < n.namedChildCount; i++) {
1440
+ const c = n.namedChild(i);
1441
+ if (c && c !== tName && c.type !== "type_parameters") rhs.push(c);
1442
+ }
1443
+ emit(n, rhs, "type_definition", "type_alias", tName.text);
1444
+ }
1445
+ break;
1446
+ }
1447
+ case "self_type": {
1448
+ const last = n.namedChild(n.namedChildCount - 1);
1449
+ if (last) emit(n, [last], "self_type", "self_type");
1450
+ const required = this.selfTypeRequiredTypeNames(n);
1451
+ if (required.length > 0) {
1452
+ const src = resolveSource(n);
1453
+ for (const { name: rname, line: rline } of required) {
1454
+ if (!rname) continue;
1455
+ const leaf = rname.includes(".") ? rname.slice(rname.lastIndexOf(".") + 1) : rname;
1456
+ if (rname === src || leaf === src) continue;
1457
+ const ikey = `implements|${src}|${rname}`;
1458
+ if (emitted.has(ikey)) continue;
1459
+ emitted.add(ikey);
1460
+ relationships.push({
1461
+ source: src,
1462
+ target: rname,
1463
+ type: ASTEngine.RelationshipType.Implements,
1464
+ // S1.4: an explicit conformance requirement is 1.0 — the construct says so.
1465
+ confidence: 1,
1466
+ // M59 (#825): the NAME's own line, not the declaration's.
1467
+ line_number: rline,
1468
+ metadata: { relationshipType: "self_type", isSelfType: true }
1469
+ });
1470
+ }
1471
+ }
1472
+ break;
1473
+ }
1474
+ case "field_expression": {
1475
+ const isCallFn = n.parent && n.parent.type === "call_expression" && n.parent.childForFieldName?.("function")?.id === n.id;
1476
+ if (!isCallFn) {
1477
+ const recv = this.leftmostIdentifier(n);
1478
+ if (recv && recv.text.length > 0 && recv.text.charCodeAt(0) >= 65 && recv.text.charCodeAt(0) <= 90) {
1479
+ const source = resolveSource(n);
1480
+ const key = `${source}|${recv.text}|field_expression`;
1481
+ if (!emitted.has(key)) {
1482
+ emitted.add(key);
1483
+ relationships.push({
1484
+ source,
1485
+ target: recv.text,
1486
+ type: ASTEngine.RelationshipType.References,
1487
+ confidence: 0.95,
1488
+ // M59 (#825): the referenced type is named by the receiver.
1489
+ line_number: recv.startPosition.row + 1,
1490
+ metadata: { relationshipType: "method_value", declaredOn: "field_expression" }
1491
+ });
1492
+ }
1493
+ }
1494
+ }
1495
+ break;
1496
+ }
1497
+ }
1498
+ for (let i = 0; i < n.namedChildCount; i++) {
1499
+ const c = n.namedChild(i);
1500
+ if (c) walk(c);
1501
+ }
1502
+ };
1503
+ walk(rootNode);
1504
+ }
1505
+ /**
1506
+ * M14 #419 G4/G8: expression-level Uses edges.
1507
+ * - interpolated_string_expression > interpolation: s"hi $name" USES the
1508
+ * interpolated identifier (simple $name form — the direct identifier
1509
+ * child). The ${expr} block form is expression-use territory (scala
1510
+ * emits no expression-level uses by design — D1/D3 boundary).
1511
+ * - field_expression (non-call position): h.value reads/writes and nullary
1512
+ * access xs.length USE the receiver — scala had no field-access uses
1513
+ * collector (only extension methods emitted Uses), the family gap vs
1514
+ * java/groovy/kotlin. Receiver-only: never guess method-vs-field (D3
1515
+ * ruling). Call positions (xs.map(...)) are skipped — the call edge
1516
+ * carries them; capitalized receivers (Foo.bar) are References (G3,
1517
+ * collectDeclaredTypeRefs). Deduped per (source, target).
1518
+ */
1519
+ collectExpressionUses(rootNode, relationships) {
1520
+ const emitted = /* @__PURE__ */ new Set();
1521
+ const resolveSource = (start) => {
1522
+ let p = start && (start.type === "function_definition" || start.type === "function_declaration") ? start : start ? start.parent : null;
1523
+ while (p) {
1524
+ if (p.type === "function_definition" || p.type === "function_declaration") {
1525
+ const name = this.scalaDeclaredNameOf(p);
1526
+ if (name) return name;
1527
+ }
1528
+ if (p.type === "class_definition" || p.type === "object_definition" || p.type === "trait_definition" || p.type === "enum_definition") {
1529
+ const name = this.scalaDeclaredNameOf(p);
1530
+ if (name) return name;
1531
+ }
1532
+ p = p.parent;
1533
+ }
1534
+ return "(script)";
1535
+ };
1536
+ const emitUse = (n, target, nameNode) => {
1537
+ if (!target) return;
1538
+ if (target === "_") return;
1539
+ const p = n.parent;
1540
+ const isCompoundAssign = p !== null && p.type === "infix_expression" && scalaOperatorIsAssignment(p.childForFieldName?.("operator")?.text ?? p.namedChild(1)?.text ?? "");
1541
+ const lhs = p && (p.type === "assignment_expression" || isCompoundAssign) ? p.namedChild(0) : null;
1542
+ if (lhs && lhs.startIndex === n.startIndex && lhs.endIndex === n.endIndex) return;
1543
+ const source = resolveSource(n.parent);
1544
+ const key = `${source}|${target}|uses`;
1545
+ if (emitted.has(key)) return;
1546
+ emitted.add(key);
1547
+ relationships.push({
1548
+ source,
1549
+ target,
1550
+ type: ASTEngine.RelationshipType.Uses,
1551
+ // M59 (#825): a read happens where the NAME is written, so the line
1552
+ // comes from the name node — not from `n`, which for a field access is
1553
+ // the receiver and would hand a multi-line chain the line it opened on
1554
+ // (the same defect class fixed for calls above). `n` is the fallback
1555
+ // only when no name node was passed.
1556
+ line_number: (nameNode ?? n).startPosition.row + 1,
1557
+ confidence: 0.9,
1558
+ metadata: { relationshipType: "expression_use" }
1559
+ });
1560
+ };
1561
+ const walk = (n) => {
1562
+ if (n.type === "interpolation") {
1563
+ const id = n.namedChild(0);
1564
+ if (id && id.type === "identifier" && !isKeywordUsedAsName(id.text) && id.text !== "_") emitUse(n, id.text, id);
1565
+ } else if (n.type === "field_expression") {
1566
+ const isCallFn = n.parent && n.parent.type === "call_expression" && n.parent.childForFieldName?.("function")?.id === n.id;
1567
+ if (!isCallFn) {
1568
+ const recv = this.leftmostIdentifier(n);
1569
+ const isSelfReceiver = recv !== null && (recv.text === "this" || recv.text === "super");
1570
+ if (isSelfReceiver) {
1571
+ const leaf = n.childForFieldName?.("field");
1572
+ if (leaf && leaf.text.length > 0) emitUse(n, stripScalaQuoting(leaf.text), leaf);
1573
+ } else if (recv && recv.text.length > 0 && !(recv.text.charCodeAt(0) >= 65 && recv.text.charCodeAt(0) <= 90)) {
1574
+ emitUse(n, stripScalaQuoting(recv.text), recv);
1575
+ }
1576
+ }
1577
+ }
1578
+ for (let i = 0; i < n.namedChildCount; i++) {
1579
+ const c = n.namedChild(i);
1580
+ if (c) walk(c);
1581
+ }
1582
+ };
1583
+ walk(rootNode);
1584
+ }
1585
+ /**
1586
+ * SCALA 3 RE-EXPORT CLAUSES — `export a.b.{x, y}`, `export a.b.*`,
1587
+ * `export a.b.{x => y}`.
1588
+ *
1589
+ * This is NOT the export convention. A public top-level declaration is this
1590
+ * file's OWN API; a re-export clause names something the file does not define
1591
+ * and makes it reachable under this package — a different fact, and the only
1592
+ * one of the two that is a DEPENDENCY in the graph's sense (it points at
1593
+ * another compilation unit's name).
1594
+ *
1595
+ * Node shape, measured (script/m14/tmp/scala-probe-export.ts):
1596
+ * export a.b.{x, y} -> (export_declaration path: (identifier) path: (identifier)
1597
+ * (namespace_selectors (identifier) (identifier)))
1598
+ * export a.b.* -> (export_declaration path: … path: … (namespace_wildcard))
1599
+ * export a.b.{x => y} -> (export_declaration … (namespace_selectors (arrow_renamed_identifier
1600
+ * name: (identifier) alias: (identifier))))
1601
+ *
1602
+ * The pinned grammar parses ONLY this standalone form. `export class Alpha`,
1603
+ * `export object Beta`, `export type Foo = Int` and `export def f(..)` all
1604
+ * produce an ERROR node, so the export MODIFIER spelling of the language does
1605
+ * not reach this analyzer at all — a separate limitation, recorded in the lane
1606
+ * doc, because a construct the grammar drops cannot be read by any walker over
1607
+ * it. (dotty's own sources use no re-export clause: 0 occurrences over 9,353
1608
+ * files. The emission is here because the CORPUS is one repository and the
1609
+ * construct is real — "the corpus does not use it" is not "the language cannot
1610
+ * express it", which is the distinction the ontology gate's json `exports`
1611
+ * note exists to enforce.)
1612
+ *
1613
+ * The target is the FULL re-exported path (`a.b.x`), not the bare selector,
1614
+ * because the clause's meaning is "this package now offers the name a.b.x";
1615
+ * publishing `x` alone would name a symbol the file neither declares nor
1616
+ * imports. That is the same `lastSegment` exception the derives-clause already
1617
+ * makes (emit()'s `leafOnly`, G6), stated for the same reason: a re-export is
1618
+ * written as a path.
1619
+ */
1620
+ collectScalaReExports(rootNode, relationships) {
1621
+ const stack = [rootNode];
1622
+ while (stack.length > 0) {
1623
+ const n = stack.pop();
1624
+ if (n.type === "export_declaration") {
1625
+ const parts = [];
1626
+ let wildcard = false;
1627
+ for (let i = 0; i < n.childCount; i++) {
1628
+ const c = n.child(i);
1629
+ if (!c) continue;
1630
+ if (c.type === "identifier") parts.push(stripScalaQuoting(c.text));
1631
+ else if (c.type === "namespace_wildcard") wildcard = true;
1632
+ else if (c.type === "namespace_selectors") {
1633
+ for (let j = 0; j < c.childCount; j++) {
1634
+ const sel = c.child(j);
1635
+ if (!sel) continue;
1636
+ if (sel.type === "identifier") parts.push(stripScalaQuoting(sel.text));
1637
+ else if (sel.type === "arrow_renamed_identifier") {
1638
+ const nm = sel.childForFieldName?.("name") ?? sel.namedChild(0);
1639
+ if (nm) parts.push(stripScalaQuoting(nm.text));
1640
+ }
1641
+ }
1642
+ }
1643
+ }
1644
+ if (parts.length === 0) continue;
1645
+ const target = parts.join(".");
1646
+ relationships.push({
1647
+ source: "(file)",
1648
+ target,
1649
+ type: ASTEngine.RelationshipType.Exports,
1650
+ confidence: 1,
1651
+ line_number: n.startPosition.row + 1,
1652
+ metadata: { isScalaExport: true, isScalaReExport: true, wildcard }
1653
+ });
1654
+ }
1655
+ for (let i = 0; i < n.namedChildCount; i++) {
1656
+ const c = n.namedChild(i);
1657
+ if (c) stack.push(c);
1658
+ }
1659
+ }
1660
+ }
1661
+ /**
1662
+ * CERTIFY THE JVM PLATFORM TARGETS — one pass over the finished edge list, additive.
1663
+ *
1664
+ * The claim, the list and the measurements behind it are on `SCALA_JVM_PLATFORM_ROOTS` above. This
1665
+ * method is the mechanism; these are the four guards, each one forced by a measured case rather than
1666
+ * by taste:
1667
+ *
1668
+ * 1. **DOTTED ONLY.** A bare target is refused even when it IS a platform word, because a scala
1669
+ * file can and does declare that name (`class java`, `val java`). 14 of the 117 candidate edges
1670
+ * in the measured population are a bare `java` read; every one of them is refused here.
1671
+ * 2. **NOT A NAME THIS FILE DECLARES.** The same guard zig uses, and the second line of defence
1672
+ * behind guard 4.
1673
+ * 3. **NO SECOND CLAIM.** An edge already certified by another mechanism keeps it.
1674
+ * 4. **NOT IN A FILE THAT WRITES IN A PLATFORM NAMESPACE.** `scala3/tests/pos/t8367.scala` declares
1675
+ * `package java.lang` and a `class Throwable` in it; a file whose own module leads with a
1676
+ * platform root is writing IN the namespace, so nothing it names is certified.
1677
+ *
1678
+ * RETURNS the number of edges certified, so a caller can print it — the count is not asserted here
1679
+ * because the analyzer publishes no file-level metadata key for it (adding one would change a payload
1680
+ * every fixture reader sees, for a number the gate already reports).
1681
+ */
1682
+ certifyJvmPlatformTargets(rootNode, relationships, symbols) {
1683
+ const moduleName = this.scalaModuleName(rootNode);
1684
+ const headDot = moduleName.indexOf(".");
1685
+ const ownModuleHead = moduleName === _RealScalaAnalyzer.FILE_MODULE ? "" : headDot < 0 ? moduleName : moduleName.slice(0, headDot);
1686
+ if (SCALA_JVM_PLATFORM_ROOTS.has(ownModuleHead)) return 0;
1687
+ const declaredHere = /* @__PURE__ */ new Set();
1688
+ for (const s of symbols) {
1689
+ if (s.name) declaredHere.add(String(s.name));
1690
+ const q = s.qualifiedName;
1691
+ if (typeof q === "string" && q.length > 0) declaredHere.add(q);
1692
+ }
1693
+ let certified = 0;
1694
+ for (const rel of relationships) {
1695
+ const md = rel.metadata;
1696
+ if (!md || typeof md !== "object") continue;
1697
+ let claimed = false;
1698
+ for (const k of SCALA_CLAIM_KEYS_ON_EDGE) {
1699
+ if (md[k] !== void 0) {
1700
+ claimed = true;
1701
+ break;
1702
+ }
1703
+ }
1704
+ if (claimed) continue;
1705
+ const target = String(rel.target ?? "");
1706
+ const dot = target.indexOf(".");
1707
+ if (dot <= 0) continue;
1708
+ if (declaredHere.has(target)) continue;
1709
+ if (!SCALA_JVM_PLATFORM_ROOTS.has(target.slice(0, dot))) continue;
1710
+ md.languageNamespace = [...SCALA_JVM_PLATFORM_ROOTS];
1711
+ certified++;
1712
+ }
1713
+ return certified;
1714
+ }
1715
+ classifyEvidenceTier(relationships) {
1716
+ for (const r of relationships) {
1717
+ const md = r.metadata ?? (r.metadata = {});
1718
+ if (md.evidenceClass) continue;
1719
+ const t = String(r.type ?? r.relationshipType ?? "");
1720
+ const conf = typeof r.confidence === "number" ? r.confidence : 1;
1721
+ if (t === "contains" || t === "exports") {
1722
+ md.evidenceClass = "structural";
1723
+ continue;
1724
+ }
1725
+ if (t === "imports") {
1726
+ md.evidenceClass = "import_based";
1727
+ continue;
1728
+ }
1729
+ if (t === "references") {
1730
+ md.evidenceClass = md.isImportDep ? "import_based" : "reference_qualified";
1731
+ continue;
1732
+ }
1733
+ if (t === "extends" || t === "implements") {
1734
+ md.evidenceClass = conf >= 1 ? "structural" : "inferred";
1735
+ continue;
1736
+ }
1737
+ if (t === "overrides") {
1738
+ md.evidenceClass = conf >= 1 ? "structural" : conf >= 0.85 ? "usage_contextual" : "inferred";
1739
+ continue;
1740
+ }
1741
+ if (t === "instantiates") {
1742
+ md.evidenceClass = conf >= 0.9 ? "structural" : "usage_contextual";
1743
+ continue;
1744
+ }
1745
+ md.evidenceClass = "usage_contextual";
1746
+ }
1747
+ }
1748
+ createSymbol(name, kind, node, parent, extraMetadata = {}) {
1749
+ name = stripScalaQuoting(name);
1750
+ const decorators = this.collectAnnotations(node);
1751
+ const kdoc = this.extractScalaDoc(node);
1752
+ const decl = this.declarationLineNode(node);
1753
+ return {
1754
+ name,
1755
+ kind,
1756
+ range: {
1757
+ startLine: decl.startPosition.row + 1,
1758
+ startColumn: decl.startPosition.column + 1,
1759
+ endLine: node.endPosition.row + 1,
1760
+ endColumn: node.endPosition.column + 1,
1761
+ startIndex: decl.startIndex,
1762
+ endIndex: node.endIndex
1763
+ },
1764
+ id: `${parent ? parent.id + "." : ""}${name}`,
1765
+ parent: parent ? parent.name : void 0,
1766
+ ownerName: parent ? parent.name : void 0,
1767
+ // ScalaD-G (M37.21 #610, scala3 Setup.scala): companion objects share
1768
+ // the class NAME (object Setup + class Setup) and the override post-pass
1769
+ // looks up the OWNER in a name-keyed typeIndex — without ownerKind the
1770
+ // module shadowed the class and its superTypes were unreachable (BFS
1771
+ // died at hop 0, Setup.isPreCC -> SetupAPI.isPreCC never emitted).
1772
+ // Stamp ownerKind at walk time so the post-pass routes lookups to the
1773
+ // same-kind index entry.
1774
+ ownerKind: parent ? parent.kind : void 0,
1775
+ visibility: this.getVisibility(node),
1776
+ parameters: extraMetadata.parameters,
1777
+ returnType: extraMetadata.returnType,
1778
+ decorators,
1779
+ comments: kdoc ? [kdoc] : [],
1780
+ // Hoisted top-level per family contract: the DB ingester reads
1781
+ // symbol.complexity (|| 1) for gravity scoring — leaving it only in
1782
+ // metadata left every scala symbol at complexity 1 in the index.
1783
+ complexity: extraMetadata.complexity,
1784
+ metadata: { ...extraMetadata, annotations: decorators }
1785
+ };
1786
+ }
1787
+ // ==========================================================================
1788
+ // ZERO-REGEX STRING HELPERS (M49 JVM-closure wave, user directive: analyzers
1789
+ // carry no regex unless a text micro-parse genuinely requires it).
1790
+ //
1791
+ // Each helper replaces a former regex literal with a char-scan whose
1792
+ // equivalence is provable by string algebra, recorded here per the
1793
+ // closure-audit rule that equivalence claims carry their own proof:
1794
+ //
1795
+ // - stripBracketGroups ≡ replace(/\[[^\[\]]*\]/g, ""), FAITHFULLY:
1796
+ // the regex matches `[` + a run containing NO `[` and NO `]` + `]`.
1797
+ // At each `[` the scan looks for the FIRST `]`; if any `[` occurs
1798
+ // before it, the match at this position fails (regex would advance)
1799
+ // and the `[` is emitted verbatim. A bare `>` or `<` is NEVER
1800
+ // consumed — measured on cats-effect `ParallelF[Id, *] ~> Id` where
1801
+ // a depth-scanner prototype dropped the `>` of `~>` (fabricated
1802
+ // `(ParallelF ~ Id)` targets, caught by the corpus differential).
1803
+ // - stripAngleGroups ≡ replace(/<[^<>]*>/g, ""), same first-delimiter
1804
+ // discipline for `<`/`>`; `~>` survives because a `>` with no
1805
+ // preceding unmatched `<` is copied through.
1806
+ // - stripTypeArgs ≡ bracket pass THEN angle pass (the original chained
1807
+ // replaces ran left-to-right in that order), then trim. Callers
1808
+ // loop to a fixpoint for nested groups, as before.
1809
+ // - compactType ≡ replace(/\s+/g, ""): `\s+` is a maximal whitespace
1810
+ // run; the scan copies non-space chars and skips runs — identical
1811
+ // output for every input (whitespace = Unicode space class per
1812
+ // JS \s; the scan uses the same set via c===" "||\t||\n||\r||\f||\v).
1813
+ // - isTypeishNodeName ≡ /identifier|type_identifier|_parameter$/.test
1814
+ // (unanchored) and ≡ /_type$|type_identifier/.test: `includes(sub)`
1815
+ // ≡ unanchored literal alternation; suffix tests use endsWith.
1816
+ // - isTypePositionNode ≡ (/type$|_type$/.test(t) || t==="type_identifier"
1817
+ // || t==="stable_type_identifier" || t==="generic_type"):
1818
+ // /type$|_type$/ over a node-type string ≡ endsWith("type") because
1819
+ // _type$ is subsumed by type$ (any string ending in "_type" ends
1820
+ // in "type") — the disjunction is exactly endsWith("type").
1821
+ // =========================================================================
1822
+ stripBracketGroups(s) {
1823
+ let out = "";
1824
+ for (let i = 0; i < s.length; i++) {
1825
+ if (s[i] !== "[") {
1826
+ out += s[i];
1827
+ continue;
1828
+ }
1829
+ let j = i + 1;
1830
+ let nested = false;
1831
+ while (j < s.length && s[j] !== "]") {
1832
+ if (s[j] === "[") {
1833
+ nested = true;
1834
+ break;
1835
+ }
1836
+ j++;
1837
+ }
1838
+ if (j < s.length && !nested) {
1839
+ i = j;
1840
+ continue;
1841
+ }
1842
+ out += s[i];
1843
+ }
1844
+ return out;
1845
+ }
1846
+ stripAngleGroups(s) {
1847
+ let out = "";
1848
+ for (let i = 0; i < s.length; i++) {
1849
+ if (s[i] !== "<") {
1850
+ out += s[i];
1851
+ continue;
1852
+ }
1853
+ let j = i + 1;
1854
+ let nested = false;
1855
+ while (j < s.length && s[j] !== ">") {
1856
+ if (s[j] === "<") {
1857
+ nested = true;
1858
+ break;
1859
+ }
1860
+ j++;
1861
+ }
1862
+ if (j < s.length && !nested) {
1863
+ i = j;
1864
+ continue;
1865
+ }
1866
+ out += s[i];
1867
+ }
1868
+ return out;
1869
+ }
1870
+ /**
1871
+ * M60 (#924, scala E): the HEAD TYPE NAME of a type node — never the node's source
1872
+ * text. `new ScalaRandom[F](Applicative[F].pure(new JavaSecureRandom())) with
1873
+ * SecureRandom[F] {}` is an instance_expression whose type is a compound_type: a
1874
+ * whole EXPRESSION (constructor arguments included) whose NAME is the head type
1875
+ * constructor. The generic_type path already collapsed to the head; this applies the
1876
+ * same collapse to every type shape, descending the head position (compound base →
1877
+ * applied constructor → generic type) and falling back to the old text behaviour only
1878
+ * when the shape is none of those. Measured: SecureRandomCompanionPlatform.scala:70,
1879
+ * node shape in script/m14/tmp/c924-scala-val-shape.ts.
1880
+ */
1881
+ scalaTypeHeadName(typeNode) {
1882
+ let n = typeNode;
1883
+ for (let guard = 0; n && guard < 8; guard++) {
1884
+ if (n.type === "type_identifier") return n.text;
1885
+ if (n.type === "compound_type") n = n.childForFieldName("base") ?? n.namedChild(0);
1886
+ else if (n.type === "applied_constructor_type") n = n.namedChild(0);
1887
+ else if (n.type === "generic_type") n = n.namedChild(0);
1888
+ else break;
1889
+ }
1890
+ return this.stripTypeArgs(typeNode.text);
1891
+ }
1892
+ /**
1893
+ * M60/S3: the SUPER TYPE NAME for an ANONYMOUS class — structurally walked, and
1894
+ * "" when no type name is reachable.
1895
+ *
1896
+ * This exists beside scalaTypeHeadName rather than replacing it because the two
1897
+ * answer different questions. scalaTypeHeadName names an INSTANTIATION target,
1898
+ * where the node's own text is the honest type (`new (IO ~> F) {}` really does
1899
+ * instantiate the natural-transformation type `(IO ~> F)`), and that label is
1900
+ * audited. An ANONYMOUS CLASS's name is SYNTHETIC — the analyzer's own rule,
1901
+ * stated at its emission site, is "a SYNTHETIC name must be canonical - never
1902
+ * source text" (#924) — and scalaTypeHeadName's text fallback violates it: for
1903
+ * a type shape the walk does not recognise it returns `stripTypeArgs(text)`.
1904
+ *
1905
+ * Measured on both corpora (script/m14/tmp/scala-garbage-names.ts, 400 files
1906
+ * each) — `anonymous_<source text>` symbols, whose name is no declared name in
1907
+ * their own file:
1908
+ *
1909
+ * cats-effect 20 of 291, of which 18 are a natural-transformation supertype
1910
+ * `new (IO ~> F) { ... }` -> `anonymous_(IO ~> F)` (LiftIO.scala:36,
1911
+ * Async.scala:199, Resource.scala:244/580/997/1123, pure.scala x5, ...)
1912
+ * scala3 15 of 191, of which 2 are a MULTI-LINE body published as a NAME:
1913
+ * `anonymous_:\n private var cur = Env.this\n def hasNext = ...`
1914
+ * (CheckCaptures.scala:77, CapturedVars.scala:114)
1915
+ *
1916
+ * The shapes that must still resolve, probed before this was written rather
1917
+ * than guessed (script/m14/tmp/scala-snip.ts /tmp/anon.txt):
1918
+ *
1919
+ * new Runnable {} -> instance_expression > type_identifier
1920
+ * new java.util.Comparator[String] {} -> generic_type > stable_type_identifier
1921
+ * > stable_identifier(id,id) + type_identifier
1922
+ * new Console.MapKConsole[Int] {} -> generic_type > stable_type_identifier
1923
+ * > identifier + type_identifier
1924
+ * new ScalaRandom[F](a) with Sec[F] {} -> compound_type > base >
1925
+ * applied_constructor_type > generic_type
1926
+ *
1927
+ * A qualified name is its leading identifiers joined by "." plus the final
1928
+ * type_identifier — which is what the text happened to be, now for a reason.
1929
+ */
1930
+ scalaAnonymousSupertypeName(typeNode) {
1931
+ if (!typeNode) return "";
1932
+ const walk = (n, guard) => {
1933
+ if (!n || guard > 8) return "";
1934
+ switch (n.type) {
1935
+ case "type_identifier":
1936
+ return n.text;
1937
+ case "compound_type": {
1938
+ const base = n.childForFieldName("base");
1939
+ return walk(base ?? n.namedChild(0), guard + 1);
1940
+ }
1941
+ case "applied_constructor_type":
1942
+ case "generic_type":
1943
+ return walk(n.namedChild(0), guard + 1);
1944
+ case "stable_type_identifier": {
1945
+ const segs = [];
1946
+ const collectStable = (t, guard2) => {
1947
+ if (guard2 > 8) return;
1948
+ for (let i = 0; i < t.namedChildCount; i++) {
1949
+ const c = t.namedChild(i);
1950
+ if (!c || c.type === "type_identifier") continue;
1951
+ if (c.type === "stable_identifier") collectStable(c, guard2 + 1);
1952
+ else if (c.type === "identifier") segs.push(c.text);
1953
+ }
1954
+ };
1955
+ collectStable(n, 0);
1956
+ let last = "";
1957
+ for (let i = 0; i < n.namedChildCount; i++) {
1958
+ const c = n.namedChild(i);
1959
+ if (c && c.type === "type_identifier") last = c.text;
1960
+ }
1961
+ if (!last) return "";
1962
+ return segs.length > 0 ? `${segs.join(".")}.${last}` : last;
1963
+ }
1964
+ default:
1965
+ return "";
1966
+ }
1967
+ };
1968
+ return walk(typeNode, 0);
1969
+ }
1970
+ stripTypeArgs(s) {
1971
+ return this.stripAngleGroups(this.stripBracketGroups(s)).trim();
1972
+ }
1973
+ compactType(s) {
1974
+ let out = "";
1975
+ for (let i = 0; i < s.length; i++) {
1976
+ const c = s[i];
1977
+ if (c === " " || c === " " || c === "\n" || c === "\r" || c === "\f" || c === "\v") continue;
1978
+ out += c;
1979
+ }
1980
+ let seg = "";
1981
+ let sawDot = false;
1982
+ for (let i = 0; i < out.length; i++) {
1983
+ const c = out[i];
1984
+ if (c === ".") {
1985
+ sawDot = true;
1986
+ seg = "";
1987
+ continue;
1988
+ }
1989
+ seg += c;
1990
+ }
1991
+ return sawDot ? seg : out;
1992
+ }
1993
+ /** ≡ /identifier|type_identifier|_parameter$/.test(t): unanchored "identifier" ≡ includes (type_identifier is subsumed by it). NO _type disjunct — deliberately narrower than isExtensionReceiverTypeNode. */
1994
+ isTypeishNodeName(t) {
1995
+ return t.includes("identifier") || t.endsWith("_parameter");
1996
+ }
1997
+ /** ≡ /_type$|type_identifier/.test(t): suffix or the exact node type — NOT the bare-identifier disjunct of isTypeishNodeName. */
1998
+ isExtensionReceiverTypeNode(t) {
1999
+ return t.endsWith("_type") || t.includes("type_identifier");
2000
+ }
2001
+ /** ≡ /type$|_type$/.test(t) || t === "type_identifier" || t === "stable_type_identifier" (generic_type variant adds itself). */
2002
+ isTypePositionNode(t) {
2003
+ return t.endsWith("type") || t === "type_identifier" || t === "stable_type_identifier";
2004
+ }
2005
+ /**
2006
+ * Is this `type_identifier` part of a Scala 3 PLACEHOLDER rather than a type
2007
+ * name?
2008
+ *
2009
+ * The grammar splits `-_` / `+_` / `-_*` into the operator character as a
2010
+ * `type_identifier` and the wildcard as a NEIGHBOUR TOKEN — but not always as
2011
+ * a sibling, which is why a first version of this guard (next SIBLING only)
2012
+ * suppressed 2 of the 6 corpus sites and left 4 published. Both shapes are
2013
+ * measured, on `scala/wildcard-is-not-a-name` line 12 and on the two corpus
2014
+ * files:
2015
+ *
2016
+ * vararg: -_* infix_type -> type_identifier "-" + identifier "_" + type_identifier "*"
2017
+ * byname: => -_ lazy_parameter_type -> type_identifier "-", and the `_`
2018
+ * lands as an ERROR SIBLING OF THE PARAMETER, one level up
2019
+ *
2020
+ * So the test is on the ADJACENT TOKEN in source order, found by walking to
2021
+ * the parent for as long as the node is the last child. A placeholder's
2022
+ * `_` is what follows the operator and what precedes the `*`, so the test is
2023
+ * "the next token is a wildcard, or the previous token is `_`".
2024
+ *
2025
+ * What this deliberately does NOT decide: a `?` wildcard (`def f(x: List[?])`)
2026
+ * parses as a BARE `type_identifier "?"` with no wildcard neighbour, so it is
2027
+ * indistinguishable here from the legal Scala 3 type name `?` (`type ?[A]`,
2028
+ * test/files/neg/qmark-deprecated.scala). Deciding it needs the file's
2029
+ * declared-type table, which this collector does not hold; it is recorded as
2030
+ * an open item in the lane doc rather than guessed at.
2031
+ */
2032
+ isScalaPlaceholderFragment(n) {
2033
+ if (n.type !== "type_identifier") return false;
2034
+ let cur = n;
2035
+ while (cur) {
2036
+ const p = cur.parent;
2037
+ if (!p) break;
2038
+ let sawNext = false;
2039
+ for (let i = 0; i < p.childCount; i++) {
2040
+ const c = p.child(i);
2041
+ if (!c) continue;
2042
+ if (c.id === cur.id) {
2043
+ const next = p.child(i + 1);
2044
+ if (next) {
2045
+ sawNext = true;
2046
+ if (next.text === "_" || next.text === "?") return true;
2047
+ }
2048
+ break;
2049
+ }
2050
+ }
2051
+ if (sawNext) break;
2052
+ cur = p;
2053
+ }
2054
+ cur = n;
2055
+ while (cur) {
2056
+ const p = cur.parent;
2057
+ if (!p) break;
2058
+ let sawPrev = false;
2059
+ for (let i = p.childCount - 1; i >= 0; i--) {
2060
+ const c = p.child(i);
2061
+ if (!c) continue;
2062
+ if (c.id === cur.id) {
2063
+ const prev = p.child(i - 1);
2064
+ if (prev) {
2065
+ sawPrev = true;
2066
+ if (prev.text === "_") return true;
2067
+ }
2068
+ break;
2069
+ }
2070
+ }
2071
+ if (sawPrev) break;
2072
+ cur = p;
2073
+ }
2074
+ return false;
2075
+ }
2076
+ /**
2077
+ * THE SELF-TYPE'S REQUIRED TYPE — the names a self type REQUIRES.
2078
+ *
2079
+ * ★ WHY THIS IS A DEDICATED READER AND NOT `collectTypeNames`. Two shapes need a decision the
2080
+ * generic reader does not make, and both are MEASURED over the admitted population
2081
+ * (validation/m14/measurements/scala-construct-census.json, 1,017 self_type sites in 636 files):
2082
+ *
2083
+ * 1. `singleton_type` — `this: IO.type =>` — spells its target as an `identifier` child
2084
+ * (`N:identifier A:. A:type`), and the generic reader only pushes `type_identifier` and
2085
+ * `stable_type_identifier`, so it would publish NOTHING for the 154 singleton sites.
2086
+ * The target of a singleton type is the MODULE's name, and that is the name the row needs.
2087
+ * 2. `type_arguments` — `this: xs.Cap[A] =>` — the `[A]` is a type PARAMETER of the required
2088
+ * type, NOT a second conformance requirement. The generic reader recurses into it and
2089
+ * would publish `A` as a conformance target, which is a fabrication: `A` is bound by the
2090
+ * required type, not required BY the declaring type.
2091
+ *
2092
+ * ★ AND THE POSITION IS FOUND BY THE ":" TOKEN, NOT BY INDEX. The grammar spells a self type
2093
+ * `<self-name> [":" <type>] "=>"`, and a trailing block comment is a NAMED child too — the
2094
+ * measured shape `identifier | block_comment | : | type_identifier | block_comment | =>`
2095
+ * would make "the second named child" the comment.
2096
+ *
2097
+ * ★ AND A `self =>` WITH NO ":" IS NOT A CONFORMANCE. It binds a self NAME and requires
2098
+ * nothing: the measured shapes are 344 `identifier =>` and 2 `wildcard =>` against 671 that
2099
+ * carry a type, so this reader returns nothing for them and the `implements` row is not
2100
+ * published — a self name is not a conformance requirement.
2101
+ *
2102
+ * ★ AND A `structural_type` REQUIRED TYPE NAMES NO CONFORMANCE TARGET: `this: { def f: Int } =>`
2103
+ * (4 sites) describes a shape, and the members inside it are not types the declaring type
2104
+ * conforms to. It is refused rather than mined for a name.
2105
+ */
2106
+ selfTypeRequiredTypeNames(st) {
2107
+ const out = [];
2108
+ let colon = -1;
2109
+ for (let i = 0; i < st.childCount; i++) {
2110
+ const c = st.child(i);
2111
+ if (c && !c.isNamed && c.type === ":") {
2112
+ colon = i;
2113
+ break;
2114
+ }
2115
+ }
2116
+ if (colon < 0) return out;
2117
+ let req = null;
2118
+ for (let i = colon + 1; i < st.childCount; i++) {
2119
+ const c = st.child(i);
2120
+ if (!c || !c.isNamed) continue;
2121
+ if (c.type === "block_comment" || c.type === "comment") continue;
2122
+ req = c;
2123
+ break;
2124
+ }
2125
+ if (!req) return out;
2126
+ if (req.type === "structural_type") return out;
2127
+ if (req.type === "singleton_type") {
2128
+ for (let i = 0; i < req.namedChildCount; i++) {
2129
+ const c = req.namedChild(i);
2130
+ if (c && (c.type === "identifier" || c.type === "stable_identifier" || c.type === "stable_type_identifier")) {
2131
+ const nm = stripScalaQuoting(c.text);
2132
+ if (nm) out.push({ name: nm, line: c.startPosition.row + 1 });
2133
+ return out;
2134
+ }
2135
+ }
2136
+ return out;
2137
+ }
2138
+ const seen = /* @__PURE__ */ new Set();
2139
+ const walk = (n) => {
2140
+ if (n.type === "type_arguments" || n.type === "wildcard") return;
2141
+ if (n.type === "type_identifier" || n.type === "stable_type_identifier") {
2142
+ const nm = stripScalaQuoting(n.text);
2143
+ if (nm && !seen.has(nm)) {
2144
+ seen.add(nm);
2145
+ out.push({ name: nm, line: n.startPosition.row + 1 });
2146
+ }
2147
+ return;
2148
+ }
2149
+ for (let i = 0; i < n.namedChildCount; i++) {
2150
+ const c = n.namedChild(i);
2151
+ if (c) walk(c);
2152
+ }
2153
+ };
2154
+ walk(req);
2155
+ return out;
2156
+ }
2157
+ /** Extract supertypes from an extends_clause (direct type children in v0.26.2). */
2158
+ /**
2159
+ * M59 (#825): each supertype name is paired with the LINE IT IS WRITTEN ON.
2160
+ *
2161
+ * Scala writes the parents of a long inheritance list one per line:
2162
+ *
2163
+ * trait IterableLike[+A]
2164
+ * extends FromIterable ← 106
2165
+ * with IterableOps[A] ← 107
2166
+ * with IterablePolyTransforms[A] ← 108
2167
+ *
2168
+ * Stamping the clause's start line put IterableOps/PolyTransforms on 106, a
2169
+ * line they are not written on — 8 of 8 remaining scala oracle failures at the
2170
+ * 300-file scale, including the `trait T1 extends { ... } with Base` form where
2171
+ * the parent appears AFTER an early-init block. Same defect family as the call
2172
+ * site (`#825` half one) and the declared-type reference: the line belongs to
2173
+ * the NAME, never to the construct containing it.
2174
+ */
2175
+ extractSupertypes(extendsClause) {
2176
+ const out = [];
2177
+ const push = (name, n) => {
2178
+ if (!name) return;
2179
+ if (out.some((x) => x.name === name)) return;
2180
+ out.push({ name, line: n.startPosition.row + 1 });
2181
+ };
2182
+ for (let i = 0; i < extendsClause.childCount; i++) {
2183
+ const c = extendsClause.child(i);
2184
+ if (!c || !c.isNamed) continue;
2185
+ if (c.type === "type_identifier" || c.type === "identifier") {
2186
+ push(c.text, c);
2187
+ } else if (c.type === "stable_type_identifier") {
2188
+ push(c.text, c);
2189
+ } else if (c.type === "generic_type") {
2190
+ let pushed = false;
2191
+ for (let j = 0; j < c.childCount; j++) {
2192
+ const ch = c.child(j);
2193
+ if (ch && ch.type === "stable_type_identifier") {
2194
+ push(ch.text, ch);
2195
+ pushed = true;
2196
+ break;
2197
+ }
2198
+ }
2199
+ if (!pushed) {
2200
+ for (let j = 0; j < c.childCount; j++) {
2201
+ const ch = c.child(j);
2202
+ if (ch && ch.type === "type_identifier") {
2203
+ push(ch.text, ch);
2204
+ break;
2205
+ }
2206
+ }
2207
+ }
2208
+ } else if (c.type === "compound_type") {
2209
+ for (let j = 0; j < c.namedChildCount; j++) {
2210
+ const ch = c.namedChild(j);
2211
+ if (ch && (ch.type === "type_identifier" || ch.type === "generic_type" || ch.type === "annotated_type")) {
2212
+ if (ch.type === "type_identifier") push(ch.text, ch);
2213
+ else if (ch.type === "annotated_type") {
2214
+ const base = this.annotatedTypeBase(ch);
2215
+ if (base) push(base.name, base.node);
2216
+ } else {
2217
+ const inner = this.extractSupertypes(ch);
2218
+ if (inner.length > 0) push(inner[0].name, ch);
2219
+ else push(ch.text, ch);
2220
+ }
2221
+ }
2222
+ }
2223
+ } else if (c.type === "annotated_type") {
2224
+ const base = this.annotatedTypeBase(c);
2225
+ if (base) push(base.name, base.node);
2226
+ }
2227
+ }
2228
+ return out;
2229
+ }
2230
+ /**
2231
+ * ★ THE BASE TYPE UNDER AN ANNOTATION. `annotated_type` holds the TYPE and then the `annotation`,
2232
+ * so the base is its first named child — and it is the base that names the supertype.
2233
+ *
2234
+ * The shape is measured from the pinned build, not assumed: `annotated_type` ->
2235
+ * (`type_identifier` | `generic_type` | `stable_type_identifier`) + `annotation`.
2236
+ */
2237
+ // SCALA-ANN-SUPERTYPE-BLOCK-C:BEGIN
2238
+ annotatedTypeBase(n) {
2239
+ for (let j = 0; j < n.namedChildCount; j++) {
2240
+ const ch = n.namedChild(j);
2241
+ if (!ch) continue;
2242
+ if (ch.type === "type_identifier" || ch.type === "identifier" || ch.type === "stable_type_identifier") {
2243
+ return { name: ch.text, node: ch };
2244
+ }
2245
+ if (ch.type === "generic_type") {
2246
+ for (let k = 0; k < ch.childCount; k++) {
2247
+ const g = ch.child(k);
2248
+ if (g && g.type === "stable_type_identifier") return { name: g.text, node: g };
2249
+ }
2250
+ for (let k = 0; k < ch.childCount; k++) {
2251
+ const g = ch.child(k);
2252
+ if (g && g.type === "type_identifier") return { name: g.text, node: g };
2253
+ }
2254
+ return null;
2255
+ }
2256
+ }
2257
+ return null;
2258
+ }
2259
+ // SCALA-ANN-SUPERTYPE-BLOCK-C:END
2260
+ handleClass(node, symbols, relationships, exports, patternsObj, parent) {
2261
+ const name = this.extractIdentifier(node, "name");
2262
+ if (!name) return null;
2263
+ const mods = this.collectModifiers(node);
2264
+ const isCase = this.isCaseClass(node);
2265
+ const isSealed = mods.has("sealed");
2266
+ const isAbstract = mods.has("abstract");
2267
+ const isFinal = mods.has("final");
2268
+ const isImplicit = mods.has("implicit");
2269
+ const classParamsNode = this.findChild(node, "class_parameters");
2270
+ const constructorParams = [];
2271
+ if (classParamsNode) {
2272
+ const ownerName = stripScalaQuoting(name);
2273
+ const idStub = { id: ownerName, name: ownerName };
2274
+ const ctor = this.createSymbol("constructor", ASTEngine.SymbolKind.Constructor, classParamsNode, idStub, {
2275
+ type: "constructor",
2276
+ ownerName,
2277
+ ownerKind: ASTEngine.SymbolKind.Class,
2278
+ scalaSpecific: true
2279
+ });
2280
+ ctor.ownerName = ownerName;
2281
+ const ctorParams = [];
2282
+ symbols.push(ctor);
2283
+ for (let i = 0; i < classParamsNode.namedChildCount; i++) {
2284
+ const cp = classParamsNode.namedChild(i);
2285
+ if (cp?.type !== "class_parameter") continue;
2286
+ const pName = cp.childForFieldName("name")?.text;
2287
+ if (!pName) continue;
2288
+ constructorParams.push(pName);
2289
+ const paramType = cp.childForFieldName("type")?.text;
2290
+ if (paramType) ctorParams.push({ name: pName, type: paramType, isImplicit: false });
2291
+ const pMods = this.collectModifiers(cp);
2292
+ const isVal = pMods.has("val");
2293
+ const isVar = pMods.has("var");
2294
+ if (isVal || isVar || isCase) {
2295
+ const prop = this.createSymbol(pName, ASTEngine.SymbolKind.Property, cp, idStub, {
2296
+ type: isVar ? "var" : "val",
2297
+ isConstructorProperty: true,
2298
+ ownerName,
2299
+ ownerKind: ASTEngine.SymbolKind.Class,
2300
+ isCaseClassParam: isCase
2301
+ });
2302
+ prop.ownerName = ownerName;
2303
+ symbols.push(prop);
2304
+ }
2305
+ }
2306
+ if (ctorParams.length > 0) ctor.parameters = ctorParams;
2307
+ }
2308
+ if (isSealed) patternsObj["Sealed Types"] = true;
2309
+ const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Class, node, parent, {
2310
+ type: "class",
2311
+ isObject: false,
2312
+ isCaseClass: isCase,
2313
+ isSealed,
2314
+ isAbstract,
2315
+ isFinal,
2316
+ isImplicit,
2317
+ constructorParams: constructorParams.length > 0 ? constructorParams : void 0
2318
+ });
2319
+ symbols.push(symbol);
2320
+ if (isImplicit) patternsObj["Implicit Classes (Extension Methods)"] = true;
2321
+ if (this.isPublic(node) && !parent) {
2322
+ exports.push({
2323
+ // `symbol.name`, not the raw `name`: createSymbol strips quoting syntax,
2324
+ // so the array used to publish `` `strip margin` `` where the symbol was
2325
+ // `strip margin` — one public surface with two spellings of every
2326
+ // backticked name in it. Measured on scala/scala (package.scala:40,
2327
+ // `implicit val `strip margin``), and the `exports` RELATIONSHIP derives
2328
+ // from this array, so the two spellings would have met in the graph.
2329
+ name: symbol.name,
2330
+ kind: ASTEngine.SymbolKind.Class,
2331
+ range: symbol.range
2332
+ });
2333
+ }
2334
+ const typeParams = this.extractScalaTypeParameters(node);
2335
+ if (typeParams.length > 0) {
2336
+ symbol.genericParameters = typeParams;
2337
+ symbol.metadata.typeParameters = typeParams;
2338
+ }
2339
+ const extendsClause = this.findChild(node, "extends_clause");
2340
+ if (extendsClause) {
2341
+ const supertypes = this.extractSupertypes(extendsClause);
2342
+ symbol.metadata.superTypes = supertypes.map((s) => s.name);
2343
+ for (let i = 0; i < supertypes.length; i++) {
2344
+ relationships.push({
2345
+ source: symbol.name,
2346
+ target: supertypes[i].name,
2347
+ // FLEET MIXIN RULING (2026-10-19, Lead): position 0 is the single
2348
+ // supertype `extends`; positions >=1 are MIXIN COMPOSITION and publish
2349
+ // `mixes_in`, the canonical value for `class A with M` / Ruby
2350
+ // `include/extend/prepend` / Dart `with`. They are NOT `implements`
2351
+ // conformance (the implements value is ruled NOT-APPLICABLE for scala;
2352
+ // the false-negative-if-conformance-without-composition falsifier is in
2353
+ // the lane's SCALA-CENSUS.md). This shape is the same for the
2354
+ // object handler below.
2355
+ type: i === 0 ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.MixesIn,
2356
+ // S1.4: explicit structural confidence (1.0) — undefined is
2357
+ // classifier-defaulted today but downstream >= 0.7 filters read
2358
+ // the field directly.
2359
+ confidence: 1,
2360
+ // M59 (#825): the parent's OWN line, not the clause's.
2361
+ line_number: supertypes[i].line,
2362
+ metadata: { isTrait: i > 0 }
2363
+ });
2364
+ }
2365
+ }
2366
+ return symbol;
2367
+ }
2368
+ handleObject(node, symbols, relationships, exports, patternsObj, parent) {
2369
+ const name = this.extractIdentifier(node, "name");
2370
+ if (!name) return null;
2371
+ const mods = this.collectModifiers(node);
2372
+ const isCase = this.isCaseClass(node);
2373
+ const isSealed = mods.has("sealed");
2374
+ const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Module, node, parent, {
2375
+ type: "object",
2376
+ isObject: true,
2377
+ isCaseObject: isCase,
2378
+ isSealed,
2379
+ scalaSpecific: true,
2380
+ isStatic: true
2381
+ });
2382
+ symbol.isStatic = true;
2383
+ symbols.push(symbol);
2384
+ if (this.isPublic(node) && !parent && !exports.some((e) => e.name === symbol.name)) {
2385
+ exports.push({
2386
+ name: symbol.name,
2387
+ kind: ASTEngine.SymbolKind.Module,
2388
+ range: symbol.range
2389
+ });
2390
+ }
2391
+ const typeParams = this.extractScalaTypeParameters(node);
2392
+ if (typeParams.length > 0) {
2393
+ symbol.genericParameters = typeParams;
2394
+ symbol.metadata.typeParameters = typeParams;
2395
+ }
2396
+ const extendsClause = this.findChild(node, "extends_clause");
2397
+ if (extendsClause) {
2398
+ const supertypes = this.extractSupertypes(extendsClause);
2399
+ symbol.metadata.superTypes = supertypes.map((s) => s.name);
2400
+ for (let i = 0; i < supertypes.length; i++) {
2401
+ relationships.push({
2402
+ source: symbol.name,
2403
+ target: supertypes[i].name,
2404
+ // FLEET MIXIN RULING (2026-10-19, Lead): position 0 is the single
2405
+ // supertype `extends`; positions >=1 are MIXIN COMPOSITION and publish
2406
+ // `mixes_in`, the canonical value for `class A with M` / Ruby
2407
+ // `include/extend/prepend` / Dart `with`. They are NOT `implements`
2408
+ // conformance (the implements value is ruled NOT-APPLICABLE for scala;
2409
+ // the false-negative-if-conformance-without-composition falsifier is in
2410
+ // the lane's SCALA-CENSUS.md). This shape is the same for the
2411
+ // object handler below.
2412
+ type: i === 0 ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.MixesIn,
2413
+ confidence: 1,
2414
+ line_number: extendsClause.startPosition.row + 1,
2415
+ metadata: { isTrait: i > 0 }
2416
+ });
2417
+ }
2418
+ }
2419
+ return symbol;
2420
+ }
2421
+ handleTrait(node, symbols, relationships, exports, patternsObj, parent) {
2422
+ const name = this.extractIdentifier(node, "name");
2423
+ if (!name) return null;
2424
+ const mods = this.collectModifiers(node);
2425
+ const isSealed = mods.has("sealed");
2426
+ if (isSealed) patternsObj["Sealed Types"] = true;
2427
+ const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Interface, node, parent, {
2428
+ type: "trait",
2429
+ isTrait: true,
2430
+ scalaSpecific: true,
2431
+ isSealed
2432
+ });
2433
+ symbols.push(symbol);
2434
+ if (this.isPublic(node) && !parent) {
2435
+ exports.push({
2436
+ name: symbol.name,
2437
+ kind: ASTEngine.SymbolKind.Interface,
2438
+ range: symbol.range
2439
+ });
2440
+ }
2441
+ const typeParams = this.extractScalaTypeParameters(node);
2442
+ if (typeParams.length > 0) {
2443
+ symbol.genericParameters = typeParams;
2444
+ symbol.metadata.typeParameters = typeParams;
2445
+ }
2446
+ const extendsClause = this.findChild(node, "extends_clause");
2447
+ if (extendsClause) {
2448
+ const supertypes = this.extractSupertypes(extendsClause);
2449
+ symbol.metadata.superTypes = supertypes.map((s) => s.name);
2450
+ for (let i = 0; i < supertypes.length; i++) {
2451
+ relationships.push({
2452
+ source: symbol.name,
2453
+ target: supertypes[i].name,
2454
+ // FLEET MIXIN RULING (2026-10-19): a trait's own supertype list is
2455
+ // the same composition construct — position 0 stays `extends`, every
2456
+ // further trait in the list is `mixes_in` (it is not interface
2457
+ // conformance; implements is ruled NOT-APPLICABLE for scala).
2458
+ type: i === 0 ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.MixesIn,
2459
+ confidence: 1,
2460
+ // M59 (#825): the parent's OWN line, not the clause's.
2461
+ line_number: supertypes[i].line,
2462
+ metadata: { isTrait: i > 0 }
2463
+ });
2464
+ }
2465
+ }
2466
+ return symbol;
2467
+ }
2468
+ handleEnum(node, symbols, relationships, exports, patternsObj, parent) {
2469
+ const name = this.extractIdentifier(node, "name");
2470
+ if (!name) return null;
2471
+ const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Enum, node, parent, { type: "enum", scala3: true });
2472
+ symbols.push(symbol);
2473
+ patternsObj["Scala 3 Enums"] = true;
2474
+ if (this.isPublic(node) && !parent) {
2475
+ exports.push({ name: symbol.name, kind: ASTEngine.SymbolKind.Enum, range: symbol.range });
2476
+ }
2477
+ const enumExtends = this.findChild(node, "extends_clause");
2478
+ if (enumExtends) {
2479
+ const supertypes = this.extractSupertypes(enumExtends);
2480
+ symbol.metadata.superTypes = supertypes.map((s) => s.name);
2481
+ for (let i = 0; i < supertypes.length; i++) {
2482
+ const st = supertypes[i];
2483
+ relationships.push({
2484
+ source: name,
2485
+ target: st.name,
2486
+ // FLEET MIXIN RULING (2026-10-19): same extends_clause composition
2487
+ // construct — first supertype extends, further ones mixes_in.
2488
+ type: i === 0 ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.MixesIn,
2489
+ confidence: 1,
2490
+ // M59 (#825): the parent's OWN line, not the clause's.
2491
+ line_number: st.line,
2492
+ metadata: { isTrait: true }
2493
+ });
2494
+ }
2495
+ }
2496
+ const enumParamsNode = this.findChild(node, "class_parameters");
2497
+ if (enumParamsNode) {
2498
+ const ownerName = stripScalaQuoting(name);
2499
+ const idStub = { id: ownerName, name: ownerName };
2500
+ const ctor = this.createSymbol("constructor", ASTEngine.SymbolKind.Constructor, enumParamsNode, idStub, {
2501
+ type: "constructor",
2502
+ ownerName,
2503
+ ownerKind: ASTEngine.SymbolKind.Enum,
2504
+ scalaSpecific: true
2505
+ });
2506
+ ctor.ownerName = ownerName;
2507
+ const ctorParams = [];
2508
+ symbols.push(ctor);
2509
+ for (let i = 0; i < enumParamsNode.namedChildCount; i++) {
2510
+ const cp = enumParamsNode.namedChild(i);
2511
+ if (cp?.type !== "class_parameter") continue;
2512
+ const pName = cp.childForFieldName("name")?.text;
2513
+ if (!pName) continue;
2514
+ const paramType = cp.childForFieldName("type")?.text;
2515
+ if (paramType) ctorParams.push({ name: pName, type: paramType, isImplicit: false });
2516
+ const pMods = this.collectModifiers(cp);
2517
+ const isVal = pMods.has("val");
2518
+ const isVar = pMods.has("var");
2519
+ if (isVal || isVar) {
2520
+ const prop = this.createSymbol(pName, ASTEngine.SymbolKind.Property, cp, idStub, {
2521
+ type: isVar ? "var" : "val",
2522
+ isConstructorProperty: true,
2523
+ ownerName,
2524
+ ownerKind: ASTEngine.SymbolKind.Enum
2525
+ });
2526
+ prop.ownerName = ownerName;
2527
+ symbols.push(prop);
2528
+ }
2529
+ }
2530
+ if (ctorParams.length > 0) ctor.parameters = ctorParams;
2531
+ }
2532
+ const walkEnumBody = (n) => {
2533
+ for (let i = 0; i < n.namedChildCount; i++) {
2534
+ const c = n.namedChild(i);
2535
+ if (!c) continue;
2536
+ if (c.type === "simple_enum_case" || c.type === "full_enum_case") {
2537
+ const caseName = this.findChildIdentifier(c);
2538
+ if (caseName) {
2539
+ const member = this.createSymbol(caseName, ASTEngine.SymbolKind.EnumMember, c, symbol, {
2540
+ type: "enum_case",
2541
+ // stripped, like every other ownerName (see handleClass): a case
2542
+ // declared ``case `type` `` must not source its `contains` edge
2543
+ // from a spelling the symbol index does not hold.
2544
+ ownerName: stripScalaQuoting(name),
2545
+ ownerKind: ASTEngine.SymbolKind.Enum,
2546
+ scala3: true
2547
+ });
2548
+ member.ownerName = stripScalaQuoting(name);
2549
+ symbols.push(member);
2550
+ }
2551
+ } else {
2552
+ walkEnumBody(c);
2553
+ }
2554
+ }
2555
+ };
2556
+ const enumBody = this.findChild(node, "enum_body");
2557
+ if (enumBody) walkEnumBody(enumBody);
2558
+ return symbol;
2559
+ }
2560
+ handleTypeAlias(node, symbols, parent, exports) {
2561
+ const name = this.extractIdentifier(node, "name");
2562
+ if (!name) return;
2563
+ const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Type, node, parent, { isTypeAlias: true });
2564
+ symbols.push(symbol);
2565
+ if (this.isPublic(node) && !parent) exports.push({ name, kind: ASTEngine.SymbolKind.Type, range: symbol.range });
2566
+ }
2567
+ // ───────────────────────────────────────────────────────────────────────
2568
+ // Scala 3 EXPORT CLAUSE -> `exports` relationships.
2569
+ //
2570
+ // The lane handover recorded `exports` as silent and the census measured the
2571
+ // construct present: `export_declaration` — 277 nodes in 158 files of the
2572
+ // 18,965-file population (script/m14/tmp/scala3-ontology-census.ts), e.g.
2573
+ // compiler/src/dotty/tools/dotc/core/Flags.scala, library/src/scala/
2574
+ // NamedTuple.scala. So this is a GAP, not an absence, and a prose ruling
2575
+ // ("Scala has no export construct") would have been wrong on 277 measured
2576
+ // nodes — the ontology gate's founding premise.
2577
+ //
2578
+ // THE RECONSTRUCTION, and why child position cannot do it. The natural first
2579
+ // rule — "everything before the last named child is the qualifier, the last
2580
+ // named child is the selection" — is wrong on real corpus shapes, measured in
2581
+ // script/m14/tmp/scala3-export-reconstruct-census.mts:
2582
+ //
2583
+ // * `export toppingPrice.apply, crustPrice.apply, crustPrice.unlift`
2584
+ // (tests/neg/i20511-1.scala) parses with SIX `field: path` children and
2585
+ // NO selection node at all. The positional rule reads the qualifier as
2586
+ // "toppingPrice.apply.crustPrice.apply.crustPrice" — three unrelated
2587
+ // exportees fused into one nonsense path. The FIELD is what separates
2588
+ // them: a `,` terminates the exportee and the path children after it
2589
+ // begin the next one.
2590
+ // * the anonymous `.` between path segments does NOT consistently carry
2591
+ // `field: path` (it does between two path segments, it does NOT before a
2592
+ // selection node), so it has to be skipped by node type, not by field.
2593
+ //
2594
+ // With that, the rule reconstructs 277 of 277 declarations and produces 280
2595
+ // exportees. The first version of the rule reconstructed 90 of 277; the
2596
+ // 187 it missed were all wildcards and selector lists, which is why the
2597
+ // coverage number is printed rather than asserted.
2598
+ //
2599
+ // WHAT AN EDGE IS. A Scala 3 export clause re-publishes members of another
2600
+ // object under the enclosing declaration's own name. So the edge reads
2601
+ // <enclosing declaration> exports <exported name>
2602
+ // which is the same shape makefile/python/zig emit (module -> name) and the
2603
+ // shape parallel-indexer's file-level `exports` section expects. The
2604
+ // QUALIFIER is carried in metadata rather than glued into the target: it is
2605
+ // the SOURCE path of the member, not part of the name the export publishes,
2606
+ // and gluing it would produce targets no symbol in the graph is named by.
2607
+ //
2608
+ // CONFIDENCE. 1.0 on a NAMED exportee: the name is written in the clause, so
2609
+ // there is nothing inferred. A wildcard exportee (`export X._`, 133 of the
2610
+ // 277 declarations) names no member, so an edge naming one would be
2611
+ // fabricated; it is published at 0.7 with `reasonCode: "export-wildcard"` and
2612
+ // an `isWildcard` flag, which makes it countable as a heuristic per
2613
+ // M14-HONESTY-CONTRACT §4 rather than silently indistinguishable from the
2614
+ // named ones.
2615
+ //
2616
+ // ENDPOINT RESOLUTION. The source is the enclosing declaration's name, which
2617
+ // is a symbol the same walk publishes, so it resolves. The target is a member
2618
+ // name that is usually NOT a symbol in this file (it lives on the qualifier's
2619
+ // object), so it resolves externally or not at all — which is the same
2620
+ // position every `references` import edge is already in, and is why the
2621
+ // qualifier goes in metadata where a consumer can still find it.
2622
+ handleExportClause(node, relationships, parent) {
2623
+ const SELECTION_TYPES = /* @__PURE__ */ new Set(["namespace_selectors", "namespace_wildcard", "as_renamed_identifier"]);
2624
+ const exportees = [];
2625
+ let pathParts = [];
2626
+ let pathNodes = [];
2627
+ for (let i = 0; i < node.childCount; i++) {
2628
+ const c = node.child(i);
2629
+ if (!c) return;
2630
+ const t = c.type;
2631
+ const field = node.fieldNameForChild(i) ?? "";
2632
+ if (t === "export") continue;
2633
+ if (t === ",") {
2634
+ if (pathParts.length === 0) continue;
2635
+ const leaf = pathParts.pop();
2636
+ const leafNode = pathNodes.pop() ?? c;
2637
+ exportees.push({ prefix: pathParts, selKind: "identifier", src: leafNode, names: [leaf] });
2638
+ pathParts = [];
2639
+ pathNodes = [];
2640
+ continue;
2641
+ }
2642
+ if (t === ".") continue;
2643
+ if (c.isNamed && t === "block_comment") continue;
2644
+ if (c.isNamed && SELECTION_TYPES.has(t)) {
2645
+ if (field !== "") continue;
2646
+ if (t === "namespace_wildcard") {
2647
+ exportees.push({ prefix: pathParts.slice(), selKind: t, src: c, names: [] });
2648
+ } else if (t === "namespace_selectors") {
2649
+ const picked = [];
2650
+ let innerWildcard = false;
2651
+ for (let j = 0; j < c.namedChildCount; j++) {
2652
+ const s = c.namedChild(j);
2653
+ if (!s || s.isNamed && s.type === "block_comment") continue;
2654
+ if (s.type === "namespace_wildcard") {
2655
+ innerWildcard = true;
2656
+ continue;
2657
+ }
2658
+ const nm = this.exportSelectorName(s);
2659
+ if (nm === null) continue;
2660
+ const alias = this.exportSelectorAlias(s);
2661
+ picked.push({ name: nm, node: s, ...alias && alias !== "*" ? { exportedAs: alias } : {} });
2662
+ }
2663
+ for (const pk of picked) {
2664
+ exportees.push({ prefix: pathParts.slice(), selKind: t, src: pk.node, names: [pk.name], ...pk.exportedAs ? { exportedAs: pk.exportedAs } : {} });
2665
+ }
2666
+ if (innerWildcard && pathParts.length > 0) {
2667
+ exportees.push({ prefix: pathParts.slice(), selKind: "namespace_wildcard", src: c, names: [] });
2668
+ }
2669
+ } else {
2670
+ const srcNode = c.namedChild(0);
2671
+ const srcName = srcNode ? this.exportSelectorName(srcNode) : null;
2672
+ if (srcName !== null) {
2673
+ const alias = this.exportSelectorAlias(c);
2674
+ exportees.push({ prefix: pathParts.slice(), selKind: t, src: srcNode ?? c, names: [srcName], ...alias && alias !== "*" ? { exportedAs: alias } : {} });
2675
+ }
2676
+ }
2677
+ pathParts = [];
2678
+ pathNodes = [];
2679
+ continue;
2680
+ }
2681
+ if (field === "path") {
2682
+ if (c.isNamed) {
2683
+ pathParts.push(c.text);
2684
+ pathNodes.push(c);
2685
+ }
2686
+ continue;
2687
+ }
2688
+ return;
2689
+ }
2690
+ if (pathParts.length > 0) {
2691
+ const leaf = pathParts.pop();
2692
+ const leafNode = pathNodes.pop() ?? node;
2693
+ exportees.push({ prefix: pathParts, selKind: "identifier", src: leafNode, names: [leaf] });
2694
+ }
2695
+ if (exportees.length === 0) return;
2696
+ const source = parent?.name ?? _RealScalaAnalyzer.FILE_MODULE;
2697
+ const seen = /* @__PURE__ */ new Set();
2698
+ for (const e of exportees) {
2699
+ const qualifier = e.prefix.join(".");
2700
+ if (e.names.length === 0) {
2701
+ if (qualifier.length === 0) continue;
2702
+ const key = `${source}|${qualifier}|*`;
2703
+ if (seen.has(key)) continue;
2704
+ seen.add(key);
2705
+ relationships.push({
2706
+ source,
2707
+ target: qualifier,
2708
+ type: ASTEngine.RelationshipType.Exports,
2709
+ confidence: 0.7,
2710
+ line_number: e.src.startPosition.row + 1,
2711
+ metadata: { isExportClause: true, isWildcard: true, reasonCode: "export-wildcard", exportKind: "all_members", evidenceClass: "structural" }
2712
+ });
2713
+ continue;
2714
+ }
2715
+ for (const nm of e.names) {
2716
+ const published = e.exportedAs ?? nm;
2717
+ const target = stripScalaQuoting(published);
2718
+ if (target.length === 0 || target === "throws") continue;
2719
+ if (target === "_" || target === "*" || target === "unknown" || target === "?") continue;
2720
+ const key = `${source}|${target}`;
2721
+ if (seen.has(key)) continue;
2722
+ seen.add(key);
2723
+ relationships.push({
2724
+ source,
2725
+ target,
2726
+ type: ASTEngine.RelationshipType.Exports,
2727
+ confidence: 1,
2728
+ // M59 (#825): the exported name is written in the clause, so the line
2729
+ // is where that NAME is written — the selector's own line inside a
2730
+ // selector list, the leaf segment's own line for `export a.b`, and
2731
+ // never the declaration's first line.
2732
+ line_number: e.src.startPosition.row + 1,
2733
+ metadata: {
2734
+ isExportClause: true,
2735
+ exportKind: e.exportedAs ? "renamed" : "named",
2736
+ ...qualifier.length > 0 ? { exportFrom: qualifier } : {},
2737
+ ...e.exportedAs ? { exportedAs: stripScalaQuoting(e.exportedAs) } : {},
2738
+ evidenceClass: "structural"
2739
+ }
2740
+ });
2741
+ }
2742
+ }
2743
+ }
2744
+ /** The RENAMING alias one selector publishes, or null when it does not
2745
+ * rename. `x as y` / `x => y` publish `y`; `x as _` / `x => _` publish `x`,
2746
+ * which is why `_` and `*` are never returned as an alias. */
2747
+ exportSelectorAlias(sel) {
2748
+ const t = sel.type;
2749
+ if (t !== "as_renamed_identifier" && t !== "arrow_renamed_identifier") return null;
2750
+ if (sel.namedChildCount < 2) return null;
2751
+ const last = sel.namedChild(sel.namedChildCount - 1);
2752
+ if (!last) return null;
2753
+ const txt = last.text;
2754
+ if (txt === "_" || txt === "*") return null;
2755
+ return this.exportSelectorName(last);
2756
+ }
2757
+ /** The member name ONE selector contributes, or null when it contributes
2758
+ * none. `as`/`=>` forms contribute their SOURCE name; the alias is read
2759
+ * separately so the caller can record the rename. */
2760
+ exportSelectorName(sel) {
2761
+ const t = sel.type;
2762
+ if (t === "identifier" || t === "operator_identifier" || t === "type_identifier") return sel.text;
2763
+ if (t === "generic_type") {
2764
+ const head = sel.namedChild(0);
2765
+ return head ? this.exportSelectorName(head) : null;
2766
+ }
2767
+ if (t === "as_renamed_identifier" || t === "arrow_renamed_identifier") {
2768
+ const src = sel.namedChild(0);
2769
+ return src ? this.exportSelectorName(src) : null;
2770
+ }
2771
+ if (t === "namespace_wildcard") return "*";
2772
+ return null;
2773
+ }
2774
+ handleGiven(node, symbols, relationships, exports, patternsObj, parent) {
2775
+ patternsObj["Given/Using (Scala 3)"] = true;
2776
+ patternsObj["Contextual Abstractions"] = true;
2777
+ const explicitName = node.childForFieldName("name")?.text ?? null;
2778
+ if (!explicitName) this.givenCounter += 1;
2779
+ const name = explicitName || `given_${this.givenCounter}`;
2780
+ const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Constant, node, parent, {
2781
+ type: "given",
2782
+ isGiven: true,
2783
+ scala3: true,
2784
+ anonymous: !explicitName
2785
+ });
2786
+ symbols.push(symbol);
2787
+ if (explicitName && this.isPublic(node) && !parent) {
2788
+ exports.push({ name: symbol.name, kind: ASTEngine.SymbolKind.Constant, range: symbol.range });
2789
+ }
2790
+ return symbol;
2791
+ }
2792
+ handleFunction(node, symbols, relationships, exports, patternsObj, parent) {
2793
+ const name = this.extractIdentifier(node, "name");
2794
+ let extensionReceiverType;
2795
+ let extensionReceiverTypeNode = null;
2796
+ {
2797
+ let anc = node.parent;
2798
+ while (anc) {
2799
+ if (anc.type === "extension_definition") {
2800
+ for (let i = 0; i < anc.namedChildCount; i++) {
2801
+ const pc = anc.namedChild(i);
2802
+ if (!pc || pc.type !== "parameters") continue;
2803
+ if (pc.namedChildCount > 0 && pc.namedChild(0)) {
2804
+ const p0 = pc.namedChild(0);
2805
+ for (let j = 0; j < p0.namedChildCount; j++) {
2806
+ const tc = p0.namedChild(j);
2807
+ if (tc && this.isExtensionReceiverTypeNode(tc.type)) {
2808
+ const t = tc.text;
2809
+ extensionReceiverType = t.endsWith("?") ? t.slice(0, -1) : t;
2810
+ extensionReceiverTypeNode = tc;
2811
+ break;
2812
+ }
2813
+ }
2814
+ }
2815
+ break;
2816
+ }
2817
+ break;
2818
+ }
2819
+ if (anc.type === "class_definition" || anc.type === "trait_definition" || anc.type === "object_definition") break;
2820
+ anc = anc.parent;
2821
+ }
2822
+ }
2823
+ if (!name) return null;
2824
+ if (name === "this" && parent && (parent.kind === ASTEngine.SymbolKind.Class || parent.kind === ASTEngine.SymbolKind.Module)) {
2825
+ let ctor;
2826
+ for (const s of symbols) {
2827
+ if (s.ownerName === parent.name && s.kind === ASTEngine.SymbolKind.Constructor && s.name === "constructor") {
2828
+ ctor = s;
2829
+ break;
2830
+ }
2831
+ }
2832
+ if (ctor) {
2833
+ const existing = new Set((ctor.parameters ?? []).map((p) => `${p.name}:${p.type}`));
2834
+ const ps = ctor.parameters ?? (ctor.parameters = []);
2835
+ for (let i = 0; i < node.namedChildCount; i++) {
2836
+ const pn = node.namedChild(i);
2837
+ if (!pn || pn.type !== "parameters") continue;
2838
+ for (let j = 0; j < pn.namedChildCount; j++) {
2839
+ const p = pn.namedChild(j);
2840
+ if (!p || p.type !== "parameter") continue;
2841
+ const pname = p.childForFieldName("name")?.text || "";
2842
+ const ptype = p.childForFieldName("type")?.text || "";
2843
+ const key = `${pname}:${ptype}`;
2844
+ if (existing.has(key)) continue;
2845
+ existing.add(key);
2846
+ ps.push({ name: pname, type: ptype, isImplicit: false });
2847
+ }
2848
+ }
2849
+ }
2850
+ patternsObj["Auxiliary Constructors"] = true;
2851
+ return parent;
2852
+ }
2853
+ const isMethod = parent && (parent.metadata?.type === "class" || parent.metadata?.type === "trait" || parent.metadata?.type === "object" || parent.kind === ASTEngine.SymbolKind.Class || parent.kind === ASTEngine.SymbolKind.Interface || parent.kind === ASTEngine.SymbolKind.Module);
2854
+ const mods = this.collectModifiers(node);
2855
+ const isAbstract = node.type === "function_declaration" || mods.has("abstract");
2856
+ const isOverride = mods.has("override");
2857
+ const isImplicit = mods.has("implicit");
2858
+ const isInline = mods.has("inline_modifier") || mods.has("transparent_modifier");
2859
+ const parameters = [];
2860
+ let returnType = void 0;
2861
+ for (let i = 0; i < node.childCount; i++) {
2862
+ const child = node.child(i);
2863
+ if (!child) continue;
2864
+ if (child.type === "parameters") {
2865
+ let hasImplicit = false;
2866
+ for (let j = 0; j < child.childCount; j++) {
2867
+ const paramChild = child.child(j);
2868
+ if (paramChild && (paramChild.type === "implicit" || paramChild.type === "using")) {
2869
+ hasImplicit = true;
2870
+ break;
2871
+ }
2872
+ }
2873
+ for (let j = 0; j < child.namedChildCount; j++) {
2874
+ const paramNode = child.namedChild(j);
2875
+ if (!paramNode) continue;
2876
+ if (paramNode.type === "parameter") {
2877
+ const paramName = paramNode.childForFieldName("name")?.text || "";
2878
+ const paramType = paramNode.childForFieldName("type")?.text || "";
2879
+ parameters.push({
2880
+ name: paramName,
2881
+ type: paramType,
2882
+ isImplicit: hasImplicit
2883
+ });
2884
+ continue;
2885
+ }
2886
+ const isTypeNode = paramNode.type.endsWith("_type") || paramNode.type.includes("type_identifier");
2887
+ if (isTypeNode) {
2888
+ parameters.push({ name: "", type: String(paramNode.text), isImplicit: true });
2889
+ }
2890
+ }
2891
+ }
2892
+ }
2893
+ const returnTypeNode = node.childForFieldName("return_type") || node.childForFieldName("type");
2894
+ if (returnTypeNode) {
2895
+ returnType = returnTypeNode.text;
2896
+ }
2897
+ const isEnumMethod = parent?.kind === ASTEngine.SymbolKind.Enum || parent?.metadata?.type === "enum";
2898
+ const methodKind = isMethod || isEnumMethod ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function;
2899
+ const symbol = this.createSymbol(
2900
+ name,
2901
+ methodKind,
2902
+ node,
2903
+ parent,
2904
+ {
2905
+ type: "function",
2906
+ parameters: parameters.length > 0 ? parameters : void 0,
2907
+ returnType,
2908
+ isAbstract,
2909
+ isOverride,
2910
+ isImplicit,
2911
+ isInline,
2912
+ // Scala 3 extension methods: the receiver lives on the enclosing
2913
+ // extension_definition's parameters, NOT on the function node, and
2914
+ // the wrapper never becomes a parent symbol. Detect via ancestors.
2915
+ isExtension: !!extensionReceiverType,
2916
+ // M60/SDJ-9: the receiver is SEMANTIC — a Scala 3 extension method is
2917
+ // in scope only for its receiver type — and it was recorded NOWHERE.
2918
+ // Two `isPackage` methods, one on AbstractFile and one on JFile,
2919
+ // published byte-identical symbols: same id (`FileUtils.isPackage`),
2920
+ // same kind, same owner, no field to tell them apart. Storing the
2921
+ // receiver is pure information gain and is what lets the identity pass
2922
+ // below separate them.
2923
+ extensionReceiver: extensionReceiverType,
2924
+ complexity: this.calculateScalaFunctionComplexity(node)
2925
+ }
2926
+ );
2927
+ symbols.push(symbol);
2928
+ const fnTypeParams = this.extractScalaTypeParameters(node);
2929
+ if (fnTypeParams.length > 0) {
2930
+ symbol.genericParameters = fnTypeParams;
2931
+ symbol.metadata.typeParameters = fnTypeParams;
2932
+ }
2933
+ if (extensionReceiverType) {
2934
+ relationships.push({
2935
+ source: name,
2936
+ target: extensionReceiverType,
2937
+ type: ASTEngine.RelationshipType.Uses,
2938
+ confidence: 0.9,
2939
+ // M59 (#825): the line belongs to the NAME — the receiver TYPE node,
2940
+ // not the method that uses it. `extension (x: Bla)` on its own line
2941
+ // puts `Bla` there; the method body follows below.
2942
+ line_number: (extensionReceiverTypeNode ?? node).startPosition.row + 1,
2943
+ metadata: { relationshipType: "scala_extension_method", isExtension: true }
2944
+ });
2945
+ }
2946
+ if (this.isPublic(node) && methodKind === ASTEngine.SymbolKind.Function && !parent) {
2947
+ exports.push({
2948
+ name: symbol.name,
2949
+ kind: ASTEngine.SymbolKind.Function,
2950
+ range: symbol.range
2951
+ });
2952
+ }
2953
+ if (name === "apply") patternsObj["Factory Pattern (apply)"] = true;
2954
+ if (name === "unapply" || name === "unapplySeq") patternsObj["Extractor Pattern (unapply)"] = true;
2955
+ return symbol;
2956
+ }
2957
+ handleVariable(node, symbols, exports, parent) {
2958
+ let ancestor = node.parent;
2959
+ while (ancestor) {
2960
+ if (ancestor.type === "function_definition" || ancestor.type === "function_declaration" || ancestor.type === "lambda_expression" || ancestor.type === "block" && ancestor.parent?.type === "function_definition") {
2961
+ return;
2962
+ }
2963
+ if (ancestor.type === "class_definition" || ancestor.type === "trait_definition" || ancestor.type === "object_definition" || ancestor.type === "enum_definition" || // M49/M37 #866: `template_body` is the body of an anonymous class
2964
+ // (`new Runnable { val field = target() }`). A val/var written
2965
+ // directly in it is a MEMBER of that type, not a local of the method
2966
+ // the literal happens to sit in — so the boundary check comes first,
2967
+ // exactly like the three named type forms above. Pre-fix the scan
2968
+ // walked past the body, reached the enclosing `def`, and dropped the
2969
+ // member entirely (java/groovy emit `property field
2970
+ // owner=anonymous_Runnable`). The body's PARENT must be the
2971
+ // instance_expression, otherwise this is a named/anonymous class body
2972
+ // handled by the branches above.
2973
+ ancestor.type === "template_body" && ancestor.parent?.type === "instance_expression") {
2974
+ break;
2975
+ }
2976
+ ancestor = ancestor.parent;
2977
+ }
2978
+ let patternNode = node.childForFieldName("pattern");
2979
+ if (!patternNode) {
2980
+ for (let i = 0; i < node.childCount; i++) {
2981
+ const child = node.child(i);
2982
+ if (child && child.isNamed && child.type === "identifier") {
2983
+ patternNode = child;
2984
+ break;
2985
+ }
2986
+ }
2987
+ }
2988
+ if (!patternNode) return;
2989
+ if (patternNode.type === "tuple_pattern") return;
2990
+ if (patternNode.text === "this") return;
2991
+ if (patternNode.type !== "identifier" && patternNode.type !== "identifiers" && patternNode.type !== "operator_identifier") return;
2992
+ const kind = node.type === "val_definition" || node.type === "val_declaration" ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Variable;
2993
+ const declared = [];
2994
+ if (patternNode.type === "identifiers") {
2995
+ for (let i = 0; i < patternNode.childCount; i++) {
2996
+ const child = patternNode.child(i);
2997
+ if (child && child.isNamed && child.type === "identifier") declared.push(child.text);
2998
+ }
2999
+ }
3000
+ if (declared.length === 0) declared.push(patternNode.text);
3001
+ for (const name of declared) {
3002
+ const symbol = this.createSymbol(name, kind, node, parent, { type: node.type, isAbstract: node.type === "val_declaration" || node.type === "var_declaration" });
3003
+ symbols.push(symbol);
3004
+ if (this.isPublic(node) && !parent) {
3005
+ exports.push({
3006
+ name: symbol.name,
3007
+ kind,
3008
+ range: symbol.range
3009
+ });
3010
+ }
3011
+ }
3012
+ }
3013
+ handleInstantiation(node, relationships, parent) {
3014
+ const sourceName = parent?.name ?? "(script)";
3015
+ const typeNode = node.namedChild(0);
3016
+ if (!typeNode) return;
3017
+ let typeName = this.scalaTypeHeadName(typeNode);
3018
+ let prevTypeName;
3019
+ do {
3020
+ prevTypeName = typeName;
3021
+ typeName = this.stripTypeArgs(typeName);
3022
+ } while (typeName !== prevTypeName);
3023
+ if (typeName && typeName !== "unknown") {
3024
+ relationships.push({
3025
+ source: sourceName,
3026
+ target: typeName,
3027
+ type: ASTEngine.RelationshipType.Instantiates,
3028
+ confidence: 0.9,
3029
+ line_number: typeNode.startPosition.row + 1,
3030
+ metadata: { isInstantiation: true }
3031
+ });
3032
+ }
3033
+ }
3034
+ handleCall(node, relationships, patternsObj, parent) {
3035
+ const fnNode = node.childForFieldName("function");
3036
+ if (fnNode) {
3037
+ if (fnNode.text.startsWith("'") || fnNode.text.startsWith("`")) return;
3038
+ if (parent && fnNode.type !== "call_expression") {
3039
+ if (!parent.qualified_calls) parent.qualified_calls = [];
3040
+ parent.qualified_calls.push(this.buildScalaCallPath(fnNode));
3041
+ }
3042
+ if (fnNode.type === "call_expression") return;
3043
+ const CALLEE_NAME_SHAPES = /* @__PURE__ */ new Set([
3044
+ "identifier",
3045
+ "field_expression",
3046
+ "generic_function",
3047
+ "operator_identifier",
3048
+ "type_identifier",
3049
+ "stable_type_identifier"
3050
+ ]);
3051
+ if (!CALLEE_NAME_SHAPES.has(fnNode.type)) return;
3052
+ let calledName = "";
3053
+ let isQualified = false;
3054
+ let calleeNode = null;
3055
+ if (fnNode.type === "generic_function") {
3056
+ const leaves = [];
3057
+ const findLeaves = (nn) => {
3058
+ if (nn.type === "identifier") {
3059
+ leaves.push(nn);
3060
+ return;
3061
+ }
3062
+ if (nn.type === "type_arguments") return;
3063
+ for (let i = 0; i < nn.namedChildCount; i++) {
3064
+ const c = nn.namedChild(i);
3065
+ if (c) findLeaves(c);
3066
+ }
3067
+ };
3068
+ findLeaves(fnNode);
3069
+ if (leaves.length > 0) {
3070
+ calleeNode = leaves[leaves.length - 1];
3071
+ calledName = calleeNode.text;
3072
+ isQualified = leaves.length > 1;
3073
+ }
3074
+ } else {
3075
+ calledName = fnNode.text;
3076
+ calleeNode = fnNode;
3077
+ isQualified = fnNode.type === "field_expression";
3078
+ if (isQualified) {
3079
+ const leaves = [];
3080
+ for (let i = 0; i < fnNode.namedChildCount; i++) {
3081
+ const c = fnNode.namedChild(i);
3082
+ if (c && (c.type === "identifier" || c.type === "operator_identifier")) leaves.push(c);
3083
+ }
3084
+ if (leaves.length > 0) {
3085
+ calleeNode = leaves[leaves.length - 1];
3086
+ calledName = calleeNode.text;
3087
+ }
3088
+ }
3089
+ }
3090
+ const calleeLine = (calleeNode ?? node).startPosition.row + 1;
3091
+ const sourceName = parent?.name ?? "(script)";
3092
+ if (calledName === "this") return;
3093
+ if (calledName) {
3094
+ const first = calledName.charCodeAt(0);
3095
+ const isCapitalized = first >= 65 && first <= 90;
3096
+ let prevCalled;
3097
+ do {
3098
+ prevCalled = calledName;
3099
+ calledName = this.stripBracketGroups(calledName).trim();
3100
+ } while (calledName !== prevCalled);
3101
+ if (isCapitalized && !isQualified) {
3102
+ relationships.push({
3103
+ source: sourceName,
3104
+ target: calledName,
3105
+ type: ASTEngine.RelationshipType.Instantiates,
3106
+ // S1.4: explicit heuristic confidence — a bare PascalCase call is
3107
+ // a companion-apply GUESS (documented 0.7 contract), never an
3108
+ // implicit-1.0 structural edge.
3109
+ confidence: 0.7,
3110
+ line_number: calleeLine,
3111
+ metadata: { isInstantiation: true }
3112
+ });
3113
+ } else {
3114
+ relationships.push({
3115
+ source: sourceName,
3116
+ target: calledName,
3117
+ type: ASTEngine.RelationshipType.Calls,
3118
+ // S1.4: method calls carry the documented 0.9 confidence (docs
3119
+ // promise 0.9 for calls; the classifier reads it explicitly).
3120
+ confidence: 0.9,
3121
+ line_number: calleeLine,
3122
+ metadata: {}
3123
+ });
3124
+ }
3125
+ if (["map", "filter", "flatMap", "foreach", "fold", "reduce", "collect"].includes(calledName)) {
3126
+ patternsObj["Higher-Order Functions"] = true;
3127
+ patternsObj["Functional Programming"] = true;
3128
+ }
3129
+ }
3130
+ }
3131
+ }
3132
+ handleImport(node, imports) {
3133
+ if (node.type !== "import_declaration") return;
3134
+ const parts = [];
3135
+ const selectors = [];
3136
+ const groups = [];
3137
+ let groupParts = [];
3138
+ let moduleLine = node.startPosition.row + 1;
3139
+ const part = (text) => stripScalaQuoting(text);
3140
+ for (let i = 0; i < node.childCount; i++) {
3141
+ const child = node.child(i);
3142
+ if (!child) continue;
3143
+ if (child.type === "identifier" || child.type === ".") {
3144
+ if (child.type === "identifier") {
3145
+ parts.push(part(child.text));
3146
+ groupParts.push(part(child.text));
3147
+ moduleLine = child.startPosition.row + 1;
3148
+ }
3149
+ } else if (child.type === ",") {
3150
+ const completed = groupParts.join(".");
3151
+ if (completed.length > 0) groups.push({ prefix: completed, names: [] });
3152
+ groupParts = [];
3153
+ } else if (child.type === "namespace_wildcard") {
3154
+ selectors.push({ local: "*", imported: "*" });
3155
+ const gprefix = groupParts.join(".");
3156
+ if (gprefix.length > 0) groups.push({ prefix: gprefix, names: [] });
3157
+ groupParts = [];
3158
+ } else if (child.type === "namespace_selectors") {
3159
+ const groupPrefix = groupParts.join(".");
3160
+ const groupNames = [];
3161
+ for (let j = 0; j < child.childCount; j++) {
3162
+ const sel = child.child(j);
3163
+ if (!sel) continue;
3164
+ if (sel.type === "identifier") {
3165
+ groupNames.push(part(sel.text));
3166
+ selectors.push({ local: sel.text, imported: sel.text });
3167
+ } else if (sel.type === "arrow_renamed_identifier") {
3168
+ const imported = sel.namedChild(0);
3169
+ const local = sel.namedChild(1);
3170
+ if (imported && local) {
3171
+ const isExclusion = local.type === "wildcard" || local.text === "_";
3172
+ parts.push(part(imported.text));
3173
+ moduleLine = imported.startPosition.row + 1;
3174
+ if (!isExclusion) groupNames.push(part(imported.text));
3175
+ selectors.push({ local: isExclusion ? "_" : local.text, imported: imported.text, excluded: isExclusion });
3176
+ }
3177
+ }
3178
+ }
3179
+ if (groupPrefix.length > 0) groups.push({ prefix: groupPrefix, names: groupNames });
3180
+ groupParts = [];
3181
+ } else if (child.type === "as_renamed_identifier") {
3182
+ const original = child.namedChild(0);
3183
+ const alias = child.namedChild(1);
3184
+ if (original && original.type === "identifier") {
3185
+ parts.push(part(original.text));
3186
+ groupParts.push(part(original.text));
3187
+ moduleLine = original.startPosition.row + 1;
3188
+ selectors.push({ local: alias && alias.type === "identifier" ? alias.text : original.text, imported: original.text });
3189
+ const gprefix = groupParts.join(".");
3190
+ if (gprefix.length > 0) groups.push({ prefix: gprefix, names: [] });
3191
+ groupParts = [];
3192
+ }
3193
+ }
3194
+ }
3195
+ const tailPrefix = groupParts.join(".");
3196
+ if (tailPrefix.length > 0) groups.push({ prefix: tailPrefix, names: [] });
3197
+ const pathStr = parts.join(".");
3198
+ if (selectors.length === 0 && pathStr.length > 0) {
3199
+ const idx = pathStr.lastIndexOf(".");
3200
+ const last = idx >= 0 ? pathStr.substring(idx + 1) : pathStr;
3201
+ if (last.length > 0) selectors.push({ local: last, imported: last });
3202
+ }
3203
+ imports.push({
3204
+ module: pathStr,
3205
+ // M59 (#825): the line the module's last segment is written on (see above).
3206
+ moduleLine,
3207
+ // S1.25 (M49 #715): family consumers read `items` (engine.ts
3208
+ // ImportInfo.items: string[]) — scala was the ONLY family member
3209
+ // populating the legacy `specifiers` shape, so downstream import
3210
+ // listing silently showed nothing for scala. Both shapes are filled:
3211
+ // items = imported names, specifiers kept for any legacy reader.
3212
+ items: selectors.map((s) => s.imported),
3213
+ specifiers: selectors,
3214
+ // Read by the `imports` edge builder; see handleImport's comment on why the
3215
+ // dependency edges and the module path are computed from different bases.
3216
+ ...groups.length > 0 ? { metadata: { importGroups: groups } } : {},
3217
+ range: {
3218
+ startLine: node.startPosition.row + 1,
3219
+ startColumn: node.startPosition.column + 1,
3220
+ endLine: node.endPosition.row + 1,
3221
+ endColumn: node.endPosition.column + 1,
3222
+ startIndex: node.startIndex,
3223
+ endIndex: node.endIndex
3224
+ }
3225
+ });
3226
+ }
3227
+ /**
3228
+ * M14 #419 G3/G8: the LEFTMOST identifier of a field_expression — for
3229
+ * a.b.c that is a (recursing through nested field_expressions); package
3230
+ * paths (com.foo.bar) yield com, so capitalized-receiver detection is
3231
+ * safe against package-qualified members.
3232
+ */
3233
+ leftmostIdentifier(node) {
3234
+ if (node.type === "identifier") return node;
3235
+ const first = node.namedChild(0);
3236
+ return first ? this.leftmostIdentifier(first) : null;
3237
+ }
3238
+ /**
3239
+ * The DECLARED NAME of a definition, read from its `name:` FIELD.
3240
+ *
3241
+ * M60/S8: this replaces "first child of type identifier", which is wrong for
3242
+ * an operator-named method. `def <&[B](that: IO[B]): IO[A]` carries its name
3243
+ * as an `operator_identifier`, so the first `identifier` child is the TYPE
3244
+ * PARAMETER `B` — and `resolveSource` then attributed every reference inside
3245
+ * that method to a type parameter named `B`. Two failures in one: the edge's
3246
+ * source is a binding rather than a declaration, and because the edge dedupe
3247
+ * key is (source, target, kind), every operator-named method in a file
3248
+ * collapses onto ONE bogus source, so the edge COUNT drops too.
3249
+ *
3250
+ * Measured on cats-effect/core/shared/src/main/scala/cats/effect/IO.scala,
3251
+ * where the operators `<*`, `<&`, `*>`, `>>`, `!>`, `&>` are the only sources
3252
+ * the derived truth attributes references to and the analyzer does not: 26
3253
+ * rows on the whole cats-effect corpus, and the whole of a 60-file rotation
3254
+ * read clean because the draw did not include this file.
3255
+ *
3256
+ * The lane's comparison matches on (label, target), so it cannot see a wrong
3257
+ * SOURCE at all — which is why this survived the `uses` audit at 100% too:
3258
+ * collectExpressionUses carries the same resolveSource.
3259
+ */
3260
+ scalaDeclaredNameOf(node) {
3261
+ const f = node.childForFieldName?.("name");
3262
+ if (f) return f.text === "this" ? null : stripScalaQuoting(f.text);
3263
+ const i = this.findChild(node, "identifier")?.text;
3264
+ if (i === void 0) return null;
3265
+ return i === "this" ? null : stripScalaQuoting(i);
3266
+ }
3267
+ findChild(node, type) {
3268
+ for (let i = 0; i < node.childCount; i++) {
3269
+ const c = node.child(i);
3270
+ if (c && c.type === type) return c;
3271
+ }
3272
+ return null;
3273
+ }
3274
+ /**
3275
+ * The declared name of a node that carries it as a bare `identifier` child —
3276
+ * an enum case (`case Red`, `case B(val x: Int) extends E`). Its `name:` field
3277
+ * would serve too, but the grammar's enum-case shape is the one place a
3278
+ * first-identifier scan and the field agree, and this helper is NOT safe for a
3279
+ * `given`: an anonymous given has no `name:` field and its BODY can be a bare
3280
+ * identifier, so the scan returns the given's VALUE. handleGiven reads the
3281
+ * field for that reason (M60/S6).
3282
+ */
3283
+ findChildIdentifier(node) {
3284
+ for (let i = 0; i < node.childCount; i++) {
3285
+ const c = node.child(i);
3286
+ if (c && c.isNamed && c.type === "identifier") return c.text;
3287
+ }
3288
+ return null;
3289
+ }
3290
+ };
3291
+ export {
3292
+ RealScalaAnalyzer
3293
+ };