@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,1731 @@
|
|
|
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-erlang-analyzer.ts
|
|
12
|
+
var OTP_BEHAVIOURS = /* @__PURE__ */ new Set(["gen_server", "gen_statem", "gen_event", "supervisor", "application"]);
|
|
13
|
+
var SPAWN_BIFS = /* @__PURE__ */ new Set(["spawn", "spawn_link", "spawn_monitor"]);
|
|
14
|
+
var TYPE_SCOPE_NODES = /* @__PURE__ */ new Set([
|
|
15
|
+
"spec",
|
|
16
|
+
"callback",
|
|
17
|
+
"type_alias",
|
|
18
|
+
"opaque",
|
|
19
|
+
"nominal",
|
|
20
|
+
"type_sig",
|
|
21
|
+
"fun_type_sig",
|
|
22
|
+
"ann_type",
|
|
23
|
+
"ann_var",
|
|
24
|
+
"field_type",
|
|
25
|
+
"fun_type",
|
|
26
|
+
"range_type",
|
|
27
|
+
"var_args",
|
|
28
|
+
"type_name",
|
|
29
|
+
"type_guards",
|
|
30
|
+
"pipe"
|
|
31
|
+
]);
|
|
32
|
+
var ARM_NODES = /* @__PURE__ */ new Set(["cr_clause", "if_clause", "catch_clause", "receive_after"]);
|
|
33
|
+
var COMPREHENSION_NODES = /* @__PURE__ */ new Set(["list_comprehension", "binary_comprehension", "map_comprehension"]);
|
|
34
|
+
var PRE_PASS_DESCEND = /* @__PURE__ */ new Set([
|
|
35
|
+
"pp_if",
|
|
36
|
+
"pp_ifdef",
|
|
37
|
+
"pp_ifndef",
|
|
38
|
+
"pp_else",
|
|
39
|
+
"pp_elif",
|
|
40
|
+
"pp_endif",
|
|
41
|
+
"ERROR"
|
|
42
|
+
]);
|
|
43
|
+
var MATCH_OPERATORS = /* @__PURE__ */ new Set(["=", "?=", "<-", "<:=", "<="]);
|
|
44
|
+
var DECL_NODES = /* @__PURE__ */ new Set([
|
|
45
|
+
"module_attribute",
|
|
46
|
+
"fun_decl",
|
|
47
|
+
"spec",
|
|
48
|
+
"record_decl",
|
|
49
|
+
"pp_define",
|
|
50
|
+
"type_alias",
|
|
51
|
+
"opaque",
|
|
52
|
+
"nominal",
|
|
53
|
+
"callback"
|
|
54
|
+
]);
|
|
55
|
+
var DEFAULT_EVIDENCE_CLASS = {
|
|
56
|
+
contains: "structural",
|
|
57
|
+
exports: "structural",
|
|
58
|
+
implements: "structural",
|
|
59
|
+
instantiates: "structural",
|
|
60
|
+
overrides: "structural",
|
|
61
|
+
extends: "structural",
|
|
62
|
+
imports: "import_based",
|
|
63
|
+
references: "reference_qualified",
|
|
64
|
+
calls: "usage_contextual",
|
|
65
|
+
uses: "usage_contextual"
|
|
66
|
+
};
|
|
67
|
+
function evidenceClassFor(type, confidence, target) {
|
|
68
|
+
const t = String(type ?? "");
|
|
69
|
+
if (t === "calls") return target.indexOf(":") !== -1 ? "reference_qualified" : "usage_contextual";
|
|
70
|
+
return DEFAULT_EVIDENCE_CLASS[t] ?? (confidence >= 0.85 ? "structural" : "inferred");
|
|
71
|
+
}
|
|
72
|
+
var RealErlangAnalyzer = class {
|
|
73
|
+
analyze(rootNode, content, filePath) {
|
|
74
|
+
const ctx = {
|
|
75
|
+
filePath,
|
|
76
|
+
symbols: [],
|
|
77
|
+
relationships: [],
|
|
78
|
+
imports: [],
|
|
79
|
+
exports: [],
|
|
80
|
+
errors: [],
|
|
81
|
+
patterns: {},
|
|
82
|
+
seenRels: /* @__PURE__ */ new Set(),
|
|
83
|
+
funDecls: /* @__PURE__ */ new Map(),
|
|
84
|
+
declaredFnKeys: /* @__PURE__ */ new Set(),
|
|
85
|
+
clauseCountByKey: /* @__PURE__ */ new Map(),
|
|
86
|
+
fnBranches: /* @__PURE__ */ new Map(),
|
|
87
|
+
specs: /* @__PURE__ */ new Map(),
|
|
88
|
+
macroAtoms: /* @__PURE__ */ new Map(),
|
|
89
|
+
nifEntries: /* @__PURE__ */ new Map(),
|
|
90
|
+
onLoadEntries: /* @__PURE__ */ new Map(),
|
|
91
|
+
functionSymbolByKey: /* @__PURE__ */ new Map(),
|
|
92
|
+
moduleName: void 0,
|
|
93
|
+
typeDepth: 0,
|
|
94
|
+
macroDepth: 0,
|
|
95
|
+
declStack: [],
|
|
96
|
+
currentFun: void 0,
|
|
97
|
+
totalBranches: 0,
|
|
98
|
+
optionalCallbacks: [],
|
|
99
|
+
moduleSymbol: void 0,
|
|
100
|
+
stats: { dynamicRemote: 0, unresolvedNifs: 0, macroHeadDecls: 0 },
|
|
101
|
+
grammarDamaged: false
|
|
102
|
+
};
|
|
103
|
+
this.collectPrePass(rootNode, ctx);
|
|
104
|
+
const DECLARATION_FORMS = /* @__PURE__ */ new Set([
|
|
105
|
+
"fun_decl",
|
|
106
|
+
"module_attribute",
|
|
107
|
+
"pp_define",
|
|
108
|
+
"record_decl",
|
|
109
|
+
"type_alias",
|
|
110
|
+
"opaque",
|
|
111
|
+
"nominal",
|
|
112
|
+
"import_attribute",
|
|
113
|
+
"export_attribute",
|
|
114
|
+
"export_type_attribute",
|
|
115
|
+
// `-include` / `-include_lib` are declarations too. An include-ONLY header has
|
|
116
|
+
// no symbol to publish but a real dependency to record, so calling it
|
|
117
|
+
// "grammar damaged" would be as false as calling a comment-only header
|
|
118
|
+
// damaged was.
|
|
119
|
+
"pp_include",
|
|
120
|
+
"pp_include_lib"
|
|
121
|
+
]);
|
|
122
|
+
const hasErrorNodes = rootNode.hasError === true;
|
|
123
|
+
if (hasErrorNodes && !this.prePassForms(rootNode).some((f) => DECLARATION_FORMS.has(f.type))) {
|
|
124
|
+
ctx.grammarDamaged = true;
|
|
125
|
+
}
|
|
126
|
+
const stack = [{ node: rootNode }];
|
|
127
|
+
while (stack.length > 0) {
|
|
128
|
+
const { node, isExit } = stack.pop();
|
|
129
|
+
const type = node.type;
|
|
130
|
+
const named = node.isNamed;
|
|
131
|
+
if (isExit) {
|
|
132
|
+
if (named && TYPE_SCOPE_NODES.has(type)) ctx.typeDepth--;
|
|
133
|
+
if (named && type === "pp_define") ctx.macroDepth--;
|
|
134
|
+
if (named && DECL_NODES.has(type)) ctx.declStack.pop();
|
|
135
|
+
if (named && type === "fun_decl") {
|
|
136
|
+
const done = ctx.currentFun;
|
|
137
|
+
if (done) ctx.fnBranches.set(done.key, (ctx.fnBranches.get(done.key) ?? 0) + done.branches);
|
|
138
|
+
ctx.currentFun = void 0;
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let pushScope = false;
|
|
143
|
+
if (named && TYPE_SCOPE_NODES.has(type)) ctx.typeDepth++;
|
|
144
|
+
if (named && type === "pp_define") ctx.macroDepth++;
|
|
145
|
+
if (named && (TYPE_SCOPE_NODES.has(type) || DECL_NODES.has(type) || type === "pp_define")) pushScope = true;
|
|
146
|
+
if (type === "ERROR" || node.isMissing) {
|
|
147
|
+
ctx.errors.push({
|
|
148
|
+
message: `Erlang syntax ${node.isMissing ? "MISSING" : "ERROR"} node.`,
|
|
149
|
+
range: this.nodeToRange(node),
|
|
150
|
+
severity: "warning"
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (named) {
|
|
154
|
+
switch (type) {
|
|
155
|
+
// ---- module-level declarations ----
|
|
156
|
+
case "module_attribute":
|
|
157
|
+
this.processModuleAttribute(node, ctx, filePath);
|
|
158
|
+
break;
|
|
159
|
+
case "behaviour_attribute":
|
|
160
|
+
this.processBehaviourAttribute(node, ctx, filePath);
|
|
161
|
+
break;
|
|
162
|
+
case "export_attribute":
|
|
163
|
+
this.processFaListAttribute(node, ctx, filePath, ASTEngine.SymbolKind.Function);
|
|
164
|
+
break;
|
|
165
|
+
case "export_type_attribute":
|
|
166
|
+
this.processFaListAttribute(node, ctx, filePath, ASTEngine.SymbolKind.Type);
|
|
167
|
+
break;
|
|
168
|
+
case "import_attribute":
|
|
169
|
+
this.processImportAttribute(node, ctx, filePath);
|
|
170
|
+
break;
|
|
171
|
+
case "optional_callbacks_attribute":
|
|
172
|
+
this.processOptionalCallbacksAttribute(node, ctx);
|
|
173
|
+
break;
|
|
174
|
+
// ---- preprocessor ----
|
|
175
|
+
case "pp_include":
|
|
176
|
+
case "pp_include_lib":
|
|
177
|
+
this.processInclude(node, ctx, type === "pp_include", filePath);
|
|
178
|
+
break;
|
|
179
|
+
case "pp_define":
|
|
180
|
+
this.processDefine(node, ctx);
|
|
181
|
+
break;
|
|
182
|
+
// ---- type declarations: the type graph ----
|
|
183
|
+
case "type_alias":
|
|
184
|
+
case "opaque":
|
|
185
|
+
case "nominal":
|
|
186
|
+
this.processTypeDeclaration(node, ctx, type, filePath);
|
|
187
|
+
break;
|
|
188
|
+
case "callback":
|
|
189
|
+
this.processCallback(node, ctx, filePath);
|
|
190
|
+
break;
|
|
191
|
+
case "spec":
|
|
192
|
+
this.processSpecTypes(node, ctx);
|
|
193
|
+
break;
|
|
194
|
+
case "record_decl":
|
|
195
|
+
this.processRecordDecl(node, ctx, filePath);
|
|
196
|
+
break;
|
|
197
|
+
case "field_type":
|
|
198
|
+
this.processTypeReferences(node, void 0, this.currentSource(ctx) ?? "", ctx);
|
|
199
|
+
break;
|
|
200
|
+
// ---- functions ----
|
|
201
|
+
case "fun_decl":
|
|
202
|
+
this.enterFunDecl(node, ctx, filePath);
|
|
203
|
+
break;
|
|
204
|
+
// ---- evaluation-time edges ----
|
|
205
|
+
case "call":
|
|
206
|
+
this.processCall(node, ctx);
|
|
207
|
+
break;
|
|
208
|
+
case "record_expr":
|
|
209
|
+
this.processRecordExpr(node, ctx);
|
|
210
|
+
break;
|
|
211
|
+
case "record_field_expr":
|
|
212
|
+
case "record_index_expr":
|
|
213
|
+
this.processRecordRead(node, ctx);
|
|
214
|
+
break;
|
|
215
|
+
case "record_update_expr":
|
|
216
|
+
this.processRecordUpdate(node, ctx);
|
|
217
|
+
break;
|
|
218
|
+
case "macro_call_expr":
|
|
219
|
+
this.processMacroUse(node, ctx);
|
|
220
|
+
break;
|
|
221
|
+
case "internal_fun":
|
|
222
|
+
this.processFunReference(node, ctx, false);
|
|
223
|
+
break;
|
|
224
|
+
case "external_fun":
|
|
225
|
+
this.processFunReference(node, ctx, true);
|
|
226
|
+
break;
|
|
227
|
+
// ---- control constructs: patterns + decision points ----
|
|
228
|
+
case "binary_op_expr":
|
|
229
|
+
this.processBinaryOp(node, ctx);
|
|
230
|
+
break;
|
|
231
|
+
case "receive_expr":
|
|
232
|
+
ctx.patterns["Message Reception (receive)"] = true;
|
|
233
|
+
ctx.patterns["Actor Model (OTP)"] = true;
|
|
234
|
+
break;
|
|
235
|
+
case "case_expr":
|
|
236
|
+
case "if_expr":
|
|
237
|
+
case "try_expr":
|
|
238
|
+
case "maybe_expr":
|
|
239
|
+
ctx.patterns["Pattern Matching"] = true;
|
|
240
|
+
break;
|
|
241
|
+
case "anonymous_fun":
|
|
242
|
+
ctx.patterns["Higher-Order Functions"] = true;
|
|
243
|
+
break;
|
|
244
|
+
default:
|
|
245
|
+
if (ARM_NODES.has(type)) this.countBranch(ctx);
|
|
246
|
+
else if (COMPREHENSION_NODES.has(type)) {
|
|
247
|
+
ctx.patterns["Comprehensions"] = true;
|
|
248
|
+
this.countBranch(ctx);
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (pushScope) stack.push({ node, isExit: true });
|
|
254
|
+
for (let i = node.childCount - 1; i >= 0; i--) {
|
|
255
|
+
const child = node.child(i);
|
|
256
|
+
if (child) stack.push({ node: child });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const [key, branches] of ctx.fnBranches) {
|
|
260
|
+
const sym = ctx.functionSymbolByKey.get(key);
|
|
261
|
+
if (sym) sym.complexity = 1 + branches;
|
|
262
|
+
}
|
|
263
|
+
this.annotateNativeFunctions(ctx);
|
|
264
|
+
if (ctx.grammarDamaged) {
|
|
265
|
+
return {
|
|
266
|
+
language: "erlang",
|
|
267
|
+
symbols: [],
|
|
268
|
+
relationships: [],
|
|
269
|
+
imports: [],
|
|
270
|
+
exports: [],
|
|
271
|
+
errors: ctx.errors,
|
|
272
|
+
metadata: {
|
|
273
|
+
complexity: 0,
|
|
274
|
+
linesOfCode: content.split("\n").length,
|
|
275
|
+
patterns: [],
|
|
276
|
+
framework: "Erlang",
|
|
277
|
+
hasFrameworkIntelligence: false,
|
|
278
|
+
hasTypeInference: false,
|
|
279
|
+
grammarDamaged: true
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
this.emitOwnership(ctx);
|
|
284
|
+
if (ctx.moduleName) {
|
|
285
|
+
ctx.patterns["OTP Module"] = true;
|
|
286
|
+
const ms = ctx.moduleSymbol;
|
|
287
|
+
if (ms && ctx.optionalCallbacks.length > 0) {
|
|
288
|
+
ms.metadata = { ...ms.metadata ?? {}, optionalCallbacks: ctx.optionalCallbacks };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const pa = [];
|
|
292
|
+
for (const k in ctx.patterns) pa.push(k);
|
|
293
|
+
for (const rel of ctx.relationships) {
|
|
294
|
+
const md = rel.metadata ?? {};
|
|
295
|
+
if (typeof md.evidenceClass !== "string" || md.evidenceClass.length === 0) {
|
|
296
|
+
md.evidenceClass = evidenceClassFor(rel.type, Number(rel.confidence), String(rel.target ?? ""));
|
|
297
|
+
rel.metadata = md;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
language: "erlang",
|
|
302
|
+
symbols: ctx.symbols,
|
|
303
|
+
relationships: ctx.relationships,
|
|
304
|
+
imports: ctx.imports,
|
|
305
|
+
exports: ctx.exports,
|
|
306
|
+
errors: ctx.errors,
|
|
307
|
+
metadata: {
|
|
308
|
+
complexity: 1 + ctx.totalBranches,
|
|
309
|
+
linesOfCode: content.split("\n").length,
|
|
310
|
+
patterns: pa,
|
|
311
|
+
framework: "Erlang",
|
|
312
|
+
hasFrameworkIntelligence: true,
|
|
313
|
+
hasTypeInference: false
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// ==========================================================================
|
|
318
|
+
// Pre-pass
|
|
319
|
+
// ==========================================================================
|
|
320
|
+
/**
|
|
321
|
+
* ER-D29: every DECLARATION is a direct child of `source_file`. Measured over
|
|
322
|
+
* the full 4,326-file census: module_attribute, behaviour_attribute,
|
|
323
|
+
* export_attribute, export_type_attribute, import_attribute,
|
|
324
|
+
* optional_callbacks_attribute, pp_include, pp_include_lib, pp_define,
|
|
325
|
+
* type_alias, opaque, nominal, spec, callback, record_decl — every one of them
|
|
326
|
+
* has `source_file` as its ONLY parent (the census's other "parent" entries are
|
|
327
|
+
* the anonymous keyword token that shares the type's name, e.g. the `spec`
|
|
328
|
+
* inside a `spec`).
|
|
329
|
+
*
|
|
330
|
+
* So the pre-pass reads the top level instead of walking all ~4,000 nodes per
|
|
331
|
+
* file again. The one place a declaration can hide is inside conditional
|
|
332
|
+
* compilation (`-ifdef … -endif`), whose bodies are top-level forms too, so
|
|
333
|
+
* those containers are descended into explicitly.
|
|
334
|
+
*/
|
|
335
|
+
collectPrePass(rootNode, ctx) {
|
|
336
|
+
for (const form of this.prePassForms(rootNode)) {
|
|
337
|
+
if (form.type !== "pp_define") continue;
|
|
338
|
+
const lhs = this.getChildByType(form, "macro_lhs");
|
|
339
|
+
const nameNode = lhs ? this.getChildByType(lhs, "var") ?? this.getChildByType(lhs, "atom") : void 0;
|
|
340
|
+
if (!nameNode?.text) continue;
|
|
341
|
+
for (let i = 0; i < form.namedChildCount; i++) {
|
|
342
|
+
const body = form.namedChild(i);
|
|
343
|
+
if (!body || body.type !== "atom" || !body.text) continue;
|
|
344
|
+
ctx.macroAtoms.set(nameNode.text, body.text);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const top of this.prePassForms(rootNode)) {
|
|
349
|
+
if (top.type !== "module_attribute") continue;
|
|
350
|
+
const atom = this.getChildByType(top, "atom");
|
|
351
|
+
if (atom?.text) {
|
|
352
|
+
ctx.moduleName = atom.text;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
const expanded = this.expandModuleNameMacro(top, ctx);
|
|
356
|
+
if (expanded) {
|
|
357
|
+
ctx.moduleName = expanded;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const forms = this.prePassForms(rootNode);
|
|
362
|
+
const stack = [];
|
|
363
|
+
for (let i = forms.length - 1; i >= 0; i--) {
|
|
364
|
+
const top = forms[i];
|
|
365
|
+
if (top) stack.push(top);
|
|
366
|
+
}
|
|
367
|
+
while (stack.length > 0) {
|
|
368
|
+
const node = stack.pop();
|
|
369
|
+
if (!node.isNamed) continue;
|
|
370
|
+
const type = node.type;
|
|
371
|
+
if (type === "fun_decl") {
|
|
372
|
+
const clauses = this.collectChildrenByType(node, "function_clause");
|
|
373
|
+
const head = this.readClauseHead(clauses[0], ctx.moduleName);
|
|
374
|
+
if (head) {
|
|
375
|
+
ctx.funDecls.set(node.startIndex, {
|
|
376
|
+
name: head.name,
|
|
377
|
+
arity: head.arity,
|
|
378
|
+
clauseCount: clauses.length,
|
|
379
|
+
macroHead: head.macroHead,
|
|
380
|
+
startIndex: node.startIndex
|
|
381
|
+
});
|
|
382
|
+
const key = this.fnKey(head.name, head.arity);
|
|
383
|
+
ctx.declaredFnKeys.add(key);
|
|
384
|
+
ctx.clauseCountByKey.set(key, (ctx.clauseCountByKey.get(key) ?? 0) + clauses.length);
|
|
385
|
+
if (head.macroHead) ctx.stats.macroHeadDecls++;
|
|
386
|
+
}
|
|
387
|
+
} else if (type === "spec") {
|
|
388
|
+
const nameNode = this.getChildByType(node, "atom");
|
|
389
|
+
const sig = this.getChildByType(node, "type_sig");
|
|
390
|
+
if (nameNode?.text && sig) {
|
|
391
|
+
const params = this.namedChildrenTexts(this.getChildByType(sig, "expr_args"));
|
|
392
|
+
ctx.specs.set(this.fnKey(nameNode.text, params.length), {
|
|
393
|
+
params,
|
|
394
|
+
ret: this.typeSigReturnText(sig),
|
|
395
|
+
line: nameNode.startPosition.row + 1
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
} else if (type === "wild_attribute") {
|
|
399
|
+
const attrName = this.attrNameText(node);
|
|
400
|
+
if (attrName === "nifs") this.collectAttributeFaEntries(node, ctx.nifEntries, ctx);
|
|
401
|
+
else if (attrName === "on_load") this.collectAttributeFaEntries(node, ctx.onLoadEntries, ctx);
|
|
402
|
+
} else if (PRE_PASS_DESCEND.has(type)) {
|
|
403
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
404
|
+
const c = node.namedChild(i);
|
|
405
|
+
if (c) stack.push(c);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* The forms the pre-pass reads: `source_file`'s named children, with any
|
|
412
|
+
* conditional-compilation or grammar-ERROR container's children spliced in
|
|
413
|
+
* (ER-D29). Every DECLARATION lives here — measured over the 4,326-file
|
|
414
|
+
* census — so the pre-pass costs O(declarations) instead of a second walk of
|
|
415
|
+
* all ~4,000 nodes per file.
|
|
416
|
+
*/
|
|
417
|
+
prePassForms(rootNode) {
|
|
418
|
+
const out = [];
|
|
419
|
+
const stack = [];
|
|
420
|
+
for (let i = rootNode.namedChildCount - 1; i >= 0; i--) {
|
|
421
|
+
const c = rootNode.namedChild(i);
|
|
422
|
+
if (c) stack.push(c);
|
|
423
|
+
}
|
|
424
|
+
while (stack.length > 0) {
|
|
425
|
+
const n = stack.pop();
|
|
426
|
+
if (!n.isNamed) continue;
|
|
427
|
+
if (PRE_PASS_DESCEND.has(n.type)) {
|
|
428
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
429
|
+
const c = n.namedChild(i);
|
|
430
|
+
if (c) stack.push(c);
|
|
431
|
+
}
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
out.push(n);
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
fnKey(name, arity) {
|
|
439
|
+
return `${name}/${arity}`;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The inverse of `fnKey`, for the two places that must go back from a collected
|
|
443
|
+
* "name/arity" entry to a symbol. Erlang function names cannot contain `/`, so
|
|
444
|
+
* the LAST slash is the separator and a name like `mod/1` never appears here.
|
|
445
|
+
*/
|
|
446
|
+
splitFnKey(key) {
|
|
447
|
+
const i = key.lastIndexOf("/");
|
|
448
|
+
return i === -1 ? [key, ""] : [key.slice(0, i), key.slice(i + 1)];
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* A clause head's declared name and arity.
|
|
452
|
+
*
|
|
453
|
+
* ER-D25: the name is normally the head's `atom`, but a MACRO-headed
|
|
454
|
+
* definition carries a `macro_call_expr` instead, and the real function name
|
|
455
|
+
* only exists AFTER the preprocessor expands it. Measured over the whole
|
|
456
|
+
* corpus, every one of the 79 such clauses is `?MODULE() -> …` — the OTP test
|
|
457
|
+
* suite idiom — and `?MODULE` is a PREDEFINED macro whose value is this file's
|
|
458
|
+
* module atom, so the expansion is exact: the function is `<module>/<arity>`.
|
|
459
|
+
* That is the OTP `yes_2.erl` / `stacktrace.erl` shape, and resolving it
|
|
460
|
+
* recovers 79 function definitions that previously produced NO symbol at all
|
|
461
|
+
* (the "unreadable clause name" the pre-ER3 oracle had to mirror).
|
|
462
|
+
*
|
|
463
|
+
* Any OTHER macro head is refused on purpose. `-define(DOUBLE(X), helper(X))`
|
|
464
|
+
* plus `?DOUBLE(X) -> X` declares `helper/1`, not `DOUBLE/1`: naming the macro
|
|
465
|
+
* would FABRICATE a function that does not exist, and the analyzer does not
|
|
466
|
+
* expand macros. The clause's body is then attributed to the module, and the
|
|
467
|
+
* parse is reported by the error channel.
|
|
468
|
+
*/
|
|
469
|
+
readClauseHead(clause, moduleName) {
|
|
470
|
+
if (!clause) return void 0;
|
|
471
|
+
const arity = this.countArgs(this.getChildByType(clause, "expr_args"));
|
|
472
|
+
const direct = this.getChildByType(clause, "atom") ?? this.getChildByType(clause, "var");
|
|
473
|
+
if (direct?.text) return { name: direct.text, arity, macroHead: false };
|
|
474
|
+
const macro = this.getChildByType(clause, "macro_call_expr");
|
|
475
|
+
const macroName = macro ? (this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom"))?.text : void 0;
|
|
476
|
+
if (macroName === "MODULE" && moduleName) return { name: moduleName, arity, macroHead: true };
|
|
477
|
+
return void 0;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* A parameter list's arity: the named children of `expr_args`, comments
|
|
481
|
+
* excluded. ONE counter serves a clause head, a call site and a `fa`, so
|
|
482
|
+
* `f/1` and `f/2` can never be confused anywhere in this analyzer (ER-D2).
|
|
483
|
+
*/
|
|
484
|
+
countArgs(args) {
|
|
485
|
+
if (!args) return 0;
|
|
486
|
+
let n = 0;
|
|
487
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
488
|
+
const c = args.namedChild(i);
|
|
489
|
+
if (c && c.type !== "comment") n++;
|
|
490
|
+
}
|
|
491
|
+
return n;
|
|
492
|
+
}
|
|
493
|
+
namedChildrenTexts(args) {
|
|
494
|
+
if (!args) return [];
|
|
495
|
+
const out = [];
|
|
496
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
497
|
+
const c = args.namedChild(i);
|
|
498
|
+
if (c && c.type !== "comment") out.push(c.text);
|
|
499
|
+
}
|
|
500
|
+
return out;
|
|
501
|
+
}
|
|
502
|
+
/** The text after a type signature's `->`, or undefined when it has none. */
|
|
503
|
+
typeSigReturnText(sig) {
|
|
504
|
+
for (let i = 0; i < sig.childCount; i++) {
|
|
505
|
+
const c = sig.child(i);
|
|
506
|
+
if (c?.type !== "->") continue;
|
|
507
|
+
const t = sig.text.slice(c.endIndex - sig.startIndex).trim();
|
|
508
|
+
return t.length > 0 ? t : void 0;
|
|
509
|
+
}
|
|
510
|
+
return void 0;
|
|
511
|
+
}
|
|
512
|
+
// ==========================================================================
|
|
513
|
+
// Module-level declarations
|
|
514
|
+
// ==========================================================================
|
|
515
|
+
/**
|
|
516
|
+
* The module name a `-module(?MACRO)` attribute expands to, or undefined when
|
|
517
|
+
* it cannot be known without running the preprocessor.
|
|
518
|
+
*
|
|
519
|
+
* ER-D34. Common Test suites name themselves through a macro by convention, so
|
|
520
|
+
* this is not exotic: otp/lib/kernel/test/file_SUITE.erl writes
|
|
521
|
+
* `-module(?FILE_SUITE).` and is the only such file in the 4,326-file corpus
|
|
522
|
+
* — rare, but the failure was silent and total. The attribute parsed as a
|
|
523
|
+
* module_attribute with a macro_call_expr child, and a reader that required an
|
|
524
|
+
* `atom` emitted no module symbol at all, so the file fell back to being owned
|
|
525
|
+
* by its path: wrong owner on every edge, no module in the store, exports
|
|
526
|
+
* unmatched.
|
|
527
|
+
*/
|
|
528
|
+
expandModuleNameMacro(attr, ctx) {
|
|
529
|
+
const macro = this.getChildByType(attr, "macro_call_expr") ?? this.getChildByType(attr, "macro_expr");
|
|
530
|
+
if (!macro) return void 0;
|
|
531
|
+
const nameNode = this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom");
|
|
532
|
+
if (!nameNode?.text) return void 0;
|
|
533
|
+
return ctx.macroAtoms.get(nameNode.text);
|
|
534
|
+
}
|
|
535
|
+
processModuleAttribute(node, ctx, filePath) {
|
|
536
|
+
if (ctx.moduleSymbol) return;
|
|
537
|
+
let nameNode = this.getChildByType(node, "atom");
|
|
538
|
+
let moduleNameOverride;
|
|
539
|
+
if (!nameNode?.text) {
|
|
540
|
+
const macro = this.getChildByType(node, "macro_call_expr") ?? this.getChildByType(node, "macro_expr");
|
|
541
|
+
const macroName = macro ? this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom") : void 0;
|
|
542
|
+
const expanded = this.expandModuleNameMacro(node, ctx);
|
|
543
|
+
if (!macroName?.text) return;
|
|
544
|
+
const source = expanded ?? this.ownerNameOf(ctx) ?? node.text;
|
|
545
|
+
this.pushRel(
|
|
546
|
+
ctx,
|
|
547
|
+
source,
|
|
548
|
+
macroName.text,
|
|
549
|
+
ASTEngine.RelationshipType.References,
|
|
550
|
+
0.9,
|
|
551
|
+
{
|
|
552
|
+
erlangSpecific: true,
|
|
553
|
+
context: "macro use",
|
|
554
|
+
isModuleNameMacro: true,
|
|
555
|
+
moduleNameResolved: !!expanded
|
|
556
|
+
},
|
|
557
|
+
macroName.startPosition.row + 1
|
|
558
|
+
);
|
|
559
|
+
if (!expanded) return;
|
|
560
|
+
moduleNameOverride = expanded;
|
|
561
|
+
nameNode = macroName;
|
|
562
|
+
}
|
|
563
|
+
const moduleName = moduleNameOverride ?? nameNode.text;
|
|
564
|
+
const symbol = {
|
|
565
|
+
name: moduleName,
|
|
566
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
567
|
+
range: this.nodeToRange(node),
|
|
568
|
+
metadata: {
|
|
569
|
+
nodeType: "module_attribute",
|
|
570
|
+
line: nameNode.startPosition.row + 1,
|
|
571
|
+
...moduleNameOverride ? { nameFromMacro: nameNode.text, expandsTo: moduleNameOverride } : {}
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
ctx.moduleSymbol = symbol;
|
|
575
|
+
ctx.symbols.push(symbol);
|
|
576
|
+
ctx.moduleName = moduleName;
|
|
577
|
+
ctx.declStack.push({ source: moduleName, kind: "module" });
|
|
578
|
+
void filePath;
|
|
579
|
+
}
|
|
580
|
+
processBehaviourAttribute(node, ctx, filePath) {
|
|
581
|
+
const beNode = this.getChildByType(node, "atom");
|
|
582
|
+
if (!beNode?.text) return;
|
|
583
|
+
this.pushRel(
|
|
584
|
+
ctx,
|
|
585
|
+
ctx.moduleName ?? filePath,
|
|
586
|
+
beNode.text,
|
|
587
|
+
ASTEngine.RelationshipType.Implements,
|
|
588
|
+
1,
|
|
589
|
+
{ erlangSpecific: true, behaviour: beNode.text, context: "OTP behaviour callback implementation" },
|
|
590
|
+
beNode.startPosition.row + 1
|
|
591
|
+
);
|
|
592
|
+
if (OTP_BEHAVIOURS.has(beNode.text)) {
|
|
593
|
+
ctx.patterns["Actor Model (OTP)"] = true;
|
|
594
|
+
ctx.patterns["OTP Behaviours"] = true;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/** `-export` / `-export_type`: a list of `fa` nodes. */
|
|
598
|
+
processFaListAttribute(node, ctx, filePath, kind) {
|
|
599
|
+
const owner = ctx.moduleName ?? filePath;
|
|
600
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
601
|
+
const child = node.namedChild(i);
|
|
602
|
+
if (child?.type !== "fa") continue;
|
|
603
|
+
const fa = this.readFa(child, ctx.moduleName);
|
|
604
|
+
if (!fa) continue;
|
|
605
|
+
ctx.exports.push({
|
|
606
|
+
name: fa.name,
|
|
607
|
+
arity: fa.arity,
|
|
608
|
+
file: filePath,
|
|
609
|
+
line: fa.line,
|
|
610
|
+
kind,
|
|
611
|
+
...kind === ASTEngine.SymbolKind.Type ? { metadata: { isTypeExport: true } } : {}
|
|
612
|
+
});
|
|
613
|
+
this.pushRel(
|
|
614
|
+
ctx,
|
|
615
|
+
owner,
|
|
616
|
+
fa.name,
|
|
617
|
+
ASTEngine.RelationshipType.Exports,
|
|
618
|
+
1,
|
|
619
|
+
{
|
|
620
|
+
erlangSpecific: true,
|
|
621
|
+
arity: fa.arity,
|
|
622
|
+
exportForm: kind === ASTEngine.SymbolKind.Type ? "export_type" : "export",
|
|
623
|
+
isTypeExport: kind === ASTEngine.SymbolKind.Type
|
|
624
|
+
},
|
|
625
|
+
fa.line
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
processImportAttribute(node, ctx, filePath) {
|
|
630
|
+
const modNode = this.getChildByType(node, "atom");
|
|
631
|
+
if (!modNode?.text) return;
|
|
632
|
+
const funcs = [];
|
|
633
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
634
|
+
const child = node.namedChild(i);
|
|
635
|
+
if (child?.type !== "fa") continue;
|
|
636
|
+
const fa = this.readFa(child, ctx.moduleName);
|
|
637
|
+
if (fa) funcs.push(fa.name);
|
|
638
|
+
}
|
|
639
|
+
ctx.imports.push({
|
|
640
|
+
module: modNode.text,
|
|
641
|
+
path: modNode.text,
|
|
642
|
+
importType: "named",
|
|
643
|
+
// ER-D20: `-import(lists, …)` names an OTP module, which by construction
|
|
644
|
+
// cannot be declared in this file — M60/#910's `is_external` contract,
|
|
645
|
+
// which the file-dependency engine reads to keep system dependencies out
|
|
646
|
+
// of internal resolution. The old code wrote no classification at all.
|
|
647
|
+
isExternal: true,
|
|
648
|
+
importedNames: funcs,
|
|
649
|
+
items: funcs,
|
|
650
|
+
file: filePath,
|
|
651
|
+
line: modNode.startPosition.row + 1,
|
|
652
|
+
isNif: false
|
|
653
|
+
});
|
|
654
|
+
this.pushRel(
|
|
655
|
+
ctx,
|
|
656
|
+
ctx.moduleName ?? filePath,
|
|
657
|
+
modNode.text,
|
|
658
|
+
ASTEngine.RelationshipType.Imports,
|
|
659
|
+
1,
|
|
660
|
+
{ erlangSpecific: true, isModuleImport: true, importedFunctions: funcs, isExternal: true },
|
|
661
|
+
modNode.startPosition.row + 1
|
|
662
|
+
);
|
|
663
|
+
this.pushRel(
|
|
664
|
+
ctx,
|
|
665
|
+
ctx.moduleName ?? filePath,
|
|
666
|
+
modNode.text,
|
|
667
|
+
ASTEngine.RelationshipType.References,
|
|
668
|
+
1,
|
|
669
|
+
{ erlangSpecific: true, isModuleImport: true, importedFunctions: funcs },
|
|
670
|
+
modNode.startPosition.row + 1
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* `-optional_callbacks([f/2, …])` names callbacks a behaviour MAY omit. It
|
|
675
|
+
* declares nothing, so no symbol and no edge; the names are recorded on the
|
|
676
|
+
* module symbol, which is the only place they are true.
|
|
677
|
+
*/
|
|
678
|
+
processOptionalCallbacksAttribute(node, ctx) {
|
|
679
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
680
|
+
const child = node.namedChild(i);
|
|
681
|
+
if (child?.type !== "fa") continue;
|
|
682
|
+
const fa = this.readFa(child, ctx.moduleName);
|
|
683
|
+
if (fa) ctx.optionalCallbacks.push(this.fnKey(fa.name, fa.arity));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// ==========================================================================
|
|
687
|
+
// Preprocessor
|
|
688
|
+
// ==========================================================================
|
|
689
|
+
/**
|
|
690
|
+
* `-include("x.hrl")` / `-include_lib("kernel/include/x.hrl")`.
|
|
691
|
+
*
|
|
692
|
+
* ER-D46 (2026-10-23): THE LABEL IS `part_of`, NOT `references`. An include is
|
|
693
|
+
* FILE INCLUSION: the header is textually spliced into THIS module's single
|
|
694
|
+
* compilation scope, it cannot be compiled as a unit of its own, and it
|
|
695
|
+
* imports nothing — which is the vocabulary's `part_of` contract
|
|
696
|
+
* (`src/ast/types.ts:PartOf`: "a library unit INCLUDES a part file … one
|
|
697
|
+
* compilation unit"), the same construct php's include/require and fsharp's
|
|
698
|
+
* `#load` were relabelled to. `references` said the path was MENTIONED; it
|
|
699
|
+
* cannot say the file became part of this unit.
|
|
700
|
+
*
|
|
701
|
+
* DIRECTION AND ROLE follow the directive's own: source = the file carrying
|
|
702
|
+
* the directive (its module symbol, else its path — ER-D32); target = the
|
|
703
|
+
* path as written. `metadata.partRole:"library"` because THIS file is the
|
|
704
|
+
* including library and names the part. `isExternal` keeps ER-D20's
|
|
705
|
+
* classification: `-include_lib` resolves through the code path (a system
|
|
706
|
+
* header), `-include` is this project's.
|
|
707
|
+
*
|
|
708
|
+
* NOT `imports`: `-import(Mod, [f/a])` stays imports + references (ER-D41) —
|
|
709
|
+
* a name import creates binding scope, an include splices text. The two
|
|
710
|
+
* constructs were distinguished by the fixture `imports-and-try` before this
|
|
711
|
+
* change and are pinned apart by the `extended-part-of` fixture after it.
|
|
712
|
+
*
|
|
713
|
+
* THE TIER FOLLOWS THE CONSTRUCT, as php's `part_of` relabel did: a file
|
|
714
|
+
* inclusion directive is `import_based`, confidence 1.0 (the directive IS the
|
|
715
|
+
* syntax) — not the old row's 0.9/`reference_qualified`, which was the
|
|
716
|
+
* mention reading the relabel exists to replace.
|
|
717
|
+
*
|
|
718
|
+
* M59 (#825): the line belongs to the NAME-BEARING node — the path STRING,
|
|
719
|
+
* never the `-include` keyword. The fixture erlang/callsite-lines-825 splits
|
|
720
|
+
* the attribute across three lines precisely to pin that difference.
|
|
721
|
+
*/
|
|
722
|
+
processInclude(node, ctx, isPlain, filePath) {
|
|
723
|
+
const fNode = this.getChildByType(node, "string");
|
|
724
|
+
if (!fNode) return;
|
|
725
|
+
const includePath = this.stripQuotes(fNode.text);
|
|
726
|
+
const line = fNode.startPosition.row + 1;
|
|
727
|
+
ctx.imports.push({
|
|
728
|
+
module: includePath,
|
|
729
|
+
path: includePath,
|
|
730
|
+
importType: isPlain ? "include" : "include_lib",
|
|
731
|
+
// ER-D20: `-include_lib("kernel/include/…")` resolves through the code
|
|
732
|
+
// path — a system dependency. `-include("local.hrl")` is this project's.
|
|
733
|
+
isExternal: !isPlain,
|
|
734
|
+
file: includePath,
|
|
735
|
+
line,
|
|
736
|
+
isNif: false
|
|
737
|
+
});
|
|
738
|
+
this.pushRel(
|
|
739
|
+
ctx,
|
|
740
|
+
ctx.moduleName ?? filePath,
|
|
741
|
+
includePath,
|
|
742
|
+
ASTEngine.RelationshipType.PartOf,
|
|
743
|
+
1,
|
|
744
|
+
{
|
|
745
|
+
erlangSpecific: true,
|
|
746
|
+
// THE MANDATORY ROLE FIELD (part_of's contract): this file carries the
|
|
747
|
+
// directive and names the part, so it is the library side of the pair.
|
|
748
|
+
partRole: "library",
|
|
749
|
+
isInclude: isPlain,
|
|
750
|
+
isLibInclude: !isPlain,
|
|
751
|
+
// A PATH TARGET, not a symbol: the include names a file. Consumers that
|
|
752
|
+
// resolve names must not read it as one, and the declaration's
|
|
753
|
+
// resolution caveat relies on this marker.
|
|
754
|
+
isFileInclusion: true,
|
|
755
|
+
// The tier follows the construct (ER-D46) — see the doc block above.
|
|
756
|
+
evidenceClass: "import_based"
|
|
757
|
+
},
|
|
758
|
+
line
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
processDefine(node, ctx) {
|
|
762
|
+
const lhs = this.getChildByType(node, "macro_lhs");
|
|
763
|
+
if (!lhs) return;
|
|
764
|
+
const nameNode = this.getChildByType(lhs, "var") ?? this.getChildByType(lhs, "atom");
|
|
765
|
+
if (!nameNode?.text) return;
|
|
766
|
+
const params = this.getChildByType(lhs, "var_args");
|
|
767
|
+
const isFunctionLike = params !== void 0;
|
|
768
|
+
ctx.symbols.push({
|
|
769
|
+
name: nameNode.text,
|
|
770
|
+
kind: isFunctionLike ? ASTEngine.SymbolKind.Macro : ASTEngine.SymbolKind.Constant,
|
|
771
|
+
range: this.nodeToRange(node),
|
|
772
|
+
ownerName: this.ownerNameOf(ctx),
|
|
773
|
+
ownerKind: ASTEngine.SymbolKind.Module,
|
|
774
|
+
metadata: {
|
|
775
|
+
nodeType: "macro_definition",
|
|
776
|
+
isMacro: true,
|
|
777
|
+
isFunctionLike,
|
|
778
|
+
isFunctionLikeMacro: isFunctionLike,
|
|
779
|
+
parameterCount: this.countArgs(params),
|
|
780
|
+
line: nameNode.startPosition.row + 1
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
ctx.declStack.push({ source: nameNode.text, kind: "macro" });
|
|
784
|
+
}
|
|
785
|
+
// ==========================================================================
|
|
786
|
+
// Types
|
|
787
|
+
// ==========================================================================
|
|
788
|
+
processTypeDeclaration(node, ctx, type, filePath) {
|
|
789
|
+
const typeNameNode = this.getChildByType(node, "type_name");
|
|
790
|
+
const nameNode = typeNameNode ? this.getChildByType(typeNameNode, "atom") : this.getChildByType(node, "atom");
|
|
791
|
+
const typeName = nameNode?.text ?? (typeNameNode ? this.stripFirstParen(typeNameNode.text) : void 0);
|
|
792
|
+
if (!typeName) return;
|
|
793
|
+
const params = typeNameNode ? this.getChildByType(typeNameNode, "var_args") : void 0;
|
|
794
|
+
ctx.symbols.push({
|
|
795
|
+
name: typeName,
|
|
796
|
+
kind: ASTEngine.SymbolKind.Type,
|
|
797
|
+
range: this.nodeToRange(node),
|
|
798
|
+
ownerName: this.ownerNameOf(ctx),
|
|
799
|
+
ownerKind: ASTEngine.SymbolKind.Module,
|
|
800
|
+
metadata: {
|
|
801
|
+
nodeType: type,
|
|
802
|
+
isOpaque: type === "opaque",
|
|
803
|
+
isNominal: type === "nominal",
|
|
804
|
+
arity: this.countArgs(params),
|
|
805
|
+
line: (nameNode ?? typeNameNode ?? node).startPosition.row + 1
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
ctx.declStack.push({ source: typeName, kind: "type" });
|
|
809
|
+
this.processTypeReferences(node, typeNameNode, typeName, ctx);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* ER-G3: a behaviour's callback contract is a Method owned by the module.
|
|
813
|
+
* The arity is the type signature's parameter count. A behaviour that also
|
|
814
|
+
* DEFINES its callback (`-callback init/1` plus `init/1 -> …`) yields ONE
|
|
815
|
+
* symbol: both declare the same (name, arity) identity.
|
|
816
|
+
*/
|
|
817
|
+
processCallback(node, ctx, filePath) {
|
|
818
|
+
const nameNode = this.getChildByType(node, "atom");
|
|
819
|
+
if (!nameNode?.text) return;
|
|
820
|
+
const sig = this.getChildByType(node, "type_sig");
|
|
821
|
+
const key = this.fnKey(nameNode.text, this.countArgs(sig ? this.getChildByType(sig, "expr_args") : void 0));
|
|
822
|
+
const existing = ctx.functionSymbolByKey.get(key);
|
|
823
|
+
if (existing) {
|
|
824
|
+
existing.metadata = {
|
|
825
|
+
...existing.metadata ?? {},
|
|
826
|
+
isCallback: true,
|
|
827
|
+
callbackLine: nameNode.startPosition.row + 1
|
|
828
|
+
};
|
|
829
|
+
} else {
|
|
830
|
+
const symbol = {
|
|
831
|
+
name: nameNode.text,
|
|
832
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
833
|
+
range: this.nodeToRange(node),
|
|
834
|
+
ownerName: this.ownerNameOf(ctx),
|
|
835
|
+
ownerKind: ASTEngine.SymbolKind.Module,
|
|
836
|
+
metadata: {
|
|
837
|
+
nodeType: "callback",
|
|
838
|
+
isCallback: true,
|
|
839
|
+
arity: this.countArgs(sig ? this.getChildByType(sig, "expr_args") : void 0),
|
|
840
|
+
line: nameNode.startPosition.row + 1
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
ctx.functionSymbolByKey.set(key, symbol);
|
|
844
|
+
ctx.symbols.push(symbol);
|
|
845
|
+
}
|
|
846
|
+
this.processTypeReferences(node, nameNode, nameNode.text, ctx);
|
|
847
|
+
ctx.declStack.push({ source: nameNode.text, kind: "callback" });
|
|
848
|
+
}
|
|
849
|
+
processSpecTypes(node, ctx) {
|
|
850
|
+
const nameNode = this.getChildByType(node, "atom");
|
|
851
|
+
if (!nameNode?.text) return;
|
|
852
|
+
this.processTypeReferences(node, nameNode, nameNode.text, ctx);
|
|
853
|
+
ctx.declStack.push({ source: nameNode.text, kind: "spec" });
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* A record is the struct analog: one Struct symbol owned by the module, one
|
|
857
|
+
* Property per field owned by the record. Field DEFAULTS are EVALUATIONS and
|
|
858
|
+
* field TYPES are TYPE references, both attributed to the RECORD (ER-D12) —
|
|
859
|
+
* `-record(state, {t = erlang:make_ref()})` calls a function from the record's
|
|
860
|
+
* own declaration, and `-record(r, {f :: pid()})` names a type from it.
|
|
861
|
+
*/
|
|
862
|
+
processRecordDecl(node, ctx, filePath) {
|
|
863
|
+
const nameNode = this.getChildByType(node, "atom");
|
|
864
|
+
if (!nameNode?.text) return;
|
|
865
|
+
const recordName = nameNode.text;
|
|
866
|
+
ctx.declStack.push({ source: recordName, kind: "record" });
|
|
867
|
+
const fields = [];
|
|
868
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
869
|
+
const child = node.namedChild(i);
|
|
870
|
+
if (child?.type !== "record_field") continue;
|
|
871
|
+
const fieldName = this.getChildByType(child, "atom");
|
|
872
|
+
if (!fieldName?.text) continue;
|
|
873
|
+
fields.push(fieldName.text);
|
|
874
|
+
const fieldType = this.getChildByType(child, "field_type");
|
|
875
|
+
const defaultExpr = this.getChildByType(child, "field_expr");
|
|
876
|
+
ctx.symbols.push({
|
|
877
|
+
name: fieldName.text,
|
|
878
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
879
|
+
range: this.nodeToRange(child),
|
|
880
|
+
ownerName: recordName,
|
|
881
|
+
ownerKind: ASTEngine.SymbolKind.Struct,
|
|
882
|
+
metadata: {
|
|
883
|
+
nodeType: "record_field",
|
|
884
|
+
isRecordField: true,
|
|
885
|
+
line: fieldName.startPosition.row + 1,
|
|
886
|
+
...defaultExpr ? { defaultValue: defaultExpr.text } : {},
|
|
887
|
+
...fieldType ? { fieldType: fieldType.text } : {}
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
if (fieldType) this.processTypeReferences(fieldType, void 0, recordName, ctx);
|
|
891
|
+
}
|
|
892
|
+
ctx.symbols.push({
|
|
893
|
+
name: recordName,
|
|
894
|
+
kind: ASTEngine.SymbolKind.Struct,
|
|
895
|
+
range: this.nodeToRange(node),
|
|
896
|
+
ownerName: this.ownerNameOf(ctx),
|
|
897
|
+
ownerKind: ASTEngine.SymbolKind.Module,
|
|
898
|
+
metadata: { nodeType: "record_decl", fields, line: nameNode.startPosition.row + 1 }
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
// ==========================================================================
|
|
902
|
+
// Functions
|
|
903
|
+
// ==========================================================================
|
|
904
|
+
enterFunDecl(node, ctx, filePath) {
|
|
905
|
+
const info = ctx.funDecls.get(node.startIndex);
|
|
906
|
+
if (!info) {
|
|
907
|
+
ctx.declStack.push({ source: ctx.moduleName ?? filePath, kind: "module" });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const name = info.name;
|
|
911
|
+
const arity = info.arity;
|
|
912
|
+
const key = this.fnKey(name, arity);
|
|
913
|
+
const clause = this.collectChildrenByType(node, "function_clause")[0];
|
|
914
|
+
let symbol = ctx.functionSymbolByKey.get(key);
|
|
915
|
+
if (symbol) {
|
|
916
|
+
this.extendRange(symbol, node);
|
|
917
|
+
if (symbol.kind === ASTEngine.SymbolKind.Method && symbol.metadata?.isCallback) {
|
|
918
|
+
symbol.kind = ASTEngine.SymbolKind.Function;
|
|
919
|
+
symbol.metadata = {
|
|
920
|
+
...symbol.metadata ?? {},
|
|
921
|
+
definedHere: true,
|
|
922
|
+
...clause ? { definitionLine: clause.startPosition.row + 1, line: clause.startPosition.row + 1 } : {}
|
|
923
|
+
};
|
|
924
|
+
if (clause) {
|
|
925
|
+
const r = this.nodeToRange(clause);
|
|
926
|
+
symbol.range.startLine = r.startLine;
|
|
927
|
+
symbol.range.startColumn = r.startColumn;
|
|
928
|
+
if (symbol.range.startIndex !== void 0 && r.startIndex !== void 0) {
|
|
929
|
+
symbol.range.startIndex = r.startIndex;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
934
|
+
symbol = this.makeFunctionSymbol(ctx, name, arity, clause, node, info, filePath);
|
|
935
|
+
ctx.functionSymbolByKey.set(key, symbol);
|
|
936
|
+
ctx.symbols.push(symbol);
|
|
937
|
+
}
|
|
938
|
+
ctx.currentFun = { name, key, symbol, branches: 0 };
|
|
939
|
+
const extra = (ctx.clauseCountByKey.get(key) ?? 1) - 1;
|
|
940
|
+
for (let i = 0; i < extra; i++) this.countBranch(ctx);
|
|
941
|
+
ctx.declStack.push({ source: name, kind: "function" });
|
|
942
|
+
}
|
|
943
|
+
extendRange(symbol, node) {
|
|
944
|
+
const r = this.nodeToRange(node);
|
|
945
|
+
symbol.range.endLine = Math.max(symbol.range.endLine, r.endLine);
|
|
946
|
+
symbol.range.endColumn = Math.max(symbol.range.endColumn, r.endColumn);
|
|
947
|
+
if (symbol.range.endIndex !== void 0 && r.endIndex !== void 0) {
|
|
948
|
+
symbol.range.endIndex = Math.max(symbol.range.endIndex, r.endIndex);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* The name of the macro sitting in a remote-module position, or undefined when
|
|
953
|
+
* the module is a plain atom.
|
|
954
|
+
*
|
|
955
|
+
* ER-D25 vs ER-D35. `?MODULE:f(X)` is the idiomatic self-dispatch and resolves
|
|
956
|
+
* to this file's module. `?ANYTHING_ELSE:f(X)` does not: the macro is an
|
|
957
|
+
* ordinary build-time constant whose value this analyzer does not expand, and
|
|
958
|
+
* treating it as `?MODULE` fabricates a self-call. The distinction is the
|
|
959
|
+
* macro's NAME, and the full-corpus draw is what forced it — otp's
|
|
960
|
+
* `file_SUITE.erl` writes `?FILE_MODULE:read_file_info(Priv)`, and before this
|
|
961
|
+
* check every one of those calls was reported as a call into its own module.
|
|
962
|
+
*/
|
|
963
|
+
remoteMacroModuleName(remoteModule) {
|
|
964
|
+
if (!remoteModule) return void 0;
|
|
965
|
+
const macro = this.getChildByType(remoteModule, "macro_call_expr") ?? this.getChildByType(remoteModule, "macro_expr");
|
|
966
|
+
if (!macro) return void 0;
|
|
967
|
+
const nameNode = this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom");
|
|
968
|
+
return nameNode?.text;
|
|
969
|
+
}
|
|
970
|
+
makeFunctionSymbol(ctx, name, arity, clause, declNode, info, filePath) {
|
|
971
|
+
const key = this.fnKey(name, arity);
|
|
972
|
+
const spec = ctx.specs.get(key);
|
|
973
|
+
const params = this.namedChildrenOf(
|
|
974
|
+
clause ? this.getChildByType(clause, "expr_args") : void 0
|
|
975
|
+
).map((c, i) => ({
|
|
976
|
+
// ER-D21: a clause head is a PATTERN, not a declaration. The binding's
|
|
977
|
+
// name is carried when the pattern is a plain variable; the pattern itself
|
|
978
|
+
// is always carried, because that is what the head really says.
|
|
979
|
+
name: c.type === "var" ? c.text : "",
|
|
980
|
+
pattern: c.text,
|
|
981
|
+
...spec?.params[i] ? { type: spec.params[i] } : {}
|
|
982
|
+
}));
|
|
983
|
+
return {
|
|
984
|
+
name,
|
|
985
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
986
|
+
range: this.nodeToRange(declNode),
|
|
987
|
+
ownerName: this.ownerNameOf(ctx),
|
|
988
|
+
ownerKind: ASTEngine.SymbolKind.Module,
|
|
989
|
+
parameters: params,
|
|
990
|
+
...spec?.ret ? { returnType: spec.ret } : {},
|
|
991
|
+
complexity: 1,
|
|
992
|
+
metadata: {
|
|
993
|
+
nodeType: "function_clause",
|
|
994
|
+
arity,
|
|
995
|
+
clauseCount: ctx.clauseCountByKey.get(key) ?? 1,
|
|
996
|
+
...clause ? { line: clause.startPosition.row + 1 } : {},
|
|
997
|
+
...info?.macroHead ? { isMacroHead: true } : {},
|
|
998
|
+
...spec ? { hasSpec: true, specLine: spec.line } : {}
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* ER-D22: `-nifs([f/1])` and `-on_load([g/0])` are ATTRIBUTES about functions,
|
|
1004
|
+
* not declarations. The old code minted a second Function symbol per entry —
|
|
1005
|
+
* and could not have: `attr_name.text` is `-nifs` (the dash is INSIDE the
|
|
1006
|
+
* node), so its `=== "nifs"` test never matched, and the entries are
|
|
1007
|
+
* `binary_op_expr` (`f/1`), not the `fa` the branch then looked for. Two
|
|
1008
|
+
* independent defects hid the same behaviour. The entries now annotate the
|
|
1009
|
+
* declared function, which is the honest surface: a NIF always has an
|
|
1010
|
+
* Erlang-visible stub.
|
|
1011
|
+
*/
|
|
1012
|
+
annotateNativeFunctions(ctx) {
|
|
1013
|
+
const owner = this.ownerNameOf(ctx);
|
|
1014
|
+
for (const [key, line] of ctx.nifEntries) {
|
|
1015
|
+
const sym = ctx.functionSymbolByKey.get(key);
|
|
1016
|
+
if (sym) {
|
|
1017
|
+
sym.metadata = { ...sym.metadata ?? {}, isNif: true };
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
const [name, arityText] = this.splitFnKey(key);
|
|
1021
|
+
const arity = Number(arityText);
|
|
1022
|
+
if (!name || !Number.isFinite(arity)) {
|
|
1023
|
+
ctx.stats.unresolvedNifs++;
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const published = {
|
|
1027
|
+
name,
|
|
1028
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
1029
|
+
range: { startLine: line, startColumn: 1, endLine: line, endColumn: 1 },
|
|
1030
|
+
ownerName: owner,
|
|
1031
|
+
ownerKind: ASTEngine.SymbolKind.Module,
|
|
1032
|
+
metadata: {
|
|
1033
|
+
nodeType: "nif_declaration",
|
|
1034
|
+
arity,
|
|
1035
|
+
clauseCount: 0,
|
|
1036
|
+
isNif: true,
|
|
1037
|
+
declaredByNifsAttribute: true,
|
|
1038
|
+
line
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
ctx.symbols.push(published);
|
|
1042
|
+
ctx.functionSymbolByKey.set(key, published);
|
|
1043
|
+
}
|
|
1044
|
+
for (const [key] of ctx.onLoadEntries) {
|
|
1045
|
+
const sym = ctx.functionSymbolByKey.get(key);
|
|
1046
|
+
if (sym) sym.metadata = { ...sym.metadata ?? {}, isOnLoad: true };
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// ==========================================================================
|
|
1050
|
+
// Calls
|
|
1051
|
+
// ==========================================================================
|
|
1052
|
+
processCall(node, ctx) {
|
|
1053
|
+
const callee = node.namedChild(0);
|
|
1054
|
+
if (!callee) return;
|
|
1055
|
+
if (callee.type === "atom" && SPAWN_BIFS.has(callee.text)) {
|
|
1056
|
+
ctx.patterns["Actor Model (OTP)"] = true;
|
|
1057
|
+
ctx.patterns["Process Spawning"] = true;
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (ctx.typeDepth > 0) return;
|
|
1061
|
+
if (ctx.macroDepth > 0) return;
|
|
1062
|
+
const source = this.currentSource(ctx);
|
|
1063
|
+
if (!source) return;
|
|
1064
|
+
const arity = this.countArgs(this.getChildByType(node, "expr_args"));
|
|
1065
|
+
const remote = this.getChildByType(node, "remote");
|
|
1066
|
+
if (remote) {
|
|
1067
|
+
const remoteModule = this.getChildByType(remote, "remote_module");
|
|
1068
|
+
const modAtom = remoteModule ? this.getChildByType(remoteModule, "atom") : void 0;
|
|
1069
|
+
const macroMod = this.remoteMacroModuleName(remoteModule);
|
|
1070
|
+
const fnAtom = this.getChildByType(remote, "atom");
|
|
1071
|
+
if (!fnAtom?.text) {
|
|
1072
|
+
ctx.stats.dynamicRemote++;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
if (modAtom?.text) {
|
|
1076
|
+
this.pushRel(
|
|
1077
|
+
ctx,
|
|
1078
|
+
source,
|
|
1079
|
+
`${modAtom.text}:${fnAtom.text}`,
|
|
1080
|
+
ASTEngine.RelationshipType.Calls,
|
|
1081
|
+
0.9,
|
|
1082
|
+
{ erlangSpecific: true, context: "remote function call", callArity: arity },
|
|
1083
|
+
fnAtom.startPosition.row + 1
|
|
1084
|
+
);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (macroMod) {
|
|
1088
|
+
const mod = macroMod === "MODULE" ? ctx.moduleName : void 0;
|
|
1089
|
+
if (!mod) {
|
|
1090
|
+
ctx.stats.dynamicRemote++;
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
this.pushRel(
|
|
1094
|
+
ctx,
|
|
1095
|
+
source,
|
|
1096
|
+
`${mod}:${fnAtom.text}`,
|
|
1097
|
+
ASTEngine.RelationshipType.Calls,
|
|
1098
|
+
0.9,
|
|
1099
|
+
{ erlangSpecific: true, context: "remote function call", callArity: arity, macroModule: true },
|
|
1100
|
+
fnAtom.startPosition.row + 1
|
|
1101
|
+
);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
ctx.stats.dynamicRemote++;
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (callee.type !== "atom" || !callee.text) return;
|
|
1108
|
+
if (!ctx.declaredFnKeys.has(this.fnKey(callee.text, arity))) return;
|
|
1109
|
+
this.pushRel(
|
|
1110
|
+
ctx,
|
|
1111
|
+
source,
|
|
1112
|
+
callee.text,
|
|
1113
|
+
ASTEngine.RelationshipType.Calls,
|
|
1114
|
+
0.9,
|
|
1115
|
+
{ erlangSpecific: true, context: "local function call", callArity: arity },
|
|
1116
|
+
callee.startPosition.row + 1
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
// ==========================================================================
|
|
1120
|
+
// Records
|
|
1121
|
+
// ==========================================================================
|
|
1122
|
+
processRecordExpr(node, ctx) {
|
|
1123
|
+
const nameNode = this.getChildByType(node, "record_name");
|
|
1124
|
+
const atom = nameNode ? this.getChildByType(nameNode, "atom") : void 0;
|
|
1125
|
+
if (!atom?.text) return;
|
|
1126
|
+
const source = this.currentSource(ctx);
|
|
1127
|
+
if (!source) return;
|
|
1128
|
+
const line = atom.startPosition.row + 1;
|
|
1129
|
+
if (ctx.typeDepth > 0) {
|
|
1130
|
+
this.pushRel(
|
|
1131
|
+
ctx,
|
|
1132
|
+
source,
|
|
1133
|
+
atom.text,
|
|
1134
|
+
ASTEngine.RelationshipType.References,
|
|
1135
|
+
1,
|
|
1136
|
+
// The context is the generic type-dependency label so the whole family
|
|
1137
|
+
// reads uniformly, and the record-ness is an explicit flag rather than a
|
|
1138
|
+
// second context string: a consumer asking "is this a dependency on a
|
|
1139
|
+
// record?" then has a boolean to read instead of a string to match.
|
|
1140
|
+
{ erlangSpecific: true, context: "type reference", isRecordType: true },
|
|
1141
|
+
line
|
|
1142
|
+
);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (this.isPatternPosition(node)) return;
|
|
1146
|
+
this.pushRel(
|
|
1147
|
+
ctx,
|
|
1148
|
+
source,
|
|
1149
|
+
atom.text,
|
|
1150
|
+
ASTEngine.RelationshipType.Instantiates,
|
|
1151
|
+
1,
|
|
1152
|
+
{ erlangSpecific: true, isRecordExpr: true },
|
|
1153
|
+
line
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* ER-D24 (user finding #1, ER-G4): a member READ is the dominant OTP idiom —
|
|
1158
|
+
* every gen_server state transition reads the state record. Reads and index
|
|
1159
|
+
* reads emit `uses` to the RECORD and to the touched FIELD (java/kotlin
|
|
1160
|
+
* member-read parity, #739/#740). A read in PATTERN position is a match and
|
|
1161
|
+
* emits nothing.
|
|
1162
|
+
*/
|
|
1163
|
+
processRecordRead(node, ctx) {
|
|
1164
|
+
if (ctx.typeDepth > 0 || this.isPatternPosition(node)) return;
|
|
1165
|
+
const source = this.currentSource(ctx);
|
|
1166
|
+
if (!source) return;
|
|
1167
|
+
const recordAtom = this.atomIn(this.getChildByType(node, "record_name"));
|
|
1168
|
+
if (recordAtom) {
|
|
1169
|
+
this.pushRel(
|
|
1170
|
+
ctx,
|
|
1171
|
+
source,
|
|
1172
|
+
recordAtom.text,
|
|
1173
|
+
ASTEngine.RelationshipType.Uses,
|
|
1174
|
+
1,
|
|
1175
|
+
{ erlangSpecific: true, context: "record member read" },
|
|
1176
|
+
recordAtom.startPosition.row + 1
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
const fieldAtom = this.atomIn(this.getChildByType(node, "record_field_name"));
|
|
1180
|
+
if (fieldAtom) {
|
|
1181
|
+
this.pushRel(
|
|
1182
|
+
ctx,
|
|
1183
|
+
source,
|
|
1184
|
+
fieldAtom.text,
|
|
1185
|
+
ASTEngine.RelationshipType.Uses,
|
|
1186
|
+
1,
|
|
1187
|
+
{ erlangSpecific: true, context: "record member read" },
|
|
1188
|
+
fieldAtom.startPosition.row + 1
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* ER-D4: an UPDATE is member mutation of an existing value, never a
|
|
1194
|
+
* construction — so `uses`, never `instantiates` — and it touches the RECORD
|
|
1195
|
+
* plus every ASSIGNED field.
|
|
1196
|
+
*/
|
|
1197
|
+
processRecordUpdate(node, ctx) {
|
|
1198
|
+
if (ctx.typeDepth > 0 || this.isPatternPosition(node)) return;
|
|
1199
|
+
const source = this.currentSource(ctx);
|
|
1200
|
+
if (!source) return;
|
|
1201
|
+
const recordAtom = this.atomIn(this.getChildByType(node, "record_name"));
|
|
1202
|
+
if (recordAtom) {
|
|
1203
|
+
this.pushRel(
|
|
1204
|
+
ctx,
|
|
1205
|
+
source,
|
|
1206
|
+
recordAtom.text,
|
|
1207
|
+
ASTEngine.RelationshipType.Uses,
|
|
1208
|
+
1,
|
|
1209
|
+
{ erlangSpecific: true, context: "record field update" },
|
|
1210
|
+
recordAtom.startPosition.row + 1
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1214
|
+
const child = node.namedChild(i);
|
|
1215
|
+
if (child?.type !== "record_field") continue;
|
|
1216
|
+
const fieldAtom = this.getChildByType(child, "atom");
|
|
1217
|
+
if (!fieldAtom?.text) continue;
|
|
1218
|
+
this.pushRel(
|
|
1219
|
+
ctx,
|
|
1220
|
+
source,
|
|
1221
|
+
fieldAtom.text,
|
|
1222
|
+
ASTEngine.RelationshipType.Uses,
|
|
1223
|
+
1,
|
|
1224
|
+
{ erlangSpecific: true, context: "record field update" },
|
|
1225
|
+
fieldAtom.startPosition.row + 1
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
// ==========================================================================
|
|
1230
|
+
// Macros and fun values
|
|
1231
|
+
// ==========================================================================
|
|
1232
|
+
/**
|
|
1233
|
+
* ER-D9 (user finding #2, ER-G1): every `?MACRO` use is a reference to the
|
|
1234
|
+
* macro's Constant symbol, per site, whether or not the macro is defined in
|
|
1235
|
+
* this file. A use of a header-defined macro lands as an EXTERNAL target,
|
|
1236
|
+
* which is the honest answer — the declaration lives in another file, and the
|
|
1237
|
+
* fixture erlang/refs-er2 pins exactly that.
|
|
1238
|
+
*
|
|
1239
|
+
* The name is the `var` child when the macro takes no arguments and the `atom`
|
|
1240
|
+
* child when it does (`?FOO(A)` → atom + macro_call_args). Reading only the
|
|
1241
|
+
* `var` would drop every function-like macro use (measured: 2,691 of the 6,612
|
|
1242
|
+
* macro_call_expr sites in 300 corpus files carry a macro_call_args).
|
|
1243
|
+
*/
|
|
1244
|
+
processMacroUse(node, ctx) {
|
|
1245
|
+
if (node.parent?.type === "module_attribute") return;
|
|
1246
|
+
const nameNode = this.getChildByType(node, "var") ?? this.getChildByType(node, "atom");
|
|
1247
|
+
if (!nameNode?.text) return;
|
|
1248
|
+
const source = this.currentSource(ctx);
|
|
1249
|
+
if (!source) return;
|
|
1250
|
+
this.pushRel(
|
|
1251
|
+
ctx,
|
|
1252
|
+
source,
|
|
1253
|
+
nameNode.text,
|
|
1254
|
+
ASTEngine.RelationshipType.References,
|
|
1255
|
+
1,
|
|
1256
|
+
{ erlangSpecific: true, context: "macro use" },
|
|
1257
|
+
nameNode.startPosition.row + 1
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* ER-D10 (user finding #4, ER-G2): `fun f/1` and `fun lists:map/2` name a
|
|
1262
|
+
* function VALUE. Taking a reference is not calling it, so these are
|
|
1263
|
+
* `references`, never `calls` (the fixture forbids a calls edge at the same
|
|
1264
|
+
* site). A qualified reference keeps its module (ER-D1); `fun ?MODULE:g/0`
|
|
1265
|
+
* expands to this module exactly like the remote-call rule.
|
|
1266
|
+
*/
|
|
1267
|
+
processFunReference(node, ctx, external) {
|
|
1268
|
+
if (ctx.typeDepth > 0 || ctx.macroDepth > 0) return;
|
|
1269
|
+
const source = this.currentSource(ctx);
|
|
1270
|
+
if (!source) return;
|
|
1271
|
+
const arityNode = this.getChildByType(node, "arity");
|
|
1272
|
+
const arity = arityNode ? this.digitsOf(this.getChildByType(arityNode, "integer")?.text) : 0;
|
|
1273
|
+
if (!external) {
|
|
1274
|
+
const nameAtom = this.getChildByType(node, "atom");
|
|
1275
|
+
if (!nameAtom?.text) return;
|
|
1276
|
+
this.pushRel(
|
|
1277
|
+
ctx,
|
|
1278
|
+
source,
|
|
1279
|
+
nameAtom.text,
|
|
1280
|
+
ASTEngine.RelationshipType.References,
|
|
1281
|
+
0.9,
|
|
1282
|
+
{ erlangSpecific: true, context: "fun value reference", callArity: arity },
|
|
1283
|
+
nameAtom.startPosition.row + 1
|
|
1284
|
+
);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const fnAtom = this.getChildByType(node, "atom");
|
|
1288
|
+
if (!fnAtom?.text) return;
|
|
1289
|
+
const modNode = this.getChildByType(node, "module");
|
|
1290
|
+
const modAtom = modNode ? this.getChildByType(modNode, "atom") : void 0;
|
|
1291
|
+
const macroMod = this.remoteMacroModuleName(modNode);
|
|
1292
|
+
if (modAtom?.text) {
|
|
1293
|
+
this.pushRel(
|
|
1294
|
+
ctx,
|
|
1295
|
+
source,
|
|
1296
|
+
`${modAtom.text}:${fnAtom.text}`,
|
|
1297
|
+
ASTEngine.RelationshipType.References,
|
|
1298
|
+
0.9,
|
|
1299
|
+
{ erlangSpecific: true, context: "fun value reference", callArity: arity },
|
|
1300
|
+
fnAtom.startPosition.row + 1
|
|
1301
|
+
);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (macroMod === "MODULE" && ctx.moduleName) {
|
|
1305
|
+
this.pushRel(
|
|
1306
|
+
ctx,
|
|
1307
|
+
source,
|
|
1308
|
+
`${ctx.moduleName}:${fnAtom.text}`,
|
|
1309
|
+
ASTEngine.RelationshipType.References,
|
|
1310
|
+
0.9,
|
|
1311
|
+
{ erlangSpecific: true, context: "fun value reference", callArity: arity, macroModule: true },
|
|
1312
|
+
fnAtom.startPosition.row + 1
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
// ==========================================================================
|
|
1317
|
+
// Operators
|
|
1318
|
+
// ==========================================================================
|
|
1319
|
+
processBinaryOp(node, ctx) {
|
|
1320
|
+
const op = node.child(1)?.text;
|
|
1321
|
+
if (op === "!") {
|
|
1322
|
+
ctx.patterns["Message Passing"] = true;
|
|
1323
|
+
ctx.patterns["Actor Model (OTP)"] = true;
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if ((op === "andalso" || op === "orelse") && ctx.macroDepth === 0) {
|
|
1327
|
+
ctx.patterns["Pattern Matching"] = true;
|
|
1328
|
+
this.countBranch(ctx);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
countBranch(ctx) {
|
|
1332
|
+
if (ctx.macroDepth > 0) return;
|
|
1333
|
+
ctx.totalBranches++;
|
|
1334
|
+
if (ctx.currentFun) ctx.currentFun.branches++;
|
|
1335
|
+
}
|
|
1336
|
+
// ==========================================================================
|
|
1337
|
+
// Type references (the type graph)
|
|
1338
|
+
// ==========================================================================
|
|
1339
|
+
/**
|
|
1340
|
+
* ER-D10 (user finding #6, ER-G5's type half): a declared type's BODY names
|
|
1341
|
+
* other types, and that dependency graph is the whole value of reading a
|
|
1342
|
+
* `-type`. Inside a type context every `atom` and every applied `call` head is
|
|
1343
|
+
* a type name; `var` is a type parameter and `string` is a literal, so both are
|
|
1344
|
+
* silent.
|
|
1345
|
+
*
|
|
1346
|
+
* The declared type's OWN `type_name` is skipped by SPAN CONTAINMENT
|
|
1347
|
+
* (startIndex/endIndex), never by object identity: web-tree-sitter hands out a
|
|
1348
|
+
* FRESH wrapper per access, so `child !== skip` never matches and the type
|
|
1349
|
+
* would reference itself. That was a real defect the M14 hygiene gate caught
|
|
1350
|
+
* (KAD2/TSD2 class); the mirrored copy in the oracle was invisible because
|
|
1351
|
+
* both sides agreed.
|
|
1352
|
+
*
|
|
1353
|
+
* A self-reference that survives this filter is LEGAL Erlang (a recursive
|
|
1354
|
+
* type, `-type t() :: {t, integer()}`) and is emitted — the span filter removes
|
|
1355
|
+
* the declaration's name, not a genuine recursive dependency.
|
|
1356
|
+
*/
|
|
1357
|
+
processTypeReferences(declNode, skipSpan, declName, ctx) {
|
|
1358
|
+
const stack = [declNode];
|
|
1359
|
+
while (stack.length > 0) {
|
|
1360
|
+
const node = stack.pop();
|
|
1361
|
+
if (node.isNamed && node.type === "record_name") continue;
|
|
1362
|
+
if (node.isNamed && (node.type === "remote" || node.type === "remote_module")) continue;
|
|
1363
|
+
if (node.isNamed && node.type === "call") {
|
|
1364
|
+
let name;
|
|
1365
|
+
let nameNode;
|
|
1366
|
+
if (node.type === "call") {
|
|
1367
|
+
const remote = this.getChildByType(node, "remote");
|
|
1368
|
+
if (remote) {
|
|
1369
|
+
const remoteModule = this.getChildByType(remote, "remote_module");
|
|
1370
|
+
const modAtom = remoteModule ? this.getChildByType(remoteModule, "atom") : void 0;
|
|
1371
|
+
const fnAtom = this.getChildByType(remote, "atom");
|
|
1372
|
+
if (modAtom?.text && fnAtom?.text) {
|
|
1373
|
+
name = `${modAtom.text}:${fnAtom.text}`;
|
|
1374
|
+
nameNode = fnAtom;
|
|
1375
|
+
}
|
|
1376
|
+
} else {
|
|
1377
|
+
nameNode = node.namedChild(0) ?? void 0;
|
|
1378
|
+
name = nameNode?.text;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
const inDeclarationName = skipSpan !== void 0 && nameNode !== void 0 && this.spanContains(skipSpan, nameNode);
|
|
1382
|
+
if (name && !inDeclarationName) {
|
|
1383
|
+
this.pushRel(
|
|
1384
|
+
ctx,
|
|
1385
|
+
declName,
|
|
1386
|
+
name,
|
|
1387
|
+
ASTEngine.RelationshipType.References,
|
|
1388
|
+
0.9,
|
|
1389
|
+
{ erlangSpecific: true, context: "type reference" },
|
|
1390
|
+
(nameNode ?? node).startPosition.row + 1
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
for (let i = node.childCount - 1; i >= 0; i--) {
|
|
1395
|
+
const child = node.child(i);
|
|
1396
|
+
if (child) stack.push(child);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
// ==========================================================================
|
|
1401
|
+
// Ownership
|
|
1402
|
+
// ==========================================================================
|
|
1403
|
+
/**
|
|
1404
|
+
* `contains` for every owned symbol: module -> its functions, methods, types,
|
|
1405
|
+
* records and macros; record -> its fields. The line is the member's own
|
|
1406
|
+
* declaration line, which is where bb_graph's call hierarchy and the M59
|
|
1407
|
+
* symbol-line contract read it from.
|
|
1408
|
+
*/
|
|
1409
|
+
/**
|
|
1410
|
+
* The owner of a module-scope declaration.
|
|
1411
|
+
*
|
|
1412
|
+
* Erlang has an honest gap here: a source file may declare NO `-module` at
|
|
1413
|
+
* all. OTP's own `lib/syntax_tools/examples/merl/basic_test.erl` is one — its
|
|
1414
|
+
* `-module` line is commented out because the file is textually included into
|
|
1415
|
+
* another module. Nothing in the file names the module it will end up in, and
|
|
1416
|
+
* the analyzer cannot know it. So the owner falls back to the FILE PATH: the
|
|
1417
|
+
* outermost container that definitely holds the declaration, and the only
|
|
1418
|
+
* identity the file has. (The alternative — inventing a module name from the
|
|
1419
|
+
* filename — would fabricate a module that does not exist.)
|
|
1420
|
+
*
|
|
1421
|
+
* Every module-scope declaration must route through here. The 400-file corpus
|
|
1422
|
+
* draw caught the one site that did not: macros took bare `ctx.moduleName`
|
|
1423
|
+
* while functions, records, types and callbacks took the fallback, so a
|
|
1424
|
+
* macro in a module-less file was emitted with NO owner while the function
|
|
1425
|
+
* beside it had one. A fallback that is duplicated per declaration is a
|
|
1426
|
+
* fallback that will drift.
|
|
1427
|
+
*/
|
|
1428
|
+
ownerNameOf(ctx) {
|
|
1429
|
+
return ctx.moduleName ?? ctx.filePath;
|
|
1430
|
+
}
|
|
1431
|
+
emitOwnership(ctx) {
|
|
1432
|
+
for (const s of ctx.symbols) {
|
|
1433
|
+
const owner = s.ownerName;
|
|
1434
|
+
if (!owner) continue;
|
|
1435
|
+
const nameLine = s.metadata?.line;
|
|
1436
|
+
const line = typeof nameLine === "number" && nameLine > 0 ? nameLine : s.range.startLine;
|
|
1437
|
+
this.pushRel(
|
|
1438
|
+
ctx,
|
|
1439
|
+
owner,
|
|
1440
|
+
s.name,
|
|
1441
|
+
ASTEngine.RelationshipType.Contains,
|
|
1442
|
+
1,
|
|
1443
|
+
{ erlangSpecific: true, isModuleMember: s.ownerKind === ASTEngine.SymbolKind.Module },
|
|
1444
|
+
line
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
// ==========================================================================
|
|
1449
|
+
// Relationship emission
|
|
1450
|
+
// ==========================================================================
|
|
1451
|
+
/**
|
|
1452
|
+
* One edge per (source, type, target, line, callArity).
|
|
1453
|
+
*
|
|
1454
|
+
* ER-D2: call-site arity is part of call identity — `pick(1)` and `pick(1,2)`
|
|
1455
|
+
* are different callees and must not collapse into one row.
|
|
1456
|
+
* M59 (#825): the line is part of identity for per-SITE edges — `run/0` calling
|
|
1457
|
+
* `target/0` on four lines is four call sites, and the fixture pins all four.
|
|
1458
|
+
* The line also makes the ancestor-wins rule deterministic: in
|
|
1459
|
+
* `S#state{count = S#state.count + 1}` the update and the inner read share
|
|
1460
|
+
* (source, target, line), and because the outer node is visited first its
|
|
1461
|
+
* "record field update" edge is the one that survives.
|
|
1462
|
+
*
|
|
1463
|
+
* ER-D43 (measured 2026-10-03): the PUBLISHED IDENTITY is part of identity too. An
|
|
1464
|
+
* `exports` edge publishes a module's interface, and the interface is keyed by
|
|
1465
|
+
* `name/arity` (ER-D2), so `-export([start/0, start/1, complete/1, complete/0])` on
|
|
1466
|
+
* ONE line is four publications. The key carried `callArity` — which an `exports` edge
|
|
1467
|
+
* does not set — and `line`, which is the same line for all four, so the second
|
|
1468
|
+
* publication of each name was dropped and the module's interface was published at half
|
|
1469
|
+
* its real size with no error and no counter. 340 sites in a 204-file corpus draw;
|
|
1470
|
+
* `otp/lib/asn1/examples/test_driver.erl:10` and
|
|
1471
|
+
* `otp/lib/common_test/src/ct_doctest.erl:318` are two of them. `metadata.arity` is set
|
|
1472
|
+
* on the `exports` edge and on nothing else, so reading it here widens the key for that
|
|
1473
|
+
* one label and changes no other.
|
|
1474
|
+
*/
|
|
1475
|
+
pushRel(ctx, source, target, type, confidence, metadata = {}, line) {
|
|
1476
|
+
const publishedIdentity = metadata.callArity ?? metadata.arity ?? "";
|
|
1477
|
+
const key = `${source}|${type}|${target}|${line ?? ""}|${publishedIdentity}`;
|
|
1478
|
+
if (ctx.seenRels.has(key)) return;
|
|
1479
|
+
ctx.seenRels.add(key);
|
|
1480
|
+
ctx.relationships.push({
|
|
1481
|
+
source,
|
|
1482
|
+
target,
|
|
1483
|
+
type,
|
|
1484
|
+
confidence,
|
|
1485
|
+
metadata,
|
|
1486
|
+
...typeof line === "number" && line > 0 ? { line_number: line } : {}
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
// ==========================================================================
|
|
1490
|
+
// Context helpers
|
|
1491
|
+
// ==========================================================================
|
|
1492
|
+
/** The symbol every edge inside the current declaration belongs to. */
|
|
1493
|
+
currentSource(ctx) {
|
|
1494
|
+
const top = ctx.declStack[ctx.declStack.length - 1];
|
|
1495
|
+
return top?.source ?? this.ownerNameOf(ctx);
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Is this node in PATTERN position — a clause head, a `case`/`receive`/`try`
|
|
1499
|
+
* arm pattern, the left side of a `=` / `?=` match, or a comprehension
|
|
1500
|
+
* generator's left side?
|
|
1501
|
+
*
|
|
1502
|
+
* A pattern is a MATCH, not an evaluation: `S#state.count` in a head is
|
|
1503
|
+
* destructuring, while `State#state{…}` in a body is a real update. The rule
|
|
1504
|
+
* climbs to the nearest boundary. A CLAUSE HEAD makes everything below it a
|
|
1505
|
+
* pattern (`case X of Y = #state{} -> …` binds, it does not construct). An
|
|
1506
|
+
* EVALUATION context (clause body, guard) makes only the pattern side of a
|
|
1507
|
+
* match or generator a pattern.
|
|
1508
|
+
*/
|
|
1509
|
+
/**
|
|
1510
|
+
* Is this node in PATTERN position?
|
|
1511
|
+
*
|
|
1512
|
+
* A pattern is a MATCH, not an evaluation. The rule is "the NEAREST enclosing
|
|
1513
|
+
* boundary decides", which is the only formulation that survives Erlang's
|
|
1514
|
+
* nesting — three corpus shapes decided it, and each one is a case a
|
|
1515
|
+
* cumulative flag gets wrong:
|
|
1516
|
+
*
|
|
1517
|
+
* `D#r.f == []` in a GUARD -> evaluation (a guard computes).
|
|
1518
|
+
* Climbing to the clause head would call it a pattern; the boundary is the
|
|
1519
|
+
* guard, and a guard evaluates.
|
|
1520
|
+
* `[C || #cert{der = C} <- Certs]` -> pattern. The generator's left
|
|
1521
|
+
* side binds, whatever expression holds the comprehension. The nearest
|
|
1522
|
+
* boundary is the generator and the node is on its `<-` side, so the
|
|
1523
|
+
* enclosing `= unzip4(…)` match is never consulted.
|
|
1524
|
+
* `Y = #r{} -> …` in a case arm -> pattern. The node is on the
|
|
1525
|
+
* match's VALUE side, but the match itself sits in an arm head, so the
|
|
1526
|
+
* rule re-asks the boundary's own context.
|
|
1527
|
+
* `X = #r{}` in a clause body -> evaluation.
|
|
1528
|
+
*/
|
|
1529
|
+
isPatternPosition(node) {
|
|
1530
|
+
let probe = node;
|
|
1531
|
+
for (; ; ) {
|
|
1532
|
+
const boundary = this.nearestPatternBoundary(probe.parent);
|
|
1533
|
+
if (!boundary) return false;
|
|
1534
|
+
if (boundary.kind === "head") return true;
|
|
1535
|
+
if (boundary.kind === "body" || boundary.kind === "guard") return false;
|
|
1536
|
+
if (this.isOnPatternSide(node, boundary.node)) return true;
|
|
1537
|
+
probe = boundary.node;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
nearestPatternBoundary(from) {
|
|
1541
|
+
let cur = from;
|
|
1542
|
+
while (cur) {
|
|
1543
|
+
const t = cur.type;
|
|
1544
|
+
if (t === "function_clause" || t === "fun_clause" || t === "cr_clause") return { node: cur, kind: "head" };
|
|
1545
|
+
if (t === "clause_body") return { node: cur, kind: "body" };
|
|
1546
|
+
if (t === "guard" || t === "guard_clause") return { node: cur, kind: "guard" };
|
|
1547
|
+
if (t === "match_expr" || t === "cond_match_expr" || t === "lc_or_zc_expr") return { node: cur, kind: "match" };
|
|
1548
|
+
cur = cur.parent;
|
|
1549
|
+
}
|
|
1550
|
+
return void 0;
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Does `node` sit left of the boundary operator of `owner`?
|
|
1554
|
+
*
|
|
1555
|
+
* The `<-` of a comprehension generator is NOT a direct child of the
|
|
1556
|
+
* `lc_or_zc_expr` — the grammar wraps it in a `generator` node (measured shape:
|
|
1557
|
+
* `lc_or_zc_expr → generator → {LHS, <-, RHS}`). Both this analyzer AND the
|
|
1558
|
+
* independent oracle first missed that and agreed with each other on the wrong
|
|
1559
|
+
* answer, which is precisely the case a mirror cannot see: only the authored
|
|
1560
|
+
* fixture `erlang/patterns-er3` catches it. The `generator` / `map_generator` /
|
|
1561
|
+
* `b_generator` wrapper is resolved here.
|
|
1562
|
+
*
|
|
1563
|
+
* A `lc_or_zc_expr` with NO generator is a comprehension FILTER, which
|
|
1564
|
+
* computes — no operator, so the answer is false.
|
|
1565
|
+
*/
|
|
1566
|
+
isOnPatternSide(node, owner) {
|
|
1567
|
+
let target = owner;
|
|
1568
|
+
if (owner.type === "lc_or_zc_expr") {
|
|
1569
|
+
const gen = this.getChildByType(owner, "generator") ?? this.getChildByType(owner, "map_generator") ?? this.getChildByType(owner, "b_generator");
|
|
1570
|
+
if (gen) target = gen;
|
|
1571
|
+
}
|
|
1572
|
+
for (let i = 0; i < target.childCount; i++) {
|
|
1573
|
+
const c = target.child(i);
|
|
1574
|
+
if (c && MATCH_OPERATORS.has(c.type)) return node.startIndex < c.startIndex;
|
|
1575
|
+
}
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
/** Span containment — the only correct way to compare two tree-sitter nodes. */
|
|
1579
|
+
spanContains(outer, inner) {
|
|
1580
|
+
return outer.startIndex <= inner.startIndex && inner.endIndex <= outer.endIndex;
|
|
1581
|
+
}
|
|
1582
|
+
// ==========================================================================
|
|
1583
|
+
// Node readers
|
|
1584
|
+
// ==========================================================================
|
|
1585
|
+
/**
|
|
1586
|
+
* `name/arity` from an `fa`, with the NAME's line (M59).
|
|
1587
|
+
*
|
|
1588
|
+
* An `fa`'s name has THREE spellings in this grammar and a reader that takes only the
|
|
1589
|
+
* first loses rows SILENTLY — no error, no counter, and the label simply has fewer
|
|
1590
|
+
* members than the file publishes:
|
|
1591
|
+
*
|
|
1592
|
+
* atom `-export([run/0])`
|
|
1593
|
+
* macro_call_expr `-export([?MODULE/0])` — ER-D28: how OTP test suites export the
|
|
1594
|
+
* function their `?MODULE() -> …` clause declares. `?MODULE`
|
|
1595
|
+
* expands to the module atom, so the entry resolves exactly like
|
|
1596
|
+
* the clause head does. Any other macro in an `fa` needs expansion
|
|
1597
|
+
* and is refused.
|
|
1598
|
+
* var `-export([HEAD/1])` — ER-D42, measured 2026-10-03. An
|
|
1599
|
+
* identifier beginning with an UPPERCASE letter is a VARIABLE in
|
|
1600
|
+
* Erlang, so the grammar types it `var`, not `atom`, and this
|
|
1601
|
+
* reader required `atom`. Every all-caps export name was therefore
|
|
1602
|
+
* dropped: `-export([HEAD/1])` in the lane's own
|
|
1603
|
+
* `macros-er3.erl` fixture produced no `exports` row at all, and the
|
|
1604
|
+
* fixture did not catch it because nothing pinned the row. A macro
|
|
1605
|
+
* is a real Erlang function name here — `-define(HEAD(X), X).` plus
|
|
1606
|
+
* `-export([HEAD/1])` is ordinary code — so the name is read and the
|
|
1607
|
+
* arity with it.
|
|
1608
|
+
*/
|
|
1609
|
+
readFa(fa, moduleName) {
|
|
1610
|
+
const nameNode = this.getChildByType(fa, "atom");
|
|
1611
|
+
const arityNode = this.getChildByType(fa, "arity");
|
|
1612
|
+
const arity = arityNode ? this.digitsOf(this.getChildByType(arityNode, "integer")?.text) : 0;
|
|
1613
|
+
if (nameNode?.text) return { name: nameNode.text, arity, line: nameNode.startPosition.row + 1 };
|
|
1614
|
+
const macro = this.getChildByType(fa, "macro_call_expr");
|
|
1615
|
+
const macroName = macro ? (this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom"))?.text : void 0;
|
|
1616
|
+
if (macroName === "MODULE" && moduleName) {
|
|
1617
|
+
const at = this.getChildByType(macro, "var") ?? this.getChildByType(macro, "atom");
|
|
1618
|
+
return { name: moduleName, arity, line: (at ?? macro).startPosition.row + 1 };
|
|
1619
|
+
}
|
|
1620
|
+
const varNode = this.getChildByType(fa, "var");
|
|
1621
|
+
if (varNode?.text) return { name: varNode.text, arity, line: varNode.startPosition.row + 1 };
|
|
1622
|
+
return void 0;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* `-nifs([f/1])` / `-on_load([g/0])` entries. The list holds
|
|
1626
|
+
* `binary_op_expr` (`f` `/` `1`), NOT an `fa` — the second half of the
|
|
1627
|
+
* dead-branch defect ER-D22 records. Both spellings are read, because the
|
|
1628
|
+
* grammar's own `-record`-in-`wild_attribute` error path produces `fa` nodes
|
|
1629
|
+
* for some vendor attributes.
|
|
1630
|
+
*/
|
|
1631
|
+
collectAttributeFaEntries(node, out, ctx) {
|
|
1632
|
+
const line = node.startPosition.row + 1;
|
|
1633
|
+
const stack = [node];
|
|
1634
|
+
while (stack.length > 0) {
|
|
1635
|
+
const n = stack.pop();
|
|
1636
|
+
if (n.isNamed && n.type === "binary_op_expr" && n.child(1)?.type === "/") {
|
|
1637
|
+
const nameNode = this.getChildByType(n, "atom");
|
|
1638
|
+
if (nameNode?.text) {
|
|
1639
|
+
out.set(this.fnKey(nameNode.text, this.digitsOf(this.getChildByType(n, "integer")?.text)), line);
|
|
1640
|
+
}
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
if (n.isNamed && n.type === "fa") {
|
|
1644
|
+
const fa = this.readFa(n, ctx.moduleName);
|
|
1645
|
+
if (fa) out.set(this.fnKey(fa.name, fa.arity), line);
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
for (let i = n.childCount - 1; i >= 0; i--) {
|
|
1649
|
+
const c = n.child(i);
|
|
1650
|
+
if (c) stack.push(c);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
/** `-nifs`, not `nifs`: the dash is INSIDE the `attr_name` node. */
|
|
1655
|
+
attrNameText(node) {
|
|
1656
|
+
const attrName = this.getChildByType(node, "attr_name");
|
|
1657
|
+
if (!attrName) return void 0;
|
|
1658
|
+
return this.getChildByType(attrName, "atom")?.text ?? attrName.text;
|
|
1659
|
+
}
|
|
1660
|
+
atomIn(node) {
|
|
1661
|
+
return node ? this.getChildByType(node, "atom") : void 0;
|
|
1662
|
+
}
|
|
1663
|
+
namedChildrenOf(args) {
|
|
1664
|
+
if (!args) return [];
|
|
1665
|
+
const out = [];
|
|
1666
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1667
|
+
const c = args.namedChild(i);
|
|
1668
|
+
if (c && c.type !== "comment") out.push(c);
|
|
1669
|
+
}
|
|
1670
|
+
return out;
|
|
1671
|
+
}
|
|
1672
|
+
getChildByType(node, targetType) {
|
|
1673
|
+
if (!node) return void 0;
|
|
1674
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1675
|
+
const child = node.child(i);
|
|
1676
|
+
if (child?.type === targetType) return child;
|
|
1677
|
+
}
|
|
1678
|
+
return void 0;
|
|
1679
|
+
}
|
|
1680
|
+
collectChildrenByType(node, targetType) {
|
|
1681
|
+
const results = [];
|
|
1682
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1683
|
+
const child = node.namedChild(i);
|
|
1684
|
+
if (child && child.type === targetType) results.push(child);
|
|
1685
|
+
}
|
|
1686
|
+
return results;
|
|
1687
|
+
}
|
|
1688
|
+
digitsOf(text) {
|
|
1689
|
+
if (!text) return 0;
|
|
1690
|
+
const digits = this.keepDigits(text);
|
|
1691
|
+
return digits.length > 0 ? parseInt(digits, 10) || 0 : 0;
|
|
1692
|
+
}
|
|
1693
|
+
// ==========================================================================
|
|
1694
|
+
// Pure-string helpers (no regex — M12 census, M49 #849)
|
|
1695
|
+
// ==========================================================================
|
|
1696
|
+
stripQuotes(text) {
|
|
1697
|
+
let out = "";
|
|
1698
|
+
for (let i = 0; i < text.length; i++) {
|
|
1699
|
+
const ch = text[i];
|
|
1700
|
+
if (ch === '"' || ch === "'") continue;
|
|
1701
|
+
out += ch;
|
|
1702
|
+
}
|
|
1703
|
+
return out;
|
|
1704
|
+
}
|
|
1705
|
+
keepDigits(text) {
|
|
1706
|
+
let out = "";
|
|
1707
|
+
for (let i = 0; i < text.length; i++) {
|
|
1708
|
+
const ch = text[i];
|
|
1709
|
+
if (ch >= "0" && ch <= "9") out += ch;
|
|
1710
|
+
}
|
|
1711
|
+
return out;
|
|
1712
|
+
}
|
|
1713
|
+
/** `name()` -> `name`; `maybe(T)` -> `maybe`. */
|
|
1714
|
+
stripFirstParen(text) {
|
|
1715
|
+
const paren = text.indexOf("(");
|
|
1716
|
+
return (paren >= 0 ? text.slice(0, paren) : text).trim();
|
|
1717
|
+
}
|
|
1718
|
+
nodeToRange(node) {
|
|
1719
|
+
return {
|
|
1720
|
+
startLine: node.startPosition.row + 1,
|
|
1721
|
+
startColumn: node.startPosition.column + 1,
|
|
1722
|
+
endLine: node.endPosition.row + 1,
|
|
1723
|
+
endColumn: node.endPosition.column + 1,
|
|
1724
|
+
startIndex: node.startIndex,
|
|
1725
|
+
endIndex: node.endIndex
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
export {
|
|
1730
|
+
RealErlangAnalyzer
|
|
1731
|
+
};
|