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.
- package/README.md +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +26 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.js +22 -3
- package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
- package/dist/_shared/scope-resolution/registries/lookup-core.js +18 -1
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -1
- package/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +30 -12
- package/dist/cli/analyze.d.ts +18 -0
- package/dist/cli/analyze.js +33 -4
- package/dist/cli/augment.d.ts +1 -1
- package/dist/cli/augment.js +1 -1
- package/dist/cli/index.js +6 -1
- package/dist/cli/setup.js +6 -0
- package/dist/core/augmentation/engine.d.ts +2 -2
- package/dist/core/augmentation/engine.js +26 -6
- package/dist/core/embeddings/ast-utils.js +2 -1
- package/dist/core/embeddings/http-client.js +22 -6
- package/dist/core/group/extractors/grpc-extractor.js +2 -1
- package/dist/core/group/extractors/http-patterns/python.js +175 -0
- package/dist/core/group/extractors/http-route-extractor.js +2 -1
- package/dist/core/group/extractors/include-extractor.js +2 -1
- package/dist/core/group/extractors/thrift-extractor.js +2 -1
- package/dist/core/group/extractors/tree-sitter-scanner.js +2 -1
- package/dist/core/incremental/shadow-candidates.d.ts +44 -0
- package/dist/core/incremental/shadow-candidates.js +74 -0
- package/dist/core/incremental/subgraph-extract.d.ts +64 -0
- package/dist/core/incremental/subgraph-extract.js +111 -0
- package/dist/core/ingestion/call-processor.js +166 -47
- package/dist/core/ingestion/community-processor.js +18 -0
- package/dist/core/ingestion/heritage-processor.js +3 -2
- package/dist/core/ingestion/import-processor.js +2 -1
- package/dist/core/ingestion/import-resolvers/standard.d.ts +5 -0
- package/dist/core/ingestion/import-resolvers/standard.js +27 -1
- package/dist/core/ingestion/import-resolvers/utils.js +4 -0
- package/dist/core/ingestion/languages/c/arity-metadata.d.ts +14 -0
- package/dist/core/ingestion/languages/c/arity-metadata.js +94 -0
- package/dist/core/ingestion/languages/c/arity.d.ts +6 -0
- package/dist/core/ingestion/languages/c/arity.js +18 -0
- package/dist/core/ingestion/languages/c/captures.d.ts +2 -0
- package/dist/core/ingestion/languages/c/captures.js +105 -0
- package/dist/core/ingestion/languages/c/header-scan.d.ts +7 -0
- package/dist/core/ingestion/languages/c/header-scan.js +55 -0
- package/dist/core/ingestion/languages/c/import-decomposer.d.ts +8 -0
- package/dist/core/ingestion/languages/c/import-decomposer.js +50 -0
- package/dist/core/ingestion/languages/c/import-target.d.ts +14 -0
- package/dist/core/ingestion/languages/c/import-target.js +57 -0
- package/dist/core/ingestion/languages/c/index.d.ts +11 -0
- package/dist/core/ingestion/languages/c/index.js +11 -0
- package/dist/core/ingestion/languages/c/interpret.d.ts +14 -0
- package/dist/core/ingestion/languages/c/interpret.js +48 -0
- package/dist/core/ingestion/languages/c/merge-bindings.d.ts +7 -0
- package/dist/core/ingestion/languages/c/merge-bindings.js +23 -0
- package/dist/core/ingestion/languages/c/query.d.ts +3 -0
- package/dist/core/ingestion/languages/c/query.js +161 -0
- package/dist/core/ingestion/languages/c/scope-resolver.d.ts +13 -0
- package/dist/core/ingestion/languages/c/scope-resolver.js +60 -0
- package/dist/core/ingestion/languages/c/simple-hooks.d.ts +14 -0
- package/dist/core/ingestion/languages/c/simple-hooks.js +19 -0
- package/dist/core/ingestion/languages/c/static-linkage.d.ts +13 -0
- package/dist/core/ingestion/languages/c/static-linkage.js +57 -0
- package/dist/core/ingestion/languages/c-cpp.js +10 -0
- package/dist/core/ingestion/languages/csharp/captures.js +2 -1
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +3 -2
- package/dist/core/ingestion/languages/go/captures.js +2 -1
- package/dist/core/ingestion/languages/go/range-binding.js +2 -1
- package/dist/core/ingestion/languages/java/arity-metadata.d.ts +18 -0
- package/dist/core/ingestion/languages/java/arity-metadata.js +40 -0
- package/dist/core/ingestion/languages/java/arity.d.ts +10 -0
- package/dist/core/ingestion/languages/java/arity.js +24 -0
- package/dist/core/ingestion/languages/java/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/java/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/java/captures.d.ts +17 -0
- package/dist/core/ingestion/languages/java/captures.js +187 -0
- package/dist/core/ingestion/languages/java/import-decomposer.d.ts +18 -0
- package/dist/core/ingestion/languages/java/import-decomposer.js +85 -0
- package/dist/core/ingestion/languages/java/import-target.d.ts +17 -0
- package/dist/core/ingestion/languages/java/import-target.js +100 -0
- package/dist/core/ingestion/languages/java/index.d.ts +29 -0
- package/dist/core/ingestion/languages/java/index.js +29 -0
- package/dist/core/ingestion/languages/java/interpret.d.ts +13 -0
- package/dist/core/ingestion/languages/java/interpret.js +131 -0
- package/dist/core/ingestion/languages/java/merge-bindings.d.ts +12 -0
- package/dist/core/ingestion/languages/java/merge-bindings.js +40 -0
- package/dist/core/ingestion/languages/java/query.d.ts +30 -0
- package/dist/core/ingestion/languages/java/query.js +192 -0
- package/dist/core/ingestion/languages/java/receiver-binding.d.ts +11 -0
- package/dist/core/ingestion/languages/java/receiver-binding.js +95 -0
- package/dist/core/ingestion/languages/java/scope-resolver.d.ts +50 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +74 -0
- package/dist/core/ingestion/languages/java/simple-hooks.d.ts +13 -0
- package/dist/core/ingestion/languages/java/simple-hooks.js +34 -0
- package/dist/core/ingestion/languages/java.js +11 -0
- package/dist/core/ingestion/languages/php/arity-metadata.d.ts +28 -0
- package/dist/core/ingestion/languages/php/arity-metadata.js +63 -0
- package/dist/core/ingestion/languages/php/arity.d.ts +25 -0
- package/dist/core/ingestion/languages/php/arity.js +40 -0
- package/dist/core/ingestion/languages/php/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/php/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/php/captures.d.ts +34 -0
- package/dist/core/ingestion/languages/php/captures.js +739 -0
- package/dist/core/ingestion/languages/php/import-decomposer.d.ts +28 -0
- package/dist/core/ingestion/languages/php/import-decomposer.js +265 -0
- package/dist/core/ingestion/languages/php/import-target.d.ts +47 -0
- package/dist/core/ingestion/languages/php/import-target.js +100 -0
- package/dist/core/ingestion/languages/php/index.d.ts +68 -0
- package/dist/core/ingestion/languages/php/index.js +72 -0
- package/dist/core/ingestion/languages/php/interpret.d.ts +36 -0
- package/dist/core/ingestion/languages/php/interpret.js +241 -0
- package/dist/core/ingestion/languages/php/merge-bindings.d.ts +19 -0
- package/dist/core/ingestion/languages/php/merge-bindings.js +47 -0
- package/dist/core/ingestion/languages/php/namespace-siblings.d.ts +51 -0
- package/dist/core/ingestion/languages/php/namespace-siblings.js +288 -0
- package/dist/core/ingestion/languages/php/query.d.ts +32 -0
- package/dist/core/ingestion/languages/php/query.js +326 -0
- package/dist/core/ingestion/languages/php/receiver-binding.d.ts +36 -0
- package/dist/core/ingestion/languages/php/receiver-binding.js +128 -0
- package/dist/core/ingestion/languages/php/scope-resolver.d.ts +23 -0
- package/dist/core/ingestion/languages/php/scope-resolver.js +358 -0
- package/dist/core/ingestion/languages/php/simple-hooks.d.ts +42 -0
- package/dist/core/ingestion/languages/php/simple-hooks.js +111 -0
- package/dist/core/ingestion/languages/php.d.ts +0 -7
- package/dist/core/ingestion/languages/php.js +15 -0
- package/dist/core/ingestion/languages/python/captures.js +2 -1
- package/dist/core/ingestion/languages/typescript/captures.js +2 -1
- package/dist/core/ingestion/parsing-processor.d.ts +22 -2
- package/dist/core/ingestion/parsing-processor.js +85 -50
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +5 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +119 -11
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +14 -0
- package/dist/core/ingestion/pipeline.d.ts +13 -0
- package/dist/core/ingestion/registry-primary-flag.js +2 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +65 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +1 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +12 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +5 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +25 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +79 -7
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +7 -3
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +24 -4
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +21 -2
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +13 -1
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +6 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +31 -7
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +10 -0
- package/dist/core/ingestion/workers/parse-worker.js +2 -1
- package/dist/core/lbug/lbug-adapter.d.ts +26 -0
- package/dist/core/lbug/lbug-adapter.js +174 -40
- package/dist/core/lbug/pool-adapter.js +19 -2
- package/dist/core/run-analyze.d.ts +2 -0
- package/dist/core/run-analyze.js +323 -29
- package/dist/core/tree-sitter/safe-parse.d.ts +6 -0
- package/dist/core/tree-sitter/safe-parse.js +32 -0
- package/dist/server/git-clone.js +10 -13
- package/dist/storage/file-hash.d.ts +47 -0
- package/dist/storage/file-hash.js +86 -0
- package/dist/storage/git.d.ts +17 -6
- package/dist/storage/git.js +46 -12
- package/dist/storage/parse-cache.d.ts +67 -0
- package/dist/storage/parse-cache.js +182 -0
- package/dist/storage/repo-manager.d.ts +41 -1
- package/dist/storage/repo-manager.js +17 -2
- package/hooks/claude/gitnexus-hook.cjs +8 -1
- package/hooks/claude/hook-lock.cjs +119 -0
- package/package.json +1 -1
- package/web/assets/{agent-BpHQleHE.js → agent-BG6RCOzz.js} +1 -1
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-F-tujre-.js +1 -0
- package/web/assets/architectureDiagram-UL44E2DR-2GL7nOvO.js +36 -0
- package/web/assets/blockDiagram-7IZFK4PR-5AjX0pPU.js +132 -0
- package/web/assets/{c4Diagram-DFAF54RM-DeJV1rCn.js → c4Diagram-Y2BXMSZH-hLOeq7xC.js} +2 -2
- package/web/assets/chunk-2T2R6R2M-B_pq_2nf.js +4 -0
- package/web/assets/chunk-2UTLFMKG-CpoohCFN.js +1 -0
- package/web/assets/{chunk-7RZVMHOQ-BdIU-RGO.js → chunk-3SSMPTDK-CfqpR3Nr.js} +1 -1
- package/web/assets/chunk-4R4BOZG6-D3gm7tYj.js +159 -0
- package/web/assets/chunk-6764PJDD-WcI965eO.js +1 -0
- package/web/assets/chunk-AQ6EADP3-BpMj3rQl.js +1 -0
- package/web/assets/{chunk-KSICW3F5-C2tZmXwv.js → chunk-AZZRMDJM-DSoCJlUA.js} +1 -1
- package/web/assets/chunk-C62D2QBJ-CvFF2PnB.js +1 -0
- package/web/assets/chunk-CEXFNPSA-DdNtJtrF.js +1 -0
- package/web/assets/chunk-J5EP6P6S-DtcVaAJI.js +1 -0
- package/web/assets/{chunk-AEOMTBSW-D7qjBMHW.js → chunk-JQRUD6KW-DElaNjc0.js} +1 -1
- package/web/assets/chunk-KGYTTC2M-D0K5or7e.js +1 -0
- package/web/assets/chunk-KRXBNO2N-DpR8iDt8.js +1 -0
- package/web/assets/chunk-LCXTWHL2-DdMObOEk.js +231 -0
- package/web/assets/chunk-LII3EMHJ-I78zXtzi.js +1 -0
- package/web/assets/chunk-RERM46MO-Betu1cSy.js +1 -0
- package/web/assets/chunk-RG4AUYOV-BUCJ2JP5.js +206 -0
- package/web/assets/chunk-RKZBBQEN-Cutpza0t.js +1 -0
- package/web/assets/chunk-RLI5ZMPA-BFUxCvX2.js +1 -0
- package/web/assets/chunk-T5OCTHI4-DKwlkuvB.js +1 -0
- package/web/assets/chunk-UP6H54XL-0YWa0NDg.js +1 -0
- package/web/assets/chunk-UXSXWOXI-CSz0OXpz.js +1 -0
- package/web/assets/chunk-W44A43WB-AOmgoT-X.js +1 -0
- package/web/assets/{chunk-RWUO3TPN-DTWzAhQ1.js → chunk-ZXARS5L4-DIzgxjMw.js} +1 -1
- package/web/assets/classDiagram-KGZ6W3CR-BT-rn7LF.js +1 -0
- package/web/assets/classDiagram-v2-72OJOZXJ-uR3nDadY.js +1 -0
- package/web/assets/{cose-bilkent-PNC4W37J-DdZq7Qz_.js → cose-bilkent-UX7MHV2Q-DCSCD1TK.js} +1 -1
- package/web/assets/dagre-ND4H6XIP-CsciHSij.js +4 -0
- package/web/assets/diagram-3NCE3AQN-BKUT5Rpc.js +43 -0
- package/web/assets/diagram-GF46GFSD-Dxb4hZSo.js +24 -0
- package/web/assets/diagram-HNR7UZ2L-D3qus9Cn.js +3 -0
- package/web/assets/diagram-QXG6HAR7-r9BMLGGP.js +24 -0
- package/web/assets/diagram-WEQXMOUZ-BGXqs7-m.js +10 -0
- package/web/assets/{erDiagram-GCSMX5X6-DfdN6iqF.js → erDiagram-L5TCEMPS-LgSWVrkZ.js} +2 -2
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-By7r3bla.js +1 -0
- package/web/assets/flowDiagram-H6V6AXG4-Bj_vV5ky.js +162 -0
- package/web/assets/ganttDiagram-JCBTUEKG-B1eVITSh.js +292 -0
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-ClEP6RdE.js +1 -0
- package/web/assets/gitGraphDiagram-S2ZK5IYY-BP0R-_FD.js +106 -0
- package/web/assets/{index-3NUCUW2s.js → index-oL-ZNYwb.js} +80 -81
- package/web/assets/info-J43DQDTF-KCYPFFUO-bBJRjmP5.js +1 -0
- package/web/assets/infoDiagram-3YFTVSEB-Dmj4OEO3.js +2 -0
- package/web/assets/{ishikawaDiagram-YMYX4NHK-BF8AtDaj.js → ishikawaDiagram-BNXS4ZKH-B3NgQ4lJ.js} +2 -2
- package/web/assets/{journeyDiagram-SO5T7YLQ-BW8oKa3N.js → journeyDiagram-M6C3CM3L-DALmL4_N.js} +3 -3
- package/web/assets/{kanban-definition-LJHFXRCJ-DzbOsR-2.js → kanban-definition-75IXJCU3-AvVbHpnN.js} +4 -4
- package/web/assets/{katex-GD7MH7QM-CJiOjBBJ.js → katex-K3KEBU37-CqF6_ZN-.js} +1 -1
- package/web/assets/{mindmap-definition-2EUWGEK5-BIXGQALq.js → mindmap-definition-2TDM6QVE-B5c9VOzs.js} +28 -28
- package/web/assets/packet-YPE3B663-LP52Z2RK-1ZCldnu3.js +1 -0
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-EhjHC5EE.js +1 -0
- package/web/assets/pieDiagram-CU6KROY3-Po2HSSR4.js +30 -0
- package/web/assets/quadrantDiagram-VICAPDV7-C47UxSZD.js +7 -0
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-CvXjgxlW.js +1 -0
- package/web/assets/{requirementDiagram-M5DCFWZL-CzUUrhQD.js → requirementDiagram-JXO7QTGE-CpECN_Hq.js} +6 -6
- package/web/assets/sankeyDiagram-URQDO5SZ-B3b7cLrS.js +40 -0
- package/web/assets/sequenceDiagram-VS2MUI6T-CRJ95oVP.js +162 -0
- package/web/assets/stateDiagram-7D4R322I-C7U9b3ks.js +1 -0
- package/web/assets/stateDiagram-v2-36443NZ5-BQajbtov.js +1 -0
- package/web/assets/{timeline-definition-5SPVSISX-DkMiGj4Z.js → timeline-definition-O6YCAMPW-rtbk6BBo.js} +4 -4
- package/web/assets/treeView-BLDUP644-QA4HXRO3-R2o5gwfP.js +1 -0
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-Cesmac_u.js +1 -0
- package/web/assets/{vennDiagram-IE5QUKF5-BCxMmf9q.js → vennDiagram-MWXL3ELB-W8Yoaayh.js} +3 -3
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-B9HYoRgA.js +1 -0
- package/web/assets/wardleyDiagram-CUQ6CDDI-DIZw5Abj.js +78 -0
- package/web/assets/{xychartDiagram-ZHJ5623Y-DZ7fP1_3.js → xychartDiagram-N2JHSOCM-DGRtrj4P.js} +3 -3
- package/web/index.html +2 -2
- package/web/assets/architecture-YZFGNWBL-S5CXDPWN-BUzkWgDl.js +0 -1
- package/web/assets/architectureDiagram-EMZXCZ2Q-CXbzok1G.js +0 -36
- package/web/assets/blockDiagram-IGV67L2C-DA1QJ4ZB.js +0 -132
- package/web/assets/chunk-3GS5O3IE-B8hpnia8.js +0 -231
- package/web/assets/chunk-3YCYZ6SJ-B-BVu21C.js +0 -1
- package/web/assets/chunk-6NTNNK5N-DW-jYSDr.js +0 -1
- package/web/assets/chunk-A34GCYZU-BbZl-IKy.js +0 -1
- package/web/assets/chunk-DJ7UZH7F-B5tMfF87.js +0 -1
- package/web/assets/chunk-DKKBVRCY-Djm-6K_e.js +0 -4
- package/web/assets/chunk-DU5LTGQ6-QPoynYcp.js +0 -1
- package/web/assets/chunk-FXACKDTF-jieSmbe2.js +0 -159
- package/web/assets/chunk-H3VCZNTA-8EMHZ3SV.js +0 -1
- package/web/assets/chunk-HN6EAY2L-CgE_agsD.js +0 -1
- package/web/assets/chunk-O5ABG6QK-DEZV1O19.js +0 -1
- package/web/assets/chunk-PK6DOVAG-BXv0JnsJ.js +0 -206
- package/web/assets/chunk-RNJOYNJ4-BiBkF9dN.js +0 -1
- package/web/assets/chunk-TBF5ZNIQ-BSl6s9lT.js +0 -1
- package/web/assets/chunk-TU3PZOEN-DE5Qhc0N.js +0 -1
- package/web/assets/chunk-TYMNRAUI-D1NDLIMG.js +0 -1
- package/web/assets/chunk-VELTKBKT-C9dVN39o.js +0 -1
- package/web/assets/chunk-W7ZLLLMY-Bu-cdLzs.js +0 -1
- package/web/assets/chunk-WSB5WSVC-Dor71SOz.js +0 -1
- package/web/assets/chunk-XGPFEOL4-CUonLRle.js +0 -1
- package/web/assets/classDiagram-PPOCWD7C-Cdd6UdVv.js +0 -1
- package/web/assets/classDiagram-v2-23LJLIIU-DNhxY2Ib.js +0 -1
- package/web/assets/dagre-E77IOHMT-Cfj_6e17.js +0 -4
- package/web/assets/diagram-H7BISOXX-BNvpsrBv.js +0 -43
- package/web/assets/diagram-JC5VWROH-Cn0rtDbJ.js +0 -24
- package/web/assets/diagram-LXUTUG65-CHfoVKNm.js +0 -10
- package/web/assets/diagram-WEHSV5V5-BLxHksnu.js +0 -24
- package/web/assets/flowDiagram-OTCZ4VVT-aAZw4c_M.js +0 -162
- package/web/assets/ganttDiagram-MUNLMDZQ-BEfC_7Or.js +0 -292
- package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-B6ZeRLP0.js +0 -1
- package/web/assets/gitGraphDiagram-3HKGZ4G3-CG0kC8Q5.js +0 -106
- package/web/assets/info-OMHHGYJF-BF2H5H6G-BIOsZy0W.js +0 -1
- package/web/assets/infoDiagram-MN7RKWGX-DUoMH-pq.js +0 -2
- package/web/assets/packet-4T2RLAQJ-EV4IVRXR-Bs7ysNMU.js +0 -1
- package/web/assets/pie-ZZUOXDRM-N23DN5KN-Csi8nnbH.js +0 -1
- package/web/assets/pieDiagram-3IATQBI2-QY-u7P6a.js +0 -30
- package/web/assets/quadrantDiagram-E256RVCF-Di4aNiU8.js +0 -7
- package/web/assets/radar-PYXPWWZC-P6TP7ZYP-DJlyXh2-.js +0 -1
- package/web/assets/sankeyDiagram-L3NBLAOT-D2fbMiIU.js +0 -10
- package/web/assets/sequenceDiagram-ZOUHS735-BNdkqguh.js +0 -157
- package/web/assets/stateDiagram-MLPALWAM-51_Q2w2L.js +0 -1
- package/web/assets/stateDiagram-v2-B5LQ5ZB2-8nHs2aNE.js +0 -1
- package/web/assets/treeView-SZITEDCU-5DXDK3XO-Cy6P0_CZ.js +0 -1
- package/web/assets/treemap-W4RFUUIX-WYLRDWKO-C_pp1R7A.js +0 -1
- package/web/assets/wardley-RL74JXVD-BCRCBASE-CMStJhpn.js +0 -1
- package/web/assets/wardleyDiagram-XU3VSMPF-ijlLF77s.js +0 -20
- /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
|
+
}
|