@backbencher/brain 0.0.0-stage → 0.1.0
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/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-KY7K552E.js +17 -0
- package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2RTN2DFU.js +711 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3LFTSMIV.js +174 -0
- package/dist/chunk-47HKX4AJ.js +930 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5SD45FLC.js +818 -0
- package/dist/chunk-66A5ZFYA.js +1758 -0
- package/dist/chunk-6AIRWAO2.js +247 -0
- package/dist/chunk-6WNGD3NN.js +196 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-BHB7BYYI.js +626 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-CW4XXSNN.js +1479 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-FVVQTSIM.js +3427 -0
- package/dist/chunk-FWSL4VG6.js +1290 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HOWAQXJO.js +236 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-IFZNR5UJ.js +2863 -0
- package/dist/chunk-J4PMBXQQ.js +336 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-KLK3PBTO.js +1119 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-PJTNGD3Z.js +6924 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RH7KC5HU.js +12050 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UFE77L67.js +782 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-V44PGPA6.js +2104 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-VURYFIHW.js +201 -0
- package/dist/chunk-XJKJYIPP.js +7436 -0
- package/dist/chunk-YI6FF2ZR.js +340 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-642F7E5V.js +593 -0
- package/dist/database-TCTZDLE5.js +9 -0
- package/dist/doctor-GXCKSJOX.js +395 -0
- package/dist/edit-engine-KM7MPFEC.js +51 -0
- package/dist/engine-42TZFNIZ.js +11 -0
- package/dist/engine-LO6L4JLQ.js +18 -0
- package/dist/fingerprint-XDXFRWO6.js +33 -0
- package/dist/framework-intelligence-DWFCKV44.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-PL5YA7BA.js +21 -0
- package/dist/graph_request-MM4REP6H.js +350 -0
- package/dist/impact_request-WDE47IZ5.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-6BD7UBHR.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-4PW3AFXP.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-ZS5TS2Z6.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-DGZBIZS7.js +355 -0
- package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
- package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
- package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
- package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
- package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
- package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
- package/dist/real-css-analyzer-HPU277Q6.js +15 -0
- package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
- package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
- package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
- package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
- package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
- package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
- package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
- package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
- package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
- package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
- package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
- package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
- package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
- package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
- package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
- package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
- package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
- package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
- package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
- package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
- package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
- package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
- package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
- package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
- package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
- package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
- package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
- package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
- package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
- package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
- package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
- package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
- package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
- package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
- package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
- package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
- package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
- package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
- package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
- package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
- package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
- package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
- package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
- package/dist/regex-search-OPTLKUN5.js +239 -0
- package/dist/rules-I453JTRC.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-Y5GB5KXA.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-KB4MU4SI.js +16 -0
- package/dist/taint-YOTN5X55.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-G4NAGJIH.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-RF675A3C.js +32 -0
- package/dist/type-inference-4Y7WTMFE.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-LXGNFO37.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,1865 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTEngine
|
|
3
|
+
} from "./chunk-V44PGPA6.js";
|
|
4
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
5
|
+
import "./chunk-SK6YVHFZ.js";
|
|
6
|
+
import "./chunk-2BSKD7O3.js";
|
|
7
|
+
import "./chunk-KAET47ZL.js";
|
|
8
|
+
import "./chunk-KCDECRJY.js";
|
|
9
|
+
import "./chunk-MLKGABMK.js";
|
|
10
|
+
|
|
11
|
+
// src/ast/real-perl-analyzer.ts
|
|
12
|
+
var PERL_MAGIC_TOKENS = /* @__PURE__ */ new Set(["__PACKAGE__", "__FILE__", "__LINE__", "__SUB__", "__DATA__", "__END__"]);
|
|
13
|
+
var isPerlIdentStart = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
|
|
14
|
+
var isPerlIdentChar = (c) => isPerlIdentStart(c) || c >= "0" && c <= "9";
|
|
15
|
+
var PERL_BUILTINS = /* @__PURE__ */ new Set([
|
|
16
|
+
"abs",
|
|
17
|
+
"accept",
|
|
18
|
+
"alarm",
|
|
19
|
+
"atan2",
|
|
20
|
+
"bind",
|
|
21
|
+
"binmode",
|
|
22
|
+
"bless",
|
|
23
|
+
"break",
|
|
24
|
+
"caller",
|
|
25
|
+
"catch",
|
|
26
|
+
"chdir",
|
|
27
|
+
"chmod",
|
|
28
|
+
"chomp",
|
|
29
|
+
"chop",
|
|
30
|
+
"chown",
|
|
31
|
+
"chr",
|
|
32
|
+
"chroot",
|
|
33
|
+
"close",
|
|
34
|
+
"closedir",
|
|
35
|
+
"cmp",
|
|
36
|
+
"connect",
|
|
37
|
+
"continue",
|
|
38
|
+
"cos",
|
|
39
|
+
"crypt",
|
|
40
|
+
"dbmclose",
|
|
41
|
+
"dbmopen",
|
|
42
|
+
"defined",
|
|
43
|
+
"delete",
|
|
44
|
+
"die",
|
|
45
|
+
"do",
|
|
46
|
+
"dump",
|
|
47
|
+
"each",
|
|
48
|
+
"eof",
|
|
49
|
+
"eval",
|
|
50
|
+
"exec",
|
|
51
|
+
"exists",
|
|
52
|
+
"exit",
|
|
53
|
+
"exp",
|
|
54
|
+
"fc",
|
|
55
|
+
"fcntl",
|
|
56
|
+
"fileno",
|
|
57
|
+
"flock",
|
|
58
|
+
"fork",
|
|
59
|
+
"format",
|
|
60
|
+
"formline",
|
|
61
|
+
"getc",
|
|
62
|
+
"getgrent",
|
|
63
|
+
"getgrgid",
|
|
64
|
+
"getgrnam",
|
|
65
|
+
"gethostbyaddr",
|
|
66
|
+
"gethostbyname",
|
|
67
|
+
"getlogin",
|
|
68
|
+
"getnetbyaddr",
|
|
69
|
+
"getnetbyname",
|
|
70
|
+
"getpeername",
|
|
71
|
+
"getpgrp",
|
|
72
|
+
"getppid",
|
|
73
|
+
"getpriority",
|
|
74
|
+
"getprotobyname",
|
|
75
|
+
"getprotobynumber",
|
|
76
|
+
"getpwnam",
|
|
77
|
+
"getpwuid",
|
|
78
|
+
"getservbyname",
|
|
79
|
+
"getservbyport",
|
|
80
|
+
"getsockname",
|
|
81
|
+
"getsockopt",
|
|
82
|
+
"glob",
|
|
83
|
+
"gmtime",
|
|
84
|
+
"goto",
|
|
85
|
+
"grep",
|
|
86
|
+
"hex",
|
|
87
|
+
"if",
|
|
88
|
+
"index",
|
|
89
|
+
"int",
|
|
90
|
+
"ioctl",
|
|
91
|
+
"join",
|
|
92
|
+
"keys",
|
|
93
|
+
"kill",
|
|
94
|
+
"last",
|
|
95
|
+
"lc",
|
|
96
|
+
"lcfirst",
|
|
97
|
+
"length",
|
|
98
|
+
"link",
|
|
99
|
+
"listen",
|
|
100
|
+
"local",
|
|
101
|
+
"localtime",
|
|
102
|
+
"lock",
|
|
103
|
+
"log",
|
|
104
|
+
"lstat",
|
|
105
|
+
"map",
|
|
106
|
+
"mkdir",
|
|
107
|
+
"msgctl",
|
|
108
|
+
"msgget",
|
|
109
|
+
"msgrcv",
|
|
110
|
+
"msgsnd",
|
|
111
|
+
"my",
|
|
112
|
+
"next",
|
|
113
|
+
"oct",
|
|
114
|
+
"open",
|
|
115
|
+
"opendir",
|
|
116
|
+
"ord",
|
|
117
|
+
"pack",
|
|
118
|
+
"pipe",
|
|
119
|
+
"pop",
|
|
120
|
+
"pos",
|
|
121
|
+
"print",
|
|
122
|
+
"printf",
|
|
123
|
+
"prototype",
|
|
124
|
+
"push",
|
|
125
|
+
"quotemeta",
|
|
126
|
+
"rand",
|
|
127
|
+
"read",
|
|
128
|
+
"readdir",
|
|
129
|
+
"readline",
|
|
130
|
+
"readlink",
|
|
131
|
+
"readpipe",
|
|
132
|
+
"recv",
|
|
133
|
+
"redo",
|
|
134
|
+
"ref",
|
|
135
|
+
"rename",
|
|
136
|
+
"require",
|
|
137
|
+
"reset",
|
|
138
|
+
"return",
|
|
139
|
+
"reverse",
|
|
140
|
+
"rewinddir",
|
|
141
|
+
"rindex",
|
|
142
|
+
"rmdir",
|
|
143
|
+
"say",
|
|
144
|
+
"scalar",
|
|
145
|
+
"seek",
|
|
146
|
+
"seekdir",
|
|
147
|
+
"select",
|
|
148
|
+
"semctl",
|
|
149
|
+
"semget",
|
|
150
|
+
"semop",
|
|
151
|
+
"send",
|
|
152
|
+
"setpgrp",
|
|
153
|
+
"setpriority",
|
|
154
|
+
"setsockopt",
|
|
155
|
+
"shift",
|
|
156
|
+
"shmctl",
|
|
157
|
+
"shmget",
|
|
158
|
+
"shmread",
|
|
159
|
+
"shmwrite",
|
|
160
|
+
"shutdown",
|
|
161
|
+
"sin",
|
|
162
|
+
"sleep",
|
|
163
|
+
"socket",
|
|
164
|
+
"socketpair",
|
|
165
|
+
"sort",
|
|
166
|
+
"splice",
|
|
167
|
+
"split",
|
|
168
|
+
"sprintf",
|
|
169
|
+
"sqrt",
|
|
170
|
+
"srand",
|
|
171
|
+
"stat",
|
|
172
|
+
"state",
|
|
173
|
+
"study",
|
|
174
|
+
"substr",
|
|
175
|
+
"symlink",
|
|
176
|
+
"syscall",
|
|
177
|
+
"sysopen",
|
|
178
|
+
"sysread",
|
|
179
|
+
"sysseek",
|
|
180
|
+
"system",
|
|
181
|
+
"syswrite",
|
|
182
|
+
"tell",
|
|
183
|
+
"telldir",
|
|
184
|
+
"tie",
|
|
185
|
+
"tied",
|
|
186
|
+
"time",
|
|
187
|
+
"times",
|
|
188
|
+
"truncate",
|
|
189
|
+
"uc",
|
|
190
|
+
"ucfirst",
|
|
191
|
+
"umask",
|
|
192
|
+
"undef",
|
|
193
|
+
"unlink",
|
|
194
|
+
"unpack",
|
|
195
|
+
"unshift",
|
|
196
|
+
"untie",
|
|
197
|
+
"until",
|
|
198
|
+
"use",
|
|
199
|
+
"utime",
|
|
200
|
+
"values",
|
|
201
|
+
"vec",
|
|
202
|
+
"wait",
|
|
203
|
+
"waitpid",
|
|
204
|
+
"wantarray",
|
|
205
|
+
"warn",
|
|
206
|
+
"while",
|
|
207
|
+
"write",
|
|
208
|
+
"xor",
|
|
209
|
+
"our"
|
|
210
|
+
]);
|
|
211
|
+
var PERL_REASON_CODES = {
|
|
212
|
+
nameOnlyCallee: "name-only-callee",
|
|
213
|
+
dynamicReceiver: "dynamic-receiver",
|
|
214
|
+
concreteInvocant: "concrete-invocant",
|
|
215
|
+
useParentDeclaration: "use-parent-declaration",
|
|
216
|
+
isaArray: "isa-array",
|
|
217
|
+
mojoBaseDeclaration: "mojo-base-declaration",
|
|
218
|
+
mooRoleComposition: "moo-role-composition",
|
|
219
|
+
mooExtendsKeyword: "moo-extends-keyword",
|
|
220
|
+
indirectObjectInvocation: "indirect-object-invocation",
|
|
221
|
+
useRoleComposition: "use-role-composition",
|
|
222
|
+
// THE IMPORT AND EXPORT EDGES ✓✓ — 2026-10-21 ✓✓.
|
|
223
|
+
//
|
|
224
|
+
// The corpus EXPRESSES both ✓✓ (3,539 use/require sites ✓✓, 33 @EXPORT
|
|
225
|
+
// conventions in 11 files ✓✓) and the ARM's TABLE 1 carried `imports 0` and
|
|
226
|
+
// `exports 0` ✓✓, because the facts went into the analyzer's OUTPUT SHAPE ✓✓
|
|
227
|
+
// and never became RELATIONSHIPS ✓✓. These two codes are the receipt ✓✓.
|
|
228
|
+
moduleImport: "module-import",
|
|
229
|
+
exportedSymbol: "exported-symbol",
|
|
230
|
+
exportedArrayPush: "exporter-array-push",
|
|
231
|
+
exportedArrayDeclaration: "exporter-array-declaration",
|
|
232
|
+
exporterSurface: "exporter-surface",
|
|
233
|
+
useModuleLoad: "use-module-load",
|
|
234
|
+
runtimeRequire: "runtime-require",
|
|
235
|
+
isaArrayPush: "isa-array-push",
|
|
236
|
+
baseDeclaresMethod: "base-declares-method",
|
|
237
|
+
firstParentGuess: "first-parent-guess",
|
|
238
|
+
magicTokenInvocant: "magic-token-invocant",
|
|
239
|
+
codeReference: "code-reference",
|
|
240
|
+
qualifiedVariableRead: "qualified-variable-read",
|
|
241
|
+
qualifiedNameAccess: "qualified-name-access",
|
|
242
|
+
blessClassLiteral: "bless-class-literal",
|
|
243
|
+
refClassComparison: "ref-class-comparison",
|
|
244
|
+
universalIsaLiteral: "universal-isa-literal",
|
|
245
|
+
isaClassLiteral: "isa-class-literal",
|
|
246
|
+
doesRoleLiteral: "does-role-literal",
|
|
247
|
+
typeQueryLiteral: "type-query-literal"
|
|
248
|
+
};
|
|
249
|
+
var REASON = PERL_REASON_CODES;
|
|
250
|
+
function perlFindChildByType(node, type) {
|
|
251
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
252
|
+
const child = node.namedChild(i);
|
|
253
|
+
if (child?.type === type) return child;
|
|
254
|
+
}
|
|
255
|
+
return void 0;
|
|
256
|
+
}
|
|
257
|
+
function perlWordValues(node) {
|
|
258
|
+
if (node.type === "string_content" || node.type === "autoquoted_bareword" || node.type === "bareword" || node.type === "package") return node.text ? splitWords(node.text) : [];
|
|
259
|
+
if (node.type === "quoted_word_list") {
|
|
260
|
+
const content = node.childForFieldName("content") || perlFindChildByType(node, "string_content");
|
|
261
|
+
return content ? splitWords(content.text) : [];
|
|
262
|
+
}
|
|
263
|
+
if (node.type === "string_literal") {
|
|
264
|
+
const content = node.childForFieldName("content") || perlFindChildByType(node, "string_content");
|
|
265
|
+
return [content ? content.text : unquote(node.text)];
|
|
266
|
+
}
|
|
267
|
+
if (node.type === "interpolated_string_literal") {
|
|
268
|
+
const values2 = [];
|
|
269
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
270
|
+
const child = node.namedChild(i);
|
|
271
|
+
if (!child) continue;
|
|
272
|
+
if (child.type === "string_content") {
|
|
273
|
+
for (const w of splitWords(child.text)) values2.push(w);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return values2;
|
|
277
|
+
}
|
|
278
|
+
if (node.type === "subroutine_declaration_statement" || node.type === "anonymous_subroutine_expression" || node.type === "block" || node.type === "block_statement") return [];
|
|
279
|
+
if (node.type === "conditional_expression") return [];
|
|
280
|
+
if (node.type === "function_call_expression" || node.type === "method_call_expression" || node.type === "func0op_call_expression" || node.type === "func1op_call_expression" || node.type === "coderef_call_expression" || node.type === "ambiguous_function_call_expression") return [];
|
|
281
|
+
if (node.type === "binary_expression" || node.type === "lowprec_logical_expression" || node.type === "logical_not_expression") {
|
|
282
|
+
const op = node.childForFieldName("operator") ?? node.children.find((c) => !c.isNamed);
|
|
283
|
+
const t = op ? op.type : "";
|
|
284
|
+
if (t === "and" || t === "or" || t === "||" || t === "&&" || t === "//") return [];
|
|
285
|
+
}
|
|
286
|
+
const values = [];
|
|
287
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
288
|
+
const child = node.namedChild(i);
|
|
289
|
+
if (child) values.push(...perlWordValues(child));
|
|
290
|
+
}
|
|
291
|
+
return values;
|
|
292
|
+
}
|
|
293
|
+
function unquote(text) {
|
|
294
|
+
if (text.length >= 2) {
|
|
295
|
+
const first = text[0];
|
|
296
|
+
const last = text[text.length - 1];
|
|
297
|
+
if (first === "'" && last === "'" || first === '"' && last === '"') return text.slice(1, -1);
|
|
298
|
+
}
|
|
299
|
+
return text;
|
|
300
|
+
}
|
|
301
|
+
function isPerlPragma(module) {
|
|
302
|
+
return PERL_PRAGMAS.has(module);
|
|
303
|
+
}
|
|
304
|
+
var PERL_PRAGMAS = /* @__PURE__ */ new Set([
|
|
305
|
+
"strict",
|
|
306
|
+
"warnings",
|
|
307
|
+
"integer",
|
|
308
|
+
"feature",
|
|
309
|
+
"lib",
|
|
310
|
+
"utf8",
|
|
311
|
+
"bytes",
|
|
312
|
+
"constant",
|
|
313
|
+
"vars",
|
|
314
|
+
"parent",
|
|
315
|
+
"base",
|
|
316
|
+
"overload",
|
|
317
|
+
"bigint",
|
|
318
|
+
"bignum",
|
|
319
|
+
"bigrat",
|
|
320
|
+
"open",
|
|
321
|
+
"charnames",
|
|
322
|
+
"autodie",
|
|
323
|
+
"re",
|
|
324
|
+
"attributes",
|
|
325
|
+
"sigtrap",
|
|
326
|
+
"sort",
|
|
327
|
+
"subs",
|
|
328
|
+
"threads",
|
|
329
|
+
"fields",
|
|
330
|
+
"if",
|
|
331
|
+
"less",
|
|
332
|
+
"locale",
|
|
333
|
+
"mro",
|
|
334
|
+
"ops",
|
|
335
|
+
"version",
|
|
336
|
+
"vmsish",
|
|
337
|
+
"arybase",
|
|
338
|
+
"deprecated",
|
|
339
|
+
"experimental",
|
|
340
|
+
"diagnostics",
|
|
341
|
+
"sigtrap",
|
|
342
|
+
"class",
|
|
343
|
+
"builtin"
|
|
344
|
+
]);
|
|
345
|
+
var PERL_BUILTIN_REF_TYPES = /* @__PURE__ */ new Set(["ARRAY", "HASH", "SCALAR", "CODE", "REF", "GLOB", "LVALUE", "REGEXP", "IO", "VSTRING", "UNKNOWN"]);
|
|
346
|
+
function splitWords(text) {
|
|
347
|
+
const result = [];
|
|
348
|
+
let current = "";
|
|
349
|
+
for (const char of text) {
|
|
350
|
+
if (char === " " || char === " " || char === "\n" || char === ",") {
|
|
351
|
+
if (current) result.push(current);
|
|
352
|
+
current = "";
|
|
353
|
+
} else current += char;
|
|
354
|
+
}
|
|
355
|
+
if (current) result.push(current);
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
function isArgumentSpecification(statement) {
|
|
359
|
+
const WRAPPERS = /* @__PURE__ */ new Set(["list_expression", "array_expression", "hash_expression", "anonymous_array_expression", "anonymous_hash_expression"]);
|
|
360
|
+
const hasFatArrow = (n) => {
|
|
361
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
362
|
+
const child = n.child(i);
|
|
363
|
+
if (!child) continue;
|
|
364
|
+
if (!child.isNamed && child.type === "=>") return true;
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
};
|
|
368
|
+
const nodes = [statement, ...Array.from({ length: statement.childCount }, (_, i) => statement.child(i)).filter(Boolean)];
|
|
369
|
+
for (const n of nodes) {
|
|
370
|
+
if (!n) continue;
|
|
371
|
+
if (hasFatArrow(n)) return true;
|
|
372
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
373
|
+
const child = n.child(i);
|
|
374
|
+
if (child && WRAPPERS.has(child.type) && hasFatArrow(child)) return true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
function perlArgumentNodes(node) {
|
|
380
|
+
const module = node.childForFieldName("module");
|
|
381
|
+
const fromField = [];
|
|
382
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
383
|
+
const child = node.child(i);
|
|
384
|
+
if (child && node.fieldNameForChild(i) === "arguments" && perlWordValues(child).length > 0) fromField.push(child);
|
|
385
|
+
}
|
|
386
|
+
if (fromField.length) return fromField;
|
|
387
|
+
const list = perlFindChildByType(node, "list_expression");
|
|
388
|
+
if (list) return list.namedChildren.filter((c) => !!c && perlWordValues(c).length > 0);
|
|
389
|
+
const result = [];
|
|
390
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
391
|
+
const child = node.namedChild(i);
|
|
392
|
+
if (!child || child === module) continue;
|
|
393
|
+
if (child.type === "string_literal" || child.type === "interpolated_string_literal" || child.type === "quoted_word_list" || child.type === "autoquoted_bareword") {
|
|
394
|
+
if (perlWordValues(child).length > 0) result.push(child);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
function hasArgumentSyntax(node) {
|
|
400
|
+
if (node.childrenForFieldName("arguments").length > 0) return true;
|
|
401
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
402
|
+
const t = node.child(i)?.type;
|
|
403
|
+
if (t === "(" || t === ")") return true;
|
|
404
|
+
}
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
var RealPerlAnalyzer = class {
|
|
408
|
+
analyze(rootNode, content, _filePath, context) {
|
|
409
|
+
const symbols = [];
|
|
410
|
+
const relationships = [];
|
|
411
|
+
const imports = [];
|
|
412
|
+
const exports = [];
|
|
413
|
+
const errors = [];
|
|
414
|
+
const frameworks = /* @__PURE__ */ new Set(["Perl"]);
|
|
415
|
+
const patterns = /* @__PURE__ */ new Set();
|
|
416
|
+
const symbolKeys = /* @__PURE__ */ new Set();
|
|
417
|
+
const symbolsByKey = /* @__PURE__ */ new Map();
|
|
418
|
+
const relationshipKeys = /* @__PURE__ */ new Set();
|
|
419
|
+
const packageBases = /* @__PURE__ */ new Map();
|
|
420
|
+
const packageBaseOrigin = /* @__PURE__ */ new Map();
|
|
421
|
+
const packageBaseLine = /* @__PURE__ */ new Map();
|
|
422
|
+
const packageMethods = /* @__PURE__ */ new Map();
|
|
423
|
+
const deferredIsa = [];
|
|
424
|
+
const indirectObjectClassNodes = /* @__PURE__ */ new Set();
|
|
425
|
+
const nodeKey = (n) => `${n.startIndex}:${n.endIndex}:${n.type}`;
|
|
426
|
+
const versionRequirements = [];
|
|
427
|
+
if (!rootNode) return this.result(symbols, relationships, imports, exports, errors, content, frameworks, patterns);
|
|
428
|
+
this.collectErrors(rootNode, errors);
|
|
429
|
+
const addSymbol = (name, kind, node, state, extra = {}, ownerOverride, nameNode) => {
|
|
430
|
+
if (!name) return void 0;
|
|
431
|
+
const owner = ownerOverride ?? state.ownerName;
|
|
432
|
+
const key = `${kind}${owner || ""}${name}`;
|
|
433
|
+
if (symbolKeys.has(key)) return symbolsByKey.get(key);
|
|
434
|
+
const nameLineSource = nameNode ?? node.childForFieldName("name") ?? this.findChildByType(node, "bareword");
|
|
435
|
+
const nameLine = name === node.text?.trim() ? node.startPosition.row + 1 : nameLineSource ? nameLineSource.startPosition.row + 1 : node.startPosition.row + 1;
|
|
436
|
+
const symbol = {
|
|
437
|
+
name,
|
|
438
|
+
kind,
|
|
439
|
+
range: this.nodeToRange(node),
|
|
440
|
+
scope: owner || state.packageName || void 0,
|
|
441
|
+
ownerName: owner,
|
|
442
|
+
ownerKind: owner ? state.ownerKind : void 0,
|
|
443
|
+
visibility: state.visibility,
|
|
444
|
+
metadata: { perlSpecific: true, nameLine, ...extra }
|
|
445
|
+
};
|
|
446
|
+
symbolKeys.add(key);
|
|
447
|
+
symbolsByKey.set(key, symbol);
|
|
448
|
+
symbols.push(symbol);
|
|
449
|
+
return symbol;
|
|
450
|
+
};
|
|
451
|
+
const nameLineOf = (symbol) => {
|
|
452
|
+
const recorded = symbol.metadata?.nameLine;
|
|
453
|
+
if (typeof recorded === "number" && recorded > 0) return recorded;
|
|
454
|
+
const start = symbol.range?.startLine;
|
|
455
|
+
return typeof start === "number" && start > 0 ? start : void 0;
|
|
456
|
+
};
|
|
457
|
+
const addRelationship = (source, target, type, metadata2 = {}, confidence = 0.9, lineNode, explicitLine) => {
|
|
458
|
+
if (!source || !target) return;
|
|
459
|
+
const key = `${type}${source}${target}`;
|
|
460
|
+
if (relationshipKeys.has(key)) return;
|
|
461
|
+
relationshipKeys.add(key);
|
|
462
|
+
const line = explicitLine ?? (lineNode ? lineNode.startPosition.row + 1 : void 0);
|
|
463
|
+
relationships.push({
|
|
464
|
+
source,
|
|
465
|
+
target,
|
|
466
|
+
type,
|
|
467
|
+
confidence,
|
|
468
|
+
...typeof line === "number" && line > 0 ? { line_number: line } : {},
|
|
469
|
+
metadata: { perlSpecific: true, ...metadata2 }
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
const addReferenceName = (rawName, nameNode, state, reasonCode) => {
|
|
473
|
+
const leaf = this.leafName(rawName);
|
|
474
|
+
const source = state.functionName || state.ownerName || state.packageName || "global";
|
|
475
|
+
if (!leaf || leaf === source) return;
|
|
476
|
+
const qualifier = rawName.includes("::") ? rawName.slice(0, rawName.lastIndexOf("::")) : void 0;
|
|
477
|
+
addRelationship(source, leaf, ASTEngine.RelationshipType.References, { reasonCode, ...qualifier ? { qualifiedAs: rawName, qualifier } : {} }, 0.9, nameNode);
|
|
478
|
+
};
|
|
479
|
+
const addBase = (owner, parents, reasonCode, nameNode, parentLines) => {
|
|
480
|
+
const existing = packageBases.get(owner) || [];
|
|
481
|
+
for (const parent of parents) {
|
|
482
|
+
if (!parent) continue;
|
|
483
|
+
if (!existing.includes(parent)) {
|
|
484
|
+
existing.push(parent);
|
|
485
|
+
if (!packageBaseOrigin.has(owner)) packageBaseOrigin.set(owner, reasonCode);
|
|
486
|
+
}
|
|
487
|
+
const key = `${owner}${parent}`;
|
|
488
|
+
if (packageBaseLine.has(key)) continue;
|
|
489
|
+
const perParent = parentLines?.get(parent);
|
|
490
|
+
if (typeof perParent === "number") packageBaseLine.set(key, perParent);
|
|
491
|
+
else if (nameNode) packageBaseLine.set(key, nameNode.startPosition.row + 1);
|
|
492
|
+
}
|
|
493
|
+
packageBases.set(owner, existing);
|
|
494
|
+
};
|
|
495
|
+
const qualifiedClassNames = /* @__PURE__ */ new Set();
|
|
496
|
+
const addCall = (target, state, calleeNode, dynamicReceiver, callNode, builtinProbe) => {
|
|
497
|
+
const named = target.startsWith("&") ? target.slice(1) : target;
|
|
498
|
+
const probe = builtinProbe ?? named;
|
|
499
|
+
if (!target || PERL_BUILTINS.has(probe) || probe === "use" || probe === "no") return;
|
|
500
|
+
if (target.startsWith("&") && !hasArgumentSyntax(callNode ?? calleeNode)) {
|
|
501
|
+
const source = state.functionName || state.ownerName || state.packageName || "global";
|
|
502
|
+
const leaf = this.leafName(named);
|
|
503
|
+
if (leaf && leaf !== source) {
|
|
504
|
+
const qualifier = named.includes("::") ? named.slice(0, named.lastIndexOf("::")) : void 0;
|
|
505
|
+
addRelationship(source, leaf, ASTEngine.RelationshipType.Uses, { reasonCode: REASON.codeReference, ...qualifier ? { qualifiedAs: named, qualifier } : {} }, 0.9, calleeNode);
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
addRelationship(
|
|
510
|
+
state.functionName || state.ownerName || state.packageName || "global",
|
|
511
|
+
this.leafName(named),
|
|
512
|
+
ASTEngine.RelationshipType.Calls,
|
|
513
|
+
{ reasonCode: dynamicReceiver ? REASON.dynamicReceiver : REASON.nameOnlyCallee },
|
|
514
|
+
0.9,
|
|
515
|
+
calleeNode
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
const walk = (node, state) => {
|
|
519
|
+
switch (node.type) {
|
|
520
|
+
case "package_statement": {
|
|
521
|
+
const nameNode = node.childForFieldName("name") ?? void 0;
|
|
522
|
+
const name = nameNode?.text || "";
|
|
523
|
+
if (!name) break;
|
|
524
|
+
const kind = ASTEngine.SymbolKind.Class;
|
|
525
|
+
addSymbol(name, kind, nameNode ?? node, { ...state, packageName: name }, { isPackage: true });
|
|
526
|
+
patterns.add("Packages");
|
|
527
|
+
const block = node.childForFieldName("body") || this.findChildByType(node, "block");
|
|
528
|
+
if (block) {
|
|
529
|
+
walkChildren(block, {
|
|
530
|
+
...state,
|
|
531
|
+
packageName: name,
|
|
532
|
+
ownerName: name,
|
|
533
|
+
ownerKind: kind,
|
|
534
|
+
insideFunction: false,
|
|
535
|
+
bodyKind: "package"
|
|
536
|
+
});
|
|
537
|
+
} else {
|
|
538
|
+
state.packageName = name;
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
case "class_statement":
|
|
543
|
+
case "role_statement": {
|
|
544
|
+
const nameNode = node.childForFieldName("name") ?? void 0;
|
|
545
|
+
const name = nameNode?.text || "";
|
|
546
|
+
if (!name) break;
|
|
547
|
+
const isRole = node.type === "role_statement";
|
|
548
|
+
const kind = isRole ? ASTEngine.SymbolKind.Trait : ASTEngine.SymbolKind.Class;
|
|
549
|
+
addSymbol(name, kind, nameNode ?? node, { ...state, packageName: name }, isRole ? { isPerlRole: true } : { isPerlClass: true });
|
|
550
|
+
patterns.add(isRole ? "Roles" : "Classes");
|
|
551
|
+
frameworks.add("Perl 5.38 core OO");
|
|
552
|
+
const block = node.childForFieldName("body") || this.findChildByType(node, "block");
|
|
553
|
+
if (block) {
|
|
554
|
+
walkChildren(block, {
|
|
555
|
+
...state,
|
|
556
|
+
packageName: name,
|
|
557
|
+
ownerName: name,
|
|
558
|
+
ownerKind: kind,
|
|
559
|
+
insideFunction: false,
|
|
560
|
+
bodyKind: isRole ? "role" : "class"
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
case "subroutine_declaration_statement":
|
|
566
|
+
case "method_declaration_statement": {
|
|
567
|
+
if (state.insideFunction) {
|
|
568
|
+
const nestedBody = node.childForFieldName("body") || this.findChildByType(node, "block");
|
|
569
|
+
if (nestedBody) walkChildren(nestedBody, { ...state, insideFunction: true });
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const nameNode = node.childForFieldName("name") || this.findChildByType(node, "bareword");
|
|
573
|
+
const rawName = nameNode?.text || "";
|
|
574
|
+
if (!rawName) break;
|
|
575
|
+
const isConstructor = rawName === "new";
|
|
576
|
+
const name = isConstructor ? "constructor" : rawName;
|
|
577
|
+
const owner = state.packageName || state.ownerName;
|
|
578
|
+
const kind = isConstructor ? ASTEngine.SymbolKind.Constructor : owner ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function;
|
|
579
|
+
const symbol = addSymbol(name, kind, node, {
|
|
580
|
+
...state,
|
|
581
|
+
ownerName: owner,
|
|
582
|
+
ownerKind: owner ? ASTEngine.SymbolKind.Class : void 0
|
|
583
|
+
}, {
|
|
584
|
+
originalName: rawName,
|
|
585
|
+
isMethod: !!owner,
|
|
586
|
+
isConstructor
|
|
587
|
+
}, owner);
|
|
588
|
+
if (symbol) {
|
|
589
|
+
symbol.parameters = this.extractParameters(node);
|
|
590
|
+
symbol.ownerKind = owner ? ASTEngine.SymbolKind.Class : void 0;
|
|
591
|
+
const decorators = this.extractAttributes(node);
|
|
592
|
+
if (decorators.length) symbol.decorators = decorators;
|
|
593
|
+
}
|
|
594
|
+
if (owner && symbol) {
|
|
595
|
+
let methodSet = packageMethods.get(owner);
|
|
596
|
+
if (!methodSet) {
|
|
597
|
+
methodSet = /* @__PURE__ */ new Set();
|
|
598
|
+
packageMethods.set(owner, methodSet);
|
|
599
|
+
}
|
|
600
|
+
methodSet.add(rawName);
|
|
601
|
+
}
|
|
602
|
+
const body = node.childForFieldName("body") || this.findChildByType(node, "block");
|
|
603
|
+
if (body) walkChildren(body, { ...state, ownerName: owner, ownerKind: owner ? ASTEngine.SymbolKind.Class : void 0, functionName: name, insideFunction: true });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
case "variable_declaration": {
|
|
607
|
+
const declaration = this.declarationKind(node);
|
|
608
|
+
patterns.add("Variable Declarations");
|
|
609
|
+
const inClassBody = state.bodyKind === "class" || state.bodyKind === "role";
|
|
610
|
+
const isPackageGlobal = declaration === "our" && !state.insideFunction;
|
|
611
|
+
const isClassField = declaration === "field" && inClassBody && !state.insideFunction;
|
|
612
|
+
if (!isPackageGlobal && !isClassField) break;
|
|
613
|
+
for (const variable of this.variableChildren(node)) {
|
|
614
|
+
const varNameNode = this.findChildByType(variable, "varname");
|
|
615
|
+
const rawName = varNameNode?.text || "";
|
|
616
|
+
const name = this.stripSigil(rawName);
|
|
617
|
+
if (!name || name === "_") continue;
|
|
618
|
+
if (isClassField) {
|
|
619
|
+
addSymbol(name, ASTEngine.SymbolKind.Property, variable, state, { isPerlField: true, perlDeclType: declaration, sigil: variable.type }, state.packageName || state.ownerName);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
const kind = name === "ISA" ? ASTEngine.SymbolKind.Variable : this.isAllCapsName(name) ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Variable;
|
|
623
|
+
addSymbol(name, kind, variable, state, { perlDeclType: declaration, sigil: variable.type, ...name === "ISA" ? { isInheritanceVariable: true } : {} }, state.packageName || void 0);
|
|
624
|
+
}
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
case "refgen_expression": {
|
|
628
|
+
const fnNode = this.findChildByType(node, "function");
|
|
629
|
+
if (!fnNode) break;
|
|
630
|
+
const varname = this.findChildByType(fnNode, "varname");
|
|
631
|
+
if (!varname) break;
|
|
632
|
+
if (fnNode.namedChildCount !== 1) break;
|
|
633
|
+
const name = varname.text;
|
|
634
|
+
if (!name || !this.isWrittenCalleeName(name)) break;
|
|
635
|
+
const leaf = this.leafName(name);
|
|
636
|
+
const source = state.functionName || state.ownerName || state.packageName || "global";
|
|
637
|
+
if (leaf === source) break;
|
|
638
|
+
const qualifier = name.includes("::") ? name.slice(0, name.lastIndexOf("::")) : void 0;
|
|
639
|
+
addRelationship(source, leaf, ASTEngine.RelationshipType.Uses, { reasonCode: REASON.codeReference, ...qualifier ? { qualifiedAs: name, qualifier } : {} }, 0.9, varname);
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case "scalar":
|
|
643
|
+
case "array":
|
|
644
|
+
case "hash": {
|
|
645
|
+
const varname = this.findChildByType(node, "varname");
|
|
646
|
+
const qualified = varname?.text || "";
|
|
647
|
+
if (!qualified || !qualified.includes("::")) break;
|
|
648
|
+
if (!this.isWrittenCalleeName(qualified)) break;
|
|
649
|
+
const readSource = state.functionName || state.ownerName || state.packageName || "global";
|
|
650
|
+
addRelationship(readSource, qualified, ASTEngine.RelationshipType.Uses, { reasonCode: REASON.qualifiedVariableRead }, 0.9, varname);
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
case "equality_expression": {
|
|
654
|
+
const kids = [];
|
|
655
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
656
|
+
const c = node.namedChild(i);
|
|
657
|
+
if (c) kids.push(c);
|
|
658
|
+
}
|
|
659
|
+
const isRefCall = (n) => n.type === "func1op_call_expression" && String(n.child(0)?.text ?? "") === "ref";
|
|
660
|
+
const refKid = kids.find(isRefCall);
|
|
661
|
+
const litKid = kids.find((k) => k.type === "string_literal" || k.type === "interpolated_string_literal");
|
|
662
|
+
if (refKid && litKid) {
|
|
663
|
+
const word = this.wordValues(litKid)[0];
|
|
664
|
+
if (word && this.isWrittenCalleeName(word) && !PERL_BUILTIN_REF_TYPES.has(word)) addReferenceName(word, litKid, state, REASON.refClassComparison);
|
|
665
|
+
}
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
case "bareword": {
|
|
669
|
+
const text = String(node.text ?? "");
|
|
670
|
+
if (!text.includes("::") || text.endsWith("::")) break;
|
|
671
|
+
const parent = node.parent;
|
|
672
|
+
const parentType = parent ? String(parent.type) : "";
|
|
673
|
+
if (parentType === "hash_element_expression" || parentType === "slice_container_variable") break;
|
|
674
|
+
if (parent) {
|
|
675
|
+
let nextIsFatComma = false;
|
|
676
|
+
for (let i = 0; i < parent.childCount; i++) {
|
|
677
|
+
const child = parent.child(i);
|
|
678
|
+
if (!child || child.id !== node.id) continue;
|
|
679
|
+
const next = parent.child(i + 1);
|
|
680
|
+
if (next && !next.isNamed && next.type === "=>") nextIsFatComma = true;
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
if (nextIsFatComma) break;
|
|
684
|
+
}
|
|
685
|
+
const VALUE_PARENT = /* @__PURE__ */ new Set(["assignment_expression", "return_expression", "list_expression", "expression_statement", "keyval_expression"]);
|
|
686
|
+
if (!VALUE_PARENT.has(parentType)) break;
|
|
687
|
+
let ancestor = parent;
|
|
688
|
+
let insideDirective = false;
|
|
689
|
+
for (let depth = 0; ancestor && depth < 4; depth++) {
|
|
690
|
+
if (ancestor.type === "use_statement" || ancestor.type === "require_expression" || ancestor.type === "require_version_expression") {
|
|
691
|
+
insideDirective = true;
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
ancestor = ancestor.parent;
|
|
695
|
+
}
|
|
696
|
+
if (insideDirective) break;
|
|
697
|
+
const source = state.functionName || state.ownerName || state.packageName || "global";
|
|
698
|
+
addRelationship(source, text, ASTEngine.RelationshipType.Uses, { reasonCode: REASON.qualifiedNameAccess }, 0.9, node);
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
case "use_statement": {
|
|
702
|
+
if (node.child(0)?.type === "no") break;
|
|
703
|
+
const moduleNode = node.childForFieldName("module") ?? void 0;
|
|
704
|
+
const module = moduleNode?.text || "";
|
|
705
|
+
if (!module) break;
|
|
706
|
+
if (module === "constant") {
|
|
707
|
+
for (const constant of this.constantDeclarationNames(node)) {
|
|
708
|
+
addSymbol(constant.name, ASTEngine.SymbolKind.Constant, constant.node, state, { isPerlConstant: true }, state.packageName || void 0);
|
|
709
|
+
}
|
|
710
|
+
patterns.add("Constants");
|
|
711
|
+
frameworks.add("Compile-time Constants");
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
const args = this.argumentValues(node);
|
|
715
|
+
imports.push({
|
|
716
|
+
module,
|
|
717
|
+
items: args.length > 0 && !isArgumentSpecification(node) ? args : ["*"],
|
|
718
|
+
isDefault: true,
|
|
719
|
+
range: this.nodeToRange(node),
|
|
720
|
+
metadata: { perlSpecific: true, type: "use" }
|
|
721
|
+
});
|
|
722
|
+
patterns.add("Module Includes");
|
|
723
|
+
addRelationship(state.packageName || "global", module, ASTEngine.RelationshipType.Imports, {
|
|
724
|
+
reasonCode: REASON.useModuleLoad,
|
|
725
|
+
perlSpecific: true,
|
|
726
|
+
relationshipType: "use_module",
|
|
727
|
+
isImport: true,
|
|
728
|
+
fileLevel: true,
|
|
729
|
+
isPragma: isPerlPragma(module),
|
|
730
|
+
items: imports[imports.length - 1]?.items ?? []
|
|
731
|
+
}, 1, node);
|
|
732
|
+
if (module === "Moose" || module === "Moo" || module === "Mojo::Base") frameworks.add(module === "Mojo::Base" ? "Mojolicious" : "Moose/Moo OOP");
|
|
733
|
+
if (module === "parent" || module === "base") {
|
|
734
|
+
const parents = args.filter((arg) => !this.isOption(arg));
|
|
735
|
+
const parentLines = /* @__PURE__ */ new Map();
|
|
736
|
+
for (const argNode of this.argumentNodes(node)) {
|
|
737
|
+
for (const value of this.wordValues(argNode)) {
|
|
738
|
+
if (this.isOption(value)) continue;
|
|
739
|
+
const line = this.lineOfNameWithin(argNode, value);
|
|
740
|
+
if (line !== void 0) parentLines.set(value, line);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (state.packageName) addBase(state.packageName, parents, REASON.useParentDeclaration, moduleNode, parentLines);
|
|
744
|
+
} else if (module === "role") {
|
|
745
|
+
for (const argNode of this.argumentNodes(node)) {
|
|
746
|
+
for (const value of this.wordValues(argNode)) {
|
|
747
|
+
if (this.isOption(value)) continue;
|
|
748
|
+
const roleName = this.leafName(value);
|
|
749
|
+
const roleLine = this.lineOfNameWithin(argNode, value);
|
|
750
|
+
addRelationship(
|
|
751
|
+
state.packageName || "global",
|
|
752
|
+
roleName,
|
|
753
|
+
ASTEngine.RelationshipType.Implements,
|
|
754
|
+
{ reasonCode: REASON.useRoleComposition },
|
|
755
|
+
0.9,
|
|
756
|
+
void 0,
|
|
757
|
+
roleLine
|
|
758
|
+
);
|
|
759
|
+
addRelationship(
|
|
760
|
+
state.packageName || "global",
|
|
761
|
+
roleName,
|
|
762
|
+
ASTEngine.RelationshipType.MixesIn,
|
|
763
|
+
{ reasonCode: REASON.useRoleComposition, compositionSite: "role-composition" },
|
|
764
|
+
0.9,
|
|
765
|
+
void 0,
|
|
766
|
+
roleLine
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (state.packageName) frameworks.add("Moose/Moo OOP");
|
|
771
|
+
}
|
|
772
|
+
if (module === "Mojo::Base" && state.packageName) {
|
|
773
|
+
const named = args.find((arg) => arg && !this.isOption(arg));
|
|
774
|
+
addBase(state.packageName, [named ?? module], REASON.mojoBaseDeclaration, moduleNode);
|
|
775
|
+
}
|
|
776
|
+
const useArgList = this.findChildByType(node, "list_expression");
|
|
777
|
+
if (useArgList) walk(useArgList, state);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
case "use_version_statement": {
|
|
781
|
+
const versionNode = node.childForFieldName("version");
|
|
782
|
+
const version = versionNode?.text || "";
|
|
783
|
+
if (version) {
|
|
784
|
+
versionRequirements.push(version);
|
|
785
|
+
patterns.add("Version Requirements");
|
|
786
|
+
}
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
case "require_expression": {
|
|
790
|
+
const moduleNode = this.findChildByType(node, "bareword") || this.findChildByType(node, "package");
|
|
791
|
+
const pathNode = this.findChildByType(node, "string_literal");
|
|
792
|
+
const module = pathNode ? this.wordValues(pathNode)[0] ?? "" : moduleNode?.text ?? "";
|
|
793
|
+
if (!module) break;
|
|
794
|
+
imports.push({
|
|
795
|
+
module,
|
|
796
|
+
items: [],
|
|
797
|
+
isDefault: false,
|
|
798
|
+
range: this.nodeToRange(node),
|
|
799
|
+
metadata: { perlSpecific: true, type: "require" }
|
|
800
|
+
});
|
|
801
|
+
addRelationship(state.packageName || "global", module, ASTEngine.RelationshipType.Imports, {
|
|
802
|
+
reasonCode: REASON.runtimeRequire,
|
|
803
|
+
perlSpecific: true,
|
|
804
|
+
relationshipType: pathNode ? "require_file" : "require_module",
|
|
805
|
+
...pathNode ? { specifier: "path" } : {},
|
|
806
|
+
isImport: true,
|
|
807
|
+
fileLevel: true,
|
|
808
|
+
isPragma: false,
|
|
809
|
+
items: []
|
|
810
|
+
}, 0.9, node);
|
|
811
|
+
patterns.add("Module Includes");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
case "require_version_expression": {
|
|
815
|
+
const versionNode = node.childForFieldName("version") || this.findChildByType(node, "number");
|
|
816
|
+
const version = versionNode?.text || "";
|
|
817
|
+
if (version) {
|
|
818
|
+
versionRequirements.push(version);
|
|
819
|
+
patterns.add("Version Requirements");
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
case "assignment_expression": {
|
|
824
|
+
const lefts = this.fieldNodes(node, "left");
|
|
825
|
+
const rights = this.fieldNodes(node, "right");
|
|
826
|
+
const leftVar = lefts.flatMap((l) => this.variableChildren(l).map((variable) => this.findChildByType(variable, "varname")).find(Boolean)).find(Boolean);
|
|
827
|
+
const leftName = leftVar?.text || "";
|
|
828
|
+
const right = rights.find((r) => r.isNamed && r.namedChildCount > 0) ?? rights[0];
|
|
829
|
+
if (leftName === "ISA" && right && state.packageName) deferredIsa.push({ owner: state.packageName, right });
|
|
830
|
+
if (right && (leftName === "EXPORT" || leftName === "EXPORT_OK")) {
|
|
831
|
+
for (const exported of this.wordValues(right)) {
|
|
832
|
+
exports.push({ name: exported, kind: ASTEngine.SymbolKind.Function, range: this.nodeToRange(node), metadata: { perlSpecific: true, exportArray: leftName } });
|
|
833
|
+
addRelationship(state.packageName || "global", exported, ASTEngine.RelationshipType.Exports, { reasonCode: REASON.exportedArrayDeclaration, perlSpecific: true, exportArray: leftName }, 1, node, this.lineOfNameWithin(right, exported));
|
|
834
|
+
}
|
|
835
|
+
patterns.add("Exports");
|
|
836
|
+
}
|
|
837
|
+
if (leftName === "EXPORT_TAGS" && right) {
|
|
838
|
+
for (const tag of this.fatCommaKeys(right)) {
|
|
839
|
+
exports.push({ name: tag.text, kind: ASTEngine.SymbolKind.Function, range: this.nodeToRange(node), metadata: { perlSpecific: true, exportTag: "EXPORT_TAGS" } });
|
|
840
|
+
addRelationship(state.packageName || "global", tag.text, ASTEngine.RelationshipType.Exports, { reasonCode: REASON.exporterSurface, perlSpecific: true, exportTag: "EXPORT_TAGS" }, 1, node, tag.startPosition.row + 1);
|
|
841
|
+
}
|
|
842
|
+
patterns.add("Exports");
|
|
843
|
+
}
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
case "ambiguous_function_call_expression":
|
|
847
|
+
case "function_call_expression": {
|
|
848
|
+
const functionNode = node.childForFieldName("function") || this.findChildByType(node, "function");
|
|
849
|
+
const target = functionNode?.text || "";
|
|
850
|
+
if (!this.isWrittenCalleeName(target)) break;
|
|
851
|
+
if (target === "bless") {
|
|
852
|
+
const els = this.compositionElements(node);
|
|
853
|
+
let cls;
|
|
854
|
+
let clsNode;
|
|
855
|
+
for (const el of els) {
|
|
856
|
+
if (el.type !== "string_literal" && el.type !== "interpolated_string_literal") continue;
|
|
857
|
+
const word = this.wordValues(el)[0];
|
|
858
|
+
if (word && this.isWrittenCalleeName(word)) {
|
|
859
|
+
cls = word;
|
|
860
|
+
clsNode = el;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (cls && clsNode) addReferenceName(cls, clsNode, state, REASON.blessClassLiteral);
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
if (target === "UNIVERSAL::isa") {
|
|
867
|
+
const els = this.compositionElements(node);
|
|
868
|
+
let cls;
|
|
869
|
+
let clsNode;
|
|
870
|
+
for (const el of els) {
|
|
871
|
+
if (el.type !== "string_literal" && el.type !== "interpolated_string_literal") continue;
|
|
872
|
+
const word = this.wordValues(el)[0];
|
|
873
|
+
if (word && this.isWrittenCalleeName(word)) {
|
|
874
|
+
cls = word;
|
|
875
|
+
clsNode = el;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (cls && clsNode) addReferenceName(cls, clsNode, state, REASON.universalIsaLiteral);
|
|
879
|
+
}
|
|
880
|
+
if (target === "push") {
|
|
881
|
+
const elements = this.compositionElements(node);
|
|
882
|
+
const targetEl = elements[0];
|
|
883
|
+
const arrayVar = targetEl && targetEl.type === "array" ? this.findChildByType(targetEl, "varname")?.text ?? "" : "";
|
|
884
|
+
const arrayName = this.leafName(arrayVar);
|
|
885
|
+
const owner = arrayVar.includes("::") ? arrayVar.slice(0, arrayVar.lastIndexOf("::")) : state.packageName || "global";
|
|
886
|
+
const NAMED_ELEMENT = /* @__PURE__ */ new Set(["quoted_word_list", "string_literal", "interpolated_string_literal", "autoquoted_bareword", "bareword"]);
|
|
887
|
+
if (arrayName === "EXPORT" || arrayName === "EXPORT_OK") {
|
|
888
|
+
for (const el of elements.slice(1)) {
|
|
889
|
+
if (!NAMED_ELEMENT.has(el.type)) continue;
|
|
890
|
+
for (const value of this.wordValues(el)) {
|
|
891
|
+
if (!value || this.isOption(value)) continue;
|
|
892
|
+
exports.push({ name: value, kind: ASTEngine.SymbolKind.Function, range: this.nodeToRange(node), metadata: { perlSpecific: true, exportArray: arrayName } });
|
|
893
|
+
addRelationship(owner, value, ASTEngine.RelationshipType.Exports, { reasonCode: REASON.exportedArrayPush, perlSpecific: true, exportArray: arrayName }, 1, node, this.lineOfNameWithin(el, value));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
patterns.add("Exports");
|
|
897
|
+
} else if (arrayName === "ISA") {
|
|
898
|
+
const bases = [];
|
|
899
|
+
const lines = /* @__PURE__ */ new Map();
|
|
900
|
+
for (const el of elements.slice(1)) {
|
|
901
|
+
if (!NAMED_ELEMENT.has(el.type)) continue;
|
|
902
|
+
for (const value of this.wordValues(el)) {
|
|
903
|
+
if (!value || this.isOption(value)) continue;
|
|
904
|
+
bases.push(value);
|
|
905
|
+
const line = this.lineOfNameWithin(el, value);
|
|
906
|
+
if (line !== void 0) lines.set(value, line);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (bases.length) addBase(owner, bases, REASON.isaArrayPush, targetEl, lines);
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
if (target === "has") {
|
|
914
|
+
const attrs = this.attributeNames(node);
|
|
915
|
+
for (const attr of attrs) {
|
|
916
|
+
if (!attr.name || !state.packageName) continue;
|
|
917
|
+
if (symbolKeys.has(`${ASTEngine.SymbolKind.Property}${state.packageName}${attr.name}`)) continue;
|
|
918
|
+
addSymbol(attr.name, ASTEngine.SymbolKind.Property, attr.node, { ...state, ownerName: state.packageName, ownerKind: ASTEngine.SymbolKind.Class }, { isMooseProperty: true }, state.packageName);
|
|
919
|
+
}
|
|
920
|
+
frameworks.add("Moose/Moo OOP");
|
|
921
|
+
patterns.add("Declarative Properties");
|
|
922
|
+
} else if (target === "with" || target === "extends") {
|
|
923
|
+
const isWith = target === "with";
|
|
924
|
+
for (const arg of this.compositionElements(node)) {
|
|
925
|
+
if (arg.type === "anonymous_hash_expression" || arg.type === "anonymous_array_expression") continue;
|
|
926
|
+
for (const value of this.wordValues(arg)) {
|
|
927
|
+
if (!value || this.isOption(value)) continue;
|
|
928
|
+
if (isWith) {
|
|
929
|
+
const roleName = this.leafName(value);
|
|
930
|
+
const roleLine = this.lineOfNameWithin(arg, value);
|
|
931
|
+
addRelationship(
|
|
932
|
+
state.packageName || "global",
|
|
933
|
+
roleName,
|
|
934
|
+
ASTEngine.RelationshipType.Implements,
|
|
935
|
+
{ reasonCode: REASON.mooRoleComposition, mooseKeyword: target },
|
|
936
|
+
0.9,
|
|
937
|
+
arg,
|
|
938
|
+
// `arg` is the right NODE but the wrong LINE for a `qw` list that
|
|
939
|
+
// wraps: the list starts on the `with` line and the role names
|
|
940
|
+
// are on the lines after it. Prefer the name's own line and keep
|
|
941
|
+
// the node as the fallback, so a shape the offset search cannot
|
|
942
|
+
// resolve keeps its previous (correct) line.
|
|
943
|
+
roleLine
|
|
944
|
+
);
|
|
945
|
+
addRelationship(
|
|
946
|
+
state.packageName || "global",
|
|
947
|
+
roleName,
|
|
948
|
+
ASTEngine.RelationshipType.MixesIn,
|
|
949
|
+
{ reasonCode: REASON.mooRoleComposition, mooseKeyword: target, compositionSite: "role-composition" },
|
|
950
|
+
0.9,
|
|
951
|
+
arg,
|
|
952
|
+
roleLine
|
|
953
|
+
);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
addBase(state.packageName || "global", [value], REASON.mooExtendsKeyword, arg, /* @__PURE__ */ new Map([[value, this.lineOfNameWithin(arg, value) ?? arg.startPosition.row + 1]]));
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
frameworks.add("Moose/Moo OOP");
|
|
960
|
+
} else if (target === "new") {
|
|
961
|
+
const indirect = this.indirectObjectClass(node);
|
|
962
|
+
const inner = indirect?.carrier;
|
|
963
|
+
if (inner && indirect?.name) {
|
|
964
|
+
indirectObjectClassNodes.add(nodeKey(inner));
|
|
965
|
+
if (indirect.name.includes("::")) qualifiedClassNames.add(indirect.name);
|
|
966
|
+
addRelationship(state.functionName || state.packageName || "global", `${this.leafName(indirect.name)}.constructor`, ASTEngine.RelationshipType.Instantiates, { isConstructor: true, reasonCode: REASON.indirectObjectInvocation }, 0.9, node);
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
const requestedType = this.firstValue(node);
|
|
970
|
+
const instanceTarget = requestedType && !requestedType.includes("$") ? `${this.leafName(requestedType)}.constructor` : "constructor";
|
|
971
|
+
if (requestedType && requestedType.includes("::")) qualifiedClassNames.add(requestedType);
|
|
972
|
+
addRelationship(state.functionName || state.packageName || "global", instanceTarget, ASTEngine.RelationshipType.Instantiates, { isConstructor: true, reasonCode: REASON.concreteInvocant }, 0.9, node);
|
|
973
|
+
} else {
|
|
974
|
+
if (indirectObjectClassNodes.has(nodeKey(node))) break;
|
|
975
|
+
addCall(target, state, functionNode ?? node, false, node);
|
|
976
|
+
}
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
case "method_call_expression": {
|
|
980
|
+
const methodNode = this.findChildByType(node, "method");
|
|
981
|
+
const method = methodNode?.text || "";
|
|
982
|
+
if (!this.isWrittenCalleeName(method)) {
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
const invocant = this.fieldNodes(node, "invocant").find((c) => c.isNamed) ?? this.firstNamedChild(node);
|
|
986
|
+
if (method === "new") {
|
|
987
|
+
const target = invocant ? this.invocantName(invocant) : "";
|
|
988
|
+
if (target && PERL_MAGIC_TOKENS.has(target)) {
|
|
989
|
+
const owner = state.packageName;
|
|
990
|
+
if (owner && owner !== "main") {
|
|
991
|
+
if (owner.includes("::")) qualifiedClassNames.add(owner);
|
|
992
|
+
addRelationship(state.functionName || owner, `${this.leafName(owner)}.constructor`, ASTEngine.RelationshipType.Instantiates, { isConstructor: true, reasonCode: REASON.magicTokenInvocant }, 0.9, invocant ?? node);
|
|
993
|
+
}
|
|
994
|
+
} else if (invocant && this.namesAClass(invocant) && target && !target.includes("$") && !target.includes("->")) {
|
|
995
|
+
if (target.includes("::")) qualifiedClassNames.add(target);
|
|
996
|
+
addRelationship(state.functionName || state.packageName || "global", `${this.leafName(target)}.constructor`, ASTEngine.RelationshipType.Instantiates, { isConstructor: true, reasonCode: REASON.concreteInvocant }, 0.9, invocant ?? node);
|
|
997
|
+
}
|
|
998
|
+
} else {
|
|
999
|
+
const recvTarget = invocant ? this.invocantName(invocant) : "";
|
|
1000
|
+
const provableClass = invocant != null && this.namesAClass(invocant) && (invocant.type === "package" || recvTarget.includes("::")) && !recvTarget.includes("$") && !recvTarget.includes("->");
|
|
1001
|
+
if (provableClass) {
|
|
1002
|
+
if (recvTarget.includes("::")) qualifiedClassNames.add(recvTarget);
|
|
1003
|
+
addCall(`${this.leafName(recvTarget)}.${method}`, state, methodNode ?? node, false, node, method);
|
|
1004
|
+
} else {
|
|
1005
|
+
addCall(method, state, methodNode ?? node, true, node, method);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (method === "isa" || method === "DOES" || method === "can") {
|
|
1009
|
+
const args = this.argumentNodes(node);
|
|
1010
|
+
const named = this.fieldNodes(node, "arguments").filter((a) => a.isNamed);
|
|
1011
|
+
const first = named[0] ?? args[0];
|
|
1012
|
+
const queried = this.literalNameIn(first);
|
|
1013
|
+
if (queried && this.isWrittenCalleeName(queried) && !PERL_MAGIC_TOKENS.has(queried)) {
|
|
1014
|
+
const leaf = this.leafName(queried);
|
|
1015
|
+
const source = state.functionName || state.ownerName || state.packageName || "global";
|
|
1016
|
+
const selfByMethod = leaf === source;
|
|
1017
|
+
const selfByPackage = state.packageName !== "" && queried === state.packageName;
|
|
1018
|
+
if (!selfByMethod && !selfByPackage) {
|
|
1019
|
+
const nameNode = this.queriedNameNode(first) ?? first;
|
|
1020
|
+
addRelationship(
|
|
1021
|
+
source,
|
|
1022
|
+
leaf,
|
|
1023
|
+
ASTEngine.RelationshipType.References,
|
|
1024
|
+
// The written package rides in `qualifiedAs` for the same
|
|
1025
|
+
// reason the other reference sites record it: the target is the
|
|
1026
|
+
// LEAF (one class, one node) and the qualification the source
|
|
1027
|
+
// wrote is not lost, it is metadata a reader can ask for.
|
|
1028
|
+
{ reasonCode: REASON.typeQueryLiteral, query: method, ...queried.includes("::") ? { qualifiedAs: queried } : {} },
|
|
1029
|
+
0.9,
|
|
1030
|
+
nameNode
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (node.type !== "source_file") {
|
|
1039
|
+
const childState = node.type === "block" || node.type === "block_statement" ? { ...state } : state;
|
|
1040
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1041
|
+
const child = node.namedChild(i);
|
|
1042
|
+
if (child) walk(child, childState);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
const walkChildren = (container, state) => {
|
|
1047
|
+
for (let i = 0; i < container.namedChildCount; i++) {
|
|
1048
|
+
const child = container.namedChild(i);
|
|
1049
|
+
if (child) walk(child, state);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
const rootState = { packageName: "", insideFunction: false, visibility: "public", bodyKind: "file" };
|
|
1053
|
+
if (rootNode.type === "source_file") walkChildren(rootNode, rootState);
|
|
1054
|
+
else walk(rootNode, rootState);
|
|
1055
|
+
for (const item of deferredIsa) {
|
|
1056
|
+
const parents = this.wordValues(item.right);
|
|
1057
|
+
if (parents.length) addBase(item.owner, parents, REASON.isaArray, item.right);
|
|
1058
|
+
}
|
|
1059
|
+
for (const [owner, bases] of packageBases) {
|
|
1060
|
+
const origin = packageBaseOrigin.get(owner) ?? REASON.useParentDeclaration;
|
|
1061
|
+
for (const base of bases) {
|
|
1062
|
+
if (base === owner) continue;
|
|
1063
|
+
addRelationship(
|
|
1064
|
+
owner,
|
|
1065
|
+
this.leafName(base),
|
|
1066
|
+
ASTEngine.RelationshipType.Extends,
|
|
1067
|
+
{ via: "Perl inheritance", reasonCode: origin },
|
|
1068
|
+
0.9,
|
|
1069
|
+
void 0,
|
|
1070
|
+
packageBaseLine.get(`${owner}${base}`)
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const symbol of symbols) {
|
|
1075
|
+
if (symbol.kind !== ASTEngine.SymbolKind.Method && symbol.kind !== ASTEngine.SymbolKind.Constructor || !symbol.ownerName) continue;
|
|
1076
|
+
const base = packageBases.get(symbol.ownerName)?.[0];
|
|
1077
|
+
if (!base) continue;
|
|
1078
|
+
const baseLeaf = this.leafName(base);
|
|
1079
|
+
if (base === symbol.ownerName) continue;
|
|
1080
|
+
const method = String(symbol.metadata?.originalName || symbol.name);
|
|
1081
|
+
const candidateNames = [method, symbol.name];
|
|
1082
|
+
const localMethods = packageMethods.get(base) || packageMethods.get(baseLeaf);
|
|
1083
|
+
const companionMethods = context?.overrideCompanion?.get(base) || context?.overrideCompanion?.get(baseLeaf);
|
|
1084
|
+
const localDeclares = candidateNames.some((candidate) => localMethods?.has(candidate));
|
|
1085
|
+
const companionDeclares = candidateNames.some((candidate) => companionMethods?.has(candidate));
|
|
1086
|
+
if (localDeclares || companionDeclares) {
|
|
1087
|
+
addRelationship(`${symbol.ownerName}.${method}`, method, ASTEngine.RelationshipType.Overrides, { isOverride: true, reasonCode: REASON.baseDeclaresMethod, overriddenOwner: baseLeaf }, 0.95, void 0, nameLineOf(symbol));
|
|
1088
|
+
} else if (!context?.overrideCompanion?.has(base) && !context?.overrideCompanion?.has(baseLeaf)) {
|
|
1089
|
+
if (base.includes("::")) qualifiedClassNames.add(base);
|
|
1090
|
+
addRelationship(`${symbol.ownerName}.${method}`, `${baseLeaf}.${method}`, ASTEngine.RelationshipType.Overrides, { isOverride: true, reasonCode: REASON.firstParentGuess }, 0.5, void 0, nameLineOf(symbol));
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
for (const symbol of symbols) {
|
|
1094
|
+
if (symbol.ownerName && symbol.ownerName !== symbol.name) addRelationship(symbol.ownerName, symbol.name, ASTEngine.RelationshipType.Contains, {}, 1, void 0, nameLineOf(symbol));
|
|
1095
|
+
}
|
|
1096
|
+
const metadata = {
|
|
1097
|
+
complexity: symbols.length + relationships.length,
|
|
1098
|
+
linesOfCode: this.lineCount(content),
|
|
1099
|
+
cyclomaticComplexity: this.countBranches(rootNode) + 1,
|
|
1100
|
+
framework: [...frameworks][0],
|
|
1101
|
+
detectedFrameworks: [...frameworks],
|
|
1102
|
+
patterns: [...patterns],
|
|
1103
|
+
frameworkPatterns: [...patterns].map((pattern) => ({ framework: "Perl", patterns: [{ type: "usage", detected: true, description: pattern }] })),
|
|
1104
|
+
architecturalInsights: [],
|
|
1105
|
+
architecturalScore: Math.max(0, 100 - errors.length * 5),
|
|
1106
|
+
codeQuality: errors.length === 0 ? "Excellent" : "Fair",
|
|
1107
|
+
hasTypeInference: false,
|
|
1108
|
+
hasFrameworkIntelligence: true,
|
|
1109
|
+
analysisVersion: "3.1.0-perl-ast",
|
|
1110
|
+
...versionRequirements.length ? { perlVersion: versionRequirements[0], perlVersions: versionRequirements } : {}
|
|
1111
|
+
};
|
|
1112
|
+
this.classifyEvidenceTier(relationships);
|
|
1113
|
+
this.publishStatedCauses(symbols, relationships, qualifiedClassNames);
|
|
1114
|
+
this.publishExternalDependencies(symbols, relationships);
|
|
1115
|
+
return { symbols, relationships, imports, exports, errors, metadata };
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Perl's own confidences decide the floors, not a copied constant: the
|
|
1119
|
+
* source-declared edges (`extends`/`implements`/`instantiates` from `use
|
|
1120
|
+
* parent`, `with`, `use role`, a class invocant) are all 0.9, `contains` is
|
|
1121
|
+
* 1.0, `calls` is 0.9, and `overrides` is 0.5 because it is guessed from a
|
|
1122
|
+
* first-parent chain. So the guessed kind lands on `inferred` on its own,
|
|
1123
|
+
* with no special case: a 0.5 edge can never clear a 0.7 structural floor.
|
|
1124
|
+
*/
|
|
1125
|
+
classifyEvidenceTier(relationships) {
|
|
1126
|
+
for (const r of relationships) {
|
|
1127
|
+
const type = r.type ?? r.relationshipType ?? "";
|
|
1128
|
+
const md = r.metadata ?? {};
|
|
1129
|
+
r.metadata = md;
|
|
1130
|
+
if (type === "contains") {
|
|
1131
|
+
md.evidenceClass = "structural";
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (type === "imports" || type === "exports") {
|
|
1135
|
+
md.evidenceClass = "import_based";
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
if (type === "instantiates" || type === "extends" || type === "implements") {
|
|
1139
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.9 ? "structural" : "inferred";
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
if (type === "calls") {
|
|
1143
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.85 ? "usage_contextual" : "inferred";
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (type === "uses" || type === "references") {
|
|
1147
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.7 ? "usage_contextual" : "inferred";
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (type === "overrides") {
|
|
1151
|
+
md.evidenceClass = "inferred";
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
md.evidenceClass = "inferred";
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
result(symbols, relationships, imports, exports, errors, content, frameworks, patterns) {
|
|
1158
|
+
return {
|
|
1159
|
+
symbols,
|
|
1160
|
+
relationships,
|
|
1161
|
+
imports,
|
|
1162
|
+
exports,
|
|
1163
|
+
errors,
|
|
1164
|
+
metadata: {
|
|
1165
|
+
complexity: symbols.length + relationships.length,
|
|
1166
|
+
linesOfCode: this.lineCount(content),
|
|
1167
|
+
cyclomaticComplexity: 1,
|
|
1168
|
+
framework: [...frameworks][0],
|
|
1169
|
+
detectedFrameworks: [...frameworks],
|
|
1170
|
+
patterns: [...patterns],
|
|
1171
|
+
frameworkPatterns: [],
|
|
1172
|
+
architecturalInsights: [],
|
|
1173
|
+
architecturalScore: Math.max(0, 100 - errors.length * 5),
|
|
1174
|
+
codeQuality: errors.length === 0 ? "Excellent" : "Fair",
|
|
1175
|
+
hasTypeInference: false,
|
|
1176
|
+
hasFrameworkIntelligence: true,
|
|
1177
|
+
analysisVersion: "3.1.0-perl-ast"
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
collectErrors(root, errors) {
|
|
1182
|
+
const stack = [root];
|
|
1183
|
+
while (stack.length > 0) {
|
|
1184
|
+
const node = stack.pop();
|
|
1185
|
+
if (node.type === "ERROR" || node.isMissing) {
|
|
1186
|
+
errors.push({ message: `Syntax ${node.type} detected.`, range: this.nodeToRange(node), severity: "warning" });
|
|
1187
|
+
}
|
|
1188
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1189
|
+
const child = node.namedChild(i);
|
|
1190
|
+
if (child) stack.push(child);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* ALL children carrying `name` as their field.
|
|
1196
|
+
*
|
|
1197
|
+
* The pinned grammar gives one field name to SEVERAL children whenever a
|
|
1198
|
+
* construct is parenthesised: `our @EXPORT_OK = (a => 1, b => 2)` puts `(`,
|
|
1199
|
+
* the `list_expression` and `)` ALL under the `right` field, so
|
|
1200
|
+
* `childForFieldName("right")` returns the EMPTY `(` marker. Measured over
|
|
1201
|
+
* mojo+Moose+DBI+plack: `assignment_expression.right` 1,593 sites,
|
|
1202
|
+
* `ambiguous_function_call_expression.arguments` 1,058,
|
|
1203
|
+
* `method_call_expression.invocant` 168, `function_call_expression.arguments`
|
|
1204
|
+
* 3, `method_call_expression.arguments` 36 — every one of them lost by the
|
|
1205
|
+
* single-child read. `variable_declaration` additionally spells the paren form
|
|
1206
|
+
* `variables` (plural) against the bare form `variable`, 14,272 sites.
|
|
1207
|
+
*/
|
|
1208
|
+
fieldNodes(node, name) {
|
|
1209
|
+
const out = [];
|
|
1210
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1211
|
+
const child = node.child(i);
|
|
1212
|
+
if (child && node.fieldNameForChild(i) === name) out.push(child);
|
|
1213
|
+
}
|
|
1214
|
+
return out;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* The declared variables of a `variable_declaration`. The bare form
|
|
1218
|
+
* (`our $VERSION`) spells the field `variable`; the parenthesised form
|
|
1219
|
+
* (`our ($x, $y) = ...`) spells it `variables`, so BOTH are read.
|
|
1220
|
+
*/
|
|
1221
|
+
variableChildren(node) {
|
|
1222
|
+
const result = [];
|
|
1223
|
+
for (const child of [...this.fieldNodes(node, "variable"), ...this.fieldNodes(node, "variables")]) {
|
|
1224
|
+
if (child.type === "scalar" || child.type === "array" || child.type === "hash") result.push(child);
|
|
1225
|
+
}
|
|
1226
|
+
return result;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* The declaration scope, read from the grammar's own scope node.
|
|
1230
|
+
*
|
|
1231
|
+
* `variable_declaration` declares NO `scope` field: `our`/`my`/`state`/`field`
|
|
1232
|
+
* are CHILD NODES of the declaration. The pre-review implementation matched
|
|
1233
|
+
* `node.text.startsWith("our ")` — source text doing a syntax job, which
|
|
1234
|
+
* mis-reads any declaration whose keyword is followed by a newline or whose
|
|
1235
|
+
* text begins with something else.
|
|
1236
|
+
*
|
|
1237
|
+
* Those scope tokens are UNNAMED (`node.isNamed === false`, no field), so the
|
|
1238
|
+
* scan walks the RAW child list. A named-child scan silently finds nothing
|
|
1239
|
+
* and every `our` global in the corpus would vanish — the 2,379 constants
|
|
1240
|
+
* measured on the pre-review corpus all came from the text prefix.
|
|
1241
|
+
*/
|
|
1242
|
+
declarationKind(node) {
|
|
1243
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1244
|
+
const child = node.child(i);
|
|
1245
|
+
if (!child) continue;
|
|
1246
|
+
if (child.type === "our" || child.type === "my" || child.type === "state" || child.type === "field") return child.type;
|
|
1247
|
+
}
|
|
1248
|
+
return "unknown";
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* The argument nodes of a call or `use`.
|
|
1252
|
+
*
|
|
1253
|
+
* Two grammar generations have to be read here, and both are live:
|
|
1254
|
+
* - 1.2.1 (the WIRED build): the `arguments` field is MULTI-CHILD — a
|
|
1255
|
+
* parenthesised argument list spreads one field across `(`, the payload
|
|
1256
|
+
* and `)`, so `childForFieldName` returns the empty bracket marker.
|
|
1257
|
+
* Measured on 5,875 corpus files: 39,897 such sites over 41 field pairs.
|
|
1258
|
+
* - 2.0.0: the paren is a `parenthesized_expression` NODE, and on a
|
|
1259
|
+
* `use_statement` it carries NO field at all — so a field-driven read sees
|
|
1260
|
+
* nothing and the whole import list disappears.
|
|
1261
|
+
*
|
|
1262
|
+
* Reading every child that yields words, and descending through the paren
|
|
1263
|
+
* wrapper, is correct under BOTH: measured over the same 5,875 files, the
|
|
1264
|
+
* analyzer's emissions are then identical under a 1.2.1 build and a 2.0.0
|
|
1265
|
+
* build on 5,875 of 5,875 files, so a grammar bump needs no analyzer work.
|
|
1266
|
+
*/
|
|
1267
|
+
argumentNodes(node) {
|
|
1268
|
+
return perlArgumentNodes(node);
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* The line a NAME is written on INSIDE a node, when the node's own start line
|
|
1272
|
+
* is not it. `with qw/\n MooseRoleOne\n MooseRoleTwo\n/` is ONE
|
|
1273
|
+
* `quoted_word_list` node starting on the `with` line, so stamping the edge from
|
|
1274
|
+
* that node puts `implements` two lines above the role it names.
|
|
1275
|
+
*
|
|
1276
|
+
* Scoped to the ARGUMENT node, never the whole statement: for
|
|
1277
|
+
* `use Moose::Role 'My::Role'`, searching the statement for `Role` finds the
|
|
1278
|
+
* one inside `Moose::Role` and returns the module's line — exactly the bug
|
|
1279
|
+
* this avoids. Returns undefined when the name is not in the node's text, so
|
|
1280
|
+
* callers fall back to the node's own line rather than inventing one.
|
|
1281
|
+
*/
|
|
1282
|
+
lineOfNameWithin(node, name) {
|
|
1283
|
+
if (!name) return void 0;
|
|
1284
|
+
const text = String(node.text ?? "");
|
|
1285
|
+
let from = 0;
|
|
1286
|
+
for (; ; ) {
|
|
1287
|
+
const idx = text.indexOf(name, from);
|
|
1288
|
+
if (idx < 0) return void 0;
|
|
1289
|
+
const before = idx > 0 ? text[idx - 1] : "";
|
|
1290
|
+
const after = text[idx + name.length] ?? "";
|
|
1291
|
+
if (!isPerlIdentChar(before) && !isPerlIdentChar(after)) {
|
|
1292
|
+
let line = node.startPosition.row;
|
|
1293
|
+
for (let i = 0; i < idx; i++) if (text.charCodeAt(i) === 10) line++;
|
|
1294
|
+
return line + 1;
|
|
1295
|
+
}
|
|
1296
|
+
from = idx + 1;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Is this callee text a NAME the source actually writes?
|
|
1301
|
+
*
|
|
1302
|
+
* A target that is not written cannot be verified — the M59 line oracle reads
|
|
1303
|
+
* every claimed target back against the file's own text, and one that is not
|
|
1304
|
+
* there is a FABRICATED claim rather than a weak one. Same reasoning that
|
|
1305
|
+
* removed the phantom `DSN` and `use_module` classes, applied to the method
|
|
1306
|
+
* side, and found by that oracle:
|
|
1307
|
+
*
|
|
1308
|
+
* Moo/Object.pm:56 calls -> DemolishAll->new
|
|
1309
|
+
* Moo/Role.pm:224 calls -> ${\( $tc->can('coercion')||sub { ... } )}
|
|
1310
|
+
*
|
|
1311
|
+
* Both are symbolic deref — `$self->${\( EXPR \)}`, perl's "call the method
|
|
1312
|
+
* whose name this expression computes". The grammar puts the whole expression
|
|
1313
|
+
* in the `method` field, so the lane published arbitrary source text as a
|
|
1314
|
+
* callee name. A POSITIVE allowlist again, not a "does this look wrong" test:
|
|
1315
|
+
* a negative test only enumerates the dynamic cases somebody already hit, and
|
|
1316
|
+
* this pair has been wrong that way three times.
|
|
1317
|
+
*
|
|
1318
|
+
* Rejected: a sigil (`->$name`), braces, `$`, `->`, whitespace. For a rejected
|
|
1319
|
+
* one NO edge is emitted — and the children are still walked, so the real calls
|
|
1320
|
+
* INSIDE the expression survive: `Method::Generate::DemolishAll->new` inside a
|
|
1321
|
+
* deref becomes its own `instantiates` edge. 19 such edges left a 14-file
|
|
1322
|
+
* draw.
|
|
1323
|
+
*/
|
|
1324
|
+
isWrittenCalleeName(text) {
|
|
1325
|
+
const t = String(text ?? "").trim();
|
|
1326
|
+
if (!t) return false;
|
|
1327
|
+
for (let i2 = 0; i2 < t.length; i2++) {
|
|
1328
|
+
const c = t.charAt(i2);
|
|
1329
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") return false;
|
|
1330
|
+
if (c === "$" || c === "{" || c === "}") return false;
|
|
1331
|
+
}
|
|
1332
|
+
if (t.includes("->")) return false;
|
|
1333
|
+
let i = 0;
|
|
1334
|
+
if (t.charAt(i) === "&") i++;
|
|
1335
|
+
if (t.charAt(i) === ":" && t.charAt(i + 1) === ":") i += 2;
|
|
1336
|
+
let segments = 0;
|
|
1337
|
+
for (; ; ) {
|
|
1338
|
+
if (segments > 0) {
|
|
1339
|
+
if (t.charAt(i) !== ":" || t.charAt(i + 1) !== ":") break;
|
|
1340
|
+
i += 2;
|
|
1341
|
+
}
|
|
1342
|
+
if (!isPerlIdentStart(t.charAt(i))) return false;
|
|
1343
|
+
i++;
|
|
1344
|
+
while (isPerlIdentChar(t.charAt(i))) i++;
|
|
1345
|
+
segments++;
|
|
1346
|
+
if (i >= t.length) return true;
|
|
1347
|
+
}
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* The ELEMENTS of a role-composition argument list — the shapes `with` and
|
|
1352
|
+
* `extends` are written in, as separate elements rather than as one blob.
|
|
1353
|
+
*
|
|
1354
|
+
* The call's `arguments` field holds ONE `list_expression` for every
|
|
1355
|
+
* multi-argument form, so a reader that treated that field as a single argument
|
|
1356
|
+
* read the first word of the WHOLE list and dropped the rest. Expanding the list
|
|
1357
|
+
* into its named children restores the element boundary: in
|
|
1358
|
+
* `with A, { -excludes => [...] }, B` the elements are A, the modifier hash
|
|
1359
|
+
* and B, which is exactly the information the role reader needs.
|
|
1360
|
+
*/
|
|
1361
|
+
compositionElements(node) {
|
|
1362
|
+
const out = [];
|
|
1363
|
+
for (const arg of this.argumentNodes(node)) {
|
|
1364
|
+
if (arg.type === "list_expression") {
|
|
1365
|
+
for (let i = 0; i < arg.namedChildCount; i++) {
|
|
1366
|
+
const child = arg.namedChild(i);
|
|
1367
|
+
if (child) out.push(child);
|
|
1368
|
+
}
|
|
1369
|
+
} else out.push(arg);
|
|
1370
|
+
}
|
|
1371
|
+
return out;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* The names a `has` declaration declares — ONE per element of its name list.
|
|
1375
|
+
*
|
|
1376
|
+
* TWO facts live here and each was a measured defect. (1) A QUOTED name is ONE
|
|
1377
|
+
* name whatever it contains: `has 'has spaces' => (is => 'ro')` is a legal
|
|
1378
|
+
* attribute whose name has a space in it, and the old reader ran the name
|
|
1379
|
+
* through `wordValues`, whose whitespace splitter exists for `qw(a b)` (a real
|
|
1380
|
+
* LIST) and truncated this to `has` — publishing a member named `has`, which is
|
|
1381
|
+
* a real perl function, while the declared member got no symbol at all. (2) A
|
|
1382
|
+
* `qw`/array list declares SEVERAL attributes: `has [qw(handlers helpers)]` is
|
|
1383
|
+
* two members, and reading only the first dropped the rest.
|
|
1384
|
+
*
|
|
1385
|
+
* The splitter is therefore applied to the shapes that ARE lists
|
|
1386
|
+
* (`quoted_word_list`, each element of an array), and never to a string
|
|
1387
|
+
* literal's own content.
|
|
1388
|
+
*/
|
|
1389
|
+
attributeNames(node) {
|
|
1390
|
+
const args = this.argumentNodes(node);
|
|
1391
|
+
const first = args[0];
|
|
1392
|
+
if (!first) return [];
|
|
1393
|
+
const head = first.type === "list_expression" && first.namedChildCount > 0 ? first.namedChild(0) : first;
|
|
1394
|
+
if (!head) return [];
|
|
1395
|
+
return this.namesOfDeclarator(head);
|
|
1396
|
+
}
|
|
1397
|
+
namesOfDeclarator(head) {
|
|
1398
|
+
if (head.type === "string_literal") {
|
|
1399
|
+
const content = head.childForFieldName("content") || this.findChildByType(head, "string_content");
|
|
1400
|
+
const name = content ? content.text : unquote(head.text);
|
|
1401
|
+
return name ? [{ name, node: content ?? head }] : [];
|
|
1402
|
+
}
|
|
1403
|
+
if (head.type === "quoted_word_list") {
|
|
1404
|
+
const content = head.childForFieldName("content") || this.findChildByType(head, "string_content");
|
|
1405
|
+
if (!content) return [];
|
|
1406
|
+
return splitWords(content.text).filter(Boolean).map((name) => ({ name, node: content }));
|
|
1407
|
+
}
|
|
1408
|
+
if (head.type === "anonymous_array_expression") {
|
|
1409
|
+
const out = [];
|
|
1410
|
+
for (let i = 0; i < head.namedChildCount; i++) {
|
|
1411
|
+
const child = head.namedChild(i);
|
|
1412
|
+
if (child) out.push(...this.namesOfDeclarator(child));
|
|
1413
|
+
}
|
|
1414
|
+
return out;
|
|
1415
|
+
}
|
|
1416
|
+
if (head.type === "interpolated_string_literal") {
|
|
1417
|
+
const pieces = [];
|
|
1418
|
+
let interpolates = false;
|
|
1419
|
+
for (let i = 0; i < head.namedChildCount; i++) {
|
|
1420
|
+
const child = head.namedChild(i);
|
|
1421
|
+
if (!child) continue;
|
|
1422
|
+
if (child.type === "string_content") pieces.push(...splitWords(child.text));
|
|
1423
|
+
else if (child.type === "scalar" || child.type === "array" || child.type === "hash") interpolates = true;
|
|
1424
|
+
}
|
|
1425
|
+
if (interpolates) return pieces.filter(Boolean).map((name) => ({ name, node: head }));
|
|
1426
|
+
const content = head.childForFieldName("content") || this.findChildByType(head, "string_content");
|
|
1427
|
+
const verbatim = content ? content.text : "";
|
|
1428
|
+
return verbatim ? [{ name: verbatim, node: content ?? head }] : [];
|
|
1429
|
+
}
|
|
1430
|
+
return this.wordValues(head).filter(Boolean).map((name) => ({ name, node: head }));
|
|
1431
|
+
}
|
|
1432
|
+
argumentValues(node) {
|
|
1433
|
+
const module = node.childForFieldName("module");
|
|
1434
|
+
if (module && module.type !== "package") return this.wordValues(module);
|
|
1435
|
+
const values = [];
|
|
1436
|
+
for (const arg of this.argumentNodes(node)) values.push(...this.wordValues(arg));
|
|
1437
|
+
return values;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* The fat-comma KEYS of a list: the named child immediately followed by a
|
|
1441
|
+
* `=>` token. Read from the child list, so `A => 1, B => 2` yields A and B
|
|
1442
|
+
* and never the values.
|
|
1443
|
+
*/
|
|
1444
|
+
fatCommaKeys(node) {
|
|
1445
|
+
let list;
|
|
1446
|
+
if (node.type === "list_expression") list = node;
|
|
1447
|
+
else {
|
|
1448
|
+
list = this.findChildByType(node, "list_expression");
|
|
1449
|
+
if (!list) {
|
|
1450
|
+
const hash = this.findChildByType(node, "anonymous_hash_expression");
|
|
1451
|
+
if (hash) list = this.findChildByType(hash, "list_expression");
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (!list) return [];
|
|
1455
|
+
const keys = [];
|
|
1456
|
+
let pending = null;
|
|
1457
|
+
for (let i = 0; i < list.childCount; i++) {
|
|
1458
|
+
const child = list.child(i);
|
|
1459
|
+
if (!child) continue;
|
|
1460
|
+
if (child.type === "=>") {
|
|
1461
|
+
if (pending) keys.push(pending);
|
|
1462
|
+
pending = null;
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
pending = child.isNamed ? child : pending;
|
|
1466
|
+
}
|
|
1467
|
+
return keys;
|
|
1468
|
+
}
|
|
1469
|
+
/** Names declared by `use constant ...` — the fat-comma keys. */
|
|
1470
|
+
constantDeclarationNames(node) {
|
|
1471
|
+
const result = [];
|
|
1472
|
+
for (const key of this.fatCommaKeys(node)) {
|
|
1473
|
+
const name = this.wordValues(key)[0];
|
|
1474
|
+
if (name) result.push({ name, node: key });
|
|
1475
|
+
}
|
|
1476
|
+
return result;
|
|
1477
|
+
}
|
|
1478
|
+
wordValues(node) {
|
|
1479
|
+
return perlWordValues(node);
|
|
1480
|
+
}
|
|
1481
|
+
firstValue(node) {
|
|
1482
|
+
const values = this.argumentValues(node);
|
|
1483
|
+
return values[0];
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* M14 #863 REPAYMENT — the dead `childForFieldName("parameters")`.
|
|
1487
|
+
*
|
|
1488
|
+
* The pinned tree-sitter-perl grammar declares NO `parameters` field, so that
|
|
1489
|
+
* lookup returned undefined for every sub in every file and the lane shipped
|
|
1490
|
+
* 0 parameters across 29,640 symbols on 5,866 corpus files. A signature is the
|
|
1491
|
+
* `signature` NODE TYPE, whose children are `mandatory_parameter`,
|
|
1492
|
+
* `optional_parameter` and `slurpy_parameter`; each wraps the
|
|
1493
|
+
* scalar/array/hash node that carries the `varname`.
|
|
1494
|
+
*
|
|
1495
|
+
* The first version of this fix said FIELD here and reached for
|
|
1496
|
+
* `childForFieldName("signature")`, which is dead for the same reason the
|
|
1497
|
+
* original was — `signature` is not among the grammar's 44 fields either. The
|
|
1498
|
+
* working `findChildByType` beside it is what has been producing every
|
|
1499
|
+
* parameter all along, and the dead-literal audit is what finally said so. The
|
|
1500
|
+
* wording is corrected here because the wrong word is what produced the bug.
|
|
1501
|
+
*/
|
|
1502
|
+
extractParameters(node) {
|
|
1503
|
+
const signature = this.findChildByType(node, "signature");
|
|
1504
|
+
if (!signature) return [];
|
|
1505
|
+
const result = [];
|
|
1506
|
+
for (let i = 0; i < signature.namedChildCount; i++) {
|
|
1507
|
+
const param = signature.namedChild(i);
|
|
1508
|
+
if (!param) continue;
|
|
1509
|
+
const kind = param.type;
|
|
1510
|
+
if (kind !== "mandatory_parameter" && kind !== "optional_parameter" && kind !== "slurpy_parameter") continue;
|
|
1511
|
+
const carrier = this.findChildByType(param, "scalar") || this.findChildByType(param, "array") || this.findChildByType(param, "hash");
|
|
1512
|
+
if (!carrier) continue;
|
|
1513
|
+
const varname = this.findChildByType(carrier, "varname");
|
|
1514
|
+
if (!varname) continue;
|
|
1515
|
+
result.push({ name: this.stripSigil(varname.text), isOptional: kind === "optional_parameter" });
|
|
1516
|
+
}
|
|
1517
|
+
return result;
|
|
1518
|
+
}
|
|
1519
|
+
/** `sub f :lvalue :method` — the grammar's `attributes` field. */
|
|
1520
|
+
extractAttributes(node) {
|
|
1521
|
+
const attrlist = node.childForFieldName("attributes") || this.findChildByType(node, "attrlist");
|
|
1522
|
+
if (!attrlist) return [];
|
|
1523
|
+
const result = [];
|
|
1524
|
+
for (let i = 0; i < attrlist.namedChildCount; i++) {
|
|
1525
|
+
const attribute = attrlist.namedChild(i);
|
|
1526
|
+
if (!attribute || attribute.type !== "attribute") continue;
|
|
1527
|
+
const nameNode = attribute.childForFieldName("name") || this.findChildByType(attribute, "attribute_name");
|
|
1528
|
+
const name = nameNode?.text || "";
|
|
1529
|
+
if (name) result.push(name);
|
|
1530
|
+
}
|
|
1531
|
+
return result;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Does this receiver node NAME a class, as opposed to being a runtime value?
|
|
1535
|
+
*
|
|
1536
|
+
* The allowlist is the set of node types whose entire content is a name. A
|
|
1537
|
+
* `function_call_expression` is deliberately absent: its `function` field is
|
|
1538
|
+
* the name of a FUNCTION, and calling it yields a value, so
|
|
1539
|
+
* `use_module('X')->new` constructs nothing knowable. `ambiguous_function_
|
|
1540
|
+
* call_expression` IS present because that is the indirect-object spelling,
|
|
1541
|
+
* where the node's `function` field is the class.
|
|
1542
|
+
*/
|
|
1543
|
+
/**
|
|
1544
|
+
* The CLASS half of an indirect-object call: `new Class @args` is exactly
|
|
1545
|
+
* `Class->new(@args)`, and the grammar spells the class as the `function`
|
|
1546
|
+
* FIELD of a nested `ambiguous_function_call_expression`.
|
|
1547
|
+
*
|
|
1548
|
+
* Read STRUCTURALLY, from the node's named children, rather than through
|
|
1549
|
+
* `argumentNodes` — that helper keeps an argument only when it contributes at
|
|
1550
|
+
* least one WORD, and this nested call contributes none whenever its own
|
|
1551
|
+
* arguments are a plain scalar or a list of literals. Measured: it returned
|
|
1552
|
+
* `[]` for `new Win32::ODBC $dbname` and `new Foo 1, 2` but DID return the
|
|
1553
|
+
* call for `new Win32::ODBC $x->{DSN}` — kept only because of a hash KEY, which
|
|
1554
|
+
* is the same accident that published the phantom class `DSN`. A structural
|
|
1555
|
+
* read is right for all three, and for the third shape (`new Foo;`, where the
|
|
1556
|
+
* class is a direct `bareword` child and there is no nested call at all) the
|
|
1557
|
+
* caller's existing path already reads the bareword correctly.
|
|
1558
|
+
*/
|
|
1559
|
+
indirectObjectClass(node) {
|
|
1560
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1561
|
+
const child = node.namedChild(i);
|
|
1562
|
+
if (!child || child.type !== "ambiguous_function_call_expression") continue;
|
|
1563
|
+
const fn = child.childForFieldName("function") || this.findChildByType(child, "function");
|
|
1564
|
+
if (fn?.text) return { name: fn.text, carrier: child };
|
|
1565
|
+
}
|
|
1566
|
+
return void 0;
|
|
1567
|
+
}
|
|
1568
|
+
namesAClass(node) {
|
|
1569
|
+
switch (node.type) {
|
|
1570
|
+
case "bareword":
|
|
1571
|
+
case "autoquoted_bareword":
|
|
1572
|
+
case "package":
|
|
1573
|
+
case "ambiguous_function_call_expression":
|
|
1574
|
+
return true;
|
|
1575
|
+
case "string_literal":
|
|
1576
|
+
case "interpolated_string_literal":
|
|
1577
|
+
return !node.text.includes("$");
|
|
1578
|
+
default:
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
invocantName(node) {
|
|
1583
|
+
if (node.type === "scalar" || node.type === "array" || node.type === "hash") {
|
|
1584
|
+
return node.text;
|
|
1585
|
+
}
|
|
1586
|
+
if (node.type === "function_call_expression" || node.type === "ambiguous_function_call_expression") {
|
|
1587
|
+
const fn = node.childForFieldName("function") || this.findChildByType(node, "function");
|
|
1588
|
+
return fn?.text || "";
|
|
1589
|
+
}
|
|
1590
|
+
return node.text;
|
|
1591
|
+
}
|
|
1592
|
+
isOption(value) {
|
|
1593
|
+
return value.length > 0 && value[0] === "-";
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* The NAME an argument node writes, and only when it writes one literally.
|
|
1597
|
+
*
|
|
1598
|
+
* `'Class::Name'` and `Class::Name` are names. `$class`, `@classes` and
|
|
1599
|
+
* `"Pkg::$sub"` are not: the file states no name, so an edge built on one
|
|
1600
|
+
* would name a symbol the source never mentions. The interpolation test is
|
|
1601
|
+
* character comparison on the literal's own text, never a pattern -- the
|
|
1602
|
+
* grammar exposes the unquoted body under `content`, so there is nothing to
|
|
1603
|
+
* unquote here and nothing to strip.
|
|
1604
|
+
*
|
|
1605
|
+
* Returns undefined for every other node type, which is the refusal the
|
|
1606
|
+
* measured counts depend on: 170 `isa($var)` and 5 `DOES($var)` sites in
|
|
1607
|
+
* 5,127 files resolve to nothing rather than to a guess.
|
|
1608
|
+
*/
|
|
1609
|
+
literalNameIn(node) {
|
|
1610
|
+
if (!node) return void 0;
|
|
1611
|
+
if (node.type !== "string_literal" && node.type !== "bareword" && node.type !== "package" && node.type !== "autoquoted_bareword") return void 0;
|
|
1612
|
+
if (node.text.includes("$")) return void 0;
|
|
1613
|
+
const content = node.childForFieldName("content") ?? this.findChildByType(node, "string_content");
|
|
1614
|
+
const text = String(content?.text ?? node.text ?? "").trim();
|
|
1615
|
+
return text.length ? text : void 0;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* The node a queried name is WRITTEN on, so the edge's `line_number` is the
|
|
1619
|
+
* line that names it and not the line the call starts on (M59.A7: the line
|
|
1620
|
+
* comes from the node that NAMES the edge). `literalNameIn` already decided
|
|
1621
|
+
* the name exists; this only locates it.
|
|
1622
|
+
*/
|
|
1623
|
+
queriedNameNode(node) {
|
|
1624
|
+
if (!node) return void 0;
|
|
1625
|
+
return node.childForFieldName("content") ?? this.findChildByType(node, "string_content") ?? this.findChildByType(node, "varname") ?? node;
|
|
1626
|
+
}
|
|
1627
|
+
stripSigil(name) {
|
|
1628
|
+
if (!name) return name;
|
|
1629
|
+
const first = name[0];
|
|
1630
|
+
return first === "$" || first === "@" || first === "%" || first === "#" ? name.slice(1) : name;
|
|
1631
|
+
}
|
|
1632
|
+
leafName(name) {
|
|
1633
|
+
let trimmed = name;
|
|
1634
|
+
while (trimmed.length >= 2 && trimmed.charAt(trimmed.length - 1) === ":" && trimmed.charAt(trimmed.length - 2) === ":") trimmed = trimmed.slice(0, -2);
|
|
1635
|
+
const colon = trimmed.lastIndexOf("::");
|
|
1636
|
+
return colon >= 0 ? trimmed.slice(colon + 2) : trimmed;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* THE BASE A `leaf.member` TARGET NAMES — the single-dot, identifier-shaped owner the declared
|
|
1640
|
+
* cause channel is defined over, or null when the target cannot carry that claim at all.
|
|
1641
|
+
*
|
|
1642
|
+
* This is not a style choice, it is the channel's own precondition. The arbiter takes the owner
|
|
1643
|
+
* as everything before the LAST dot (`incoming-rule.ts` `targetOwner`) and accepts a stated base
|
|
1644
|
+
* only when it is IDENTIFIER-SHAPED and IS that owner
|
|
1645
|
+
* (`classifyEdge`: `const shaped = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(claim.base)` then
|
|
1646
|
+
* `const consistent = shaped && owner === claim.base`). A target carrying two dots, an empty
|
|
1647
|
+
* member, or an owner with `::` in it therefore cannot be given this claim: writing one there
|
|
1648
|
+
* reads `causeRefuted`, which is `causeFAIL` — a clause-4 defect, not a stated cause.
|
|
1649
|
+
*
|
|
1650
|
+
* The character test is the lane's own `isPerlIdentStart`/`isPerlIdentChar` rather than a regex
|
|
1651
|
+
* literal, because this file's contract forbids regex literals (proved by
|
|
1652
|
+
* `script/m14/tmp/perl-zero-regex.mts`), and reusing them keeps the predicate from drifting away
|
|
1653
|
+
* from the rest of the lane.
|
|
1654
|
+
*/
|
|
1655
|
+
statedOwnerOf(target) {
|
|
1656
|
+
const dot = target.lastIndexOf(".");
|
|
1657
|
+
if (dot <= 0 || dot === target.length - 1) return null;
|
|
1658
|
+
if (target.indexOf(".") !== dot) return null;
|
|
1659
|
+
const owner = target.slice(0, dot);
|
|
1660
|
+
if (!isPerlIdentStart(owner[0])) return null;
|
|
1661
|
+
for (let i = 1; i < owner.length; i++) if (!isPerlIdentChar(owner[i])) return null;
|
|
1662
|
+
return owner;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* PUBLISH THE REASON THE LANE ALREADY COMPUTES, ON THE KEY THE GATE READS — the fleet's finding
|
|
1666
|
+
* (a), measured on this lane rather than assumed: every perl edge below confidence 1.0 carries a
|
|
1667
|
+
* machine-readable `metadata.reasonCode` (M14 HONESTY-CONTRACT §4.1 — the 1,664 `contains` edges
|
|
1668
|
+
* are @1.0 and carry none), and the arbiter's bucket-3 column reads
|
|
1669
|
+
* `metadata.baseBeyondRunScope` ONLY (`incoming-rule.ts` `CAUSE_CLAIM_KEY`). So the gate printed
|
|
1670
|
+
* `causeOK 0`, `clause 2 ok — no edge carries a declared cause (residual 6998)`, and clause 2
|
|
1671
|
+
* passed VACUOUSLY over a 4,110-edge unresolved residual. See
|
|
1672
|
+
* `validation/m14/verification/PERL-FIX.md` for the bucketed residual and the measurements.
|
|
1673
|
+
*
|
|
1674
|
+
* WHAT THE CLAIM MEANS, and it is a claim about the RUN, not about the project: "the base this
|
|
1675
|
+
* edge names is beyond THIS RUN's reach" (`incoming-rule.ts`, `CAUSE_CLAIM_KEY`). Production
|
|
1676
|
+
* parses a perl file with NO project index and NO override companion
|
|
1677
|
+
* (`parallel-indexer.ts` `PROJECT_INDEX_EXTENSIONS` names only `.sol`), so a class another file
|
|
1678
|
+
* declares is genuinely out of this run's sight — which is exactly what the lane's own reason
|
|
1679
|
+
* codes already say (`first-parent-guess`: "the base does not declare it anywhere visible";
|
|
1680
|
+
* `name-only-callee`: "whether it binds to a sub in this file, an imported sub, or a
|
|
1681
|
+
* module-level sub is not decided at this edge"). Dart is the reference implementation of this
|
|
1682
|
+
* mechanism for the index-less case (`real-dart-analyzer.ts:2089-2091`).
|
|
1683
|
+
*
|
|
1684
|
+
* THE GUARD, and it is the difference between a stated cause and a laundering vector: the claim
|
|
1685
|
+
* is attached ONLY where the class the target names is NOT published by the file being analyzed.
|
|
1686
|
+
* When this file declares the class, the run DID reach it, and "beyond this run's reach" would be
|
|
1687
|
+
* FALSE — so `IOLoop.start` inside `package IOLoop` keeps its reason code and states nothing.
|
|
1688
|
+
* The arbiter cannot check this half (property 4 of the channel — a second implementation — is
|
|
1689
|
+
* explicitly not claimed to exist), so the analyzer must not state it.
|
|
1690
|
+
*
|
|
1691
|
+
* It can never move a resolution number. `classifyEdge` reads a cause ONLY inside its
|
|
1692
|
+
* `decls === 0` branch, so a claimed edge is already unresolved; and it is REFUTED outright if
|
|
1693
|
+
* the edge also carries an external claim — the lane publishes none, which is why the conflict
|
|
1694
|
+
* branch cannot fire here.
|
|
1695
|
+
*/
|
|
1696
|
+
publishStatedCauses(symbols, relationships, qualifiedClassNames) {
|
|
1697
|
+
const declaredHere = /* @__PURE__ */ new Set();
|
|
1698
|
+
for (const symbol of symbols) {
|
|
1699
|
+
const kind = symbol.kind;
|
|
1700
|
+
if (kind !== ASTEngine.SymbolKind.Class && kind !== ASTEngine.SymbolKind.Trait) continue;
|
|
1701
|
+
const name = String(symbol.name ?? "");
|
|
1702
|
+
if (name) declaredHere.add(this.leafName(name));
|
|
1703
|
+
}
|
|
1704
|
+
const heldQualified = /* @__PURE__ */ new Set();
|
|
1705
|
+
for (const qualified of qualifiedClassNames) heldQualified.add(this.leafName(qualified));
|
|
1706
|
+
for (const relationship of relationships) {
|
|
1707
|
+
const type = relationship.type;
|
|
1708
|
+
if (type !== ASTEngine.RelationshipType.Calls && type !== ASTEngine.RelationshipType.Overrides && type !== ASTEngine.RelationshipType.Instantiates) continue;
|
|
1709
|
+
const metadata = relationship.metadata;
|
|
1710
|
+
if (!metadata || metadata.baseBeyondRunScope !== void 0) continue;
|
|
1711
|
+
const owner = this.statedOwnerOf(String(relationship.target ?? ""));
|
|
1712
|
+
if (owner === null || declaredHere.has(owner)) continue;
|
|
1713
|
+
if (heldQualified.has(owner)) continue;
|
|
1714
|
+
metadata.baseBeyondRunScope = owner;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* PERL-1 — A DEPENDENCY THE FILE DOES NOT DECLARE IS A CLAIM, NOT SILENCE.
|
|
1719
|
+
*
|
|
1720
|
+
* The lane published no externality reading at all: its whole-population arm read **10,096 edges ·
|
|
1721
|
+
* 4,605 resolved · 9,435 ambiguous/unresolved · 0 verified · 0 refuted**, i.e. 93 % of the graph was
|
|
1722
|
+
* wall. The two largest unresolved labels are the two dependencies Perl names without any syntax of
|
|
1723
|
+
* its own:
|
|
1724
|
+
*
|
|
1725
|
+
* `use strict` / `use warnings` / `use Test::More` an IMPORT of a module or pragma
|
|
1726
|
+
* `is(…)` / `plan(…)` / `ok(…)` / `run_tests(…)` a BARE function call
|
|
1727
|
+
*
|
|
1728
|
+
* MEASURED: `imports` 1,654 unresolved (strict 328 · Test::More 267 · warnings 228 ·
|
|
1729
|
+
* Test::Nginx::Socket::Lua 120) and `calls` 2,966 unresolved (is 206 · plan 175 · run_tests 124 ·
|
|
1730
|
+
* repeat_each 122 · blocks 120 · ok 118). None of those names is declared by a corpus file that does
|
|
1731
|
+
* not already declare it, so every row was a dependency published with no reading at all.
|
|
1732
|
+
*
|
|
1733
|
+
* THE CLAIM IS SAFE BY CONSTRUCTION, exactly as on the powershell and haskell lanes: the arbiter
|
|
1734
|
+
* consults a claim only where the name model has NOTHING (`decls === 0`), so a name the project
|
|
1735
|
+
* declares keeps its resolved/ambiguous answer and this claim cannot refute it. The guard is the
|
|
1736
|
+
* file's own declaration table, so a local sub or a package this file declares keeps no claim.
|
|
1737
|
+
*/
|
|
1738
|
+
publishExternalDependencies(symbols, relationships) {
|
|
1739
|
+
const declared = /* @__PURE__ */ new Set();
|
|
1740
|
+
for (const s of symbols) if (s.name) declared.add(s.name);
|
|
1741
|
+
for (const rel of relationships) {
|
|
1742
|
+
const type = rel.type;
|
|
1743
|
+
if (type !== ASTEngine.RelationshipType.Imports && type !== ASTEngine.RelationshipType.Calls && type !== ASTEngine.RelationshipType.Instantiates && type !== ASTEngine.RelationshipType.Extends && type !== ASTEngine.RelationshipType.Uses) continue;
|
|
1744
|
+
const md = rel.metadata;
|
|
1745
|
+
if (!md || md.externalTarget === true) continue;
|
|
1746
|
+
if (type === ASTEngine.RelationshipType.Calls && md.baseBeyondRunScope !== void 0) continue;
|
|
1747
|
+
const target = String(rel.target ?? "");
|
|
1748
|
+
if (target.length === 0) continue;
|
|
1749
|
+
if (!this.isPerlName(target)) continue;
|
|
1750
|
+
if (declared.has(target) || declared.has(this.leafName(target))) continue;
|
|
1751
|
+
if (target.indexOf("::") >= 0) {
|
|
1752
|
+
md.externalTarget = true;
|
|
1753
|
+
md.externalReason = "module-namespace";
|
|
1754
|
+
md.externalSpecifier = target;
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
md.externalTarget = true;
|
|
1758
|
+
md.externalVia = type === ASTEngine.RelationshipType.Imports ? "use" : "call";
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
/** A Perl module/function spelling: `strict`, `Test::More`, `is`, `run_tests`. */
|
|
1762
|
+
isPerlName(name) {
|
|
1763
|
+
const code = name.charCodeAt(0);
|
|
1764
|
+
const firstOk = code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95;
|
|
1765
|
+
if (!firstOk) return false;
|
|
1766
|
+
for (let i = 1; i < name.length; i++) {
|
|
1767
|
+
const c = name.charCodeAt(i);
|
|
1768
|
+
const ok = c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 95 || c === 58;
|
|
1769
|
+
if (!ok) return false;
|
|
1770
|
+
}
|
|
1771
|
+
return !name.endsWith(":");
|
|
1772
|
+
}
|
|
1773
|
+
isAllCapsName(name) {
|
|
1774
|
+
let hasLetter = false;
|
|
1775
|
+
for (const char of name) {
|
|
1776
|
+
if (char >= "a" && char <= "z") return false;
|
|
1777
|
+
if (char >= "A" && char <= "Z" || char >= "0" && char <= "9" || char === "_") hasLetter = true;
|
|
1778
|
+
}
|
|
1779
|
+
return hasLetter;
|
|
1780
|
+
}
|
|
1781
|
+
findChildByType(node, type) {
|
|
1782
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1783
|
+
const child = node.namedChild(i);
|
|
1784
|
+
if (child?.type === type) return child;
|
|
1785
|
+
}
|
|
1786
|
+
return void 0;
|
|
1787
|
+
}
|
|
1788
|
+
firstNamedChild(node) {
|
|
1789
|
+
return node.namedChildCount > 0 ? node.namedChild(0) || void 0 : void 0;
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Cyclomatic decision points, counted from the pinned grammar's OWN node
|
|
1793
|
+
* types (probed: script/m14/tmp/perl-branch-shapes.ts).
|
|
1794
|
+
*
|
|
1795
|
+
* The pre-review set named `if_statement`, `unless_statement`,
|
|
1796
|
+
* `while_statement`, `until_statement`, `foreach_statement` and
|
|
1797
|
+
* `ternary_expression` — SIX literals the grammar does not declare, so only
|
|
1798
|
+
* `for_statement` and `conditional_expression` ever fired and
|
|
1799
|
+
* cyclomaticComplexity was 1 for almost every real file. This grammar spells
|
|
1800
|
+
* them `conditional_statement` (if/unless), `loop_statement` (while/until/
|
|
1801
|
+
* bare foreach), `for_statement`, `cstyle_for_statement`,
|
|
1802
|
+
* `conditional_expression` (ternary), `postfix_conditional_expression`
|
|
1803
|
+
* (statement modifier) and `try_statement`; `elsif` and `catch` are further
|
|
1804
|
+
* decisions in their own chains.
|
|
1805
|
+
*/
|
|
1806
|
+
countBranches(root) {
|
|
1807
|
+
const branchTypes = /* @__PURE__ */ new Set([
|
|
1808
|
+
"conditional_statement",
|
|
1809
|
+
"elsif",
|
|
1810
|
+
"loop_statement",
|
|
1811
|
+
"for_statement",
|
|
1812
|
+
"cstyle_for_statement",
|
|
1813
|
+
"conditional_expression",
|
|
1814
|
+
"postfix_conditional_expression",
|
|
1815
|
+
"try_statement",
|
|
1816
|
+
"catch",
|
|
1817
|
+
"when",
|
|
1818
|
+
"given"
|
|
1819
|
+
]);
|
|
1820
|
+
const logicalTypes = /* @__PURE__ */ new Set(["lowprec_logical_expression", "logical_not_expression"]);
|
|
1821
|
+
let count = 0;
|
|
1822
|
+
const stack = [root];
|
|
1823
|
+
while (stack.length > 0) {
|
|
1824
|
+
const node = stack.pop();
|
|
1825
|
+
if (branchTypes.has(node.type)) count++;
|
|
1826
|
+
else if (logicalTypes.has(node.type)) {
|
|
1827
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1828
|
+
const child = node.child(i);
|
|
1829
|
+
if (child && (child.type === "&&" || child.type === "||" || child.type === "and" || child.type === "or")) {
|
|
1830
|
+
count++;
|
|
1831
|
+
break;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1836
|
+
const child = node.namedChild(i);
|
|
1837
|
+
if (child) stack.push(child);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return count;
|
|
1841
|
+
}
|
|
1842
|
+
lineCount(content) {
|
|
1843
|
+
let lines = 1;
|
|
1844
|
+
for (const char of content) if (char === "\n") lines++;
|
|
1845
|
+
return lines;
|
|
1846
|
+
}
|
|
1847
|
+
nodeToRange(node) {
|
|
1848
|
+
return {
|
|
1849
|
+
startLine: node.startPosition.row + 1,
|
|
1850
|
+
startColumn: node.startPosition.column + 1,
|
|
1851
|
+
endLine: node.endPosition.row + 1,
|
|
1852
|
+
endColumn: node.endPosition.column + 1,
|
|
1853
|
+
startIndex: node.startIndex,
|
|
1854
|
+
endIndex: node.endIndex
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
export {
|
|
1859
|
+
PERL_BUILTINS,
|
|
1860
|
+
PERL_REASON_CODES,
|
|
1861
|
+
RealPerlAnalyzer,
|
|
1862
|
+
hasArgumentSyntax,
|
|
1863
|
+
perlArgumentNodes,
|
|
1864
|
+
perlWordValues
|
|
1865
|
+
};
|