@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,2533 @@
|
|
|
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 {
|
|
8
|
+
BINDING_SEP
|
|
9
|
+
} from "./chunk-KAET47ZL.js";
|
|
10
|
+
import "./chunk-KCDECRJY.js";
|
|
11
|
+
import "./chunk-MLKGABMK.js";
|
|
12
|
+
|
|
13
|
+
// src/ast/real-haskell-analyzer.ts
|
|
14
|
+
var HASKELL_DECLARATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
15
|
+
"type",
|
|
16
|
+
"data",
|
|
17
|
+
"newtype",
|
|
18
|
+
"class",
|
|
19
|
+
"instance",
|
|
20
|
+
"family",
|
|
21
|
+
"pattern",
|
|
22
|
+
"proc",
|
|
23
|
+
"default",
|
|
24
|
+
"module"
|
|
25
|
+
]);
|
|
26
|
+
var HASKELL_TYPE_BEARING_NODES = /* @__PURE__ */ new Set([
|
|
27
|
+
"type",
|
|
28
|
+
"signature",
|
|
29
|
+
"type_synomym",
|
|
30
|
+
"type_synonym",
|
|
31
|
+
"data_type",
|
|
32
|
+
"newtype",
|
|
33
|
+
"type_patterns",
|
|
34
|
+
"type_family",
|
|
35
|
+
"type_instance",
|
|
36
|
+
"deriving",
|
|
37
|
+
"deriving_instance",
|
|
38
|
+
"class",
|
|
39
|
+
"instance",
|
|
40
|
+
"strict_field",
|
|
41
|
+
"field_pattern",
|
|
42
|
+
"forall",
|
|
43
|
+
"quantified_variables",
|
|
44
|
+
"promoted",
|
|
45
|
+
"type_params",
|
|
46
|
+
"constraint",
|
|
47
|
+
"context",
|
|
48
|
+
"deriving_strategy",
|
|
49
|
+
"via",
|
|
50
|
+
"kind_signature",
|
|
51
|
+
"arity",
|
|
52
|
+
"kind",
|
|
53
|
+
"children"
|
|
54
|
+
]);
|
|
55
|
+
var RealHaskellAnalyzer = class {
|
|
56
|
+
analyze(rootNode, content, filePath) {
|
|
57
|
+
const ctx = {
|
|
58
|
+
symbols: [],
|
|
59
|
+
relationships: [],
|
|
60
|
+
imports: [],
|
|
61
|
+
exports: [],
|
|
62
|
+
errors: [],
|
|
63
|
+
patterns: {},
|
|
64
|
+
frameworks: {},
|
|
65
|
+
typeSignatures: {},
|
|
66
|
+
functionSymbolIndex: {},
|
|
67
|
+
declaredFunctions: /* @__PURE__ */ new Set(),
|
|
68
|
+
declaredTypes: /* @__PURE__ */ new Set(),
|
|
69
|
+
declaredConstructors: /* @__PURE__ */ new Set(),
|
|
70
|
+
importAliases: /* @__PURE__ */ new Map(),
|
|
71
|
+
annPragmas: 0,
|
|
72
|
+
classMethods: /* @__PURE__ */ new Map(),
|
|
73
|
+
localScopes: { callableScoped: /* @__PURE__ */ new Map(), blockLocals: /* @__PURE__ */ new Map(), frameLocals: /* @__PURE__ */ new Map() },
|
|
74
|
+
// A file with no `module` header is module `Main` — the language rule.
|
|
75
|
+
// Defaulted HERE, before the walk, because the import edge is pushed during
|
|
76
|
+
// it and read `currentModule || ""`; defaulting in the post-loop was too
|
|
77
|
+
// late and still produced `imports -> Distribution.Simple` with no source.
|
|
78
|
+
currentModule: "Main",
|
|
79
|
+
currentFunction: void 0,
|
|
80
|
+
seenRels: /* @__PURE__ */ new Set()
|
|
81
|
+
};
|
|
82
|
+
const scopeStack = [];
|
|
83
|
+
const stack = [{ node: rootNode }];
|
|
84
|
+
let cyclomaticComplexity = 1;
|
|
85
|
+
this.collectDeclaredFunctions(rootNode, ctx.declaredFunctions);
|
|
86
|
+
this.collectDeclaredTypes(rootNode, ctx.declaredTypes);
|
|
87
|
+
this.collectDeclaredConstructors(rootNode, ctx.declaredConstructors);
|
|
88
|
+
this.collectClassMethods(rootNode, ctx.classMethods);
|
|
89
|
+
this.collectLocalScopes(rootNode, ctx.localScopes);
|
|
90
|
+
while (stack.length > 0) {
|
|
91
|
+
const { node, isExit, restoreFn } = stack.pop();
|
|
92
|
+
if (isExit) {
|
|
93
|
+
if (restoreFn !== void 0) ctx.currentFunction = restoreFn;
|
|
94
|
+
else if (node.type === "function" || node.type === "bind" || node.type === "class" || node.type === "instance" || node.type === "data" || node.type === "newtype") {
|
|
95
|
+
ctx.currentFunction = void 0;
|
|
96
|
+
}
|
|
97
|
+
if (scopeStack.length > 0 && scopeStack[scopeStack.length - 1].node === node) scopeStack.pop();
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (node.hasError || node.isMissing) {
|
|
101
|
+
if (node.type === "ERROR" || node.type === "MISSING") {
|
|
102
|
+
ctx.errors.push({
|
|
103
|
+
message: `Syntax ${node.type} detected.`,
|
|
104
|
+
range: this.nodeToRange(node),
|
|
105
|
+
severity: "warning"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let pushedScope = false;
|
|
110
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
111
|
+
if (node.type === "case" || node.type === "conditional" || node.type === "guards") {
|
|
112
|
+
cyclomaticComplexity++;
|
|
113
|
+
}
|
|
114
|
+
if (node.type === "do") ctx.patterns["Monads"] = true;
|
|
115
|
+
if (node.type === "list_comprehension") ctx.patterns["List Comprehensions"] = true;
|
|
116
|
+
if (node.type === "case" || node.type === "patterns") ctx.patterns["Pattern Matching"] = true;
|
|
117
|
+
if (node.type === "header") {
|
|
118
|
+
pushedScope = this.processModule(node, ctx, scopeStack);
|
|
119
|
+
} else if (node.type === "import") {
|
|
120
|
+
this.processImport(node, ctx);
|
|
121
|
+
} else if (node.type === "signature") {
|
|
122
|
+
const p = node.parent?.type;
|
|
123
|
+
const isDeclSig = p === "declarations" || p === "local_binds" || p === "class" || p === "instance" || p === "class_declarations" || p === "instance_declarations";
|
|
124
|
+
if (isDeclSig) {
|
|
125
|
+
this.processSignature(node, ctx, currentScope, currentScope?.kind === ASTEngine.SymbolKind.Namespace);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const expr = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
129
|
+
if (expr) stack.push({ node: expr });
|
|
130
|
+
continue;
|
|
131
|
+
} else if (node.type === "function" || node.type === "bind") {
|
|
132
|
+
const hasArrowBind = this.hasChildOfType(node, "<-");
|
|
133
|
+
const localScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
134
|
+
const insideLocalBlock = localScope?.kind === ASTEngine.SymbolKind.Namespace;
|
|
135
|
+
if (hasArrowBind) {
|
|
136
|
+
stack.push({ node, isExit: true, restoreFn: ctx.currentFunction });
|
|
137
|
+
} else if (!insideLocalBlock) {
|
|
138
|
+
pushedScope = this.processFunction(node, ctx, currentScope, scopeStack);
|
|
139
|
+
if (!pushedScope && !(node.type === "function" && (node.childForFieldName("parameter") || node.childForFieldName("result")))) {
|
|
140
|
+
stack.push({ node, isExit: true, restoreFn: ctx.currentFunction });
|
|
141
|
+
}
|
|
142
|
+
} else if (node.type === "function") {
|
|
143
|
+
stack.push({ node, isExit: true, restoreFn: ctx.currentFunction });
|
|
144
|
+
const head = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
145
|
+
let localName;
|
|
146
|
+
if (head?.type === "variable") {
|
|
147
|
+
localName = head.text;
|
|
148
|
+
} else if (head && (head.type === "infix" || head.type === "prefix_id")) {
|
|
149
|
+
const op = this.findNamedChildByType(head, ["operator"])?.text;
|
|
150
|
+
if (op) {
|
|
151
|
+
localName = op;
|
|
152
|
+
} else {
|
|
153
|
+
const infixId = this.findNamedChildByType(head, ["infix_id"]);
|
|
154
|
+
const tv = infixId ? this.findNamedChildByType(infixId, ["variable"]) : void 0;
|
|
155
|
+
if (tv?.text) localName = tv.text;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const scopedLocal = ctx.localScopes.callableScoped.get(node.id) ?? localName;
|
|
159
|
+
ctx.currentFunction = scopedLocal;
|
|
160
|
+
scopeStack.push({
|
|
161
|
+
node,
|
|
162
|
+
name: scopedLocal ?? "local",
|
|
163
|
+
kind: ASTEngine.SymbolKind.Namespace,
|
|
164
|
+
locals: ctx.localScopes.frameLocals.get(node.id),
|
|
165
|
+
isFrame: true
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
stack.push({ node, isExit: true, restoreFn: ctx.currentFunction });
|
|
169
|
+
const head = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
170
|
+
const scopedLocal = ctx.localScopes.callableScoped.get(node.id) ?? (head?.type === "variable" ? head.text : void 0);
|
|
171
|
+
if (scopedLocal !== void 0) {
|
|
172
|
+
ctx.currentFunction = scopedLocal;
|
|
173
|
+
scopeStack.push({
|
|
174
|
+
node,
|
|
175
|
+
name: scopedLocal,
|
|
176
|
+
kind: ASTEngine.SymbolKind.Namespace,
|
|
177
|
+
locals: ctx.localScopes.frameLocals.get(node.id),
|
|
178
|
+
isFrame: true
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else if (node.type === "let" || node.type === "local_binds" || node.type === "list_comprehension") {
|
|
183
|
+
const blockLocals = ctx.localScopes.blockLocals.get(node.id);
|
|
184
|
+
if (blockLocals && this.isForwardVisibleLocalBlock(node)) {
|
|
185
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
186
|
+
if (scopeStack[i].isFrame) {
|
|
187
|
+
const fm = scopeStack[i].locals ?? /* @__PURE__ */ new Map();
|
|
188
|
+
for (const [k, v] of blockLocals) fm.set(k, v);
|
|
189
|
+
scopeStack[i].locals = fm;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
scopeStack.push({ node, name: "local", kind: ASTEngine.SymbolKind.Namespace, locals: blockLocals });
|
|
195
|
+
pushedScope = true;
|
|
196
|
+
} else if (node.type === "class") {
|
|
197
|
+
pushedScope = this.processClass(node, ctx, scopeStack);
|
|
198
|
+
} else if (node.type === "instance") {
|
|
199
|
+
pushedScope = this.processInstance(node, ctx, scopeStack);
|
|
200
|
+
} else if (node.type === "data_type" || node.type === "newtype") {
|
|
201
|
+
pushedScope = this.processData(node, ctx, scopeStack);
|
|
202
|
+
} else if (node.type === "type_synomym") {
|
|
203
|
+
pushedScope = this.processTypeSynonym(node, ctx, scopeStack);
|
|
204
|
+
} else if (node.type === "pattern_synonym") {
|
|
205
|
+
pushedScope = this.processPatternSynonym(node, ctx);
|
|
206
|
+
} else if (node.type === "foreign_import") {
|
|
207
|
+
pushedScope = this.processForeignImport(node, ctx, scopeStack);
|
|
208
|
+
} else if (node.type === "deriving_instance") {
|
|
209
|
+
const grab = (nn) => {
|
|
210
|
+
let cls;
|
|
211
|
+
let clsNode = null;
|
|
212
|
+
let target;
|
|
213
|
+
for (let i = 0; i < nn.namedChildCount; i++) {
|
|
214
|
+
const c = nn.namedChild(i);
|
|
215
|
+
if (!c) continue;
|
|
216
|
+
if (c.type === "name" || c.type === "qualified") {
|
|
217
|
+
cls = c.text;
|
|
218
|
+
clsNode = c;
|
|
219
|
+
} else if (c.type === "apply") {
|
|
220
|
+
const h = c.namedChildCount > 0 ? c.namedChild(0) : null;
|
|
221
|
+
if (h?.text) {
|
|
222
|
+
cls = h.text;
|
|
223
|
+
clsNode = h;
|
|
224
|
+
}
|
|
225
|
+
} else if (c.type === "type_patterns") {
|
|
226
|
+
const t2 = this.findNamedChildByType(c, ["name", "constructor"]);
|
|
227
|
+
target = t2?.text;
|
|
228
|
+
} else if (c.type === "parens") grab(c);
|
|
229
|
+
}
|
|
230
|
+
if (cls && target) {
|
|
231
|
+
this.pushRel(
|
|
232
|
+
ctx,
|
|
233
|
+
target,
|
|
234
|
+
cls,
|
|
235
|
+
ASTEngine.RelationshipType.Implements,
|
|
236
|
+
1,
|
|
237
|
+
{
|
|
238
|
+
derived: true,
|
|
239
|
+
isStandaloneDeriving: true
|
|
240
|
+
},
|
|
241
|
+
this.lineOf(clsNode)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
grab(node);
|
|
246
|
+
} else if (node.type === "record") {
|
|
247
|
+
const ctor = this.findNamedChildByType(node, ["constructor"]);
|
|
248
|
+
if (ctor?.text && ctx.currentFunction && !this.isDataDeclarationPosition(node) && !this.isPatternPosition(node)) {
|
|
249
|
+
this.pushRel(
|
|
250
|
+
ctx,
|
|
251
|
+
ctx.currentFunction,
|
|
252
|
+
ctor.text,
|
|
253
|
+
ASTEngine.RelationshipType.Instantiates,
|
|
254
|
+
0.9,
|
|
255
|
+
{
|
|
256
|
+
haskellSpecific: true,
|
|
257
|
+
isRecordConstruction: true
|
|
258
|
+
},
|
|
259
|
+
this.lineOf(ctor)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
} else if (node.type === "apply") {
|
|
263
|
+
this.extractApplyCall(node, ctx, scopeStack);
|
|
264
|
+
} else if (node.type === "infix") {
|
|
265
|
+
this.processInfix(node, ctx, scopeStack);
|
|
266
|
+
} else if (node.type === "prefix_id") {
|
|
267
|
+
this.emitOperatorApplication(node, ctx, scopeStack);
|
|
268
|
+
} else if (node.type === "top_splice") {
|
|
269
|
+
this.decorateFromSplice(node, ctx);
|
|
270
|
+
} else if (node.type === "pragma") {
|
|
271
|
+
this.countPragmas(node, ctx);
|
|
272
|
+
} else if (node.type === "cpp") {
|
|
273
|
+
this.processCppDirective(node, ctx);
|
|
274
|
+
}
|
|
275
|
+
if (pushedScope) stack.push({ node, isExit: true });
|
|
276
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
277
|
+
const child = node.namedChild(i);
|
|
278
|
+
if (child) stack.push({ node: child });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const frameworks = [];
|
|
282
|
+
for (const f in ctx.frameworks) frameworks.push(f);
|
|
283
|
+
const patterns = [];
|
|
284
|
+
for (const p in ctx.patterns) patterns.push(p);
|
|
285
|
+
const frameworkPatterns = [];
|
|
286
|
+
for (let i = 0; i < frameworks.length; i++) {
|
|
287
|
+
frameworkPatterns.push({
|
|
288
|
+
framework: frameworks[i],
|
|
289
|
+
patterns: [{ type: "usage", detected: true, description: `${frameworks[i]} usage detected` }]
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (!ctx.symbols.some((s) => s.kind === ASTEngine.SymbolKind.Module)) {
|
|
293
|
+
ctx.symbols.push({
|
|
294
|
+
name: "Main",
|
|
295
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
296
|
+
range: this.nodeToRange(rootNode),
|
|
297
|
+
scope: "",
|
|
298
|
+
ownerName: "",
|
|
299
|
+
visibility: "public",
|
|
300
|
+
metadata: { haskellSpecific: true, isImplicitMain: true }
|
|
301
|
+
});
|
|
302
|
+
for (const s of ctx.symbols) {
|
|
303
|
+
if (s.kind !== ASTEngine.SymbolKind.Module && !s.ownerName && !s.scope) s.ownerName = "Main";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const declaredHere = /* @__PURE__ */ new Set();
|
|
307
|
+
for (const s of ctx.symbols) {
|
|
308
|
+
if (s.ownerName && s.ownerName !== ctx.currentModule) continue;
|
|
309
|
+
if (s.kind === ASTEngine.SymbolKind.Class || s.kind === ASTEngine.SymbolKind.Type || // `TypeAlias` is a legacy ALIAS for `type` -- both members carry the value
|
|
310
|
+
// "type" (see SymbolKind in engine.ts) -- so comparing it here tested the
|
|
311
|
+
// same value twice. TypeScript collapses same-valued enum members, which is
|
|
312
|
+
// what made this comparison an error rather than a redundancy.
|
|
313
|
+
s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Enum) declaredHere.add(s.name);
|
|
314
|
+
}
|
|
315
|
+
for (const s of ctx.symbols) {
|
|
316
|
+
const _owner = s.ownerName || (s.scope && s.scope !== ctx.currentModule ? s.scope : void 0);
|
|
317
|
+
if (_owner && _owner !== ctx.currentModule && s.kind === ASTEngine.SymbolKind.Method && !declaredHere.has(_owner)) continue;
|
|
318
|
+
if (_owner && _owner !== s.name && s.kind !== ASTEngine.SymbolKind.Instance) {
|
|
319
|
+
this.pushRel(ctx, _owner, s.name, ASTEngine.RelationshipType.Contains, 1, {}, s.range?.startLine);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (ctx.exports.length === 0) {
|
|
323
|
+
for (const s of ctx.symbols) {
|
|
324
|
+
if (s.ownerName === ctx.currentModule || !s.ownerName && !s.scope) {
|
|
325
|
+
ctx.exports.push({ name: s.name, kind: s.kind, range: s.range, visibility: "public" });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const localIdentities = /* @__PURE__ */ new Set();
|
|
330
|
+
for (const block of ctx.localScopes.blockLocals.values()) {
|
|
331
|
+
for (const identity of block.values()) localIdentities.add(identity);
|
|
332
|
+
}
|
|
333
|
+
if (localIdentities.size > 0) {
|
|
334
|
+
for (const rel of ctx.relationships) {
|
|
335
|
+
const target = rel.target ?? "";
|
|
336
|
+
if (!localIdentities.has(target)) continue;
|
|
337
|
+
rel.target = `${rel.source ?? ""}${BINDING_SEP}${target}`;
|
|
338
|
+
rel.metadata = { ...rel.metadata ?? {}, localBinding: true };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
this.classifyEvidenceTier(ctx.relationships);
|
|
342
|
+
return {
|
|
343
|
+
language: "haskell",
|
|
344
|
+
symbols: ctx.symbols,
|
|
345
|
+
relationships: ctx.relationships,
|
|
346
|
+
imports: ctx.imports,
|
|
347
|
+
exports: ctx.exports,
|
|
348
|
+
errors: ctx.errors,
|
|
349
|
+
metadata: {
|
|
350
|
+
complexity: cyclomaticComplexity,
|
|
351
|
+
linesOfCode: content.split("\n").length,
|
|
352
|
+
cyclomaticComplexity,
|
|
353
|
+
framework: frameworks[0],
|
|
354
|
+
patterns,
|
|
355
|
+
detectedFrameworks: frameworks,
|
|
356
|
+
frameworkPatterns,
|
|
357
|
+
architecturalScore: Math.min(100, Math.max(0, 100 - ctx.errors.length * 5)),
|
|
358
|
+
codeQuality: ctx.errors.length === 0 ? "Excellent" : "Fair",
|
|
359
|
+
hasTypeInference: true,
|
|
360
|
+
hasFrameworkIntelligence: true,
|
|
361
|
+
analysisVersion: "10.1.0-haskell-pure-ast",
|
|
362
|
+
// The passive-metadata pragma class, disclosed as a count. `{-# ANN #-}`
|
|
363
|
+
// attaches a value to a target and applies no callable, so it emits no
|
|
364
|
+
// relationship; the number is here so the omission is a stated class and
|
|
365
|
+
// not a silence a reader has to interpret.
|
|
366
|
+
...ctx.annPragmas > 0 ? { annPragmaSites: ctx.annPragmas } : {}
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// ==========================================================================
|
|
371
|
+
// Relationship emission (dedupe by source|type|target identity).
|
|
372
|
+
// ==========================================================================
|
|
373
|
+
// M59.A7 / formula §8 (dimension 8) — the site line, the clean/erlang
|
|
374
|
+
// contract verbatim:
|
|
375
|
+
// * the line is part of the DEDUPE KEY: a function called from three lines
|
|
376
|
+
// is three edges at three lines, not one edge at whichever line the walk
|
|
377
|
+
// saw first (the store keys its rows the same way, parallel-indexer.ts);
|
|
378
|
+
// * the anchor is the NAME-BEARING node — the callee head for a call, the
|
|
379
|
+
// constructed name for an `instantiates`, the class name for
|
|
380
|
+
// `implements`/`extends`, the imported module name for `references`, and
|
|
381
|
+
// the child's OWN naming line for `contains` (#921: it is what resolves
|
|
382
|
+
// the intended target among same-named siblings);
|
|
383
|
+
// * a line that is not a positive integer is recorded as ABSENT, never as 0
|
|
384
|
+
// (M59.O2: the contract is "the site line or NULL" — a fabricated 0 reads
|
|
385
|
+
// as "line 0" to every reader downstream).
|
|
386
|
+
pushRel(ctx, source, target, type, confidence, metadata = {}, line) {
|
|
387
|
+
const hasLine = typeof line === "number" && Number.isInteger(line) && line > 0;
|
|
388
|
+
const key = `${source}|${type}|${target}|${hasLine ? line : ""}`;
|
|
389
|
+
if (ctx.seenRels.has(key)) return;
|
|
390
|
+
ctx.seenRels.add(key);
|
|
391
|
+
ctx.relationships.push({
|
|
392
|
+
source,
|
|
393
|
+
target,
|
|
394
|
+
type,
|
|
395
|
+
confidence,
|
|
396
|
+
// HSA1 — the claim rides every row this writer publishes, so a new emission site cannot
|
|
397
|
+
// silently drop it (see `bareExternalClaim`).
|
|
398
|
+
metadata: { ...metadata, ...this.bareExternalClaim(ctx, type, target) },
|
|
399
|
+
...hasLine ? { line_number: line } : {}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* HSA1 — A DEPENDENCY THE FILE DOES NOT DECLARE IS A CLAIM, NOT SILENCE.
|
|
404
|
+
*
|
|
405
|
+
* The lane's wall after its declaration wave was 60,898 ambiguous/unresolved of 193,418 edges, and
|
|
406
|
+
* the two largest classes are the two dependencies Haskell spells as a BARE CAPITALISED NAME:
|
|
407
|
+
*
|
|
408
|
+
* `instance Show Foo where …` the class `Show` lives in `base`, not in this project
|
|
409
|
+
* `Just x`, `Left e`, `Right v` the constructors are the language's own
|
|
410
|
+
*
|
|
411
|
+
* MEASURED over the whole admitted population (3,702 files / 6 repos): `implements` 11,563
|
|
412
|
+
* unresolved, led by `Show` 2,459 / `Eq` 1,899 / `Generic` 1,404 / `Ord` 703 / `ToJSON` 662 /
|
|
413
|
+
* `FromJSON` 641 / `NFData` 530 / `Hashable` 327; and `instantiates` 10,978 unresolved, led by
|
|
414
|
+
* `Just` 3,657 / `Right` 1,017 / `Left` 617. None of those names can be declared by a corpus file
|
|
415
|
+
* that does not already declare it, so every row was a dependency published with NO reading at all.
|
|
416
|
+
*
|
|
417
|
+
* THE CLAIM IS THE FLEET'S `externalTarget`, and it is SAFE BY CONSTRUCTION rather than by care: the
|
|
418
|
+
* arbiter consults a claim only where the name model has NOTHING (`decls === 0`), so a name the
|
|
419
|
+
* project really does declare keeps its resolved/ambiguous answer and can never be refuted by this
|
|
420
|
+
* row. A REFUTATION requires `decls > 0` on an UNRESOLVED row, which this claim cannot reach.
|
|
421
|
+
*
|
|
422
|
+
* THREE REFUSALS, each because the spelling is not a bare declaration name:
|
|
423
|
+
* · a QUALIFIED target (`Csv.ToNamedRecord`, `T.unpack`) — a module path, and the arbiter's own
|
|
424
|
+
* `importPath` claim is the one that reads it; a second claim would be a different question.
|
|
425
|
+
* · a target that is not capitalised — Haskell type and data constructors are upper-case, so a
|
|
426
|
+
* lower-case name here is a parse artifact, not a type.
|
|
427
|
+
* · a name THIS FILE declares — the row is a local dependency and must resolve, not claim.
|
|
428
|
+
*/
|
|
429
|
+
bareExternalClaim(ctx, type, target) {
|
|
430
|
+
if (type === ASTEngine.RelationshipType.Exports && target.length > 0) {
|
|
431
|
+
if (target === ctx.currentModule) return {};
|
|
432
|
+
if (ctx.declaredTypes.has(target) || ctx.declaredFunctions.has(target)) return {};
|
|
433
|
+
const first2 = target.charCodeAt(0);
|
|
434
|
+
const firstOk = first2 >= 65 && first2 <= 90 || first2 >= 97 && first2 <= 122 || first2 === 95;
|
|
435
|
+
if (!firstOk) return {};
|
|
436
|
+
for (let i = 1; i < target.length; i++) {
|
|
437
|
+
const c = target.charCodeAt(i);
|
|
438
|
+
const ok = c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 95 || c === 39;
|
|
439
|
+
if (!ok) return {};
|
|
440
|
+
}
|
|
441
|
+
return { externalTarget: true, externalVia: "re-export" };
|
|
442
|
+
}
|
|
443
|
+
if (type === ASTEngine.RelationshipType.Calls) {
|
|
444
|
+
const dot = target.indexOf(".");
|
|
445
|
+
if (dot > 0) {
|
|
446
|
+
const mod = ctx.importAliases.get(target.slice(0, dot));
|
|
447
|
+
if (mod !== void 0 && mod.length > 0) {
|
|
448
|
+
return { externalTarget: true, externalReason: "module-namespace", externalSpecifier: mod };
|
|
449
|
+
}
|
|
450
|
+
return {};
|
|
451
|
+
}
|
|
452
|
+
return {};
|
|
453
|
+
}
|
|
454
|
+
if (type === ASTEngine.RelationshipType.Imports && target.length > 0) {
|
|
455
|
+
if (ctx.declaredTypes.has(target) || ctx.declaredFunctions.has(target)) return {};
|
|
456
|
+
return { externalTarget: true, externalReason: "module-namespace", externalSpecifier: target };
|
|
457
|
+
}
|
|
458
|
+
if (type === ASTEngine.RelationshipType.Implements) {
|
|
459
|
+
const dotI = target.indexOf(".");
|
|
460
|
+
if (dotI > 0) {
|
|
461
|
+
const mod = ctx.importAliases.get(target.slice(0, dotI));
|
|
462
|
+
if (mod !== void 0 && mod.length > 0) {
|
|
463
|
+
return { externalTarget: true, externalReason: "module-namespace", externalSpecifier: mod };
|
|
464
|
+
}
|
|
465
|
+
return {};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (type === ASTEngine.RelationshipType.Uses) {
|
|
469
|
+
if (target.length === 0 || target.indexOf(".") >= 0) return {};
|
|
470
|
+
const firstU = target.charCodeAt(0);
|
|
471
|
+
if (!(firstU >= 65 && firstU <= 90)) return {};
|
|
472
|
+
for (let i = 1; i < target.length; i++) {
|
|
473
|
+
const c = target.charCodeAt(i);
|
|
474
|
+
const ok = c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 95 || c === 39;
|
|
475
|
+
if (!ok) return {};
|
|
476
|
+
}
|
|
477
|
+
if (ctx.declaredTypes.has(target) || ctx.declaredFunctions.has(target)) return {};
|
|
478
|
+
return { externalTarget: true, externalVia: "type" };
|
|
479
|
+
}
|
|
480
|
+
if (type === ASTEngine.RelationshipType.Instantiates) {
|
|
481
|
+
const dot = target.indexOf(".");
|
|
482
|
+
if (dot > 0) {
|
|
483
|
+
const mod = ctx.importAliases.get(target.slice(0, dot));
|
|
484
|
+
if (mod !== void 0 && mod.length > 0) {
|
|
485
|
+
return { externalTarget: true, externalReason: "module-namespace", externalSpecifier: mod };
|
|
486
|
+
}
|
|
487
|
+
return {};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (type === ASTEngine.RelationshipType.Extends) {
|
|
491
|
+
if (target.length === 0 || target.indexOf(".") >= 0) return {};
|
|
492
|
+
const firstE = target.charCodeAt(0);
|
|
493
|
+
if (!(firstE >= 65 && firstE <= 90)) return {};
|
|
494
|
+
for (let i = 1; i < target.length; i++) {
|
|
495
|
+
const c = target.charCodeAt(i);
|
|
496
|
+
const ok = c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 95 || c === 39;
|
|
497
|
+
if (!ok) return {};
|
|
498
|
+
}
|
|
499
|
+
if (ctx.declaredTypes.has(target)) return {};
|
|
500
|
+
return { externalTarget: true, externalVia: "superclass" };
|
|
501
|
+
}
|
|
502
|
+
if (type !== ASTEngine.RelationshipType.Implements && type !== ASTEngine.RelationshipType.Instantiates) return {};
|
|
503
|
+
if (!target || target.length === 0) return {};
|
|
504
|
+
for (let i = 0; i < target.length; i++) {
|
|
505
|
+
const c = target.charCodeAt(i);
|
|
506
|
+
if (c === 46 || c === 58) return {};
|
|
507
|
+
}
|
|
508
|
+
const first = target.charCodeAt(0);
|
|
509
|
+
if (!(first >= 65 && first <= 90)) return {};
|
|
510
|
+
if (ctx.declaredTypes.has(target) || ctx.declaredFunctions.has(target) || ctx.declaredConstructors.has(target)) return {};
|
|
511
|
+
return {
|
|
512
|
+
externalTarget: true,
|
|
513
|
+
externalVia: type === ASTEngine.RelationshipType.Implements ? "instance-class" : "constructor"
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/** 1-based line of a node, or undefined — never 0 (M59.O2). */
|
|
517
|
+
lineOf(node) {
|
|
518
|
+
if (!node) return void 0;
|
|
519
|
+
const line = node.startPosition.row + 1;
|
|
520
|
+
return line > 0 ? line : void 0;
|
|
521
|
+
}
|
|
522
|
+
// ==========================================================================
|
|
523
|
+
// Template Haskell: `decorates` from a declaration-position splice.
|
|
524
|
+
//
|
|
525
|
+
// WHY THIS IS `decorates` AND NOT A RULING. Template Haskell runs code at
|
|
526
|
+
// COMPILE TIME. `$(makeLenses ''Widget)` applies the function `makeLenses` to
|
|
527
|
+
// the named declaration `Widget` and the function GENERATES accessor
|
|
528
|
+
// declarations for it; the declaration therefore CARRIES the decorator in
|
|
529
|
+
// exactly the sense the vocabulary means (`decorates`: a decorator is applied
|
|
530
|
+
// to a declaration). The measured population has 420 declaration-position
|
|
531
|
+
// splices, 177 of which name a declaration this file declares.
|
|
532
|
+
//
|
|
533
|
+
// THE DISCRIMINATOR IS ON THE DECORATED DECLARATION, NEVER ON THE HEAD.
|
|
534
|
+
// TH decorators are LIBRARY functions — `makeLenses` is imported from
|
|
535
|
+
// `Control.Lens.TH`, `deriveJSON` from `Data.Aeson.TH` — exactly as Python's
|
|
536
|
+
// `@dataclass` is imported from `dataclasses`. Requiring the HEAD to be
|
|
537
|
+
// declared in the file would exclude the construct and publish nothing; the
|
|
538
|
+
// test that identifies the construct is the `th_quoted_name` argument naming a
|
|
539
|
+
// declaration of THIS file. Measured near misses that must therefore stay
|
|
540
|
+
// silent: a splice quoting an IMPORTED declaration (33 sites — the quoted
|
|
541
|
+
// name is declared in another module, so this file cannot source an edge from
|
|
542
|
+
// it), and a declaration-position splice with no callable head (77 sites —
|
|
543
|
+
// `$(do ... [d| ... |])` generators and C-preprocessor macro lines such as
|
|
544
|
+
// `INSTANCE_CLUMP_1(...)`, which the grammar parses as a bare top-level
|
|
545
|
+
// expression splice).
|
|
546
|
+
//
|
|
547
|
+
// THE EDGE, per the lane's brief: source is the ENCLOSING MODULE SCOPE and the
|
|
548
|
+
// target is the DECORATED DECLARATION's own symbol. The decorator name is
|
|
549
|
+
// often external and unresolvable, so it is carried in
|
|
550
|
+
// `metadata.decoratorName` rather than emitted as a dangling endpoint — an
|
|
551
|
+
// edge to a name nothing declares would be a claim the index cannot answer.
|
|
552
|
+
//
|
|
553
|
+
// `{-# ANN #-}` IS NOT THIS CONSTRUCT. It attaches a value to a target
|
|
554
|
+
// (`{-# ANN getEnvironment ("HLINT: ..." :: String) #-}`) and applies no
|
|
555
|
+
// callable, so it is the passive-metadata class and publishes no edge; its
|
|
556
|
+
// sites are counted in `metadata.annPragmaSites` so the class is disclosed
|
|
557
|
+
// rather than silently dropped.
|
|
558
|
+
// ==========================================================================
|
|
559
|
+
decorateFromSplice(node, ctx) {
|
|
560
|
+
ctx.patterns["Template Haskell"] = true;
|
|
561
|
+
let expr = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
562
|
+
for (let guard = 0; guard < 6 && expr; guard++) {
|
|
563
|
+
if (expr.type === "splice" || expr.type === "parens") {
|
|
564
|
+
expr = expr.namedChildCount > 0 ? expr.namedChild(0) : null;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
if (!expr) return;
|
|
570
|
+
let head = expr;
|
|
571
|
+
let decoratorName;
|
|
572
|
+
let decoratorQualified = false;
|
|
573
|
+
for (let guard = 0; guard < 12 && head; guard++) {
|
|
574
|
+
if (head.type === "variable" || head.type === "name") {
|
|
575
|
+
decoratorName = head.text;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
if (head.type === "qualified") {
|
|
579
|
+
const mod = this.findNamedChildByType(head, ["module"]);
|
|
580
|
+
const v = this.findNamedChildByType(head, ["variable", "constructor"]);
|
|
581
|
+
const modText = this.stripTrailingDot(mod?.text ?? "");
|
|
582
|
+
decoratorName = modText && v?.text ? `${modText}.${v.text}` : head.text;
|
|
583
|
+
decoratorQualified = true;
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
if (head.type === "apply") {
|
|
587
|
+
head = head.namedChildCount > 0 ? head.namedChild(0) : null;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
if (!decoratorName) return;
|
|
593
|
+
const quotes = this.thQuotedNames(expr);
|
|
594
|
+
let decorated;
|
|
595
|
+
for (const q of quotes) {
|
|
596
|
+
if (ctx.declaredTypes.has(q.name) || ctx.declaredFunctions.has(q.name)) {
|
|
597
|
+
decorated = q;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (!decorated) return;
|
|
602
|
+
const scope = ctx.currentModule || "Main";
|
|
603
|
+
this.pushRel(
|
|
604
|
+
ctx,
|
|
605
|
+
scope,
|
|
606
|
+
decorated.name,
|
|
607
|
+
ASTEngine.RelationshipType.Decorates,
|
|
608
|
+
1,
|
|
609
|
+
{
|
|
610
|
+
decoratorName,
|
|
611
|
+
isQualifiedDecorator: decoratorQualified,
|
|
612
|
+
thQuotedForm: decorated.raw,
|
|
613
|
+
isTemplateHaskell: true,
|
|
614
|
+
haskellSpecific: true
|
|
615
|
+
},
|
|
616
|
+
this.lineOf(node)
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Every `th_quoted_name` under a subtree, with the quotes stripped.
|
|
621
|
+
*
|
|
622
|
+
* The grammar puts the quoted name in a `name` child (`''Foo` -> `''` token +
|
|
623
|
+
* `name` "Foo"); the raw text is kept so the edge's metadata can say which
|
|
624
|
+
* quote form was used. A quote with no readable name is skipped rather than
|
|
625
|
+
* published with its ticks — a name that is not a name is not an endpoint.
|
|
626
|
+
*/
|
|
627
|
+
thQuotedNames(node) {
|
|
628
|
+
const out = [];
|
|
629
|
+
const visit = (n) => {
|
|
630
|
+
if (n.type === "th_quoted_name") {
|
|
631
|
+
const raw = n.text ?? "";
|
|
632
|
+
const nameNode = this.findNamedChildByType(n, ["name", "variable", "constructor"]);
|
|
633
|
+
const name = (nameNode?.text ?? "").trim();
|
|
634
|
+
if (name.length > 0) out.push({ name, raw });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
638
|
+
const c = n.namedChild(i);
|
|
639
|
+
if (c) visit(c);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
visit(node);
|
|
643
|
+
return out;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Count the passive-metadata pragma family (`{-# ANN ... #-}`).
|
|
647
|
+
*
|
|
648
|
+
* The pragma node's body is OPAQUE to the grammar (the node carries no named
|
|
649
|
+
* children — measured over the whole population), so the keyword is read from
|
|
650
|
+
* the text between `{-#` and the first whitespace, with no regular
|
|
651
|
+
* expression: the analyzer's no-regex criterion forbids a pattern literal and
|
|
652
|
+
* a keyword test does not need one.
|
|
653
|
+
*/
|
|
654
|
+
countPragmas(node, ctx) {
|
|
655
|
+
const text = (node.text ?? "").trim();
|
|
656
|
+
if (!text.startsWith("{-#")) return;
|
|
657
|
+
let i = 3;
|
|
658
|
+
while (i < text.length && (text[i] === " " || text[i] === " ")) i++;
|
|
659
|
+
const keyword = text.slice(i, i + 3).toUpperCase();
|
|
660
|
+
if (keyword === "ANN") ctx.annPragmas++;
|
|
661
|
+
}
|
|
662
|
+
// ==========================================================================
|
|
663
|
+
// `part_of` FROM A CPP `#include` — THE R3 EMISSION ON AN EXISTING NODE TYPE.
|
|
664
|
+
//
|
|
665
|
+
// WHY THIS IS `part_of` AND NOT AN `imports`. Haskell has no textual include
|
|
666
|
+
// of its own: `import Module` is a MODULE LOAD — it creates binding scope for
|
|
667
|
+
// the module's exports and can be resolved as `Module.item`, which is the
|
|
668
|
+
// OPPOSITE of a fragment. The ONE textual-inclusion construct a Haskell file
|
|
669
|
+
// can carry is the C preprocessor's `#include`, available under
|
|
670
|
+
// `{-# LANGUAGE CPP #-}`: the named file's text is spliced into this module
|
|
671
|
+
// BEFORE parsing, it shares this module's single scope, and it has no module
|
|
672
|
+
// identity at all. That is the contract's `part_of` verbatim ("a part shares
|
|
673
|
+
// the library's single scope, imports nothing, and cannot be resolved as a
|
|
674
|
+
// separate module"), and the fleet's implemented `part_of` set is unanimously
|
|
675
|
+
// textual file inclusion (cpp `#include`, fsharp `#load`, php include/require,
|
|
676
|
+
// erlang `-include`, typescript reference directives).
|
|
677
|
+
//
|
|
678
|
+
// RUNG WALKED: R1 and R2 are UNNECESSARY — `cpp` is already a NAMED node type
|
|
679
|
+
// of the SERVED grammar (253 named of 552 ids, digest 37a6b07b1a83; measured
|
|
680
|
+
// in validation/m14/measurements/haskell-grammar-nodes.json), so R3 is live
|
|
681
|
+
// and no grammar build or patch is needed.
|
|
682
|
+
//
|
|
683
|
+
// THE TARGET IS THE PART'S MODULE SYMBOL (its file STEM), NOT A PATH, and the
|
|
684
|
+
// written path is kept in `metadata.partPath`. The arbiter resolves TARGET
|
|
685
|
+
// STRINGS AGAINST SYMBOL NAMES and has no space for a path — the cpp lane
|
|
686
|
+
// measured that emitting paths gave 0 resolved of 4,612 rows — while the
|
|
687
|
+
// contract permits either ("the part's file OR ITS MODULE SYMBOL"). The stem
|
|
688
|
+
// is symbol-shaped, so it can bind when it names something.
|
|
689
|
+
//
|
|
690
|
+
// MEASURED over the whole admitted population (3,702 files): FOUR real
|
|
691
|
+
// directive sites, every one a quoted `#include "MachDeps.h"` (three Cabal
|
|
692
|
+
// Cmm test-suite files, one hasura-engine module), 0 non-literal. The SEVEN
|
|
693
|
+
// other `#include` strings in the corpus are NOT directives and emit nothing:
|
|
694
|
+
// a string-built line `["#include \"" ++ hdr ++ "\""]`, the `-#include` ghc
|
|
695
|
+
// FLAG as a string, a Haddock comment mentioning the flag, and a JSON
|
|
696
|
+
// quasiquote carrying the text. All four are pinned as negatives by the
|
|
697
|
+
// fixture. `MachDeps.h` is a GHC SYSTEM header, so its stem resolves to
|
|
698
|
+
// nothing in the admitted set — the system-header case the cpp lane handles,
|
|
699
|
+
// and it reads unresolved rather than wrong.
|
|
700
|
+
// ==========================================================================
|
|
701
|
+
processCppDirective(node, ctx) {
|
|
702
|
+
const include = this.readCppInclude(node.text ?? "");
|
|
703
|
+
if (!include) return;
|
|
704
|
+
ctx.patterns["CPP"] = true;
|
|
705
|
+
const scope = ctx.currentModule || "Main";
|
|
706
|
+
if (include.form === "quoted") {
|
|
707
|
+
this.pushRel(
|
|
708
|
+
ctx,
|
|
709
|
+
scope,
|
|
710
|
+
include.stem,
|
|
711
|
+
ASTEngine.RelationshipType.PartOf,
|
|
712
|
+
1,
|
|
713
|
+
{
|
|
714
|
+
// MANDATORY on every part_of row (src/ast/types.ts): "library" because
|
|
715
|
+
// THIS file is the library/unit and the directive names its part.
|
|
716
|
+
partRole: "library",
|
|
717
|
+
// The written path is EVIDENCE; the stem is the target. Keeping both
|
|
718
|
+
// is what makes the difference between them checkable.
|
|
719
|
+
partPath: include.specifier,
|
|
720
|
+
partSymbol: include.stem,
|
|
721
|
+
isQuotedInclude: true,
|
|
722
|
+
isCppDirective: true,
|
|
723
|
+
relationshipType: "part_of_directive",
|
|
724
|
+
haskellSpecific: true
|
|
725
|
+
},
|
|
726
|
+
this.lineOf(node)
|
|
727
|
+
);
|
|
728
|
+
} else {
|
|
729
|
+
this.pushRel(
|
|
730
|
+
ctx,
|
|
731
|
+
scope,
|
|
732
|
+
include.specifier,
|
|
733
|
+
ASTEngine.RelationshipType.Imports,
|
|
734
|
+
1,
|
|
735
|
+
{
|
|
736
|
+
isSystemInclude: true,
|
|
737
|
+
isCppDirective: true,
|
|
738
|
+
relationshipType: "include_dependency",
|
|
739
|
+
haskellSpecific: true
|
|
740
|
+
},
|
|
741
|
+
this.lineOf(node)
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Read a `#include` directive's specifier from a `cpp` node's text.
|
|
747
|
+
*
|
|
748
|
+
* Returns undefined unless the directive's keyword is EXACTLY `include` and it
|
|
749
|
+
* names a quoted or angle specifier. `#if`, `#else`, `#endif` and a shebang
|
|
750
|
+
* share the `cpp` node type and name no file, so every one of them falls out
|
|
751
|
+
* here. PURE STRING SCAN — the analyzer's no-regex contract forbids a pattern
|
|
752
|
+
* literal, and a keyword test does not need one.
|
|
753
|
+
*/
|
|
754
|
+
readCppInclude(text) {
|
|
755
|
+
const isSpace = (c) => c === " " || c === " " || c === "\n" || c === "\r";
|
|
756
|
+
let i = 0;
|
|
757
|
+
while (i < text.length && isSpace(text[i])) i++;
|
|
758
|
+
if (i >= text.length || text[i] !== "#") return void 0;
|
|
759
|
+
i++;
|
|
760
|
+
while (i < text.length && (text[i] === " " || text[i] === " ")) i++;
|
|
761
|
+
const keyword = "include";
|
|
762
|
+
if (i + keyword.length > text.length) return void 0;
|
|
763
|
+
if (text.slice(i, i + keyword.length).toLowerCase() !== keyword) return void 0;
|
|
764
|
+
let j = i + keyword.length;
|
|
765
|
+
if (j < text.length && !(text[j] === " " || text[j] === " " || text[j] === '"' || text[j] === "<")) return void 0;
|
|
766
|
+
while (j < text.length && (text[j] === " " || text[j] === " ")) j++;
|
|
767
|
+
if (j >= text.length) return void 0;
|
|
768
|
+
const open = text[j];
|
|
769
|
+
if (open === '"') {
|
|
770
|
+
const end = text.indexOf('"', j + 1);
|
|
771
|
+
if (end < 0) return void 0;
|
|
772
|
+
const specifier = text.slice(j + 1, end).trim();
|
|
773
|
+
if (specifier.length === 0) return void 0;
|
|
774
|
+
return { form: "quoted", specifier, stem: this.cppIncludeStem(specifier) };
|
|
775
|
+
}
|
|
776
|
+
if (open === "<") {
|
|
777
|
+
const end = text.indexOf(">", j + 1);
|
|
778
|
+
if (end < 0) return void 0;
|
|
779
|
+
const specifier = text.slice(j + 1, end).trim();
|
|
780
|
+
if (specifier.length === 0) return void 0;
|
|
781
|
+
return { form: "angle", specifier, stem: this.cppIncludeStem(specifier) };
|
|
782
|
+
}
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
/** A specifier's module stem: no directories, no extension (`util/Service.h` -> `Service`). */
|
|
786
|
+
cppIncludeStem(specifier) {
|
|
787
|
+
const slash = specifier.lastIndexOf("/");
|
|
788
|
+
let base = slash >= 0 ? specifier.slice(slash + 1) : specifier;
|
|
789
|
+
const backslash = base.lastIndexOf("\\");
|
|
790
|
+
if (backslash >= 0) base = base.slice(backslash + 1);
|
|
791
|
+
const dot = base.lastIndexOf(".");
|
|
792
|
+
return (dot > 0 ? base.slice(0, dot) : base).trim();
|
|
793
|
+
}
|
|
794
|
+
// ==========================================================================
|
|
795
|
+
// Pre-pass: declared TYPE names at module scope (forward-reference resolution
|
|
796
|
+
// for TH quotes). A `data`/`newtype`/`type`/`class`/`pattern` declaration is
|
|
797
|
+
// the thing a Template Haskell decorator is applied to, and the splice may
|
|
798
|
+
// appear before or after it in source order.
|
|
799
|
+
// ==========================================================================
|
|
800
|
+
collectDeclaredTypes(node, out) {
|
|
801
|
+
const t = node.type;
|
|
802
|
+
if (t === "data_type" || t === "newtype" || t === "type_synomym" || t === "class" || t === "pattern_synonym") {
|
|
803
|
+
const nameNode = this.findNamedChildByType(node, ["name"]);
|
|
804
|
+
if (nameNode?.text) out.add(nameNode.text);
|
|
805
|
+
}
|
|
806
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
807
|
+
const child = node.namedChild(i);
|
|
808
|
+
if (!child) continue;
|
|
809
|
+
if (child.type === "local_binds" || child.type === "let" || child.type === "let_in") continue;
|
|
810
|
+
this.collectDeclaredTypes(child, out);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* HSA1 — the DATA CONSTRUCTORS this module declares, for the external-claim guard only.
|
|
815
|
+
*
|
|
816
|
+
* The walk is deliberately generous: every `constructor` under a `data_type`/`newtype` is
|
|
817
|
+
* collected, field types included (`data T = C Int` also collects `Int`). Over-collecting can only
|
|
818
|
+
* WITHHOLD a claim, and a withheld claim leaves the row where it was — the safe direction. Missing
|
|
819
|
+
* one would publish a false statement about the module's own declaration, which is what the fixture
|
|
820
|
+
* caught.
|
|
821
|
+
*/
|
|
822
|
+
collectDeclaredConstructors(node, out) {
|
|
823
|
+
if (node.type === "data_type" || node.type === "newtype") {
|
|
824
|
+
const grab = (n) => {
|
|
825
|
+
if (n.type === "constructor" && n.text) out.add(n.text);
|
|
826
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
827
|
+
const c = n.namedChild(i);
|
|
828
|
+
if (c) grab(c);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
grab(node);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
835
|
+
const child = node.namedChild(i);
|
|
836
|
+
if (!child) continue;
|
|
837
|
+
if (child.type === "local_binds" || child.type === "let" || child.type === "let_in") continue;
|
|
838
|
+
this.collectDeclaredConstructors(child, out);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// ==========================================================================
|
|
842
|
+
// Pre-pass: declared function/method names (forward-reference resolution).
|
|
843
|
+
// ==========================================================================
|
|
844
|
+
// class -> its method names. This is what makes `overrides` derivable at all:
|
|
845
|
+
// an instance declares a method name, and the only way to know that name
|
|
846
|
+
// OVERRIDES rather than merely exists is to compare it against the class
|
|
847
|
+
// body's method set. A single walk cannot do that — the instance may be
|
|
848
|
+
// visited before the class, and in the payloads/cabal corpus it frequently is
|
|
849
|
+
// — so the class side is gathered first and the instance side is compared
|
|
850
|
+
// against it when the instance is reached.
|
|
851
|
+
collectClassMethods(node, out) {
|
|
852
|
+
if (node.type === "class") {
|
|
853
|
+
const clsName = this.findNamedChildByType(node, ["name"])?.text ?? "";
|
|
854
|
+
if (clsName) {
|
|
855
|
+
let set = out.get(clsName);
|
|
856
|
+
if (!set) {
|
|
857
|
+
set = /* @__PURE__ */ new Set();
|
|
858
|
+
out.set(clsName, set);
|
|
859
|
+
}
|
|
860
|
+
const decls = this.findNamedChildByType(node, ["class_declarations"]);
|
|
861
|
+
const groups = decls ? [decls] : [];
|
|
862
|
+
for (const g of groups) {
|
|
863
|
+
for (const sig of this.collectNamedChildrenByType(g, ["signature"])) {
|
|
864
|
+
const v = this.findNamedChildByType(sig, ["variable"]);
|
|
865
|
+
if (v?.text) set.add(v.text);
|
|
866
|
+
}
|
|
867
|
+
for (const fn of this.collectNamedChildrenByType(g, ["function", "bind"])) {
|
|
868
|
+
const first = fn.namedChildCount > 0 ? fn.namedChild(0) : null;
|
|
869
|
+
if (first?.type === "variable" && first.text) set.add(first.text);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
875
|
+
const c = node.namedChild(i);
|
|
876
|
+
if (c) this.collectClassMethods(c, out);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
collectDeclaredFunctions(node, out) {
|
|
880
|
+
if (node.type === "function" && (node.childForFieldName("parameter") || node.childForFieldName("result"))) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (node.type === "function" || node.type === "bind") {
|
|
884
|
+
if (!(node.type === "bind" && this.hasChildOfType(node, "<-"))) {
|
|
885
|
+
const nameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
886
|
+
if (nameNode && nameNode.type === "variable") {
|
|
887
|
+
out.add(nameNode.text);
|
|
888
|
+
} else if (nameNode && (nameNode.type === "infix" || nameNode.type === "prefix_id")) {
|
|
889
|
+
const op = this.findNamedChildByType(nameNode, ["operator"]);
|
|
890
|
+
const tv = op ? void 0 : (() => {
|
|
891
|
+
const infixId = this.findNamedChildByType(nameNode, ["infix_id"]);
|
|
892
|
+
return infixId ? this.findNamedChildByType(infixId, ["variable"]) : void 0;
|
|
893
|
+
})();
|
|
894
|
+
const callable = op?.text ?? tv?.text;
|
|
895
|
+
if (callable) out.add(callable);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (node.type === "signature") {
|
|
900
|
+
if (node.parent?.type === "parens") return;
|
|
901
|
+
const first = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
902
|
+
if (first?.type !== "variable") return;
|
|
903
|
+
out.add(first.text);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
907
|
+
const child = node.namedChild(i);
|
|
908
|
+
if (!child) continue;
|
|
909
|
+
if (child.type === "local_binds" || child.type === "let" || child.type === "let_in") continue;
|
|
910
|
+
this.collectDeclaredFunctions(child, out);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
// ==========================================================================
|
|
914
|
+
// Pre-pass: lexical local identity (M60 functional lane, 2026-09-26).
|
|
915
|
+
// A callable declared inside a where/let block is a real callable but NOT
|
|
916
|
+
// a module symbol (HSD6/HSD19 contract, unchanged). Its identity carries
|
|
917
|
+
// its lexical owner chain: `outer.local` (nested: `outer.inner.local`), so
|
|
918
|
+
// it can never be confused with a module-level callable of the same name.
|
|
919
|
+
// ==========================================================================
|
|
920
|
+
collectLocalScopes(root, out) {
|
|
921
|
+
const stack = [
|
|
922
|
+
{ n: root, frameId: -1, frameScoped: void 0 }
|
|
923
|
+
];
|
|
924
|
+
while (stack.length > 0) {
|
|
925
|
+
const { n, frameId, frameScoped } = stack.pop();
|
|
926
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
927
|
+
const child = n.namedChild(i);
|
|
928
|
+
if (!child) continue;
|
|
929
|
+
const t = child.type;
|
|
930
|
+
if (t === "local_binds" || t === "let" || t === "let_in") {
|
|
931
|
+
const binders = this.localBlockBinders(child);
|
|
932
|
+
if (binders.length > 0) {
|
|
933
|
+
const map = /* @__PURE__ */ new Map();
|
|
934
|
+
for (const b of binders) {
|
|
935
|
+
const nm2 = this.callableHeadName(b);
|
|
936
|
+
if (!nm2) continue;
|
|
937
|
+
const scoped = frameScoped !== void 0 ? `${frameScoped}.${nm2}` : nm2;
|
|
938
|
+
map.set(nm2, scoped);
|
|
939
|
+
out.callableScoped.set(b.id, scoped);
|
|
940
|
+
}
|
|
941
|
+
if (map.size > 0) {
|
|
942
|
+
out.blockLocals.set(child.id, map);
|
|
943
|
+
if (t === "local_binds" && frameId >= 0) {
|
|
944
|
+
const fm = out.frameLocals.get(frameId) ?? /* @__PURE__ */ new Map();
|
|
945
|
+
for (const [k, v] of map) fm.set(k, v);
|
|
946
|
+
out.frameLocals.set(frameId, fm);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
951
|
+
const c = child.namedChild(j);
|
|
952
|
+
if (!c) continue;
|
|
953
|
+
const scoped = out.callableScoped.get(c.id);
|
|
954
|
+
if (scoped !== void 0 && this.callableHeadName(c) !== void 0) {
|
|
955
|
+
stack.push({ n: c, frameId: c.id, frameScoped: scoped });
|
|
956
|
+
} else {
|
|
957
|
+
stack.push({ n: c, frameId, frameScoped });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const nm = this.callableHeadName(child);
|
|
963
|
+
if (nm !== void 0) {
|
|
964
|
+
const scoped = frameScoped !== void 0 ? `${frameScoped}.${nm}` : nm;
|
|
965
|
+
out.callableScoped.set(child.id, scoped);
|
|
966
|
+
stack.push({ n: child, frameId: child.id, frameScoped: scoped });
|
|
967
|
+
} else {
|
|
968
|
+
stack.push({ n: child, frameId, frameScoped });
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
/** Direct callable children of a where/let block (the local binders). */
|
|
974
|
+
localBlockBinders(block) {
|
|
975
|
+
const out = [];
|
|
976
|
+
for (let i = 0; i < block.namedChildCount; i++) {
|
|
977
|
+
const c = block.namedChild(i);
|
|
978
|
+
if (!c || c.type !== "function" && c.type !== "bind") continue;
|
|
979
|
+
if (this.callableHeadName(c) === void 0) continue;
|
|
980
|
+
out.push(c);
|
|
981
|
+
}
|
|
982
|
+
return out;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* The declared name of a callable node, or undefined when the node is not a
|
|
986
|
+
* callable at all (type-position function, do-bind, pattern-headed bind).
|
|
987
|
+
* Mirrors collectDeclaredFunctions' naming rules exactly — one ruler for
|
|
988
|
+
* module-level and local callables.
|
|
989
|
+
*/
|
|
990
|
+
callableHeadName(node) {
|
|
991
|
+
if (node.type !== "function" && node.type !== "bind") return void 0;
|
|
992
|
+
if (node.type === "function" && (node.childForFieldName("parameter") || node.childForFieldName("result"))) {
|
|
993
|
+
return void 0;
|
|
994
|
+
}
|
|
995
|
+
if (node.type === "bind" && this.hasChildOfType(node, "<-")) return void 0;
|
|
996
|
+
const head = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
997
|
+
if (!head) return void 0;
|
|
998
|
+
if (head.type === "variable") return head.text;
|
|
999
|
+
if (head.type === "infix" || head.type === "prefix_id") {
|
|
1000
|
+
const op = this.findNamedChildByType(head, ["operator"]);
|
|
1001
|
+
if (op?.text) return op.text;
|
|
1002
|
+
const infixId = this.findNamedChildByType(head, ["infix_id"]);
|
|
1003
|
+
const tv = infixId ? this.findNamedChildByType(infixId, ["variable"]) : void 0;
|
|
1004
|
+
return tv?.text;
|
|
1005
|
+
}
|
|
1006
|
+
return void 0;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* A do-block `let` (`do { let g = ...; ... g x }`): its binders stay visible
|
|
1010
|
+
* for the REST of the do block — subsequent statements are siblings, not
|
|
1011
|
+
* descendants, so the block scope alone cannot carry them. Folded into the
|
|
1012
|
+
* nearest callable frame as the walk reaches the block (source order).
|
|
1013
|
+
*/
|
|
1014
|
+
isForwardVisibleLocalBlock(node) {
|
|
1015
|
+
if (node.type !== "local_binds") return false;
|
|
1016
|
+
const p = node.parent;
|
|
1017
|
+
return p?.type === "let" && p.parent?.type === "do";
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Resolve a bare variable head to its callable target: the innermost
|
|
1021
|
+
* visible local wins (Haskell scoping — a where-binder shadows the module
|
|
1022
|
+
* binding for its whole RHS), else the module-level declared set, else no
|
|
1023
|
+
* edge (imported/Prelude/parameter names produce none).
|
|
1024
|
+
*/
|
|
1025
|
+
resolveCallableTarget(name, scopeStack, ctx) {
|
|
1026
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
1027
|
+
const locals = scopeStack[i].locals;
|
|
1028
|
+
if (locals !== void 0) {
|
|
1029
|
+
const hit = locals.get(name);
|
|
1030
|
+
if (hit !== void 0) return hit;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return ctx.declaredFunctions.has(name) ? name : void 0;
|
|
1034
|
+
}
|
|
1035
|
+
processModule(node, ctx, scopeStack) {
|
|
1036
|
+
let moduleName = "";
|
|
1037
|
+
const moduleNode = this.findNamedChildByType(node, ["module"]);
|
|
1038
|
+
if (moduleNode) {
|
|
1039
|
+
moduleName = this.assembleModuleId(moduleNode);
|
|
1040
|
+
} else {
|
|
1041
|
+
const mIdx = this.findChildIndexByText(node, "module");
|
|
1042
|
+
if (mIdx !== -1 && node.childCount > mIdx + 1) {
|
|
1043
|
+
moduleName = node.child(mIdx + 1)?.text || "";
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (!moduleName) return false;
|
|
1047
|
+
ctx.currentModule = moduleName;
|
|
1048
|
+
const exportsList = this.findNamedChildByType(node, ["exports"]);
|
|
1049
|
+
if (exportsList) {
|
|
1050
|
+
for (let i = 0; i < exportsList.namedChildCount; i++) {
|
|
1051
|
+
const expNode = exportsList.namedChild(i);
|
|
1052
|
+
if (!expNode) continue;
|
|
1053
|
+
let exportName;
|
|
1054
|
+
let isReExport = false;
|
|
1055
|
+
let anchor = expNode;
|
|
1056
|
+
if (expNode.type === "export") {
|
|
1057
|
+
const read = this.readExportName(expNode, exportsList, i);
|
|
1058
|
+
exportName = read.name;
|
|
1059
|
+
if (read.anchor) anchor = read.anchor;
|
|
1060
|
+
} else if (expNode.type === "module_export") {
|
|
1061
|
+
const mod = this.findNamedChildByType(expNode, ["module", "module_id"]);
|
|
1062
|
+
exportName = mod ? this.assembleModuleId(mod) : void 0;
|
|
1063
|
+
isReExport = exportName !== void 0;
|
|
1064
|
+
} else {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
if (exportName && exportName !== ",") {
|
|
1068
|
+
ctx.exports.push({ name: exportName, isDefault: false, range: this.nodeToRange(expNode) });
|
|
1069
|
+
this.pushRel(
|
|
1070
|
+
ctx,
|
|
1071
|
+
moduleName,
|
|
1072
|
+
exportName,
|
|
1073
|
+
ASTEngine.RelationshipType.Exports,
|
|
1074
|
+
1,
|
|
1075
|
+
{ evidenceClass: "structural", ...isReExport ? { isReExport: true } : {} },
|
|
1076
|
+
this.lineOf(anchor)
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
ctx.symbols.push({
|
|
1082
|
+
name: moduleName,
|
|
1083
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
1084
|
+
range: this.nodeToRange(node),
|
|
1085
|
+
scope: "",
|
|
1086
|
+
visibility: "public",
|
|
1087
|
+
metadata: { haskellSpecific: true }
|
|
1088
|
+
});
|
|
1089
|
+
scopeStack.push({ node, name: moduleName, kind: ASTEngine.SymbolKind.Module });
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* THE NAME an `export` node exports, or undefined when it exports none.
|
|
1094
|
+
*
|
|
1095
|
+
* WHY THIS IS NOT `expNode.text`. The export list states a name with a
|
|
1096
|
+
* DECLARATION KEYWORD in front of it whenever the entry narrows what is
|
|
1097
|
+
* exported, and the raw text is the whole phrase:
|
|
1098
|
+
*
|
|
1099
|
+
* export -> namespace("type") + name("T") text "type T"
|
|
1100
|
+
* export -> namespace("pattern") + name("P") text "pattern P"
|
|
1101
|
+
* export -> name("U") + children("(..)") text "U(..)"
|
|
1102
|
+
* export -> variable("data") + ERROR("D") text "data" (a mis-parse)
|
|
1103
|
+
*
|
|
1104
|
+
* Measured over the whole corpus before this method existed (analyzer pass,
|
|
1105
|
+
* `script/m14/tmp/hs-ontology-analyse.ts`): 392 of 54,830 `exports` edges
|
|
1106
|
+
* carried a target that was a keyword or a keyword phrase — 100 `type`,
|
|
1107
|
+
* 32 `data`, 8 `pattern`, and the rest `pattern <Ctor>` / `type <Ty>`.
|
|
1108
|
+
* `exports Data.Type.Bool -> type` names the Haskell keyword: it resolves to
|
|
1109
|
+
* nothing, and `T(A, B)`-style narrowing means the thing the module actually
|
|
1110
|
+
* exports is `T`, not `T(A, B)`.
|
|
1111
|
+
*
|
|
1112
|
+
* Tree shapes, counted on the pinned grammar over all 14,066 admitted files
|
|
1113
|
+
* (`script/m14/tmp/hs-export-ffi-probe.ts`): 31,892 `export` nodes —
|
|
1114
|
+
* 6,697 with a `name`/`constructor` child, 24,515 a bare `variable`, 43 a bare
|
|
1115
|
+
* declaration keyword whose real name the grammar pushed into a SIBLING `ERROR`
|
|
1116
|
+
* (37 of the 43 recoverable that way), and 0 unclassified.
|
|
1117
|
+
*
|
|
1118
|
+
* The 6 unrecoverable ones emit NOTHING. Under standing rule R7 the honest
|
|
1119
|
+
* state for "I cannot read this name" is absence, not a keyword standing in for
|
|
1120
|
+
* a name — a literal in a name-bearing field becomes an edge endpoint that does
|
|
1121
|
+
* not exist in the source, and `bb_graph` will happily count and display it.
|
|
1122
|
+
*/
|
|
1123
|
+
readExportName(expNode, exportsList, index) {
|
|
1124
|
+
const named = this.findNamedChildByType(expNode, ["name", "constructor"]);
|
|
1125
|
+
if (named?.text) return { name: named.text, anchor: named };
|
|
1126
|
+
const opId = this.findNamedChildByType(expNode, ["prefix_id", "infix_id"]);
|
|
1127
|
+
if (opId) {
|
|
1128
|
+
const op = this.findNamedChildByType(opId, ["operator", "constructor_operator"]);
|
|
1129
|
+
if (op?.text) return { name: op.text, anchor: op };
|
|
1130
|
+
const innerVar = this.findNamedChildByType(opId, ["variable"]);
|
|
1131
|
+
if (innerVar?.text) return { name: innerVar.text, anchor: innerVar };
|
|
1132
|
+
}
|
|
1133
|
+
const variable = this.findNamedChildByType(expNode, ["variable"]);
|
|
1134
|
+
if (variable?.text) {
|
|
1135
|
+
if (!HASKELL_DECLARATION_KEYWORDS.has(variable.text)) return { name: variable.text, anchor: variable };
|
|
1136
|
+
for (let j = index + 1; j < exportsList.namedChildCount; j++) {
|
|
1137
|
+
const nx = exportsList.namedChild(j);
|
|
1138
|
+
if (!nx) continue;
|
|
1139
|
+
if (nx.type === "export" || nx.type === "module_export") break;
|
|
1140
|
+
if (nx.type === "ERROR") {
|
|
1141
|
+
const rec = this.findNamedChildByType(nx, ["name", "constructor"]);
|
|
1142
|
+
if (rec?.text) return { name: rec.text, anchor: rec };
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return { name: void 0 };
|
|
1146
|
+
}
|
|
1147
|
+
const text = expNode.text.trim();
|
|
1148
|
+
if (!text) return { name: void 0 };
|
|
1149
|
+
const paren = text.indexOf("(");
|
|
1150
|
+
return { name: paren >= 0 ? text.slice(0, paren).trim() : text, anchor: expNode };
|
|
1151
|
+
}
|
|
1152
|
+
// module_id children compose dotted names: Data → List → "Data.List"
|
|
1153
|
+
assembleModuleId(node) {
|
|
1154
|
+
let out = "";
|
|
1155
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1156
|
+
const c = node.namedChild(i);
|
|
1157
|
+
if (!c) continue;
|
|
1158
|
+
const leaf = c.type === "module_id" ? this.assembleModuleId(c) : c.text;
|
|
1159
|
+
if (out) out += ".";
|
|
1160
|
+
out += leaf;
|
|
1161
|
+
}
|
|
1162
|
+
return out || node.text;
|
|
1163
|
+
}
|
|
1164
|
+
processImport(node, ctx) {
|
|
1165
|
+
let isQualified = false;
|
|
1166
|
+
const items = [];
|
|
1167
|
+
const modulesToImport = [];
|
|
1168
|
+
let currentIsHiding = this.findChildIndexByText(node, "hiding") !== -1;
|
|
1169
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1170
|
+
const child = node.child(i);
|
|
1171
|
+
if (!child) continue;
|
|
1172
|
+
if (child.type === "qualified") {
|
|
1173
|
+
isQualified = true;
|
|
1174
|
+
} else if (child.type === "module" || child.type === "module_id") {
|
|
1175
|
+
const name = this.assembleModuleId(child);
|
|
1176
|
+
if (i > 0 && node.child(i - 1)?.text.trim() === "as") {
|
|
1177
|
+
const lastMod = modulesToImport[modulesToImport.length - 1];
|
|
1178
|
+
if (lastMod) lastMod.alias = name;
|
|
1179
|
+
} else {
|
|
1180
|
+
modulesToImport.push({ name, isHiding: currentIsHiding, node: child });
|
|
1181
|
+
}
|
|
1182
|
+
} else if (child.type === "hiding") {
|
|
1183
|
+
currentIsHiding = true;
|
|
1184
|
+
} else if (child.type === "import_list") {
|
|
1185
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1186
|
+
const item = child.namedChild(j);
|
|
1187
|
+
if (item && item.type !== "hiding") {
|
|
1188
|
+
const itemText = this.stripOuterParens(item.text.trim());
|
|
1189
|
+
if (itemText && itemText !== "hiding" && itemText !== ",") {
|
|
1190
|
+
const paren = itemText.indexOf("(");
|
|
1191
|
+
items.push(paren >= 0 ? itemText.slice(0, paren).trim() : itemText);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
for (const mod of modulesToImport) {
|
|
1198
|
+
if (!mod.name) continue;
|
|
1199
|
+
const importItems = [];
|
|
1200
|
+
if (items.length > 0) {
|
|
1201
|
+
for (const item of items) importItems.push(item);
|
|
1202
|
+
} else {
|
|
1203
|
+
importItems.push("*");
|
|
1204
|
+
}
|
|
1205
|
+
if (mod.alias && mod.alias.length > 0) ctx.importAliases.set(mod.alias, mod.name);
|
|
1206
|
+
const own = mod.name.indexOf(".") < 0 ? mod.name : mod.name.slice(mod.name.lastIndexOf(".") + 1);
|
|
1207
|
+
if (own.length > 0 && !ctx.importAliases.has(own)) ctx.importAliases.set(own, mod.name);
|
|
1208
|
+
ctx.imports.push({
|
|
1209
|
+
module: mod.name,
|
|
1210
|
+
source: mod.name,
|
|
1211
|
+
items: importItems,
|
|
1212
|
+
isTypeOnly: false,
|
|
1213
|
+
range: this.nodeToRange(node),
|
|
1214
|
+
metadata: { isQualified, alias: mod.alias, isHiding: mod.isHiding }
|
|
1215
|
+
});
|
|
1216
|
+
this.pushRel(
|
|
1217
|
+
ctx,
|
|
1218
|
+
ctx.currentModule || "",
|
|
1219
|
+
mod.name,
|
|
1220
|
+
ASTEngine.RelationshipType.Imports,
|
|
1221
|
+
0.9,
|
|
1222
|
+
{
|
|
1223
|
+
isHaskellImport: true
|
|
1224
|
+
},
|
|
1225
|
+
this.lineOf(mod.node)
|
|
1226
|
+
);
|
|
1227
|
+
if (mod.name.indexOf("Scotty") !== -1) ctx.frameworks["Scotty"] = true;
|
|
1228
|
+
if (mod.name.indexOf("Servant") !== -1) ctx.frameworks["Servant"] = true;
|
|
1229
|
+
if (mod.name.indexOf("QuickCheck") !== -1) ctx.frameworks["QuickCheck"] = true;
|
|
1230
|
+
if (mod.name.indexOf("Database.Persist") !== -1) ctx.frameworks["Persistent"] = true;
|
|
1231
|
+
if (mod.name.indexOf("Yesod") !== -1) ctx.frameworks["Yesod"] = true;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
processSignature(node, ctx, currentScope, inLocalBlock = false) {
|
|
1235
|
+
const nameNode = this.findNamedChildByType(node, ["variable"]);
|
|
1236
|
+
if (nameNode && !inLocalBlock) {
|
|
1237
|
+
const idx = node.text.indexOf("::");
|
|
1238
|
+
if (idx !== -1) {
|
|
1239
|
+
ctx.typeSignatures[nameNode.text] = node.text.slice(idx + 2).trim();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (nameNode && currentScope && (currentScope.kind === ASTEngine.SymbolKind.Interface || currentScope.kind === ASTEngine.SymbolKind.Class)) {
|
|
1243
|
+
const methodKey = `${currentScope.name}::${nameNode.text}`;
|
|
1244
|
+
const existingIdx = ctx.functionSymbolIndex[methodKey];
|
|
1245
|
+
if (existingIdx === void 0) {
|
|
1246
|
+
ctx.functionSymbolIndex[methodKey] = ctx.symbols.length;
|
|
1247
|
+
ctx.symbols.push({
|
|
1248
|
+
name: nameNode.text,
|
|
1249
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1250
|
+
range: this.nodeToRange(node),
|
|
1251
|
+
scope: currentScope.name || "",
|
|
1252
|
+
ownerName: currentScope.name,
|
|
1253
|
+
ownerKind: currentScope.kind,
|
|
1254
|
+
visibility: "public",
|
|
1255
|
+
metadata: { haskellSpecific: true, isInterfaceMethod: true }
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
processFunction(node, ctx, currentScope, scopeStack) {
|
|
1261
|
+
if (node.type === "function" && (node.childForFieldName("parameter") || node.childForFieldName("result"))) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
let name = "";
|
|
1265
|
+
const nameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
1266
|
+
if (nameNode && nameNode.type === "variable") {
|
|
1267
|
+
name = nameNode.text;
|
|
1268
|
+
} else if (nameNode && (nameNode.type === "infix" || nameNode.type === "prefix_id")) {
|
|
1269
|
+
const op = this.findNamedChildByType(nameNode, ["operator"]);
|
|
1270
|
+
if (op?.text) {
|
|
1271
|
+
name = op.text;
|
|
1272
|
+
} else {
|
|
1273
|
+
const infixId = this.findNamedChildByType(nameNode, ["infix_id"]);
|
|
1274
|
+
const tv = infixId ? this.findNamedChildByType(infixId, ["variable"]) : void 0;
|
|
1275
|
+
if (tv?.text) name = tv.text;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (!name || name === "=" || name === "|" || node.parent?.type === "signature" || node.parent?.type === "function") {
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
const functionKey = `${currentScope?.name || ""}::${name}`;
|
|
1282
|
+
const existingIndex = ctx.functionSymbolIndex[functionKey];
|
|
1283
|
+
const existing = existingIndex !== void 0 ? ctx.symbols[existingIndex] : void 0;
|
|
1284
|
+
if (existing) {
|
|
1285
|
+
const newRange = this.nodeToRange(node);
|
|
1286
|
+
existing.range.endLine = Math.max(existing.range.endLine, newRange.endLine);
|
|
1287
|
+
if (existing.range.endIndex !== void 0 && newRange.endIndex !== void 0) {
|
|
1288
|
+
existing.range.endIndex = Math.max(existing.range.endIndex, newRange.endIndex);
|
|
1289
|
+
} else if (newRange.endIndex !== void 0) {
|
|
1290
|
+
existing.range.endIndex = newRange.endIndex;
|
|
1291
|
+
}
|
|
1292
|
+
if (existing.complexity !== void 0) {
|
|
1293
|
+
existing.complexity = Math.max(existing.complexity, this.calculateNodeComplexity(node));
|
|
1294
|
+
}
|
|
1295
|
+
ctx.currentFunction = name;
|
|
1296
|
+
scopeStack.push({
|
|
1297
|
+
node,
|
|
1298
|
+
name,
|
|
1299
|
+
kind: existing.kind,
|
|
1300
|
+
locals: ctx.localScopes.frameLocals.get(node.id),
|
|
1301
|
+
isFrame: true
|
|
1302
|
+
});
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
const typeSignature = ctx.typeSignatures[name];
|
|
1306
|
+
let isHigherOrder = false;
|
|
1307
|
+
let returnType = void 0;
|
|
1308
|
+
if (typeSignature && typeof typeSignature === "string") {
|
|
1309
|
+
isHigherOrder = this.hasArrowInParens(typeSignature);
|
|
1310
|
+
const parts = this.splitOnArrows(typeSignature);
|
|
1311
|
+
if (parts.length > 0) returnType = parts[parts.length - 1].trim();
|
|
1312
|
+
}
|
|
1313
|
+
if (isHigherOrder) ctx.patterns["Higher-Order Functions"] = true;
|
|
1314
|
+
const kind = currentScope && (currentScope.kind === ASTEngine.SymbolKind.Interface || currentScope.kind === ASTEngine.SymbolKind.Class) ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function;
|
|
1315
|
+
ctx.symbols.push({
|
|
1316
|
+
name,
|
|
1317
|
+
kind,
|
|
1318
|
+
range: this.nodeToRange(node),
|
|
1319
|
+
scope: currentScope?.name || "",
|
|
1320
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1321
|
+
ownerKind: currentScope?.kind,
|
|
1322
|
+
visibility: "public",
|
|
1323
|
+
returnType,
|
|
1324
|
+
complexity: this.calculateNodeComplexity(node),
|
|
1325
|
+
metadata: { typeSignature, isHigherOrder, haskellSpecific: true }
|
|
1326
|
+
});
|
|
1327
|
+
ctx.functionSymbolIndex[functionKey] = ctx.symbols.length - 1;
|
|
1328
|
+
ctx.currentFunction = name;
|
|
1329
|
+
scopeStack.push({ node, name, kind, locals: ctx.localScopes.frameLocals.get(node.id), isFrame: true });
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
// A type synonym is a first-class declaration — `type Result = [Int64]` names
|
|
1333
|
+
// a type that other code can annotate with, so omitting it left every use of
|
|
1334
|
+
// the name with nothing to resolve to. The grammar's node is spelled
|
|
1335
|
+
// `type_synomym` (the upstream typo is in the node NAME, not this comment), and
|
|
1336
|
+
// its shape is `name` + right-hand side, so the name is the first `name` child.
|
|
1337
|
+
processTypeSynonym(node, ctx, scopeStack) {
|
|
1338
|
+
ctx.patterns["Type Synonyms"] = true;
|
|
1339
|
+
const nameNode = this.findNamedChildByType(node, ["name"]);
|
|
1340
|
+
const name = nameNode?.text ?? "";
|
|
1341
|
+
if (!name) return false;
|
|
1342
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
1343
|
+
ctx.symbols.push({
|
|
1344
|
+
name,
|
|
1345
|
+
kind: ASTEngine.SymbolKind.Type,
|
|
1346
|
+
range: this.nodeToRange(node),
|
|
1347
|
+
scope: currentScope?.name || "",
|
|
1348
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1349
|
+
visibility: "public",
|
|
1350
|
+
metadata: { haskellSpecific: true, isTypeSynonym: true }
|
|
1351
|
+
});
|
|
1352
|
+
if (currentScope) {
|
|
1353
|
+
this.pushRel(
|
|
1354
|
+
ctx,
|
|
1355
|
+
currentScope.name,
|
|
1356
|
+
name,
|
|
1357
|
+
ASTEngine.RelationshipType.Contains,
|
|
1358
|
+
1,
|
|
1359
|
+
{ evidenceClass: "structural" },
|
|
1360
|
+
this.lineOf(node)
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
if (nameNode) {
|
|
1364
|
+
for (const leaf of this.collectTypeNameLeafNodes(node, nameNode)) {
|
|
1365
|
+
if (leaf.text.trim() === name) continue;
|
|
1366
|
+
this.pushRel(
|
|
1367
|
+
ctx,
|
|
1368
|
+
name,
|
|
1369
|
+
leaf.text.trim(),
|
|
1370
|
+
ASTEngine.RelationshipType.Uses,
|
|
1371
|
+
1,
|
|
1372
|
+
{ evidenceClass: "declared_type" },
|
|
1373
|
+
this.lineOf(leaf)
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
// Type-name leaves reachable in a subtree, skipping the subtree rooted at
|
|
1380
|
+
// `skip`, returned as NODES so a caller can cite each one's own line.
|
|
1381
|
+
// Haskell type expressions nest arbitrarily (`Map.Map ByteString ByteString`
|
|
1382
|
+
// is apply → qualified(name) + name), so this walks rather than reading one
|
|
1383
|
+
// level, and it takes `name` and `constructor` only: a `variable` in a type
|
|
1384
|
+
// position is a type PARAMETER (`f a`), not a type, and publishing `a` as a
|
|
1385
|
+
// use of some type named `a` is exactly the fabrication the declared-type
|
|
1386
|
+
// contract forbids.
|
|
1387
|
+
collectTypeNameLeafNodes(node, skip, out = []) {
|
|
1388
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1389
|
+
const c = node.namedChild(i);
|
|
1390
|
+
if (!c || skip !== null && c.id === skip.id) continue;
|
|
1391
|
+
if (c.type === "name" || c.type === "constructor") {
|
|
1392
|
+
if (c.text.trim() && !out.some((o) => o.text === c.text)) out.push(c);
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
this.collectTypeNameLeafNodes(c, null, out);
|
|
1396
|
+
}
|
|
1397
|
+
return out;
|
|
1398
|
+
}
|
|
1399
|
+
// A pattern synonym is a first-class top-level DECLARATION — `pattern Head a b =
|
|
1400
|
+
// expr` names a constructor-shaped value the module defines, so omitting it left
|
|
1401
|
+
// every use of the name with nothing to resolve to, exactly as omitting a type
|
|
1402
|
+
// synonym did. It is not exotic: GHC's own sources carry 1,094 of them across
|
|
1403
|
+
// 261 files (`script/m14/tmp/hs-pattern-synonyms.ts`, all 14,038 admitted files),
|
|
1404
|
+
// and 94 export entries name one, so those `exports` edges had a target nothing
|
|
1405
|
+
// declared. Found by the `haskell/exports-and-ffi` fixture, which the M14 runner
|
|
1406
|
+
// refused with `export(s) [":|"] do not resolve to a top-level symbol`.
|
|
1407
|
+
//
|
|
1408
|
+
// MEASURED shape distribution over those 1,094 nodes: 449 carry a readable name
|
|
1409
|
+
// (421 a `signature` child, 28 a bare `equation`), and 645 are VIEW PATTERNS
|
|
1410
|
+
// (`pattern P x <- Q x where P x = Q x`) whose name the previous reader could not
|
|
1411
|
+
// reach at all. The name is ALWAYS the head of the equation's left-hand side, so
|
|
1412
|
+
// one rule covers every shape:
|
|
1413
|
+
//
|
|
1414
|
+
// pattern_synonym -> pattern + signature -> prefix_id -> constructor_operator (`:|`)
|
|
1415
|
+
// pattern_synonym -> pattern + signature -> constructor (`Zero`)
|
|
1416
|
+
// pattern_synonym -> pattern + equation -> apply(apply(constructor, x), y) (`OptMResult`)
|
|
1417
|
+
// pattern_synonym -> pattern + equation -> apply -> constructor (a view pattern)
|
|
1418
|
+
// pattern_synonym -> pattern + equation -> infix -> constructor_operator (`:|`)
|
|
1419
|
+
//
|
|
1420
|
+
// The symbol kind is `constructor`, not `function`: a pattern synonym is built
|
|
1421
|
+
// and matched like a data constructor (`instantiates` already targets it through
|
|
1422
|
+
// the `apply`-head rule), and calling it a function would put a name the module
|
|
1423
|
+
// never defines into the FP-family `calls` set.
|
|
1424
|
+
processPatternSynonym(node, ctx) {
|
|
1425
|
+
ctx.patterns["Pattern Synonyms"] = true;
|
|
1426
|
+
const sig = this.findNamedChildByType(node, ["signature"]);
|
|
1427
|
+
const eq = this.findNamedChildByType(node, ["equation"]);
|
|
1428
|
+
let name;
|
|
1429
|
+
let anchor;
|
|
1430
|
+
if (sig) {
|
|
1431
|
+
const head = this.findNamedChildByType(sig, ["prefix_id", "infix_id", "constructor", "constructor_operator", "variable"]);
|
|
1432
|
+
if (head) {
|
|
1433
|
+
name = head.type === "prefix_id" || head.type === "infix_id" ? this.findNamedChildByType(head, ["constructor_operator", "operator", "constructor", "variable"])?.text : head.text;
|
|
1434
|
+
anchor = head;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (!name && eq) {
|
|
1438
|
+
for (let i = 0; i < eq.namedChildCount; i++) {
|
|
1439
|
+
const c = eq.namedChild(i);
|
|
1440
|
+
if (!c) continue;
|
|
1441
|
+
if (c.type === "constructor" && c.text) {
|
|
1442
|
+
name = c.text;
|
|
1443
|
+
anchor = c;
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
if (c.type === "infix") {
|
|
1447
|
+
const op = this.findNamedChildByType(c, ["constructor_operator", "operator"]);
|
|
1448
|
+
if (op?.text) {
|
|
1449
|
+
name = op.text;
|
|
1450
|
+
anchor = op;
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
if (c.type === "apply") {
|
|
1456
|
+
const head = this.haskellAppliedHeadName(c);
|
|
1457
|
+
if (head) {
|
|
1458
|
+
name = head;
|
|
1459
|
+
anchor = this.findAppliedHeadNode(c) ?? c;
|
|
1460
|
+
break;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (!name || !anchor) return false;
|
|
1466
|
+
return this.publishPatternSynonym(name, node, anchor, ctx);
|
|
1467
|
+
}
|
|
1468
|
+
publishPatternSynonym(name, declNode, anchor, ctx) {
|
|
1469
|
+
if (!name || name === " ") return false;
|
|
1470
|
+
const key = `${ctx.currentModule ?? ""}::${name}`;
|
|
1471
|
+
const existingIdx = ctx.functionSymbolIndex[key];
|
|
1472
|
+
if (existingIdx !== void 0) {
|
|
1473
|
+
const existing = ctx.symbols[existingIdx];
|
|
1474
|
+
if (existing) {
|
|
1475
|
+
const declRange = this.nodeToRange(declNode);
|
|
1476
|
+
existing.range.endLine = Math.max(existing.range.endLine, declRange.endLine);
|
|
1477
|
+
if (existing.range.endIndex !== void 0 && declRange.endIndex !== void 0) {
|
|
1478
|
+
existing.range.endIndex = Math.max(existing.range.endIndex, declRange.endIndex);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1483
|
+
const range = this.nodeToRange(declNode);
|
|
1484
|
+
range.startLine = anchor.startPosition.row + 1;
|
|
1485
|
+
range.startColumn = anchor.startPosition.column + 1;
|
|
1486
|
+
ctx.symbols.push({
|
|
1487
|
+
name,
|
|
1488
|
+
kind: ASTEngine.SymbolKind.Constructor,
|
|
1489
|
+
range,
|
|
1490
|
+
scope: ctx.currentModule ?? "",
|
|
1491
|
+
ownerName: ctx.currentModule,
|
|
1492
|
+
ownerKind: ASTEngine.SymbolKind.Type,
|
|
1493
|
+
visibility: "public",
|
|
1494
|
+
metadata: { haskellSpecific: true, isPatternSynonym: true }
|
|
1495
|
+
});
|
|
1496
|
+
ctx.functionSymbolIndex[key] = ctx.symbols.length - 1;
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
/** The node carrying the head name of an applied type/expression spine. */
|
|
1500
|
+
findAppliedHeadNode(node) {
|
|
1501
|
+
let cur = node;
|
|
1502
|
+
let guard = 0;
|
|
1503
|
+
while (cur && guard++ < 64) {
|
|
1504
|
+
if (cur.type === "apply" || cur.type === "parens" || cur.type === "strict") {
|
|
1505
|
+
cur = cur.namedChildCount > 0 ? cur.namedChild(0) : null;
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
break;
|
|
1509
|
+
}
|
|
1510
|
+
return cur;
|
|
1511
|
+
}
|
|
1512
|
+
// FFI declarations. `foreign import capi "libpq-fe.h PQresultStatus"`
|
|
1513
|
+
// parses as foreign_import → calling_convention, entity (the C symbol),
|
|
1514
|
+
// signature → variable (the HASKELL name) → function (its type). Nothing in
|
|
1515
|
+
// the tree above the module carried the Haskell-side binding, so a file whose
|
|
1516
|
+
// entire content is FFI published one symbol: the module. postgrest's
|
|
1517
|
+
// `Hasql/LibPq14/Ffi.hs` is exactly that file — 6 declarations, 0 published.
|
|
1518
|
+
// The Haskell name is `signature → variable`; the C entity is kept as metadata
|
|
1519
|
+
// and as a `references` edge, because that string is how a reader finds the
|
|
1520
|
+
// symbol in the C headers and nothing else in the graph points at it.
|
|
1521
|
+
processForeignImport(node, ctx, scopeStack) {
|
|
1522
|
+
ctx.patterns["Foreign Imports"] = true;
|
|
1523
|
+
const sig = this.findNamedChildByType(node, ["signature"]);
|
|
1524
|
+
const varNode = sig ? this.findNamedChildByType(sig, ["variable"]) : null;
|
|
1525
|
+
const name = varNode?.text ?? "";
|
|
1526
|
+
if (!name) return false;
|
|
1527
|
+
const convention = this.findNamedChildByType(node, ["calling_convention"])?.text ?? "";
|
|
1528
|
+
const entity = this.foreignEntityName(this.readForeignEntity(node) ?? "", convention);
|
|
1529
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
1530
|
+
ctx.symbols.push({
|
|
1531
|
+
name,
|
|
1532
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
1533
|
+
range: this.nodeToRange(node),
|
|
1534
|
+
scope: currentScope?.name || "",
|
|
1535
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1536
|
+
visibility: "public",
|
|
1537
|
+
metadata: {
|
|
1538
|
+
haskellSpecific: true,
|
|
1539
|
+
isForeignImport: true,
|
|
1540
|
+
...entity ? { foreignEntity: entity } : {},
|
|
1541
|
+
...convention ? { callingConvention: convention } : {}
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
const owner = currentScope?.name || ctx.currentModule;
|
|
1545
|
+
if (owner) {
|
|
1546
|
+
this.pushRel(
|
|
1547
|
+
ctx,
|
|
1548
|
+
owner,
|
|
1549
|
+
name,
|
|
1550
|
+
ASTEngine.RelationshipType.Contains,
|
|
1551
|
+
1,
|
|
1552
|
+
// The entity travels with the edge as well as the symbol: the cited line
|
|
1553
|
+
// is the `foreign import` line, which spells the C name
|
|
1554
|
+
// (`WaitForSingleObject`) while the Haskell binding adds an underscore
|
|
1555
|
+
// (`waitForSingleObject_`). A consumer — or a gate — checking that the
|
|
1556
|
+
// target name appears at the cited line has to be able to find the
|
|
1557
|
+
// spelling that IS there.
|
|
1558
|
+
{ evidenceClass: "structural", isForeignImport: true, ...entity ? { foreignEntity: entity } : {} },
|
|
1559
|
+
this.lineOf(node)
|
|
1560
|
+
);
|
|
1561
|
+
if (entity) {
|
|
1562
|
+
this.pushRel(
|
|
1563
|
+
ctx,
|
|
1564
|
+
name,
|
|
1565
|
+
entity,
|
|
1566
|
+
ASTEngine.RelationshipType.References,
|
|
1567
|
+
0.9,
|
|
1568
|
+
{ evidenceClass: "foreign_symbol", isForeignEntity: true },
|
|
1569
|
+
this.lineOf(node)
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return true;
|
|
1574
|
+
}
|
|
1575
|
+
processData(node, ctx, scopeStack) {
|
|
1576
|
+
ctx.patterns["Algebraic Data Types"] = true;
|
|
1577
|
+
let name = "";
|
|
1578
|
+
const nameNode = this.findNamedChildByType(node, ["name"]);
|
|
1579
|
+
if (nameNode) name = nameNode.text;
|
|
1580
|
+
else {
|
|
1581
|
+
const dataIdx = this.findChildIndexByText(node, ["data", "newtype"]);
|
|
1582
|
+
if (dataIdx !== -1 && node.childCount > dataIdx + 1) {
|
|
1583
|
+
name = node.child(dataIdx + 1)?.text || "";
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (!name) return false;
|
|
1587
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
1588
|
+
const typeRange = this.nodeToRange(node);
|
|
1589
|
+
if (nameNode) {
|
|
1590
|
+
typeRange.startLine = nameNode.startPosition.row + 1;
|
|
1591
|
+
typeRange.startColumn = nameNode.startPosition.column;
|
|
1592
|
+
}
|
|
1593
|
+
ctx.symbols.push({
|
|
1594
|
+
name,
|
|
1595
|
+
kind: ASTEngine.SymbolKind.Type,
|
|
1596
|
+
range: typeRange,
|
|
1597
|
+
scope: currentScope?.name || "",
|
|
1598
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1599
|
+
visibility: "public",
|
|
1600
|
+
metadata: { haskellSpecific: true, isData: node.type === "data_type" }
|
|
1601
|
+
});
|
|
1602
|
+
const ctorNodes = [];
|
|
1603
|
+
for (const group of ["data_constructors", "gadt_constructors"]) {
|
|
1604
|
+
const g = this.findNamedChildByType(node, [group]);
|
|
1605
|
+
if (!g) continue;
|
|
1606
|
+
for (const c of this.collectNamedChildrenByType(g, ["data_constructor", "gadt_constructor"])) {
|
|
1607
|
+
if (!ctorNodes.includes(c)) ctorNodes.push(c);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
for (const c of this.collectNamedChildrenByType(node, ["newtype_constructor", "data_constructor", "gadt_constructor"])) {
|
|
1611
|
+
if (!ctorNodes.includes(c)) ctorNodes.push(c);
|
|
1612
|
+
}
|
|
1613
|
+
for (const cNode of ctorNodes) {
|
|
1614
|
+
const cName = this.extractConstructorName(cNode);
|
|
1615
|
+
if (cName) {
|
|
1616
|
+
ctx.symbols.push({
|
|
1617
|
+
name: cName,
|
|
1618
|
+
kind: ASTEngine.SymbolKind.Constructor,
|
|
1619
|
+
range: this.nodeToRange(cNode),
|
|
1620
|
+
scope: name,
|
|
1621
|
+
ownerName: name,
|
|
1622
|
+
ownerKind: ASTEngine.SymbolKind.Type,
|
|
1623
|
+
visibility: "public",
|
|
1624
|
+
metadata: { isDataConstructor: true }
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
const recordNode = this.findNamedChildByType(cNode, ["record"]);
|
|
1628
|
+
if (recordNode) {
|
|
1629
|
+
const fields = this.collectFields(recordNode);
|
|
1630
|
+
for (const field of fields) {
|
|
1631
|
+
const fNameNode = this.findNamedChildByType(field, ["field_name"]);
|
|
1632
|
+
const varNode = fNameNode ? fNameNode.namedChild(0) : null;
|
|
1633
|
+
if (varNode?.text) {
|
|
1634
|
+
ctx.symbols.push({
|
|
1635
|
+
name: varNode.text,
|
|
1636
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1637
|
+
range: this.nodeToRange(field),
|
|
1638
|
+
scope: name,
|
|
1639
|
+
ownerName: name,
|
|
1640
|
+
ownerKind: ASTEngine.SymbolKind.Type,
|
|
1641
|
+
visibility: "public",
|
|
1642
|
+
metadata: { haskellSpecific: true, isRecordField: true }
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const deriving = this.findNamedChildByType(node, ["deriving", "deriving_strategy"]);
|
|
1649
|
+
if (deriving) {
|
|
1650
|
+
const derived = [];
|
|
1651
|
+
const derivedNodes = [];
|
|
1652
|
+
const grab = (nn) => {
|
|
1653
|
+
for (let i = 0; i < nn.namedChildCount; i++) {
|
|
1654
|
+
const c = nn.namedChild(i);
|
|
1655
|
+
if (!c) continue;
|
|
1656
|
+
if (c.type === "name") {
|
|
1657
|
+
derived.push(c.text);
|
|
1658
|
+
derivedNodes.push(c);
|
|
1659
|
+
} else if (c.type === "qualified") {
|
|
1660
|
+
derived.push(c.text);
|
|
1661
|
+
derivedNodes.push(c);
|
|
1662
|
+
} else if (c.type === "apply") {
|
|
1663
|
+
const h = c.namedChildCount > 0 ? c.namedChild(0) : null;
|
|
1664
|
+
if (h?.text) {
|
|
1665
|
+
derived.push(h.text);
|
|
1666
|
+
derivedNodes.push(h);
|
|
1667
|
+
}
|
|
1668
|
+
} else if (c.type === "parens") grab(c);
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
const tuple = this.findNamedChildByType(deriving, ["tuple"]);
|
|
1672
|
+
if (tuple) grab(tuple);
|
|
1673
|
+
grab(deriving);
|
|
1674
|
+
for (let i = 0; i < derived.length; i++) {
|
|
1675
|
+
this.pushRel(
|
|
1676
|
+
ctx,
|
|
1677
|
+
name,
|
|
1678
|
+
derived[i],
|
|
1679
|
+
ASTEngine.RelationshipType.Implements,
|
|
1680
|
+
1,
|
|
1681
|
+
{ derived: true },
|
|
1682
|
+
this.lineOf(derivedNodes[i])
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
scopeStack.push({ node, name, kind: ASTEngine.SymbolKind.Type });
|
|
1687
|
+
return true;
|
|
1688
|
+
}
|
|
1689
|
+
// data_constructor → prefix → constructor "Node" | record → constructor "Rect"
|
|
1690
|
+
extractConstructorName(node) {
|
|
1691
|
+
const prefix = this.findNamedChildByType(node, ["prefix"]);
|
|
1692
|
+
if (prefix) {
|
|
1693
|
+
const con = this.findNamedChildByType(prefix, ["constructor", "name"]);
|
|
1694
|
+
if (con?.text) return con.text;
|
|
1695
|
+
}
|
|
1696
|
+
const record = this.findNamedChildByType(node, ["record"]);
|
|
1697
|
+
if (record) {
|
|
1698
|
+
const con = this.findNamedChildByType(record, ["constructor"]);
|
|
1699
|
+
if (con?.text) return con.text;
|
|
1700
|
+
}
|
|
1701
|
+
const direct = this.findNamedChildByType(node, ["constructor", "name"]);
|
|
1702
|
+
if (direct?.text) return direct.text;
|
|
1703
|
+
return void 0;
|
|
1704
|
+
}
|
|
1705
|
+
processClass(node, ctx, scopeStack) {
|
|
1706
|
+
ctx.patterns["Type Classes"] = true;
|
|
1707
|
+
let name = "";
|
|
1708
|
+
const nameNode = this.findNamedChildByType(node, ["name"]);
|
|
1709
|
+
if (nameNode) name = nameNode.text;
|
|
1710
|
+
else {
|
|
1711
|
+
const classIdx = this.findChildIndexByText(node, "class");
|
|
1712
|
+
if (classIdx !== -1 && node.childCount > classIdx + 1) {
|
|
1713
|
+
name = node.child(classIdx + 1)?.text || "";
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (!name) return false;
|
|
1717
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
1718
|
+
const context = this.findNamedChildByType(node, ["context"]);
|
|
1719
|
+
if (context) {
|
|
1720
|
+
const superNames = /* @__PURE__ */ new Set();
|
|
1721
|
+
const superNodes = /* @__PURE__ */ new Map();
|
|
1722
|
+
const stack = [context];
|
|
1723
|
+
while (stack.length > 0) {
|
|
1724
|
+
const x = stack.pop();
|
|
1725
|
+
if (x.type === "apply") {
|
|
1726
|
+
let head = x;
|
|
1727
|
+
while (head && head.type === "apply") head = head.namedChildren[0] ?? null;
|
|
1728
|
+
if (head && (head.type === "name" || head.type === "constructor" || head.type === "qualified")) {
|
|
1729
|
+
superNames.add(head.text);
|
|
1730
|
+
if (!superNodes.has(head.text)) superNodes.set(head.text, head);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
for (const c of x.namedChildren) if (c) stack.push(c);
|
|
1734
|
+
}
|
|
1735
|
+
for (const sn of superNames) {
|
|
1736
|
+
if (sn === name) continue;
|
|
1737
|
+
this.pushRel(
|
|
1738
|
+
ctx,
|
|
1739
|
+
name,
|
|
1740
|
+
sn,
|
|
1741
|
+
ASTEngine.RelationshipType.Extends,
|
|
1742
|
+
1,
|
|
1743
|
+
{ isSuperclass: true },
|
|
1744
|
+
this.lineOf(superNodes.get(sn))
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
const classRange = this.nodeToRange(node);
|
|
1749
|
+
if (nameNode) {
|
|
1750
|
+
classRange.startLine = nameNode.startPosition.row + 1;
|
|
1751
|
+
classRange.startColumn = nameNode.startPosition.column;
|
|
1752
|
+
}
|
|
1753
|
+
ctx.symbols.push({
|
|
1754
|
+
name,
|
|
1755
|
+
kind: ASTEngine.SymbolKind.Interface,
|
|
1756
|
+
range: classRange,
|
|
1757
|
+
scope: currentScope?.name || "",
|
|
1758
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1759
|
+
visibility: "public",
|
|
1760
|
+
metadata: { haskellSpecific: true }
|
|
1761
|
+
});
|
|
1762
|
+
scopeStack.push({ node, name, kind: ASTEngine.SymbolKind.Interface });
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* M60 (#924, haskell E half): the TYPE NAME at the head of an applied type.
|
|
1767
|
+
*
|
|
1768
|
+
* An instance declaration such as instance GArbitrary (M1 i c f) declares an
|
|
1769
|
+
* implementation FOR the type M1 i c f, whose name is its head type constructor
|
|
1770
|
+
* M1; the published symbol was the whole application (M1 i c), i.e. a payload
|
|
1771
|
+
* where a name belongs — measured as 2 distinct strings over 6 occurrences on the
|
|
1772
|
+
* census's own haskell samples (script/m14/tmp/c924-name-surface-probe.ts). Walks
|
|
1773
|
+
* the APPLICATION SPINE (apply / parens / strict) to the leftmost leaf and returns
|
|
1774
|
+
* its text verbatim (a qualified head keeps its dots). The class head already
|
|
1775
|
+
* takes this same left-spine descent; only the instance TARGET did not.
|
|
1776
|
+
*/
|
|
1777
|
+
haskellAppliedHeadName(node) {
|
|
1778
|
+
let cur = node;
|
|
1779
|
+
let guard = 0;
|
|
1780
|
+
while (cur && guard++ < 64) {
|
|
1781
|
+
if (cur.type === "apply" || cur.type === "parens" || cur.type === "strict") {
|
|
1782
|
+
cur = cur.namedChildCount > 0 ? cur.namedChild(0) : null;
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
return cur?.text || void 0;
|
|
1788
|
+
}
|
|
1789
|
+
processInstance(node, ctx, scopeStack) {
|
|
1790
|
+
let className = "";
|
|
1791
|
+
let targetType = "";
|
|
1792
|
+
let targetNode = null;
|
|
1793
|
+
const classNode = this.findNamedChildByType(node, ["name"]);
|
|
1794
|
+
const tpNode = this.findNamedChildByType(node, ["type_patterns"]);
|
|
1795
|
+
if (classNode?.text) className = classNode.text;
|
|
1796
|
+
if (tpNode) {
|
|
1797
|
+
const direct = this.findNamedChildByType(tpNode, ["name", "constructor"]);
|
|
1798
|
+
if (direct?.text) {
|
|
1799
|
+
targetType = direct.text;
|
|
1800
|
+
targetNode = direct;
|
|
1801
|
+
} else {
|
|
1802
|
+
const list = this.findNamedChildByType(tpNode, ["list"]);
|
|
1803
|
+
if (list) {
|
|
1804
|
+
targetType = "[]";
|
|
1805
|
+
} else {
|
|
1806
|
+
const parens = this.findNamedChildByType(tpNode, ["parens"]);
|
|
1807
|
+
if (parens) {
|
|
1808
|
+
const ap = this.findNamedChildByType(parens, ["apply"]);
|
|
1809
|
+
const first = ap?.namedChildCount ? ap.namedChild(0) : null;
|
|
1810
|
+
if (first) {
|
|
1811
|
+
targetType = this.haskellAppliedHeadName(first) ?? first.text;
|
|
1812
|
+
const head = first.namedChildren.find((c) => c.type === "name" || c.type === "constructor");
|
|
1813
|
+
targetNode = head ?? first;
|
|
1814
|
+
}
|
|
1815
|
+
} else {
|
|
1816
|
+
const firstPattern = tpNode.namedChildCount > 0 ? tpNode.namedChild(0) : null;
|
|
1817
|
+
if (firstPattern?.type === "qualified" && firstPattern.text) {
|
|
1818
|
+
targetType = firstPattern.text;
|
|
1819
|
+
targetNode = firstPattern;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (!className || !targetType) {
|
|
1826
|
+
const named = [];
|
|
1827
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1828
|
+
const c = node.namedChild(i);
|
|
1829
|
+
if (c && (c.type === "name" || c.type === "constructor")) {
|
|
1830
|
+
named.push(c.text);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (!className && named.length >= 1) className = named[0];
|
|
1834
|
+
if (!targetType && named.length >= 2) targetType = named[1];
|
|
1835
|
+
}
|
|
1836
|
+
const name = targetType || className;
|
|
1837
|
+
if (!name || name === " ") return false;
|
|
1838
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
1839
|
+
const instRange = this.nodeToRange(node);
|
|
1840
|
+
if (targetNode) {
|
|
1841
|
+
instRange.startLine = targetNode.startPosition.row + 1;
|
|
1842
|
+
instRange.startColumn = targetNode.startPosition.column;
|
|
1843
|
+
}
|
|
1844
|
+
ctx.symbols.push({
|
|
1845
|
+
name,
|
|
1846
|
+
// An INSTANCE, not a class. It is named after the type it instantiates,
|
|
1847
|
+
// and `contains` is derived from symbol ownership, so publishing it as
|
|
1848
|
+
// a Class asserted this module DECLARES that type. The scope pushed for
|
|
1849
|
+
// the body below stays Class on purpose -- method resolution reads it.
|
|
1850
|
+
kind: ASTEngine.SymbolKind.Instance,
|
|
1851
|
+
range: instRange,
|
|
1852
|
+
scope: currentScope?.name || "",
|
|
1853
|
+
ownerName: currentScope?.name || ctx.currentModule,
|
|
1854
|
+
visibility: "public",
|
|
1855
|
+
metadata: { haskellSpecific: true, className, targetType }
|
|
1856
|
+
});
|
|
1857
|
+
if (className && targetType) {
|
|
1858
|
+
this.pushRel(
|
|
1859
|
+
ctx,
|
|
1860
|
+
targetType,
|
|
1861
|
+
className,
|
|
1862
|
+
ASTEngine.RelationshipType.Implements,
|
|
1863
|
+
1,
|
|
1864
|
+
{ isInstance: true },
|
|
1865
|
+
this.lineOf(classNode)
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
const classM = ctx.classMethods.get(className);
|
|
1869
|
+
if (classM && classM.size > 0 && targetType) {
|
|
1870
|
+
const decls = this.findNamedChildByType(node, ["instance_declarations"]);
|
|
1871
|
+
const bodies = [];
|
|
1872
|
+
if (decls) {
|
|
1873
|
+
for (const d of this.collectNamedChildrenByType(decls, ["function", "bind"])) bodies.push(d);
|
|
1874
|
+
}
|
|
1875
|
+
for (const d of this.collectNamedChildrenByType(node, ["function", "bind"])) {
|
|
1876
|
+
if (!bodies.includes(d)) bodies.push(d);
|
|
1877
|
+
}
|
|
1878
|
+
for (const b of bodies) {
|
|
1879
|
+
const mName = b.namedChildCount > 0 ? b.namedChild(0)?.text : void 0;
|
|
1880
|
+
if (!mName || !classM.has(mName)) continue;
|
|
1881
|
+
this.pushRel(
|
|
1882
|
+
ctx,
|
|
1883
|
+
`${targetType}.${mName}`,
|
|
1884
|
+
`${className}.${mName}`,
|
|
1885
|
+
ASTEngine.RelationshipType.Override,
|
|
1886
|
+
1,
|
|
1887
|
+
{ isInstanceMethod: true, evidenceClass: "structural" },
|
|
1888
|
+
this.lineOf(b)
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
scopeStack.push({ node, name: className || name, kind: ASTEngine.SymbolKind.Class });
|
|
1893
|
+
return true;
|
|
1894
|
+
}
|
|
1895
|
+
// ==========================================================================
|
|
1896
|
+
// Call extraction — apply nodes in body position.
|
|
1897
|
+
// ==========================================================================
|
|
1898
|
+
extractApplyCall(node, ctx, scopeStack) {
|
|
1899
|
+
const head = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
1900
|
+
if (!head) return;
|
|
1901
|
+
const fn = ctx.currentFunction;
|
|
1902
|
+
if (!fn) return;
|
|
1903
|
+
if (head.type === "constructor" || head.type === "prefix") {
|
|
1904
|
+
if (this.isPatternPosition(node)) return;
|
|
1905
|
+
this.pushRel(
|
|
1906
|
+
ctx,
|
|
1907
|
+
fn,
|
|
1908
|
+
head.text,
|
|
1909
|
+
ASTEngine.RelationshipType.Instantiates,
|
|
1910
|
+
0.9,
|
|
1911
|
+
{
|
|
1912
|
+
haskellSpecific: true,
|
|
1913
|
+
isConstructorApplication: true
|
|
1914
|
+
},
|
|
1915
|
+
this.lineOf(head)
|
|
1916
|
+
);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (head.type === "qualified") {
|
|
1920
|
+
const mod = this.findNamedChildByType(head, ["module"]);
|
|
1921
|
+
const modText = mod ? this.stripTrailingDot(mod.text) : "";
|
|
1922
|
+
const ctor = this.findNamedChildByType(head, ["constructor"]);
|
|
1923
|
+
if (modText && ctor?.text) {
|
|
1924
|
+
if (!this.isPatternPosition(node)) {
|
|
1925
|
+
this.pushRel(
|
|
1926
|
+
ctx,
|
|
1927
|
+
fn,
|
|
1928
|
+
`${modText}.${ctor.text}`,
|
|
1929
|
+
ASTEngine.RelationshipType.Instantiates,
|
|
1930
|
+
0.9,
|
|
1931
|
+
{
|
|
1932
|
+
haskellSpecific: true,
|
|
1933
|
+
isQualifiedConstructorApplication: true
|
|
1934
|
+
},
|
|
1935
|
+
this.lineOf(head)
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const func = this.findNamedChildByType(head, ["variable"]);
|
|
1941
|
+
if (modText && func?.text) {
|
|
1942
|
+
this.pushRel(
|
|
1943
|
+
ctx,
|
|
1944
|
+
fn,
|
|
1945
|
+
`${modText}.${func.text}`,
|
|
1946
|
+
ASTEngine.RelationshipType.Calls,
|
|
1947
|
+
0.9,
|
|
1948
|
+
{
|
|
1949
|
+
haskellSpecific: true,
|
|
1950
|
+
isQualifiedCall: true
|
|
1951
|
+
},
|
|
1952
|
+
this.lineOf(head)
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
if (head.type === "variable") {
|
|
1958
|
+
const target = this.resolveCallableTarget(head.text, scopeStack, ctx);
|
|
1959
|
+
if (target !== void 0) {
|
|
1960
|
+
this.pushRel(
|
|
1961
|
+
ctx,
|
|
1962
|
+
fn,
|
|
1963
|
+
target,
|
|
1964
|
+
ASTEngine.RelationshipType.Calls,
|
|
1965
|
+
0.9,
|
|
1966
|
+
{
|
|
1967
|
+
haskellSpecific: true,
|
|
1968
|
+
context: "local function call"
|
|
1969
|
+
},
|
|
1970
|
+
this.lineOf(head)
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
isPatternPosition(node) {
|
|
1976
|
+
let cur = node.parent;
|
|
1977
|
+
let prev = node;
|
|
1978
|
+
while (cur) {
|
|
1979
|
+
if (cur.type === "patterns") return true;
|
|
1980
|
+
if (cur.type === "alternative") return true;
|
|
1981
|
+
if (cur.type === "pattern_guard") {
|
|
1982
|
+
if (prev && prev.id === cur.namedChild(0)?.id) return true;
|
|
1983
|
+
} else if (cur.type === "match" || cur.type === "do" || cur.type === "class" || cur.type === "instance") {
|
|
1984
|
+
return false;
|
|
1985
|
+
} else if (cur.type === "function" || cur.type === "bind") {
|
|
1986
|
+
if (prev.type === "match" || prev.type === "patterns") return false;
|
|
1987
|
+
if (prev.id === cur.namedChild(0)?.id) return true;
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
prev = cur;
|
|
1991
|
+
cur = cur.parent;
|
|
1992
|
+
}
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
// =========================================================================
|
|
1996
|
+
// Infix handling: backtick calls, $ application, and pattern marking.
|
|
1997
|
+
// HSD1/HSD2 (probe 2026-09-03): infix_id is a backtick call (`x `elem2` xs`);
|
|
1998
|
+
// operator "$" is function application — Haskell's most common idiom. Both
|
|
1999
|
+
// were previously dropped entirely.
|
|
2000
|
+
// =========================================================================
|
|
2001
|
+
processInfix(node, ctx, scopeStack) {
|
|
2002
|
+
const opNode = this.findNamedChildByType(node, ["operator"]);
|
|
2003
|
+
const infixId = this.findNamedChildByType(node, ["infix_id"]);
|
|
2004
|
+
if (infixId) {
|
|
2005
|
+
const tickedCtor = this.findNamedChildByType(infixId, ["constructor"]);
|
|
2006
|
+
if (tickedCtor?.text) {
|
|
2007
|
+
const fnC = ctx.currentFunction;
|
|
2008
|
+
if (fnC && !this.isPatternPosition(node) && !this.isTypePosition(node)) {
|
|
2009
|
+
this.pushRel(
|
|
2010
|
+
ctx,
|
|
2011
|
+
fnC,
|
|
2012
|
+
tickedCtor.text,
|
|
2013
|
+
ASTEngine.RelationshipType.Instantiates,
|
|
2014
|
+
0.9,
|
|
2015
|
+
{
|
|
2016
|
+
haskellSpecific: true,
|
|
2017
|
+
isTickedConstructorApplication: true
|
|
2018
|
+
},
|
|
2019
|
+
this.lineOf(tickedCtor)
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
if (this.isPatternPosition(node)) return;
|
|
2025
|
+
const fn = ctx.currentFunction;
|
|
2026
|
+
const target = this.findNamedChildByType(infixId, ["variable"]);
|
|
2027
|
+
const resolved = target?.text !== void 0 ? this.resolveCallableTarget(target.text, scopeStack, ctx) : void 0;
|
|
2028
|
+
if (fn && resolved !== void 0) {
|
|
2029
|
+
this.pushRel(
|
|
2030
|
+
ctx,
|
|
2031
|
+
fn,
|
|
2032
|
+
resolved,
|
|
2033
|
+
ASTEngine.RelationshipType.Calls,
|
|
2034
|
+
0.9,
|
|
2035
|
+
{
|
|
2036
|
+
haskellSpecific: true,
|
|
2037
|
+
context: "backtick infix call"
|
|
2038
|
+
},
|
|
2039
|
+
this.lineOf(target)
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
} else if (opNode?.text === "$") {
|
|
2043
|
+
const fn = ctx.currentFunction;
|
|
2044
|
+
if (fn) {
|
|
2045
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2046
|
+
const c = node.namedChild(i);
|
|
2047
|
+
if (!c || c.type === "operator") continue;
|
|
2048
|
+
const resolved = c.type === "variable" ? this.resolveCallableTarget(c.text, scopeStack, ctx) : void 0;
|
|
2049
|
+
if (resolved !== void 0) {
|
|
2050
|
+
this.pushRel(
|
|
2051
|
+
ctx,
|
|
2052
|
+
fn,
|
|
2053
|
+
resolved,
|
|
2054
|
+
ASTEngine.RelationshipType.Calls,
|
|
2055
|
+
0.9,
|
|
2056
|
+
{
|
|
2057
|
+
haskellSpecific: true,
|
|
2058
|
+
context: "$ application"
|
|
2059
|
+
},
|
|
2060
|
+
this.lineOf(c)
|
|
2061
|
+
);
|
|
2062
|
+
} else if (c.type === "qualified") {
|
|
2063
|
+
const mod = this.findNamedChildByType(c, ["module"]);
|
|
2064
|
+
const func = this.findNamedChildByType(c, ["variable"]);
|
|
2065
|
+
const modText = mod ? this.stripTrailingDot(mod.text) : "";
|
|
2066
|
+
if (modText && func?.text) {
|
|
2067
|
+
this.pushRel(
|
|
2068
|
+
ctx,
|
|
2069
|
+
fn,
|
|
2070
|
+
`${modText}.${func.text}`,
|
|
2071
|
+
ASTEngine.RelationshipType.Calls,
|
|
2072
|
+
0.9,
|
|
2073
|
+
{
|
|
2074
|
+
haskellSpecific: true,
|
|
2075
|
+
isQualifiedCall: true,
|
|
2076
|
+
context: "$ application"
|
|
2077
|
+
},
|
|
2078
|
+
this.lineOf(c)
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
break;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
} else {
|
|
2086
|
+
this.emitOperatorApplication(node, ctx, scopeStack);
|
|
2087
|
+
}
|
|
2088
|
+
const op = opNode?.text ?? "";
|
|
2089
|
+
if (op === ">>=" || op === ">>" || op === "<-") ctx.patterns["Monads"] = true;
|
|
2090
|
+
if (op === "<$>" || op === "<*>") ctx.patterns["Functors and Applicatives"] = true;
|
|
2091
|
+
if (op === "|>") ctx.patterns["Pipe Operator"] = true;
|
|
2092
|
+
}
|
|
2093
|
+
// ==========================================================================
|
|
2094
|
+
// VERIFY_HS9 — operator and infix-constructor APPLICATION (G1 + G2).
|
|
2095
|
+
//
|
|
2096
|
+
// What was missing, measured (script/m14/tmp/hs9-opshape-probe.ts, 60-file
|
|
2097
|
+
// GHC draw, all counts AST-side):
|
|
2098
|
+
//
|
|
2099
|
+
// EXP/infix:operator 3,140 of which 86 name an operator THIS
|
|
2100
|
+
// file declares as a callable
|
|
2101
|
+
// EXP/infix:constructor_operator 111 an infix DATA-CONSTRUCTOR
|
|
2102
|
+
// application (`a :+: b`) — a
|
|
2103
|
+
// construction, emitted as nothing
|
|
2104
|
+
// EXP/infix_id:constructor 9 a ticked constructor (`x `Cons` y`)
|
|
2105
|
+
// EXP/prefix_id:operator 22 `(<+>) a b`, a prefix application
|
|
2106
|
+
// EXP/prefix_id:constructor_operator 3 `(:+:) a b`
|
|
2107
|
+
// TYP/infix:operator 29 TYPE-level operator — must emit
|
|
2108
|
+
// nothing, and the node type alone
|
|
2109
|
+
// cannot tell it from an expression
|
|
2110
|
+
// PAT/infix:constructor_operator 98 pattern — must emit nothing
|
|
2111
|
+
//
|
|
2112
|
+
// WHY THE POSITION TEST IS NOT OPTIONAL: `infix` is ONE node type for THREE
|
|
2113
|
+
// syntactic positions in the pinned grammar. _exp_infix (grammar/exp.js:353),
|
|
2114
|
+
// _pat_infix (grammar/pat.js:99) and _type_infix (grammar/type.js:116) are all
|
|
2115
|
+
// aliased to `infix`, and all three use the field names left_operand /
|
|
2116
|
+
// operator / right_operand. A type-level operator is a bare `operator` node,
|
|
2117
|
+
// exactly like an expression's — the 29 TYP rows above are sites that an
|
|
2118
|
+
// operator-only test would have turned into 29 fabricated calls.
|
|
2119
|
+
//
|
|
2120
|
+
// WHY NAME RESOLUTION IS RIGHT AND NOT A GUESS: Haskell resolves an
|
|
2121
|
+
// unqualified name lexically, and a module's own top-level declaration — or
|
|
2122
|
+
// class method — SHADOWS the implicit Prelude import. So in a module that
|
|
2123
|
+
// declares `==`, `x == y` IS that module's `==`; the alternative would be an
|
|
2124
|
+
// ambiguous-occurrence error. This is the same rule resolveCallableTarget
|
|
2125
|
+
// already applies to plain variables, extended to operators.
|
|
2126
|
+
//
|
|
2127
|
+
// WHY SECTIONS ARE NOT EMITTED: `(x +)` is a PARTIAL application — it builds
|
|
2128
|
+
// a function value, it performs no call. `(<+>) a b` is a full application
|
|
2129
|
+
// and is emitted. The line is full-application vs partial, and it is stated
|
|
2130
|
+
// here rather than left to a reader of the code.
|
|
2131
|
+
// ==========================================================================
|
|
2132
|
+
emitOperatorApplication(node, ctx, scopeStack) {
|
|
2133
|
+
const fn = ctx.currentFunction;
|
|
2134
|
+
if (!fn) return;
|
|
2135
|
+
if (this.isPatternPosition(node) || this.isTypePosition(node)) return;
|
|
2136
|
+
const opField = node.childForFieldName("operator");
|
|
2137
|
+
const head = opField ?? (node.namedChildCount > 0 ? node.namedChild(0) : null);
|
|
2138
|
+
if (!head) return;
|
|
2139
|
+
if (head.type === "constructor_operator") {
|
|
2140
|
+
this.pushRel(
|
|
2141
|
+
ctx,
|
|
2142
|
+
fn,
|
|
2143
|
+
head.text,
|
|
2144
|
+
ASTEngine.RelationshipType.Instantiates,
|
|
2145
|
+
0.9,
|
|
2146
|
+
{
|
|
2147
|
+
haskellSpecific: true,
|
|
2148
|
+
isInfixConstructorApplication: true
|
|
2149
|
+
},
|
|
2150
|
+
this.lineOf(head)
|
|
2151
|
+
);
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
if (head.type !== "operator") return;
|
|
2155
|
+
if (head.text === "$") return;
|
|
2156
|
+
const resolved = this.resolveCallableTarget(head.text, scopeStack, ctx);
|
|
2157
|
+
if (resolved === void 0) return;
|
|
2158
|
+
this.pushRel(
|
|
2159
|
+
ctx,
|
|
2160
|
+
fn,
|
|
2161
|
+
resolved,
|
|
2162
|
+
ASTEngine.RelationshipType.Calls,
|
|
2163
|
+
0.9,
|
|
2164
|
+
{
|
|
2165
|
+
haskellSpecific: true,
|
|
2166
|
+
context: "operator application"
|
|
2167
|
+
},
|
|
2168
|
+
this.lineOf(head)
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* VERIFY_HS9: is this node part of a TYPE rather than an expression?
|
|
2173
|
+
*
|
|
2174
|
+
* Needed because `infix` covers expression, pattern AND type position in the
|
|
2175
|
+
* pinned grammar (see emitOperatorApplication). The list is the grammar's own
|
|
2176
|
+
* type-bearing vocabulary; the loop stops at an expression-bearing node so a
|
|
2177
|
+
* `type` buried inside a body does not capture expression operators above it.
|
|
2178
|
+
* Measured 29 type-level `infix:operator` sites on a 60-file draw, so this
|
|
2179
|
+
* test is load-bearing rather than defensive.
|
|
2180
|
+
*/
|
|
2181
|
+
isTypePosition(node) {
|
|
2182
|
+
let cur = node.parent;
|
|
2183
|
+
let guard = 0;
|
|
2184
|
+
while (cur && guard++ < 256) {
|
|
2185
|
+
if (HASKELL_TYPE_BEARING_NODES.has(cur.type)) return true;
|
|
2186
|
+
if (cur.type === "match" || cur.type === "do" || cur.type === "bind" || cur.type === "let_in" || cur.type === "lambda" || cur.type === "record" || cur.type === "guard") {
|
|
2187
|
+
return false;
|
|
2188
|
+
}
|
|
2189
|
+
cur = cur.parent;
|
|
2190
|
+
}
|
|
2191
|
+
return false;
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Per-symbol cyclomatic complexity (family contract: python #270 / ruby
|
|
2195
|
+
* #269 / php #268). Haskell decision points per the analyzer's own
|
|
2196
|
+
* module-level counter: case, conditional (if), guards.
|
|
2197
|
+
*/
|
|
2198
|
+
calculateNodeComplexity(node) {
|
|
2199
|
+
if (!node) return 1;
|
|
2200
|
+
let complexity = 1;
|
|
2201
|
+
const stack = [node];
|
|
2202
|
+
let guard = 0;
|
|
2203
|
+
while (stack.length > 0 && guard++ < 2e5) {
|
|
2204
|
+
const n = stack.pop();
|
|
2205
|
+
if (n.type === "case" || n.type === "conditional" || n.type === "guards") complexity++;
|
|
2206
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2207
|
+
const child = n.namedChild(i);
|
|
2208
|
+
if (child) stack.push(child);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return complexity;
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Evidence-tier classification — same rules as the python/ruby/php family
|
|
2215
|
+
* (M14 section D requires >=2 distinct evidenceClass values).
|
|
2216
|
+
*/
|
|
2217
|
+
classifyEvidenceTier(relationships) {
|
|
2218
|
+
for (const r of relationships) {
|
|
2219
|
+
const md = r.metadata ??= {};
|
|
2220
|
+
if (md.evidenceClass) continue;
|
|
2221
|
+
const t = r.type;
|
|
2222
|
+
const conf = r.confidence ?? 0;
|
|
2223
|
+
if (t === "contains" || t === "exports") {
|
|
2224
|
+
md.evidenceClass = "structural";
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
if (t === "imports") {
|
|
2228
|
+
md.evidenceClass = md.isHaskellImport ? "import_based" : "structural";
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
if (t === "references") {
|
|
2232
|
+
md.evidenceClass = md.isHaskellImport ? "import_based" : "reference_qualified";
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
if (t === "extends" || t === "implements") {
|
|
2236
|
+
md.evidenceClass = "structural";
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
if (t === "overrides") {
|
|
2240
|
+
md.evidenceClass = md.isInstanceMethod ? "structural" : md.evidenceClass ?? "structural";
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
if (t === "decorates") {
|
|
2244
|
+
md.evidenceClass = "structural";
|
|
2245
|
+
continue;
|
|
2246
|
+
}
|
|
2247
|
+
if (t === "part_of") {
|
|
2248
|
+
md.evidenceClass = "structural";
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
if (t === "instantiates") {
|
|
2252
|
+
md.evidenceClass = md.isRecordConstruction || md.isConstructorApplication || md.isQualifiedConstructorApplication || md.isInfixConstructorApplication || md.isTickedConstructorApplication ? "usage_contextual" : "structural";
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
md.evidenceClass = conf >= 0.85 ? "usage_contextual" : "inferred";
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
// Data-DECLARATION records live inside data_constructors; body-position
|
|
2259
|
+
// records sit under a match/do/bind. Walk up: crossing a match/bind/do
|
|
2260
|
+
// before any data_constructor means body position.
|
|
2261
|
+
isDataDeclarationPosition(node) {
|
|
2262
|
+
let cur = node.parent;
|
|
2263
|
+
while (cur) {
|
|
2264
|
+
if (cur.type === "match" || cur.type === "do" || cur.type === "bind" || cur.type === "let_in") return false;
|
|
2265
|
+
if (cur.type === "data_constructor" || cur.type === "newtype_constructor") return true;
|
|
2266
|
+
cur = cur.parent;
|
|
2267
|
+
}
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
// ==========================================================================
|
|
2271
|
+
// Pure-string helpers (no regex).
|
|
2272
|
+
// ==========================================================================
|
|
2273
|
+
stripOuterParens(text) {
|
|
2274
|
+
let start = 0;
|
|
2275
|
+
let end = text.length;
|
|
2276
|
+
while (start < end && text[start] === "(") start++;
|
|
2277
|
+
while (end > start && text[end - 1] === ")") end--;
|
|
2278
|
+
return text.slice(start, end);
|
|
2279
|
+
}
|
|
2280
|
+
stripTrailingDot(text) {
|
|
2281
|
+
return text.length > 0 && text[text.length - 1] === "." ? text.slice(0, -1) : text;
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* The C entity a `foreign_import` binds, or undefined when the declaration
|
|
2285
|
+
* names none.
|
|
2286
|
+
*
|
|
2287
|
+
* The Haskell 2010 FFI grammar is `foreign import [cc] [safety] import
|
|
2288
|
+
* [stringliteral] (entity | variadic)`: the string literal is the OPTIONAL
|
|
2289
|
+
* HEADER and the entity is the name that follows it. The pinned grammar models
|
|
2290
|
+
* both as `entity` children, so `foreign import capi "libwidget.h"
|
|
2291
|
+
* "widget_alloc" c_widgetAlloc` carries TWO of them — the header first — and
|
|
2292
|
+
* taking the FIRST match publishes the header as the symbol:
|
|
2293
|
+
* `references c_widgetAlloc -> "libwidget.h"`, a file name where a function
|
|
2294
|
+
* name belongs.
|
|
2295
|
+
*
|
|
2296
|
+
* MEASURED, and it corrected the hypothesis: over all 14,038 admitted `.hs`
|
|
2297
|
+
* files there are 1,153 `foreign_import` nodes and the shape distribution is
|
|
2298
|
+
* 1,040 with ONE quoted `entity` (which IS the entity — GHC writes
|
|
2299
|
+
* `foreign import ccall unsafe "rts_isDebugged" c_rtsIsDebugged`), 113 with NO
|
|
2300
|
+
* `entity` node at all, exactly 1 whose entity sits inside a sibling `ERROR`,
|
|
2301
|
+
* and ZERO with two `entity` children
|
|
2302
|
+
* (`script/m14/tmp/hs-ffi-shapes.ts`). So on THIS corpus the header case never
|
|
2303
|
+
* fires and the only live FFI defect was the quotes. The LAST-entity rule is
|
|
2304
|
+
* kept because it is strictly more correct on a shape the grammar does produce
|
|
2305
|
+
* elsewhere, and the count above is what says its reach here is 0 — a fix whose
|
|
2306
|
+
* corpus measurement is 0 is still worth having, and its 0 is recorded rather
|
|
2307
|
+
* than implied.
|
|
2308
|
+
*/
|
|
2309
|
+
readForeignEntity(node) {
|
|
2310
|
+
let found = null;
|
|
2311
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2312
|
+
const c = node.namedChild(i);
|
|
2313
|
+
if (c?.type === "entity") found = c;
|
|
2314
|
+
}
|
|
2315
|
+
return found?.text;
|
|
2316
|
+
}
|
|
2317
|
+
/**
|
|
2318
|
+
* Drop ONE surrounding pair of double quotes.
|
|
2319
|
+
*
|
|
2320
|
+
* The FFI `entity` is a Haskell string literal, so the grammar's node text is
|
|
2321
|
+
* `"name"` and the quotes are part of the node's text rather than of the name.
|
|
2322
|
+
* Only the exact pair is removed, and only when both ends have one: a C
|
|
2323
|
+
* identifier cannot contain `"`, and a bare word that happens to be an entity
|
|
2324
|
+
* (the `foreign export`/`foreign import` variants whose entity is not a literal)
|
|
2325
|
+
* is left exactly as it was. No regex — the lane's string contract is char
|
|
2326
|
+
* scans over values already extracted from AST nodes.
|
|
2327
|
+
*/
|
|
2328
|
+
stripQuotes(text) {
|
|
2329
|
+
if (text.length >= 2 && text[0] === '"' && text[text.length - 1] === '"') return text.slice(1, -1);
|
|
2330
|
+
return text;
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* THE C SYMBOL an FFI `entity` literal binds, not the whole literal.
|
|
2334
|
+
*
|
|
2335
|
+
* MEASURED DEFECT (2026-10-03, `script/m14/tmp/hs-verify-ffi-instance.ts`, tree
|
|
2336
|
+
* pass over a stride-40 draw of the GHC corpus — 351 files, 23 `foreign_import`
|
|
2337
|
+
* nodes): 7 of those 23 entity literals (30.4 %) carry a HEADER AND THE C NAME
|
|
2338
|
+
* IN ONE STRING, separated by a space, and the analyzer published the header as
|
|
2339
|
+
* the symbol:
|
|
2340
|
+
*
|
|
2341
|
+
* references log1pDoubleOld -> "math.h log1p" <- a FILE NAME
|
|
2342
|
+
* references functionInC -> "T15933.h function_in_c"
|
|
2343
|
+
*
|
|
2344
|
+
* This is the Haskell 2010 Report's own grammar, F.3.2.1:
|
|
2345
|
+
*
|
|
2346
|
+
* impent → 'static' [chname] [&] [cid] | string
|
|
2347
|
+
*
|
|
2348
|
+
* and for the string form the header and the entity share one literal, so
|
|
2349
|
+
* `foreign import ccall "T15933.h function_in_c"` binds the C symbol
|
|
2350
|
+
* `function_in_c`. GHC writes both shapes in this very corpus:
|
|
2351
|
+
* `foreign import ccall unsafe "RTSkeepAlive" c_keepCAFsForGHCi` (name only)
|
|
2352
|
+
* and the two above (header + name). The name-only shape is unaffected, which
|
|
2353
|
+
* is why the defect sat unread: every spot check drew the common form.
|
|
2354
|
+
*
|
|
2355
|
+
* WHY THE PRIOR MEASUREMENT MISSED IT. `readForeignEntity`'s own comment
|
|
2356
|
+
* records a tree census of 1,153 `foreign_import` nodes with "ZERO with two
|
|
2357
|
+
* `entity` children", and concluded the header case never fires. That count is
|
|
2358
|
+
* of NODES, and it is correct — this grammar really does model the header and
|
|
2359
|
+
* the entity as ONE `entity` node whose text is the whole literal. The header
|
|
2360
|
+
* is inside the string, not beside it, so no node census can see it; only the
|
|
2361
|
+
* literal's CONTENT distinguishes the two shapes. A fix whose measured reach
|
|
2362
|
+
* was recorded as 0 was recorded from the wrong quantity.
|
|
2363
|
+
*
|
|
2364
|
+
* RULE, IN TWO PARTS, BOTH CONDITIONED ON THE CALLING CONVENTION.
|
|
2365
|
+
*
|
|
2366
|
+
* (a) `javascript` NAMES NO ENTITY, so it yields "". `foreign import
|
|
2367
|
+
* javascript "(($1, $2) => { return $1($2); })"` binds a JAVASCRIPT BODY.
|
|
2368
|
+
* Measured over a stride-12 draw (1,170 files, 62 `foreign_import` nodes,
|
|
2369
|
+
* `script/m14/tmp/hs-verify-ffi-conv.ts`): 13 of 13 `javascript` nodes name
|
|
2370
|
+
* JS code and not one names an entity. Before this branch the analyzer
|
|
2371
|
+
* published those bodies verbatim, and 8 of them SHIPPED — they contain
|
|
2372
|
+
* none of the characters `pool-worker.filterNoiseRelationships` bans, so
|
|
2373
|
+
* the filter did not catch them:
|
|
2374
|
+
*
|
|
2375
|
+
* references js_print -> "console.log($1)"
|
|
2376
|
+
* references js_req_to_str -> "$1.text($2)"
|
|
2377
|
+
* references js_server_create -> "$1.createServer($2)"
|
|
2378
|
+
* references js_fetch_post -> "r.text();"
|
|
2379
|
+
*
|
|
2380
|
+
* That is the R7 payload-as-name shape: a code fragment standing where a
|
|
2381
|
+
* symbol belongs, counted by bb_graph and displayed by bb_impact. Under R7
|
|
2382
|
+
* the honest state for "no entity is named here" is ABSENCE. The declaration
|
|
2383
|
+
* still publishes its Haskell symbol and its `contains` edge; only the
|
|
2384
|
+
* `references` edge — which asserts an external symbol exists — is withheld.
|
|
2385
|
+
*
|
|
2386
|
+
* (b) For every C convention (`ccall`, `capi`, `stdcall`, `prim`, …) the C
|
|
2387
|
+
* entity is the LAST whitespace-separated token of the unquoted literal,
|
|
2388
|
+
* less a leading `&`. A C identifier contains neither a space nor a `&`
|
|
2389
|
+
* sigil, and the Report puts the header and the `static` keyword before the
|
|
2390
|
+
* entity, separated by a space. Measured in the same draw, the C
|
|
2391
|
+
* conventions are where the header and the `&` actually live:
|
|
2392
|
+
*
|
|
2393
|
+
* capi HEADER_PLUS_NAME x6 "math.h log1p"
|
|
2394
|
+
* ccall HEADER_PLUS_NAME x2 "test_error_throwing.h equivalent_to_id"
|
|
2395
|
+
* ccall AMPERSAND x1 "stdio.h &putchar"
|
|
2396
|
+
*
|
|
2397
|
+
* against 37 name-only C nodes and 13 javascript ones. The `&` is Report
|
|
2398
|
+
* F.3.2.1's `impent → 'static' [chname] [&] [cid]`: it means "pass the
|
|
2399
|
+
* address of this entity" and is not part of the entity's spelling, so
|
|
2400
|
+
* `foreign import ccall unsafe "&ghc_unique_counter64" ghc_unique_counter64`
|
|
2401
|
+
* (compiler/GHC/Types/Unique/Supply.hs:280) names the C symbol
|
|
2402
|
+
* `ghc_unique_counter64`.
|
|
2403
|
+
*
|
|
2404
|
+
* A corpus-wide count of the two prefix defects together, over the 1,356 quoted
|
|
2405
|
+
* FFI literals GHC writes: **391 header+name and 42 `&`-prefixed = 433 of
|
|
2406
|
+
* 1,356 (31.9 %)**, with 923 clean. That count is taken over the literals; the
|
|
2407
|
+
* convention split is taken over the tree, because only the tree knows which
|
|
2408
|
+
* convention a literal belongs to and the two must not be conflated — 10 of the
|
|
2409
|
+
* 391 text-level "header" hits are JavaScript bodies, not headers.
|
|
2410
|
+
*/
|
|
2411
|
+
foreignEntityName(literal, convention) {
|
|
2412
|
+
if (convention === "javascript") return "";
|
|
2413
|
+
const unquoted = this.stripQuotes(literal).trim();
|
|
2414
|
+
if (!unquoted) return "";
|
|
2415
|
+
let start = 0;
|
|
2416
|
+
for (let i = 0; i < unquoted.length; i++) {
|
|
2417
|
+
if (unquoted[i] === " " || unquoted[i] === " " || unquoted[i] === "\n" || unquoted[i] === "\r") {
|
|
2418
|
+
start = i + 1;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
let name = unquoted.slice(start);
|
|
2422
|
+
if (name[0] === "&") name = name.slice(1);
|
|
2423
|
+
return name;
|
|
2424
|
+
}
|
|
2425
|
+
hasArrowInParens(text) {
|
|
2426
|
+
let depth = 0;
|
|
2427
|
+
for (let i = 0; i < text.length - 1; i++) {
|
|
2428
|
+
const ch = text[i];
|
|
2429
|
+
if (ch === "(") depth++;
|
|
2430
|
+
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
2431
|
+
else if (ch === "-" && text[i + 1] === ">" && depth > 0) return true;
|
|
2432
|
+
}
|
|
2433
|
+
return false;
|
|
2434
|
+
}
|
|
2435
|
+
splitOnArrows(text) {
|
|
2436
|
+
const parts = [];
|
|
2437
|
+
let cur = "";
|
|
2438
|
+
for (let i = 0; i < text.length; i++) {
|
|
2439
|
+
if (text[i] === "-" && text[i + 1] === ">") {
|
|
2440
|
+
parts.push(cur);
|
|
2441
|
+
cur = "";
|
|
2442
|
+
i++;
|
|
2443
|
+
} else {
|
|
2444
|
+
cur += text[i];
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
parts.push(cur);
|
|
2448
|
+
return parts;
|
|
2449
|
+
}
|
|
2450
|
+
nodeToRange(node) {
|
|
2451
|
+
return {
|
|
2452
|
+
startLine: node.startPosition.row + 1,
|
|
2453
|
+
startColumn: node.startPosition.column + 1,
|
|
2454
|
+
endLine: node.endPosition.row + 1,
|
|
2455
|
+
endColumn: node.endPosition.column + 1,
|
|
2456
|
+
startIndex: node.startIndex,
|
|
2457
|
+
endIndex: node.endIndex
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
findNamedChildByType(node, types, extraPredicate) {
|
|
2461
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2462
|
+
const child = node.namedChild(i);
|
|
2463
|
+
if (!child) continue;
|
|
2464
|
+
let found = false;
|
|
2465
|
+
for (let j = 0; j < types.length; j++) {
|
|
2466
|
+
if (types[j] === child.type) {
|
|
2467
|
+
found = true;
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
if (found || extraPredicate && extraPredicate(child.type)) return child;
|
|
2472
|
+
}
|
|
2473
|
+
return null;
|
|
2474
|
+
}
|
|
2475
|
+
collectFields(recordNode) {
|
|
2476
|
+
const fields = [];
|
|
2477
|
+
const stack = [recordNode];
|
|
2478
|
+
while (stack.length > 0) {
|
|
2479
|
+
const n = stack.pop();
|
|
2480
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2481
|
+
const child = n.namedChild(i);
|
|
2482
|
+
if (!child) continue;
|
|
2483
|
+
if (child.type === "field") fields.push(child);
|
|
2484
|
+
else stack.push(child);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return fields;
|
|
2488
|
+
}
|
|
2489
|
+
collectNamedChildrenByType(node, types) {
|
|
2490
|
+
const matches = [];
|
|
2491
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2492
|
+
const child = node.namedChild(i);
|
|
2493
|
+
if (child) {
|
|
2494
|
+
let found = false;
|
|
2495
|
+
for (let j = 0; j < types.length; j++) {
|
|
2496
|
+
if (types[j] === child.type) {
|
|
2497
|
+
found = true;
|
|
2498
|
+
break;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if (found) matches.push(child);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
return matches;
|
|
2505
|
+
}
|
|
2506
|
+
hasChildOfType(node, type) {
|
|
2507
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2508
|
+
const child = node.child(i);
|
|
2509
|
+
if (child && child.type === type) return true;
|
|
2510
|
+
}
|
|
2511
|
+
return false;
|
|
2512
|
+
}
|
|
2513
|
+
findChildIndexByText(node, needle) {
|
|
2514
|
+
const needles = [];
|
|
2515
|
+
if (Array.isArray(needle)) {
|
|
2516
|
+
for (let i = 0; i < needle.length; i++) needles.push(needle[i]);
|
|
2517
|
+
} else {
|
|
2518
|
+
needles.push(needle);
|
|
2519
|
+
}
|
|
2520
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2521
|
+
const child = node.child(i);
|
|
2522
|
+
if (child) {
|
|
2523
|
+
for (let j = 0; j < needles.length; j++) {
|
|
2524
|
+
if (needles[j] === child.text) return i;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
return -1;
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
export {
|
|
2532
|
+
RealHaskellAnalyzer
|
|
2533
|
+
};
|