gitnexus 1.6.8-rc.9 → 1.6.8
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 +3 -3
- package/dist/_shared/graph/types.d.ts +31 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +11 -1
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +6 -0
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +6 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/cli/ai-context.d.ts +31 -1
- package/dist/cli/ai-context.js +18 -16
- package/dist/cli/analyze-config.js +11 -0
- package/dist/cli/analyze.d.ts +14 -0
- package/dist/cli/analyze.js +98 -0
- package/dist/cli/embedding-dims.d.ts +33 -0
- package/dist/cli/embedding-dims.js +39 -0
- package/dist/cli/eval-server.d.ts +8 -0
- package/dist/cli/eval-server.js +242 -2
- package/dist/cli/help-i18n.js +19 -0
- package/dist/cli/i18n/en.d.ts +15 -1
- package/dist/cli/i18n/en.js +15 -1
- package/dist/cli/i18n/resources.d.ts +29 -1
- package/dist/cli/i18n/zh-CN.d.ts +14 -0
- package/dist/cli/i18n/zh-CN.js +15 -1
- package/dist/cli/index.js +89 -1
- package/dist/cli/mcp.d.ts +6 -1
- package/dist/cli/mcp.js +26 -1
- package/dist/cli/optional-grammars.d.ts +6 -8
- package/dist/cli/optional-grammars.js +8 -11
- package/dist/cli/setup.d.ts +3 -1
- package/dist/cli/setup.js +67 -17
- package/dist/cli/skill-gen.js +2 -1
- package/dist/cli/tool.d.ts +19 -1
- package/dist/cli/tool.js +96 -3
- package/dist/core/embeddings/hf-env.d.ts +0 -66
- package/dist/core/embeddings/http-client.d.ts +7 -0
- package/dist/core/embeddings/http-client.js +5 -3
- package/dist/core/embeddings/server-mapping.d.ts +1 -1
- package/dist/core/embeddings/server-mapping.js +7 -3
- package/dist/core/graph/import-cycles.d.ts +10 -0
- package/dist/core/graph/import-cycles.js +103 -0
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +5 -1
- package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
- package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
- package/dist/core/group/extractors/http-route-extractor.js +22 -4
- package/dist/core/group/extractors/include-extractor.js +22 -13
- package/dist/core/group/sync.d.ts +27 -1
- package/dist/core/group/sync.js +0 -0
- package/dist/core/incremental/subgraph-extract.js +18 -1
- package/dist/core/ingestion/cfg/cfg-builder.d.ts +114 -0
- package/dist/core/ingestion/cfg/cfg-builder.js +196 -0
- package/dist/core/ingestion/cfg/collect.d.ts +51 -0
- package/dist/core/ingestion/cfg/collect.js +77 -0
- package/dist/core/ingestion/cfg/control-dependence.d.ts +58 -0
- package/dist/core/ingestion/cfg/control-dependence.js +182 -0
- package/dist/core/ingestion/cfg/control-flow-context.d.ts +106 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +124 -0
- package/dist/core/ingestion/cfg/emit.d.ts +293 -0
- package/dist/core/ingestion/cfg/emit.js +646 -0
- package/dist/core/ingestion/cfg/post-dominators.d.ts +79 -0
- package/dist/core/ingestion/cfg/post-dominators.js +176 -0
- package/dist/core/ingestion/cfg/reaching-def-reason-codec.d.ts +106 -0
- package/dist/core/ingestion/cfg/reaching-def-reason-codec.js +133 -0
- package/dist/core/ingestion/cfg/reaching-defs-graph.d.ts +18 -0
- package/dist/core/ingestion/cfg/reaching-defs-graph.js +312 -0
- package/dist/core/ingestion/cfg/reaching-defs.d.ts +162 -0
- package/dist/core/ingestion/cfg/reaching-defs.js +785 -0
- package/dist/core/ingestion/cfg/synthetic-escape.d.ts +124 -0
- package/dist/core/ingestion/cfg/synthetic-escape.js +300 -0
- package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
- package/dist/core/ingestion/cfg/traversal-result.js +2 -0
- package/dist/core/ingestion/cfg/types.d.ts +274 -0
- package/dist/core/ingestion/cfg/types.js +13 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.d.ts +111 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.js +541 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp.d.ts +74 -0
- package/dist/core/ingestion/cfg/visitors/c-cpp.js +584 -0
- package/dist/core/ingestion/cfg/visitors/call-site-harvest.d.ts +195 -0
- package/dist/core/ingestion/cfg/visitors/call-site-harvest.js +350 -0
- package/dist/core/ingestion/cfg/visitors/csharp-harvest.d.ts +124 -0
- package/dist/core/ingestion/cfg/visitors/csharp-harvest.js +589 -0
- package/dist/core/ingestion/cfg/visitors/csharp.d.ts +87 -0
- package/dist/core/ingestion/cfg/visitors/csharp.js +867 -0
- package/dist/core/ingestion/cfg/visitors/dart-harvest.d.ts +278 -0
- package/dist/core/ingestion/cfg/visitors/dart-harvest.js +870 -0
- package/dist/core/ingestion/cfg/visitors/dart.d.ts +119 -0
- package/dist/core/ingestion/cfg/visitors/dart.js +836 -0
- package/dist/core/ingestion/cfg/visitors/go-harvest.d.ts +160 -0
- package/dist/core/ingestion/cfg/visitors/go-harvest.js +621 -0
- package/dist/core/ingestion/cfg/visitors/go.d.ts +108 -0
- package/dist/core/ingestion/cfg/visitors/go.js +638 -0
- package/dist/core/ingestion/cfg/visitors/java-harvest.d.ts +108 -0
- package/dist/core/ingestion/cfg/visitors/java-harvest.js +513 -0
- package/dist/core/ingestion/cfg/visitors/java.d.ts +101 -0
- package/dist/core/ingestion/cfg/visitors/java.js +812 -0
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.d.ts +253 -0
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.js +719 -0
- package/dist/core/ingestion/cfg/visitors/kotlin.d.ts +121 -0
- package/dist/core/ingestion/cfg/visitors/kotlin.js +809 -0
- package/dist/core/ingestion/cfg/visitors/php-harvest.d.ts +172 -0
- package/dist/core/ingestion/cfg/visitors/php-harvest.js +626 -0
- package/dist/core/ingestion/cfg/visitors/php.d.ts +96 -0
- package/dist/core/ingestion/cfg/visitors/php.js +721 -0
- package/dist/core/ingestion/cfg/visitors/python-harvest.d.ts +212 -0
- package/dist/core/ingestion/cfg/visitors/python-harvest.js +739 -0
- package/dist/core/ingestion/cfg/visitors/python.d.ts +103 -0
- package/dist/core/ingestion/cfg/visitors/python.js +558 -0
- package/dist/core/ingestion/cfg/visitors/ruby-harvest.d.ts +223 -0
- package/dist/core/ingestion/cfg/visitors/ruby-harvest.js +587 -0
- package/dist/core/ingestion/cfg/visitors/ruby.d.ts +112 -0
- package/dist/core/ingestion/cfg/visitors/ruby.js +756 -0
- package/dist/core/ingestion/cfg/visitors/rust-harvest.d.ts +324 -0
- package/dist/core/ingestion/cfg/visitors/rust-harvest.js +873 -0
- package/dist/core/ingestion/cfg/visitors/rust.d.ts +95 -0
- package/dist/core/ingestion/cfg/visitors/rust.js +558 -0
- package/dist/core/ingestion/cfg/visitors/scope-tree-harvest.d.ts +87 -0
- package/dist/core/ingestion/cfg/visitors/scope-tree-harvest.js +116 -0
- package/dist/core/ingestion/cfg/visitors/swift-harvest.d.ts +255 -0
- package/dist/core/ingestion/cfg/visitors/swift-harvest.js +679 -0
- package/dist/core/ingestion/cfg/visitors/swift.d.ts +116 -0
- package/dist/core/ingestion/cfg/visitors/swift.js +787 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +149 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1056 -0
- package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
- package/dist/core/ingestion/cfg/visitors/typescript.js +583 -0
- package/dist/core/ingestion/import-resolvers/utils.js +2 -0
- package/dist/core/ingestion/language-provider.d.ts +11 -0
- package/dist/core/ingestion/languages/c-cpp.js +6 -1
- package/dist/core/ingestion/languages/cobol.js +4 -0
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +41 -2
- package/dist/core/ingestion/languages/cpp/captures.js +50 -1
- package/dist/core/ingestion/languages/cpp/conversion-rank.d.ts +2 -0
- package/dist/core/ingestion/languages/cpp/conversion-rank.js +89 -0
- package/dist/core/ingestion/languages/cpp/header-scan.d.ts +1 -1
- package/dist/core/ingestion/languages/cpp/header-scan.js +2 -2
- package/dist/core/ingestion/languages/cpp/inline-namespaces.js +7 -2
- package/dist/core/ingestion/languages/cpp/member-lookup.js +2 -1
- package/dist/core/ingestion/languages/cpp/query.js +23 -0
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +2 -1
- package/dist/core/ingestion/languages/csharp.js +2 -0
- package/dist/core/ingestion/languages/dart.js +2 -0
- package/dist/core/ingestion/languages/go.js +2 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +3 -2
- package/dist/core/ingestion/languages/kotlin.js +3 -0
- package/dist/core/ingestion/languages/php.js +2 -0
- package/dist/core/ingestion/languages/python.js +2 -0
- package/dist/core/ingestion/languages/ruby.js +2 -0
- package/dist/core/ingestion/languages/rust.js +2 -0
- package/dist/core/ingestion/languages/swift.js +2 -0
- package/dist/core/ingestion/languages/typescript.js +5 -0
- package/dist/core/ingestion/languages/vue.js +6 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
- package/dist/core/ingestion/method-extractors/generic.js +1 -0
- package/dist/core/ingestion/method-types.d.ts +2 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
- package/dist/core/ingestion/model/symbol-table.js +1 -0
- package/dist/core/ingestion/mro-processor.js +109 -0
- package/dist/core/ingestion/parsing-processor.js +23 -0
- package/dist/core/ingestion/pipeline-phases/call-summaries.d.ts +26 -0
- package/dist/core/ingestion/pipeline-phases/call-summaries.js +46 -0
- package/dist/core/ingestion/pipeline-phases/index.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/index.js +2 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +0 -21
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
- package/dist/core/ingestion/pipeline-phases/routes.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/routes.js +30 -1
- package/dist/core/ingestion/pipeline-phases/taint-summaries.d.ts +30 -0
- package/dist/core/ingestion/pipeline-phases/taint-summaries.js +81 -0
- package/dist/core/ingestion/pipeline.d.ts +92 -10
- package/dist/core/ingestion/pipeline.js +11 -2
- package/dist/core/ingestion/scope-extractor.js +12 -2
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +9 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.d.ts +75 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js +67 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +12 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +14 -2
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +6 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +13 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +9 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +28 -2
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +4 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +9 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +6 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +174 -40
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +24 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +276 -190
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +75 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +349 -1
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
- package/dist/core/ingestion/taint/call-summary-codec.d.ts +75 -0
- package/dist/core/ingestion/taint/call-summary-codec.js +128 -0
- package/dist/core/ingestion/taint/call-summary-emit.d.ts +43 -0
- package/dist/core/ingestion/taint/call-summary-emit.js +70 -0
- package/dist/core/ingestion/taint/call-summary-harvest.d.ts +77 -0
- package/dist/core/ingestion/taint/call-summary-harvest.js +179 -0
- package/dist/core/ingestion/taint/call-summary-model.d.ts +47 -0
- package/dist/core/ingestion/taint/call-summary-model.js +25 -0
- package/dist/core/ingestion/taint/emit.d.ts +125 -0
- package/dist/core/ingestion/taint/emit.js +208 -0
- package/dist/core/ingestion/taint/interproc-emit.d.ts +50 -0
- package/dist/core/ingestion/taint/interproc-emit.js +88 -0
- package/dist/core/ingestion/taint/interproc-solver.d.ts +110 -0
- package/dist/core/ingestion/taint/interproc-solver.js +312 -0
- package/dist/core/ingestion/taint/match.d.ts +153 -0
- package/dist/core/ingestion/taint/match.js +278 -0
- package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
- package/dist/core/ingestion/taint/path-codec.js +190 -0
- package/dist/core/ingestion/taint/propagate.d.ts +216 -0
- package/dist/core/ingestion/taint/propagate.js +659 -0
- package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
- package/dist/core/ingestion/taint/site-safety.js +98 -0
- package/dist/core/ingestion/taint/source-sink-config.d.ts +107 -23
- package/dist/core/ingestion/taint/source-sink-config.js +35 -12
- package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
- package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
- package/dist/core/ingestion/taint/summary-harvest-driver.d.ts +80 -0
- package/dist/core/ingestion/taint/summary-harvest-driver.js +179 -0
- package/dist/core/ingestion/taint/summary-harvest.d.ts +89 -0
- package/dist/core/ingestion/taint/summary-harvest.js +537 -0
- package/dist/core/ingestion/taint/summary-model.d.ts +200 -0
- package/dist/core/ingestion/taint/summary-model.js +86 -0
- package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
- package/dist/core/ingestion/taint/typescript-model.js +102 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +21 -0
- package/dist/core/ingestion/utils/env.d.ts +7 -0
- package/dist/core/ingestion/utils/env.js +12 -0
- package/dist/core/ingestion/utils/method-props.js +1 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +13 -0
- package/dist/core/ingestion/workers/parse-worker.js +76 -17
- package/dist/core/ingestion/workers/worker-pool.d.ts +9 -40
- package/dist/core/ingestion/workers/worker-pool.js +15 -2
- package/dist/core/lbug/csv-generator.d.ts +51 -3
- package/dist/core/lbug/csv-generator.js +369 -255
- package/dist/core/lbug/lbug-adapter.d.ts +58 -18
- package/dist/core/lbug/lbug-adapter.js +346 -84
- package/dist/core/lbug/lbug-config.d.ts +19 -0
- package/dist/core/lbug/lbug-config.js +29 -0
- package/dist/core/lbug/pdg-emit-sink.d.ts +127 -0
- package/dist/core/lbug/pdg-emit-sink.js +358 -0
- package/dist/core/lbug/pool-adapter.d.ts +35 -0
- package/dist/core/lbug/pool-adapter.js +90 -1
- package/dist/core/lbug/rel-pair-routing.d.ts +54 -0
- package/dist/core/lbug/rel-pair-routing.js +140 -0
- package/dist/core/lbug/schema.d.ts +2 -2
- package/dist/core/lbug/schema.js +3 -0
- package/dist/core/logger.d.ts +0 -5
- package/dist/core/run-analyze.d.ts +96 -0
- package/dist/core/run-analyze.js +207 -3
- package/dist/core/tree-sitter/parser-loader.js +5 -4
- package/dist/core/tree-sitter/safe-parse.d.ts +0 -5
- package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
- package/dist/core/tree-sitter/vendored-grammars.js +57 -0
- package/dist/core/wiki/generator.js +3 -1
- package/dist/core/wiki/graph-queries.d.ts +6 -0
- package/dist/core/wiki/graph-queries.js +9 -1
- package/dist/mcp/http-transport.d.ts +134 -0
- package/dist/mcp/http-transport.js +503 -0
- package/dist/mcp/local/local-backend.d.ts +110 -24
- package/dist/mcp/local/local-backend.js +1225 -19
- package/dist/mcp/local/pdg-impact.d.ts +384 -0
- package/dist/mcp/local/pdg-impact.js +1972 -0
- package/dist/mcp/resources.js +13 -1
- package/dist/mcp/tools.d.ts +12 -0
- package/dist/mcp/tools.js +214 -7
- package/dist/server/analyze-launch.d.ts +29 -0
- package/dist/server/analyze-launch.js +137 -0
- package/dist/server/analyze-upload.d.ts +33 -0
- package/dist/server/analyze-upload.js +123 -0
- package/dist/server/analyze-worker.d.ts +17 -1
- package/dist/server/api.d.ts +17 -0
- package/dist/server/api.js +132 -168
- package/dist/server/git-clone.d.ts +43 -2
- package/dist/server/git-clone.js +211 -16
- package/dist/server/mcp-http.d.ts +5 -6
- package/dist/server/mcp-http.js +9 -82
- package/dist/server/middleware.d.ts +40 -0
- package/dist/server/middleware.js +92 -0
- package/dist/server/private-ip.d.ts +1 -0
- package/dist/server/private-ip.js +15 -0
- package/dist/server/upload-ingest.d.ts +56 -0
- package/dist/server/upload-ingest.js +276 -0
- package/dist/server/upload-paths.d.ts +37 -0
- package/dist/server/upload-paths.js +57 -0
- package/dist/server/upload-sweep.d.ts +20 -0
- package/dist/server/upload-sweep.js +57 -0
- package/dist/storage/git.js +5 -8
- package/dist/storage/parse-cache.d.ts +22 -1
- package/dist/storage/parse-cache.js +41 -10
- package/dist/storage/repo-manager.d.ts +121 -10
- package/dist/storage/repo-manager.js +43 -5
- package/dist/types/pipeline.d.ts +9 -0
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +110 -10
- package/hooks/claude/gitnexus-hook.cjs +110 -13
- package/hooks/claude/hook-db-lock-probe.cjs +513 -26
- package/package.json +8 -2
- package/scripts/assert-publish-grammar-coverage.cjs +31 -0
- package/scripts/build-tree-sitter-grammars.cjs +16 -10
- package/scripts/cross-platform-tests.ts +1 -0
- package/skills/gitnexus-debugging.md +16 -4
- package/skills/gitnexus-exploring.md +3 -3
- package/skills/gitnexus-guide.md +33 -0
- package/skills/gitnexus-pdg-query.md +89 -0
- package/skills/gitnexus-refactoring.md +1 -1
- package/skills/gitnexus-taint-analysis.md +178 -0
- package/vendor/tree-sitter-c/package.json +1 -1
- package/vendor/tree-sitter-dart/package.json +1 -1
- package/vendor/tree-sitter-kotlin/package.json +1 -1
- package/vendor/tree-sitter-proto/package.json +1 -1
- package/vendor/tree-sitter-swift/package.json +1 -1
- package/web/assets/{agent-CKPMqImC.js → agent-D3Xd7zZE.js} +123 -123
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-DxJw65fT.js +1 -0
- package/web/assets/architectureDiagram-UL44E2DR-ChH9xW-N.js +36 -0
- package/web/assets/{blockDiagram-7IZFK4PR-B4niSLvR.js → blockDiagram-7IZFK4PR-BkWhpzWB.js} +1 -1
- package/web/assets/{c4Diagram-Y2BXMSZH-DIzEXalR.js → c4Diagram-Y2BXMSZH-DtBvpzWD.js} +1 -1
- package/web/assets/{chunk-3SSMPTDK-BzGeeheI.js → chunk-3SSMPTDK-B4_etUhr.js} +2 -2
- package/web/assets/{chunk-5IMINLNL-CnTLsXIV.js → chunk-5IMINLNL-DOdTeQri.js} +1 -1
- package/web/assets/{chunk-5VCL7Z4A-w1BCx98Z.js → chunk-5VCL7Z4A-CQ-2dYWj.js} +1 -1
- package/web/assets/chunk-6764PJDD-DpIS7hxD.js +1 -0
- package/web/assets/{chunk-67TQ5CYL-CChC2cAv.js → chunk-67TQ5CYL-4sNPJU1q.js} +3 -3
- package/web/assets/{chunk-7W6UQGC5-CgFyfZ6k.js → chunk-7W6UQGC5-KyEG0HQg.js} +1 -1
- package/web/assets/{chunk-AZZRMDJM-5E9ARdDQ.js → chunk-AZZRMDJM-y-xJBuMz.js} +1 -1
- package/web/assets/{chunk-INKRHTLW-ELpfuk9h.js → chunk-INKRHTLW-Nu4ri9P-.js} +1 -1
- package/web/assets/{chunk-JQRUD6KW-Dcq_Qnyy.js → chunk-JQRUD6KW-Dbva2Z17.js} +1 -1
- package/web/assets/{chunk-KGFNY3KK-W-VR57xb.js → chunk-KGFNY3KK-JGMJvKCK.js} +1 -1
- package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +161 -0
- package/web/assets/{chunk-KNLZD3CH-BuZBOcso.js → chunk-KNLZD3CH-BGlP6X9R.js} +1 -1
- package/web/assets/{chunk-KRXBNO2N-DH2NPM7I.js → chunk-KRXBNO2N-BW5o0KUN.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-zoomRtWX.js} +1 -1
- package/web/assets/{chunk-LII3EMHJ-3X33tCpU.js → chunk-LII3EMHJ-D-jm-dLa.js} +1 -1
- package/web/assets/{chunk-QA3QBVWF-D5Ha3GgB.js → chunk-QA3QBVWF-BgRW3SXd.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-DD0B2N2B.js} +1 -1
- package/web/assets/{chunk-T2UQINTJ-kGYG7ppP.js → chunk-T2UQINTJ-DSBqkzH7.js} +1 -1
- package/web/assets/chunk-T5OCTHI4-DdZvN-9i.js +1 -0
- package/web/assets/{chunk-UY5QBCOK-Du-c56gs.js → chunk-UY5QBCOK-DyF0vyGd.js} +1 -1
- package/web/assets/{chunk-W44A43WB-lrCpuupA.js → chunk-W44A43WB-JqMLIpR0.js} +1 -1
- package/web/assets/{chunk-ZXARS5L4-CSZRiyOf.js → chunk-ZXARS5L4-CxN8oiwI.js} +1 -1
- package/web/assets/classDiagram-KGZ6W3CR-DJm9BgF-.js +1 -0
- package/web/assets/classDiagram-v2-72OJOZXJ-DJm9BgF-.js +1 -0
- package/web/assets/{context-builder-_HS0v2ma.js → context-builder-BHiFUA8O.js} +10 -2
- package/web/assets/{cose-bilkent-UX7MHV2Q-BHv2bbtl.js → cose-bilkent-UX7MHV2Q-D71wNYRJ.js} +1 -1
- package/web/assets/dagre-ND4H6XIP-BIkxmhcu.js +4 -0
- package/web/assets/diagram-3NCE3AQN-BTa6iH0T.js +43 -0
- package/web/assets/diagram-GF46GFSD-DZDV0vBj.js +24 -0
- package/web/assets/{diagram-HNR7UZ2L-fUWreZzx.js → diagram-HNR7UZ2L-DFCfd5LI.js} +2 -2
- package/web/assets/diagram-QXG6HAR7-BzM--Jj0.js +24 -0
- package/web/assets/diagram-WEQXMOUZ-DFFwz0k4.js +10 -0
- package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-CfHMJJqo.js} +1 -1
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +1 -0
- package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-Be805QiJ.js} +3 -3
- package/web/assets/{ganttDiagram-JCBTUEKG-BHQssTz6.js → ganttDiagram-JCBTUEKG-D3iL2Aet.js} +1 -1
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-Cr_Bm2Nb.js +1 -0
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CPIDgxGm.js +106 -0
- package/web/assets/index-BvYev1UJ.js +635 -0
- package/web/assets/index-_lgn7hs5.css +2 -0
- package/web/assets/info-J43DQDTF-KCYPFFUO-Cs4p2oyk.js +1 -0
- package/web/assets/infoDiagram-3YFTVSEB-Y7BKSoke.js +2 -0
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CT4SHFuI.js} +1 -1
- package/web/assets/{journeyDiagram-M6C3CM3L-DNPYCpvq.js → journeyDiagram-M6C3CM3L-CvArszLo.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-C6yDxr7N.js} +3 -3
- package/web/assets/{katex-K3KEBU37-CrVLz8l7.js → katex-K3KEBU37-CTc5BslQ.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-BOc2WEne.js} +1 -1
- package/web/assets/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +1 -0
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +1 -0
- package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-Cvc0f_Pd.js} +1 -1
- package/web/assets/{quadrantDiagram-VICAPDV7-bAm6Hi_5.js → quadrantDiagram-VICAPDV7-yXSZ2lLz.js} +1 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +1 -0
- package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-BRcZvHs4.js} +1 -1
- package/web/assets/{sankeyDiagram-URQDO5SZ-LFJz53u5.js → sankeyDiagram-URQDO5SZ-1qEjlFIB.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-PcSCuvrB.js} +3 -3
- package/web/assets/stateDiagram-7D4R322I-D6qYSV1E.js +1 -0
- package/web/assets/stateDiagram-v2-36443NZ5-Bg-ImOSX.js +1 -0
- package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-QWkh01xw.js} +1 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +1 -0
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +1 -0
- package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-ak4DfwuM.js} +3 -3
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +1 -0
- package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-CTzR4Loq.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-DA1BEaQz.js} +1 -1
- package/web/index.html +13 -15
- package/scripts/materialize-vendor-grammars.cjs +0 -97
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-BETj2x4g.js +0 -1
- package/web/assets/architectureDiagram-UL44E2DR-D1EXI0zA.js +0 -36
- package/web/assets/chunk-2T2R6R2M-n6s9JZqv.js +0 -4
- package/web/assets/chunk-2UTLFMKG-CMBB1TMN.js +0 -1
- package/web/assets/chunk-4R4BOZG6-mKhL59ul.js +0 -159
- package/web/assets/chunk-6764PJDD-ChwMM2z4.js +0 -1
- package/web/assets/chunk-7J6CGLKN-DT-b53FT.js +0 -10
- package/web/assets/chunk-C62D2QBJ-CDAWj26E.js +0 -1
- package/web/assets/chunk-CEXFNPSA-D68Tk6ls.js +0 -1
- package/web/assets/chunk-CilyBKbf.js +0 -1
- package/web/assets/chunk-J5EP6P6S-DxWW0yvu.js +0 -1
- package/web/assets/chunk-KGYTTC2M-C6PHeuay.js +0 -1
- package/web/assets/chunk-RERM46MO-DQNbXtfw.js +0 -1
- package/web/assets/chunk-RKZBBQEN-BmTPLSyv.js +0 -1
- package/web/assets/chunk-RLI5ZMPA-DExu2DOK.js +0 -1
- package/web/assets/chunk-T5OCTHI4--9wWpVws.js +0 -1
- package/web/assets/chunk-UP6H54XL-DsKdC6jC.js +0 -1
- package/web/assets/chunk-UXSXWOXI-DR81EqLr.js +0 -1
- package/web/assets/classDiagram-KGZ6W3CR-Bv93af_b.js +0 -1
- package/web/assets/classDiagram-v2-72OJOZXJ-CLfEqHUa.js +0 -1
- package/web/assets/dagre-ND4H6XIP-DsIr9MOv.js +0 -4
- package/web/assets/diagram-3NCE3AQN-BqAtKUpW.js +0 -43
- package/web/assets/diagram-GF46GFSD-CaBG6n6o.js +0 -24
- package/web/assets/diagram-QXG6HAR7-CZ-O3rcV.js +0 -24
- package/web/assets/diagram-WEQXMOUZ-P4lSL4GH.js +0 -10
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-B3myHUst.js +0 -1
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DF3rtf4A.js +0 -1
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CCwVLT__.js +0 -106
- package/web/assets/index-BKWA-m7o.css +0 -2
- package/web/assets/index-CG6q8eTs.js +0 -626
- package/web/assets/info-J43DQDTF-KCYPFFUO-R4XV_akJ.js +0 -1
- package/web/assets/infoDiagram-3YFTVSEB-DmLICZx1.js +0 -2
- package/web/assets/packet-YPE3B663-LP52Z2RK-CgQPdNZa.js +0 -1
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BzWOwc_c.js +0 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-B7KG0FN7.js +0 -1
- package/web/assets/stateDiagram-7D4R322I-CB2nABwH.js +0 -1
- package/web/assets/stateDiagram-v2-36443NZ5-COBGd2RL.js +0 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-CRSxEwLO.js +0 -1
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-a0PfKoVn.js +0 -1
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-BKQ6e5mC.js +0 -1
- /package/web/assets/{chunk-AQ6EADP3-CCFje6lL.js → chunk-AQ6EADP3-CZhslHi-.js} +0 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import { CfgBuilder } from '../cfg-builder.js';
|
|
2
|
+
import { ControlFlowContext, drainFinalizerPending, wireJumpThroughFinalizers, } from '../control-flow-context.js';
|
|
3
|
+
import { PhpHarvester } from './php-harvest.js';
|
|
4
|
+
/** PHP node types that own a CFG-bearing function body. */
|
|
5
|
+
const PHP_FUNCTION_TYPES = new Set([
|
|
6
|
+
'function_definition',
|
|
7
|
+
'method_declaration',
|
|
8
|
+
'anonymous_function',
|
|
9
|
+
'arrow_function',
|
|
10
|
+
]);
|
|
11
|
+
/** Statement node types that break a basic block (everything else coalesces). */
|
|
12
|
+
const CONTROL_FLOW_TYPES = new Set([
|
|
13
|
+
'if_statement',
|
|
14
|
+
'for_statement',
|
|
15
|
+
'foreach_statement',
|
|
16
|
+
'while_statement',
|
|
17
|
+
'do_statement',
|
|
18
|
+
'switch_statement',
|
|
19
|
+
'try_statement',
|
|
20
|
+
'return_statement',
|
|
21
|
+
'break_statement',
|
|
22
|
+
'continue_statement',
|
|
23
|
+
'goto_statement',
|
|
24
|
+
'named_label_statement',
|
|
25
|
+
'compound_statement',
|
|
26
|
+
]);
|
|
27
|
+
const startLineOf = (n) => n.startPosition.row + 1;
|
|
28
|
+
const endLineOf = (n) => n.endPosition.row + 1;
|
|
29
|
+
const isComment = (n) => n.type === 'comment';
|
|
30
|
+
/**
|
|
31
|
+
* Per-function PHP walk state. One instance per function so the
|
|
32
|
+
* {@link ControlFlowContext}, exception-handler stack, and the `break N` /
|
|
33
|
+
* `continue N` synthetic-label bookkeeping are scoped to that function and never
|
|
34
|
+
* leak across functions.
|
|
35
|
+
*/
|
|
36
|
+
class PhpCfgWalk {
|
|
37
|
+
builder;
|
|
38
|
+
harvest;
|
|
39
|
+
cfc = new ControlFlowContext();
|
|
40
|
+
/** Stack of exception-handler entry blocks (catch / finally) a `throw` jumps to. */
|
|
41
|
+
handlers = [];
|
|
42
|
+
/**
|
|
43
|
+
* Synthetic labels of the active loop/switch frames, innermost LAST — so the
|
|
44
|
+
* N-th enclosing frame's label is `loopLabels[length - N]`. PHP's `break N` /
|
|
45
|
+
* `continue N` resolve against these (no source labels exist).
|
|
46
|
+
*/
|
|
47
|
+
loopLabels = [];
|
|
48
|
+
labelSeq = 0;
|
|
49
|
+
constructor(builder, harvest) {
|
|
50
|
+
this.builder = builder;
|
|
51
|
+
this.harvest = harvest;
|
|
52
|
+
}
|
|
53
|
+
/** Statements of a body node, ignoring comments. */
|
|
54
|
+
statementsOf(block) {
|
|
55
|
+
return block.namedChildren.filter((c) => !isComment(c));
|
|
56
|
+
}
|
|
57
|
+
/** The `body` block of a node (a `compound_statement` / `colon_block` / stmt). */
|
|
58
|
+
bodyBlockOf(node) {
|
|
59
|
+
return node.childForFieldName('body') ?? undefined;
|
|
60
|
+
}
|
|
61
|
+
/** Strip a `parenthesized_expression` wrapper (PHP `if`/`while` conditions). */
|
|
62
|
+
unwrapParen(node) {
|
|
63
|
+
if (node.type === 'parenthesized_expression') {
|
|
64
|
+
const inner = node.namedChildren.find((c) => !isComment(c));
|
|
65
|
+
if (inner)
|
|
66
|
+
return inner;
|
|
67
|
+
}
|
|
68
|
+
return node;
|
|
69
|
+
}
|
|
70
|
+
/** Visit a body that may be a block-ish container or a single statement. */
|
|
71
|
+
visitBody(node) {
|
|
72
|
+
return this.builder.withNesting(() => {
|
|
73
|
+
if (!node)
|
|
74
|
+
return null;
|
|
75
|
+
if (node.type === 'compound_statement' || node.type === 'colon_block') {
|
|
76
|
+
return this.visitSeq(this.statementsOf(node));
|
|
77
|
+
}
|
|
78
|
+
return this.visitStmt(node);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Wire a sequence of statements, coalescing straight-line runs into blocks. */
|
|
82
|
+
visitSeq(stmts) {
|
|
83
|
+
return this.builder.withNesting(() => {
|
|
84
|
+
let entry;
|
|
85
|
+
let dangling = [];
|
|
86
|
+
let openSimple;
|
|
87
|
+
for (const stmt of stmts) {
|
|
88
|
+
// An `expression_statement` wrapping a bare `throw_expression` is a
|
|
89
|
+
// terminator (PHP has no `throw_statement` node), so it breaks the block.
|
|
90
|
+
// An `$x = match($v) {…}` value-position assignment breaks too (#2207).
|
|
91
|
+
const breaks = CONTROL_FLOW_TYPES.has(stmt.type) ||
|
|
92
|
+
this.isThrowStatement(stmt) ||
|
|
93
|
+
this.isValueBranchAssignment(stmt);
|
|
94
|
+
if (breaks) {
|
|
95
|
+
openSimple = undefined; // close any open straight-line block
|
|
96
|
+
const res = this.visitStmt(stmt);
|
|
97
|
+
if (res === null)
|
|
98
|
+
continue; // transparent (empty nested block)
|
|
99
|
+
if (entry === undefined)
|
|
100
|
+
entry = res.entry;
|
|
101
|
+
else
|
|
102
|
+
this.builder.connect(dangling, res.entry, 'seq');
|
|
103
|
+
dangling = [...res.exits];
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
if (openSimple === undefined) {
|
|
107
|
+
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
|
|
108
|
+
if (entry === undefined)
|
|
109
|
+
entry = idx;
|
|
110
|
+
else
|
|
111
|
+
this.builder.connect(dangling, idx, 'seq');
|
|
112
|
+
openSimple = idx;
|
|
113
|
+
dangling = [idx];
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
this.builder.extendBlock(openSimple, endLineOf(stmt), stmt.text, this.harvest.facts(stmt));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (entry === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
return { entry, exits: dangling };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** Dispatch one statement to its handler. Non-null except for empty blocks. */
|
|
126
|
+
visitStmt(stmt) {
|
|
127
|
+
if (this.isThrowStatement(stmt))
|
|
128
|
+
return this.visitThrow(stmt);
|
|
129
|
+
// `$x = match($v) { … };` (#2207): model the match arms as control flow and
|
|
130
|
+
// bind the assignment target on the rejoin.
|
|
131
|
+
const assign = this.assignmentBranch(stmt);
|
|
132
|
+
if (assign)
|
|
133
|
+
return this.visitBindAssign(stmt, assign.expr, assign.match);
|
|
134
|
+
switch (stmt.type) {
|
|
135
|
+
case 'if_statement':
|
|
136
|
+
return this.visitIf(stmt);
|
|
137
|
+
case 'for_statement':
|
|
138
|
+
return this.visitFor(stmt);
|
|
139
|
+
case 'foreach_statement':
|
|
140
|
+
return this.visitForEach(stmt);
|
|
141
|
+
case 'while_statement':
|
|
142
|
+
return this.visitWhile(stmt);
|
|
143
|
+
case 'do_statement':
|
|
144
|
+
return this.visitDoWhile(stmt);
|
|
145
|
+
case 'switch_statement':
|
|
146
|
+
return this.visitSwitch(stmt);
|
|
147
|
+
case 'try_statement':
|
|
148
|
+
return this.visitTry(stmt);
|
|
149
|
+
case 'return_statement':
|
|
150
|
+
return this.visitReturn(stmt);
|
|
151
|
+
case 'break_statement':
|
|
152
|
+
return this.visitBreak(stmt);
|
|
153
|
+
case 'continue_statement':
|
|
154
|
+
return this.visitContinue(stmt);
|
|
155
|
+
case 'compound_statement':
|
|
156
|
+
case 'colon_block':
|
|
157
|
+
return this.visitSeq(this.statementsOf(stmt));
|
|
158
|
+
case 'goto_statement':
|
|
159
|
+
case 'named_label_statement':
|
|
160
|
+
// `goto` / labels are modeled as straight-line blocks (no jump edge — see
|
|
161
|
+
// the visitor limitations); they still carry their text + facts.
|
|
162
|
+
return this.visitSimple(stmt);
|
|
163
|
+
default:
|
|
164
|
+
return this.visitSimple(stmt);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** True for an `expression_statement` whose value is a bare `throw_expression`. */
|
|
168
|
+
isThrowStatement(stmt) {
|
|
169
|
+
if (stmt.type !== 'expression_statement')
|
|
170
|
+
return false;
|
|
171
|
+
const inner = stmt.namedChildren.find((c) => !isComment(c));
|
|
172
|
+
return inner?.type === 'throw_expression';
|
|
173
|
+
}
|
|
174
|
+
visitSimple(stmt) {
|
|
175
|
+
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
|
|
176
|
+
return { entry: idx, exits: [idx] };
|
|
177
|
+
}
|
|
178
|
+
visitReturn(stmt) {
|
|
179
|
+
// `return match($v) { … };` (#2207): the returned value is a value-position
|
|
180
|
+
// branch — model it as control flow, with each arm returning (its value IS
|
|
181
|
+
// the function result), threading every active finally per arm.
|
|
182
|
+
const branch = stmt.namedChildren.find((c) => !isComment(c));
|
|
183
|
+
if (branch && this.isModelableValueBranch(branch)) {
|
|
184
|
+
const res = this.visitBranchExpr(branch);
|
|
185
|
+
const finalizers = this.cfc.finalizersForReturn();
|
|
186
|
+
for (const ex of res.exits) {
|
|
187
|
+
wireJumpThroughFinalizers(this.builder, ex, finalizers, this.builder.exitIndex, 'return');
|
|
188
|
+
}
|
|
189
|
+
return { entry: res.entry, exits: [] };
|
|
190
|
+
}
|
|
191
|
+
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
|
|
192
|
+
// A return crosses EVERY active finally before EXIT.
|
|
193
|
+
wireJumpThroughFinalizers(this.builder, idx, this.cfc.finalizersForReturn(), this.builder.exitIndex, 'return');
|
|
194
|
+
return { entry: idx, exits: [] };
|
|
195
|
+
}
|
|
196
|
+
visitThrow(stmt) {
|
|
197
|
+
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
|
|
198
|
+
this.builder.edge(idx, this.currentHandler(), 'throw');
|
|
199
|
+
return { entry: idx, exits: [] };
|
|
200
|
+
}
|
|
201
|
+
visitBreak(stmt) {
|
|
202
|
+
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
|
|
203
|
+
const label = this.jumpLabel(stmt);
|
|
204
|
+
const res = this.cfc.resolveBreak(label);
|
|
205
|
+
const { target, finalizers } = res ?? {
|
|
206
|
+
target: this.builder.exitIndex,
|
|
207
|
+
finalizers: this.cfc.finalizersForReturn(),
|
|
208
|
+
};
|
|
209
|
+
wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'break');
|
|
210
|
+
return { entry: idx, exits: [] };
|
|
211
|
+
}
|
|
212
|
+
visitContinue(stmt) {
|
|
213
|
+
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
|
|
214
|
+
const label = this.jumpLabel(stmt);
|
|
215
|
+
const res = this.cfc.resolveContinue(label);
|
|
216
|
+
const { target, finalizers } = res ?? {
|
|
217
|
+
target: this.builder.exitIndex,
|
|
218
|
+
finalizers: this.cfc.finalizersForReturn(),
|
|
219
|
+
};
|
|
220
|
+
wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'continue');
|
|
221
|
+
return { entry: idx, exits: [] };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Resolve a `break N` / `continue N` to the SYNTHETIC label of the N-th
|
|
225
|
+
* enclosing loop/switch frame (innermost = 1). Returns undefined for a bare
|
|
226
|
+
* `break`/`continue` (no level), so the context resolves the nearest frame as
|
|
227
|
+
* usual. PHP counts BOTH loop AND switch frames for `break N` and `continue N`
|
|
228
|
+
* (a `switch` acts like a loop level for `continue`), which is exactly the set
|
|
229
|
+
* pushed onto {@link loopLabels} here — so one count serves both.
|
|
230
|
+
*/
|
|
231
|
+
jumpLabel(stmt) {
|
|
232
|
+
const level = this.jumpLevel(stmt);
|
|
233
|
+
if (level <= 1)
|
|
234
|
+
return undefined; // bare break/continue → nearest frame
|
|
235
|
+
const n = this.loopLabels.length;
|
|
236
|
+
if (level > n)
|
|
237
|
+
return undefined; // over-deep level → conservative fallback
|
|
238
|
+
return this.loopLabels[n - level];
|
|
239
|
+
}
|
|
240
|
+
/** The integer level of a `break N;` / `continue N;` (default 1). */
|
|
241
|
+
jumpLevel(stmt) {
|
|
242
|
+
const intNode = stmt.namedChildren.find((c) => c.type === 'integer');
|
|
243
|
+
if (!intNode)
|
|
244
|
+
return 1;
|
|
245
|
+
const v = parseInt(intNode.text, 10);
|
|
246
|
+
return Number.isFinite(v) && v >= 1 ? v : 1;
|
|
247
|
+
}
|
|
248
|
+
/** Push a fresh synthetic loop/switch label and return it. */
|
|
249
|
+
nextLabel() {
|
|
250
|
+
const label = `__php_lvl_${this.labelSeq++}`;
|
|
251
|
+
return label;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* `if cond: … elseif cond: … else: …`. PHP has NO nested-if else chain: the
|
|
255
|
+
* `if_statement` carries the condition + body plus zero-or-more `alternative`
|
|
256
|
+
* fields, each an `else_if_clause` (its own condition + body) or a trailing
|
|
257
|
+
* `else_clause`. The elif chain is threaded on the `cond-false` edge. Handles
|
|
258
|
+
* both brace bodies and the colon (`endif`) syntax uniformly (body field).
|
|
259
|
+
*/
|
|
260
|
+
visitIf(stmt) {
|
|
261
|
+
const cond = this.condOf(stmt) ?? stmt;
|
|
262
|
+
const header = this.builder.newBlock(startLineOf(stmt), endLineOf(cond), cond.text, 'normal', this.harvest.facts(cond));
|
|
263
|
+
const exits = [];
|
|
264
|
+
const thenRes = this.visitBody(stmt.childForFieldName('body'));
|
|
265
|
+
if (thenRes) {
|
|
266
|
+
this.builder.edge(header, thenRes.entry, 'cond-true');
|
|
267
|
+
exits.push(...thenRes.exits);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
exits.push(header); // empty then — true path falls through
|
|
271
|
+
}
|
|
272
|
+
const alternatives = this.alternativesOf(stmt);
|
|
273
|
+
let falseFrom = header;
|
|
274
|
+
for (const alt of alternatives) {
|
|
275
|
+
if (alt.type === 'else_if_clause') {
|
|
276
|
+
const elifCondRaw = alt.childForFieldName('condition');
|
|
277
|
+
const elifCond = elifCondRaw ? this.unwrapParen(elifCondRaw) : alt;
|
|
278
|
+
const elifHeader = this.builder.newBlock(startLineOf(alt), endLineOf(elifCond), elifCond.text, 'normal', this.harvest.facts(elifCond));
|
|
279
|
+
this.builder.edge(falseFrom, elifHeader, 'cond-false');
|
|
280
|
+
const elifRes = this.visitBody(alt.childForFieldName('body'));
|
|
281
|
+
if (elifRes) {
|
|
282
|
+
this.builder.edge(elifHeader, elifRes.entry, 'cond-true');
|
|
283
|
+
exits.push(...elifRes.exits);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
exits.push(elifHeader);
|
|
287
|
+
}
|
|
288
|
+
falseFrom = elifHeader;
|
|
289
|
+
}
|
|
290
|
+
else if (alt.type === 'else_clause') {
|
|
291
|
+
const elseRes = this.visitBody(alt.childForFieldName('body'));
|
|
292
|
+
if (elseRes) {
|
|
293
|
+
this.builder.edge(falseFrom, elseRes.entry, 'cond-false');
|
|
294
|
+
exits.push(...elseRes.exits);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
exits.push(falseFrom);
|
|
298
|
+
}
|
|
299
|
+
falseFrom = -1; // an else consumes the false path entirely
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (falseFrom >= 0)
|
|
303
|
+
exits.push(falseFrom); // no trailing else → fall through
|
|
304
|
+
return { entry: header, exits: [...new Set(exits)] };
|
|
305
|
+
}
|
|
306
|
+
/** The `alternative`-field children of an `if_statement`, in source order. */
|
|
307
|
+
alternativesOf(stmt) {
|
|
308
|
+
const out = [];
|
|
309
|
+
for (let i = 0; i < stmt.childCount; i++) {
|
|
310
|
+
if (stmt.fieldNameForChild(i) === 'alternative') {
|
|
311
|
+
const c = stmt.child(i);
|
|
312
|
+
if (c)
|
|
313
|
+
out.push(c);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
/** The (paren-unwrapped) condition expression of an if/while/do/switch. */
|
|
319
|
+
condOf(stmt) {
|
|
320
|
+
const cond = stmt.childForFieldName('condition');
|
|
321
|
+
return cond ? this.unwrapParen(cond) : undefined;
|
|
322
|
+
}
|
|
323
|
+
visitWhile(stmt) {
|
|
324
|
+
const label = this.nextLabel();
|
|
325
|
+
const cond = this.condOf(stmt) ?? stmt;
|
|
326
|
+
const header = this.builder.newBlock(startLineOf(stmt), endLineOf(cond), cond.text, 'normal', this.harvest.facts(cond));
|
|
327
|
+
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
|
328
|
+
this.loopLabels.push(label);
|
|
329
|
+
this.cfc.pushLoop(header, loopExit, [label]);
|
|
330
|
+
const body = this.visitBody(this.bodyBlockOf(stmt));
|
|
331
|
+
this.cfc.pop();
|
|
332
|
+
this.loopLabels.pop();
|
|
333
|
+
if (body) {
|
|
334
|
+
this.builder.edge(header, body.entry, 'cond-true');
|
|
335
|
+
this.builder.connect(body.exits, header, 'loop-back');
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this.builder.edge(header, header, 'loop-back'); // empty body re-tests
|
|
339
|
+
}
|
|
340
|
+
// Always emit the structural exit edge — even `while (true)` keeps EXIT
|
|
341
|
+
// reverse-reachable for the post-dominator / CDG pass.
|
|
342
|
+
this.builder.edge(header, loopExit, 'cond-false');
|
|
343
|
+
return { entry: header, exits: [loopExit] };
|
|
344
|
+
}
|
|
345
|
+
visitDoWhile(stmt) {
|
|
346
|
+
const label = this.nextLabel();
|
|
347
|
+
const cond = this.condOf(stmt) ?? stmt;
|
|
348
|
+
const condBlock = this.builder.newBlock(startLineOf(cond), endLineOf(cond), cond.text, 'normal', this.harvest.facts(cond));
|
|
349
|
+
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
|
350
|
+
this.loopLabels.push(label);
|
|
351
|
+
this.cfc.pushLoop(condBlock, loopExit, [label]);
|
|
352
|
+
const body = this.visitBody(this.bodyBlockOf(stmt));
|
|
353
|
+
this.cfc.pop();
|
|
354
|
+
this.loopLabels.pop();
|
|
355
|
+
const backTarget = body ? body.entry : condBlock;
|
|
356
|
+
if (body)
|
|
357
|
+
this.builder.connect(body.exits, condBlock, 'seq');
|
|
358
|
+
this.builder.edge(condBlock, backTarget, 'loop-back'); // cond true → run body again
|
|
359
|
+
this.builder.edge(condBlock, loopExit, 'cond-false');
|
|
360
|
+
return { entry: backTarget, exits: [loopExit] };
|
|
361
|
+
}
|
|
362
|
+
visitFor(stmt) {
|
|
363
|
+
const label = this.nextLabel();
|
|
364
|
+
const init = stmt.childForFieldName('initialize');
|
|
365
|
+
const cond = stmt.childForFieldName('condition');
|
|
366
|
+
const incr = stmt.childForFieldName('update');
|
|
367
|
+
const header = this.builder.newBlock(startLineOf(stmt), cond ? endLineOf(cond) : startLineOf(stmt), cond ? cond.text : 'for(;;)', 'normal', cond ? this.harvest.facts(cond) : undefined);
|
|
368
|
+
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
|
369
|
+
let incrBlock = header;
|
|
370
|
+
if (incr) {
|
|
371
|
+
incrBlock = this.builder.newBlock(startLineOf(incr), endLineOf(incr), incr.text, 'normal', this.harvest.facts(incr));
|
|
372
|
+
this.builder.edge(incrBlock, header, 'loop-back');
|
|
373
|
+
}
|
|
374
|
+
this.loopLabels.push(label);
|
|
375
|
+
this.cfc.pushLoop(incrBlock, loopExit, [label]);
|
|
376
|
+
const body = this.visitBody(this.bodyBlockOf(stmt));
|
|
377
|
+
this.cfc.pop();
|
|
378
|
+
this.loopLabels.pop();
|
|
379
|
+
if (body) {
|
|
380
|
+
this.builder.edge(header, body.entry, 'cond-true');
|
|
381
|
+
this.builder.connect(body.exits, incrBlock, incr ? 'seq' : 'loop-back');
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
this.builder.edge(header, incrBlock, 'cond-true');
|
|
385
|
+
if (!incr)
|
|
386
|
+
this.builder.edge(header, header, 'loop-back');
|
|
387
|
+
}
|
|
388
|
+
// Structural exit edge — `for (;;) {}` (no condition) still keeps EXIT
|
|
389
|
+
// reverse-reachable so CDG is not silently skipped for the function.
|
|
390
|
+
this.builder.edge(header, loopExit, 'cond-false');
|
|
391
|
+
let entry = header;
|
|
392
|
+
if (init) {
|
|
393
|
+
const initBlock = this.builder.newBlock(startLineOf(init), endLineOf(init), init.text, 'normal', this.harvest.facts(init));
|
|
394
|
+
this.builder.edge(initBlock, header, 'seq');
|
|
395
|
+
entry = initBlock;
|
|
396
|
+
}
|
|
397
|
+
return { entry, exits: [loopExit] };
|
|
398
|
+
}
|
|
399
|
+
visitForEach(stmt) {
|
|
400
|
+
const label = this.nextLabel();
|
|
401
|
+
// Header text is SYNTHESIZED, so facts come from the iterable (use) + the
|
|
402
|
+
// loop target variable(s) (def) directly.
|
|
403
|
+
const header = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), this.forEachHeaderText(stmt), 'normal', this.harvest.foreachHeadFacts(stmt));
|
|
404
|
+
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
|
405
|
+
this.loopLabels.push(label);
|
|
406
|
+
this.cfc.pushLoop(header, loopExit, [label]);
|
|
407
|
+
const body = this.visitBody(this.bodyBlockOf(stmt));
|
|
408
|
+
this.cfc.pop();
|
|
409
|
+
this.loopLabels.pop();
|
|
410
|
+
if (body) {
|
|
411
|
+
this.builder.edge(header, body.entry, 'cond-true');
|
|
412
|
+
this.builder.connect(body.exits, header, 'loop-back');
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
this.builder.edge(header, header, 'loop-back');
|
|
416
|
+
}
|
|
417
|
+
this.builder.edge(header, loopExit, 'cond-false');
|
|
418
|
+
return { entry: header, exits: [loopExit] };
|
|
419
|
+
}
|
|
420
|
+
forEachHeaderText(stmt) {
|
|
421
|
+
const first = stmt.namedChild(0);
|
|
422
|
+
return first ? `foreach(${first.text} as …)` : 'foreach(… as …)';
|
|
423
|
+
}
|
|
424
|
+
visitSwitch(stmt) {
|
|
425
|
+
const label = this.nextLabel();
|
|
426
|
+
const value = this.condOf(stmt) ?? stmt;
|
|
427
|
+
const dispatch = this.builder.newBlock(startLineOf(stmt), endLineOf(value), value.text, 'normal', this.harvest.facts(value));
|
|
428
|
+
const switchExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
|
429
|
+
this.loopLabels.push(label);
|
|
430
|
+
this.cfc.pushSwitch(switchExit, [label]);
|
|
431
|
+
const body = stmt.childForFieldName('body');
|
|
432
|
+
// A `switch_block` holds `case_statement`s (field `value`, fall through) and a
|
|
433
|
+
// `default_statement`.
|
|
434
|
+
const groups = body
|
|
435
|
+
? body.namedChildren.filter((c) => c.type === 'case_statement' || c.type === 'default_statement')
|
|
436
|
+
: [];
|
|
437
|
+
// Each case-test expression evaluates before its body runs — harvest its uses
|
|
438
|
+
// onto the dispatch block, CONDITIONALLY (a later case test only runs when
|
|
439
|
+
// earlier cases didn't match).
|
|
440
|
+
for (const g of groups) {
|
|
441
|
+
const test = this.caseTest(g);
|
|
442
|
+
if (test)
|
|
443
|
+
this.builder.attachFacts(dispatch, this.harvest.factsConditional(test));
|
|
444
|
+
}
|
|
445
|
+
const groupResults = groups.map((g) => this.visitSeq(this.caseStatements(g)));
|
|
446
|
+
const hasDefault = groups.some((g) => g.type === 'default_statement');
|
|
447
|
+
const entryOf = new Array(groups.length);
|
|
448
|
+
let after = switchExit;
|
|
449
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
450
|
+
entryOf[i] = groupResults[i]?.entry ?? after;
|
|
451
|
+
after = entryOf[i];
|
|
452
|
+
}
|
|
453
|
+
for (let i = 0; i < groups.length; i++) {
|
|
454
|
+
this.builder.edge(dispatch, entryOf[i], 'switch-case');
|
|
455
|
+
}
|
|
456
|
+
if (!hasDefault)
|
|
457
|
+
this.builder.edge(dispatch, switchExit, 'switch-case'); // no-match path
|
|
458
|
+
// C-style FALLTHROUGH: a case with no break/return falls through to the next.
|
|
459
|
+
for (let i = 0; i < groups.length; i++) {
|
|
460
|
+
const res = groupResults[i];
|
|
461
|
+
if (!res)
|
|
462
|
+
continue;
|
|
463
|
+
const fallTarget = i + 1 < groups.length ? entryOf[i + 1] : switchExit;
|
|
464
|
+
this.builder.connect(res.exits, fallTarget, 'fallthrough');
|
|
465
|
+
}
|
|
466
|
+
this.cfc.pop();
|
|
467
|
+
this.loopLabels.pop();
|
|
468
|
+
return { entry: dispatch, exits: [switchExit] };
|
|
469
|
+
}
|
|
470
|
+
/** A switch group's body statements (everything but its case-test value). */
|
|
471
|
+
caseStatements(group) {
|
|
472
|
+
const value = group.childForFieldName('value');
|
|
473
|
+
return group.namedChildren.filter((c) => c.id !== value?.id && !isComment(c));
|
|
474
|
+
}
|
|
475
|
+
/** The case-test value expression of a `case_statement` (default has none). */
|
|
476
|
+
caseTest(group) {
|
|
477
|
+
return group.childForFieldName('value') ?? undefined;
|
|
478
|
+
}
|
|
479
|
+
// ── value-position match expression (#2207) ─────────────────────────────────
|
|
480
|
+
/**
|
|
481
|
+
* The `{expr, match}` of an `$x = match($v) {…}` value-position assignment
|
|
482
|
+
* carrier, or undefined. `expr` is the `assignment_expression` (for the target
|
|
483
|
+
* def); `match` is the modelable `match_expression` RHS. Only a plain `=`
|
|
484
|
+
* assignment qualifies (an augmented `??=` etc. is not a value-branch bind).
|
|
485
|
+
*/
|
|
486
|
+
assignmentBranch(stmt) {
|
|
487
|
+
if (stmt.type !== 'expression_statement')
|
|
488
|
+
return undefined;
|
|
489
|
+
const expr = stmt.namedChildren.find((c) => !isComment(c));
|
|
490
|
+
if (!expr || expr.type !== 'assignment_expression')
|
|
491
|
+
return undefined;
|
|
492
|
+
const right = expr.childForFieldName('right');
|
|
493
|
+
return right && this.isModelableValueBranch(right) ? { expr, match: right } : undefined;
|
|
494
|
+
}
|
|
495
|
+
/** Whether a statement is an `$x = match(…) {…}` value-branch assignment. */
|
|
496
|
+
isValueBranchAssignment(stmt) {
|
|
497
|
+
return this.assignmentBranch(stmt) !== undefined;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Whether `node` is a value-position branch worth modeling as control flow
|
|
501
|
+
* (#2207): a `match_expression` with ≥2 arms — a real dispatch. PHP `match` is
|
|
502
|
+
* the only value-position branch (there is no `if`-expression); the ternary
|
|
503
|
+
* `?:` is deliberately excluded, like elvis in Kotlin.
|
|
504
|
+
*/
|
|
505
|
+
isModelableValueBranch(node) {
|
|
506
|
+
if (node.type !== 'match_expression')
|
|
507
|
+
return false;
|
|
508
|
+
const block = node.childForFieldName('body');
|
|
509
|
+
if (!block)
|
|
510
|
+
return false;
|
|
511
|
+
return (block.namedChildren.filter((c) => c.type === 'match_conditional_expression' || c.type === 'match_default_expression').length >= 2);
|
|
512
|
+
}
|
|
513
|
+
/** Model a value-position branch as control flow (only `match_expression`). */
|
|
514
|
+
visitBranchExpr(node) {
|
|
515
|
+
return this.visitMatch(node);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Model a value-position `match($v) { c => v, default => v }` as a CFG dispatch:
|
|
519
|
+
* a discriminant block, each arm's value expression a block reached by a
|
|
520
|
+
* `switch-case` edge, all arms rejoining at one exit (no fallthrough — `match`
|
|
521
|
+
* never falls through). The arm condition lists are harvested as conditional
|
|
522
|
+
* uses on the dispatch (a later arm test runs only when earlier arms missed).
|
|
523
|
+
*/
|
|
524
|
+
visitMatch(node) {
|
|
525
|
+
const condRaw = node.childForFieldName('condition');
|
|
526
|
+
const cond = condRaw ? this.unwrapParen(condRaw) : node;
|
|
527
|
+
const dispatch = this.builder.newBlock(startLineOf(node), endLineOf(cond), cond.text, 'normal', this.harvest.facts(cond));
|
|
528
|
+
const matchExit = this.builder.newBlock(endLineOf(node), endLineOf(node), '');
|
|
529
|
+
const block = node.childForFieldName('body');
|
|
530
|
+
const arms = block
|
|
531
|
+
? block.namedChildren.filter((c) => c.type === 'match_conditional_expression' || c.type === 'match_default_expression')
|
|
532
|
+
: [];
|
|
533
|
+
let hasDefault = false;
|
|
534
|
+
for (const arm of arms) {
|
|
535
|
+
const condList = arm.namedChildren.find((c) => c.type === 'match_condition_list');
|
|
536
|
+
if (condList)
|
|
537
|
+
this.builder.attachFacts(dispatch, this.harvest.factsConditional(condList));
|
|
538
|
+
if (arm.type === 'match_default_expression')
|
|
539
|
+
hasDefault = true;
|
|
540
|
+
const value = this.matchArmValue(arm);
|
|
541
|
+
const armBlock = this.builder.newBlock(startLineOf(value ?? arm), endLineOf(value ?? arm), (value ?? arm).text, 'normal', value ? this.harvest.facts(value) : undefined);
|
|
542
|
+
this.builder.edge(dispatch, armBlock, 'switch-case');
|
|
543
|
+
this.builder.edge(armBlock, matchExit, 'seq');
|
|
544
|
+
}
|
|
545
|
+
// `match` with no `default` throws `\UnhandledMatchError` on no match; keep
|
|
546
|
+
// EXIT reachable via a conservative no-match edge when no default arm exists.
|
|
547
|
+
if (!hasDefault)
|
|
548
|
+
this.builder.edge(dispatch, matchExit, 'switch-case');
|
|
549
|
+
return { entry: dispatch, exits: [matchExit] };
|
|
550
|
+
}
|
|
551
|
+
/** The value (result) expression of a match arm — its LAST named child. */
|
|
552
|
+
matchArmValue(arm) {
|
|
553
|
+
const named = arm.namedChildren.filter((c) => !isComment(c));
|
|
554
|
+
return named[named.length - 1];
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* `$x = match($v) { … }` (#2207): visit the match as control flow, then rejoin
|
|
558
|
+
* its arms at a facts-only continuation carrying ONLY the LHS target def (the
|
|
559
|
+
* condition + arm-value uses are already on the match's blocks). The arms are
|
|
560
|
+
* now control-dependent on the dispatch — mirrors the Ruby value-branch assign.
|
|
561
|
+
*/
|
|
562
|
+
visitBindAssign(stmt, assignExpr, branch) {
|
|
563
|
+
const res = this.visitBranchExpr(branch);
|
|
564
|
+
const cont = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), '', 'normal', this.harvest.assignmentDefFacts(assignExpr));
|
|
565
|
+
this.builder.connect(res.exits, cont, 'seq');
|
|
566
|
+
return { entry: res.entry, exits: [cont] };
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* try / catch / finally. A `finally` runs on BOTH normal and exception exit —
|
|
570
|
+
* a `return`/`break`/`continue` crossing it threads through it (`finally-*`
|
|
571
|
+
* completion edges).
|
|
572
|
+
*/
|
|
573
|
+
visitTry(stmt) {
|
|
574
|
+
const bodyNode = stmt.childForFieldName('body');
|
|
575
|
+
const catchClauses = [];
|
|
576
|
+
let finallyClause;
|
|
577
|
+
for (let i = 0; i < stmt.namedChildCount; i++) {
|
|
578
|
+
const c = stmt.namedChild(i);
|
|
579
|
+
if (c?.type === 'catch_clause')
|
|
580
|
+
catchClauses.push(c);
|
|
581
|
+
else if (c?.type === 'finally_clause')
|
|
582
|
+
finallyClause = c;
|
|
583
|
+
}
|
|
584
|
+
const finallyBody = finallyClause?.childForFieldName('body');
|
|
585
|
+
return this.buildProtected(bodyNode ?? null, catchClauses, finallyBody ?? null);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Shared try/catch/finally builder (mirrors the Java visitor). `catchClauses`
|
|
589
|
+
* may be empty; `finallyBody` is the explicit finally's body (or null).
|
|
590
|
+
*
|
|
591
|
+
* Normal completion of try AND catch flows through the finally; a throw in the
|
|
592
|
+
* protected region routes to the handler; early exits crossing the finally
|
|
593
|
+
* thread through it (`finally-*` completion edges).
|
|
594
|
+
*/
|
|
595
|
+
buildProtected(bodyNode, catchClauses, finallyBody) {
|
|
596
|
+
const finallyRes = finallyBody ? this.visitBody(finallyBody) : null;
|
|
597
|
+
const finFrame = finallyRes ? this.cfc.pushFinalizer(finallyRes.entry) : null;
|
|
598
|
+
const finalizerEntry = finallyRes?.entry;
|
|
599
|
+
// Build each catch handler.
|
|
600
|
+
const catchEntries = [];
|
|
601
|
+
const catchExits = [];
|
|
602
|
+
let firstCatchEntry;
|
|
603
|
+
for (const clause of catchClauses) {
|
|
604
|
+
const clauseBody = clause.childForFieldName('body');
|
|
605
|
+
if (finalizerEntry !== undefined)
|
|
606
|
+
this.handlers.push(finalizerEntry);
|
|
607
|
+
let res = clauseBody ? this.visitBody(clauseBody) : null;
|
|
608
|
+
if (finalizerEntry !== undefined)
|
|
609
|
+
this.handlers.pop();
|
|
610
|
+
if (res === null) {
|
|
611
|
+
// Empty `catch {}` still catches — synthesize one block so exception flow
|
|
612
|
+
// lands somewhere and the post-try code stays reachable.
|
|
613
|
+
const idx = this.builder.newBlock(startLineOf(clause), endLineOf(clause), '');
|
|
614
|
+
res = { entry: idx, exits: [idx] };
|
|
615
|
+
}
|
|
616
|
+
const paramFacts = this.harvest.catchParamFacts(clause);
|
|
617
|
+
if (paramFacts) {
|
|
618
|
+
const paramBlock = this.builder.newBlock(startLineOf(clause), startLineOf(clause), '', 'normal', paramFacts);
|
|
619
|
+
this.builder.edge(paramBlock, res.entry, 'seq');
|
|
620
|
+
res = { entry: paramBlock, exits: res.exits };
|
|
621
|
+
}
|
|
622
|
+
catchEntries.push(res.entry);
|
|
623
|
+
catchExits.push(...res.exits);
|
|
624
|
+
if (firstCatchEntry === undefined)
|
|
625
|
+
firstCatchEntry = res.entry;
|
|
626
|
+
}
|
|
627
|
+
// Handler for the try body: first catch if present, else the finally, else
|
|
628
|
+
// the outer handler.
|
|
629
|
+
const tryHandler = firstCatchEntry ?? finalizerEntry ?? this.currentHandler();
|
|
630
|
+
const protectedStart = this.builder.blockCount;
|
|
631
|
+
this.handlers.push(tryHandler);
|
|
632
|
+
const bodyRes = bodyNode ? this.visitBody(bodyNode) : null;
|
|
633
|
+
this.handlers.pop();
|
|
634
|
+
if (catchClauses.length > 0 || finalizerEntry !== undefined) {
|
|
635
|
+
for (let b = protectedStart; b < this.builder.blockCount; b++) {
|
|
636
|
+
this.builder.edge(b, tryHandler, 'throw');
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Pop the finalizer frame and drain its pending crossing-jump legs.
|
|
640
|
+
if (finFrame && finallyRes) {
|
|
641
|
+
this.cfc.pop();
|
|
642
|
+
drainFinalizerPending(this.builder, finFrame, finallyRes.exits);
|
|
643
|
+
}
|
|
644
|
+
const exits = [];
|
|
645
|
+
if (finalizerEntry !== undefined && finallyRes) {
|
|
646
|
+
if (bodyRes)
|
|
647
|
+
this.builder.connect(bodyRes.exits, finalizerEntry, 'seq');
|
|
648
|
+
for (const e of catchExits)
|
|
649
|
+
this.builder.edge(e, finalizerEntry, 'seq');
|
|
650
|
+
exits.push(...finallyRes.exits);
|
|
651
|
+
// No catch → an exception re-propagates out after the finally runs.
|
|
652
|
+
if (catchClauses.length === 0) {
|
|
653
|
+
this.builder.connect(finallyRes.exits, this.currentHandler(), 'throw');
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
if (bodyRes)
|
|
658
|
+
exits.push(...bodyRes.exits);
|
|
659
|
+
exits.push(...catchExits);
|
|
660
|
+
}
|
|
661
|
+
const entry = bodyRes?.entry ?? finalizerEntry ?? catchEntries[0];
|
|
662
|
+
if (entry === undefined)
|
|
663
|
+
return null;
|
|
664
|
+
return { entry, exits: [...new Set(exits)] };
|
|
665
|
+
}
|
|
666
|
+
/** Nearest enclosing exception handler, or the function EXIT. */
|
|
667
|
+
currentHandler() {
|
|
668
|
+
return this.handlers.length ? this.handlers[this.handlers.length - 1] : this.builder.exitIndex;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/** Build the CFG for one PHP function node, or `undefined` if not modelable. */
|
|
672
|
+
function buildFunctionCfg(fnNode, filePath) {
|
|
673
|
+
try {
|
|
674
|
+
if (!PHP_FUNCTION_TYPES.has(fnNode.type))
|
|
675
|
+
return undefined;
|
|
676
|
+
const startLine = startLineOf(fnNode);
|
|
677
|
+
const endLine = endLineOf(fnNode);
|
|
678
|
+
const startColumn = fnNode.startPosition.column;
|
|
679
|
+
const body = fnNode.childForFieldName('body');
|
|
680
|
+
if (!body)
|
|
681
|
+
return undefined; // abstract / interface method — no body
|
|
682
|
+
const builder = new CfgBuilder(filePath, startLine, endLine, startColumn);
|
|
683
|
+
const harvest = new PhpHarvester(fnNode);
|
|
684
|
+
const paramFacts = harvest.paramFacts();
|
|
685
|
+
if (paramFacts)
|
|
686
|
+
builder.attachFacts(builder.entryIndex, paramFacts);
|
|
687
|
+
if (fnNode.type === 'arrow_function' || body.type !== 'compound_statement') {
|
|
688
|
+
// `fn($x) => expr` — the body is an EXPRESSION (no block): one block whose
|
|
689
|
+
// value is returned.
|
|
690
|
+
const blk = builder.newBlock(startLineOf(body), endLineOf(body), body.text, 'normal', harvest.facts(body));
|
|
691
|
+
builder.edge(builder.entryIndex, blk, 'seq');
|
|
692
|
+
builder.edge(blk, builder.exitIndex, 'return');
|
|
693
|
+
return builder.finish(harvest.bindingTable());
|
|
694
|
+
}
|
|
695
|
+
const walk = new PhpCfgWalk(builder, harvest);
|
|
696
|
+
const res = walk.visitSeq(body.namedChildren.filter((c) => c.type !== 'comment'));
|
|
697
|
+
if (!res) {
|
|
698
|
+
builder.edge(builder.entryIndex, builder.exitIndex, 'seq'); // empty body
|
|
699
|
+
return builder.finish(harvest.bindingTable());
|
|
700
|
+
}
|
|
701
|
+
builder.edge(builder.entryIndex, res.entry, 'seq');
|
|
702
|
+
builder.connect(res.exits, builder.exitIndex, 'seq'); // normal fall-off → EXIT
|
|
703
|
+
return builder.finish(harvest.bindingTable());
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
// Never throw out of buildFunctionCfg — a malformed AST shape must skip only
|
|
707
|
+
// this one function's CFG, never drop the whole file's language group (R4).
|
|
708
|
+
// eslint-disable-next-line no-console
|
|
709
|
+
console.warn(`[cfg] PHP buildFunctionCfg skipped a function in ${filePath}: ${String(err)}`);
|
|
710
|
+
return undefined;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/** Whether a node is a PHP function this visitor builds a CFG for. */
|
|
714
|
+
function isFunction(node) {
|
|
715
|
+
return PHP_FUNCTION_TYPES.has(node.type);
|
|
716
|
+
}
|
|
717
|
+
/** The PHP CFG visitor. */
|
|
718
|
+
export function createPhpCfgVisitor() {
|
|
719
|
+
return { buildFunctionCfg, isFunction };
|
|
720
|
+
}
|
|
721
|
+
export { PHP_FUNCTION_TYPES };
|