@backbencher/brain 0.0.0-stage → 0.1.1
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-Q5APTJQL.js +17 -0
- package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-22V37YTK.js +1758 -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-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3ZTOHHSW.js +2104 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5BVAH26D.js +340 -0
- package/dist/chunk-62DHLYW6.js +3427 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-7DRPNVIN.js +782 -0
- package/dist/chunk-7MBYT777.js +174 -0
- package/dist/chunk-AHDWGM7Q.js +236 -0
- package/dist/chunk-B5GEAAYF.js +818 -0
- package/dist/chunk-BEIZZ73B.js +1479 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DWKXZ6IF.js +336 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-ENW4BTBA.js +930 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-EY5ZZUUA.js +626 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HJEWO5EV.js +247 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NO3U3FJQ.js +201 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-QKTRECYS.js +1119 -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-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SGGCSHAX.js +12050 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UQNQZXHY.js +711 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-WIG36E67.js +196 -0
- package/dist/chunk-XHK3WWGB.js +7436 -0
- package/dist/chunk-XTQM2IP3.js +2863 -0
- package/dist/chunk-ZE2LG76D.js +1296 -0
- package/dist/chunk-ZETTGPAW.js +6950 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-7XIP555Q.js +593 -0
- package/dist/database-76RP44JG.js +9 -0
- package/dist/doctor-ANGETYMN.js +395 -0
- package/dist/edit-engine-CG72LDZD.js +51 -0
- package/dist/engine-BKEOGN7N.js +11 -0
- package/dist/engine-VIW6NSKO.js +18 -0
- package/dist/fingerprint-OBLIMZLP.js +33 -0
- package/dist/framework-intelligence-3LORTPOX.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-HBSQNPTB.js +21 -0
- package/dist/graph_request-Q6NKB6N2.js +350 -0
- package/dist/impact_request-LYP672WE.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-7OVGYSGW.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-FK2XKZXY.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-6XPENVAA.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-ZJ3DVUGO.js +355 -0
- package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
- package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
- package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
- package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
- package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
- package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
- package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
- package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
- package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
- package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
- package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
- package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
- package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
- package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
- package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
- package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
- package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
- package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
- package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
- package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
- package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
- package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
- package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
- package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
- package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
- package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
- package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
- package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
- package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
- package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
- package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
- package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
- package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
- package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
- package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
- package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
- package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
- package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
- package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
- package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
- package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
- package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
- package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
- package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
- package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
- package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
- package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
- package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
- package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
- package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
- package/dist/regex-search-NJKHMZNO.js +239 -0
- package/dist/rules-ACHPB7QU.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-NT5ZHYNE.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-GZJIM2J2.js +16 -0
- package/dist/taint-KXUHHPIE.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-OJDC35DI.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-Q57YMMBX.js +32 -0
- package/dist/type-inference-QMT2RMLV.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-ABP2TUL2.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,2192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTEngine
|
|
3
|
+
} from "./chunk-3ZTOHHSW.js";
|
|
4
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
5
|
+
import "./chunk-SK6YVHFZ.js";
|
|
6
|
+
import "./chunk-2BSKD7O3.js";
|
|
7
|
+
import "./chunk-KAET47ZL.js";
|
|
8
|
+
import "./chunk-KCDECRJY.js";
|
|
9
|
+
import "./chunk-MLKGABMK.js";
|
|
10
|
+
|
|
11
|
+
// src/ast/real-elixir-analyzer.ts
|
|
12
|
+
var EXPORTED_KINDS = /* @__PURE__ */ new Set([
|
|
13
|
+
ASTEngine.SymbolKind.Module,
|
|
14
|
+
ASTEngine.SymbolKind.Interface,
|
|
15
|
+
ASTEngine.SymbolKind.Class,
|
|
16
|
+
ASTEngine.SymbolKind.Struct,
|
|
17
|
+
ASTEngine.SymbolKind.Function,
|
|
18
|
+
ASTEngine.SymbolKind.Method,
|
|
19
|
+
ASTEngine.SymbolKind.Macro,
|
|
20
|
+
ASTEngine.SymbolKind.Type
|
|
21
|
+
]);
|
|
22
|
+
var DEFAULT_EVIDENCE_CLASS = {
|
|
23
|
+
contains: "structural",
|
|
24
|
+
exports: "structural",
|
|
25
|
+
implements: "structural",
|
|
26
|
+
instantiates: "structural",
|
|
27
|
+
overrides: "structural",
|
|
28
|
+
extends: "structural",
|
|
29
|
+
imports: "import_based",
|
|
30
|
+
references: "reference_qualified",
|
|
31
|
+
// A `calls` edge's class depends on the CALLEE'S SPELLING (`Mod.fun` was read
|
|
32
|
+
// off a dot node and resolved through the alias table; a bare `f(...)` is
|
|
33
|
+
// checked against the module's declared set), which a table cannot see —
|
|
34
|
+
// `evidenceClassFor` splits it.
|
|
35
|
+
calls: "usage_contextual",
|
|
36
|
+
uses: "usage_contextual"
|
|
37
|
+
};
|
|
38
|
+
function evidenceClassFor(type, confidence, target) {
|
|
39
|
+
if (String(type) === "calls") return target.indexOf(".") !== -1 ? "reference_qualified" : "usage_contextual";
|
|
40
|
+
return DEFAULT_EVIDENCE_CLASS[String(type)] ?? (confidence >= 0.85 ? "structural" : "inferred");
|
|
41
|
+
}
|
|
42
|
+
var complexityNodes = (function() {
|
|
43
|
+
const r = /* @__PURE__ */ Object.create(null);
|
|
44
|
+
const arr = ["if", "unless", "case", "cond", "receive", "with", "for", "def", "defp", "defmacro", "defmacrop"];
|
|
45
|
+
for (let i = 0; i < arr.length; i++) r[arr[i]] = true;
|
|
46
|
+
return r;
|
|
47
|
+
})();
|
|
48
|
+
var DEF_CONSTRUCTS = /* @__PURE__ */ new Set(["def", "defp", "defmacro", "defmacrop", "defdelegate", "defguard", "defguardp"]);
|
|
49
|
+
var DIRECTIVES = /* @__PURE__ */ new Set(["use", "import", "alias", "require"]);
|
|
50
|
+
var DECLARATION_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
51
|
+
"moduledoc",
|
|
52
|
+
"doc",
|
|
53
|
+
"spec",
|
|
54
|
+
"type",
|
|
55
|
+
"typep",
|
|
56
|
+
"opaque",
|
|
57
|
+
"callback",
|
|
58
|
+
"macrocallback",
|
|
59
|
+
"optional_callbacks",
|
|
60
|
+
"behaviour",
|
|
61
|
+
"behavior",
|
|
62
|
+
"derive",
|
|
63
|
+
"deprecated"
|
|
64
|
+
]);
|
|
65
|
+
var SPECIAL_FORMS = /* @__PURE__ */ new Set([
|
|
66
|
+
"if",
|
|
67
|
+
"unless",
|
|
68
|
+
"case",
|
|
69
|
+
"cond",
|
|
70
|
+
"receive",
|
|
71
|
+
"for",
|
|
72
|
+
"with",
|
|
73
|
+
"try",
|
|
74
|
+
"rescue",
|
|
75
|
+
"catch",
|
|
76
|
+
"after",
|
|
77
|
+
"else",
|
|
78
|
+
"quote",
|
|
79
|
+
"unquote",
|
|
80
|
+
"super",
|
|
81
|
+
"raise",
|
|
82
|
+
"throw",
|
|
83
|
+
"exit",
|
|
84
|
+
"fn",
|
|
85
|
+
"do"
|
|
86
|
+
]);
|
|
87
|
+
var KERNEL_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
88
|
+
"length",
|
|
89
|
+
"hd",
|
|
90
|
+
"tl",
|
|
91
|
+
"elem",
|
|
92
|
+
"put_elem",
|
|
93
|
+
"map_size",
|
|
94
|
+
"tuple_size",
|
|
95
|
+
"byte_size",
|
|
96
|
+
"bit_size",
|
|
97
|
+
"div",
|
|
98
|
+
"rem",
|
|
99
|
+
"abs",
|
|
100
|
+
"round",
|
|
101
|
+
"trunc",
|
|
102
|
+
"ceil",
|
|
103
|
+
"floor",
|
|
104
|
+
"min",
|
|
105
|
+
"max",
|
|
106
|
+
"is_atom",
|
|
107
|
+
"is_binary",
|
|
108
|
+
"is_bitstring",
|
|
109
|
+
"is_boolean",
|
|
110
|
+
"is_float",
|
|
111
|
+
"is_function",
|
|
112
|
+
"is_integer",
|
|
113
|
+
"is_list",
|
|
114
|
+
"is_map",
|
|
115
|
+
"is_map_key",
|
|
116
|
+
"is_number",
|
|
117
|
+
"is_pid",
|
|
118
|
+
"is_port",
|
|
119
|
+
"is_reference",
|
|
120
|
+
"is_tuple",
|
|
121
|
+
"is_nil",
|
|
122
|
+
"is_struct",
|
|
123
|
+
"is_exception",
|
|
124
|
+
"map_get",
|
|
125
|
+
"to_string",
|
|
126
|
+
"inspect",
|
|
127
|
+
"apply",
|
|
128
|
+
"self",
|
|
129
|
+
"send",
|
|
130
|
+
"send_after",
|
|
131
|
+
"spawn",
|
|
132
|
+
"spawn_link",
|
|
133
|
+
"spawn_monitor",
|
|
134
|
+
"make_ref",
|
|
135
|
+
"node",
|
|
136
|
+
"put_in",
|
|
137
|
+
"get_in",
|
|
138
|
+
"update_in",
|
|
139
|
+
"get_and_update_in",
|
|
140
|
+
"pop_in",
|
|
141
|
+
"struct",
|
|
142
|
+
"struct!",
|
|
143
|
+
"binary_part",
|
|
144
|
+
"binding",
|
|
145
|
+
"tap",
|
|
146
|
+
"then",
|
|
147
|
+
"dbg",
|
|
148
|
+
"tuple_to_list",
|
|
149
|
+
"list_to_tuple",
|
|
150
|
+
"atom_to_binary",
|
|
151
|
+
"binary_to_atom",
|
|
152
|
+
"term_to_binary",
|
|
153
|
+
"binary_to_term",
|
|
154
|
+
"process_flag",
|
|
155
|
+
"sleep",
|
|
156
|
+
"yield",
|
|
157
|
+
"match?",
|
|
158
|
+
"function_exported?",
|
|
159
|
+
"macro_exported?",
|
|
160
|
+
"reraise"
|
|
161
|
+
]);
|
|
162
|
+
var RealElixirAnalyzer = class {
|
|
163
|
+
analyze(rootNode, content, filePath) {
|
|
164
|
+
const ctx = {
|
|
165
|
+
symbols: [],
|
|
166
|
+
relationships: [],
|
|
167
|
+
imports: [],
|
|
168
|
+
exports: [],
|
|
169
|
+
errors: [],
|
|
170
|
+
patterns: {},
|
|
171
|
+
frameworks: {},
|
|
172
|
+
content,
|
|
173
|
+
// W23-E1: alias frames are prototype CHAINS (Object.create(parent));
|
|
174
|
+
// the null-prototype root makes chain lookups pollution-safe.
|
|
175
|
+
aliases: /* @__PURE__ */ Object.create(null),
|
|
176
|
+
functionHeads: {},
|
|
177
|
+
pendingDoc: void 0,
|
|
178
|
+
importList: [],
|
|
179
|
+
callbackSpecs: {},
|
|
180
|
+
optionalCallbacks: {},
|
|
181
|
+
symbolByName: /* @__PURE__ */ new Map(),
|
|
182
|
+
seenRels: /* @__PURE__ */ new Set(),
|
|
183
|
+
overridableNames: /* @__PURE__ */ new Map(),
|
|
184
|
+
overridableModules: /* @__PURE__ */ new Map(),
|
|
185
|
+
overridable: /* @__PURE__ */ new Map(),
|
|
186
|
+
lastDeclLine: /* @__PURE__ */ new Map(),
|
|
187
|
+
moduleUses: /* @__PURE__ */ new Map(),
|
|
188
|
+
pendingImpl: void 0,
|
|
189
|
+
moduleBehaviours: {},
|
|
190
|
+
moduleCallbacks: {},
|
|
191
|
+
usesCandidates: [],
|
|
192
|
+
mapBoundByScope: /* @__PURE__ */ new Map(),
|
|
193
|
+
usesVerified: 0,
|
|
194
|
+
usesContextual: 0,
|
|
195
|
+
mapKeyReceiverDropped: 0
|
|
196
|
+
};
|
|
197
|
+
const scopeStack = [];
|
|
198
|
+
const stack = [{ node: rootNode }];
|
|
199
|
+
let cyclomaticComplexity = 1;
|
|
200
|
+
while (stack.length > 0) {
|
|
201
|
+
const { node, isExit } = stack.pop();
|
|
202
|
+
if (isExit) {
|
|
203
|
+
if (scopeStack.length > 0 && scopeStack[scopeStack.length - 1].node === node) {
|
|
204
|
+
const sc = scopeStack.pop();
|
|
205
|
+
if (sc.symbol) sc.symbol.complexity = Math.max(sc.symbol.complexity ?? 1, sc.complexity + 1);
|
|
206
|
+
if (sc.savedAliases) ctx.aliases = sc.savedAliases;
|
|
207
|
+
if ((sc.kind === ASTEngine.SymbolKind.Module || sc.kind === ASTEngine.SymbolKind.Interface) && sc.name) {
|
|
208
|
+
const implied = sc.name.split(".").pop();
|
|
209
|
+
if (implied && !ctx.aliases[implied]) ctx.aliases[implied] = sc.name;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (node.hasError || node.isMissing) {
|
|
215
|
+
if (node.type === "ERROR" || node.type === "MISSING") {
|
|
216
|
+
ctx.errors.push({ message: `Syntax ${node.type} detected.`, range: this.nodeToRange(node), severity: "warning" });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
let pushedScope = false;
|
|
220
|
+
const currentScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
221
|
+
if (node.type === "call") {
|
|
222
|
+
const targetNode = this.findChild(node, (c) => c?.type === "identifier" || c?.type === "dot");
|
|
223
|
+
const t = targetNode?.text || "";
|
|
224
|
+
if (complexityNodes[t] && !this.isInQuotedCode(node)) {
|
|
225
|
+
cyclomaticComplexity++;
|
|
226
|
+
if (scopeStack.length > 0) scopeStack[scopeStack.length - 1].complexity++;
|
|
227
|
+
}
|
|
228
|
+
if (t) {
|
|
229
|
+
if (this.isInQuotedCode(node)) {
|
|
230
|
+
} else if (t === "defmodule") {
|
|
231
|
+
pushedScope = this.processModule(node, ctx, currentScope, scopeStack);
|
|
232
|
+
} else if (DEF_CONSTRUCTS.has(t)) {
|
|
233
|
+
pushedScope = this.processFunction(node, t, ctx, currentScope, scopeStack);
|
|
234
|
+
} else if (t === "defprotocol") {
|
|
235
|
+
pushedScope = this.processProtocol(node, ctx, currentScope, scopeStack);
|
|
236
|
+
} else if (t === "defimpl") {
|
|
237
|
+
pushedScope = this.processImpl(node, ctx, currentScope, scopeStack);
|
|
238
|
+
} else if (t === "defstruct" || t === "defexception") {
|
|
239
|
+
this.processStruct(node, ctx, currentScope);
|
|
240
|
+
} else if (DIRECTIVES.has(t)) {
|
|
241
|
+
this.processDirective(node, t, ctx, currentScope, scopeStack);
|
|
242
|
+
} else if (t === "defoverridable") {
|
|
243
|
+
this.recordOverridable(node, ctx, currentScope);
|
|
244
|
+
} else if (t === "case" || t === "cond" || t === "receive" || t === "match") {
|
|
245
|
+
ctx.patterns["Pattern Matching"] = true;
|
|
246
|
+
if (t === "receive") ctx.patterns["Actor Model (OTP)"] = true;
|
|
247
|
+
} else if (t === "struct" || t === "struct!" || t === "raise" || t === "reraise") {
|
|
248
|
+
if (!this.isInDeclarationAttributeValue(node) && !this.isDefHead(node)) {
|
|
249
|
+
const structArgs = this.findChild(node, (c) => c?.type === "arguments");
|
|
250
|
+
const first = structArgs && structArgs.namedChildCount > 0 ? structArgs.namedChild(0) : void 0;
|
|
251
|
+
if (first?.type === "alias") this.recordInstantiates(this.resolveAlias(first.text, ctx), ctx, currentScope, first.startPosition.row + 1);
|
|
252
|
+
else if (first?.type === "identifier" && first.text === "__MODULE__") {
|
|
253
|
+
let modScope;
|
|
254
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
255
|
+
const k = scopeStack[i].kind;
|
|
256
|
+
if (k === ASTEngine.SymbolKind.Module || k === ASTEngine.SymbolKind.Interface) {
|
|
257
|
+
modScope = scopeStack[i].name;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (modScope) this.recordInstantiates(modScope, ctx, currentScope, first.startPosition.row + 1);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
if (t === "send" || t === "send_after") {
|
|
266
|
+
ctx.patterns["Actor Model (OTP)"] = true;
|
|
267
|
+
ctx.patterns["Message Passing"] = true;
|
|
268
|
+
} else if (t === "spawn" || t === "spawn_link" || t === "spawn_monitor" || t === "Task.async" || t === "Task.start" || t === "Task.start_link" || t === "Agent.start" || t === "Agent.start_link") {
|
|
269
|
+
ctx.patterns["Actor Model (OTP)"] = true;
|
|
270
|
+
ctx.patterns["Process Spawning"] = true;
|
|
271
|
+
}
|
|
272
|
+
const memberLeaf = this.memberReadLeaf(node);
|
|
273
|
+
if (memberLeaf && !this.isDefHead(node) && !this.isInDeclarationAttributeValue(node) && !this.isPatternPosition(node)) {
|
|
274
|
+
ctx.usesCandidates.push({
|
|
275
|
+
source: currentScope?.name || "Global",
|
|
276
|
+
field: memberLeaf.leaf,
|
|
277
|
+
receiver: memberLeaf.receiver,
|
|
278
|
+
line: targetNode ? targetNode.startPosition.row + 1 : node.startPosition.row + 1
|
|
279
|
+
});
|
|
280
|
+
} else if (!this.shouldSkipCall(node, t) && !this.isPipeStep(node) && !this.isCaptureHead(node)) {
|
|
281
|
+
let meta = {};
|
|
282
|
+
if (t === "Task.async" || t === "Task.start" || t === "Task.start_link" || t === "Agent.start" || t === "Agent.start_link") meta = { isProcessSpawn: true };
|
|
283
|
+
const callArgs = this.findChild(node, (c) => c?.type === "arguments");
|
|
284
|
+
this.recordCall(t, ctx, currentScope, this.enclosingDeclaredScope(scopeStack), meta, targetNode ? targetNode.startPosition.row + 1 : node.startPosition.row + 1, callArgs ? callArgs.namedChildCount : 0);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else if (node.type === "map") {
|
|
289
|
+
const structChild = this.findChild(node, (c) => c?.type === "struct");
|
|
290
|
+
const aliasChild = structChild ? this.findChild(structChild, (c) => c?.type === "alias") : void 0;
|
|
291
|
+
if (aliasChild?.text && !this.isInQuotedCode(node) && !this.isDefHead(node) && !this.isInDeclarationAttributeValue(node) && !this.isPatternPosition(node)) {
|
|
292
|
+
this.recordInstantiates(this.resolveAlias(aliasChild.text, ctx), ctx, currentScope, aliasChild.startPosition.row + 1);
|
|
293
|
+
} else if (!aliasChild && !this.isInQuotedCode(node) && !this.isDefHead(node) && !this.isInDeclarationAttributeValue(node) && !this.isPatternPosition(node)) {
|
|
294
|
+
const selfChild = structChild ? this.findChild(structChild, (c) => c?.type === "identifier" && c.text === "__MODULE__") : void 0;
|
|
295
|
+
if (selfChild) {
|
|
296
|
+
let modScope;
|
|
297
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
298
|
+
const k = scopeStack[i].kind;
|
|
299
|
+
if (k === ASTEngine.SymbolKind.Module || k === ASTEngine.SymbolKind.Interface) {
|
|
300
|
+
modScope = scopeStack[i].name;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (modScope) this.recordInstantiates(modScope, ctx, currentScope, selfChild.startPosition.row + 1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} else if (node.type === "unary_operator" && node.child(0)?.text === "@" && !this.isInQuotedCode(node)) {
|
|
308
|
+
const attrCall = node.child(1);
|
|
309
|
+
const attrCallId = attrCall?.type === "call" ? this.findChild(attrCall, (c) => c?.type === "identifier") : void 0;
|
|
310
|
+
const attrName = attrCallId?.text || (attrCall?.type !== "call" ? attrCall?.text || "" : "");
|
|
311
|
+
if (attrName === "moduledoc" || attrName === "doc") ctx.patterns["Documentation"] = true;
|
|
312
|
+
if (attrName === "spec" || attrName === "type" || attrName === "typep" || attrName === "opaque") ctx.patterns["Typespecs"] = true;
|
|
313
|
+
if (attrName === "callback" || attrName === "macrocallback" || attrName === "optional_callbacks") {
|
|
314
|
+
ctx.patterns["Behaviours"] = true;
|
|
315
|
+
if (attrCallId && (attrCallId.text === "callback" || attrCallId.text === "macrocallback")) {
|
|
316
|
+
const argsNode = this.findChild(attrCall, (c) => c?.type === "arguments");
|
|
317
|
+
const findTypeOp = (n, depth = 0) => {
|
|
318
|
+
if (!n || depth > 4) return null;
|
|
319
|
+
if (n.type === "binary_operator" && n.child(1)?.text === "::") return n;
|
|
320
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
321
|
+
const c = n.child(i);
|
|
322
|
+
if (c && c.type === "binary_operator") {
|
|
323
|
+
const hit = findTypeOp(c, depth + 1);
|
|
324
|
+
if (hit) return hit;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
};
|
|
329
|
+
const binOp = findTypeOp(argsNode);
|
|
330
|
+
let lhs = null;
|
|
331
|
+
if (binOp) {
|
|
332
|
+
const left = binOp.child(0);
|
|
333
|
+
if (left?.type === "identifier") lhs = left;
|
|
334
|
+
else if (left?.type === "call") lhs = this.findChild(left, (c) => c?.type === "identifier");
|
|
335
|
+
}
|
|
336
|
+
if (lhs?.text) {
|
|
337
|
+
let arity = 0;
|
|
338
|
+
if (binOp) {
|
|
339
|
+
const left = binOp.child(0);
|
|
340
|
+
if (left?.type === "call") {
|
|
341
|
+
for (let i = 0; i < left.childCount; i++) {
|
|
342
|
+
const c = left.child(i);
|
|
343
|
+
if (c && c.type === "arguments") arity = c.namedChildCount;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
let ownerName = "";
|
|
348
|
+
let ownerKind;
|
|
349
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
350
|
+
const k = scopeStack[i].kind;
|
|
351
|
+
if (k === ASTEngine.SymbolKind.Module || k === ASTEngine.SymbolKind.Interface) {
|
|
352
|
+
ownerName = scopeStack[i].name;
|
|
353
|
+
ownerKind = k;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const specText = this.nodeToRange(binOp ?? node) && (binOp ? binOp.text : node.text);
|
|
358
|
+
const key = `${ownerName}/${lhs.text}/${arity}`;
|
|
359
|
+
ctx.callbackSpecs[key] = specText;
|
|
360
|
+
if (ownerName) ctx.moduleCallbacks[ownerName] = [...ctx.moduleCallbacks[ownerName] ?? [], lhs.text];
|
|
361
|
+
const existing = ctx.symbols.find((sym) => sym.name === lhs.text && (sym.ownerName ?? sym.scope ?? "") === ownerName && sym.metadata?.arity === arity);
|
|
362
|
+
if (existing) {
|
|
363
|
+
existing.metadata = { ...existing.metadata, callbackSpec: specText, isCallbackDeclaration: true, ...attrCallId.text === "macrocallback" ? { isMacroCallback: true } : {} };
|
|
364
|
+
if (existing.isDeclaration) existing.metadata.isDeclaration = true;
|
|
365
|
+
this.extendRange(existing, this.nodeToRange(node));
|
|
366
|
+
} else {
|
|
367
|
+
const isMethod = ownerKind === ASTEngine.SymbolKind.Interface || ownerKind === ASTEngine.SymbolKind.Class;
|
|
368
|
+
const cbSym = {
|
|
369
|
+
name: lhs.text,
|
|
370
|
+
kind: isMethod ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function,
|
|
371
|
+
range: this.nodeToRange(node),
|
|
372
|
+
scope: ownerName,
|
|
373
|
+
ownerName: ownerName || void 0,
|
|
374
|
+
ownerKind,
|
|
375
|
+
visibility: "public",
|
|
376
|
+
isDeclaration: true,
|
|
377
|
+
metadata: {
|
|
378
|
+
elixirSpecific: true,
|
|
379
|
+
isCallback: true,
|
|
380
|
+
isBodylessDeclaration: true,
|
|
381
|
+
isDeclaration: true,
|
|
382
|
+
arity,
|
|
383
|
+
heads: 0,
|
|
384
|
+
clauseRanges: [],
|
|
385
|
+
...attrCallId.text === "macrocallback" ? { isMacroCallback: true } : {},
|
|
386
|
+
callbackSpec: specText,
|
|
387
|
+
hasCallbackSpec: true
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
ctx.symbols.push(cbSym);
|
|
391
|
+
if (ownerName) ctx.functionHeads[`${ownerName}.${lhs.text}/${arity}`] = cbSym;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} else if (attrCallId && attrCallId.text === "optional_callbacks") {
|
|
395
|
+
const argsNode = this.findChild(attrCall, (c) => c?.type === "arguments");
|
|
396
|
+
const names = [];
|
|
397
|
+
const keywordGroups = [];
|
|
398
|
+
if (argsNode) {
|
|
399
|
+
for (const c of this.filterChildren(argsNode, (x) => x?.type === "keywords" || x?.type === "list")) {
|
|
400
|
+
if (c.type === "keywords") {
|
|
401
|
+
keywordGroups.push(c);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
for (const inner of this.filterChildren(c, (x) => x?.type === "keywords" || x?.type === "pair")) keywordGroups.push(inner);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const grp of keywordGroups) {
|
|
408
|
+
for (let j = 0; j < grp.namedChildCount; j++) {
|
|
409
|
+
const pair = grp.namedChild(j);
|
|
410
|
+
if (!pair || pair.type !== "pair") continue;
|
|
411
|
+
const kw = this.findChild(pair, (c) => c?.type === "keyword" || c?.type === "quoted_keyword");
|
|
412
|
+
if (kw?.text) names.push(this.stripAfterColon(this.stripQuotes(kw.text)));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
let ownerName = "";
|
|
416
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
417
|
+
const k = scopeStack[i].kind;
|
|
418
|
+
if (k === ASTEngine.SymbolKind.Module || k === ASTEngine.SymbolKind.Interface) {
|
|
419
|
+
ownerName = scopeStack[i].name;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (names.length > 0 && ownerName) {
|
|
424
|
+
ctx.optionalCallbacks[ownerName] = [...ctx.optionalCallbacks[ownerName] ?? [], ...names];
|
|
425
|
+
const ownerSym = ctx.symbolByName.get(ownerName);
|
|
426
|
+
if (ownerSym) ownerSym.metadata = { ...ownerSym.metadata, optionalCallbacks: [...ownerSym.metadata?.optionalCallbacks ?? [], ...names] };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (attrCallId && (attrCallId.text === "type" || attrCallId.text === "typep" || attrCallId.text === "opaque")) {
|
|
431
|
+
const argsNode = this.findChild(attrCall, (c) => c?.type === "arguments");
|
|
432
|
+
const binOp = argsNode ? this.findChild(argsNode, (c) => c?.type === "binary_operator") : void 0;
|
|
433
|
+
let typeNameNode = null;
|
|
434
|
+
if (binOp) {
|
|
435
|
+
const lhs = binOp.child(0);
|
|
436
|
+
if (lhs?.type === "identifier") typeNameNode = lhs;
|
|
437
|
+
else if (lhs?.type === "call") typeNameNode = this.findChild(lhs, (c) => c?.type === "identifier");
|
|
438
|
+
} else if (argsNode) {
|
|
439
|
+
typeNameNode = this.findChild(argsNode, (c) => c?.type === "identifier");
|
|
440
|
+
}
|
|
441
|
+
if (typeNameNode?.text) {
|
|
442
|
+
const isPrivateAlias = attrCallId.text === "typep";
|
|
443
|
+
ctx.symbols.push({ name: typeNameNode.text, kind: ASTEngine.SymbolKind.Type, range: this.nodeToRange(node), ownerName: currentScope?.unnamedModule ? void 0 : currentScope?.name, ownerKind: currentScope?.unnamedModule ? void 0 : currentScope?.kind, scope: currentScope?.unnamedModule ? "" : currentScope?.name || "", visibility: isPrivateAlias ? "private" : "public", metadata: { elixirSpecific: true, isTypeSpec: true, ...isPrivateAlias ? { isPrivateTypeAlias: true } : {}, ...currentScope?.unnamedModule ? { elixirOwnerUnknown: "un-nameable enclosing defmodule/defprotocol" } : {} } });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (attrName === "behaviour" || attrName === "behavior") {
|
|
447
|
+
const argsNode = attrCall?.type === "call" ? this.findChild(attrCall, (c) => c?.type === "arguments") : void 0;
|
|
448
|
+
const first = argsNode && argsNode.namedChildCount > 0 ? argsNode.namedChild(0) : void 0;
|
|
449
|
+
let bhvRaw = first ? this.stripQuotes(first.text).trim() : "";
|
|
450
|
+
if (bhvRaw.startsWith(":")) bhvRaw = bhvRaw.slice(1);
|
|
451
|
+
const bhv = bhvRaw ? this.resolveAlias(bhvRaw, ctx) : "";
|
|
452
|
+
if (bhv) {
|
|
453
|
+
this.pushRel(ctx, currentScope?.name || "Global", bhv, ASTEngine.RelationshipType.Implements, 0.9, { reasonCode: "elixir_behaviour_attribute" }, first ? first.startPosition.row + 1 : node.startPosition.row + 1);
|
|
454
|
+
if (currentScope) ctx.moduleBehaviours[currentScope.name] = [...ctx.moduleBehaviours[currentScope.name] ?? [], bhv];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (attrName === "impl") {
|
|
458
|
+
const argsNode = attrCall?.type === "call" ? this.findChild(attrCall, (c) => c?.type === "arguments") : void 0;
|
|
459
|
+
const first = argsNode && argsNode.namedChildCount > 0 ? argsNode.namedChild(0) : void 0;
|
|
460
|
+
const denies = !!first && (first.type === "boolean" && first.text === "false" || first.type === "nil");
|
|
461
|
+
let candidates = [];
|
|
462
|
+
let form = "denied";
|
|
463
|
+
if (denies) {
|
|
464
|
+
candidates = [];
|
|
465
|
+
} else if (first?.type === "alias") {
|
|
466
|
+
candidates = [this.resolveAlias(first.text, ctx)];
|
|
467
|
+
form = "explicit_protocol";
|
|
468
|
+
} else {
|
|
469
|
+
let implProto = "";
|
|
470
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
471
|
+
const sc = scopeStack[i];
|
|
472
|
+
if (sc.kind === ASTEngine.SymbolKind.Class) {
|
|
473
|
+
const proto = ctx.symbolByName.get(sc.name)?.metadata?.protocol;
|
|
474
|
+
if (typeof proto === "string" && proto) implProto = proto;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
if (sc.kind === ASTEngine.SymbolKind.Module || sc.kind === ASTEngine.SymbolKind.Interface) break;
|
|
478
|
+
}
|
|
479
|
+
const behaviours = currentScope ? [...new Set(ctx.moduleBehaviours[currentScope.name] ?? [])] : [];
|
|
480
|
+
if (implProto) {
|
|
481
|
+
candidates = [implProto];
|
|
482
|
+
form = "defimpl_protocol";
|
|
483
|
+
} else if (behaviours.length === 1) {
|
|
484
|
+
candidates = behaviours;
|
|
485
|
+
form = "single_behaviour";
|
|
486
|
+
} else if (behaviours.length > 1) {
|
|
487
|
+
candidates = behaviours;
|
|
488
|
+
form = "declared_callback";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
ctx.pendingImpl = candidates.length > 0 ? { candidates, form } : void 0;
|
|
492
|
+
}
|
|
493
|
+
if (attrName === "derive") {
|
|
494
|
+
const argsNode = attrCall?.type === "call" ? this.findChild(attrCall, (c) => c?.type === "arguments") : void 0;
|
|
495
|
+
const first = argsNode && argsNode.namedChildCount > 0 ? argsNode.namedChild(0) : void 0;
|
|
496
|
+
const protoNodes = [];
|
|
497
|
+
if (first?.type === "alias") protoNodes.push(first);
|
|
498
|
+
else if (first?.type === "tuple") {
|
|
499
|
+
const a = this.findChild(first, (c) => c?.type === "alias");
|
|
500
|
+
if (a) protoNodes.push(a);
|
|
501
|
+
} else if (first?.type === "list") {
|
|
502
|
+
for (let i = 0; i < first.namedChildCount; i++) {
|
|
503
|
+
const el = first.namedChild(i);
|
|
504
|
+
if (!el) continue;
|
|
505
|
+
if (el.type === "alias") protoNodes.push(el);
|
|
506
|
+
else if (el.type === "tuple") {
|
|
507
|
+
const a = this.findChild(el, (c) => c?.type === "alias");
|
|
508
|
+
if (a) protoNodes.push(a);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
for (const pn of protoNodes) {
|
|
513
|
+
const proto = this.resolveAlias(pn.text, ctx);
|
|
514
|
+
if (currentScope && proto) {
|
|
515
|
+
this.pushRel(ctx, currentScope.name, proto, ASTEngine.RelationshipType.Implements, 0.9, { reasonCode: "elixir_derive_attribute" }, pn.startPosition.row + 1);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (attrName === "moduledoc") {
|
|
520
|
+
const argsNode = attrCall?.type === "call" ? this.findChild(attrCall, (c) => c?.type === "arguments") : void 0;
|
|
521
|
+
const first = argsNode && argsNode.namedChildCount > 0 ? argsNode.namedChild(0) : void 0;
|
|
522
|
+
const text = first ? this.docContent(first) : "";
|
|
523
|
+
if (text && currentScope) {
|
|
524
|
+
const sym = ctx.symbolByName.get(currentScope.name);
|
|
525
|
+
if (sym) {
|
|
526
|
+
sym.comments = [text];
|
|
527
|
+
sym.docstring = text;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (attrName === "doc") {
|
|
532
|
+
const argsNode = attrCall?.type === "call" ? this.findChild(attrCall, (c) => c?.type === "arguments") : void 0;
|
|
533
|
+
const first = argsNode && argsNode.namedChildCount > 0 ? argsNode.namedChild(0) : void 0;
|
|
534
|
+
if (first && (first.text === "false" || first.text === "nil") && (first.type === "identifier" || first.type === "boolean" || first.type === "nil")) ctx.pendingDoc = void 0;
|
|
535
|
+
else {
|
|
536
|
+
const text = first ? this.docContent(first) : "";
|
|
537
|
+
if (text) ctx.pendingDoc = text;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
} else if (node.type === "unary_operator" && node.child(0)?.text === "&") {
|
|
541
|
+
const inner = node.child(1);
|
|
542
|
+
if (inner?.type === "binary_operator" && inner.child(1)?.text === "/" && !this.isInQuotedCode(node)) {
|
|
543
|
+
const fnNode = inner.child(0);
|
|
544
|
+
const arityNode = inner.child(2);
|
|
545
|
+
const arity = arityNode ? parseInt(arityNode.text, 10) : 0;
|
|
546
|
+
let fnName = "";
|
|
547
|
+
if (fnNode?.type === "identifier") fnName = fnNode.text;
|
|
548
|
+
else if (fnNode?.type === "call") {
|
|
549
|
+
const tgt = this.findChild(fnNode, (c) => c?.type === "identifier" || c?.type === "dot");
|
|
550
|
+
if (tgt && !this.findChild(fnNode, (c) => c?.type === "arguments")) fnName = tgt.text;
|
|
551
|
+
}
|
|
552
|
+
const leaf = fnName.split(".").pop() ?? fnName;
|
|
553
|
+
const skipName = fnName.includes(".") ? fnName : leaf;
|
|
554
|
+
if (fnName && !this.shouldSkipCall(node, skipName)) {
|
|
555
|
+
this.recordCall(fnName, ctx, currentScope, this.enclosingDeclaredScope(scopeStack), { isCapture: true }, fnNode ? fnNode.startPosition.row + 1 : node.startPosition.row + 1, arity);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} else if (node.type === "anonymous_function" || node.type === "stab_clause") {
|
|
559
|
+
ctx.patterns["Closures"] = true;
|
|
560
|
+
if (node.type === "stab_clause" && node.parent?.type === "do_block" && !this.isInQuotedCode(node)) {
|
|
561
|
+
cyclomaticComplexity++;
|
|
562
|
+
if (scopeStack.length > 0) scopeStack[scopeStack.length - 1].complexity++;
|
|
563
|
+
}
|
|
564
|
+
} else if ((node.type === "rescue_block" || node.type === "catch_block" || node.type === "after_block" || node.type === "else_block") && !this.isInQuotedCode(node)) {
|
|
565
|
+
cyclomaticComplexity++;
|
|
566
|
+
if (scopeStack.length > 0) scopeStack[scopeStack.length - 1].complexity++;
|
|
567
|
+
} else if (node.type === "binary_operator") {
|
|
568
|
+
const op = node.child(1)?.text;
|
|
569
|
+
if (op === "|>") {
|
|
570
|
+
ctx.patterns["Pipe Operator"] = true;
|
|
571
|
+
const right = node.child(2);
|
|
572
|
+
if (right) {
|
|
573
|
+
let targetName = "";
|
|
574
|
+
if (right.type === "call") {
|
|
575
|
+
const rId = this.findChild(right, (c) => c?.type === "identifier" || c?.type === "dot");
|
|
576
|
+
if (rId) targetName = rId.text;
|
|
577
|
+
} else if (right.type === "identifier" || right.type === "dot") {
|
|
578
|
+
targetName = right.text;
|
|
579
|
+
}
|
|
580
|
+
if (targetName && !this.shouldSkipCall(right, targetName)) {
|
|
581
|
+
const rightArgs = right.type === "call" ? this.findChild(right, (c) => c?.type === "arguments") : null;
|
|
582
|
+
this.recordCall(targetName, ctx, currentScope, this.enclosingDeclaredScope(scopeStack), { isPipe: true }, right.startPosition.row + 1, (rightArgs ? rightArgs.namedChildCount : 0) + 1);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if ((op === "and" || op === "or" || op === "&&" || op === "||") && !this.isInQuotedCode(node)) {
|
|
587
|
+
cyclomaticComplexity++;
|
|
588
|
+
if (scopeStack.length > 0) scopeStack[scopeStack.length - 1].complexity++;
|
|
589
|
+
}
|
|
590
|
+
} else if (node.type === "dot") {
|
|
591
|
+
const left = node.child(0)?.text;
|
|
592
|
+
if (left === "Enum" || left === "Stream") ctx.patterns["Functional Programming"] = true;
|
|
593
|
+
}
|
|
594
|
+
if (pushedScope) stack.push({ node, isExit: true });
|
|
595
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
596
|
+
const child = node.namedChild(i);
|
|
597
|
+
if (child) stack.push({ node: child });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const frameworks = [];
|
|
601
|
+
for (const f in ctx.frameworks) frameworks.push(f);
|
|
602
|
+
const patterns = [];
|
|
603
|
+
for (const p in ctx.patterns) patterns.push(p);
|
|
604
|
+
const frameworkPatterns = [];
|
|
605
|
+
for (let i = 0; i < frameworks.length; i++) {
|
|
606
|
+
frameworkPatterns.push({
|
|
607
|
+
framework: frameworks[i],
|
|
608
|
+
patterns: [{ type: "usage", detected: true, description: `${frameworks[i]} usage detected` }]
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
for (const s of ctx.symbols) {
|
|
612
|
+
const isModLevel = !s.ownerName || s.kind === ASTEngine.SymbolKind.Module || s.kind === ASTEngine.SymbolKind.Interface;
|
|
613
|
+
const isPub = s.visibility === "public";
|
|
614
|
+
const isExp = [ASTEngine.SymbolKind.Module, ASTEngine.SymbolKind.Function, ASTEngine.SymbolKind.Interface, ASTEngine.SymbolKind.Struct].includes(s.kind);
|
|
615
|
+
if (isModLevel && isPub && isExp) {
|
|
616
|
+
ctx.exports.push({ name: s.name, kind: s.kind, range: s.range, visibility: s.visibility });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
for (const s of ctx.symbols) if (s.name === "Unknown" && s.metadata) s.metadata.unresolvedName = true;
|
|
620
|
+
for (const s of ctx.symbols) {
|
|
621
|
+
const _owner = s.ownerName || s.scope;
|
|
622
|
+
if (_owner && _owner !== s.name && !s.metadata?.unresolvedName && !this.isPlaceholderName(_owner) && !this.isPlaceholderName(s.name) && s.name !== "unquote" && s.name !== "unquote_splicing") {
|
|
623
|
+
this.pushRel(ctx, _owner, s.name, ASTEngine.RelationshipType.Contains, 1, { reasonCode: "elixir_symbol_owner_ownership" }, s.range.startLine);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
for (const s of ctx.symbols) {
|
|
627
|
+
const owner = s.ownerName || s.scope || "";
|
|
628
|
+
if (owner.length === 0 || owner === s.name) continue;
|
|
629
|
+
if (s.visibility !== "public") continue;
|
|
630
|
+
if (!EXPORTED_KINDS.has(String(s.kind))) continue;
|
|
631
|
+
if (s.name.length === 0) continue;
|
|
632
|
+
if (s.metadata?.unresolvedName || this.isPlaceholderName(owner) || this.isPlaceholderName(s.name)) continue;
|
|
633
|
+
if (s.name === "unquote" || s.name === "unquote_splicing") continue;
|
|
634
|
+
this.pushRel(
|
|
635
|
+
ctx,
|
|
636
|
+
owner,
|
|
637
|
+
s.name,
|
|
638
|
+
ASTEngine.RelationshipType.Exports,
|
|
639
|
+
1,
|
|
640
|
+
{
|
|
641
|
+
elixirSpecific: true,
|
|
642
|
+
exportKind: String(s.kind),
|
|
643
|
+
exportForm: String(s.kind) === "macro" ? "macro" : String(s.kind) === "method" ? "method" : String(s.kind) === "module" ? "module" : String(s.kind) === "type" ? "type" : "function",
|
|
644
|
+
evidenceClass: "structural",
|
|
645
|
+
// `exports-public-surface`: a behaviour surface declared with `@callback`
|
|
646
|
+
// and NO `def` at all is still public callable surface, and the flag
|
|
647
|
+
// says which rows came from a callback declaration rather than from a
|
|
648
|
+
// definition. The symbol already carries it (the callback attribute
|
|
649
|
+
// writes `isCallbackDeclaration`); the edge is what the consumer reads.
|
|
650
|
+
// A callback-only behaviour surface carries `isCallback` (`@callback`
|
|
651
|
+
// creates the symbol; a later `def` would complete it and set
|
|
652
|
+
// `isCallbackDeclaration`) — both spellings mean the same thing to a
|
|
653
|
+
// consumer reading the edge, so both publish the flag.
|
|
654
|
+
...s.metadata?.isCallbackDeclaration || s.metadata?.isCallback ? { isCallbackDeclaration: true } : {}
|
|
655
|
+
},
|
|
656
|
+
s.range.startLine
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
for (const [mod, used] of ctx.moduleUses) {
|
|
660
|
+
for (const provider of used) {
|
|
661
|
+
const named = ctx.overridableNames.get(provider);
|
|
662
|
+
const moduleMarks = ctx.overridableModules.get(provider);
|
|
663
|
+
if (!named && !moduleMarks) continue;
|
|
664
|
+
for (const member of ctx.symbols) {
|
|
665
|
+
const owner = member.ownerName || "";
|
|
666
|
+
if (owner !== mod) continue;
|
|
667
|
+
const arity = typeof member.metadata?.arity === "number" ? String(member.metadata.arity) : "?";
|
|
668
|
+
const qualified = `${member.name}/${arity}`;
|
|
669
|
+
const bare = member.name;
|
|
670
|
+
let overriddenBy = "";
|
|
671
|
+
if (named && (named.has(qualified) || named.has(bare))) overriddenBy = provider;
|
|
672
|
+
else if (moduleMarks) {
|
|
673
|
+
for (const m of moduleMarks) {
|
|
674
|
+
const declares = ctx.symbols.some((s) => (s.ownerName || "") === m && s.name === member.name);
|
|
675
|
+
if (declares) {
|
|
676
|
+
overriddenBy = m;
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (overriddenBy.length === 0) continue;
|
|
682
|
+
const overridden = `${overriddenBy}.${member.name}`;
|
|
683
|
+
if (overridden === `${mod}.${member.name}`) continue;
|
|
684
|
+
this.pushRel(
|
|
685
|
+
ctx,
|
|
686
|
+
`${mod}.${member.name}`,
|
|
687
|
+
overridden,
|
|
688
|
+
ASTEngine.RelationshipType.Overrides,
|
|
689
|
+
0.9,
|
|
690
|
+
{ elixirSpecific: true, elixirOverridesKind: "use_of_defoverridable", provider: overriddenBy, evidenceClass: "structural", reasonCode: "elixir_use_defoverridable" },
|
|
691
|
+
member.range.startLine
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const declaredStructFields = /* @__PURE__ */ new Set();
|
|
697
|
+
for (const s of ctx.symbols) {
|
|
698
|
+
if (s.kind === ASTEngine.SymbolKind.Property && s.metadata?.isStructField) declaredStructFields.add(s.name);
|
|
699
|
+
}
|
|
700
|
+
const verifyAgainstDeclaredFields = declaredStructFields.size > 0;
|
|
701
|
+
for (const c of ctx.usesCandidates) {
|
|
702
|
+
if (c.receiver.charCodeAt(0) !== 37 && (ctx.mapBoundByScope.get(c.source)?.has(c.receiver) ?? false)) {
|
|
703
|
+
ctx.mapKeyReceiverDropped++;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if (verifyAgainstDeclaredFields && !declaredStructFields.has(c.field)) continue;
|
|
707
|
+
if (verifyAgainstDeclaredFields) {
|
|
708
|
+
ctx.usesVerified++;
|
|
709
|
+
this.pushRel(
|
|
710
|
+
ctx,
|
|
711
|
+
c.source,
|
|
712
|
+
c.field,
|
|
713
|
+
ASTEngine.RelationshipType.Uses,
|
|
714
|
+
0.9,
|
|
715
|
+
{ elixirSpecific: true, memberAccess: true, receiver: c.receiver, elixirUsesKind: "member_field_read", evidenceClass: "struct_field_read", reasonCode: "elixir_struct_field_read" },
|
|
716
|
+
c.line
|
|
717
|
+
);
|
|
718
|
+
} else {
|
|
719
|
+
ctx.usesContextual++;
|
|
720
|
+
this.pushRel(
|
|
721
|
+
ctx,
|
|
722
|
+
c.source,
|
|
723
|
+
c.field,
|
|
724
|
+
ASTEngine.RelationshipType.Uses,
|
|
725
|
+
0.7,
|
|
726
|
+
{ elixirSpecific: true, memberAccess: true, receiver: c.receiver, elixirUsesKind: "member_field_read", evidenceClass: "usage_contextual", reasonCode: "elixir_member_field_read" },
|
|
727
|
+
c.line
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const declaredByScope = /* @__PURE__ */ new Map();
|
|
732
|
+
for (const s of ctx.symbols) {
|
|
733
|
+
if (s.kind === ASTEngine.SymbolKind.Function || s.kind === ASTEngine.SymbolKind.Macro || s.kind === ASTEngine.SymbolKind.Method) {
|
|
734
|
+
const owner = s.ownerName || s.scope || "";
|
|
735
|
+
let set = declaredByScope.get(owner);
|
|
736
|
+
if (!set) {
|
|
737
|
+
set = /* @__PURE__ */ new Set();
|
|
738
|
+
declaredByScope.set(owner, set);
|
|
739
|
+
}
|
|
740
|
+
set.add(s.name);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const filteredRels = [];
|
|
744
|
+
for (const r of ctx.relationships) {
|
|
745
|
+
if (r.type === ASTEngine.RelationshipType.Calls && typeof r.target === "string" && r.target.indexOf(".") === -1) {
|
|
746
|
+
const declaredScope = r.metadata?._declaredScope;
|
|
747
|
+
if (typeof declaredScope === "string" && declaredScope !== "" && !declaredByScope.get(declaredScope)?.has(r.target)) {
|
|
748
|
+
const callScope = typeof r.metadata?._callScope === "string" ? r.metadata._callScope : void 0;
|
|
749
|
+
const resolved = this.resolveImport(ctx, declaredScope, r.target, callScope);
|
|
750
|
+
if (resolved) {
|
|
751
|
+
r.target = resolved;
|
|
752
|
+
if (r.metadata && typeof r.metadata === "object") {
|
|
753
|
+
r.metadata.evidenceClass = "reference_qualified";
|
|
754
|
+
r.metadata.reasonCode = "elixir_import_resolved_call";
|
|
755
|
+
}
|
|
756
|
+
} else continue;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (r.metadata && typeof r.metadata === "object") {
|
|
760
|
+
if ("_declaredScope" in r.metadata) delete r.metadata._declaredScope;
|
|
761
|
+
if ("_callScope" in r.metadata) delete r.metadata._callScope;
|
|
762
|
+
}
|
|
763
|
+
filteredRels.push(r);
|
|
764
|
+
}
|
|
765
|
+
ctx.relationships = filteredRels;
|
|
766
|
+
return {
|
|
767
|
+
language: "elixir",
|
|
768
|
+
symbols: ctx.symbols,
|
|
769
|
+
relationships: ctx.relationships,
|
|
770
|
+
imports: ctx.imports,
|
|
771
|
+
exports: ctx.exports,
|
|
772
|
+
errors: ctx.errors,
|
|
773
|
+
metadata: {
|
|
774
|
+
complexity: ctx.symbols.length + ctx.relationships.length,
|
|
775
|
+
linesOfCode: content.split("\n").length,
|
|
776
|
+
cyclomaticComplexity,
|
|
777
|
+
framework: frameworks[0],
|
|
778
|
+
patterns,
|
|
779
|
+
detectedFrameworks: frameworks,
|
|
780
|
+
frameworkPatterns,
|
|
781
|
+
architecturalScore: Math.min(100, Math.max(0, 100 - ctx.errors.length * 5)),
|
|
782
|
+
codeQuality: ctx.errors.length === 0 ? "Excellent" : "Fair",
|
|
783
|
+
hasTypeInference: false,
|
|
784
|
+
hasFrameworkIntelligence: true,
|
|
785
|
+
analysisVersion: "10.0.0-elixir-dfs-strict",
|
|
786
|
+
// EX-EX3 × ELIXIR_FIX2-U1: the countable split of the `uses` surface
|
|
787
|
+
// ( clause 3 — a ratio is only countable if the populations are named).
|
|
788
|
+
usesCandidates: ctx.usesCandidates.length,
|
|
789
|
+
usesVerified: ctx.usesVerified,
|
|
790
|
+
usesContextual: ctx.usesContextual,
|
|
791
|
+
mapKeyReceiverDropped: ctx.mapKeyReceiverDropped
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
// ELXD17: resolve an unqualified call through :only-constrained imports
|
|
796
|
+
// (last import wins, mirroring Elixir's conflict rule). Bare imports and
|
|
797
|
+
// :except lists cannot confirm a target per-file, so they never resolve —
|
|
798
|
+
// those calls stay dropped rather than fabricated.
|
|
799
|
+
// EX2-U5: the import's lexical scope must equal the declared scope that
|
|
800
|
+
// governs the call site (module-level import) OR the call site's own
|
|
801
|
+
// function (an import written inside a def body).
|
|
802
|
+
resolveImport(ctx, scope, name, callScope) {
|
|
803
|
+
for (let i = ctx.importList.length - 1; i >= 0; i--) {
|
|
804
|
+
const imp = ctx.importList[i];
|
|
805
|
+
if (imp.scope !== scope && imp.scope !== callScope) continue;
|
|
806
|
+
if (imp.only && imp.only.has(name)) return `${imp.module}.${name}`;
|
|
807
|
+
}
|
|
808
|
+
return void 0;
|
|
809
|
+
}
|
|
810
|
+
// EX2-U1: resolve the ROOT segment of a dotted name through the module's
|
|
811
|
+
// alias table (`alias MyApp.OrderBook` → OrderBook → MyApp.OrderBook).
|
|
812
|
+
// recordCall already did this; every other site that NAMES a type/module —
|
|
813
|
+
// struct literals, struct()/1, @behaviour, @derive, defimpl (protocol and
|
|
814
|
+
// target), and use/import/require module names — did not, so their edges
|
|
815
|
+
// pointed at strings that match nothing in the symbol table. Pure
|
|
816
|
+
// alias-table lookup on AST text — no scanning, no regexes.
|
|
817
|
+
resolveAlias(name, ctx) {
|
|
818
|
+
if (!name) return name;
|
|
819
|
+
const parts = name.split(".");
|
|
820
|
+
const root = parts[0];
|
|
821
|
+
if (root && ctx.aliases[root]) {
|
|
822
|
+
parts[0] = ctx.aliases[root];
|
|
823
|
+
return parts.join(".");
|
|
824
|
+
}
|
|
825
|
+
return name;
|
|
826
|
+
}
|
|
827
|
+
// The top non-function scope on the stack — the module/class whose declared
|
|
828
|
+
// set governs unqualified-call resolution for the current call site.
|
|
829
|
+
enclosingDeclaredScope(scopeStack) {
|
|
830
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
831
|
+
const k = scopeStack[i].kind;
|
|
832
|
+
if (k !== ASTEngine.SymbolKind.Function && k !== ASTEngine.SymbolKind.Macro && k !== ASTEngine.SymbolKind.Method) {
|
|
833
|
+
return scopeStack[i].name;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return void 0;
|
|
837
|
+
}
|
|
838
|
+
// ==========================================================================
|
|
839
|
+
// Relationship emission helpers (dedupe by source|type|target identity).
|
|
840
|
+
// ==========================================================================
|
|
841
|
+
pushRel(ctx, source, target, type, confidence, metadata = {}, line) {
|
|
842
|
+
const key = `${source}|${type}|${target}|${line ?? ""}|${metadata.callArity ?? ""}`;
|
|
843
|
+
if (ctx.seenRels.has(key)) return;
|
|
844
|
+
ctx.seenRels.add(key);
|
|
845
|
+
let md = metadata;
|
|
846
|
+
const needReason = confidence < 1 && !(typeof metadata.reasonCode === "string" && metadata.reasonCode.length > 0);
|
|
847
|
+
const needClass = !(typeof metadata.evidenceClass === "string" && metadata.evidenceClass.length > 0);
|
|
848
|
+
if (needReason || needClass) {
|
|
849
|
+
md = { ...metadata };
|
|
850
|
+
if (needReason) {
|
|
851
|
+
md.reasonCode = `elixir_${String(type)}_heuristic`;
|
|
852
|
+
md.reasonCodeSource = "default_at_pushrel";
|
|
853
|
+
}
|
|
854
|
+
if (needClass) md.evidenceClass = evidenceClassFor(type, confidence, target);
|
|
855
|
+
}
|
|
856
|
+
const rel = {
|
|
857
|
+
source,
|
|
858
|
+
target,
|
|
859
|
+
type,
|
|
860
|
+
confidence,
|
|
861
|
+
metadata: md,
|
|
862
|
+
...typeof line === "number" && line > 0 ? { line_number: line } : {}
|
|
863
|
+
};
|
|
864
|
+
ctx.relationships.push(rel);
|
|
865
|
+
}
|
|
866
|
+
recordCall(targetName, ctx, currentScope, declaredScope, meta = {}, line, arity) {
|
|
867
|
+
let finalTarget = targetName;
|
|
868
|
+
if (finalTarget.indexOf("(") !== -1) {
|
|
869
|
+
const lastDot = finalTarget.lastIndexOf(".");
|
|
870
|
+
finalTarget = lastDot >= 0 ? finalTarget.slice(lastDot + 1) : finalTarget;
|
|
871
|
+
const paren = finalTarget.indexOf("(");
|
|
872
|
+
if (paren !== -1) finalTarget = finalTarget.slice(0, paren);
|
|
873
|
+
if (finalTarget) {
|
|
874
|
+
if (arity && arity > 0) meta = { ...meta, callArity: arity };
|
|
875
|
+
this.pushRel(ctx, currentScope?.name || "Global", finalTarget, ASTEngine.RelationshipType.Calls, 0.8, { elixirSpecific: true, reasonCode: "elixir_call_chained_receiver_leaf", ...meta }, line);
|
|
876
|
+
}
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (finalTarget.startsWith("__MODULE__.")) {
|
|
880
|
+
if (!declaredScope) return;
|
|
881
|
+
finalTarget = declaredScope + finalTarget.slice("__MODULE__".length);
|
|
882
|
+
}
|
|
883
|
+
const parts = finalTarget.split(".");
|
|
884
|
+
if (parts.length > 1) {
|
|
885
|
+
const root = parts[0];
|
|
886
|
+
if (ctx.aliases[root]) {
|
|
887
|
+
parts[0] = ctx.aliases[root];
|
|
888
|
+
finalTarget = parts.join(".");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (!finalTarget) return;
|
|
892
|
+
if (finalTarget.startsWith("&")) return;
|
|
893
|
+
if (finalTarget.startsWith("__ENV__.") || finalTarget.startsWith("__CALLER__.") || finalTarget.startsWith("__DIR__.") || finalTarget.startsWith("__STACKTRACE__.")) return;
|
|
894
|
+
if (finalTarget.startsWith("Kernel.")) {
|
|
895
|
+
const leaf = finalTarget.slice("Kernel.".length);
|
|
896
|
+
if (KERNEL_FUNCTIONS.has(leaf) || SPECIAL_FORMS.has(leaf)) return;
|
|
897
|
+
let hasNameChar = false;
|
|
898
|
+
for (const ch of leaf) {
|
|
899
|
+
if (ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_") {
|
|
900
|
+
hasNameChar = true;
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (!hasNameChar) return;
|
|
905
|
+
}
|
|
906
|
+
if (finalTarget.indexOf(".") === -1) {
|
|
907
|
+
const aliased = ctx.aliases[finalTarget];
|
|
908
|
+
if (aliased && aliased.indexOf(".") !== -1) finalTarget = aliased;
|
|
909
|
+
}
|
|
910
|
+
if (arity && arity > 0) meta = { ...meta, callArity: arity };
|
|
911
|
+
if (finalTarget.indexOf(".") !== -1) {
|
|
912
|
+
const root = finalTarget.split(".")[0];
|
|
913
|
+
if (root && root.length > 0 && !(root[0] >= "A" && root[0] <= "Z") && root[0] !== ":") return;
|
|
914
|
+
} else if (declaredScope) {
|
|
915
|
+
const callScope = currentScope && (currentScope.kind === ASTEngine.SymbolKind.Function || currentScope.kind === ASTEngine.SymbolKind.Macro || currentScope.kind === ASTEngine.SymbolKind.Method) ? currentScope.name : void 0;
|
|
916
|
+
meta = { ...meta, _declaredScope: declaredScope, _callScope: callScope };
|
|
917
|
+
}
|
|
918
|
+
this.pushRel(
|
|
919
|
+
ctx,
|
|
920
|
+
currentScope?.name || "Global",
|
|
921
|
+
finalTarget,
|
|
922
|
+
ASTEngine.RelationshipType.Calls,
|
|
923
|
+
0.8,
|
|
924
|
+
{ elixirSpecific: true, reasonCode: finalTarget.indexOf(".") !== -1 ? "elixir_call_qualified_module_receiver" : "elixir_call_local_declared_scope", ...meta },
|
|
925
|
+
line
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
recordInstantiates(structName, ctx, currentScope, line) {
|
|
929
|
+
if (!structName) return;
|
|
930
|
+
if (structName === currentScope?.name) return;
|
|
931
|
+
this.pushRel(ctx, currentScope?.name || "Global", structName, ASTEngine.RelationshipType.Instantiates, 0.9, { elixirSpecific: true, isStruct: true }, line);
|
|
932
|
+
}
|
|
933
|
+
// ==========================================================================
|
|
934
|
+
// Call-skip predicates (the noise-elimination contract).
|
|
935
|
+
// ==========================================================================
|
|
936
|
+
// Everything inside a `quote do ... end` block is data (macro AST), not
|
|
937
|
+
// executable code — the Clojure quoted-data rule for Elixir.
|
|
938
|
+
isInQuotedCode(node) {
|
|
939
|
+
let cur = node.parent;
|
|
940
|
+
while (cur) {
|
|
941
|
+
if (cur.type === "call") {
|
|
942
|
+
const id = this.findChild(cur, (c) => c?.type === "identifier");
|
|
943
|
+
if (id?.text === "quote") return true;
|
|
944
|
+
if (id?.text === "unquote" || id?.text === "unquote_splicing") return false;
|
|
945
|
+
}
|
|
946
|
+
cur = cur.parent;
|
|
947
|
+
}
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
// EX-A14: does this `def` have a BODY? Two shapes, both real: a `do_block`
|
|
951
|
+
// child, and the single-line `, do: body` pair — which the grammar nests
|
|
952
|
+
// INSIDE the def's `arguments`, so it is not a sibling of it (the first
|
|
953
|
+
// version of this check looked for a direct `pair` child and read every
|
|
954
|
+
// `def f(x), do: y` as bodiless, which is how a definition ended up carrying
|
|
955
|
+
// isDeclaration). The walk stays inside `arguments` and never descends into
|
|
956
|
+
// a nested `do_block`, which is a body of some inner construct, not this def's.
|
|
957
|
+
hasDefBody(node) {
|
|
958
|
+
if (this.findChild(node, (c) => c?.type === "do_block") !== null) return true;
|
|
959
|
+
const args = this.findChild(node, (c) => c?.type === "arguments");
|
|
960
|
+
if (!args) return false;
|
|
961
|
+
const stack = [args];
|
|
962
|
+
while (stack.length > 0) {
|
|
963
|
+
const n = stack.pop();
|
|
964
|
+
if (n !== args && n.type === "do_block") continue;
|
|
965
|
+
if (n.type === "pair") {
|
|
966
|
+
const kw = this.findChild(n, (c) => c?.type === "keyword");
|
|
967
|
+
if (kw && this.stripAfterColon(kw.text) === "do") return true;
|
|
968
|
+
}
|
|
969
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
970
|
+
const c = n.child(i);
|
|
971
|
+
if (c) stack.push(c);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
// Inside a module attribute (@foo ...) — the attribute's inner call is the
|
|
977
|
+
// attribute declaration, never a runtime invocation.
|
|
978
|
+
isInAttribute(node) {
|
|
979
|
+
let cur = node.parent;
|
|
980
|
+
while (cur) {
|
|
981
|
+
if (cur.type === "unary_operator") {
|
|
982
|
+
const op = cur.child(0);
|
|
983
|
+
if (op?.text === "@") return true;
|
|
984
|
+
}
|
|
985
|
+
cur = cur.parent;
|
|
986
|
+
}
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
// EX-A17: the attribute's OWN call node is its NAME (`@section ...` is
|
|
990
|
+
// call[section, arguments[...]]), never an invocation of `section`. The
|
|
991
|
+
// blanket isInAttribute gate hid that correctly, so the relaxation below has
|
|
992
|
+
// to keep naming it explicitly or every attribute publishes a phantom
|
|
993
|
+
// lowercase call.
|
|
994
|
+
isAttributeNameCall(node) {
|
|
995
|
+
const p = node.parent;
|
|
996
|
+
return !!(p && p.type === "unary_operator" && p.child(0)?.text === "@" && p.child(1)?.id === node.id);
|
|
997
|
+
}
|
|
998
|
+
// EX-A17: a module attribute's VALUE is COMPILE-TIME CODE. `@section
|
|
999
|
+
// %Spark.Dsl.Section{build: Spark.Dsl.build(:x)}` builds that struct while
|
|
1000
|
+
// the module compiles, `@deferred fn -> Helper.later() end` names a real
|
|
1001
|
+
// remote call, and `@raised raise Helper.Error` raises a real exception —
|
|
1002
|
+
// the whole DSL ecosystem (Ash/Spark, Ecto.Migration, Quantum) is built on
|
|
1003
|
+
// it. The blanket gate silenced every edge inside an attribute value, so a
|
|
1004
|
+
// DSL section named no dependency at all: 21 attributes carrying 21 struct
|
|
1005
|
+
// literals in a 250-file sample of ash, 0 elsewhere in 938 files — and the
|
|
1006
|
+
// SAME walk already counted `case` inside an attribute toward cyclomatic
|
|
1007
|
+
// complexity and set its pattern flags, so the gate contradicted the
|
|
1008
|
+
// analyzer's own treatment of the same subtree. The DECLARATION attributes
|
|
1009
|
+
// keep their silence; `isInAttribute` still guards the directive and
|
|
1010
|
+
// member-read walks, because a `use`/`alias` cannot legitimately appear in
|
|
1011
|
+
// an attribute value.
|
|
1012
|
+
isInDeclarationAttributeValue(node) {
|
|
1013
|
+
let cur = node.parent;
|
|
1014
|
+
while (cur) {
|
|
1015
|
+
if (cur.type === "unary_operator" && cur.child(0)?.text === "@") {
|
|
1016
|
+
const value = cur.child(1);
|
|
1017
|
+
if (!value || value.type !== "call") return true;
|
|
1018
|
+
const id = this.findChild(value, (c) => c?.type === "identifier");
|
|
1019
|
+
return DECLARATION_ATTRIBUTES.has(id?.text ?? "");
|
|
1020
|
+
}
|
|
1021
|
+
cur = cur.parent;
|
|
1022
|
+
}
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
// The signature call (and its pattern-position arguments) nested in a
|
|
1026
|
+
// def/defp/defmacro/defmacrop `arguments` is the declaration head, not a call
|
|
1027
|
+
// site. Stops at both the do_block boundary AND the single-line `, do: body`
|
|
1028
|
+
// pair so calls in the body are never misclassified as heads. The pair
|
|
1029
|
+
// check matches exactly `do:` — keyword pairs like `domain:` in signature
|
|
1030
|
+
// patterns must not end the head walk (deep-review hygiene).
|
|
1031
|
+
// ELIXIR_FIX2-P2 — a def-shaped call inside a def-shaped BODY is a CALL.
|
|
1032
|
+
//
|
|
1033
|
+
// Elixir requires `def`/`defp`/`defmacro`/… to sit at module level; a
|
|
1034
|
+
// definition inside a definition body is a macro invocation of the same name
|
|
1035
|
+
// (the one legal way to generate definitions — inside a `quote` — is already
|
|
1036
|
+
// excluded by isInQuotedCode). Found by the contains oracle, not by reading:
|
|
1037
|
+
//
|
|
1038
|
+
// lib/elixir/lib/kernel/utils.ex:317-319
|
|
1039
|
+
// defmacro defguard(args, expr) do
|
|
1040
|
+
// defguard(args, expr, __CALLER__)
|
|
1041
|
+
// end
|
|
1042
|
+
//
|
|
1043
|
+
// published a MACRO symbol named `args`, owned by `Kernel.Utils.defguard/2`.
|
|
1044
|
+
// The shape is `call > identifier(defguard) + arguments(identifier,
|
|
1045
|
+
// identifier, identifier)` — with bare arguments there is NO nested `call`
|
|
1046
|
+
// node, so the existing signature read takes `arguments`' first named child
|
|
1047
|
+
// and the argument becomes the name. `isDefHead` cannot see it: it returns
|
|
1048
|
+
// false on the enclosing `do_block` before it ever reaches the outer call,
|
|
1049
|
+
// which is the correct answer to "is this a signature head" and the wrong
|
|
1050
|
+
// question to ask here.
|
|
1051
|
+
isInsideDefBody(node) {
|
|
1052
|
+
let cur = node.parent;
|
|
1053
|
+
while (cur) {
|
|
1054
|
+
if (cur.type === "do_block") {
|
|
1055
|
+
const owner = cur.parent;
|
|
1056
|
+
if (owner && owner.type === "call") {
|
|
1057
|
+
const id = this.findChild(owner, (c) => c?.type === "identifier");
|
|
1058
|
+
if (id && DEF_CONSTRUCTS.has(id.text)) return true;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (cur.type === "pair") {
|
|
1062
|
+
const kw = this.findChild(cur, (c) => c?.type === "keyword");
|
|
1063
|
+
const kwt = kw?.text ?? "";
|
|
1064
|
+
const owner = cur.parent;
|
|
1065
|
+
if ((kwt === "do" || kwt.startsWith("do:")) && owner && owner.type === "call") {
|
|
1066
|
+
const id = this.findChild(owner, (c) => c?.type === "identifier");
|
|
1067
|
+
if (id && DEF_CONSTRUCTS.has(id.text)) return true;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
cur = cur.parent;
|
|
1071
|
+
}
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
isDefHead(node) {
|
|
1075
|
+
const startIndex = node.startIndex;
|
|
1076
|
+
let cur = node.parent;
|
|
1077
|
+
while (cur) {
|
|
1078
|
+
if (cur.type === "do_block") return false;
|
|
1079
|
+
if (cur.type === "pair") {
|
|
1080
|
+
const kw = this.findChild(cur, (c) => c?.type === "keyword");
|
|
1081
|
+
const kwt = kw?.text ?? "";
|
|
1082
|
+
if (kwt === "do" || kwt.startsWith("do:")) return false;
|
|
1083
|
+
}
|
|
1084
|
+
if (cur.type === "binary_operator" && cur.childCount > 2 && cur.child(1)?.text === "\\\\") {
|
|
1085
|
+
const rhs = cur.child(2);
|
|
1086
|
+
if (rhs && startIndex >= rhs.startIndex) return false;
|
|
1087
|
+
}
|
|
1088
|
+
if (cur.type === "call") {
|
|
1089
|
+
const id = this.findChild(cur, (c) => c?.type === "identifier");
|
|
1090
|
+
if (id && DEF_CONSTRUCTS.has(id.text)) return true;
|
|
1091
|
+
}
|
|
1092
|
+
cur = cur.parent;
|
|
1093
|
+
}
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
// X2 (adversarial): PATTERN positions are never constructions. A struct
|
|
1097
|
+
// literal in a case/fn/receive clause pattern, or on the left of `=` or
|
|
1098
|
+
// `<-`, is a destructure — 43+16 live corpus sites each emitted a false
|
|
1099
|
+
// instantiates (e.g. `fn %Mix.Dep{app: app} = dep ->`). The arrow token in
|
|
1100
|
+
// a stab_clause separates pattern from body; the operators separate the
|
|
1101
|
+
// match subject from the built value. (A `cond` clause test evaluates —
|
|
1102
|
+
// structs there are nonsense code and are treated as patterns uniformly.)
|
|
1103
|
+
isPatternPosition(node) {
|
|
1104
|
+
let cur = node;
|
|
1105
|
+
while (cur && cur.parent) {
|
|
1106
|
+
const p = cur.parent;
|
|
1107
|
+
if (p.type === "stab_clause") {
|
|
1108
|
+
const arrow = this.findChild(p, (c) => c?.type === "->");
|
|
1109
|
+
return arrow ? node.startIndex < arrow.startIndex : false;
|
|
1110
|
+
}
|
|
1111
|
+
if (p.type === "binary_operator" && (p.child(1)?.text === "=" || p.child(1)?.text === "<-")) {
|
|
1112
|
+
const left = p.child(0);
|
|
1113
|
+
return !!(left && node.startIndex >= left.startIndex && node.endIndex <= left.endIndex);
|
|
1114
|
+
}
|
|
1115
|
+
cur = p;
|
|
1116
|
+
}
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
// Member/property access: `order.side` is a call whose target is a `dot`
|
|
1120
|
+
// node with no arguments — a field read, not an invocation. Module calls
|
|
1121
|
+
// like `Map.put(...)` carry arguments and are real invocations. A bare
|
|
1122
|
+
// remote reference (`Enum.map`, `&Mod.fun/2`, `:lists.member`) also has no
|
|
1123
|
+
// arguments but names a real remote function — only a lowercase root is a
|
|
1124
|
+
// variable receiver or field read; anything else emits (ELXD18).
|
|
1125
|
+
isMemberAccess(node) {
|
|
1126
|
+
const target = this.findChild(node, (c) => c?.type === "identifier" || c?.type === "dot");
|
|
1127
|
+
if (target?.type !== "dot") return false;
|
|
1128
|
+
if (this.findChild(node, (c) => c?.type === "arguments")) return false;
|
|
1129
|
+
const root = target.text.split(".")[0] ?? "";
|
|
1130
|
+
return root.length > 0 && root[0] >= "a" && root[0] <= "z";
|
|
1131
|
+
}
|
|
1132
|
+
// ELIXIR_FIX2-O1 — record `defoverridable`. Two shapes, both on the corpus:
|
|
1133
|
+
//
|
|
1134
|
+
// defoverridable child_spec: 1, code_change: 3 named function/arity
|
|
1135
|
+
// defoverridable EEx.Engine a whole module
|
|
1136
|
+
//
|
|
1137
|
+
// The name/arity list arrives as `keywords` or as a `list` of `pair`s /
|
|
1138
|
+
// bare `keyword`s, and the arity is irrelevant to the edge — the edge names
|
|
1139
|
+
// the FUNCTION, and Elixir's own `Module.overridable?/2` keys on name+arity,
|
|
1140
|
+
// so the arity is carried in the recorded string for readability only. No
|
|
1141
|
+
// pattern: a colon is found with indexOf and the head is a slice.
|
|
1142
|
+
recordOverridable(node, ctx, currentScope) {
|
|
1143
|
+
const owner = currentScope?.name || "";
|
|
1144
|
+
if (owner.length === 0) return;
|
|
1145
|
+
const argsNode = this.findChild(node, (c) => c?.type === "arguments");
|
|
1146
|
+
if (!argsNode) return;
|
|
1147
|
+
const addName = (name) => {
|
|
1148
|
+
if (name.length === 0) return;
|
|
1149
|
+
let set = ctx.overridableNames.get(owner);
|
|
1150
|
+
if (!set) {
|
|
1151
|
+
set = /* @__PURE__ */ new Set();
|
|
1152
|
+
ctx.overridableNames.set(owner, set);
|
|
1153
|
+
}
|
|
1154
|
+
set.add(name);
|
|
1155
|
+
};
|
|
1156
|
+
const namedKids = (n) => {
|
|
1157
|
+
const res = [];
|
|
1158
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1159
|
+
const c = n.namedChild(i);
|
|
1160
|
+
if (c) res.push(c);
|
|
1161
|
+
}
|
|
1162
|
+
return res;
|
|
1163
|
+
};
|
|
1164
|
+
const addModule = (m) => {
|
|
1165
|
+
if (m.length === 0) return;
|
|
1166
|
+
let set = ctx.overridableModules.get(owner);
|
|
1167
|
+
if (!set) {
|
|
1168
|
+
set = /* @__PURE__ */ new Set();
|
|
1169
|
+
ctx.overridableModules.set(owner, set);
|
|
1170
|
+
}
|
|
1171
|
+
set.add(this.resolveAlias(m, ctx));
|
|
1172
|
+
};
|
|
1173
|
+
const takePair = (pair) => {
|
|
1174
|
+
const kw = this.findChild(pair, (c) => c?.type === "keyword");
|
|
1175
|
+
if (!kw) return;
|
|
1176
|
+
const colon = kw.text.indexOf(":");
|
|
1177
|
+
if (colon <= 0) return;
|
|
1178
|
+
const name = kw.text.slice(0, colon);
|
|
1179
|
+
const rest = kw.text.slice(colon + 1);
|
|
1180
|
+
let digits = "";
|
|
1181
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1182
|
+
const c = rest.charCodeAt(i);
|
|
1183
|
+
if (c >= 48 && c <= 57) digits += rest[i];
|
|
1184
|
+
}
|
|
1185
|
+
addName(digits.length > 0 ? `${name}/${digits}` : name);
|
|
1186
|
+
let arityText = digits;
|
|
1187
|
+
for (let j = 0; j < pair.namedChildCount; j++) {
|
|
1188
|
+
const v = pair.namedChild(j);
|
|
1189
|
+
if (v?.type === "integer") {
|
|
1190
|
+
arityText = v.text;
|
|
1191
|
+
break;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
let marks = ctx.overridable.get(owner);
|
|
1195
|
+
if (!marks) {
|
|
1196
|
+
marks = /* @__PURE__ */ new Map();
|
|
1197
|
+
ctx.overridable.set(owner, marks);
|
|
1198
|
+
}
|
|
1199
|
+
marks.set(arityText.length > 0 ? `${name}/${arityText}` : `${name}/?`, node.startPosition.row + 1);
|
|
1200
|
+
};
|
|
1201
|
+
const visit = (n, depth) => {
|
|
1202
|
+
if (depth > 4) return;
|
|
1203
|
+
const t = String(n.type);
|
|
1204
|
+
if (t === "pair") {
|
|
1205
|
+
takePair(n);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (t === "keyword") {
|
|
1209
|
+
const colon = n.text.indexOf(":");
|
|
1210
|
+
if (colon > 0) addName(n.text.slice(0, colon));
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (t === "alias") {
|
|
1214
|
+
addModule(n.text);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (t === "identifier") {
|
|
1218
|
+
addName(n.text);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
for (const c of namedKids(n)) visit(c, depth + 1);
|
|
1222
|
+
};
|
|
1223
|
+
visit(argsNode, 0);
|
|
1224
|
+
}
|
|
1225
|
+
// EX-EX9/EX-EX14 — the `overrides` edge Elixir actually has IN one file. The
|
|
1226
|
+
// overriding definition lives in the module that `use`s the provider (a
|
|
1227
|
+
// different file, and this analyzer publishes OUTGOING edges only), so the
|
|
1228
|
+
// shape both endpoints can name here is: this module DECLARED `name/arity`
|
|
1229
|
+
// (first declaration at `priorLine`), then marked it `defoverridable` (at
|
|
1230
|
+
// `marked`), then DECLARED it again (the redefinition, at `line`). Elixir has no
|
|
1231
|
+
// classes and no superclass, so — exactly like the bash lane's BD8b — both
|
|
1232
|
+
// endpoints name the SAME function and `overridesLine` carries the prior
|
|
1233
|
+
// declaration; `contains` publishes both declarations, so both endpoints
|
|
1234
|
+
// resolve.
|
|
1235
|
+
//
|
|
1236
|
+
// The mark must come AFTER the declaration it marks (`defoverridable` appears
|
|
1237
|
+
// below its functions at every one of the corpus sites), and the redefinition
|
|
1238
|
+
// after the mark: a mark with nothing before it describes an extension point,
|
|
1239
|
+
// not an override. Promoted from analyzer/ELIXIR_EXPORTS2 (which measured the
|
|
1240
|
+
// corpus shape) and analyzer/ELIXIR4; main recorded the marks and emitted
|
|
1241
|
+
// nothing, which is why `overrides-defoverridable`'s six rows were missing.
|
|
1242
|
+
recordOverride(ctx, owner, name, arity, priorLine, line) {
|
|
1243
|
+
if (!owner || this.isPlaceholderName(name)) return;
|
|
1244
|
+
const marks = ctx.overridable.get(owner);
|
|
1245
|
+
if (!marks) return;
|
|
1246
|
+
const marked = marks.get(`${name}/${arity}`);
|
|
1247
|
+
if (marked === void 0) return;
|
|
1248
|
+
if (priorLine === void 0) return;
|
|
1249
|
+
if (priorLine >= line) return;
|
|
1250
|
+
this.pushRel(ctx, `${owner}.${name}/${arity}`, name, ASTEngine.RelationshipType.Overrides, 1, {
|
|
1251
|
+
elixirSpecific: true,
|
|
1252
|
+
redefinition: true,
|
|
1253
|
+
via: "defoverridable",
|
|
1254
|
+
overridesLine: priorLine,
|
|
1255
|
+
markedLine: marked
|
|
1256
|
+
}, line);
|
|
1257
|
+
}
|
|
1258
|
+
// ELIXIR_FIX2-U1 — the SAME predicate isMemberAccess uses, returning the two Kept next to it on purpose: if the two ever
|
|
1259
|
+
// disagree, a field read is either double-labelled (edge here AND a call) or
|
|
1260
|
+
// silently dropped (neither), and the only way to notice is the two sitting in
|
|
1261
|
+
// one file. Pure char scan — `charCodeAt` against a-z — not a pattern.
|
|
1262
|
+
memberReadLeaf(node) {
|
|
1263
|
+
const target = this.findChild(node, (c) => c?.type === "identifier" || c?.type === "dot");
|
|
1264
|
+
if (target?.type !== "dot") return void 0;
|
|
1265
|
+
if (this.findChild(node, (c) => c?.type === "arguments")) return void 0;
|
|
1266
|
+
const parts = target.text.split(".");
|
|
1267
|
+
if (parts.length < 2) return void 0;
|
|
1268
|
+
const root = parts[0] ?? "";
|
|
1269
|
+
if (root.length === 0) return void 0;
|
|
1270
|
+
const c0 = root.charCodeAt(0);
|
|
1271
|
+
if (!(c0 >= 97 && c0 <= 122 || c0 === 37)) return void 0;
|
|
1272
|
+
const leaf = parts[parts.length - 1] ?? "";
|
|
1273
|
+
if (leaf.length === 0 || leaf === "unknown") return void 0;
|
|
1274
|
+
return { leaf, receiver: root };
|
|
1275
|
+
}
|
|
1276
|
+
// EX-EX12 — the variables this DEFINITION binds to a MAP, by shape (no
|
|
1277
|
+
// patterns, no regexes):
|
|
1278
|
+
// `%{} = m` a match whose map side is a bare map -> m
|
|
1279
|
+
// `%{a: a} = m` a destructuring match -> m and a
|
|
1280
|
+
// `m = %{a: a}` the same match written the other way -> m and a
|
|
1281
|
+
// `%Mod{} = u` NOT a map (the map side carries a `struct` child) -> none
|
|
1282
|
+
// A NESTED definition owns its own bindings, so its subtree is not scanned
|
|
1283
|
+
// here — its own `processFunction` call records them under its own scope.
|
|
1284
|
+
mapBoundVariables(within) {
|
|
1285
|
+
const out = [];
|
|
1286
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1287
|
+
const add = (name) => {
|
|
1288
|
+
if (name.length === 0 || seen.has(name)) return;
|
|
1289
|
+
seen.add(name);
|
|
1290
|
+
out.push(name);
|
|
1291
|
+
};
|
|
1292
|
+
const scan = (n) => {
|
|
1293
|
+
if (n !== within && n.type === "call") {
|
|
1294
|
+
const kwt = this.findChild(n, (c) => c?.type === "identifier")?.text ?? "";
|
|
1295
|
+
if (DEF_CONSTRUCTS.has(kwt) && !this.isDefHead(n)) return;
|
|
1296
|
+
}
|
|
1297
|
+
if (n.type === "binary_operator" && this.binaryPart(n, "operator")?.text === "=") {
|
|
1298
|
+
const left = this.binaryPart(n, "left");
|
|
1299
|
+
const right = this.binaryPart(n, "right");
|
|
1300
|
+
const leftIsMap = left?.type === "map" && this.findChild(left, (c) => c?.type === "struct") === null;
|
|
1301
|
+
const rightIsMap = right?.type === "map" && this.findChild(right, (c) => c?.type === "struct") === null;
|
|
1302
|
+
if (leftIsMap || rightIsMap) {
|
|
1303
|
+
const pattern = leftIsMap ? left : right;
|
|
1304
|
+
const bound = leftIsMap ? right : left;
|
|
1305
|
+
for (const name of this.identifiersIn(pattern)) add(name);
|
|
1306
|
+
if (bound?.type === "identifier") add(bound.text);
|
|
1307
|
+
else if (bound) for (const name of this.identifiersIn(bound)) add(name);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1311
|
+
const c = n.namedChild(i);
|
|
1312
|
+
if (c) scan(c);
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
scan(within);
|
|
1316
|
+
return out;
|
|
1317
|
+
}
|
|
1318
|
+
/** The identifiers a subtree names — the keys of `%{a: a, b: b}` bind `a`/`b`.
|
|
1319
|
+
* A `pair`'s key (`:day` in `%{day: day}`) is a NAME, not a binding, so the
|
|
1320
|
+
* key child is skipped and only the value side is walked. */
|
|
1321
|
+
identifiersIn(n) {
|
|
1322
|
+
const out = [];
|
|
1323
|
+
const stack = [n];
|
|
1324
|
+
while (stack.length > 0) {
|
|
1325
|
+
const cur = stack.pop();
|
|
1326
|
+
if (cur.type === "identifier" && cur.text) out.push(cur.text);
|
|
1327
|
+
if (cur.type === "pair") {
|
|
1328
|
+
const key = this.findChild(cur, (c) => c?.type === "keyword");
|
|
1329
|
+
if (key) {
|
|
1330
|
+
for (let i = 0; i < cur.namedChildCount; i++) {
|
|
1331
|
+
const c = cur.namedChild(i);
|
|
1332
|
+
if (c && c !== key) stack.push(c);
|
|
1333
|
+
}
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
for (let i = 0; i < cur.namedChildCount; i++) {
|
|
1338
|
+
const c = cur.namedChild(i);
|
|
1339
|
+
if (c) stack.push(c);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return out;
|
|
1343
|
+
}
|
|
1344
|
+
// X6 companion: the right-hand side of `|>` is consumed by the pipe
|
|
1345
|
+
// branch (thread-adjusted arity) — its own walk must not re-emit the
|
|
1346
|
+
// written arity as a second edge once arity joins the dedupe key.
|
|
1347
|
+
// Same for the function head of a `&fun/arity` capture (the capture
|
|
1348
|
+
// branch emits it with the declared arity; the bare head is not a
|
|
1349
|
+
// zero-arity invocation).
|
|
1350
|
+
isPipeStep(node) {
|
|
1351
|
+
const p = node.parent;
|
|
1352
|
+
return !!(p && p.type === "binary_operator" && p.child(1)?.text === "|>" && p.child(2) && p.child(2).startIndex === node.startIndex);
|
|
1353
|
+
}
|
|
1354
|
+
isCaptureHead(node) {
|
|
1355
|
+
const p = node.parent;
|
|
1356
|
+
if (!p || p.type !== "binary_operator" || p.child(1)?.text !== "/") return false;
|
|
1357
|
+
const g = p.parent;
|
|
1358
|
+
return !!(g && g.type === "unary_operator" && g.child(0)?.text === "&");
|
|
1359
|
+
}
|
|
1360
|
+
shouldSkipCall(node, t) {
|
|
1361
|
+
if (SPECIAL_FORMS.has(t)) return true;
|
|
1362
|
+
if (KERNEL_FUNCTIONS.has(t)) return true;
|
|
1363
|
+
if (this.isInQuotedCode(node)) return true;
|
|
1364
|
+
if (this.isAttributeNameCall(node)) return true;
|
|
1365
|
+
if (this.isInDeclarationAttributeValue(node)) return true;
|
|
1366
|
+
if (this.isDefHead(node)) return true;
|
|
1367
|
+
if (this.isMemberAccess(node)) return true;
|
|
1368
|
+
if (t.length > 0 && (t.charCodeAt(0) === 37 || t.indexOf("{") !== -1)) return true;
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
// ==========================================================================
|
|
1372
|
+
// Pure-string helpers (no regexes).
|
|
1373
|
+
// ==========================================================================
|
|
1374
|
+
stripQuotes(text) {
|
|
1375
|
+
let out = "";
|
|
1376
|
+
for (let i = 0; i < text.length; i++) {
|
|
1377
|
+
const ch = text[i];
|
|
1378
|
+
if (ch === '"' || ch === "'") continue;
|
|
1379
|
+
out += ch;
|
|
1380
|
+
}
|
|
1381
|
+
return out;
|
|
1382
|
+
}
|
|
1383
|
+
stripParens(text) {
|
|
1384
|
+
let out = "";
|
|
1385
|
+
for (let i = 0; i < text.length; i++) {
|
|
1386
|
+
const ch = text[i];
|
|
1387
|
+
if (ch === "(" || ch === ")") continue;
|
|
1388
|
+
out += ch;
|
|
1389
|
+
}
|
|
1390
|
+
return out.trim();
|
|
1391
|
+
}
|
|
1392
|
+
stripAfterColon(text) {
|
|
1393
|
+
const colon = text.indexOf(":");
|
|
1394
|
+
return (colon >= 0 ? text.slice(0, colon) : text).trim();
|
|
1395
|
+
}
|
|
1396
|
+
// Documentation-chunk text of a string node: the grammar already excludes
|
|
1397
|
+
// delimiters (heredoc fences included) from quoted_content leaves, so
|
|
1398
|
+
// concatenating them is exact — no quote stripping that would eat
|
|
1399
|
+
// interior apostrophes (ELXD14).
|
|
1400
|
+
docContent(node) {
|
|
1401
|
+
if (node.type !== "string" && node.type !== "sigil") return "";
|
|
1402
|
+
let out = "";
|
|
1403
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1404
|
+
const c = node.namedChild(i);
|
|
1405
|
+
if (c && (c.type === "quoted_content" || c.type === "interpolation")) out += c.text;
|
|
1406
|
+
}
|
|
1407
|
+
return out;
|
|
1408
|
+
}
|
|
1409
|
+
// ==========================================================================
|
|
1410
|
+
// Node helpers.
|
|
1411
|
+
// ==========================================================================
|
|
1412
|
+
findChild(node, predicate) {
|
|
1413
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1414
|
+
const child = node.child(i);
|
|
1415
|
+
if (child && predicate(child)) return child;
|
|
1416
|
+
}
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
filterChildren(node, predicate) {
|
|
1420
|
+
const res = [];
|
|
1421
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1422
|
+
const child = node.child(i);
|
|
1423
|
+
if (child && predicate(child)) res.push(child);
|
|
1424
|
+
}
|
|
1425
|
+
return res;
|
|
1426
|
+
}
|
|
1427
|
+
// ELIXIR_FIX2-P3 — a `defmodule`/`defprotocol`/`defimpl` name must come from
|
|
1428
|
+
// an `alias` / `dot` / `identifier` node. `extractName` FALLS BACK to the
|
|
1429
|
+
// node's raw text, which is right for a directive argument (`import Foo` is
|
|
1430
|
+
// an alias, but `alias Foo.{Bar, Baz}` is a dot) and wrong for a module
|
|
1431
|
+
// declaration: found by the contains oracle at
|
|
1432
|
+
// lib/elixir/test/elixir/kernel_test.exs:906 `defmodule 1 + 2, do: :ok`
|
|
1433
|
+
// lib/elixir/test/elixir/kernel_test.exs:924 `defmodule :"foo/bar", do: :ok`
|
|
1434
|
+
// lib/elixir/test/elixir/kernel_test.exs:928 `defmodule :"foo\bar", do: :ok`
|
|
1435
|
+
// which published module symbols named `KernelTest.1 + 2, do: :ok`,
|
|
1436
|
+
// `KernelTest.:"foo/bar", do: :ok` and `KernelTest.:"foo\bar", do: :ok`. All
|
|
1437
|
+
// three are Elixir code that RAISES (the surrounding test is an
|
|
1438
|
+
// `assert_raise ArgumentError`), so the construct is a computed module name,
|
|
1439
|
+
// not a declaration, and the honest reading is silence. A name this reader
|
|
1440
|
+
// cannot produce is a name the analyzer must not invent.
|
|
1441
|
+
// R7-NAMES — a MODULE's own name, read off the first argument's node ONLY.
|
|
1442
|
+
// Promoted from `analyzer/ELIXIR4`'s `moduleNameOf` (measured there, refused
|
|
1443
|
+
// from main only by a union-parse fault): main's reader accepted `alias`/`dot`
|
|
1444
|
+
// but not the two spellings a module name can really take, so
|
|
1445
|
+
// `defmodule :"Elixir.R7Names.RawModule"` (a QUOTED ATOM carrying an absolute
|
|
1446
|
+
// name) and `defmodule :atom_named` (an Erlang module named by an ATOM)
|
|
1447
|
+
// published no module at all and left their members unowned; and
|
|
1448
|
+
// `defmodule __MODULE__.Relative` published `Parent.__MODULE__.Relative`.
|
|
1449
|
+
// `r7-module-names` pins all three shapes.
|
|
1450
|
+
//
|
|
1451
|
+
// Still rejected, and deliberately: a bare `identifier` (a VARIABLE — see
|
|
1452
|
+
// `declarationName`, whose measurement main keeps) and any computed
|
|
1453
|
+
// expression, whose "name" would be its own source text.
|
|
1454
|
+
moduleNameOf(argsNode) {
|
|
1455
|
+
const first = argsNode.namedChildCount > 0 ? argsNode.namedChild(0) : void 0;
|
|
1456
|
+
if (!first) return "";
|
|
1457
|
+
if (first.type === "atom") return first.text;
|
|
1458
|
+
if (first.type === "quoted_atom") {
|
|
1459
|
+
const inner = this.findChild(first, (c) => c?.type === "quoted_content")?.text ?? "";
|
|
1460
|
+
return this.isAliasPath(inner) ? inner : "";
|
|
1461
|
+
}
|
|
1462
|
+
if (first.type !== "alias" && first.type !== "dot") return "";
|
|
1463
|
+
const txt = first.text;
|
|
1464
|
+
if (txt.length === 0) return "";
|
|
1465
|
+
return this.isAliasPath(txt) ? txt : "";
|
|
1466
|
+
}
|
|
1467
|
+
// `__MODULE__` is not an alias segment, so it is accepted here; the caller
|
|
1468
|
+
// resolves the prefix (joinModuleName). Every other segment must begin with an
|
|
1469
|
+
// uppercase letter, which is what separates a real module name from source
|
|
1470
|
+
// text that leaked in (`1 + 2`, `:"foo/bar"`).
|
|
1471
|
+
isAliasPath(txt) {
|
|
1472
|
+
if (txt.startsWith("__MODULE__")) {
|
|
1473
|
+
const rest = txt.slice("__MODULE__".length);
|
|
1474
|
+
if (rest.length === 0) return true;
|
|
1475
|
+
if (!rest.startsWith(".")) return false;
|
|
1476
|
+
return rest.slice(1).length > 0;
|
|
1477
|
+
}
|
|
1478
|
+
let start = 0;
|
|
1479
|
+
while (start <= txt.length) {
|
|
1480
|
+
let end = txt.indexOf(".", start);
|
|
1481
|
+
if (end === -1) end = txt.length;
|
|
1482
|
+
const seg = txt.slice(start, end);
|
|
1483
|
+
if (!seg) return false;
|
|
1484
|
+
const c = seg.charAt(0);
|
|
1485
|
+
if (c !== c.toUpperCase() || c === c.toLowerCase()) return false;
|
|
1486
|
+
start = end + 1;
|
|
1487
|
+
}
|
|
1488
|
+
return true;
|
|
1489
|
+
}
|
|
1490
|
+
// R7-NAMES: a nested module is `Scope.Name`, but a name the source wrote in
|
|
1491
|
+
// FULL is already absolute and re-prefixing it doubles the namespace
|
|
1492
|
+
// (`defmodule :"Elixir.ModuleTest.RawModule"` inside ModuleTest published
|
|
1493
|
+
// `ModuleTest.:"Elixir.ModuleTest.RawModule"`), and a `__MODULE__`-relative
|
|
1494
|
+
// name resolves against the enclosing module (`defmodule __MODULE__.Relative`
|
|
1495
|
+
// published `Parent.__MODULE__.Relative`). Both shapes are pinned by
|
|
1496
|
+
// `r7-module-names` and both were measured on the corpus by analyzer/ELIXIR4.
|
|
1497
|
+
joinModuleName(scope, name) {
|
|
1498
|
+
if (!scope) return name;
|
|
1499
|
+
if (name.startsWith("__MODULE__.")) return `${scope}${name.slice("__MODULE__".length)}`;
|
|
1500
|
+
if (name.startsWith("Elixir.") || name.startsWith(":")) return name;
|
|
1501
|
+
if (name === scope || name.startsWith(scope + ".")) return name;
|
|
1502
|
+
return `${scope}.${name}`;
|
|
1503
|
+
}
|
|
1504
|
+
declarationName(argsNode) {
|
|
1505
|
+
const aliasNode = this.findChild(argsNode, (c) => c?.type === "alias" || c?.type === "dot");
|
|
1506
|
+
if (!aliasNode) return "";
|
|
1507
|
+
const txt = aliasNode.text;
|
|
1508
|
+
if (txt.length === 0) return "";
|
|
1509
|
+
for (let i = 0; i < txt.length; i++) {
|
|
1510
|
+
const c = txt.charCodeAt(i);
|
|
1511
|
+
const ok = c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 46 || c === 33;
|
|
1512
|
+
if (!ok) return "";
|
|
1513
|
+
}
|
|
1514
|
+
return txt;
|
|
1515
|
+
}
|
|
1516
|
+
extractName(node) {
|
|
1517
|
+
let aliasNode = this.findChild(node, (c) => c?.type === "alias");
|
|
1518
|
+
if (aliasNode) return aliasNode.text;
|
|
1519
|
+
if (node.namedChildCount > 0) {
|
|
1520
|
+
const first = node.namedChild(0);
|
|
1521
|
+
if (first?.type === "alias" || first?.type === "identifier") return first.text;
|
|
1522
|
+
if (first?.type === "dot") return first.text;
|
|
1523
|
+
}
|
|
1524
|
+
return this.stripParens(node.text);
|
|
1525
|
+
}
|
|
1526
|
+
// ==========================================================================
|
|
1527
|
+
// Declaration processors.
|
|
1528
|
+
// ==========================================================================
|
|
1529
|
+
processModule(node, ctx, currentScope, scopeStack) {
|
|
1530
|
+
if (this.isDefHead(node) || this.isInAttribute(node) || this.isInQuotedCode(node)) return false;
|
|
1531
|
+
const argsNode = this.findChild(node, (c) => c?.type !== "identifier" && c?.type !== "do_block");
|
|
1532
|
+
if (!argsNode) return false;
|
|
1533
|
+
if (this.isInsideDefBody(node)) return false;
|
|
1534
|
+
const name = this.moduleNameOf(argsNode);
|
|
1535
|
+
if (name.length === 0) {
|
|
1536
|
+
scopeStack.push({ node, name: currentScope?.name ?? "", kind: ASTEngine.SymbolKind.Module, complexity: 0, unnamedModule: true, savedAliases: ctx.aliases });
|
|
1537
|
+
ctx.aliases = Object.create(ctx.aliases);
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
const fullName = this.joinModuleName(currentScope?.name, name);
|
|
1541
|
+
const isAbsoluteName = name.length > 1 && name.charCodeAt(0) === 58;
|
|
1542
|
+
const lexicalOwner = isAbsoluteName ? void 0 : currentScope?.name;
|
|
1543
|
+
const symbol = {
|
|
1544
|
+
name: fullName,
|
|
1545
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
1546
|
+
range: this.nodeToRange(node),
|
|
1547
|
+
scope: lexicalOwner || "",
|
|
1548
|
+
ownerName: lexicalOwner,
|
|
1549
|
+
ownerKind: lexicalOwner ? currentScope?.kind : void 0,
|
|
1550
|
+
visibility: "public",
|
|
1551
|
+
metadata: { elixirSpecific: true }
|
|
1552
|
+
};
|
|
1553
|
+
ctx.symbols.push(symbol);
|
|
1554
|
+
ctx.symbolByName.set(fullName, symbol);
|
|
1555
|
+
ctx.pendingDoc = void 0;
|
|
1556
|
+
ctx.pendingImpl = void 0;
|
|
1557
|
+
scopeStack.push({ node, name: fullName, kind: ASTEngine.SymbolKind.Module, complexity: 0, symbol, savedAliases: ctx.aliases });
|
|
1558
|
+
ctx.aliases = Object.create(ctx.aliases);
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
processProtocol(node, ctx, currentScope, scopeStack) {
|
|
1562
|
+
if (this.isDefHead(node) || this.isInAttribute(node) || this.isInQuotedCode(node)) return false;
|
|
1563
|
+
const argsNode = this.findChild(node, (c) => c?.type !== "identifier" && c?.type !== "do_block");
|
|
1564
|
+
if (!argsNode) return false;
|
|
1565
|
+
if (this.isInsideDefBody(node)) return false;
|
|
1566
|
+
const name = this.moduleNameOf(argsNode);
|
|
1567
|
+
if (name.length === 0) {
|
|
1568
|
+
scopeStack.push({ node, name: currentScope?.name ?? "", kind: ASTEngine.SymbolKind.Module, complexity: 0, unnamedModule: true, savedAliases: ctx.aliases });
|
|
1569
|
+
ctx.aliases = Object.create(ctx.aliases);
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
const fullName = this.joinModuleName(currentScope?.name, name);
|
|
1573
|
+
const protocolSym = { name: fullName, kind: ASTEngine.SymbolKind.Interface, range: this.nodeToRange(node), scope: currentScope?.name || "", ownerName: currentScope?.name, ownerKind: currentScope?.kind, visibility: "public", metadata: { elixirSpecific: true, isProtocol: true } };
|
|
1574
|
+
ctx.symbols.push(protocolSym);
|
|
1575
|
+
ctx.symbolByName.set(fullName, protocolSym);
|
|
1576
|
+
ctx.pendingImpl = void 0;
|
|
1577
|
+
ctx.pendingDoc = void 0;
|
|
1578
|
+
scopeStack.push({ node, name: fullName, kind: ASTEngine.SymbolKind.Interface, complexity: 0, symbol: protocolSym, savedAliases: ctx.aliases });
|
|
1579
|
+
ctx.aliases = Object.create(ctx.aliases);
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
processImpl(node, ctx, currentScope, scopeStack) {
|
|
1583
|
+
ctx.pendingImpl = void 0;
|
|
1584
|
+
if (this.isDefHead(node) || this.isInAttribute(node) || this.isInQuotedCode(node)) return false;
|
|
1585
|
+
const argsNode = this.findChild(node, (c) => c?.type !== "identifier" && c?.type !== "do_block");
|
|
1586
|
+
if (!argsNode) return false;
|
|
1587
|
+
const aliases = this.filterChildren(argsNode, (c) => c?.type === "alias");
|
|
1588
|
+
const protocol = aliases.length > 0 && aliases[0] ? this.resolveAlias(aliases[0].text, ctx) : "Unknown";
|
|
1589
|
+
let forType = "Unknown";
|
|
1590
|
+
const forTypeList = [];
|
|
1591
|
+
let forTypeListNode = null;
|
|
1592
|
+
const keywordsNode = this.findChild(argsNode, (c) => c?.type === "keywords");
|
|
1593
|
+
if (keywordsNode) {
|
|
1594
|
+
for (let i = 0; i < keywordsNode.namedChildCount; i++) {
|
|
1595
|
+
const pair = keywordsNode.namedChild(i);
|
|
1596
|
+
if (!pair || pair.type !== "pair") continue;
|
|
1597
|
+
const kw = this.findChild(pair, (c) => c?.type === "keyword");
|
|
1598
|
+
if (kw && this.stripAfterColon(kw.text) === "for" && pair.namedChildCount > 1) {
|
|
1599
|
+
const val = pair.namedChild(1);
|
|
1600
|
+
if (!val) continue;
|
|
1601
|
+
if (val.type === "list") {
|
|
1602
|
+
forTypeListNode = val;
|
|
1603
|
+
for (let k = 0; k < val.namedChildCount; k++) {
|
|
1604
|
+
const el = val.namedChild(k);
|
|
1605
|
+
if (el && el.type === "alias") forTypeList.push(this.resolveAlias(el.text, ctx));
|
|
1606
|
+
}
|
|
1607
|
+
forType = forTypeList.length > 0 ? forTypeList[0] : "Unknown";
|
|
1608
|
+
} else {
|
|
1609
|
+
forType = this.resolveAlias(val.text, ctx);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const forTypeAlias = aliases.length > 1 ? this.resolveAlias(aliases[1].text, ctx) : forType;
|
|
1615
|
+
const implName = forTypeAlias && forTypeAlias !== "Unknown" ? forTypeAlias : currentScope ? currentScope.name : protocol;
|
|
1616
|
+
const fullName = implName;
|
|
1617
|
+
const resolvedForType = forTypeList.length > 0 ? forTypeList : forType !== "Unknown" ? forType : implName !== protocol ? implName : void 0;
|
|
1618
|
+
const implSym = { name: fullName, kind: ASTEngine.SymbolKind.Class, range: this.nodeToRange(node), scope: currentScope?.name || "", visibility: "public", metadata: { elixirSpecific: true, isProtocolImplementation: true, protocol, ...resolvedForType !== void 0 ? { forType: resolvedForType } : {} } };
|
|
1619
|
+
ctx.symbols.push(implSym);
|
|
1620
|
+
ctx.symbolByName.set(fullName, implSym);
|
|
1621
|
+
if (forTypeList.length > 1) {
|
|
1622
|
+
for (let ti = 1; ti < forTypeList.length; ti++) {
|
|
1623
|
+
const tSym = { name: forTypeList[ti], kind: ASTEngine.SymbolKind.Class, range: this.nodeToRange(node), scope: currentScope?.name || "", visibility: "public", metadata: { elixirSpecific: true, isProtocolImplementation: true, protocol, forType: forTypeList[ti] } };
|
|
1624
|
+
ctx.symbols.push(tSym);
|
|
1625
|
+
ctx.symbolByName.set(forTypeList[ti], tSym);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const implLineNode = aliases.length > 1 ? aliases[1] : aliases[0];
|
|
1629
|
+
if (forTypeListNode && forTypeList.length > 0) {
|
|
1630
|
+
for (let k = 0; k < forTypeListNode.namedChildCount; k++) {
|
|
1631
|
+
const el = forTypeListNode.namedChild(k);
|
|
1632
|
+
if (!el || el.type !== "alias") continue;
|
|
1633
|
+
const t = this.resolveAlias(el.text, ctx);
|
|
1634
|
+
if (t && t !== "Unknown" && t !== protocol) this.pushRel(ctx, t, protocol, ASTEngine.RelationshipType.Implements, 1, { reasonCode: "elixir_defimpl_for_type" }, el.startPosition.row + 1);
|
|
1635
|
+
}
|
|
1636
|
+
} else if (fullName !== protocol) {
|
|
1637
|
+
this.pushRel(ctx, fullName, protocol, ASTEngine.RelationshipType.Implements, 1, { reasonCode: "elixir_defimpl_protocol" }, implLineNode ? implLineNode.startPosition.row + 1 : node.startPosition.row + 1);
|
|
1638
|
+
}
|
|
1639
|
+
ctx.pendingDoc = void 0;
|
|
1640
|
+
scopeStack.push({ node, name: fullName, kind: ASTEngine.SymbolKind.Class, complexity: 0, symbol: implSym, savedAliases: ctx.aliases });
|
|
1641
|
+
ctx.aliases = Object.create(ctx.aliases);
|
|
1642
|
+
return true;
|
|
1643
|
+
}
|
|
1644
|
+
processStruct(node, ctx, currentScope) {
|
|
1645
|
+
if (this.isDefHead(node) || this.isInAttribute(node) || this.isInQuotedCode(node)) return;
|
|
1646
|
+
if (!currentScope) return;
|
|
1647
|
+
if (currentScope.unnamedModule) return;
|
|
1648
|
+
const structName = currentScope.name;
|
|
1649
|
+
const structSym = { name: structName, kind: ASTEngine.SymbolKind.Struct, range: this.nodeToRange(node), scope: currentScope.name, ownerName: currentScope.name, ownerKind: currentScope.kind, visibility: "public", metadata: { elixirSpecific: true, isStruct: true } };
|
|
1650
|
+
ctx.symbols.push(structSym);
|
|
1651
|
+
if (ctx.pendingDoc) {
|
|
1652
|
+
structSym.comments = [ctx.pendingDoc];
|
|
1653
|
+
structSym.docstring = ctx.pendingDoc;
|
|
1654
|
+
ctx.pendingDoc = void 0;
|
|
1655
|
+
}
|
|
1656
|
+
const argsNode = this.findChild(node, (c) => c?.type === "arguments");
|
|
1657
|
+
if (argsNode) {
|
|
1658
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1659
|
+
const child = argsNode.namedChild(i);
|
|
1660
|
+
if (!child) continue;
|
|
1661
|
+
const kwGroups = child.type === "keywords" ? [child] : child.type === "list" ? this.filterChildren(child, (c) => c?.type === "keywords") : [];
|
|
1662
|
+
if (child.type === "list") {
|
|
1663
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1664
|
+
const el = child.namedChild(j);
|
|
1665
|
+
if (!el || el.type !== "atom") continue;
|
|
1666
|
+
const fieldName = el.text.startsWith(":") ? el.text.slice(1) : el.text;
|
|
1667
|
+
if (!fieldName) continue;
|
|
1668
|
+
ctx.symbols.push({ name: fieldName, kind: ASTEngine.SymbolKind.Property, range: this.nodeToRange(el), ownerName: currentScope.name, ownerKind: currentScope.kind, scope: currentScope.name, visibility: "public", metadata: { elixirSpecific: true, isStructField: true } });
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
for (const kwGroup of kwGroups) {
|
|
1672
|
+
for (let j = 0; j < kwGroup.namedChildCount; j++) {
|
|
1673
|
+
const pair = kwGroup.namedChild(j);
|
|
1674
|
+
if (!pair || pair.type !== "pair") continue;
|
|
1675
|
+
const kwNode = this.findChild(pair, (c) => c?.type === "keyword" || c?.type === "quoted_keyword");
|
|
1676
|
+
if (!kwNode?.text) continue;
|
|
1677
|
+
const fieldName = this.stripQuotes(this.stripAfterColon(kwNode.text));
|
|
1678
|
+
if (!fieldName) continue;
|
|
1679
|
+
ctx.symbols.push({ name: fieldName, kind: ASTEngine.SymbolKind.Property, range: this.nodeToRange(pair), ownerName: currentScope.name, ownerKind: currentScope.kind, scope: currentScope.name, visibility: "public", metadata: { elixirSpecific: true, isStructField: true } });
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* M60 (#924, elixir E): the parts of a binary_operator read RELATIVE TO ITS OPERATOR
|
|
1687
|
+
* TOKEN, never by absolute child index. A comment between a definition head and its
|
|
1688
|
+
* `when` guard is an extra node that lands at index 1, so `child(1)` published the
|
|
1689
|
+
* COMMENT as the function name (measured: list.ex:783; node shape in
|
|
1690
|
+
* script/m14/tmp/c924-elixir-head-shape.ts — [0]left call, [1] comment,
|
|
1691
|
+
* [2]operator `when`, [3]right guard).
|
|
1692
|
+
*/
|
|
1693
|
+
binaryPart(node, part) {
|
|
1694
|
+
let opIdx = -1;
|
|
1695
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1696
|
+
const c = node.child(i);
|
|
1697
|
+
if (c && !c.isNamed) {
|
|
1698
|
+
opIdx = i;
|
|
1699
|
+
break;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (opIdx === -1) return null;
|
|
1703
|
+
if (part === "operator") return node.child(opIdx);
|
|
1704
|
+
const isTrivia = (c) => c.type === "comment";
|
|
1705
|
+
if (part === "left") {
|
|
1706
|
+
for (let i = opIdx - 1; i >= 0; i--) {
|
|
1707
|
+
const c = node.child(i);
|
|
1708
|
+
if (c && c.isNamed && !isTrivia(c)) return c;
|
|
1709
|
+
}
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
for (let i = opIdx + 1; i < node.childCount; i++) {
|
|
1713
|
+
const c = node.child(i);
|
|
1714
|
+
if (c && c.isNamed && !isTrivia(c)) return c;
|
|
1715
|
+
}
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
processFunction(node, defType, ctx, currentScope, scopeStack) {
|
|
1719
|
+
if (this.isDefHead(node) || this.isInAttribute(node) || this.isInQuotedCode(node)) return false;
|
|
1720
|
+
if (this.isInsideDefBody(node)) return false;
|
|
1721
|
+
const sigNodes = this.filterChildren(node, (c) => c?.type !== "identifier" && c?.type !== "do_block");
|
|
1722
|
+
if (sigNodes.length === 0) return false;
|
|
1723
|
+
let actualCall;
|
|
1724
|
+
let guardList = [];
|
|
1725
|
+
let name = "Unknown";
|
|
1726
|
+
let hasPattern = false;
|
|
1727
|
+
const parameters = [];
|
|
1728
|
+
let sigNode = sigNodes[0];
|
|
1729
|
+
if (sigNode && sigNode.type === "arguments" && sigNode.namedChildCount > 0) {
|
|
1730
|
+
const first = sigNode.namedChild(0);
|
|
1731
|
+
if (first) sigNode = first;
|
|
1732
|
+
}
|
|
1733
|
+
if (sigNode) {
|
|
1734
|
+
if (sigNode.type === "binary_operator" && this.binaryPart(sigNode, "operator")?.text === "when") {
|
|
1735
|
+
const guardExpr = this.binaryPart(sigNode, "right");
|
|
1736
|
+
if (guardExpr) guardList.push(guardExpr.text);
|
|
1737
|
+
actualCall = this.binaryPart(sigNode, "left") || void 0;
|
|
1738
|
+
ctx.patterns["Pattern Matching"] = true;
|
|
1739
|
+
hasPattern = true;
|
|
1740
|
+
} else {
|
|
1741
|
+
actualCall = sigNode;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
if (actualCall) {
|
|
1745
|
+
if (actualCall.type === "call") {
|
|
1746
|
+
const innerId = this.findChild(actualCall, (c) => c?.type === "identifier");
|
|
1747
|
+
if (innerId) name = innerId.text;
|
|
1748
|
+
const innerArgs = this.findChild(actualCall, (c) => c?.type === "arguments");
|
|
1749
|
+
if (innerArgs) {
|
|
1750
|
+
for (let i = 0; i < innerArgs.namedChildCount; i++) {
|
|
1751
|
+
const argChild = innerArgs.namedChild(i);
|
|
1752
|
+
if (!argChild) continue;
|
|
1753
|
+
let paramName = "_";
|
|
1754
|
+
if (argChild.type === "identifier") paramName = argChild.text;
|
|
1755
|
+
else if (argChild.type === "binary_operator" && argChild.childCount > 0 && argChild.child(0)?.type === "identifier") paramName = argChild.child(0).text;
|
|
1756
|
+
parameters.push({ name: paramName, type: "dynamic" });
|
|
1757
|
+
if (argChild.text.indexOf(":") !== -1 || argChild.text.indexOf("%") !== -1 || argChild.text.indexOf("_") !== -1 || argChild.text.startsWith("[")) {
|
|
1758
|
+
hasPattern = true;
|
|
1759
|
+
ctx.patterns["Pattern Matching"] = true;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
} else if (actualCall.type === "identifier") {
|
|
1764
|
+
name = actualCall.text;
|
|
1765
|
+
} else if (actualCall.type === "binary_operator") {
|
|
1766
|
+
name = actualCall.childCount > 1 ? actualCall.child(1)?.text || "Unknown" : "Unknown";
|
|
1767
|
+
parameters.push({ name: actualCall.childCount > 0 ? actualCall.child(0)?.text || "left" : "left", type: "dynamic" });
|
|
1768
|
+
parameters.push({ name: actualCall.childCount > 2 ? actualCall.child(2)?.text || "right" : "right", type: "dynamic" });
|
|
1769
|
+
} else if (actualCall.type === "block" || this.findChild(actualCall, (c) => c?.type === "operator_identifier") !== null) {
|
|
1770
|
+
const opNode = this.findChild(actualCall, (c) => c?.type === "operator_identifier");
|
|
1771
|
+
if (opNode?.text) {
|
|
1772
|
+
name = opNode.text;
|
|
1773
|
+
const siblingArgs = this.filterChildren(actualCall.parent ?? { namedChildCount: 0, namedChild: () => null }, (c) => c?.type === "arguments")[0];
|
|
1774
|
+
if (siblingArgs) {
|
|
1775
|
+
for (let i = 0; i < siblingArgs.namedChildCount; i++) {
|
|
1776
|
+
const a = siblingArgs.namedChild(i);
|
|
1777
|
+
if (a) parameters.push({ name: a.type === "identifier" ? a.text : "_", type: "dynamic" });
|
|
1778
|
+
}
|
|
1779
|
+
} else {
|
|
1780
|
+
parameters.push({ name: "operand", type: "dynamic" });
|
|
1781
|
+
}
|
|
1782
|
+
} else {
|
|
1783
|
+
name = this.binaryPart(actualCall, "operator")?.text || "Unknown";
|
|
1784
|
+
parameters.push({ name: this.binaryPart(actualCall, "left")?.text || "left", type: "dynamic" });
|
|
1785
|
+
parameters.push({ name: this.binaryPart(actualCall, "right")?.text || "right", type: "dynamic" });
|
|
1786
|
+
}
|
|
1787
|
+
} else if (actualCall.type === "unary_operator") {
|
|
1788
|
+
for (let i = 0; i < actualCall.childCount; i++) {
|
|
1789
|
+
const c = actualCall.child(i);
|
|
1790
|
+
if (c && !c.isNamed) {
|
|
1791
|
+
name = c.text;
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
const operand = this.findChild(actualCall, (c) => c?.type === "identifier");
|
|
1796
|
+
parameters.push({ name: operand?.text || "operand", type: "dynamic" });
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
const isDeclaration = !this.hasDefBody(node) && defType === "def";
|
|
1800
|
+
const visibility = defType === "defp" || defType === "defmacrop" || defType === "defguardp" ? "private" : "public";
|
|
1801
|
+
let isGenCallback = false, isSupCallback = false;
|
|
1802
|
+
if (name === "init" || name === "handle_call" || name === "handle_cast" || name === "handle_info" || name === "terminate") isGenCallback = true;
|
|
1803
|
+
let isSupervisor = false;
|
|
1804
|
+
if (currentScope) {
|
|
1805
|
+
const ownerSym = ctx.symbolByName.get(currentScope.name);
|
|
1806
|
+
if (ownerSym?.metadata?.isSupervisor) isSupervisor = true;
|
|
1807
|
+
}
|
|
1808
|
+
if (name === "init" && isSupervisor) isSupCallback = true;
|
|
1809
|
+
const arity = parameters.length;
|
|
1810
|
+
const fnId = `${currentScope?.name || ""}.${name}/${arity}`;
|
|
1811
|
+
const declLine = node.startPosition.row + 1;
|
|
1812
|
+
this.recordOverride(ctx, currentScope?.name, name, arity, ctx.lastDeclLine.get(fnId), declLine);
|
|
1813
|
+
ctx.lastDeclLine.set(fnId, declLine);
|
|
1814
|
+
const pendingImpl = ctx.pendingImpl;
|
|
1815
|
+
ctx.pendingImpl = void 0;
|
|
1816
|
+
if (pendingImpl && name !== "Unknown" && !isDeclaration) {
|
|
1817
|
+
const implOwner = currentScope?.name ?? "";
|
|
1818
|
+
let contract = "";
|
|
1819
|
+
if (pendingImpl.candidates.length === 1) contract = pendingImpl.candidates[0];
|
|
1820
|
+
else {
|
|
1821
|
+
const declared = ctx.moduleCallbacks[implOwner] ?? [];
|
|
1822
|
+
if (declared.includes(name)) {
|
|
1823
|
+
const owners = pendingImpl.candidates.filter((b) => (ctx.moduleCallbacks[b] ?? []).includes(name));
|
|
1824
|
+
if (owners.length === 1) contract = owners[0];
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
if (contract) {
|
|
1828
|
+
this.pushRel(
|
|
1829
|
+
ctx,
|
|
1830
|
+
fnId,
|
|
1831
|
+
`${contract}.${name}`,
|
|
1832
|
+
ASTEngine.RelationshipType.Overrides,
|
|
1833
|
+
1,
|
|
1834
|
+
{ elixirSpecific: true, evidenceClass: "structural", overrideForm: pendingImpl.form, reasonCode: "elixir_impl_annotation" },
|
|
1835
|
+
declLine
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
let symbolKind = ASTEngine.SymbolKind.Function;
|
|
1840
|
+
if (defType === "defmacro" || defType === "defmacrop" || defType === "defguard" || defType === "defguardp") {
|
|
1841
|
+
symbolKind = ASTEngine.SymbolKind.Macro;
|
|
1842
|
+
} else if (currentScope && (currentScope.kind === ASTEngine.SymbolKind.Interface || currentScope.kind === ASTEngine.SymbolKind.Class)) {
|
|
1843
|
+
symbolKind = ASTEngine.SymbolKind.Method;
|
|
1844
|
+
}
|
|
1845
|
+
const specKey = `${currentScope?.name ?? ""}/${name}/${arity}`;
|
|
1846
|
+
const parkedSpec = ctx.callbackSpecs[specKey];
|
|
1847
|
+
const parkedOptional = currentScope?.name ? ctx.optionalCallbacks[currentScope.name] : void 0;
|
|
1848
|
+
if (ctx.functionHeads[fnId]) {
|
|
1849
|
+
const existing = ctx.functionHeads[fnId];
|
|
1850
|
+
if (!isDeclaration) existing.metadata.heads = existing.metadata.heads + 1;
|
|
1851
|
+
if (parkedSpec) existing.metadata.callbackSpec = parkedSpec;
|
|
1852
|
+
if (isDeclaration) {
|
|
1853
|
+
if (!existing.isDeclaration) {
|
|
1854
|
+
existing.isDeclaration = true;
|
|
1855
|
+
existing.metadata.isBodylessDeclaration = true;
|
|
1856
|
+
}
|
|
1857
|
+
existing.metadata.isDeclaration = true;
|
|
1858
|
+
} else {
|
|
1859
|
+
existing.isDeclaration = void 0;
|
|
1860
|
+
existing.metadata.isBodylessDeclaration = void 0;
|
|
1861
|
+
if (parkedSpec) existing.metadata.isCallbackDeclaration = true;
|
|
1862
|
+
}
|
|
1863
|
+
this.extendRange(existing, this.nodeToRange(node));
|
|
1864
|
+
let clauseRanges = existing.metadata.clauseRanges;
|
|
1865
|
+
if (!clauseRanges) {
|
|
1866
|
+
clauseRanges = [];
|
|
1867
|
+
existing.metadata.clauseRanges = clauseRanges;
|
|
1868
|
+
}
|
|
1869
|
+
clauseRanges.push(this.nodeToRange(node));
|
|
1870
|
+
if (hasPattern) existing.metadata.hasPatternMatching = true;
|
|
1871
|
+
if (guardList.length > 0) {
|
|
1872
|
+
let existingGuards = existing.metadata.guards || [];
|
|
1873
|
+
for (let i = 0; i < guardList.length; i++) {
|
|
1874
|
+
existingGuards.push(guardList[i]);
|
|
1875
|
+
}
|
|
1876
|
+
existing.metadata.guards = existingGuards;
|
|
1877
|
+
}
|
|
1878
|
+
} else {
|
|
1879
|
+
const symbol = {
|
|
1880
|
+
name,
|
|
1881
|
+
kind: symbolKind,
|
|
1882
|
+
range: this.nodeToRange(node),
|
|
1883
|
+
scope: currentScope?.unnamedModule ? "" : currentScope?.name || "",
|
|
1884
|
+
ownerName: currentScope?.unnamedModule ? void 0 : currentScope?.name,
|
|
1885
|
+
ownerKind: currentScope?.unnamedModule ? void 0 : currentScope?.kind,
|
|
1886
|
+
visibility,
|
|
1887
|
+
parameters,
|
|
1888
|
+
...isDeclaration ? { isDeclaration: true } : {},
|
|
1889
|
+
metadata: {
|
|
1890
|
+
elixirSpecific: true,
|
|
1891
|
+
isMacro: defType.indexOf("macro") !== -1,
|
|
1892
|
+
arity,
|
|
1893
|
+
heads: 1,
|
|
1894
|
+
clauseRanges: [this.nodeToRange(node)],
|
|
1895
|
+
// ELIXIR_FIX2-P6: the declaration is real, its OWNER is not
|
|
1896
|
+
// knowable (the enclosing defmodule's name is a variable or a
|
|
1897
|
+
// computed expression). Recorded so a consumer can see WHY the
|
|
1898
|
+
// symbol has no owner, instead of inferring a bug.
|
|
1899
|
+
...currentScope?.unnamedModule ? { elixirOwnerUnknown: "un-nameable enclosing defmodule/defprotocol" } : {},
|
|
1900
|
+
hasPatternMatching: hasPattern,
|
|
1901
|
+
guards: guardList,
|
|
1902
|
+
isGenServerCallback: isGenCallback,
|
|
1903
|
+
isSupervisorCallback: isSupCallback,
|
|
1904
|
+
...isDeclaration ? { isBodylessDeclaration: true, isDeclaration: true } : {},
|
|
1905
|
+
...parkedSpec ? { callbackSpec: parkedSpec, isCallbackDeclaration: true } : {},
|
|
1906
|
+
...parkedOptional && parkedOptional.includes(name) ? { isOptionalCallback: true } : {}
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
ctx.symbols.push(symbol);
|
|
1910
|
+
ctx.functionHeads[fnId] = symbol;
|
|
1911
|
+
}
|
|
1912
|
+
const fnSym = ctx.functionHeads[fnId];
|
|
1913
|
+
if (ctx.pendingDoc) {
|
|
1914
|
+
if (!fnSym.comments) fnSym.comments = [ctx.pendingDoc];
|
|
1915
|
+
if (!fnSym.docstring) fnSym.docstring = ctx.pendingDoc;
|
|
1916
|
+
ctx.pendingDoc = void 0;
|
|
1917
|
+
}
|
|
1918
|
+
if (defType === "defdelegate" && name !== "Unknown") {
|
|
1919
|
+
let toMod = "";
|
|
1920
|
+
let asName = "";
|
|
1921
|
+
let toLine = node.startPosition.row + 1;
|
|
1922
|
+
const dscan = [node];
|
|
1923
|
+
while (dscan.length > 0) {
|
|
1924
|
+
const x = dscan.pop();
|
|
1925
|
+
if (x.type === "pair") {
|
|
1926
|
+
const kw = this.findChild(x, (c) => c?.type === "keyword");
|
|
1927
|
+
const kt = kw ? this.stripAfterColon(kw.text) : "";
|
|
1928
|
+
if (kt === "to" && x.namedChildCount > 1) {
|
|
1929
|
+
const v = x.namedChild(1);
|
|
1930
|
+
if (v?.type === "alias") {
|
|
1931
|
+
toMod = v.text;
|
|
1932
|
+
toLine = v.startPosition.row + 1;
|
|
1933
|
+
}
|
|
1934
|
+
} else if (kt === "as" && x.namedChildCount > 1) {
|
|
1935
|
+
const v = x.namedChild(1);
|
|
1936
|
+
if (v?.type === "atom") asName = v.text.startsWith(":") ? v.text.slice(1) : v.text;
|
|
1937
|
+
}
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
if (x !== node && (x.type === "do_block" || x.type === "anonymous_function")) continue;
|
|
1941
|
+
for (let i = x.namedChildCount - 1; i >= 0; i--) {
|
|
1942
|
+
const c = x.namedChild(i);
|
|
1943
|
+
if (c) dscan.push(c);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (toMod) {
|
|
1947
|
+
const resolved = this.resolveAlias(toMod, ctx);
|
|
1948
|
+
const delScope = currentScope ? `${currentScope.name}.${name}/${arity}` : `${name}/${arity}`;
|
|
1949
|
+
this.pushRel(ctx, delScope, `${resolved}.${asName || name}`, ASTEngine.RelationshipType.Calls, 0.9, { elixirSpecific: true, isDelegate: true, reasonCode: "elixir_defdelegate_remote_target" }, toLine);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const scopeName = currentScope ? `${currentScope.name}.${name}/${arity}` : `${name}/${arity}`;
|
|
1953
|
+
scopeStack.push({ node, name: scopeName, kind: symbolKind, complexity: 0, symbol: fnSym, savedAliases: ctx.aliases });
|
|
1954
|
+
if (ctx.content.includes("%{")) {
|
|
1955
|
+
const bound = this.mapBoundVariables(node);
|
|
1956
|
+
if (bound.length > 0) {
|
|
1957
|
+
let set = ctx.mapBoundByScope.get(scopeName);
|
|
1958
|
+
if (!set) {
|
|
1959
|
+
set = /* @__PURE__ */ new Set();
|
|
1960
|
+
ctx.mapBoundByScope.set(scopeName, set);
|
|
1961
|
+
}
|
|
1962
|
+
for (const b of bound) set.add(b);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
ctx.aliases = Object.create(ctx.aliases);
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
processDirective(node, target, ctx, currentScope, scopeStack) {
|
|
1969
|
+
if (this.isDefHead(node)) return;
|
|
1970
|
+
if (this.isInAttribute(node)) return;
|
|
1971
|
+
const argsNode = this.findChild(node, (c) => c?.type !== "identifier");
|
|
1972
|
+
if (!argsNode) return;
|
|
1973
|
+
const rawModuleName = this.extractDirectiveName(argsNode);
|
|
1974
|
+
if (!rawModuleName) return;
|
|
1975
|
+
let modScope;
|
|
1976
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
1977
|
+
const k = scopeStack[i].kind;
|
|
1978
|
+
if (k === ASTEngine.SymbolKind.Module || k === ASTEngine.SymbolKind.Interface) {
|
|
1979
|
+
modScope = scopeStack[i].name;
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const resolveSelf = (name) => {
|
|
1984
|
+
if (!name.startsWith("__MODULE__") || !modScope) return name;
|
|
1985
|
+
return modScope + name.slice("__MODULE__".length);
|
|
1986
|
+
};
|
|
1987
|
+
const moduleName = this.resolveAlias(resolveSelf(rawModuleName), ctx);
|
|
1988
|
+
if (target === "use" && modScope && moduleName.length > 0) {
|
|
1989
|
+
let set = ctx.moduleUses.get(modScope);
|
|
1990
|
+
if (!set) {
|
|
1991
|
+
set = /* @__PURE__ */ new Set();
|
|
1992
|
+
ctx.moduleUses.set(modScope, set);
|
|
1993
|
+
}
|
|
1994
|
+
set.add(moduleName);
|
|
1995
|
+
}
|
|
1996
|
+
let deps = [{ name: moduleName, line: argsNode.startPosition.row + 1 }];
|
|
1997
|
+
if (target !== "alias") {
|
|
1998
|
+
const groupDot = this.findChild(argsNode, (c) => c?.type === "dot" && !!this.findChild(c, (d) => d?.type === "tuple"));
|
|
1999
|
+
const groupTuple = groupDot ? this.findChild(groupDot, (c) => c?.type === "tuple") : void 0;
|
|
2000
|
+
if (groupDot && groupTuple) {
|
|
2001
|
+
deps = [];
|
|
2002
|
+
const rawPrefix = this.findChild(groupDot, (c) => c?.type === "alias")?.text ?? this.findChild(groupDot, (c) => c?.type === "identifier" && c.text === "__MODULE__")?.text ?? "";
|
|
2003
|
+
const prefix = this.resolveAlias(resolveSelf(rawPrefix), ctx);
|
|
2004
|
+
for (const kid of this.filterChildren(groupTuple, (c) => c?.type === "alias" || c?.type === "dot")) {
|
|
2005
|
+
const full = prefix ? `${prefix}.${kid.text}` : kid.text;
|
|
2006
|
+
if (full) deps.push({ name: full, line: kid.startPosition.row + 1 });
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
if (target === "alias") {
|
|
2011
|
+
const multiDot = this.findChild(argsNode, (c) => c?.type === "dot" && !!this.findChild(c, (d) => d?.type === "tuple"));
|
|
2012
|
+
const tuple = multiDot ? this.findChild(multiDot, (c) => c?.type === "tuple") : void 0;
|
|
2013
|
+
if (tuple) {
|
|
2014
|
+
deps = [];
|
|
2015
|
+
const rawPrefix = this.findChild(multiDot, (c) => c?.type === "alias")?.text ?? this.findChild(multiDot, (c) => c?.type === "identifier" && c.text === "__MODULE__")?.text ?? "";
|
|
2016
|
+
const prefix = this.resolveAlias(resolveSelf(rawPrefix), ctx);
|
|
2017
|
+
for (const kid of this.filterChildren(tuple, (c) => c?.type === "alias" || c?.type === "dot" || c?.type === "identifier")) {
|
|
2018
|
+
const full = prefix ? `${prefix}.${kid.text}` : kid.text;
|
|
2019
|
+
const key = kid.text.split(".").pop();
|
|
2020
|
+
if (key && full) {
|
|
2021
|
+
ctx.aliases[key] = full;
|
|
2022
|
+
deps.push({ name: full, line: kid.startPosition.row + 1 });
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
} else {
|
|
2026
|
+
let asName = null;
|
|
2027
|
+
const keywordsNode = this.findChild(argsNode, (c) => c?.type === "keywords");
|
|
2028
|
+
if (keywordsNode) {
|
|
2029
|
+
for (let i = 0; i < keywordsNode.namedChildCount; i++) {
|
|
2030
|
+
const pair = keywordsNode.namedChild(i);
|
|
2031
|
+
if (!pair || pair.type !== "pair") continue;
|
|
2032
|
+
const kw = this.findChild(pair, (c) => c?.type === "keyword");
|
|
2033
|
+
if (kw && this.stripAfterColon(kw.text) === "as" && pair.namedChildCount > 1) {
|
|
2034
|
+
const val = pair.namedChild(1);
|
|
2035
|
+
if (val) asName = val.text;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
const parts = moduleName.split(".");
|
|
2040
|
+
const defaultAs = parts[parts.length - 1];
|
|
2041
|
+
const key = asName ?? defaultAs;
|
|
2042
|
+
if (key) ctx.aliases[key] = moduleName;
|
|
2043
|
+
}
|
|
2044
|
+
} else if (target === "use") {
|
|
2045
|
+
const fwRoot = moduleName.split(".")[0];
|
|
2046
|
+
if (fwRoot === "Plug") ctx.frameworks["Plug"] = true;
|
|
2047
|
+
if (fwRoot === "Phoenix") ctx.frameworks["Phoenix"] = true;
|
|
2048
|
+
if (moduleName === "Phoenix.LiveView" || moduleName.startsWith("Phoenix.LiveView.")) ctx.frameworks["Phoenix LiveView"] = true;
|
|
2049
|
+
if (fwRoot === "Ecto") ctx.frameworks["Ecto"] = true;
|
|
2050
|
+
if (moduleName.indexOf("Phoenix.PubSub") !== -1 || moduleName.indexOf("Phoenix.Channel") !== -1) ctx.frameworks["Phoenix Channels"] = true;
|
|
2051
|
+
if (fwRoot === "Broadway") ctx.frameworks["Broadway"] = true;
|
|
2052
|
+
if (moduleName.split(".")[0] === "GenServer") {
|
|
2053
|
+
ctx.patterns["Actor Model (OTP)"] = true;
|
|
2054
|
+
const sym = currentScope ? ctx.symbolByName.get(currentScope.name) : void 0;
|
|
2055
|
+
if (sym && sym.metadata) {
|
|
2056
|
+
sym.metadata.isGenServer = true;
|
|
2057
|
+
sym.metadata.otpBehavior = "GenServer";
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
if (moduleName.split(".")[0] === "Supervisor") {
|
|
2061
|
+
ctx.patterns["Supervision Trees"] = true;
|
|
2062
|
+
ctx.patterns["Actor Model (OTP)"] = true;
|
|
2063
|
+
const sym = currentScope ? ctx.symbolByName.get(currentScope.name) : void 0;
|
|
2064
|
+
if (sym && sym.metadata) {
|
|
2065
|
+
sym.metadata.isSupervisor = true;
|
|
2066
|
+
sym.metadata.otpBehavior = "Supervisor";
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
} else if (target === "import") {
|
|
2070
|
+
const fwRoot = moduleName.split(".")[0];
|
|
2071
|
+
if (fwRoot === "Plug") ctx.frameworks["Plug"] = true;
|
|
2072
|
+
if (fwRoot === "Phoenix") ctx.frameworks["Phoenix"] = true;
|
|
2073
|
+
if (fwRoot === "Ecto") ctx.frameworks["Ecto"] = true;
|
|
2074
|
+
if (moduleName) {
|
|
2075
|
+
let only;
|
|
2076
|
+
const kwNode = this.findChild(argsNode, (c) => c?.type === "keywords");
|
|
2077
|
+
if (kwNode) {
|
|
2078
|
+
for (let i = 0; i < kwNode.namedChildCount; i++) {
|
|
2079
|
+
const pair = kwNode.namedChild(i);
|
|
2080
|
+
if (!pair || pair.type !== "pair") continue;
|
|
2081
|
+
const kw = this.findChild(pair, (c) => c?.type === "keyword");
|
|
2082
|
+
if (!kw || this.stripAfterColon(kw.text) !== "only" || pair.namedChildCount < 2) continue;
|
|
2083
|
+
const val = pair.namedChild(1);
|
|
2084
|
+
if (val?.type !== "list") continue;
|
|
2085
|
+
const names = /* @__PURE__ */ new Set();
|
|
2086
|
+
for (const kwGroup of this.filterChildren(val, (c) => c?.type === "keywords")) {
|
|
2087
|
+
for (let j = 0; j < kwGroup.namedChildCount; j++) {
|
|
2088
|
+
const fp = kwGroup.namedChild(j);
|
|
2089
|
+
if (!fp || fp.type !== "pair") continue;
|
|
2090
|
+
const fk = this.findChild(fp, (c) => c?.type === "keyword");
|
|
2091
|
+
if (fk?.text) names.add(this.stripAfterColon(fk.text));
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (names.size > 0) only = names;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
ctx.importList.push({ module: moduleName, only, scope: currentScope?.name ?? "" });
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
if (target === "use" || target === "import" || target === "alias" || target === "require") {
|
|
2101
|
+
for (const dep of deps) {
|
|
2102
|
+
if (!dep.name) continue;
|
|
2103
|
+
ctx.imports.push({ module: dep.name, source: dep.name, imported: ["*"], isTypeOnly: false, range: this.nodeToRange(node) });
|
|
2104
|
+
if (dep.name !== currentScope?.name) this.pushRel(
|
|
2105
|
+
ctx,
|
|
2106
|
+
currentScope?.name || "Global",
|
|
2107
|
+
dep.name,
|
|
2108
|
+
ASTEngine.RelationshipType.References,
|
|
2109
|
+
0.9,
|
|
2110
|
+
{ reasonCode: `elixir_directive_${target}`, directive: target },
|
|
2111
|
+
dep.line
|
|
2112
|
+
);
|
|
2113
|
+
if (target === "use" && dep.name !== currentScope?.name) {
|
|
2114
|
+
this.pushRel(
|
|
2115
|
+
ctx,
|
|
2116
|
+
currentScope?.name || "Global",
|
|
2117
|
+
dep.name,
|
|
2118
|
+
ASTEngine.RelationshipType.MixesIn,
|
|
2119
|
+
0.9,
|
|
2120
|
+
{ elixirSpecific: true, isMixin: true, via: "use", evidenceClass: "structural", reasonCode: "elixir_use_mixin_application" },
|
|
2121
|
+
dep.line
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
if (target === "import" && dep.name !== currentScope?.name) {
|
|
2125
|
+
this.pushRel(
|
|
2126
|
+
ctx,
|
|
2127
|
+
currentScope?.name || "Global",
|
|
2128
|
+
dep.name,
|
|
2129
|
+
ASTEngine.RelationshipType.Imports,
|
|
2130
|
+
1,
|
|
2131
|
+
{ elixirSpecific: true, via: "import", reasonCode: "elixir_import_directive" },
|
|
2132
|
+
dep.line
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
// EX-EX11/EX-EX11b: the placeholder spellings an edge ENDPOINT may not take.
|
|
2139
|
+
// R7's rule is that an edge endpoint is a SYMBOL NAME; `Unknown` is this
|
|
2140
|
+
// analyzer's own "I could not read it" sentinel, so an edge that names it names
|
|
2141
|
+
// nothing. `global` is the file-level pseudo-scope and is a legal SOURCE (a
|
|
2142
|
+
// directive at the top of a script has no module), never a legal TARGET.
|
|
2143
|
+
// `unknown` (lowercase) is deliberately NOT here: that is a name the source
|
|
2144
|
+
// really wrote (EX-EX11b).
|
|
2145
|
+
isPlaceholderName(name) {
|
|
2146
|
+
return name === "Unknown" || name === "global";
|
|
2147
|
+
}
|
|
2148
|
+
// R7-NAMES: the module name of a `use`/`import`/`alias`/`require` directive,
|
|
2149
|
+
// read from its FIRST ARGUMENT NODE only. `alias :lists, as: MyList` names the
|
|
2150
|
+
// Erlang module `:lists` — the atom's own text is the name and the options are
|
|
2151
|
+
// options. A group form (`import Foo.{Bar, Baz}`) is an EXPRESSION, so the
|
|
2152
|
+
// prefix alone is not a module and the caller expands it per member; a computed
|
|
2153
|
+
// name (`alias Module.concat(A, B)`) is unreadable and returns "".
|
|
2154
|
+
extractDirectiveName(node) {
|
|
2155
|
+
const first = node.namedChildCount > 0 ? node.namedChild(0) : void 0;
|
|
2156
|
+
if (!first) return "";
|
|
2157
|
+
if (first.type === "alias" || first.type === "identifier") return first.text;
|
|
2158
|
+
if (first.type === "atom") return first.text;
|
|
2159
|
+
if (first.type === "dot") {
|
|
2160
|
+
const part = (n) => {
|
|
2161
|
+
if (!n) return "";
|
|
2162
|
+
if (n.type === "dot") return part(n.childForFieldName("left")) + "." + part(n.childForFieldName("right"));
|
|
2163
|
+
return n.text;
|
|
2164
|
+
};
|
|
2165
|
+
const left = first.childForFieldName("left");
|
|
2166
|
+
const right = first.childForFieldName("right");
|
|
2167
|
+
if (left && right) return part(left) + "." + part(right);
|
|
2168
|
+
return "";
|
|
2169
|
+
}
|
|
2170
|
+
return "";
|
|
2171
|
+
}
|
|
2172
|
+
nodeToRange(node) {
|
|
2173
|
+
return { startLine: node.startPosition.row + 1, startColumn: node.startPosition.column + 1, endLine: node.endPosition.row + 1, endColumn: node.endPosition.column + 1, startIndex: node.startIndex, endIndex: node.endIndex };
|
|
2174
|
+
}
|
|
2175
|
+
// EX-A8: grow a symbol's range to include a later declaration of the SAME
|
|
2176
|
+
// definition (a merged multi-clause head). The start never moves — the
|
|
2177
|
+
// declaration site is what every id and name-line consumer is built on — and
|
|
2178
|
+
// endColumn is only meaningful together with its endLine, so a clause ending
|
|
2179
|
+
// on a later line replaces the column and a clause ending on the same line
|
|
2180
|
+
// takes the larger one. endIndex follows the same max rule.
|
|
2181
|
+
extendRange(symbol, r) {
|
|
2182
|
+
const cur = symbol.range;
|
|
2183
|
+
if (r.endLine > cur.endLine) {
|
|
2184
|
+
cur.endLine = r.endLine;
|
|
2185
|
+
cur.endColumn = r.endColumn;
|
|
2186
|
+
} else if (r.endLine === cur.endLine && r.endColumn > cur.endColumn) cur.endColumn = r.endColumn;
|
|
2187
|
+
if (typeof r.endIndex === "number" && typeof cur.endIndex === "number" && r.endIndex > cur.endIndex) cur.endIndex = r.endIndex;
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
export {
|
|
2191
|
+
RealElixirAnalyzer
|
|
2192
|
+
};
|