gitnexus 1.6.5-rc.2 → 1.6.5-rc.21

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 (288) hide show
  1. package/README.md +1 -1
  2. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +26 -0
  3. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
  4. package/dist/_shared/scope-resolution/method-dispatch-index.js +22 -3
  5. package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
  6. package/dist/_shared/scope-resolution/registries/lookup-core.js +18 -1
  7. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -1
  8. package/dist/cli/ai-context.d.ts +1 -0
  9. package/dist/cli/ai-context.js +30 -12
  10. package/dist/cli/analyze.d.ts +18 -0
  11. package/dist/cli/analyze.js +33 -4
  12. package/dist/cli/augment.d.ts +1 -1
  13. package/dist/cli/augment.js +1 -1
  14. package/dist/cli/index.js +6 -1
  15. package/dist/cli/setup.js +6 -0
  16. package/dist/core/augmentation/engine.d.ts +2 -2
  17. package/dist/core/augmentation/engine.js +26 -6
  18. package/dist/core/embeddings/ast-utils.js +2 -1
  19. package/dist/core/embeddings/http-client.js +22 -6
  20. package/dist/core/group/extractors/grpc-extractor.js +2 -1
  21. package/dist/core/group/extractors/http-patterns/python.js +175 -0
  22. package/dist/core/group/extractors/http-route-extractor.js +2 -1
  23. package/dist/core/group/extractors/include-extractor.js +2 -1
  24. package/dist/core/group/extractors/thrift-extractor.js +2 -1
  25. package/dist/core/group/extractors/tree-sitter-scanner.js +2 -1
  26. package/dist/core/incremental/shadow-candidates.d.ts +44 -0
  27. package/dist/core/incremental/shadow-candidates.js +74 -0
  28. package/dist/core/incremental/subgraph-extract.d.ts +64 -0
  29. package/dist/core/incremental/subgraph-extract.js +111 -0
  30. package/dist/core/ingestion/call-processor.js +166 -47
  31. package/dist/core/ingestion/community-processor.js +18 -0
  32. package/dist/core/ingestion/heritage-processor.js +3 -2
  33. package/dist/core/ingestion/import-processor.js +2 -1
  34. package/dist/core/ingestion/import-resolvers/standard.d.ts +5 -0
  35. package/dist/core/ingestion/import-resolvers/standard.js +27 -1
  36. package/dist/core/ingestion/import-resolvers/utils.js +4 -0
  37. package/dist/core/ingestion/languages/c/arity-metadata.d.ts +14 -0
  38. package/dist/core/ingestion/languages/c/arity-metadata.js +94 -0
  39. package/dist/core/ingestion/languages/c/arity.d.ts +6 -0
  40. package/dist/core/ingestion/languages/c/arity.js +18 -0
  41. package/dist/core/ingestion/languages/c/captures.d.ts +2 -0
  42. package/dist/core/ingestion/languages/c/captures.js +105 -0
  43. package/dist/core/ingestion/languages/c/header-scan.d.ts +7 -0
  44. package/dist/core/ingestion/languages/c/header-scan.js +55 -0
  45. package/dist/core/ingestion/languages/c/import-decomposer.d.ts +8 -0
  46. package/dist/core/ingestion/languages/c/import-decomposer.js +50 -0
  47. package/dist/core/ingestion/languages/c/import-target.d.ts +14 -0
  48. package/dist/core/ingestion/languages/c/import-target.js +57 -0
  49. package/dist/core/ingestion/languages/c/index.d.ts +11 -0
  50. package/dist/core/ingestion/languages/c/index.js +11 -0
  51. package/dist/core/ingestion/languages/c/interpret.d.ts +14 -0
  52. package/dist/core/ingestion/languages/c/interpret.js +48 -0
  53. package/dist/core/ingestion/languages/c/merge-bindings.d.ts +7 -0
  54. package/dist/core/ingestion/languages/c/merge-bindings.js +23 -0
  55. package/dist/core/ingestion/languages/c/query.d.ts +3 -0
  56. package/dist/core/ingestion/languages/c/query.js +161 -0
  57. package/dist/core/ingestion/languages/c/scope-resolver.d.ts +13 -0
  58. package/dist/core/ingestion/languages/c/scope-resolver.js +60 -0
  59. package/dist/core/ingestion/languages/c/simple-hooks.d.ts +14 -0
  60. package/dist/core/ingestion/languages/c/simple-hooks.js +19 -0
  61. package/dist/core/ingestion/languages/c/static-linkage.d.ts +13 -0
  62. package/dist/core/ingestion/languages/c/static-linkage.js +57 -0
  63. package/dist/core/ingestion/languages/c-cpp.js +10 -0
  64. package/dist/core/ingestion/languages/csharp/captures.js +2 -1
  65. package/dist/core/ingestion/languages/csharp/namespace-siblings.js +3 -2
  66. package/dist/core/ingestion/languages/go/captures.js +2 -1
  67. package/dist/core/ingestion/languages/go/range-binding.js +2 -1
  68. package/dist/core/ingestion/languages/java/arity-metadata.d.ts +18 -0
  69. package/dist/core/ingestion/languages/java/arity-metadata.js +40 -0
  70. package/dist/core/ingestion/languages/java/arity.d.ts +10 -0
  71. package/dist/core/ingestion/languages/java/arity.js +24 -0
  72. package/dist/core/ingestion/languages/java/cache-stats.d.ts +15 -0
  73. package/dist/core/ingestion/languages/java/cache-stats.js +26 -0
  74. package/dist/core/ingestion/languages/java/captures.d.ts +17 -0
  75. package/dist/core/ingestion/languages/java/captures.js +187 -0
  76. package/dist/core/ingestion/languages/java/import-decomposer.d.ts +18 -0
  77. package/dist/core/ingestion/languages/java/import-decomposer.js +85 -0
  78. package/dist/core/ingestion/languages/java/import-target.d.ts +17 -0
  79. package/dist/core/ingestion/languages/java/import-target.js +100 -0
  80. package/dist/core/ingestion/languages/java/index.d.ts +29 -0
  81. package/dist/core/ingestion/languages/java/index.js +29 -0
  82. package/dist/core/ingestion/languages/java/interpret.d.ts +13 -0
  83. package/dist/core/ingestion/languages/java/interpret.js +131 -0
  84. package/dist/core/ingestion/languages/java/merge-bindings.d.ts +12 -0
  85. package/dist/core/ingestion/languages/java/merge-bindings.js +40 -0
  86. package/dist/core/ingestion/languages/java/query.d.ts +30 -0
  87. package/dist/core/ingestion/languages/java/query.js +192 -0
  88. package/dist/core/ingestion/languages/java/receiver-binding.d.ts +11 -0
  89. package/dist/core/ingestion/languages/java/receiver-binding.js +95 -0
  90. package/dist/core/ingestion/languages/java/scope-resolver.d.ts +50 -0
  91. package/dist/core/ingestion/languages/java/scope-resolver.js +74 -0
  92. package/dist/core/ingestion/languages/java/simple-hooks.d.ts +13 -0
  93. package/dist/core/ingestion/languages/java/simple-hooks.js +34 -0
  94. package/dist/core/ingestion/languages/java.js +11 -0
  95. package/dist/core/ingestion/languages/php/arity-metadata.d.ts +28 -0
  96. package/dist/core/ingestion/languages/php/arity-metadata.js +63 -0
  97. package/dist/core/ingestion/languages/php/arity.d.ts +25 -0
  98. package/dist/core/ingestion/languages/php/arity.js +40 -0
  99. package/dist/core/ingestion/languages/php/cache-stats.d.ts +15 -0
  100. package/dist/core/ingestion/languages/php/cache-stats.js +26 -0
  101. package/dist/core/ingestion/languages/php/captures.d.ts +34 -0
  102. package/dist/core/ingestion/languages/php/captures.js +739 -0
  103. package/dist/core/ingestion/languages/php/import-decomposer.d.ts +28 -0
  104. package/dist/core/ingestion/languages/php/import-decomposer.js +265 -0
  105. package/dist/core/ingestion/languages/php/import-target.d.ts +47 -0
  106. package/dist/core/ingestion/languages/php/import-target.js +100 -0
  107. package/dist/core/ingestion/languages/php/index.d.ts +68 -0
  108. package/dist/core/ingestion/languages/php/index.js +72 -0
  109. package/dist/core/ingestion/languages/php/interpret.d.ts +36 -0
  110. package/dist/core/ingestion/languages/php/interpret.js +241 -0
  111. package/dist/core/ingestion/languages/php/merge-bindings.d.ts +19 -0
  112. package/dist/core/ingestion/languages/php/merge-bindings.js +47 -0
  113. package/dist/core/ingestion/languages/php/namespace-siblings.d.ts +51 -0
  114. package/dist/core/ingestion/languages/php/namespace-siblings.js +288 -0
  115. package/dist/core/ingestion/languages/php/query.d.ts +32 -0
  116. package/dist/core/ingestion/languages/php/query.js +326 -0
  117. package/dist/core/ingestion/languages/php/receiver-binding.d.ts +36 -0
  118. package/dist/core/ingestion/languages/php/receiver-binding.js +128 -0
  119. package/dist/core/ingestion/languages/php/scope-resolver.d.ts +23 -0
  120. package/dist/core/ingestion/languages/php/scope-resolver.js +358 -0
  121. package/dist/core/ingestion/languages/php/simple-hooks.d.ts +42 -0
  122. package/dist/core/ingestion/languages/php/simple-hooks.js +111 -0
  123. package/dist/core/ingestion/languages/php.d.ts +0 -7
  124. package/dist/core/ingestion/languages/php.js +15 -0
  125. package/dist/core/ingestion/languages/python/captures.js +2 -1
  126. package/dist/core/ingestion/languages/typescript/captures.js +2 -1
  127. package/dist/core/ingestion/parsing-processor.d.ts +22 -2
  128. package/dist/core/ingestion/parsing-processor.js +85 -50
  129. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +5 -0
  130. package/dist/core/ingestion/pipeline-phases/parse-impl.js +119 -11
  131. package/dist/core/ingestion/pipeline-phases/parse.d.ts +14 -0
  132. package/dist/core/ingestion/pipeline.d.ts +13 -0
  133. package/dist/core/ingestion/registry-primary-flag.js +2 -0
  134. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +65 -0
  135. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +1 -1
  136. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +12 -2
  137. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +5 -0
  138. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +25 -1
  139. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +79 -7
  140. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +7 -3
  141. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +24 -4
  142. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +21 -2
  143. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +13 -1
  144. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +6 -0
  145. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +17 -0
  146. package/dist/core/ingestion/scope-resolution/pipeline/run.js +31 -7
  147. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  148. package/dist/core/ingestion/tree-sitter-queries.js +10 -0
  149. package/dist/core/ingestion/workers/parse-worker.js +2 -1
  150. package/dist/core/lbug/lbug-adapter.d.ts +26 -0
  151. package/dist/core/lbug/lbug-adapter.js +174 -40
  152. package/dist/core/lbug/pool-adapter.js +19 -2
  153. package/dist/core/run-analyze.d.ts +2 -0
  154. package/dist/core/run-analyze.js +323 -29
  155. package/dist/core/tree-sitter/safe-parse.d.ts +6 -0
  156. package/dist/core/tree-sitter/safe-parse.js +32 -0
  157. package/dist/server/git-clone.js +10 -13
  158. package/dist/storage/file-hash.d.ts +47 -0
  159. package/dist/storage/file-hash.js +86 -0
  160. package/dist/storage/git.d.ts +17 -6
  161. package/dist/storage/git.js +46 -12
  162. package/dist/storage/parse-cache.d.ts +67 -0
  163. package/dist/storage/parse-cache.js +182 -0
  164. package/dist/storage/repo-manager.d.ts +41 -1
  165. package/dist/storage/repo-manager.js +17 -2
  166. package/hooks/claude/gitnexus-hook.cjs +8 -1
  167. package/hooks/claude/hook-lock.cjs +119 -0
  168. package/package.json +1 -1
  169. package/web/assets/{agent-BpHQleHE.js → agent-BG6RCOzz.js} +1 -1
  170. package/web/assets/architecture-7EHR7CIX-6QZW5X65-F-tujre-.js +1 -0
  171. package/web/assets/architectureDiagram-UL44E2DR-2GL7nOvO.js +36 -0
  172. package/web/assets/blockDiagram-7IZFK4PR-5AjX0pPU.js +132 -0
  173. package/web/assets/{c4Diagram-DFAF54RM-DeJV1rCn.js → c4Diagram-Y2BXMSZH-hLOeq7xC.js} +2 -2
  174. package/web/assets/chunk-2T2R6R2M-B_pq_2nf.js +4 -0
  175. package/web/assets/chunk-2UTLFMKG-CpoohCFN.js +1 -0
  176. package/web/assets/{chunk-7RZVMHOQ-BdIU-RGO.js → chunk-3SSMPTDK-CfqpR3Nr.js} +1 -1
  177. package/web/assets/chunk-4R4BOZG6-D3gm7tYj.js +159 -0
  178. package/web/assets/chunk-6764PJDD-WcI965eO.js +1 -0
  179. package/web/assets/chunk-AQ6EADP3-BpMj3rQl.js +1 -0
  180. package/web/assets/{chunk-KSICW3F5-C2tZmXwv.js → chunk-AZZRMDJM-DSoCJlUA.js} +1 -1
  181. package/web/assets/chunk-C62D2QBJ-CvFF2PnB.js +1 -0
  182. package/web/assets/chunk-CEXFNPSA-DdNtJtrF.js +1 -0
  183. package/web/assets/chunk-J5EP6P6S-DtcVaAJI.js +1 -0
  184. package/web/assets/{chunk-AEOMTBSW-D7qjBMHW.js → chunk-JQRUD6KW-DElaNjc0.js} +1 -1
  185. package/web/assets/chunk-KGYTTC2M-D0K5or7e.js +1 -0
  186. package/web/assets/chunk-KRXBNO2N-DpR8iDt8.js +1 -0
  187. package/web/assets/chunk-LCXTWHL2-DdMObOEk.js +231 -0
  188. package/web/assets/chunk-LII3EMHJ-I78zXtzi.js +1 -0
  189. package/web/assets/chunk-RERM46MO-Betu1cSy.js +1 -0
  190. package/web/assets/chunk-RG4AUYOV-BUCJ2JP5.js +206 -0
  191. package/web/assets/chunk-RKZBBQEN-Cutpza0t.js +1 -0
  192. package/web/assets/chunk-RLI5ZMPA-BFUxCvX2.js +1 -0
  193. package/web/assets/chunk-T5OCTHI4-DKwlkuvB.js +1 -0
  194. package/web/assets/chunk-UP6H54XL-0YWa0NDg.js +1 -0
  195. package/web/assets/chunk-UXSXWOXI-CSz0OXpz.js +1 -0
  196. package/web/assets/chunk-W44A43WB-AOmgoT-X.js +1 -0
  197. package/web/assets/{chunk-RWUO3TPN-DTWzAhQ1.js → chunk-ZXARS5L4-DIzgxjMw.js} +1 -1
  198. package/web/assets/classDiagram-KGZ6W3CR-BT-rn7LF.js +1 -0
  199. package/web/assets/classDiagram-v2-72OJOZXJ-uR3nDadY.js +1 -0
  200. package/web/assets/{cose-bilkent-PNC4W37J-DdZq7Qz_.js → cose-bilkent-UX7MHV2Q-DCSCD1TK.js} +1 -1
  201. package/web/assets/dagre-ND4H6XIP-CsciHSij.js +4 -0
  202. package/web/assets/diagram-3NCE3AQN-BKUT5Rpc.js +43 -0
  203. package/web/assets/diagram-GF46GFSD-Dxb4hZSo.js +24 -0
  204. package/web/assets/diagram-HNR7UZ2L-D3qus9Cn.js +3 -0
  205. package/web/assets/diagram-QXG6HAR7-r9BMLGGP.js +24 -0
  206. package/web/assets/diagram-WEQXMOUZ-BGXqs7-m.js +10 -0
  207. package/web/assets/{erDiagram-GCSMX5X6-DfdN6iqF.js → erDiagram-L5TCEMPS-LgSWVrkZ.js} +2 -2
  208. package/web/assets/eventmodeling-FCH6USID-MREXMVOE-By7r3bla.js +1 -0
  209. package/web/assets/flowDiagram-H6V6AXG4-Bj_vV5ky.js +162 -0
  210. package/web/assets/ganttDiagram-JCBTUEKG-B1eVITSh.js +292 -0
  211. package/web/assets/gitGraph-WXDBUCRP-R675I2BI-ClEP6RdE.js +1 -0
  212. package/web/assets/gitGraphDiagram-S2ZK5IYY-BP0R-_FD.js +106 -0
  213. package/web/assets/{index-3NUCUW2s.js → index-oL-ZNYwb.js} +80 -81
  214. package/web/assets/info-J43DQDTF-KCYPFFUO-bBJRjmP5.js +1 -0
  215. package/web/assets/infoDiagram-3YFTVSEB-Dmj4OEO3.js +2 -0
  216. package/web/assets/{ishikawaDiagram-YMYX4NHK-BF8AtDaj.js → ishikawaDiagram-BNXS4ZKH-B3NgQ4lJ.js} +2 -2
  217. package/web/assets/{journeyDiagram-SO5T7YLQ-BW8oKa3N.js → journeyDiagram-M6C3CM3L-DALmL4_N.js} +3 -3
  218. package/web/assets/{kanban-definition-LJHFXRCJ-DzbOsR-2.js → kanban-definition-75IXJCU3-AvVbHpnN.js} +4 -4
  219. package/web/assets/{katex-GD7MH7QM-CJiOjBBJ.js → katex-K3KEBU37-CqF6_ZN-.js} +1 -1
  220. package/web/assets/{mindmap-definition-2EUWGEK5-BIXGQALq.js → mindmap-definition-2TDM6QVE-B5c9VOzs.js} +28 -28
  221. package/web/assets/packet-YPE3B663-LP52Z2RK-1ZCldnu3.js +1 -0
  222. package/web/assets/pie-LRSECV5Y-TCRJHUBD-EhjHC5EE.js +1 -0
  223. package/web/assets/pieDiagram-CU6KROY3-Po2HSSR4.js +30 -0
  224. package/web/assets/quadrantDiagram-VICAPDV7-C47UxSZD.js +7 -0
  225. package/web/assets/radar-GUYGQ44K-RDLRG3WG-CvXjgxlW.js +1 -0
  226. package/web/assets/{requirementDiagram-M5DCFWZL-CzUUrhQD.js → requirementDiagram-JXO7QTGE-CpECN_Hq.js} +6 -6
  227. package/web/assets/sankeyDiagram-URQDO5SZ-B3b7cLrS.js +40 -0
  228. package/web/assets/sequenceDiagram-VS2MUI6T-CRJ95oVP.js +162 -0
  229. package/web/assets/stateDiagram-7D4R322I-C7U9b3ks.js +1 -0
  230. package/web/assets/stateDiagram-v2-36443NZ5-BQajbtov.js +1 -0
  231. package/web/assets/{timeline-definition-5SPVSISX-DkMiGj4Z.js → timeline-definition-O6YCAMPW-rtbk6BBo.js} +4 -4
  232. package/web/assets/treeView-BLDUP644-QA4HXRO3-R2o5gwfP.js +1 -0
  233. package/web/assets/treemap-LRROVOQU-LLAWBHMP-Cesmac_u.js +1 -0
  234. package/web/assets/{vennDiagram-IE5QUKF5-BCxMmf9q.js → vennDiagram-MWXL3ELB-W8Yoaayh.js} +3 -3
  235. package/web/assets/wardley-L42UT6IY-5TKZOOLJ-B9HYoRgA.js +1 -0
  236. package/web/assets/wardleyDiagram-CUQ6CDDI-DIZw5Abj.js +78 -0
  237. package/web/assets/{xychartDiagram-ZHJ5623Y-DZ7fP1_3.js → xychartDiagram-N2JHSOCM-DGRtrj4P.js} +3 -3
  238. package/web/index.html +2 -2
  239. package/web/assets/architecture-YZFGNWBL-S5CXDPWN-BUzkWgDl.js +0 -1
  240. package/web/assets/architectureDiagram-EMZXCZ2Q-CXbzok1G.js +0 -36
  241. package/web/assets/blockDiagram-IGV67L2C-DA1QJ4ZB.js +0 -132
  242. package/web/assets/chunk-3GS5O3IE-B8hpnia8.js +0 -231
  243. package/web/assets/chunk-3YCYZ6SJ-B-BVu21C.js +0 -1
  244. package/web/assets/chunk-6NTNNK5N-DW-jYSDr.js +0 -1
  245. package/web/assets/chunk-A34GCYZU-BbZl-IKy.js +0 -1
  246. package/web/assets/chunk-DJ7UZH7F-B5tMfF87.js +0 -1
  247. package/web/assets/chunk-DKKBVRCY-Djm-6K_e.js +0 -4
  248. package/web/assets/chunk-DU5LTGQ6-QPoynYcp.js +0 -1
  249. package/web/assets/chunk-FXACKDTF-jieSmbe2.js +0 -159
  250. package/web/assets/chunk-H3VCZNTA-8EMHZ3SV.js +0 -1
  251. package/web/assets/chunk-HN6EAY2L-CgE_agsD.js +0 -1
  252. package/web/assets/chunk-O5ABG6QK-DEZV1O19.js +0 -1
  253. package/web/assets/chunk-PK6DOVAG-BXv0JnsJ.js +0 -206
  254. package/web/assets/chunk-RNJOYNJ4-BiBkF9dN.js +0 -1
  255. package/web/assets/chunk-TBF5ZNIQ-BSl6s9lT.js +0 -1
  256. package/web/assets/chunk-TU3PZOEN-DE5Qhc0N.js +0 -1
  257. package/web/assets/chunk-TYMNRAUI-D1NDLIMG.js +0 -1
  258. package/web/assets/chunk-VELTKBKT-C9dVN39o.js +0 -1
  259. package/web/assets/chunk-W7ZLLLMY-Bu-cdLzs.js +0 -1
  260. package/web/assets/chunk-WSB5WSVC-Dor71SOz.js +0 -1
  261. package/web/assets/chunk-XGPFEOL4-CUonLRle.js +0 -1
  262. package/web/assets/classDiagram-PPOCWD7C-Cdd6UdVv.js +0 -1
  263. package/web/assets/classDiagram-v2-23LJLIIU-DNhxY2Ib.js +0 -1
  264. package/web/assets/dagre-E77IOHMT-Cfj_6e17.js +0 -4
  265. package/web/assets/diagram-H7BISOXX-BNvpsrBv.js +0 -43
  266. package/web/assets/diagram-JC5VWROH-Cn0rtDbJ.js +0 -24
  267. package/web/assets/diagram-LXUTUG65-CHfoVKNm.js +0 -10
  268. package/web/assets/diagram-WEHSV5V5-BLxHksnu.js +0 -24
  269. package/web/assets/flowDiagram-OTCZ4VVT-aAZw4c_M.js +0 -162
  270. package/web/assets/ganttDiagram-MUNLMDZQ-BEfC_7Or.js +0 -292
  271. package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-B6ZeRLP0.js +0 -1
  272. package/web/assets/gitGraphDiagram-3HKGZ4G3-CG0kC8Q5.js +0 -106
  273. package/web/assets/info-OMHHGYJF-BF2H5H6G-BIOsZy0W.js +0 -1
  274. package/web/assets/infoDiagram-MN7RKWGX-DUoMH-pq.js +0 -2
  275. package/web/assets/packet-4T2RLAQJ-EV4IVRXR-Bs7ysNMU.js +0 -1
  276. package/web/assets/pie-ZZUOXDRM-N23DN5KN-Csi8nnbH.js +0 -1
  277. package/web/assets/pieDiagram-3IATQBI2-QY-u7P6a.js +0 -30
  278. package/web/assets/quadrantDiagram-E256RVCF-Di4aNiU8.js +0 -7
  279. package/web/assets/radar-PYXPWWZC-P6TP7ZYP-DJlyXh2-.js +0 -1
  280. package/web/assets/sankeyDiagram-L3NBLAOT-D2fbMiIU.js +0 -10
  281. package/web/assets/sequenceDiagram-ZOUHS735-BNdkqguh.js +0 -157
  282. package/web/assets/stateDiagram-MLPALWAM-51_Q2w2L.js +0 -1
  283. package/web/assets/stateDiagram-v2-B5LQ5ZB2-8nHs2aNE.js +0 -1
  284. package/web/assets/treeView-SZITEDCU-5DXDK3XO-Cy6P0_CZ.js +0 -1
  285. package/web/assets/treemap-W4RFUUIX-WYLRDWKO-C_pp1R7A.js +0 -1
  286. package/web/assets/wardley-RL74JXVD-BCRCBASE-CMStJhpn.js +0 -1
  287. package/web/assets/wardleyDiagram-XU3VSMPF-ijlLF77s.js +0 -20
  288. /package/web/assets/{context-builder-CqQNhRj1.js → context-builder-CG-ydtp4.js} +0 -0
@@ -0,0 +1,739 @@
1
+ /**
2
+ * `emitScopeCaptures` for PHP (RFC #909 Ring 3 LANG-php).
3
+ *
4
+ * Drives the PHP scope query against tree-sitter-php and groups raw
5
+ * matches into `CaptureMatch[]` for the central extractor. Layers two
6
+ * synthesized streams on top:
7
+ *
8
+ * 1. **Decomposed use declarations** — each `namespace_use_declaration`
9
+ * is re-emitted with `@import.kind/source/name/alias` markers so
10
+ * `interpretPhpImport` can recover the ParsedImport shape without
11
+ * re-parsing raw text. Grouped uses fan out to one match per clause.
12
+ *
13
+ * 2. **Receiver-binding synthesis** — `$this` and `parent` type-bindings
14
+ * are synthesized on every non-static method entry. PHP's grammar
15
+ * does not express "implicit receiver of a non-static class method"
16
+ * via a clean `.scm` pattern, so we walk up the AST in code.
17
+ *
18
+ * 3. **Arity metadata synthesis** — `@declaration.parameter-count` /
19
+ * `@declaration.required-parameter-count` / `@declaration.parameter-types`
20
+ * are synthesized on function-like declarations so the registry can
21
+ * narrow overloads.
22
+ *
23
+ * 4. **PHPDoc synthesis** — @param and @return annotations in comment
24
+ * nodes preceding method/function declarations are extracted and emitted
25
+ * as `@type-binding.parameter` and `@type-binding.return` matches.
26
+ *
27
+ * 5. **Foreach loop synthesis** — `foreach ($users as $user)` emits
28
+ * a `@type-binding.alias` match binding the loop variable to the
29
+ * element type of the iterable (resolved from PHPDoc or scopeEnv).
30
+ *
31
+ * Pure given the input source text. No I/O, no globals consulted.
32
+ */
33
+ import { findNodeAtRange, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
34
+ import { splitNamespaceUseDeclaration } from './import-decomposer.js';
35
+ import { computePhpArityMetadata } from './arity-metadata.js';
36
+ import { synthesizePhpReceiverBinding } from './receiver-binding.js';
37
+ import { getPhpParser, getPhpScopeQuery } from './query.js';
38
+ import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
39
+ import { getTreeSitterBufferSize } from '../../constants.js';
40
+ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
41
+ /** Declaration anchors that carry function-like arity metadata. */
42
+ const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.function'];
43
+ /** tree-sitter-php node types that the method extractor accepts. */
44
+ const FUNCTION_NODE_TYPES = [
45
+ 'method_declaration',
46
+ 'function_definition',
47
+ 'anonymous_function',
48
+ 'arrow_function',
49
+ ];
50
+ export function emitPhpScopeCaptures(sourceText, _filePath, cachedTree) {
51
+ // Skip the parse when the caller already produced a Tree for this source.
52
+ // The cachedTree parameter is typed as `unknown` at the LanguageProvider
53
+ // contract layer; cast here at the use site.
54
+ let tree = cachedTree;
55
+ if (tree === undefined) {
56
+ tree = parseSourceSafe(getPhpParser(), sourceText, undefined, {
57
+ bufferSize: getTreeSitterBufferSize(sourceText),
58
+ });
59
+ recordCacheMiss();
60
+ }
61
+ else {
62
+ recordCacheHit();
63
+ }
64
+ const rawMatches = getPhpScopeQuery().matches(tree.rootNode);
65
+ const out = [];
66
+ // Pre-scan: collect anchor node IDs of property_declaration nodes already
67
+ // matched by the typed @declaration.property pattern (query.ts ~lines 95–98).
68
+ // The untyped @declaration.variable catch-all (query.ts ~lines 101–103) is
69
+ // intentionally loose — it has no `type:` constraint, so tree-sitter also
70
+ // matches it against typed property declarations and emits a second capture
71
+ // for the same property_declaration anchor. Graph-level def-id collision
72
+ // currently masks the duplicate at the node-emit layer, but the catch-all
73
+ // capture still flows through scope-binding / name-keyed registries with a
74
+ // `$`-prefixed name that the typed branch's `$`-strip never normalizes —
75
+ // a known vector for receiver-binding lookup pollution. The two patterns
76
+ // produce separate rawMatches entries with separate `grouped` maps, so the
77
+ // dedup has to be cross-match: build the set here, then skip
78
+ // @declaration.variable matches whose anchor is in it (loop below).
79
+ const typedPropertyAnchorIds = new Set();
80
+ for (const m of rawMatches) {
81
+ for (const c of m.captures) {
82
+ if (c.name === 'declaration.property') {
83
+ typedPropertyAnchorIds.add(c.node.id);
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ for (const m of rawMatches) {
89
+ // Group captures by their tag name. Tree-sitter strips the leading
90
+ // `@`; we put it back so the central extractor's prefix lookups work.
91
+ const grouped = {};
92
+ for (const c of m.captures) {
93
+ const tag = '@' + c.name;
94
+ grouped[tag] = nodeToCapture(tag, c.node);
95
+ }
96
+ if (Object.keys(grouped).length === 0)
97
+ continue;
98
+ // Cross-match dedup for the typed-property double-match described above:
99
+ // skip @declaration.variable matches whose anchor was already captured as
100
+ // @declaration.property in an earlier match.
101
+ if (grouped['@declaration.variable'] !== undefined) {
102
+ const varCap = m.captures.find((c) => c.name === 'declaration.variable');
103
+ if (varCap !== undefined && typedPropertyAnchorIds.has(varCap.node.id))
104
+ continue;
105
+ }
106
+ // Normalize PHP property declarations: strip leading `$` from
107
+ // `@declaration.name` for @declaration.property matches. PHP stores
108
+ // field names WITHOUT the `$` sigil in the graph so that member access
109
+ // lookups like `$user->address` can find the property named `address`
110
+ // (not `$address`). `@type-binding.annotation` already strips `$` in
111
+ // `interpretPhpTypeBinding`; this mirrors that for the declaration side.
112
+ //
113
+ // Only applies to `@declaration.property` — typed class properties and
114
+ // constructor-promoted parameters. Untyped `@declaration.variable` keeps
115
+ // its `$` prefix (those defs are Variable type and not in the field
116
+ // registry, so their name doesn't affect member lookup).
117
+ if (grouped['@declaration.property'] !== undefined &&
118
+ grouped['@declaration.name'] !== undefined) {
119
+ const nameCap = grouped['@declaration.name'];
120
+ if (nameCap.text.startsWith('$')) {
121
+ grouped['@declaration.name'] = { ...nameCap, text: nameCap.text.slice(1) };
122
+ }
123
+ }
124
+ // Normalize PHP receiver expressions so the compound-receiver resolver
125
+ // can walk chains expressed with `->` (PHP) as if they used `.` (the
126
+ // resolver's canonical separator). Without this, `$user->address->save()`
127
+ // has receiver text `$user->address` — the resolver sees no `.` separator,
128
+ // treats it as a bare identifier, and cannot walk field types.
129
+ //
130
+ // Transformation applied to `@reference.receiver` captures:
131
+ // 1. Replace `->` with `.` ($user->address → $user.address)
132
+ // 2. Strip leading `$` from each segment ($user.address → user.address)
133
+ // 3. Strip trailing `?` on null-safe receivers ($user? → user)
134
+ //
135
+ // This is a PHP-local normalization — no shared pipeline code is changed.
136
+ if (grouped['@reference.receiver'] !== undefined) {
137
+ const recvCap = grouped['@reference.receiver'];
138
+ const normalized = normalizePhpReceiver(recvCap.text);
139
+ if (normalized !== recvCap.text) {
140
+ grouped['@reference.receiver'] = { ...recvCap, text: normalized };
141
+ }
142
+ }
143
+ // Normalize static property write: strip leading `$` from `@reference.name`
144
+ // so `User::$count` resolves to property `count` (stored without `$` in graph).
145
+ if (grouped['@reference.write.static'] !== undefined) {
146
+ const nameCap = grouped['@reference.name'];
147
+ if (nameCap !== undefined && nameCap.text.startsWith('$')) {
148
+ grouped['@reference.name'] = {
149
+ ...nameCap,
150
+ text: nameCap.text.slice(1),
151
+ };
152
+ }
153
+ // Re-tag as @reference.write.member so downstream passes see a uniform write kind.
154
+ grouped['@reference.write.member'] = grouped['@reference.write.static'];
155
+ delete grouped['@reference.write.static'];
156
+ }
157
+ // Decompose each `namespace_use_declaration` so `interpretPhpImport`
158
+ // sees the kind/source/name/alias markers it consumes.
159
+ if (grouped['@import.statement'] !== undefined) {
160
+ const stmtCapture = grouped['@import.statement'];
161
+ const stmtNode = findNodeAtRange(tree.rootNode, stmtCapture.range, 'namespace_use_declaration');
162
+ if (stmtNode !== null) {
163
+ const decomposed = splitNamespaceUseDeclaration(stmtNode);
164
+ if (decomposed.length > 0) {
165
+ for (const d of decomposed)
166
+ out.push(d);
167
+ continue;
168
+ }
169
+ }
170
+ // Defensive fallback: emit the raw match.
171
+ out.push(grouped);
172
+ continue;
173
+ }
174
+ // Synthesize `$this` / `parent` receiver type-bindings on every
175
+ // non-static method-like. Mirrors C#'s `this` / `base` synthesis.
176
+ if (grouped['@scope.function'] !== undefined) {
177
+ out.push(grouped);
178
+ const anchor = grouped['@scope.function'];
179
+ const fnNode = findFunctionNode(tree.rootNode, anchor.range);
180
+ if (fnNode !== null) {
181
+ for (const synth of synthesizePhpReceiverBinding(fnNode)) {
182
+ out.push(synth);
183
+ }
184
+ // Synthesize PHPDoc @param and @return type bindings for this fn.
185
+ for (const synth of synthesizePhpDocBindings(fnNode)) {
186
+ out.push(synth);
187
+ }
188
+ // Synthesize foreach loop variable bindings inside this fn body.
189
+ for (const synth of synthesizeForeachBindings(fnNode)) {
190
+ out.push(synth);
191
+ }
192
+ }
193
+ continue;
194
+ }
195
+ // Synthesize arity metadata on function-like declarations so the
196
+ // registry can narrow overloads.
197
+ const declTag = FUNCTION_DECL_TAGS.find((t) => grouped[t] !== undefined);
198
+ if (declTag !== undefined) {
199
+ const anchor = grouped[declTag];
200
+ const fnNode = findFunctionNode(tree.rootNode, anchor.range);
201
+ if (fnNode !== null) {
202
+ const arity = computePhpArityMetadata(fnNode);
203
+ if (arity.parameterCount !== undefined) {
204
+ grouped['@declaration.parameter-count'] = syntheticCapture('@declaration.parameter-count', fnNode, String(arity.parameterCount));
205
+ }
206
+ if (arity.requiredParameterCount !== undefined) {
207
+ grouped['@declaration.required-parameter-count'] = syntheticCapture('@declaration.required-parameter-count', fnNode, String(arity.requiredParameterCount));
208
+ }
209
+ if (arity.parameterTypes !== undefined) {
210
+ grouped['@declaration.parameter-types'] = syntheticCapture('@declaration.parameter-types', fnNode, JSON.stringify(arity.parameterTypes));
211
+ }
212
+ }
213
+ }
214
+ // Synthesize `@reference.arity` on every call site so the registry's
215
+ // arity filter can narrow overloads. Count the `argument` children of
216
+ // the backing `arguments` node. Mirrors C#'s pattern (csharp/captures.ts
217
+ // lines 149-186). PHP needs this for arity-based dispatch (Cluster H).
218
+ const callTag = ['@reference.call.free', '@reference.call.member', '@reference.call.constructor'].find((t) => grouped[t] !== undefined);
219
+ if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
220
+ const anchor = grouped[callTag];
221
+ const callNode = findNodeAtRange(tree.rootNode, anchor.range, 'function_call_expression') ??
222
+ findNodeAtRange(tree.rootNode, anchor.range, 'member_call_expression') ??
223
+ findNodeAtRange(tree.rootNode, anchor.range, 'nullsafe_member_call_expression') ??
224
+ findNodeAtRange(tree.rootNode, anchor.range, 'scoped_call_expression') ??
225
+ findNodeAtRange(tree.rootNode, anchor.range, 'object_creation_expression');
226
+ if (callNode !== null) {
227
+ const argList = callNode.childForFieldName('arguments');
228
+ const args = [];
229
+ if (argList !== null) {
230
+ for (let i = 0; i < argList.namedChildCount; i++) {
231
+ const child = argList.namedChild(i);
232
+ if (child !== null && child.type === 'argument')
233
+ args.push(child);
234
+ }
235
+ }
236
+ grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(args.length));
237
+ // Infer argument types from literal nodes for type-based narrowing.
238
+ // Non-literal arguments emit empty string ("unknown" = any-match).
239
+ const argTypes = args.map((arg) => inferPhpArgType(arg));
240
+ grouped['@reference.parameter-types'] = syntheticCapture('@reference.parameter-types', callNode, JSON.stringify(argTypes));
241
+ }
242
+ }
243
+ out.push(grouped);
244
+ }
245
+ return out;
246
+ }
247
+ /** Find the first PHP function-like node at the given range. */
248
+ function findFunctionNode(rootNode, range) {
249
+ for (const nodeType of FUNCTION_NODE_TYPES) {
250
+ const n = findNodeAtRange(rootNode, range, nodeType);
251
+ if (n !== null)
252
+ return n;
253
+ }
254
+ return null;
255
+ }
256
+ // ─── PHP receiver normalization ──────────────────────────────────────────────
257
+ /**
258
+ * Normalize a PHP receiver expression so the language-agnostic
259
+ * compound-receiver resolver (which splits on `.`) can walk field-type chains.
260
+ *
261
+ * The compound-receiver resolver:
262
+ * - splits on `.` to get chain segments
263
+ * - looks up the first segment in `typeBindings` (keyed with `$` for variables)
264
+ * - walks subsequent segments as field names (stored without `$` in the graph)
265
+ *
266
+ * Transformation:
267
+ * 1. Replace `->` and `?->` with `.` so the resolver's splitter works
268
+ * 2. Strip any bare `?` fragment left by null-safe chain ends
269
+ * 3. Strip `$` from all segments EXCEPT the first (which is a variable
270
+ * and must keep `$` for typeBindings lookup — e.g. `$user → User`)
271
+ *
272
+ * Examples:
273
+ * `$user` → `$user` (bare variable — unchanged)
274
+ * `$user->address` → `$user.address`
275
+ * `$user->address->city` → `$user.address.city`
276
+ * `$user?` → `$user` (null-safe trailing `?` stripped)
277
+ * `$this` → `$this` (receiverBinding uses `$this`)
278
+ * `parent` → `parent` (super-receiver check)
279
+ */
280
+ function normalizePhpReceiver(raw) {
281
+ // Keep `$this`, `parent`, and `self` as-is.
282
+ if (raw === '$this' || raw === 'parent' || raw === 'self')
283
+ return raw;
284
+ // Replace `?->` (null-safe) and plain `->` with `.`.
285
+ let text = raw.replace(/\?->/g, '.').replace(/->/g, '.');
286
+ // Strip a trailing `?` (null-safe fragment on the last object node).
287
+ text = text.replace(/\?$/, '');
288
+ // Collapse any doubled dots from `?->` where `?` was on its own.
289
+ text = text.replace(/\.{2,}/g, '.');
290
+ // Strip trailing dot.
291
+ text = text.replace(/\.$/, '');
292
+ // Split on `.` and strip `$` from all segments EXCEPT the first.
293
+ // The first segment is a PHP variable (typeBinding key includes `$`).
294
+ // Subsequent segments are property/method names (stored without `$`).
295
+ const segments = text.split('.');
296
+ for (let i = 1; i < segments.length; i++) {
297
+ const s = segments[i];
298
+ if (s !== undefined && s.startsWith('$'))
299
+ segments[i] = s.slice(1);
300
+ }
301
+ return segments.join('.');
302
+ }
303
+ // ─── PHP argument type inference ─────────────────────────────────────────────
304
+ /**
305
+ * Infer the PHP type of a call argument from its literal shape.
306
+ * Returns an empty string for non-literals (treated as "unknown" = any-match).
307
+ * Mirrors C#'s `inferArgType` helper.
308
+ */
309
+ function inferPhpArgType(argNode) {
310
+ // argument node wraps the actual expression
311
+ const expr = argNode.firstNamedChild ?? argNode;
312
+ switch (expr.type) {
313
+ case 'integer':
314
+ return 'int';
315
+ case 'float':
316
+ return 'float';
317
+ case 'string':
318
+ case 'encapsed_string':
319
+ case 'heredoc':
320
+ case 'nowdoc':
321
+ return 'string';
322
+ case 'boolean':
323
+ case 'true':
324
+ case 'false':
325
+ return 'bool';
326
+ case 'null':
327
+ return 'null';
328
+ default:
329
+ return '';
330
+ }
331
+ }
332
+ // ─── PHPDoc synthesis ─────────────────────────────────────────────────────────
333
+ /** PHP 8+ attribute_list nodes that appear between PHPDoc and method. */
334
+ const SKIP_SIBLING_TYPES = new Set(['attribute_list', 'attribute', 'comment']);
335
+ /** Regex for PHPDoc @param: standard `@param Type $name` */
336
+ const PHPDOC_PARAM_RE = /@param\s+(\S+)\s+\$(\w+)/g;
337
+ /** Regex for PHPDoc @param: alternate `@param $name Type` */
338
+ const PHPDOC_PARAM_ALT_RE = /@param\s+\$(\w+)\s+(\S+)/g;
339
+ /** Regex for PHPDoc @return: `@return Type` */
340
+ const PHPDOC_RETURN_RE = /@return\s+(\S+)/;
341
+ /**
342
+ * Normalize a PHP type string to a simple class name for binding purposes.
343
+ * Returns null for primitives or uninformative types.
344
+ * Mirrors `normalizePhpType` in `interpret.ts` but operates on raw PHPDoc strings.
345
+ */
346
+ function normalizePhpDocType(raw) {
347
+ let type = raw.trim();
348
+ // Strip nullable prefix
349
+ if (type.startsWith('?'))
350
+ type = type.slice(1).trim();
351
+ // Strip array suffix: User[] → User
352
+ if (type.endsWith('[]'))
353
+ type = type.slice(0, -2).trim();
354
+ // Strip union with null/false/void
355
+ if (type.includes('|')) {
356
+ const parts = type
357
+ .split('|')
358
+ .map((p) => p.trim())
359
+ .filter((p) => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed' && p !== '');
360
+ if (parts.length !== 1)
361
+ return null;
362
+ type = parts[0];
363
+ }
364
+ // Strip intersection: take first part
365
+ if (type.includes('&')) {
366
+ const first = type.split('&')[0].trim();
367
+ if (first === '')
368
+ return null;
369
+ type = first;
370
+ }
371
+ // Strip generic wrapper: Collection<User> → User
372
+ const genericMatch = type.match(/^\w[\w\\]*\s*<([^,<>]+)>$/);
373
+ if (genericMatch) {
374
+ type = genericMatch[1].trim();
375
+ // Strip array suffix again inside generic
376
+ if (type.endsWith('[]'))
377
+ type = type.slice(0, -2).trim();
378
+ }
379
+ // Strip namespace qualifier: \App\Models\User → User
380
+ if (type.includes('\\')) {
381
+ const segs = type.split('\\').filter(Boolean);
382
+ type = segs[segs.length - 1] ?? type;
383
+ }
384
+ // Reject primitives
385
+ if (PHP_PRIMITIVES.has(type.toLowerCase()))
386
+ return null;
387
+ // Must be a simple identifier
388
+ if (!/^\w+$/.test(type))
389
+ return null;
390
+ return type;
391
+ }
392
+ const PHP_PRIMITIVES = new Set([
393
+ 'int',
394
+ 'integer',
395
+ 'float',
396
+ 'double',
397
+ 'string',
398
+ 'bool',
399
+ 'boolean',
400
+ 'array',
401
+ 'object',
402
+ 'callable',
403
+ 'iterable',
404
+ 'null',
405
+ 'void',
406
+ 'never',
407
+ 'mixed',
408
+ 'false',
409
+ 'true',
410
+ 'self',
411
+ 'static',
412
+ 'parent',
413
+ ]);
414
+ /**
415
+ * Collect comment text from siblings immediately before `fnNode`.
416
+ * Skips PHP 8+ attribute_list nodes.
417
+ */
418
+ function collectPrecedingComments(fnNode) {
419
+ const texts = [];
420
+ let sibling = fnNode.previousSibling;
421
+ while (sibling !== null) {
422
+ if (sibling.type === 'comment') {
423
+ texts.unshift(sibling.text);
424
+ }
425
+ else if (sibling.isNamed && !SKIP_SIBLING_TYPES.has(sibling.type)) {
426
+ break;
427
+ }
428
+ sibling = sibling.previousSibling;
429
+ }
430
+ return texts.join('\n');
431
+ }
432
+ /**
433
+ * Synthesize PHPDoc @param and @return type-binding captures for a
434
+ * method_declaration or function_definition node.
435
+ *
436
+ * PHPDoc @param Type $name → `@type-binding.parameter` match (anchored at fn body/return_type).
437
+ * PHPDoc @return Type → `@type-binding.return` match (anchored at fn name).
438
+ */
439
+ function synthesizePhpDocBindings(fnNode) {
440
+ if (fnNode.type !== 'method_declaration' && fnNode.type !== 'function_definition')
441
+ return [];
442
+ const commentBlock = collectPrecedingComments(fnNode);
443
+ if (commentBlock === '')
444
+ return [];
445
+ const out = [];
446
+ // Anchor for parameter type-bindings: the function body (or return_type as fallback).
447
+ // The binding must be inside the function scope so it's visible to body statements.
448
+ const bodyNode = fnNode.childForFieldName('body');
449
+ const anchorNode = bodyNode ?? fnNode;
450
+ // ── @param annotations ────────────────────────────────────────────────────
451
+ PHPDOC_PARAM_RE.lastIndex = 0;
452
+ let m;
453
+ const seenParams = new Set();
454
+ while ((m = PHPDOC_PARAM_RE.exec(commentBlock)) !== null) {
455
+ const rawType = m[1];
456
+ const paramName = '$' + m[2];
457
+ const typeName = normalizePhpDocType(rawType);
458
+ if (typeName === null)
459
+ continue;
460
+ seenParams.add(paramName);
461
+ out.push({
462
+ '@type-binding.parameter': nodeToCapture('@type-binding.parameter', anchorNode),
463
+ '@type-binding.name': syntheticCapture('@type-binding.name', anchorNode, paramName),
464
+ '@type-binding.type': syntheticCapture('@type-binding.type', anchorNode, typeName),
465
+ });
466
+ }
467
+ // Also check alternate PHPDoc order: @param $name Type
468
+ PHPDOC_PARAM_ALT_RE.lastIndex = 0;
469
+ while ((m = PHPDOC_PARAM_ALT_RE.exec(commentBlock)) !== null) {
470
+ const paramName = '$' + m[1];
471
+ if (seenParams.has(paramName))
472
+ continue; // standard format takes priority
473
+ const rawType = m[2];
474
+ const typeName = normalizePhpDocType(rawType);
475
+ if (typeName === null)
476
+ continue;
477
+ out.push({
478
+ '@type-binding.parameter': nodeToCapture('@type-binding.parameter', anchorNode),
479
+ '@type-binding.name': syntheticCapture('@type-binding.name', anchorNode, paramName),
480
+ '@type-binding.type': syntheticCapture('@type-binding.type', anchorNode, typeName),
481
+ });
482
+ }
483
+ // ── @return annotation ────────────────────────────────────────────────────
484
+ const returnMatch = PHPDOC_RETURN_RE.exec(commentBlock);
485
+ if (returnMatch !== null) {
486
+ const rawType = returnMatch[1];
487
+ const typeName = normalizePhpDocType(rawType);
488
+ if (typeName !== null) {
489
+ // @return bindings must be anchored at the method name and hoisted to Module scope
490
+ // by phpBindingScopeFor (which checks for @type-binding.return presence).
491
+ // Use the function_definition/method_declaration node itself as the anchor — it
492
+ // coincides with the innermost scope's range, so auto-hoist kicks in.
493
+ const nameNode = fnNode.childForFieldName('name') ?? fnNode;
494
+ out.push({
495
+ '@type-binding.return': nodeToCapture('@type-binding.return', fnNode),
496
+ '@type-binding.name': syntheticCapture('@type-binding.name', nameNode, nameNode.text),
497
+ '@type-binding.type': syntheticCapture('@type-binding.type', nameNode, typeName),
498
+ });
499
+ }
500
+ }
501
+ return out;
502
+ }
503
+ // ─── Foreach synthesis ───────────────────────────────────────────────────────
504
+ /**
505
+ * Walk all `foreach_statement` nodes inside `fnNode` and synthesize
506
+ * `@type-binding.alias` captures binding the loop variable to the
507
+ * element type of the iterable.
508
+ *
509
+ * Supports:
510
+ * - `foreach ($users as $user)` — simple iterable variable
511
+ * - `foreach ($users as $k => $user)` — key→value pair
512
+ * - `foreach ($this->users as $user)` — member access iterable
513
+ * - `foreach (getUsers() as $user)` — NOT yet supported (needs return type)
514
+ *
515
+ * The element type is resolved by:
516
+ * 1. Looking up the iterable name in PHPDoc @param bindings already
517
+ * collected for this function (passed via typeBindingsByName).
518
+ * 2. Direct resolution when iterable's env type IS the element type
519
+ * (because PHPDoc normalizes `User[]` → `User` already).
520
+ */
521
+ function synthesizeForeachBindings(fnNode) {
522
+ if (fnNode.type !== 'method_declaration' &&
523
+ fnNode.type !== 'function_definition' &&
524
+ fnNode.type !== 'anonymous_function' &&
525
+ fnNode.type !== 'arrow_function') {
526
+ return [];
527
+ }
528
+ const out = [];
529
+ // Build a mini type map from the function's PHPDoc @param annotations.
530
+ // This is re-parsed here (not cached from synthesizePhpDocBindings) for simplicity;
531
+ // the cost is negligible given the small comment sizes.
532
+ const commentBlock = collectPrecedingComments(fnNode);
533
+ const paramTypeMap = buildParamTypeMap(commentBlock);
534
+ // Walk the function body for foreach_statement nodes.
535
+ const bodyNode = fnNode.childForFieldName('body');
536
+ if (bodyNode === null)
537
+ return [];
538
+ collectForeachBindings(bodyNode, fnNode, paramTypeMap, out);
539
+ return out;
540
+ }
541
+ /** Build a map of `$paramName → elementTypeName` from PHPDoc @param in a comment block. */
542
+ function buildParamTypeMap(commentBlock) {
543
+ const map = new Map();
544
+ if (commentBlock === '')
545
+ return map;
546
+ PHPDOC_PARAM_RE.lastIndex = 0;
547
+ let m;
548
+ while ((m = PHPDOC_PARAM_RE.exec(commentBlock)) !== null) {
549
+ const rawType = m[1];
550
+ const paramName = '$' + m[2];
551
+ const typeName = normalizePhpDocType(rawType);
552
+ if (typeName !== null)
553
+ map.set(paramName, typeName);
554
+ }
555
+ PHPDOC_PARAM_ALT_RE.lastIndex = 0;
556
+ while ((m = PHPDOC_PARAM_ALT_RE.exec(commentBlock)) !== null) {
557
+ const paramName = '$' + m[1];
558
+ if (map.has(paramName))
559
+ continue;
560
+ const rawType = m[2];
561
+ const typeName = normalizePhpDocType(rawType);
562
+ if (typeName !== null)
563
+ map.set(paramName, typeName);
564
+ }
565
+ return map;
566
+ }
567
+ /**
568
+ * Walk a subtree and collect foreach_statement bindings.
569
+ * Recursively descends into all child nodes.
570
+ */
571
+ function collectForeachBindings(node, fnNode, paramTypeMap, out) {
572
+ if (node.type === 'foreach_statement') {
573
+ const synth = synthesizeSingleForeach(node, fnNode, paramTypeMap);
574
+ if (synth !== null)
575
+ out.push(synth);
576
+ }
577
+ for (let i = 0; i < node.childCount; i++) {
578
+ const child = node.child(i);
579
+ if (child !== null) {
580
+ collectForeachBindings(child, fnNode, paramTypeMap, out);
581
+ }
582
+ }
583
+ }
584
+ /**
585
+ * Synthesize a single `@type-binding.alias` match for a `foreach_statement`.
586
+ *
587
+ * AST structure for foreach_statement (tree-sitter-php):
588
+ * foreach ( <iterable> as <value_or_pair> ) <body>
589
+ * Named children (excluding body): first = iterable, second = value or pair.
590
+ */
591
+ function synthesizeSingleForeach(foreachNode, fnNode, paramTypeMap) {
592
+ // Collect non-body named children: [iterable, value_or_pair]
593
+ const bodyNode = foreachNode.childForFieldName('body');
594
+ const children = [];
595
+ for (let i = 0; i < foreachNode.namedChildCount; i++) {
596
+ const child = foreachNode.namedChild(i);
597
+ if (child !== null && child !== bodyNode)
598
+ children.push(child);
599
+ }
600
+ if (children.length < 2)
601
+ return null;
602
+ const iterableNode = children[0];
603
+ const valueOrPair = children[1];
604
+ // Determine the loop variable node
605
+ let loopVarNode;
606
+ if (valueOrPair.type === 'pair') {
607
+ // $key => $value — use the last named child of the pair
608
+ const lastChild = valueOrPair.namedChild(valueOrPair.namedChildCount - 1);
609
+ if (lastChild === null)
610
+ return null;
611
+ loopVarNode =
612
+ lastChild.type === 'by_ref' ? (lastChild.firstNamedChild ?? lastChild) : lastChild;
613
+ }
614
+ else {
615
+ loopVarNode =
616
+ valueOrPair.type === 'by_ref' ? (valueOrPair.firstNamedChild ?? valueOrPair) : valueOrPair;
617
+ }
618
+ // Loop variable must be a variable_name
619
+ if (loopVarNode.type !== 'variable_name')
620
+ return null;
621
+ const loopVarName = loopVarNode.text; // e.g. '$user'
622
+ // Resolve the element type from the iterable
623
+ let elementType = null;
624
+ if (iterableNode.type === 'variable_name') {
625
+ // foreach ($users as $user) — look up $users in param map
626
+ const iterableName = iterableNode.text; // e.g. '$users'
627
+ elementType = paramTypeMap.get(iterableName) ?? null;
628
+ }
629
+ else if (iterableNode.type === 'member_access_expression') {
630
+ // foreach ($this->users as $user) — property name is the field
631
+ const propNameNode = iterableNode.childForFieldName('name');
632
+ if (propNameNode !== null) {
633
+ // Property stored with $ prefix in paramTypeMap (rare for $this->prop patterns)
634
+ // Try both with and without $ prefix
635
+ const propKey = '$' + propNameNode.text;
636
+ elementType = paramTypeMap.get(propKey) ?? null;
637
+ if (elementType === null) {
638
+ // Try to find the property type from the enclosing class
639
+ elementType = findClassPropertyElementType(iterableNode, fnNode);
640
+ }
641
+ }
642
+ }
643
+ else if (iterableNode.type === 'function_call_expression') {
644
+ // foreach (getUsers() as $user) — use the function name as a type alias.
645
+ // The function's @return annotation produces a @type-binding.return binding
646
+ // in the Module scope (e.g. getUsers → User). The scope-extractor's
647
+ // followChainedRef will resolve $user → getUsers → User.
648
+ const funcNode = iterableNode.childForFieldName('function');
649
+ if (funcNode !== null && funcNode.type === 'name') {
650
+ elementType = funcNode.text; // e.g. 'getUsers' — chain will be resolved later
651
+ }
652
+ }
653
+ else if (iterableNode.type === 'member_call_expression') {
654
+ // foreach ($this->getUsers() as $user) — use the method name as a type alias.
655
+ const methodNameNode = iterableNode.childForFieldName('name');
656
+ if (methodNameNode !== null) {
657
+ elementType = methodNameNode.text; // e.g. 'getUsers'
658
+ }
659
+ }
660
+ if (elementType === null)
661
+ return null;
662
+ // Anchor the binding inside the foreach body so it's scoped to the loop.
663
+ const anchorNode = bodyNode ?? foreachNode;
664
+ return {
665
+ '@type-binding.alias': nodeToCapture('@type-binding.alias', anchorNode),
666
+ '@type-binding.name': syntheticCapture('@type-binding.name', anchorNode, loopVarName),
667
+ '@type-binding.type': syntheticCapture('@type-binding.type', anchorNode, elementType),
668
+ };
669
+ }
670
+ /**
671
+ * Try to find the element type for `$this->property` member access by walking
672
+ * up from the foreach to the enclosing class and scanning the property declaration.
673
+ */
674
+ function findClassPropertyElementType(memberAccessNode, fnNode) {
675
+ const propNameNode = memberAccessNode.childForFieldName('name');
676
+ if (propNameNode === null)
677
+ return null;
678
+ const propName = propNameNode.text;
679
+ // Walk up from fnNode to find the enclosing class declaration
680
+ let cur = fnNode.parent;
681
+ while (cur !== null) {
682
+ if (cur.type === 'class_declaration' || cur.type === 'trait_declaration') {
683
+ break;
684
+ }
685
+ cur = cur.parent;
686
+ }
687
+ if (cur === null)
688
+ return null;
689
+ // Find the property_declaration with matching variable_name '$propName'
690
+ const declList = cur.childForFieldName('body');
691
+ if (declList === null)
692
+ return null;
693
+ for (let i = 0; i < declList.namedChildCount; i++) {
694
+ const child = declList.namedChild(i);
695
+ if (child === null || child.type !== 'property_declaration')
696
+ continue;
697
+ for (let j = 0; j < child.namedChildCount; j++) {
698
+ const elem = child.namedChild(j);
699
+ if (elem === null || elem.type !== 'property_element')
700
+ continue;
701
+ const varNameNode = elem.firstNamedChild;
702
+ if (varNameNode === null || varNameNode.text !== '$' + propName)
703
+ continue;
704
+ // Found the property — get its element type from @var PHPDoc or native type
705
+ return extractPropertyElementType(child);
706
+ }
707
+ }
708
+ return null;
709
+ }
710
+ /** Regex for PHPDoc @var: `@var Type` */
711
+ const PHPDOC_VAR_RE = /@var\s+(\S+)/;
712
+ /**
713
+ * Extract element type from a property_declaration node:
714
+ * 1. PHPDoc @var annotation on a preceding comment sibling
715
+ * 2. PHP 7.4+ native type field (non-array)
716
+ */
717
+ function extractPropertyElementType(propDecl) {
718
+ // Strategy 1: PHPDoc @var on a preceding comment sibling
719
+ let sibling = propDecl.previousSibling;
720
+ while (sibling !== null) {
721
+ if (sibling.type === 'comment') {
722
+ const m = PHPDOC_VAR_RE.exec(sibling.text);
723
+ if (m !== null)
724
+ return normalizePhpDocType(m[1]);
725
+ }
726
+ else if (sibling.isNamed && !SKIP_SIBLING_TYPES.has(sibling.type)) {
727
+ break;
728
+ }
729
+ sibling = sibling.previousSibling;
730
+ }
731
+ // Strategy 2: native type field — skip generic 'array'
732
+ const typeNode = propDecl.childForFieldName('type');
733
+ if (typeNode === null)
734
+ return null;
735
+ const typeName = typeNode.text.trim();
736
+ if (typeName === 'array' || typeName === '')
737
+ return null;
738
+ return normalizePhpDocType(typeName);
739
+ }