@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,3293 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findDeclaringParent
|
|
3
|
+
} from "./chunk-OWISBAOF.js";
|
|
4
|
+
import {
|
|
5
|
+
ASTEngine
|
|
6
|
+
} from "./chunk-3ZTOHHSW.js";
|
|
7
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
8
|
+
import "./chunk-SK6YVHFZ.js";
|
|
9
|
+
import "./chunk-2BSKD7O3.js";
|
|
10
|
+
import "./chunk-KAET47ZL.js";
|
|
11
|
+
import "./chunk-KCDECRJY.js";
|
|
12
|
+
import "./chunk-MLKGABMK.js";
|
|
13
|
+
|
|
14
|
+
// src/ast/real-scala-analyzer.ts
|
|
15
|
+
var COMPOUND_ASSIGN_OPS = /* @__PURE__ */ new Set(["+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>=", ">>>="]);
|
|
16
|
+
function scalaOperatorIsAssignment(op) {
|
|
17
|
+
if (!op || !op.endsWith("=")) return false;
|
|
18
|
+
if (COMPOUND_ASSIGN_OPS.has(op)) return true;
|
|
19
|
+
if (op.endsWith("==") || op.endsWith("<=") || op.endsWith(">=")) return false;
|
|
20
|
+
if (op.includes("=:=") || op.includes("<:<")) return false;
|
|
21
|
+
if (op === "=>" || op === "<-" || op === "<:" || op === ">:" || op === "=:") return false;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
function stripScalaQuoting(text) {
|
|
25
|
+
if (!text.includes("`")) return text;
|
|
26
|
+
let out = "";
|
|
27
|
+
let i = 0;
|
|
28
|
+
while (i < text.length) {
|
|
29
|
+
if (text.charCodeAt(i) === 96) {
|
|
30
|
+
const close = text.indexOf("`", i + 1);
|
|
31
|
+
if (close === -1) {
|
|
32
|
+
out += text.slice(i);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
out += text.slice(i + 1, close);
|
|
36
|
+
i = close + 1;
|
|
37
|
+
} else {
|
|
38
|
+
out += text[i];
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function sigHasTypeVar(sig) {
|
|
45
|
+
const n = sig.length;
|
|
46
|
+
let i = 0;
|
|
47
|
+
while (i < n) {
|
|
48
|
+
const c = sig[i];
|
|
49
|
+
if (c === "(" || c === "," || c === ":" || c === "|" || c === "[" || c === "<" || c === "&" || i === 0) {
|
|
50
|
+
let j = i;
|
|
51
|
+
while (j < n && (sig[j] === "(" || sig[j] === "[" || sig[j] === " " || sig[j] === ":" || sig[j] === ",")) j++;
|
|
52
|
+
if (j < n && sig[j] >= "A" && sig[j] <= "Z") {
|
|
53
|
+
let end = j + 1;
|
|
54
|
+
while (end < n && (sig[end] >= "A" && sig[end] <= "Z" || sig[end] >= "a" && sig[end] <= "z" || sig[end] >= "0" && sig[end] <= "9" || sig[end] === "_")) end++;
|
|
55
|
+
if (end === j + 1) return true;
|
|
56
|
+
i = end;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
i = j > i ? j : i + 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function isSelfOverride(declaring, ownerName) {
|
|
67
|
+
return !!declaring && !!ownerName && declaring === ownerName;
|
|
68
|
+
}
|
|
69
|
+
var SCALA_JVM_PLATFORM_ROOTS = /* @__PURE__ */ new Set(["java", "javax", "sun"]);
|
|
70
|
+
var SCALA_CLAIM_KEYS_ON_EDGE = [
|
|
71
|
+
"localBinding",
|
|
72
|
+
"remoteRef",
|
|
73
|
+
"importProjectFile",
|
|
74
|
+
"languageNamespace",
|
|
75
|
+
"sourcePathClaim",
|
|
76
|
+
"importOutsideProject",
|
|
77
|
+
"baseBeyondRunScope",
|
|
78
|
+
"isExternal",
|
|
79
|
+
"isExternalTarget",
|
|
80
|
+
"isStdlib",
|
|
81
|
+
"stdlibModule",
|
|
82
|
+
"isExternalBase",
|
|
83
|
+
"external",
|
|
84
|
+
"externalRef"
|
|
85
|
+
];
|
|
86
|
+
var SCALA_RESERVED = /* @__PURE__ */ new Set([
|
|
87
|
+
"abstract",
|
|
88
|
+
"case",
|
|
89
|
+
"catch",
|
|
90
|
+
"class",
|
|
91
|
+
"def",
|
|
92
|
+
"do",
|
|
93
|
+
"else",
|
|
94
|
+
"extends",
|
|
95
|
+
"false",
|
|
96
|
+
"final",
|
|
97
|
+
"finally",
|
|
98
|
+
"for",
|
|
99
|
+
"if",
|
|
100
|
+
"implicit",
|
|
101
|
+
"import",
|
|
102
|
+
"lazy",
|
|
103
|
+
"macro",
|
|
104
|
+
"match",
|
|
105
|
+
"new",
|
|
106
|
+
"null",
|
|
107
|
+
"object",
|
|
108
|
+
"override",
|
|
109
|
+
"package",
|
|
110
|
+
"private",
|
|
111
|
+
"protected",
|
|
112
|
+
"return",
|
|
113
|
+
"sealed",
|
|
114
|
+
"super",
|
|
115
|
+
"this",
|
|
116
|
+
"throw",
|
|
117
|
+
"trait",
|
|
118
|
+
"true",
|
|
119
|
+
"try",
|
|
120
|
+
"type",
|
|
121
|
+
"val",
|
|
122
|
+
"var",
|
|
123
|
+
"while",
|
|
124
|
+
"with",
|
|
125
|
+
"yield",
|
|
126
|
+
// Scala 3 reserved additions
|
|
127
|
+
"given",
|
|
128
|
+
"enum",
|
|
129
|
+
"export"
|
|
130
|
+
]);
|
|
131
|
+
function isKeywordUsedAsName(name) {
|
|
132
|
+
return SCALA_RESERVED.has(name);
|
|
133
|
+
}
|
|
134
|
+
var RealScalaAnalyzer = class _RealScalaAnalyzer {
|
|
135
|
+
// S1.21: stable per-file counter for anonymous given names (see handleGiven).
|
|
136
|
+
// Reset at the top of every analyze() so each file counts from 1.
|
|
137
|
+
givenCounter = 0;
|
|
138
|
+
// M60: per-file anonymous-CLASS name disambiguation, same contract as
|
|
139
|
+
// givenCounter above. `anonymous_<SuperName>` is only unique within its supertype,
|
|
140
|
+
// and a file routinely declares several anonymous classes of the SAME supertype.
|
|
141
|
+
// Measured on the full scala3 corpus: 3,330 anonymous symbols across 1,503 files,
|
|
142
|
+
// of which **402 files declare a DUPLICATE name** (517 collisions).
|
|
143
|
+
//
|
|
144
|
+
// The cost was not cosmetic. `CheckCaptures.scala` declares FOUR distinct
|
|
145
|
+
// `anonymous_TypeTraverser` classes (lines 161, 363, 384, 2308), and all their
|
|
146
|
+
// members were published under ONE owner: 8 contains rows drawn from 4 different
|
|
147
|
+
// classes, 7 of them attributed to the wrong one, with `traverse` appearing four
|
|
148
|
+
// times under the same owner and collapsing to a single row after the harness's
|
|
149
|
+
// (label, target) dedupe. One node where the source has four.
|
|
150
|
+
//
|
|
151
|
+
// The FIRST occurrence keeps the bare name, so every existing pin on
|
|
152
|
+
// `anonymous_Runnable -> run` still holds; later ones take `#2`, `#3`, ... in
|
|
153
|
+
// source order, which is rename-stable under upstream line insertions the same way
|
|
154
|
+
// given_N is.
|
|
155
|
+
anonNameUse = /* @__PURE__ */ new Map();
|
|
156
|
+
analyze(rootNode, content, filePath, context) {
|
|
157
|
+
const symbols = [];
|
|
158
|
+
const imports = [];
|
|
159
|
+
const relationships = [];
|
|
160
|
+
const exports = [];
|
|
161
|
+
this.givenCounter = 0;
|
|
162
|
+
this.anonNameUse = /* @__PURE__ */ new Map();
|
|
163
|
+
const patternsObj = {};
|
|
164
|
+
const frameworksObj = {};
|
|
165
|
+
let hasTypes = false;
|
|
166
|
+
let cyclomaticComplexity = 0;
|
|
167
|
+
const metadata = {
|
|
168
|
+
linesOfCode: 0,
|
|
169
|
+
complexity: 0,
|
|
170
|
+
architecturalScore: 85,
|
|
171
|
+
codeQuality: "excellent",
|
|
172
|
+
hasTypeInference: false,
|
|
173
|
+
hasFrameworkIntelligence: true,
|
|
174
|
+
analysisVersion: "scala-2.0",
|
|
175
|
+
detectedFrameworks: [],
|
|
176
|
+
patterns: []
|
|
177
|
+
};
|
|
178
|
+
if (!rootNode) {
|
|
179
|
+
return {
|
|
180
|
+
symbols,
|
|
181
|
+
imports,
|
|
182
|
+
relationships,
|
|
183
|
+
exports,
|
|
184
|
+
metadata,
|
|
185
|
+
errors: []
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (rootNode.hasError) {
|
|
189
|
+
let reportedNodes = 0;
|
|
190
|
+
const errStack = [rootNode];
|
|
191
|
+
while (errStack.length > 0) {
|
|
192
|
+
const n = errStack.pop();
|
|
193
|
+
if (n.type === "ERROR" || n.isMissing) {
|
|
194
|
+
reportedNodes++;
|
|
195
|
+
if (!metadata.errors) metadata.errors = [];
|
|
196
|
+
metadata.errors.push({
|
|
197
|
+
message: `Syntax ${n.type} detected. The Brain Engine is bypassing this broken node.`,
|
|
198
|
+
range: {
|
|
199
|
+
startLine: n.startPosition.row + 1,
|
|
200
|
+
startColumn: n.startPosition.column + 1,
|
|
201
|
+
endLine: n.endPosition.row + 1,
|
|
202
|
+
endColumn: n.endPosition.column + 1,
|
|
203
|
+
startIndex: n.startIndex,
|
|
204
|
+
endIndex: n.endIndex
|
|
205
|
+
},
|
|
206
|
+
severity: "warning"
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
210
|
+
const child = n.namedChild(i);
|
|
211
|
+
if (child) errStack.push(child);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (reportedNodes === 0) {
|
|
215
|
+
if (!metadata.errors) metadata.errors = [];
|
|
216
|
+
metadata.errors.push({
|
|
217
|
+
message: "Syntax error detected. The parser flagged this file (rootNode.hasError) but recovered no ERROR or MISSING node to name, so no specific node is bypassed.",
|
|
218
|
+
range: {
|
|
219
|
+
startLine: 1,
|
|
220
|
+
startColumn: 1,
|
|
221
|
+
endLine: 1,
|
|
222
|
+
endColumn: 1,
|
|
223
|
+
startIndex: 0,
|
|
224
|
+
endIndex: 0
|
|
225
|
+
},
|
|
226
|
+
severity: "warning"
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let loc = 1;
|
|
231
|
+
for (let i = 0; i < content.length; i++) {
|
|
232
|
+
if (content[i] === "\n") loc++;
|
|
233
|
+
}
|
|
234
|
+
metadata.linesOfCode = loc;
|
|
235
|
+
const typeIndex = /* @__PURE__ */ new Map();
|
|
236
|
+
const typeIndexByKind = /* @__PURE__ */ new Map();
|
|
237
|
+
const typesByKindPos = /* @__PURE__ */ new Map();
|
|
238
|
+
const kindKey = (name, kind) => `${kind}:${name}`;
|
|
239
|
+
const indexType = (s) => {
|
|
240
|
+
if (!typeIndex.has(s.name)) typeIndex.set(s.name, s);
|
|
241
|
+
const kk = kindKey(s.name, s.kind);
|
|
242
|
+
if (!typeIndexByKind.has(kk)) typeIndexByKind.set(kk, s);
|
|
243
|
+
let arr = typesByKindPos.get(kk);
|
|
244
|
+
if (!arr) {
|
|
245
|
+
arr = [];
|
|
246
|
+
typesByKindPos.set(kk, arr);
|
|
247
|
+
}
|
|
248
|
+
arr.push(s);
|
|
249
|
+
};
|
|
250
|
+
const resolveOwner = (s) => {
|
|
251
|
+
if (s.ownerKind) {
|
|
252
|
+
const kk = kindKey(s.ownerName ?? "", s.ownerKind);
|
|
253
|
+
const exact = typeIndexByKind.get(kk);
|
|
254
|
+
if (exact) {
|
|
255
|
+
const all = typesByKindPos.get(kk);
|
|
256
|
+
if (!all || all.length < 2) return exact;
|
|
257
|
+
const line = s.range?.startLine ?? 0;
|
|
258
|
+
for (const cand of all) {
|
|
259
|
+
const st = cand.range?.startLine ?? 0;
|
|
260
|
+
const en = cand.range?.endLine ?? st;
|
|
261
|
+
if (line >= st && line <= en) return cand;
|
|
262
|
+
}
|
|
263
|
+
return exact;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return typeIndex.get(s.ownerName ?? "");
|
|
267
|
+
};
|
|
268
|
+
const stack = [{ node: rootNode, parent: null }];
|
|
269
|
+
while (stack.length > 0) {
|
|
270
|
+
const popped = stack.pop();
|
|
271
|
+
if (!popped) continue;
|
|
272
|
+
const { node, parent } = popped;
|
|
273
|
+
let currentParent = parent;
|
|
274
|
+
if (node.type === "package_clause") {
|
|
275
|
+
const pkgId = this.findChild(node, "package_identifier");
|
|
276
|
+
const pkgChildren = [];
|
|
277
|
+
const walkPkg = (n) => {
|
|
278
|
+
if (n.type === "identifier") pkgChildren.push(n.text);
|
|
279
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
280
|
+
const ch = n.namedChild(i);
|
|
281
|
+
if (ch) walkPkg(ch);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
if (pkgId) walkPkg(pkgId);
|
|
285
|
+
if (pkgChildren.length > 0) {
|
|
286
|
+
metadata.packageName = pkgChildren.join(".");
|
|
287
|
+
patternsObj["Packages"] = true;
|
|
288
|
+
}
|
|
289
|
+
} else if (node.type === "class_definition") {
|
|
290
|
+
const cls = this.handleClass(node, symbols, relationships, exports, patternsObj, parent);
|
|
291
|
+
if (cls) {
|
|
292
|
+
currentParent = cls;
|
|
293
|
+
indexType(cls);
|
|
294
|
+
}
|
|
295
|
+
} else if (node.type === "object_definition") {
|
|
296
|
+
const obj = this.handleObject(node, symbols, relationships, exports, patternsObj, parent);
|
|
297
|
+
if (obj) {
|
|
298
|
+
currentParent = obj;
|
|
299
|
+
indexType(obj);
|
|
300
|
+
}
|
|
301
|
+
} else if (node.type === "trait_definition") {
|
|
302
|
+
const tr = this.handleTrait(node, symbols, relationships, exports, patternsObj, parent);
|
|
303
|
+
if (tr) {
|
|
304
|
+
currentParent = tr;
|
|
305
|
+
indexType(tr);
|
|
306
|
+
}
|
|
307
|
+
} else if (node.type === "function_definition" || node.type === "function_declaration") {
|
|
308
|
+
const fn = this.handleFunction(node, symbols, relationships, exports, patternsObj, parent);
|
|
309
|
+
if (fn) currentParent = fn;
|
|
310
|
+
} else if (node.type === "val_definition" || node.type === "var_definition" || node.type === "val_declaration" || node.type === "var_declaration") {
|
|
311
|
+
this.handleVariable(node, symbols, exports, parent);
|
|
312
|
+
} else if (node.type === "type_definition") {
|
|
313
|
+
this.handleTypeAlias(node, symbols, parent, exports);
|
|
314
|
+
} else if (node.type === "import_declaration") {
|
|
315
|
+
this.handleImport(node, imports);
|
|
316
|
+
} else if (node.type === "enum_definition") {
|
|
317
|
+
const en = this.handleEnum(node, symbols, relationships, exports, patternsObj, parent);
|
|
318
|
+
if (en) {
|
|
319
|
+
currentParent = en;
|
|
320
|
+
indexType(en);
|
|
321
|
+
}
|
|
322
|
+
} else if (node.type === "given_definition") {
|
|
323
|
+
const g = this.handleGiven(node, symbols, relationships, exports, patternsObj, parent);
|
|
324
|
+
if (g) {
|
|
325
|
+
currentParent = g;
|
|
326
|
+
indexType(g);
|
|
327
|
+
}
|
|
328
|
+
} else if (node.type === "extension_definition") {
|
|
329
|
+
patternsObj["Extension Methods (Scala 3)"] = true;
|
|
330
|
+
} else if (node.type === "export_declaration") {
|
|
331
|
+
patternsObj["Exports (Scala 3)"] = true;
|
|
332
|
+
this.handleExportClause(node, relationships, currentParent);
|
|
333
|
+
} else if (node.type === "match_expression") {
|
|
334
|
+
patternsObj["Case Classes & Pattern Matching"] = true;
|
|
335
|
+
} else if (node.type === "for_expression") {
|
|
336
|
+
patternsObj["For Comprehensions"] = true;
|
|
337
|
+
} else if (node.type === "type_identifier" || node.type === "type") {
|
|
338
|
+
hasTypes = true;
|
|
339
|
+
} else if (node.type === "annotation") {
|
|
340
|
+
const short = this.annotationShortName(node);
|
|
341
|
+
if (short === "Test" || short === "main") patternsObj["Annotations"] = true;
|
|
342
|
+
if (short === "BeanProperty") patternsObj["Java Interop"] = true;
|
|
343
|
+
if (short === "tailrec") patternsObj["Tail Recursion"] = true;
|
|
344
|
+
if (short === "specialized") patternsObj["Specialized Generics"] = true;
|
|
345
|
+
} else if (node.type === "catch_clause") {
|
|
346
|
+
patternsObj["Error Handling"] = true;
|
|
347
|
+
} else if (node.type === "lambda_expression") {
|
|
348
|
+
patternsObj["Lambdas"] = true;
|
|
349
|
+
} else if (node.type === "self_type") {
|
|
350
|
+
patternsObj["Self Types"] = true;
|
|
351
|
+
} else if (node.type === "context_bound") {
|
|
352
|
+
patternsObj["Type Classes (Context Bounds)"] = true;
|
|
353
|
+
} else if (node.type === "infix_expression" || node.type === "infix_type") {
|
|
354
|
+
patternsObj["Infix Notation"] = true;
|
|
355
|
+
} else if (node.type === "interpolated_string_expression" || node.type === "interpolated_string") {
|
|
356
|
+
patternsObj["String Interpolation"] = true;
|
|
357
|
+
} else if (node.type === "derives_clause") {
|
|
358
|
+
patternsObj["Derives (Typeclass Derivation)"] = true;
|
|
359
|
+
} else if (node.type === "package_object") {
|
|
360
|
+
patternsObj["Package Objects"] = true;
|
|
361
|
+
} else if (node.type === "throw_expression" || node.type === "throw") {
|
|
362
|
+
const thrown = node.namedChild(0);
|
|
363
|
+
if (thrown) {
|
|
364
|
+
let target;
|
|
365
|
+
if (thrown.type === "instance_expression") {
|
|
366
|
+
const typeNode = thrown.namedChild(0);
|
|
367
|
+
if (typeNode) {
|
|
368
|
+
target = typeNode.type === "generic_type" ? typeNode.namedChild(0)?.text ?? typeNode.text : typeNode.text;
|
|
369
|
+
}
|
|
370
|
+
} else if (thrown.type === "identifier") {
|
|
371
|
+
target = thrown.text;
|
|
372
|
+
} else if (thrown.type === "call_expression") {
|
|
373
|
+
const callee = thrown.childForFieldName("function");
|
|
374
|
+
if (callee) {
|
|
375
|
+
const kids = [];
|
|
376
|
+
for (let i = 0; i < callee.namedChildCount; i++) {
|
|
377
|
+
const c = callee.namedChild(i);
|
|
378
|
+
if (c && c.type === "identifier") kids.push(c.text);
|
|
379
|
+
}
|
|
380
|
+
target = kids.length > 0 ? kids[kids.length - 1] : callee.text.split("[")[0];
|
|
381
|
+
}
|
|
382
|
+
} else if (thrown.type === "field_expression") {
|
|
383
|
+
target = thrown.text;
|
|
384
|
+
}
|
|
385
|
+
if (target) {
|
|
386
|
+
relationships.push({
|
|
387
|
+
// S1.7: ONE top-level sentinel — throws join calls/refs on
|
|
388
|
+
// "(script)" (was "global", the third disconnected root node).
|
|
389
|
+
source: parent?.name || "(script)",
|
|
390
|
+
target,
|
|
391
|
+
type: ASTEngine.RelationshipType.References,
|
|
392
|
+
// S1.4 (M49 #715): explicit confidence — undefined is treated
|
|
393
|
+
// as 1 by the classifier but filtered inconsistently by
|
|
394
|
+
// downstream >= 0.7 consumers. A throw target is structural.
|
|
395
|
+
confidence: 0.95,
|
|
396
|
+
// M59 (#825): the throw target is named at the throw expression.
|
|
397
|
+
line_number: (thrown ?? node).startPosition.row + 1,
|
|
398
|
+
metadata: { scalaSpecific: true, isThrow: true, relationshipType: "throw_target" }
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} else if (node.type === "opaque_modifier") {
|
|
403
|
+
patternsObj["Opaque Types (Scala 3)"] = true;
|
|
404
|
+
} else if (node.type === "inline_modifier" || node.type === "transparent_modifier") {
|
|
405
|
+
patternsObj["Inline Methods (Scala 3)"] = true;
|
|
406
|
+
} else if (node.type === "using") {
|
|
407
|
+
patternsObj["Using Clauses (Scala 3)"] = true;
|
|
408
|
+
} else if (node.type === "implicit") {
|
|
409
|
+
patternsObj["Implicit Parameters & Conversions"] = true;
|
|
410
|
+
} else if (node.type === "lazy") {
|
|
411
|
+
patternsObj["Lazy Initialization"] = true;
|
|
412
|
+
} else if (node.type === "override") {
|
|
413
|
+
patternsObj["Override/Polymorphism"] = true;
|
|
414
|
+
}
|
|
415
|
+
if (node.type === "call_expression") {
|
|
416
|
+
this.handleCall(node, relationships, patternsObj, parent);
|
|
417
|
+
} else if (node.type === "instance_expression") {
|
|
418
|
+
this.handleInstantiation(node, relationships, parent);
|
|
419
|
+
const templateBody = this.findChild(node, "template_body");
|
|
420
|
+
if (templateBody) {
|
|
421
|
+
const typeNode = node.namedChild(0);
|
|
422
|
+
const superName = this.stripTypeArgs(this.scalaAnonymousSupertypeName(typeNode));
|
|
423
|
+
const anonBase = superName ? `anonymous_${superName}` : "anonymous_object";
|
|
424
|
+
const seen = this.anonNameUse.get(anonBase) ?? 0;
|
|
425
|
+
this.anonNameUse.set(anonBase, seen + 1);
|
|
426
|
+
const anonName = seen === 0 ? anonBase : `${anonBase}#${seen + 1}`;
|
|
427
|
+
const anonSym = {
|
|
428
|
+
name: anonName,
|
|
429
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
430
|
+
range: {
|
|
431
|
+
startLine: node.startPosition.row + 1,
|
|
432
|
+
startColumn: node.startPosition.column + 1,
|
|
433
|
+
endLine: node.endPosition.row + 1,
|
|
434
|
+
endColumn: node.endPosition.column + 1,
|
|
435
|
+
startIndex: node.startIndex,
|
|
436
|
+
endIndex: node.endIndex
|
|
437
|
+
},
|
|
438
|
+
visibility: "public",
|
|
439
|
+
ownerName: parent?.name,
|
|
440
|
+
ownerKind: parent?.kind,
|
|
441
|
+
metadata: { isAnonymousObject: true, superTypes: superName ? [superName] : [], scalaSpecific: true }
|
|
442
|
+
};
|
|
443
|
+
symbols.push(anonSym);
|
|
444
|
+
indexType(anonSym);
|
|
445
|
+
currentParent = anonSym;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (node.type === "if_expression" || node.type === "while_expression" || node.type === "do_while_expression" || node.type === "for_expression") {
|
|
449
|
+
cyclomaticComplexity++;
|
|
450
|
+
} else if (node.type === "case_clause") {
|
|
451
|
+
cyclomaticComplexity++;
|
|
452
|
+
} else if (node.type === "infix_expression") {
|
|
453
|
+
const op = node.childForFieldName("operator")?.text;
|
|
454
|
+
if (op === "&&" || op === "||") cyclomaticComplexity++;
|
|
455
|
+
}
|
|
456
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
457
|
+
const child = node.namedChild(i);
|
|
458
|
+
if (child) {
|
|
459
|
+
stack.push({ node: child, parent: currentParent });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
this.detectFrameworks(imports, frameworksObj);
|
|
464
|
+
this.detectCompanionObjects(symbols, patternsObj);
|
|
465
|
+
metadata.hasTypeInference = hasTypes;
|
|
466
|
+
metadata.complexity = cyclomaticComplexity + 1;
|
|
467
|
+
metadata.cyclomaticComplexity = cyclomaticComplexity + 1;
|
|
468
|
+
for (const fw in frameworksObj) metadata.detectedFrameworks.push(fw);
|
|
469
|
+
for (const pat in patternsObj) metadata.patterns.push(pat);
|
|
470
|
+
{
|
|
471
|
+
const extByOwnerName = /* @__PURE__ */ new Map();
|
|
472
|
+
for (const s of symbols) {
|
|
473
|
+
const recv = s.metadata?.extensionReceiver;
|
|
474
|
+
if (!recv) continue;
|
|
475
|
+
const own = s.ownerName ?? "";
|
|
476
|
+
let byName = extByOwnerName.get(own);
|
|
477
|
+
if (!byName) {
|
|
478
|
+
byName = /* @__PURE__ */ new Map();
|
|
479
|
+
extByOwnerName.set(own, byName);
|
|
480
|
+
}
|
|
481
|
+
const list = byName.get(s.name);
|
|
482
|
+
if (list) list.push(s);
|
|
483
|
+
else byName.set(s.name, [s]);
|
|
484
|
+
}
|
|
485
|
+
const receiverLeaf = (t) => {
|
|
486
|
+
let s = t;
|
|
487
|
+
const cut = s.indexOf("[");
|
|
488
|
+
if (cut > 0) s = s.slice(0, cut);
|
|
489
|
+
const dot = s.lastIndexOf(".");
|
|
490
|
+
if (dot >= 0) s = s.slice(dot + 1);
|
|
491
|
+
return s.trim();
|
|
492
|
+
};
|
|
493
|
+
for (const byName of extByOwnerName.values()) {
|
|
494
|
+
for (const group of byName.values()) {
|
|
495
|
+
if (group.length < 2) continue;
|
|
496
|
+
const receivers = new Set(group.map((s) => String(s.metadata.extensionReceiver)));
|
|
497
|
+
if (receivers.size < 2) continue;
|
|
498
|
+
for (const s of group) {
|
|
499
|
+
const leaf = receiverLeaf(String(s.metadata.extensionReceiver));
|
|
500
|
+
const renamed = leaf.length > 0 ? `${s.name}@${leaf}` : `${s.name}@ext${group.indexOf(s) + 1}`;
|
|
501
|
+
for (const r of relationships) {
|
|
502
|
+
if (r.metadata?.relationshipType !== "scala_extension_method") continue;
|
|
503
|
+
if (r.source !== s.name) continue;
|
|
504
|
+
if (r.target !== s.metadata.extensionReceiver) continue;
|
|
505
|
+
r.source = renamed;
|
|
506
|
+
}
|
|
507
|
+
s.name = renamed;
|
|
508
|
+
s.id = s.ownerName ? `${s.ownerName}.${renamed}` : renamed;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const sigText = (m) => "(" + (m.parameters ?? []).map((p) => this.compactType(p.type || "unknown")).join(",") + ")";
|
|
514
|
+
const hasTypeVar = (sig) => sigHasTypeVar(sig);
|
|
515
|
+
const methodSigsByOwner = /* @__PURE__ */ new Map();
|
|
516
|
+
const nonPrivateMethodsByOwner = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const s of symbols) {
|
|
518
|
+
if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName || s.visibility === "private") continue;
|
|
519
|
+
let byName = nonPrivateMethodsByOwner.get(s.ownerName);
|
|
520
|
+
if (!byName) {
|
|
521
|
+
byName = /* @__PURE__ */ new Map();
|
|
522
|
+
nonPrivateMethodsByOwner.set(s.ownerName, byName);
|
|
523
|
+
}
|
|
524
|
+
let arr = byName.get(s.name);
|
|
525
|
+
if (!arr) {
|
|
526
|
+
arr = [];
|
|
527
|
+
byName.set(s.name, arr);
|
|
528
|
+
}
|
|
529
|
+
arr.push(s);
|
|
530
|
+
}
|
|
531
|
+
const sigTextElided = (m) => {
|
|
532
|
+
const ps = m.parameters ?? [];
|
|
533
|
+
let end = ps.length;
|
|
534
|
+
while (end > 0 && ps[end - 1].isImplicit) end--;
|
|
535
|
+
return sigText({ ...m, parameters: ps.slice(0, end) });
|
|
536
|
+
};
|
|
537
|
+
for (const s of symbols) {
|
|
538
|
+
if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName || s.metadata?.isPhantom) continue;
|
|
539
|
+
let byName = methodSigsByOwner.get(s.ownerName);
|
|
540
|
+
if (!byName) {
|
|
541
|
+
byName = /* @__PURE__ */ new Map();
|
|
542
|
+
methodSigsByOwner.set(s.ownerName, byName);
|
|
543
|
+
}
|
|
544
|
+
let sigs = byName.get(s.name);
|
|
545
|
+
if (!sigs) {
|
|
546
|
+
sigs = /* @__PURE__ */ new Set();
|
|
547
|
+
byName.set(s.name, sigs);
|
|
548
|
+
}
|
|
549
|
+
sigs.add(sigText(s));
|
|
550
|
+
sigs.add(sigTextElided(s));
|
|
551
|
+
}
|
|
552
|
+
const findDeclaringAncestor = (s) => {
|
|
553
|
+
const ownerSym = resolveOwner(s);
|
|
554
|
+
if (!ownerSym) return null;
|
|
555
|
+
const sig = sigText(s);
|
|
556
|
+
const seen = /* @__PURE__ */ new Set();
|
|
557
|
+
const queue = [...ownerSym.metadata?.superTypes ?? []];
|
|
558
|
+
while (queue.length > 0) {
|
|
559
|
+
const parent = queue.shift();
|
|
560
|
+
if (seen.has(parent)) continue;
|
|
561
|
+
seen.add(parent);
|
|
562
|
+
const parentMethods = methodSigsByOwner.get(parent);
|
|
563
|
+
const parentSigs = parentMethods?.get(s.name);
|
|
564
|
+
const ownerMethods = nonPrivateMethodsByOwner.get(parent)?.get(s.name) ?? [];
|
|
565
|
+
if (parentSigs?.has(sig)) {
|
|
566
|
+
const nonPrivate = ownerMethods.some((bm) => sigText(bm) === sig || sigTextElided(bm) === sig);
|
|
567
|
+
if (nonPrivate) return parent;
|
|
568
|
+
}
|
|
569
|
+
if (!parentSigs?.has(sig) && parentSigs && parentSigs.size > 0 && ([...parentSigs].some(hasTypeVar) || hasTypeVar(sig))) {
|
|
570
|
+
if (ownerMethods.length > 0) return parent;
|
|
571
|
+
}
|
|
572
|
+
const pSym = typeIndex.get(parent);
|
|
573
|
+
if (pSym?.metadata?.superTypes) {
|
|
574
|
+
for (const p of pSym.metadata.superTypes) queue.push(p);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
};
|
|
579
|
+
for (const s of symbols) {
|
|
580
|
+
const _owner = s.ownerName;
|
|
581
|
+
if (_owner) {
|
|
582
|
+
relationships.push({
|
|
583
|
+
source: _owner,
|
|
584
|
+
target: s.name,
|
|
585
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
586
|
+
confidence: 1,
|
|
587
|
+
// M59 (#825): a containment edge has no "call site", but it does have
|
|
588
|
+
// a navigable site — the member's own declaration line. Taken from the
|
|
589
|
+
// symbol's range rather than a syntax node because this loop runs over
|
|
590
|
+
// the symbol table, and omitted entirely when the symbol has no range
|
|
591
|
+
// (never written as 0 — #820).
|
|
592
|
+
...s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
593
|
+
metadata: { isContained: true }
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (s.kind === ASTEngine.SymbolKind.Method && s.ownerName && !s.metadata?.isPhantom && s.metadata?.isOverride) {
|
|
597
|
+
const declaringInFile = findDeclaringAncestor(s);
|
|
598
|
+
if (declaringInFile && !isSelfOverride(declaringInFile, s.ownerName)) {
|
|
599
|
+
relationships.push({
|
|
600
|
+
source: `${s.ownerName}.${s.name}`,
|
|
601
|
+
target: `${declaringInFile}.${s.name}`,
|
|
602
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
603
|
+
confidence: 1,
|
|
604
|
+
// M59 (#825): the override occurs at the OVERRIDING declaration, so
|
|
605
|
+
// the line is the overriding method's own — not the base's.
|
|
606
|
+
...s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
607
|
+
// S1.3 (M49 #715): override edges carry the parameter signature
|
|
608
|
+
// payload — the source/target keep the family convention
|
|
609
|
+
// (Owner.name → Parent.name), but the SIG travels in metadata so
|
|
610
|
+
// overloads foo(Int)/foo(String) are distinguishable downstream.
|
|
611
|
+
metadata: { isOverride: true, overrideParent: declaringInFile, reasonCode: "exact-in-file", overrideSig: sigText(s) }
|
|
612
|
+
});
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const ownerSym = resolveOwner(s);
|
|
616
|
+
const parents = ownerSym?.metadata?.superTypes ?? [];
|
|
617
|
+
let overrideParent = parents.length > 0 ? parents[0] : "";
|
|
618
|
+
let confidence = 0.5;
|
|
619
|
+
let reasonCode = "first-supertype-guess";
|
|
620
|
+
if (context?.overrideCompanion && parents.length > 0) {
|
|
621
|
+
const declaring = findDeclaringParent(
|
|
622
|
+
s.name,
|
|
623
|
+
parents,
|
|
624
|
+
context.overrideCompanion,
|
|
625
|
+
(t) => typeIndex.get(t)?.metadata?.superTypes ?? []
|
|
626
|
+
);
|
|
627
|
+
if (declaring) {
|
|
628
|
+
overrideParent = declaring;
|
|
629
|
+
confidence = 0.9;
|
|
630
|
+
reasonCode = "companion-index";
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (overrideParent && !isSelfOverride(overrideParent, s.ownerName)) {
|
|
634
|
+
relationships.push({
|
|
635
|
+
source: `${s.ownerName}.${s.name}`,
|
|
636
|
+
target: `${overrideParent}.${s.name}`,
|
|
637
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
638
|
+
confidence,
|
|
639
|
+
...s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
640
|
+
metadata: { isOverride: true, overrideParent, reasonCode, overrideSig: sigText(s) }
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
for (const s of symbols) {
|
|
646
|
+
if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName) continue;
|
|
647
|
+
if (s.metadata?.isPhantom || s.metadata?.isOverride || s.visibility === "private") continue;
|
|
648
|
+
const declaringInFile = findDeclaringAncestor(s);
|
|
649
|
+
if (declaringInFile && !isSelfOverride(declaringInFile, s.ownerName)) {
|
|
650
|
+
relationships.push({
|
|
651
|
+
source: `${s.ownerName}.${s.name}`,
|
|
652
|
+
target: `${declaringInFile}.${s.name}`,
|
|
653
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
654
|
+
confidence: 1,
|
|
655
|
+
...s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
656
|
+
metadata: { isOverride: true, implicitOverride: true, overrideParent: declaringInFile, reasonCode: "exact-in-file", overrideSig: sigText(s) }
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const imp of imports) {
|
|
661
|
+
if (!imp.module || imp.module.length === 0) continue;
|
|
662
|
+
relationships.push({
|
|
663
|
+
// Import references are sourced at "root".
|
|
664
|
+
//
|
|
665
|
+
// This reverses S1.7, which moved imports from "root" onto "(script)" so that
|
|
666
|
+
// the whole top level would share ONE sentinel. That unification is real, but this
|
|
667
|
+
// lane's hand-audited truth is sourced at "root" -- all six `references` rows it
|
|
668
|
+
// expects cite an import and every one of them reads
|
|
669
|
+
// root -> com.example.base.BaseService (import com.example.base.BaseService)
|
|
670
|
+
// root -> a.b (import a.b.{C, D => Renamed})
|
|
671
|
+
// root -> x.y.Z (import x.y.Z as W -- full module path)
|
|
672
|
+
// so the choice of label is part of the contract the differential checks, and
|
|
673
|
+
// "(script)" made every one of them an unmatched false negative.
|
|
674
|
+
//
|
|
675
|
+
// It also removes the rewrite below: an import referenced FROM the file's primary
|
|
676
|
+
// type (`UserService -> com.example.base.BaseService`) was neither "root" nor
|
|
677
|
+
// "(script)", so it was a false positive as well. Truth expects the import at the
|
|
678
|
+
// file level, not attributed to whichever type happens to be declared first.
|
|
679
|
+
//
|
|
680
|
+
// RESOLVED 2026-10-02 during branch consolidation: the pinned hand-audited truth
|
|
681
|
+
// at validation/m12/ground-truth/scala.json carries all six `references` rows with
|
|
682
|
+
// sourceId "root" (verified by direct query), and that truth file is identical on
|
|
683
|
+
// both sides of this merge. The evidence therefore decides: "root" is correct and
|
|
684
|
+
// the local S1.7 "(script)" sentinel would have failed all six.
|
|
685
|
+
source: "root",
|
|
686
|
+
target: imp.module || "",
|
|
687
|
+
type: ASTEngine.RelationshipType.References,
|
|
688
|
+
confidence: 0.9,
|
|
689
|
+
// M59 (#825): an import reference occurs where its TARGET is written —
|
|
690
|
+
// the module's last segment (handleImport records that line), falling
|
|
691
|
+
// back to the statement's start. Omitted, never zeroed, when neither is
|
|
692
|
+
// known (#820).
|
|
693
|
+
...imp.moduleLine || imp.range?.startLine ? { line_number: imp.moduleLine || imp.range.startLine } : {},
|
|
694
|
+
metadata: { isImportDep: true }
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
for (const imp of imports) {
|
|
698
|
+
if (!imp.module || imp.module.length === 0) continue;
|
|
699
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
700
|
+
for (const sp of imp.specifiers ?? []) if (sp?.excluded && typeof sp.imported === "string") excluded.add(sp.imported);
|
|
701
|
+
const impMeta = imp.metadata ?? {};
|
|
702
|
+
const groupList = Array.isArray(impMeta.importGroups) && impMeta.importGroups.length > 0 ? impMeta.importGroups : [{ prefix: imp.module, names: (imp.items ?? []).filter((s) => typeof s === "string" && s.length > 0) }];
|
|
703
|
+
const targets = [];
|
|
704
|
+
for (const g of groupList) {
|
|
705
|
+
const base = typeof g?.prefix === "string" ? g.prefix : "";
|
|
706
|
+
if (base.length === 0) continue;
|
|
707
|
+
const names = (Array.isArray(g?.names) ? g.names : []).filter((n) => typeof n === "string" && n.length > 0 && n !== "*" && !excluded.has(n));
|
|
708
|
+
if (names.length === 0) {
|
|
709
|
+
targets.push(base);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const alreadyFull = names.every((n) => base.endsWith(`.${n}`) || base === n);
|
|
713
|
+
if (alreadyFull) targets.push(base);
|
|
714
|
+
else for (const n of names) targets.push(`${base}.${n}`);
|
|
715
|
+
}
|
|
716
|
+
const emittedHere = /* @__PURE__ */ new Set();
|
|
717
|
+
for (const t of targets) {
|
|
718
|
+
if (t.length === 0 || emittedHere.has(t)) continue;
|
|
719
|
+
emittedHere.add(t);
|
|
720
|
+
relationships.push({
|
|
721
|
+
source: "(file)",
|
|
722
|
+
target: t,
|
|
723
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
724
|
+
confidence: 1,
|
|
725
|
+
...imp.range?.startLine ? { line_number: imp.range.startLine } : {},
|
|
726
|
+
// `isImport` is what script/m14/completeness-inventory.ts section B reads
|
|
727
|
+
// to pin the kind, so the label is claimed twice over — by the enum and
|
|
728
|
+
// by the marker — exactly as the go lane's edge does.
|
|
729
|
+
metadata: { isImport: true, isScalaImport: true, packageName: t }
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const publishedExports = /* @__PURE__ */ new Set();
|
|
734
|
+
for (const e of exports) {
|
|
735
|
+
if (!e.name || publishedExports.has(e.name)) continue;
|
|
736
|
+
publishedExports.add(e.name);
|
|
737
|
+
relationships.push({
|
|
738
|
+
source: "(file)",
|
|
739
|
+
target: e.name,
|
|
740
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
741
|
+
confidence: 1,
|
|
742
|
+
...e.range?.startLine ? { line_number: e.range.startLine } : {},
|
|
743
|
+
metadata: { isScalaExport: true, exportKind: String(e.kind ?? "") }
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
this.collectScalaReExports(rootNode, relationships);
|
|
747
|
+
this.emitExportSurface(rootNode, exports, relationships);
|
|
748
|
+
this.collectDeclaredTypeRefs(rootNode, relationships, symbols);
|
|
749
|
+
this.collectExpressionUses(rootNode, relationships);
|
|
750
|
+
this.classifyEvidenceTier(relationships);
|
|
751
|
+
this.certifyJvmPlatformTargets(rootNode, relationships, symbols);
|
|
752
|
+
return {
|
|
753
|
+
symbols,
|
|
754
|
+
relationships,
|
|
755
|
+
imports,
|
|
756
|
+
exports,
|
|
757
|
+
metadata,
|
|
758
|
+
errors: metadata.errors || []
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* The MODULE a Scala file belongs to: its full package path.
|
|
763
|
+
*
|
|
764
|
+
* Read from the `package_clause` nodes themselves rather than from
|
|
765
|
+
* `metadata.packageName`, which the walk overwrites per clause. Scala writes a
|
|
766
|
+
* package header two ways and this corpus uses the chained one exclusively:
|
|
767
|
+
*
|
|
768
|
+
* package scala.tools.nsc <- one dotted clause
|
|
769
|
+
* package typechecker <- a second clause CHAINED onto it
|
|
770
|
+
*
|
|
771
|
+
* The full path is the two joined with "."; a single braced clause
|
|
772
|
+
* (`package a.b { … }`) needs no join. Measured on 600 corpus files
|
|
773
|
+
* (script/m14/tmp/scala-package-identity.mts): 599 files chained-only, 1
|
|
774
|
+
* mixed, 0 braced-only, and `metadata.packageName` equals the FULL path in
|
|
775
|
+
* only the 292 single-clause files — on the other 308 it holds the last
|
|
776
|
+
* fragment (`typechecker`, `nsc`), which is not a package any reference site
|
|
777
|
+
* spells.
|
|
778
|
+
*
|
|
779
|
+
* Only TOP-LEVEL clauses are joined. A clause nested in a braced block is
|
|
780
|
+
* already written as its full path (`package reflect { … }` inside
|
|
781
|
+
* `package scala { … }` still spells `scala.reflect`), and joining it again
|
|
782
|
+
* would double the prefix.
|
|
783
|
+
*
|
|
784
|
+
* FILE_MODULE is the fallback for a file with no package clause at all — the
|
|
785
|
+
* default package, where the compilation unit IS the module. It is a sentinel
|
|
786
|
+
* and not a symbol, declared as such: zig's `(file)` and this lane's own
|
|
787
|
+
* `(script)`/`root` sentinels are the same kind of thing, and the alternative
|
|
788
|
+
* — publishing an empty source — is the defect the M60/SDJ-11 work removed.
|
|
789
|
+
*/
|
|
790
|
+
static FILE_MODULE = "(file)";
|
|
791
|
+
scalaModuleName(rootNode) {
|
|
792
|
+
const parts = [];
|
|
793
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
794
|
+
const c = rootNode.namedChild(i);
|
|
795
|
+
if (!c || c.type !== "package_clause") continue;
|
|
796
|
+
let id = null;
|
|
797
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
798
|
+
const x = c.namedChild(j);
|
|
799
|
+
if (x && x.type === "package_identifier") {
|
|
800
|
+
id = x;
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (!id) continue;
|
|
805
|
+
const segs = [];
|
|
806
|
+
const collect = (n) => {
|
|
807
|
+
if (n.type === "identifier") segs.push(n.text);
|
|
808
|
+
for (let k = 0; k < n.namedChildCount; k++) {
|
|
809
|
+
const y = n.namedChild(k);
|
|
810
|
+
if (y) collect(y);
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
collect(id);
|
|
814
|
+
const seg = segs.join(".");
|
|
815
|
+
if (seg.length > 0) parts.push(stripScalaQuoting(seg));
|
|
816
|
+
}
|
|
817
|
+
if (parts.length === 0) return _RealScalaAnalyzer.FILE_MODULE;
|
|
818
|
+
return parts.join(".");
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* `exports` — this module's public surface, as relationships.
|
|
822
|
+
*
|
|
823
|
+
* Two producers, one dedupe (see the call site for the measurement behind
|
|
824
|
+
* both):
|
|
825
|
+
*
|
|
826
|
+
* A. IMPLICIT, from the `ExportInfo[]` the walk already builds: every public
|
|
827
|
+
* TOP-LEVEL class/trait/object/enum/function/val/var/named-given. The
|
|
828
|
+
* array is the lane's existing definition of the surface — it is what the
|
|
829
|
+
* fixture gate's F3 export-uniqueness check reads — so the edges are
|
|
830
|
+
* derived from it rather than from a second, divergent rule that could
|
|
831
|
+
* drift from it.
|
|
832
|
+
*
|
|
833
|
+
* B. EXPLICIT, from Scala 3 `export_declaration`. The exported NAME is the
|
|
834
|
+
* leaf that is actually introduced here: a selector, or the final segment
|
|
835
|
+
* of a plain path. Two forms publish NO edge and the reason is the
|
|
836
|
+
* language's, not caution:
|
|
837
|
+
*
|
|
838
|
+
* `export a.b.*` re-exports an unknown set of names — the clause
|
|
839
|
+
* names a set, not a name, so an edge per member
|
|
840
|
+
* would mean inventing members this file never
|
|
841
|
+
* writes.
|
|
842
|
+
* `export a.b.given` re-exports the GIVEN INSTANCES resolved for
|
|
843
|
+
* `a.b`'s type — a set defined by implicit
|
|
844
|
+
* resolution in another compilation unit.
|
|
845
|
+
*
|
|
846
|
+
* Both are recorded in the edge metadata (`exportForm`) rather than
|
|
847
|
+
* dropped, so a reader can see that the file re-exports a wildcard and
|
|
848
|
+
* that the analyzer declines to enumerate it.
|
|
849
|
+
*
|
|
850
|
+
* `confidence: 1.0` and `evidenceClass: structural` (the tier classifier
|
|
851
|
+
* already routes `exports` there): the edge is read off a declaration, and
|
|
852
|
+
* nothing about it is inferred.
|
|
853
|
+
*/
|
|
854
|
+
emitExportSurface(rootNode, exports, relationships) {
|
|
855
|
+
const moduleName = this.scalaModuleName(rootNode);
|
|
856
|
+
const surfaceIndexes = /* @__PURE__ */ new Set();
|
|
857
|
+
const SURFACE_DECLS = /* @__PURE__ */ new Set([
|
|
858
|
+
"class_definition",
|
|
859
|
+
"trait_definition",
|
|
860
|
+
"object_definition",
|
|
861
|
+
"enum_definition",
|
|
862
|
+
"val_definition",
|
|
863
|
+
"var_definition",
|
|
864
|
+
"val_declaration",
|
|
865
|
+
"var_declaration",
|
|
866
|
+
"function_definition",
|
|
867
|
+
"given_definition"
|
|
868
|
+
]);
|
|
869
|
+
const collectSurface = (scope) => {
|
|
870
|
+
for (let i = 0; i < scope.namedChildCount; i++) {
|
|
871
|
+
const c = scope.namedChild(i);
|
|
872
|
+
if (!c) continue;
|
|
873
|
+
if (SURFACE_DECLS.has(c.type)) {
|
|
874
|
+
surfaceIndexes.add(this.declarationLineNode(c).startIndex);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (c.type === "package_clause" || c.type === "package_object") {
|
|
878
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
879
|
+
const b = c.namedChild(j);
|
|
880
|
+
if (b && (b.type === "block" || b.type === "template_body")) collectSurface(b);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
collectSurface(rootNode);
|
|
886
|
+
const seen = /* @__PURE__ */ new Set();
|
|
887
|
+
const surfaceChannelDuplicated = moduleName === _RealScalaAnalyzer.FILE_MODULE;
|
|
888
|
+
const key = (name) => `${moduleName} ${name}`;
|
|
889
|
+
for (const e of exports) {
|
|
890
|
+
if (surfaceChannelDuplicated) continue;
|
|
891
|
+
const name = stripScalaQuoting(String(e.name ?? ""));
|
|
892
|
+
if (name.length === 0) continue;
|
|
893
|
+
if (e.range?.startIndex !== void 0 && !surfaceIndexes.has(e.range.startIndex)) continue;
|
|
894
|
+
if (seen.has(key(name))) continue;
|
|
895
|
+
seen.add(key(name));
|
|
896
|
+
relationships.push({
|
|
897
|
+
source: moduleName,
|
|
898
|
+
target: name,
|
|
899
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
900
|
+
confidence: 1,
|
|
901
|
+
// M59 (#825): the surface is declared where the name is declared.
|
|
902
|
+
...e.range?.startLine ? { line_number: e.range.startLine } : {},
|
|
903
|
+
metadata: {
|
|
904
|
+
exportForm: "public-top-level-declaration",
|
|
905
|
+
symbolKind: String(e.kind ?? "")
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
const visit = (n) => {
|
|
910
|
+
if (n.type === "export_declaration") {
|
|
911
|
+
for (const t of this.scalaExportClauseTargets(n)) {
|
|
912
|
+
const k = key(t.name);
|
|
913
|
+
if (seen.has(k)) continue;
|
|
914
|
+
seen.add(k);
|
|
915
|
+
relationships.push({
|
|
916
|
+
source: moduleName,
|
|
917
|
+
target: t.name,
|
|
918
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
919
|
+
confidence: 1,
|
|
920
|
+
line_number: n.startPosition.row + 1,
|
|
921
|
+
metadata: { exportForm: t.form }
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
926
|
+
const c = n.namedChild(i);
|
|
927
|
+
if (c) visit(c);
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
visit(rootNode);
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* The names one Scala 3 `export` clause introduces into this file, read from
|
|
934
|
+
* the grammar's own node types (probe:
|
|
935
|
+
* script/m14/tmp/scala-scala3-export-clause.mts dumps all six forms):
|
|
936
|
+
*
|
|
937
|
+
* export a.b.c -> export_declaration > identifier "." identifier
|
|
938
|
+
* "." identifier => leaf `c`
|
|
939
|
+
* export a.b.{c, d} -> namespace_selectors > identifier => `c`, `d`
|
|
940
|
+
* export a.b.{c => d, e} -> namespace_selectors > arrow_renamed_identifier
|
|
941
|
+
* > identifier "=>" identifier => `d`
|
|
942
|
+
* (the name INTRODUCED here is `d`; `c` is
|
|
943
|
+
* only reachable under it)
|
|
944
|
+
* export a.b.{given C} -> namespace_selectors > given type_identifier
|
|
945
|
+
* => `C`
|
|
946
|
+
* export a.b.* -> namespace_wildcard => no name to publish
|
|
947
|
+
* export a.b.given -> namespace_wildcard `given` => no name to publish
|
|
948
|
+
*
|
|
949
|
+
* Each entry carries the FORM it came from, so the edge metadata says which
|
|
950
|
+
* spelling produced the name.
|
|
951
|
+
*/
|
|
952
|
+
scalaExportClauseTargets(node) {
|
|
953
|
+
const out = [];
|
|
954
|
+
const path = [];
|
|
955
|
+
let wildcard = false;
|
|
956
|
+
let sawSelectors = false;
|
|
957
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
958
|
+
const c = node.namedChild(i);
|
|
959
|
+
if (!c) continue;
|
|
960
|
+
if (c.type === "identifier") {
|
|
961
|
+
path.push(c.text);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (c.type === "namespace_selectors") {
|
|
965
|
+
sawSelectors = true;
|
|
966
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
967
|
+
const s = c.namedChild(j);
|
|
968
|
+
if (!s) continue;
|
|
969
|
+
if (s.type === "identifier") {
|
|
970
|
+
out.push({ name: stripScalaQuoting(s.text), form: "scala3-export-selector" });
|
|
971
|
+
} else if (s.type === "arrow_renamed_identifier") {
|
|
972
|
+
let last = null;
|
|
973
|
+
for (let k = 0; k < s.namedChildCount; k++) {
|
|
974
|
+
const id = s.namedChild(k);
|
|
975
|
+
if (id && id.type === "identifier") last = id.text;
|
|
976
|
+
}
|
|
977
|
+
if (last) out.push({ name: stripScalaQuoting(last), form: "scala3-export-renamed-selector" });
|
|
978
|
+
} else if (s.type === "type_identifier") {
|
|
979
|
+
out.push({ name: stripScalaQuoting(s.text), form: "scala3-export-given-selector" });
|
|
980
|
+
} else if (s.type === "generic_type") {
|
|
981
|
+
out.push({ name: this.scalaTypeHeadName(s), form: "scala3-export-given-selector" });
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (c.type === "namespace_wildcard") {
|
|
987
|
+
wildcard = true;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (out.length === 0 && !wildcard && !sawSelectors && path.length > 0) {
|
|
991
|
+
out.push({ name: stripScalaQuoting(path[path.length - 1]), form: "scala3-export-clause" });
|
|
992
|
+
}
|
|
993
|
+
return out.filter((t) => t.name.length > 0);
|
|
994
|
+
}
|
|
995
|
+
detectFrameworks(imports, frameworksObj) {
|
|
996
|
+
for (let i = 0; i < imports.length; i++) {
|
|
997
|
+
const source = imports[i].module;
|
|
998
|
+
if (!source) continue;
|
|
999
|
+
if (source.indexOf("play.api") !== -1) frameworksObj["Play Framework"] = true;
|
|
1000
|
+
if (source.indexOf("akka") !== -1) frameworksObj["Akka"] = true;
|
|
1001
|
+
if (source.indexOf("org.apache.spark") !== -1) frameworksObj["Apache Spark"] = true;
|
|
1002
|
+
const isRoot = (root) => source === root || source.startsWith(root + ".");
|
|
1003
|
+
if (isRoot("cats")) frameworksObj["Cats Effect"] = true;
|
|
1004
|
+
if (isRoot("zio")) frameworksObj["ZIO"] = true;
|
|
1005
|
+
if (isRoot("org.http4s")) frameworksObj["http4s"] = true;
|
|
1006
|
+
if (isRoot("slick") || isRoot("org.slick")) frameworksObj["Slick"] = true;
|
|
1007
|
+
if (isRoot("fs2")) frameworksObj["FS2"] = true;
|
|
1008
|
+
if (isRoot("org.scalatest")) frameworksObj["ScalaTest"] = true;
|
|
1009
|
+
if (isRoot("org.springframework.boot")) frameworksObj["Spring Boot"] = true;
|
|
1010
|
+
if (isRoot("org.springframework")) frameworksObj["Spring Framework"] = true;
|
|
1011
|
+
if (isRoot("com.fasterxml.jackson")) frameworksObj["Jackson"] = true;
|
|
1012
|
+
if (isRoot("com.github.plokhotnyuk.jsoniter_scala")) frameworksObj["jsoniter-scala"] = true;
|
|
1013
|
+
if (isRoot("com.softwaremill.sttp")) frameworksObj["sttp"] = true;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
detectCompanionObjects(symbols, patternsObj) {
|
|
1017
|
+
const classNames = {};
|
|
1018
|
+
const objectNames = {};
|
|
1019
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1020
|
+
const sym = symbols[i];
|
|
1021
|
+
if (sym.kind === ASTEngine.SymbolKind.Class || sym.metadata?.isTrait) {
|
|
1022
|
+
classNames[sym.name] = true;
|
|
1023
|
+
} else if (sym.metadata?.isObject) {
|
|
1024
|
+
objectNames[sym.name] = true;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1028
|
+
const sym = symbols[i];
|
|
1029
|
+
if ((sym.kind === ASTEngine.SymbolKind.Class || sym.metadata?.isTrait) && objectNames[sym.name]) {
|
|
1030
|
+
if (!sym.metadata) sym.metadata = {};
|
|
1031
|
+
sym.metadata.hasCompanionObject = true;
|
|
1032
|
+
patternsObj["Companion Objects"] = true;
|
|
1033
|
+
} else if (sym.metadata?.isObject && classNames[sym.name]) {
|
|
1034
|
+
if (!sym.metadata) sym.metadata = {};
|
|
1035
|
+
sym.metadata.isCompanionObject = true;
|
|
1036
|
+
patternsObj["Companion Objects"] = true;
|
|
1037
|
+
}
|
|
1038
|
+
if (sym.metadata?.isCaseClass) {
|
|
1039
|
+
if (!sym.metadata) sym.metadata = {};
|
|
1040
|
+
sym.metadata.hasCompanionObject = true;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
extractIdentifier(node, field) {
|
|
1045
|
+
const child = node.childForFieldName(field);
|
|
1046
|
+
if (child) return child.text;
|
|
1047
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1048
|
+
const c = node.child(i);
|
|
1049
|
+
if (c && c.isNamed && (c.type === "identifier" || c.type === "type_identifier")) {
|
|
1050
|
+
return c.text;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
/** Collect modifiers (sealed/abstract/final/case/implicit/override/etc). */
|
|
1056
|
+
collectModifiers(node) {
|
|
1057
|
+
const mods = /* @__PURE__ */ new Set();
|
|
1058
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1059
|
+
const c = node.child(i);
|
|
1060
|
+
if (!c) continue;
|
|
1061
|
+
if (c.type === "modifiers") {
|
|
1062
|
+
for (let j = 0; j < c.childCount; j++) {
|
|
1063
|
+
const m = c.child(j);
|
|
1064
|
+
if (m) mods.add(m.type);
|
|
1065
|
+
}
|
|
1066
|
+
} else {
|
|
1067
|
+
mods.add(c.type);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return mods;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* SCD5: the short name of ONE annotation node (@Deprecated →
|
|
1074
|
+
* "Deprecated", @org.x.Transactional → "Transactional"). The walk calls
|
|
1075
|
+
* this on the annotation node itself; collectAnnotations (below) reuses it
|
|
1076
|
+
* for every annotation child of a definition.
|
|
1077
|
+
*/
|
|
1078
|
+
annotationShortName(ann) {
|
|
1079
|
+
for (let k = 0; k < ann.namedChildCount; k++) {
|
|
1080
|
+
const nc = ann.namedChild(k);
|
|
1081
|
+
if (nc && (nc.type === "identifier" || nc.type === "type_identifier" || nc.type === "stable_type_identifier")) {
|
|
1082
|
+
const text = nc.text;
|
|
1083
|
+
const dot = text.lastIndexOf(".");
|
|
1084
|
+
return dot === -1 ? text : text.slice(dot + 1);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Annotation short names from a definition's modifiers (@Deprecated →
|
|
1091
|
+
* "Deprecated", @org.x.Transactional → "Transactional"). Previously
|
|
1092
|
+
* annotations were consumed only as pattern-flag substrings and symbols
|
|
1093
|
+
* carried no decorator data at all, unlike every sibling analyzer.
|
|
1094
|
+
*/
|
|
1095
|
+
collectAnnotations(node) {
|
|
1096
|
+
const out = [];
|
|
1097
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1098
|
+
const c = node.child(i);
|
|
1099
|
+
if (!c) continue;
|
|
1100
|
+
if (c.type !== "annotation" && c.type !== "modifiers") continue;
|
|
1101
|
+
const container = c.type === "annotation" ? null : c;
|
|
1102
|
+
const sources = container ? [container] : [c];
|
|
1103
|
+
for (const src of sources) {
|
|
1104
|
+
for (let j = 0; j < src.childCount; j++) {
|
|
1105
|
+
const ann = container ? src.child(j) : src;
|
|
1106
|
+
if (!ann || ann.type !== "annotation") continue;
|
|
1107
|
+
const short = this.annotationShortName(ann);
|
|
1108
|
+
if (short) out.push(short);
|
|
1109
|
+
if (!container) break;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return out;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* M59 (#825): the node whose START is the declaration's own line.
|
|
1117
|
+
*
|
|
1118
|
+
* tree-sitter-scala gives a definition the line of its LEADING ANNOTATION —
|
|
1119
|
+
* measured, not assumed: for
|
|
1120
|
+
*
|
|
1121
|
+
* @tailrec ← row 4
|
|
1122
|
+
* def loop(n: Int): Int = ← row 5
|
|
1123
|
+
*
|
|
1124
|
+
* the `function_definition` node starts at row 4 while its `name` field is at
|
|
1125
|
+
* row 5. Stamping the node's start put the `contains` edge (and the symbol's
|
|
1126
|
+
* own line) on the annotation, so the line no longer contained the declared
|
|
1127
|
+
* name — 4 of 13 remaining scala oracle failures on a 60-file sample, and the
|
|
1128
|
+
* shape that would otherwise be copied by java/kotlin/groovy.
|
|
1129
|
+
*
|
|
1130
|
+
* An annotation is a modifier: it decorates the declaration, it is not where
|
|
1131
|
+
* the declaration is written. Skipping leading annotations (and `modifiers`
|
|
1132
|
+
* wrappers) gives the declaration keyword line, which is what a tool must
|
|
1133
|
+
* point at and what the tier contract can check against the source.
|
|
1134
|
+
*
|
|
1135
|
+
* COMMENTS are skipped for the same reason, and that is not hypothetical —
|
|
1136
|
+
* measured on cats-effect's QueueBenchmark.scala, where
|
|
1137
|
+
*
|
|
1138
|
+
* @Param(Array("32768")) // must be a power of 2
|
|
1139
|
+
* var size: Int = _
|
|
1140
|
+
*
|
|
1141
|
+
* gives `var_definition` the named children `annotation, comment, identifier,
|
|
1142
|
+
* type_identifier, wildcard`: skipping only the annotation selected the
|
|
1143
|
+
* trailing COMMENT, so the symbol and its `contains` edge landed on the
|
|
1144
|
+
* annotation's line (47, column 26) instead of the declaration's (48, column
|
|
1145
|
+
* 6). A comment is text ABOUT the declaration, never the declaration.
|
|
1146
|
+
*/
|
|
1147
|
+
declarationLineNode(node) {
|
|
1148
|
+
const nameNode = node.childForFieldName?.("name");
|
|
1149
|
+
if (nameNode) return nameNode;
|
|
1150
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1151
|
+
const c = node.namedChild(i);
|
|
1152
|
+
if (!c) continue;
|
|
1153
|
+
if (c.type === "annotation" || c.type === "modifiers" || c.type === "opaque_modifier" || c.type === "inline_modifier") continue;
|
|
1154
|
+
if (c.type === "comment" || c.type === "block_comment") continue;
|
|
1155
|
+
return c;
|
|
1156
|
+
}
|
|
1157
|
+
return node;
|
|
1158
|
+
}
|
|
1159
|
+
getVisibility(node) {
|
|
1160
|
+
let modifiersNode = null;
|
|
1161
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1162
|
+
const c = node.child(i);
|
|
1163
|
+
if (c && c.type === "modifiers") {
|
|
1164
|
+
modifiersNode = c;
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (modifiersNode) {
|
|
1169
|
+
for (let i = 0; i < modifiersNode.childCount; i++) {
|
|
1170
|
+
const mod = modifiersNode.child(i);
|
|
1171
|
+
if (mod && mod.type === "access_modifier") {
|
|
1172
|
+
if (mod.text.includes("[")) return "package";
|
|
1173
|
+
const inner = mod.child(0);
|
|
1174
|
+
if (inner && (inner.type === "private" || inner.type === "protected")) return inner.type;
|
|
1175
|
+
}
|
|
1176
|
+
if (mod && (mod.type === "private" || mod.type === "protected")) return mod.type;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return "public";
|
|
1180
|
+
}
|
|
1181
|
+
isPublic(node) {
|
|
1182
|
+
return this.getVisibility(node) === "public";
|
|
1183
|
+
}
|
|
1184
|
+
isCaseClass(node) {
|
|
1185
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1186
|
+
const c = node.child(i);
|
|
1187
|
+
if (c && c.type === "case") return true;
|
|
1188
|
+
}
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* ScalaDoc extraction: a multiline_comment starting with '/**' immediately
|
|
1193
|
+
* preceding the declaration (previous sibling). Mirrors the kotlin KDoc
|
|
1194
|
+
* approach; previously scala symbols carried NO comments at all.
|
|
1195
|
+
*/
|
|
1196
|
+
extractScalaDoc(node) {
|
|
1197
|
+
const sibling = node.previousNamedSibling;
|
|
1198
|
+
if (sibling && sibling.type === "block_comment" && sibling.text.startsWith("/**")) return sibling.text;
|
|
1199
|
+
return void 0;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Generic type-parameter extraction from type_parameters nodes
|
|
1203
|
+
* (`class Store[T]`, `def map[R](...)`). Previously scala symbols carried
|
|
1204
|
+
* NO generic information at all - silent absence (M14 #383 fix).
|
|
1205
|
+
*/
|
|
1206
|
+
extractScalaTypeParameters(node) {
|
|
1207
|
+
const tpNode = node.namedChildren.find((c) => c.type === "type_parameters");
|
|
1208
|
+
if (!tpNode) return [];
|
|
1209
|
+
const out = [];
|
|
1210
|
+
for (let i = 0; i < tpNode.namedChildCount; i++) {
|
|
1211
|
+
const c = tpNode.namedChild(i);
|
|
1212
|
+
if (c && this.isTypeishNodeName(c.type)) {
|
|
1213
|
+
out.push(c.text.trim());
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return out;
|
|
1217
|
+
}
|
|
1218
|
+
buildScalaCallPath(fnNode) {
|
|
1219
|
+
const parts = [];
|
|
1220
|
+
let cur = fnNode;
|
|
1221
|
+
while (cur) {
|
|
1222
|
+
if (cur.type === "field_expression") {
|
|
1223
|
+
parts.unshift(cur.namedChild(cur.namedChildCount - 1)?.text ?? "");
|
|
1224
|
+
cur = cur.namedChild(0);
|
|
1225
|
+
} else if (cur.type === "call_expression") {
|
|
1226
|
+
cur = cur.namedChild(0);
|
|
1227
|
+
} else {
|
|
1228
|
+
parts.unshift(cur.text);
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return parts.filter(Boolean).join(".");
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Per-function cyclomatic complexity (1 + decision points) - java/kotlin
|
|
1236
|
+
* parity (M14 #301 prerequisite).
|
|
1237
|
+
*/
|
|
1238
|
+
calculateScalaFunctionComplexity(node) {
|
|
1239
|
+
let complexity = 1;
|
|
1240
|
+
const stack = [node];
|
|
1241
|
+
while (stack.length > 0) {
|
|
1242
|
+
const n = stack.pop();
|
|
1243
|
+
if (n.type === "if_expression" || n.type === "while_expression" || n.type === "do_while_expression" || n.type === "for_expression") {
|
|
1244
|
+
complexity++;
|
|
1245
|
+
} else if (n.type === "case_clause") {
|
|
1246
|
+
complexity++;
|
|
1247
|
+
} else if (n.type === "infix_expression") {
|
|
1248
|
+
const op = n.childForFieldName("operator")?.text;
|
|
1249
|
+
if (op === "&&" || op === "||") complexity++;
|
|
1250
|
+
}
|
|
1251
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1252
|
+
const child = n.namedChild(i);
|
|
1253
|
+
if (child) stack.push(child);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return complexity;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* M14 #299 - evidence-tier classification for EVERY emitted relationship.
|
|
1260
|
+
* Tiers: structural (grammar clauses, conf 1.0) | usage_contextual (calls,
|
|
1261
|
+
* field access, companion-apply idioms) | import_based | reference_qualified
|
|
1262
|
+
* | inferred (sub-1.0 resolutions). Applied centrally at end of analyze()
|
|
1263
|
+
* so no emission site can forget it.
|
|
1264
|
+
*/
|
|
1265
|
+
/**
|
|
1266
|
+
* Declared-type reference graph (M14). References edges from the enclosing
|
|
1267
|
+
* function/class to every named type in a declaration position. Scala's
|
|
1268
|
+
* @throws[IO] annotation parses as generic_type("throws", [IO]) — the
|
|
1269
|
+
* "throws" name is skipped, its arguments become throws_type edges.
|
|
1270
|
+
*/
|
|
1271
|
+
collectDeclaredTypeRefs(rootNode, relationships, declaredSymbols = []) {
|
|
1272
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1273
|
+
const declaredNames = /* @__PURE__ */ new Set();
|
|
1274
|
+
for (const s of declaredSymbols) if (s?.name) declaredNames.add(stripScalaQuoting(String(s.name)));
|
|
1275
|
+
const collectTypeNames = (n, out) => {
|
|
1276
|
+
const push = (name0) => {
|
|
1277
|
+
const name = stripScalaQuoting(name0);
|
|
1278
|
+
if (!name || name === "throws") return;
|
|
1279
|
+
if (out.some((x) => x.name === name)) return;
|
|
1280
|
+
out.push({ name, line: n.startPosition.row + 1 });
|
|
1281
|
+
};
|
|
1282
|
+
if (n.type === "type_identifier") {
|
|
1283
|
+
if (this.isScalaPlaceholderFragment(n)) return;
|
|
1284
|
+
const bare = stripScalaQuoting(n.text);
|
|
1285
|
+
if ((bare === "?" || bare === "_") && !declaredNames.has(bare)) return;
|
|
1286
|
+
push(n.text);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (n.type === "stable_type_identifier") {
|
|
1290
|
+
push(n.text);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1294
|
+
const c = n.namedChild(i);
|
|
1295
|
+
if (c) collectTypeNames(c, out);
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
const resolveSource = (start) => {
|
|
1299
|
+
let p = start.type === "function_definition" || start.type === "function_declaration" ? start : start.parent;
|
|
1300
|
+
while (p) {
|
|
1301
|
+
if (p.type === "function_definition" || p.type === "function_declaration") {
|
|
1302
|
+
const name = this.scalaDeclaredNameOf(p);
|
|
1303
|
+
if (name) return name;
|
|
1304
|
+
}
|
|
1305
|
+
if (p.type === "class_definition" || p.type === "object_definition" || p.type === "trait_definition" || p.type === "enum_definition") {
|
|
1306
|
+
const name = this.scalaDeclaredNameOf(p);
|
|
1307
|
+
if (name) return name;
|
|
1308
|
+
}
|
|
1309
|
+
p = p.parent;
|
|
1310
|
+
}
|
|
1311
|
+
return "(script)";
|
|
1312
|
+
};
|
|
1313
|
+
const emit = (decl, roots, kind, relType = "declared_type", sourceOverride, leafOnly = false) => {
|
|
1314
|
+
const source = sourceOverride ?? resolveSource(decl);
|
|
1315
|
+
for (const r of roots) {
|
|
1316
|
+
if (!r) continue;
|
|
1317
|
+
const names = [];
|
|
1318
|
+
collectTypeNames(r, names);
|
|
1319
|
+
for (const { name: raw, line: nameLine } of names) {
|
|
1320
|
+
const t = leafOnly ? raw.includes(".") ? raw.slice(raw.lastIndexOf(".") + 1) : raw : raw;
|
|
1321
|
+
if (!t) continue;
|
|
1322
|
+
const key = `${source}|${t}|${kind}`;
|
|
1323
|
+
if (emitted.has(key)) continue;
|
|
1324
|
+
emitted.add(key);
|
|
1325
|
+
relationships.push({
|
|
1326
|
+
source,
|
|
1327
|
+
target: t,
|
|
1328
|
+
type: ASTEngine.RelationshipType.References,
|
|
1329
|
+
confidence: 0.95,
|
|
1330
|
+
// M59 (#825): a type reference occurs where the type is NAMED, so
|
|
1331
|
+
// the line is the type node's own — this one site covers
|
|
1332
|
+
// declared_type / return_type / pattern_type / type_alias /
|
|
1333
|
+
// cast_target / instanceof_check / given_type / tuple_type /
|
|
1334
|
+
// annotation_type, which together were 1,129 line-less edges on a
|
|
1335
|
+
// 60-file scala sample. Note what it is NOT: the declaration's line.
|
|
1336
|
+
// `def f: Map[K, V]` written over several lines references Map on
|
|
1337
|
+
// the line Map is written on. `nameLine` (not `r`'s start) is what
|
|
1338
|
+
// makes that true when the type expression itself is multi-line.
|
|
1339
|
+
line_number: nameLine,
|
|
1340
|
+
metadata: { relationshipType: relType, declaredOn: decl.type }
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
const walk = (n) => {
|
|
1346
|
+
switch (n.type) {
|
|
1347
|
+
case "given_definition": {
|
|
1348
|
+
const roots = [];
|
|
1349
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1350
|
+
const c = n.namedChild(i);
|
|
1351
|
+
if (c && this.isTypePositionNode(c.type)) roots.push(c);
|
|
1352
|
+
}
|
|
1353
|
+
if (roots.length) emit(n, roots, "given_definition", "given_type");
|
|
1354
|
+
break;
|
|
1355
|
+
}
|
|
1356
|
+
case "class_parameter":
|
|
1357
|
+
case "parameter": {
|
|
1358
|
+
emit(n, [n], n.type);
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
case "val_definition":
|
|
1362
|
+
case "var_definition":
|
|
1363
|
+
case "val_declaration":
|
|
1364
|
+
case "var_declaration": {
|
|
1365
|
+
const roots = [];
|
|
1366
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1367
|
+
const c = n.namedChild(i);
|
|
1368
|
+
if (c && this.isTypePositionNode(c.type)) roots.push(c);
|
|
1369
|
+
}
|
|
1370
|
+
if (roots.length) emit(n, roots, n.type);
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1373
|
+
case "annotation": {
|
|
1374
|
+
const gt = this.findChild(n, "generic_type");
|
|
1375
|
+
if (gt && gt.namedChild(0)?.text === "throws") {
|
|
1376
|
+
const argsNode = this.findChild(gt, "type_arguments");
|
|
1377
|
+
if (argsNode) emit(n, [argsNode], "throws_annotation", "throws_type");
|
|
1378
|
+
}
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1381
|
+
case "function_definition":
|
|
1382
|
+
case "function_declaration": {
|
|
1383
|
+
let sawParams = false;
|
|
1384
|
+
const hasParamsClause = !!this.findChild(n, "parameters");
|
|
1385
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1386
|
+
const c = n.namedChild(i);
|
|
1387
|
+
if (!c) continue;
|
|
1388
|
+
if (c.type === "parameters") {
|
|
1389
|
+
sawParams = true;
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (c.type === "generic_type" || c.type === "type_identifier" || c.type === "function_type" || c.type === "tuple_type" || c.type === "stable_type_identifier") {
|
|
1393
|
+
if (!sawParams && c.namedChild(0)?.text === "throws") {
|
|
1394
|
+
const argsNode = this.findChild(c, "type_arguments");
|
|
1395
|
+
if (argsNode) emit(n, [argsNode], "throws_annotation", "throws_type");
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
emit(n, [c], c.type === "tuple_type" ? "tuple_type" : sawParams || !hasParamsClause ? "return_type" : "annotation_type", sawParams || !hasParamsClause ? "return_type" : "declared_type");
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
1403
|
+
case "call_expression": {
|
|
1404
|
+
const fn = n.childForFieldName?.("function");
|
|
1405
|
+
const fnText = fn ? fn.text : "";
|
|
1406
|
+
if (fnText.endsWith("asInstanceOf") || fnText.endsWith("isInstanceOf")) {
|
|
1407
|
+
const args = n.childForFieldName?.("arguments") || this.findChild(n, "type_arguments");
|
|
1408
|
+
if (args) emit(n, [args], fnText.endsWith("asInstanceOf") ? "cast_target" : "instanceof_check");
|
|
1409
|
+
}
|
|
1410
|
+
break;
|
|
1411
|
+
}
|
|
1412
|
+
case "generic_function": {
|
|
1413
|
+
const fe = this.findChild(n, "field_expression");
|
|
1414
|
+
const tail = fe ? fe.namedChild(fe.namedChildCount - 1)?.text ?? "" : "";
|
|
1415
|
+
if (tail === "asInstanceOf" || tail === "isInstanceOf") {
|
|
1416
|
+
const args = this.findChild(n, "type_arguments");
|
|
1417
|
+
if (args) emit(n, [args], tail === "asInstanceOf" ? "cast_target" : "instanceof_check", tail === "asInstanceOf" ? "cast_target" : "instanceof_check");
|
|
1418
|
+
}
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
case "typed_pattern": {
|
|
1422
|
+
const last = n.namedChild(n.namedChildCount - 1);
|
|
1423
|
+
if (last) emit(n, [last], "typed_pattern", "pattern_type");
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
case "case_class_pattern": {
|
|
1427
|
+
const first = n.namedChild(0);
|
|
1428
|
+
if (first) emit(n, [first], "case_class_pattern", "pattern_type");
|
|
1429
|
+
break;
|
|
1430
|
+
}
|
|
1431
|
+
case "derives_clause": {
|
|
1432
|
+
emit(n, [n], "derives_clause", "derives_type", void 0, true);
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
case "type_definition": {
|
|
1436
|
+
const tName = this.findChild(n, "type_identifier") ?? this.findChild(n, "identifier");
|
|
1437
|
+
if (tName) {
|
|
1438
|
+
const rhs = [];
|
|
1439
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1440
|
+
const c = n.namedChild(i);
|
|
1441
|
+
if (c && c !== tName && c.type !== "type_parameters") rhs.push(c);
|
|
1442
|
+
}
|
|
1443
|
+
emit(n, rhs, "type_definition", "type_alias", tName.text);
|
|
1444
|
+
}
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
case "self_type": {
|
|
1448
|
+
const last = n.namedChild(n.namedChildCount - 1);
|
|
1449
|
+
if (last) emit(n, [last], "self_type", "self_type");
|
|
1450
|
+
const required = this.selfTypeRequiredTypeNames(n);
|
|
1451
|
+
if (required.length > 0) {
|
|
1452
|
+
const src = resolveSource(n);
|
|
1453
|
+
for (const { name: rname, line: rline } of required) {
|
|
1454
|
+
if (!rname) continue;
|
|
1455
|
+
const leaf = rname.includes(".") ? rname.slice(rname.lastIndexOf(".") + 1) : rname;
|
|
1456
|
+
if (rname === src || leaf === src) continue;
|
|
1457
|
+
const ikey = `implements|${src}|${rname}`;
|
|
1458
|
+
if (emitted.has(ikey)) continue;
|
|
1459
|
+
emitted.add(ikey);
|
|
1460
|
+
relationships.push({
|
|
1461
|
+
source: src,
|
|
1462
|
+
target: rname,
|
|
1463
|
+
type: ASTEngine.RelationshipType.Implements,
|
|
1464
|
+
// S1.4: an explicit conformance requirement is 1.0 — the construct says so.
|
|
1465
|
+
confidence: 1,
|
|
1466
|
+
// M59 (#825): the NAME's own line, not the declaration's.
|
|
1467
|
+
line_number: rline,
|
|
1468
|
+
metadata: { relationshipType: "self_type", isSelfType: true }
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
case "field_expression": {
|
|
1475
|
+
const isCallFn = n.parent && n.parent.type === "call_expression" && n.parent.childForFieldName?.("function")?.id === n.id;
|
|
1476
|
+
if (!isCallFn) {
|
|
1477
|
+
const recv = this.leftmostIdentifier(n);
|
|
1478
|
+
if (recv && recv.text.length > 0 && recv.text.charCodeAt(0) >= 65 && recv.text.charCodeAt(0) <= 90) {
|
|
1479
|
+
const source = resolveSource(n);
|
|
1480
|
+
const key = `${source}|${recv.text}|field_expression`;
|
|
1481
|
+
if (!emitted.has(key)) {
|
|
1482
|
+
emitted.add(key);
|
|
1483
|
+
relationships.push({
|
|
1484
|
+
source,
|
|
1485
|
+
target: recv.text,
|
|
1486
|
+
type: ASTEngine.RelationshipType.References,
|
|
1487
|
+
confidence: 0.95,
|
|
1488
|
+
// M59 (#825): the referenced type is named by the receiver.
|
|
1489
|
+
line_number: recv.startPosition.row + 1,
|
|
1490
|
+
metadata: { relationshipType: "method_value", declaredOn: "field_expression" }
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
break;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1499
|
+
const c = n.namedChild(i);
|
|
1500
|
+
if (c) walk(c);
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
walk(rootNode);
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* M14 #419 G4/G8: expression-level Uses edges.
|
|
1507
|
+
* - interpolated_string_expression > interpolation: s"hi $name" USES the
|
|
1508
|
+
* interpolated identifier (simple $name form — the direct identifier
|
|
1509
|
+
* child). The ${expr} block form is expression-use territory (scala
|
|
1510
|
+
* emits no expression-level uses by design — D1/D3 boundary).
|
|
1511
|
+
* - field_expression (non-call position): h.value reads/writes and nullary
|
|
1512
|
+
* access xs.length USE the receiver — scala had no field-access uses
|
|
1513
|
+
* collector (only extension methods emitted Uses), the family gap vs
|
|
1514
|
+
* java/groovy/kotlin. Receiver-only: never guess method-vs-field (D3
|
|
1515
|
+
* ruling). Call positions (xs.map(...)) are skipped — the call edge
|
|
1516
|
+
* carries them; capitalized receivers (Foo.bar) are References (G3,
|
|
1517
|
+
* collectDeclaredTypeRefs). Deduped per (source, target).
|
|
1518
|
+
*/
|
|
1519
|
+
collectExpressionUses(rootNode, relationships) {
|
|
1520
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1521
|
+
const resolveSource = (start) => {
|
|
1522
|
+
let p = start && (start.type === "function_definition" || start.type === "function_declaration") ? start : start ? start.parent : null;
|
|
1523
|
+
while (p) {
|
|
1524
|
+
if (p.type === "function_definition" || p.type === "function_declaration") {
|
|
1525
|
+
const name = this.scalaDeclaredNameOf(p);
|
|
1526
|
+
if (name) return name;
|
|
1527
|
+
}
|
|
1528
|
+
if (p.type === "class_definition" || p.type === "object_definition" || p.type === "trait_definition" || p.type === "enum_definition") {
|
|
1529
|
+
const name = this.scalaDeclaredNameOf(p);
|
|
1530
|
+
if (name) return name;
|
|
1531
|
+
}
|
|
1532
|
+
p = p.parent;
|
|
1533
|
+
}
|
|
1534
|
+
return "(script)";
|
|
1535
|
+
};
|
|
1536
|
+
const emitUse = (n, target, nameNode) => {
|
|
1537
|
+
if (!target) return;
|
|
1538
|
+
if (target === "_") return;
|
|
1539
|
+
const p = n.parent;
|
|
1540
|
+
const isCompoundAssign = p !== null && p.type === "infix_expression" && scalaOperatorIsAssignment(p.childForFieldName?.("operator")?.text ?? p.namedChild(1)?.text ?? "");
|
|
1541
|
+
const lhs = p && (p.type === "assignment_expression" || isCompoundAssign) ? p.namedChild(0) : null;
|
|
1542
|
+
if (lhs && lhs.startIndex === n.startIndex && lhs.endIndex === n.endIndex) return;
|
|
1543
|
+
const source = resolveSource(n.parent);
|
|
1544
|
+
const key = `${source}|${target}|uses`;
|
|
1545
|
+
if (emitted.has(key)) return;
|
|
1546
|
+
emitted.add(key);
|
|
1547
|
+
relationships.push({
|
|
1548
|
+
source,
|
|
1549
|
+
target,
|
|
1550
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1551
|
+
// M59 (#825): a read happens where the NAME is written, so the line
|
|
1552
|
+
// comes from the name node — not from `n`, which for a field access is
|
|
1553
|
+
// the receiver and would hand a multi-line chain the line it opened on
|
|
1554
|
+
// (the same defect class fixed for calls above). `n` is the fallback
|
|
1555
|
+
// only when no name node was passed.
|
|
1556
|
+
line_number: (nameNode ?? n).startPosition.row + 1,
|
|
1557
|
+
confidence: 0.9,
|
|
1558
|
+
metadata: { relationshipType: "expression_use" }
|
|
1559
|
+
});
|
|
1560
|
+
};
|
|
1561
|
+
const walk = (n) => {
|
|
1562
|
+
if (n.type === "interpolation") {
|
|
1563
|
+
const id = n.namedChild(0);
|
|
1564
|
+
if (id && id.type === "identifier" && !isKeywordUsedAsName(id.text) && id.text !== "_") emitUse(n, id.text, id);
|
|
1565
|
+
} else if (n.type === "field_expression") {
|
|
1566
|
+
const isCallFn = n.parent && n.parent.type === "call_expression" && n.parent.childForFieldName?.("function")?.id === n.id;
|
|
1567
|
+
if (!isCallFn) {
|
|
1568
|
+
const recv = this.leftmostIdentifier(n);
|
|
1569
|
+
const isSelfReceiver = recv !== null && (recv.text === "this" || recv.text === "super");
|
|
1570
|
+
if (isSelfReceiver) {
|
|
1571
|
+
const leaf = n.childForFieldName?.("field");
|
|
1572
|
+
if (leaf && leaf.text.length > 0) emitUse(n, stripScalaQuoting(leaf.text), leaf);
|
|
1573
|
+
} else if (recv && recv.text.length > 0 && !(recv.text.charCodeAt(0) >= 65 && recv.text.charCodeAt(0) <= 90)) {
|
|
1574
|
+
emitUse(n, stripScalaQuoting(recv.text), recv);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1579
|
+
const c = n.namedChild(i);
|
|
1580
|
+
if (c) walk(c);
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
walk(rootNode);
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* SCALA 3 RE-EXPORT CLAUSES — `export a.b.{x, y}`, `export a.b.*`,
|
|
1587
|
+
* `export a.b.{x => y}`.
|
|
1588
|
+
*
|
|
1589
|
+
* This is NOT the export convention. A public top-level declaration is this
|
|
1590
|
+
* file's OWN API; a re-export clause names something the file does not define
|
|
1591
|
+
* and makes it reachable under this package — a different fact, and the only
|
|
1592
|
+
* one of the two that is a DEPENDENCY in the graph's sense (it points at
|
|
1593
|
+
* another compilation unit's name).
|
|
1594
|
+
*
|
|
1595
|
+
* Node shape, measured (script/m14/tmp/scala-probe-export.ts):
|
|
1596
|
+
* export a.b.{x, y} -> (export_declaration path: (identifier) path: (identifier)
|
|
1597
|
+
* (namespace_selectors (identifier) (identifier)))
|
|
1598
|
+
* export a.b.* -> (export_declaration path: … path: … (namespace_wildcard))
|
|
1599
|
+
* export a.b.{x => y} -> (export_declaration … (namespace_selectors (arrow_renamed_identifier
|
|
1600
|
+
* name: (identifier) alias: (identifier))))
|
|
1601
|
+
*
|
|
1602
|
+
* The pinned grammar parses ONLY this standalone form. `export class Alpha`,
|
|
1603
|
+
* `export object Beta`, `export type Foo = Int` and `export def f(..)` all
|
|
1604
|
+
* produce an ERROR node, so the export MODIFIER spelling of the language does
|
|
1605
|
+
* not reach this analyzer at all — a separate limitation, recorded in the lane
|
|
1606
|
+
* doc, because a construct the grammar drops cannot be read by any walker over
|
|
1607
|
+
* it. (dotty's own sources use no re-export clause: 0 occurrences over 9,353
|
|
1608
|
+
* files. The emission is here because the CORPUS is one repository and the
|
|
1609
|
+
* construct is real — "the corpus does not use it" is not "the language cannot
|
|
1610
|
+
* express it", which is the distinction the ontology gate's json `exports`
|
|
1611
|
+
* note exists to enforce.)
|
|
1612
|
+
*
|
|
1613
|
+
* The target is the FULL re-exported path (`a.b.x`), not the bare selector,
|
|
1614
|
+
* because the clause's meaning is "this package now offers the name a.b.x";
|
|
1615
|
+
* publishing `x` alone would name a symbol the file neither declares nor
|
|
1616
|
+
* imports. That is the same `lastSegment` exception the derives-clause already
|
|
1617
|
+
* makes (emit()'s `leafOnly`, G6), stated for the same reason: a re-export is
|
|
1618
|
+
* written as a path.
|
|
1619
|
+
*/
|
|
1620
|
+
collectScalaReExports(rootNode, relationships) {
|
|
1621
|
+
const stack = [rootNode];
|
|
1622
|
+
while (stack.length > 0) {
|
|
1623
|
+
const n = stack.pop();
|
|
1624
|
+
if (n.type === "export_declaration") {
|
|
1625
|
+
const parts = [];
|
|
1626
|
+
let wildcard = false;
|
|
1627
|
+
for (let i = 0; i < n.childCount; i++) {
|
|
1628
|
+
const c = n.child(i);
|
|
1629
|
+
if (!c) continue;
|
|
1630
|
+
if (c.type === "identifier") parts.push(stripScalaQuoting(c.text));
|
|
1631
|
+
else if (c.type === "namespace_wildcard") wildcard = true;
|
|
1632
|
+
else if (c.type === "namespace_selectors") {
|
|
1633
|
+
for (let j = 0; j < c.childCount; j++) {
|
|
1634
|
+
const sel = c.child(j);
|
|
1635
|
+
if (!sel) continue;
|
|
1636
|
+
if (sel.type === "identifier") parts.push(stripScalaQuoting(sel.text));
|
|
1637
|
+
else if (sel.type === "arrow_renamed_identifier") {
|
|
1638
|
+
const nm = sel.childForFieldName?.("name") ?? sel.namedChild(0);
|
|
1639
|
+
if (nm) parts.push(stripScalaQuoting(nm.text));
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (parts.length === 0) continue;
|
|
1645
|
+
const target = parts.join(".");
|
|
1646
|
+
relationships.push({
|
|
1647
|
+
source: "(file)",
|
|
1648
|
+
target,
|
|
1649
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
1650
|
+
confidence: 1,
|
|
1651
|
+
line_number: n.startPosition.row + 1,
|
|
1652
|
+
metadata: { isScalaExport: true, isScalaReExport: true, wildcard }
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1656
|
+
const c = n.namedChild(i);
|
|
1657
|
+
if (c) stack.push(c);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* CERTIFY THE JVM PLATFORM TARGETS — one pass over the finished edge list, additive.
|
|
1663
|
+
*
|
|
1664
|
+
* The claim, the list and the measurements behind it are on `SCALA_JVM_PLATFORM_ROOTS` above. This
|
|
1665
|
+
* method is the mechanism; these are the four guards, each one forced by a measured case rather than
|
|
1666
|
+
* by taste:
|
|
1667
|
+
*
|
|
1668
|
+
* 1. **DOTTED ONLY.** A bare target is refused even when it IS a platform word, because a scala
|
|
1669
|
+
* file can and does declare that name (`class java`, `val java`). 14 of the 117 candidate edges
|
|
1670
|
+
* in the measured population are a bare `java` read; every one of them is refused here.
|
|
1671
|
+
* 2. **NOT A NAME THIS FILE DECLARES.** The same guard zig uses, and the second line of defence
|
|
1672
|
+
* behind guard 4.
|
|
1673
|
+
* 3. **NO SECOND CLAIM.** An edge already certified by another mechanism keeps it.
|
|
1674
|
+
* 4. **NOT IN A FILE THAT WRITES IN A PLATFORM NAMESPACE.** `scala3/tests/pos/t8367.scala` declares
|
|
1675
|
+
* `package java.lang` and a `class Throwable` in it; a file whose own module leads with a
|
|
1676
|
+
* platform root is writing IN the namespace, so nothing it names is certified.
|
|
1677
|
+
*
|
|
1678
|
+
* RETURNS the number of edges certified, so a caller can print it — the count is not asserted here
|
|
1679
|
+
* because the analyzer publishes no file-level metadata key for it (adding one would change a payload
|
|
1680
|
+
* every fixture reader sees, for a number the gate already reports).
|
|
1681
|
+
*/
|
|
1682
|
+
certifyJvmPlatformTargets(rootNode, relationships, symbols) {
|
|
1683
|
+
const moduleName = this.scalaModuleName(rootNode);
|
|
1684
|
+
const headDot = moduleName.indexOf(".");
|
|
1685
|
+
const ownModuleHead = moduleName === _RealScalaAnalyzer.FILE_MODULE ? "" : headDot < 0 ? moduleName : moduleName.slice(0, headDot);
|
|
1686
|
+
if (SCALA_JVM_PLATFORM_ROOTS.has(ownModuleHead)) return 0;
|
|
1687
|
+
const declaredHere = /* @__PURE__ */ new Set();
|
|
1688
|
+
for (const s of symbols) {
|
|
1689
|
+
if (s.name) declaredHere.add(String(s.name));
|
|
1690
|
+
const q = s.qualifiedName;
|
|
1691
|
+
if (typeof q === "string" && q.length > 0) declaredHere.add(q);
|
|
1692
|
+
}
|
|
1693
|
+
let certified = 0;
|
|
1694
|
+
for (const rel of relationships) {
|
|
1695
|
+
const md = rel.metadata;
|
|
1696
|
+
if (!md || typeof md !== "object") continue;
|
|
1697
|
+
let claimed = false;
|
|
1698
|
+
for (const k of SCALA_CLAIM_KEYS_ON_EDGE) {
|
|
1699
|
+
if (md[k] !== void 0) {
|
|
1700
|
+
claimed = true;
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (claimed) continue;
|
|
1705
|
+
const target = String(rel.target ?? "");
|
|
1706
|
+
const dot = target.indexOf(".");
|
|
1707
|
+
if (dot <= 0) continue;
|
|
1708
|
+
if (declaredHere.has(target)) continue;
|
|
1709
|
+
if (!SCALA_JVM_PLATFORM_ROOTS.has(target.slice(0, dot))) continue;
|
|
1710
|
+
md.languageNamespace = [...SCALA_JVM_PLATFORM_ROOTS];
|
|
1711
|
+
certified++;
|
|
1712
|
+
}
|
|
1713
|
+
return certified;
|
|
1714
|
+
}
|
|
1715
|
+
classifyEvidenceTier(relationships) {
|
|
1716
|
+
for (const r of relationships) {
|
|
1717
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
1718
|
+
if (md.evidenceClass) continue;
|
|
1719
|
+
const t = String(r.type ?? r.relationshipType ?? "");
|
|
1720
|
+
const conf = typeof r.confidence === "number" ? r.confidence : 1;
|
|
1721
|
+
if (t === "contains" || t === "exports") {
|
|
1722
|
+
md.evidenceClass = "structural";
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (t === "imports") {
|
|
1726
|
+
md.evidenceClass = "import_based";
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
if (t === "references") {
|
|
1730
|
+
md.evidenceClass = md.isImportDep ? "import_based" : "reference_qualified";
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
if (t === "extends" || t === "implements") {
|
|
1734
|
+
md.evidenceClass = conf >= 1 ? "structural" : "inferred";
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
if (t === "overrides") {
|
|
1738
|
+
md.evidenceClass = conf >= 1 ? "structural" : conf >= 0.85 ? "usage_contextual" : "inferred";
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
if (t === "instantiates") {
|
|
1742
|
+
md.evidenceClass = conf >= 0.9 ? "structural" : "usage_contextual";
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
md.evidenceClass = "usage_contextual";
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
createSymbol(name, kind, node, parent, extraMetadata = {}) {
|
|
1749
|
+
name = stripScalaQuoting(name);
|
|
1750
|
+
const decorators = this.collectAnnotations(node);
|
|
1751
|
+
const kdoc = this.extractScalaDoc(node);
|
|
1752
|
+
const decl = this.declarationLineNode(node);
|
|
1753
|
+
return {
|
|
1754
|
+
name,
|
|
1755
|
+
kind,
|
|
1756
|
+
range: {
|
|
1757
|
+
startLine: decl.startPosition.row + 1,
|
|
1758
|
+
startColumn: decl.startPosition.column + 1,
|
|
1759
|
+
endLine: node.endPosition.row + 1,
|
|
1760
|
+
endColumn: node.endPosition.column + 1,
|
|
1761
|
+
startIndex: decl.startIndex,
|
|
1762
|
+
endIndex: node.endIndex
|
|
1763
|
+
},
|
|
1764
|
+
id: `${parent ? parent.id + "." : ""}${name}`,
|
|
1765
|
+
parent: parent ? parent.name : void 0,
|
|
1766
|
+
ownerName: parent ? parent.name : void 0,
|
|
1767
|
+
// ScalaD-G (M37.21 #610, scala3 Setup.scala): companion objects share
|
|
1768
|
+
// the class NAME (object Setup + class Setup) and the override post-pass
|
|
1769
|
+
// looks up the OWNER in a name-keyed typeIndex — without ownerKind the
|
|
1770
|
+
// module shadowed the class and its superTypes were unreachable (BFS
|
|
1771
|
+
// died at hop 0, Setup.isPreCC -> SetupAPI.isPreCC never emitted).
|
|
1772
|
+
// Stamp ownerKind at walk time so the post-pass routes lookups to the
|
|
1773
|
+
// same-kind index entry.
|
|
1774
|
+
ownerKind: parent ? parent.kind : void 0,
|
|
1775
|
+
visibility: this.getVisibility(node),
|
|
1776
|
+
parameters: extraMetadata.parameters,
|
|
1777
|
+
returnType: extraMetadata.returnType,
|
|
1778
|
+
decorators,
|
|
1779
|
+
comments: kdoc ? [kdoc] : [],
|
|
1780
|
+
// Hoisted top-level per family contract: the DB ingester reads
|
|
1781
|
+
// symbol.complexity (|| 1) for gravity scoring — leaving it only in
|
|
1782
|
+
// metadata left every scala symbol at complexity 1 in the index.
|
|
1783
|
+
complexity: extraMetadata.complexity,
|
|
1784
|
+
metadata: { ...extraMetadata, annotations: decorators }
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
// ==========================================================================
|
|
1788
|
+
// ZERO-REGEX STRING HELPERS (M49 JVM-closure wave, user directive: analyzers
|
|
1789
|
+
// carry no regex unless a text micro-parse genuinely requires it).
|
|
1790
|
+
//
|
|
1791
|
+
// Each helper replaces a former regex literal with a char-scan whose
|
|
1792
|
+
// equivalence is provable by string algebra, recorded here per the
|
|
1793
|
+
// closure-audit rule that equivalence claims carry their own proof:
|
|
1794
|
+
//
|
|
1795
|
+
// - stripBracketGroups ≡ replace(/\[[^\[\]]*\]/g, ""), FAITHFULLY:
|
|
1796
|
+
// the regex matches `[` + a run containing NO `[` and NO `]` + `]`.
|
|
1797
|
+
// At each `[` the scan looks for the FIRST `]`; if any `[` occurs
|
|
1798
|
+
// before it, the match at this position fails (regex would advance)
|
|
1799
|
+
// and the `[` is emitted verbatim. A bare `>` or `<` is NEVER
|
|
1800
|
+
// consumed — measured on cats-effect `ParallelF[Id, *] ~> Id` where
|
|
1801
|
+
// a depth-scanner prototype dropped the `>` of `~>` (fabricated
|
|
1802
|
+
// `(ParallelF ~ Id)` targets, caught by the corpus differential).
|
|
1803
|
+
// - stripAngleGroups ≡ replace(/<[^<>]*>/g, ""), same first-delimiter
|
|
1804
|
+
// discipline for `<`/`>`; `~>` survives because a `>` with no
|
|
1805
|
+
// preceding unmatched `<` is copied through.
|
|
1806
|
+
// - stripTypeArgs ≡ bracket pass THEN angle pass (the original chained
|
|
1807
|
+
// replaces ran left-to-right in that order), then trim. Callers
|
|
1808
|
+
// loop to a fixpoint for nested groups, as before.
|
|
1809
|
+
// - compactType ≡ replace(/\s+/g, ""): `\s+` is a maximal whitespace
|
|
1810
|
+
// run; the scan copies non-space chars and skips runs — identical
|
|
1811
|
+
// output for every input (whitespace = Unicode space class per
|
|
1812
|
+
// JS \s; the scan uses the same set via c===" "||\t||\n||\r||\f||\v).
|
|
1813
|
+
// - isTypeishNodeName ≡ /identifier|type_identifier|_parameter$/.test
|
|
1814
|
+
// (unanchored) and ≡ /_type$|type_identifier/.test: `includes(sub)`
|
|
1815
|
+
// ≡ unanchored literal alternation; suffix tests use endsWith.
|
|
1816
|
+
// - isTypePositionNode ≡ (/type$|_type$/.test(t) || t==="type_identifier"
|
|
1817
|
+
// || t==="stable_type_identifier" || t==="generic_type"):
|
|
1818
|
+
// /type$|_type$/ over a node-type string ≡ endsWith("type") because
|
|
1819
|
+
// _type$ is subsumed by type$ (any string ending in "_type" ends
|
|
1820
|
+
// in "type") — the disjunction is exactly endsWith("type").
|
|
1821
|
+
// =========================================================================
|
|
1822
|
+
stripBracketGroups(s) {
|
|
1823
|
+
let out = "";
|
|
1824
|
+
for (let i = 0; i < s.length; i++) {
|
|
1825
|
+
if (s[i] !== "[") {
|
|
1826
|
+
out += s[i];
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
let j = i + 1;
|
|
1830
|
+
let nested = false;
|
|
1831
|
+
while (j < s.length && s[j] !== "]") {
|
|
1832
|
+
if (s[j] === "[") {
|
|
1833
|
+
nested = true;
|
|
1834
|
+
break;
|
|
1835
|
+
}
|
|
1836
|
+
j++;
|
|
1837
|
+
}
|
|
1838
|
+
if (j < s.length && !nested) {
|
|
1839
|
+
i = j;
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
out += s[i];
|
|
1843
|
+
}
|
|
1844
|
+
return out;
|
|
1845
|
+
}
|
|
1846
|
+
stripAngleGroups(s) {
|
|
1847
|
+
let out = "";
|
|
1848
|
+
for (let i = 0; i < s.length; i++) {
|
|
1849
|
+
if (s[i] !== "<") {
|
|
1850
|
+
out += s[i];
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
let j = i + 1;
|
|
1854
|
+
let nested = false;
|
|
1855
|
+
while (j < s.length && s[j] !== ">") {
|
|
1856
|
+
if (s[j] === "<") {
|
|
1857
|
+
nested = true;
|
|
1858
|
+
break;
|
|
1859
|
+
}
|
|
1860
|
+
j++;
|
|
1861
|
+
}
|
|
1862
|
+
if (j < s.length && !nested) {
|
|
1863
|
+
i = j;
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
out += s[i];
|
|
1867
|
+
}
|
|
1868
|
+
return out;
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* M60 (#924, scala E): the HEAD TYPE NAME of a type node — never the node's source
|
|
1872
|
+
* text. `new ScalaRandom[F](Applicative[F].pure(new JavaSecureRandom())) with
|
|
1873
|
+
* SecureRandom[F] {}` is an instance_expression whose type is a compound_type: a
|
|
1874
|
+
* whole EXPRESSION (constructor arguments included) whose NAME is the head type
|
|
1875
|
+
* constructor. The generic_type path already collapsed to the head; this applies the
|
|
1876
|
+
* same collapse to every type shape, descending the head position (compound base →
|
|
1877
|
+
* applied constructor → generic type) and falling back to the old text behaviour only
|
|
1878
|
+
* when the shape is none of those. Measured: SecureRandomCompanionPlatform.scala:70,
|
|
1879
|
+
* node shape in script/m14/tmp/c924-scala-val-shape.ts.
|
|
1880
|
+
*/
|
|
1881
|
+
scalaTypeHeadName(typeNode) {
|
|
1882
|
+
let n = typeNode;
|
|
1883
|
+
for (let guard = 0; n && guard < 8; guard++) {
|
|
1884
|
+
if (n.type === "type_identifier") return n.text;
|
|
1885
|
+
if (n.type === "compound_type") n = n.childForFieldName("base") ?? n.namedChild(0);
|
|
1886
|
+
else if (n.type === "applied_constructor_type") n = n.namedChild(0);
|
|
1887
|
+
else if (n.type === "generic_type") n = n.namedChild(0);
|
|
1888
|
+
else break;
|
|
1889
|
+
}
|
|
1890
|
+
return this.stripTypeArgs(typeNode.text);
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* M60/S3: the SUPER TYPE NAME for an ANONYMOUS class — structurally walked, and
|
|
1894
|
+
* "" when no type name is reachable.
|
|
1895
|
+
*
|
|
1896
|
+
* This exists beside scalaTypeHeadName rather than replacing it because the two
|
|
1897
|
+
* answer different questions. scalaTypeHeadName names an INSTANTIATION target,
|
|
1898
|
+
* where the node's own text is the honest type (`new (IO ~> F) {}` really does
|
|
1899
|
+
* instantiate the natural-transformation type `(IO ~> F)`), and that label is
|
|
1900
|
+
* audited. An ANONYMOUS CLASS's name is SYNTHETIC — the analyzer's own rule,
|
|
1901
|
+
* stated at its emission site, is "a SYNTHETIC name must be canonical - never
|
|
1902
|
+
* source text" (#924) — and scalaTypeHeadName's text fallback violates it: for
|
|
1903
|
+
* a type shape the walk does not recognise it returns `stripTypeArgs(text)`.
|
|
1904
|
+
*
|
|
1905
|
+
* Measured on both corpora (script/m14/tmp/scala-garbage-names.ts, 400 files
|
|
1906
|
+
* each) — `anonymous_<source text>` symbols, whose name is no declared name in
|
|
1907
|
+
* their own file:
|
|
1908
|
+
*
|
|
1909
|
+
* cats-effect 20 of 291, of which 18 are a natural-transformation supertype
|
|
1910
|
+
* `new (IO ~> F) { ... }` -> `anonymous_(IO ~> F)` (LiftIO.scala:36,
|
|
1911
|
+
* Async.scala:199, Resource.scala:244/580/997/1123, pure.scala x5, ...)
|
|
1912
|
+
* scala3 15 of 191, of which 2 are a MULTI-LINE body published as a NAME:
|
|
1913
|
+
* `anonymous_:\n private var cur = Env.this\n def hasNext = ...`
|
|
1914
|
+
* (CheckCaptures.scala:77, CapturedVars.scala:114)
|
|
1915
|
+
*
|
|
1916
|
+
* The shapes that must still resolve, probed before this was written rather
|
|
1917
|
+
* than guessed (script/m14/tmp/scala-snip.ts /tmp/anon.txt):
|
|
1918
|
+
*
|
|
1919
|
+
* new Runnable {} -> instance_expression > type_identifier
|
|
1920
|
+
* new java.util.Comparator[String] {} -> generic_type > stable_type_identifier
|
|
1921
|
+
* > stable_identifier(id,id) + type_identifier
|
|
1922
|
+
* new Console.MapKConsole[Int] {} -> generic_type > stable_type_identifier
|
|
1923
|
+
* > identifier + type_identifier
|
|
1924
|
+
* new ScalaRandom[F](a) with Sec[F] {} -> compound_type > base >
|
|
1925
|
+
* applied_constructor_type > generic_type
|
|
1926
|
+
*
|
|
1927
|
+
* A qualified name is its leading identifiers joined by "." plus the final
|
|
1928
|
+
* type_identifier — which is what the text happened to be, now for a reason.
|
|
1929
|
+
*/
|
|
1930
|
+
scalaAnonymousSupertypeName(typeNode) {
|
|
1931
|
+
if (!typeNode) return "";
|
|
1932
|
+
const walk = (n, guard) => {
|
|
1933
|
+
if (!n || guard > 8) return "";
|
|
1934
|
+
switch (n.type) {
|
|
1935
|
+
case "type_identifier":
|
|
1936
|
+
return n.text;
|
|
1937
|
+
case "compound_type": {
|
|
1938
|
+
const base = n.childForFieldName("base");
|
|
1939
|
+
return walk(base ?? n.namedChild(0), guard + 1);
|
|
1940
|
+
}
|
|
1941
|
+
case "applied_constructor_type":
|
|
1942
|
+
case "generic_type":
|
|
1943
|
+
return walk(n.namedChild(0), guard + 1);
|
|
1944
|
+
case "stable_type_identifier": {
|
|
1945
|
+
const segs = [];
|
|
1946
|
+
const collectStable = (t, guard2) => {
|
|
1947
|
+
if (guard2 > 8) return;
|
|
1948
|
+
for (let i = 0; i < t.namedChildCount; i++) {
|
|
1949
|
+
const c = t.namedChild(i);
|
|
1950
|
+
if (!c || c.type === "type_identifier") continue;
|
|
1951
|
+
if (c.type === "stable_identifier") collectStable(c, guard2 + 1);
|
|
1952
|
+
else if (c.type === "identifier") segs.push(c.text);
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
collectStable(n, 0);
|
|
1956
|
+
let last = "";
|
|
1957
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1958
|
+
const c = n.namedChild(i);
|
|
1959
|
+
if (c && c.type === "type_identifier") last = c.text;
|
|
1960
|
+
}
|
|
1961
|
+
if (!last) return "";
|
|
1962
|
+
return segs.length > 0 ? `${segs.join(".")}.${last}` : last;
|
|
1963
|
+
}
|
|
1964
|
+
default:
|
|
1965
|
+
return "";
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
return walk(typeNode, 0);
|
|
1969
|
+
}
|
|
1970
|
+
stripTypeArgs(s) {
|
|
1971
|
+
return this.stripAngleGroups(this.stripBracketGroups(s)).trim();
|
|
1972
|
+
}
|
|
1973
|
+
compactType(s) {
|
|
1974
|
+
let out = "";
|
|
1975
|
+
for (let i = 0; i < s.length; i++) {
|
|
1976
|
+
const c = s[i];
|
|
1977
|
+
if (c === " " || c === " " || c === "\n" || c === "\r" || c === "\f" || c === "\v") continue;
|
|
1978
|
+
out += c;
|
|
1979
|
+
}
|
|
1980
|
+
let seg = "";
|
|
1981
|
+
let sawDot = false;
|
|
1982
|
+
for (let i = 0; i < out.length; i++) {
|
|
1983
|
+
const c = out[i];
|
|
1984
|
+
if (c === ".") {
|
|
1985
|
+
sawDot = true;
|
|
1986
|
+
seg = "";
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
seg += c;
|
|
1990
|
+
}
|
|
1991
|
+
return sawDot ? seg : out;
|
|
1992
|
+
}
|
|
1993
|
+
/** ≡ /identifier|type_identifier|_parameter$/.test(t): unanchored "identifier" ≡ includes (type_identifier is subsumed by it). NO _type disjunct — deliberately narrower than isExtensionReceiverTypeNode. */
|
|
1994
|
+
isTypeishNodeName(t) {
|
|
1995
|
+
return t.includes("identifier") || t.endsWith("_parameter");
|
|
1996
|
+
}
|
|
1997
|
+
/** ≡ /_type$|type_identifier/.test(t): suffix or the exact node type — NOT the bare-identifier disjunct of isTypeishNodeName. */
|
|
1998
|
+
isExtensionReceiverTypeNode(t) {
|
|
1999
|
+
return t.endsWith("_type") || t.includes("type_identifier");
|
|
2000
|
+
}
|
|
2001
|
+
/** ≡ /type$|_type$/.test(t) || t === "type_identifier" || t === "stable_type_identifier" (generic_type variant adds itself). */
|
|
2002
|
+
isTypePositionNode(t) {
|
|
2003
|
+
return t.endsWith("type") || t === "type_identifier" || t === "stable_type_identifier";
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Is this `type_identifier` part of a Scala 3 PLACEHOLDER rather than a type
|
|
2007
|
+
* name?
|
|
2008
|
+
*
|
|
2009
|
+
* The grammar splits `-_` / `+_` / `-_*` into the operator character as a
|
|
2010
|
+
* `type_identifier` and the wildcard as a NEIGHBOUR TOKEN — but not always as
|
|
2011
|
+
* a sibling, which is why a first version of this guard (next SIBLING only)
|
|
2012
|
+
* suppressed 2 of the 6 corpus sites and left 4 published. Both shapes are
|
|
2013
|
+
* measured, on `scala/wildcard-is-not-a-name` line 12 and on the two corpus
|
|
2014
|
+
* files:
|
|
2015
|
+
*
|
|
2016
|
+
* vararg: -_* infix_type -> type_identifier "-" + identifier "_" + type_identifier "*"
|
|
2017
|
+
* byname: => -_ lazy_parameter_type -> type_identifier "-", and the `_`
|
|
2018
|
+
* lands as an ERROR SIBLING OF THE PARAMETER, one level up
|
|
2019
|
+
*
|
|
2020
|
+
* So the test is on the ADJACENT TOKEN in source order, found by walking to
|
|
2021
|
+
* the parent for as long as the node is the last child. A placeholder's
|
|
2022
|
+
* `_` is what follows the operator and what precedes the `*`, so the test is
|
|
2023
|
+
* "the next token is a wildcard, or the previous token is `_`".
|
|
2024
|
+
*
|
|
2025
|
+
* What this deliberately does NOT decide: a `?` wildcard (`def f(x: List[?])`)
|
|
2026
|
+
* parses as a BARE `type_identifier "?"` with no wildcard neighbour, so it is
|
|
2027
|
+
* indistinguishable here from the legal Scala 3 type name `?` (`type ?[A]`,
|
|
2028
|
+
* test/files/neg/qmark-deprecated.scala). Deciding it needs the file's
|
|
2029
|
+
* declared-type table, which this collector does not hold; it is recorded as
|
|
2030
|
+
* an open item in the lane doc rather than guessed at.
|
|
2031
|
+
*/
|
|
2032
|
+
isScalaPlaceholderFragment(n) {
|
|
2033
|
+
if (n.type !== "type_identifier") return false;
|
|
2034
|
+
let cur = n;
|
|
2035
|
+
while (cur) {
|
|
2036
|
+
const p = cur.parent;
|
|
2037
|
+
if (!p) break;
|
|
2038
|
+
let sawNext = false;
|
|
2039
|
+
for (let i = 0; i < p.childCount; i++) {
|
|
2040
|
+
const c = p.child(i);
|
|
2041
|
+
if (!c) continue;
|
|
2042
|
+
if (c.id === cur.id) {
|
|
2043
|
+
const next = p.child(i + 1);
|
|
2044
|
+
if (next) {
|
|
2045
|
+
sawNext = true;
|
|
2046
|
+
if (next.text === "_" || next.text === "?") return true;
|
|
2047
|
+
}
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (sawNext) break;
|
|
2052
|
+
cur = p;
|
|
2053
|
+
}
|
|
2054
|
+
cur = n;
|
|
2055
|
+
while (cur) {
|
|
2056
|
+
const p = cur.parent;
|
|
2057
|
+
if (!p) break;
|
|
2058
|
+
let sawPrev = false;
|
|
2059
|
+
for (let i = p.childCount - 1; i >= 0; i--) {
|
|
2060
|
+
const c = p.child(i);
|
|
2061
|
+
if (!c) continue;
|
|
2062
|
+
if (c.id === cur.id) {
|
|
2063
|
+
const prev = p.child(i - 1);
|
|
2064
|
+
if (prev) {
|
|
2065
|
+
sawPrev = true;
|
|
2066
|
+
if (prev.text === "_") return true;
|
|
2067
|
+
}
|
|
2068
|
+
break;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
if (sawPrev) break;
|
|
2072
|
+
cur = p;
|
|
2073
|
+
}
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* THE SELF-TYPE'S REQUIRED TYPE — the names a self type REQUIRES.
|
|
2078
|
+
*
|
|
2079
|
+
* ★ WHY THIS IS A DEDICATED READER AND NOT `collectTypeNames`. Two shapes need a decision the
|
|
2080
|
+
* generic reader does not make, and both are MEASURED over the admitted population
|
|
2081
|
+
* (validation/m14/measurements/scala-construct-census.json, 1,017 self_type sites in 636 files):
|
|
2082
|
+
*
|
|
2083
|
+
* 1. `singleton_type` — `this: IO.type =>` — spells its target as an `identifier` child
|
|
2084
|
+
* (`N:identifier A:. A:type`), and the generic reader only pushes `type_identifier` and
|
|
2085
|
+
* `stable_type_identifier`, so it would publish NOTHING for the 154 singleton sites.
|
|
2086
|
+
* The target of a singleton type is the MODULE's name, and that is the name the row needs.
|
|
2087
|
+
* 2. `type_arguments` — `this: xs.Cap[A] =>` — the `[A]` is a type PARAMETER of the required
|
|
2088
|
+
* type, NOT a second conformance requirement. The generic reader recurses into it and
|
|
2089
|
+
* would publish `A` as a conformance target, which is a fabrication: `A` is bound by the
|
|
2090
|
+
* required type, not required BY the declaring type.
|
|
2091
|
+
*
|
|
2092
|
+
* ★ AND THE POSITION IS FOUND BY THE ":" TOKEN, NOT BY INDEX. The grammar spells a self type
|
|
2093
|
+
* `<self-name> [":" <type>] "=>"`, and a trailing block comment is a NAMED child too — the
|
|
2094
|
+
* measured shape `identifier | block_comment | : | type_identifier | block_comment | =>`
|
|
2095
|
+
* would make "the second named child" the comment.
|
|
2096
|
+
*
|
|
2097
|
+
* ★ AND A `self =>` WITH NO ":" IS NOT A CONFORMANCE. It binds a self NAME and requires
|
|
2098
|
+
* nothing: the measured shapes are 344 `identifier =>` and 2 `wildcard =>` against 671 that
|
|
2099
|
+
* carry a type, so this reader returns nothing for them and the `implements` row is not
|
|
2100
|
+
* published — a self name is not a conformance requirement.
|
|
2101
|
+
*
|
|
2102
|
+
* ★ AND A `structural_type` REQUIRED TYPE NAMES NO CONFORMANCE TARGET: `this: { def f: Int } =>`
|
|
2103
|
+
* (4 sites) describes a shape, and the members inside it are not types the declaring type
|
|
2104
|
+
* conforms to. It is refused rather than mined for a name.
|
|
2105
|
+
*/
|
|
2106
|
+
selfTypeRequiredTypeNames(st) {
|
|
2107
|
+
const out = [];
|
|
2108
|
+
let colon = -1;
|
|
2109
|
+
for (let i = 0; i < st.childCount; i++) {
|
|
2110
|
+
const c = st.child(i);
|
|
2111
|
+
if (c && !c.isNamed && c.type === ":") {
|
|
2112
|
+
colon = i;
|
|
2113
|
+
break;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
if (colon < 0) return out;
|
|
2117
|
+
let req = null;
|
|
2118
|
+
for (let i = colon + 1; i < st.childCount; i++) {
|
|
2119
|
+
const c = st.child(i);
|
|
2120
|
+
if (!c || !c.isNamed) continue;
|
|
2121
|
+
if (c.type === "block_comment" || c.type === "comment") continue;
|
|
2122
|
+
req = c;
|
|
2123
|
+
break;
|
|
2124
|
+
}
|
|
2125
|
+
if (!req) return out;
|
|
2126
|
+
if (req.type === "structural_type") return out;
|
|
2127
|
+
if (req.type === "singleton_type") {
|
|
2128
|
+
for (let i = 0; i < req.namedChildCount; i++) {
|
|
2129
|
+
const c = req.namedChild(i);
|
|
2130
|
+
if (c && (c.type === "identifier" || c.type === "stable_identifier" || c.type === "stable_type_identifier")) {
|
|
2131
|
+
const nm = stripScalaQuoting(c.text);
|
|
2132
|
+
if (nm) out.push({ name: nm, line: c.startPosition.row + 1 });
|
|
2133
|
+
return out;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return out;
|
|
2137
|
+
}
|
|
2138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2139
|
+
const walk = (n) => {
|
|
2140
|
+
if (n.type === "type_arguments" || n.type === "wildcard") return;
|
|
2141
|
+
if (n.type === "type_identifier" || n.type === "stable_type_identifier") {
|
|
2142
|
+
const nm = stripScalaQuoting(n.text);
|
|
2143
|
+
if (nm && !seen.has(nm)) {
|
|
2144
|
+
seen.add(nm);
|
|
2145
|
+
out.push({ name: nm, line: n.startPosition.row + 1 });
|
|
2146
|
+
}
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2150
|
+
const c = n.namedChild(i);
|
|
2151
|
+
if (c) walk(c);
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
walk(req);
|
|
2155
|
+
return out;
|
|
2156
|
+
}
|
|
2157
|
+
/** Extract supertypes from an extends_clause (direct type children in v0.26.2). */
|
|
2158
|
+
/**
|
|
2159
|
+
* M59 (#825): each supertype name is paired with the LINE IT IS WRITTEN ON.
|
|
2160
|
+
*
|
|
2161
|
+
* Scala writes the parents of a long inheritance list one per line:
|
|
2162
|
+
*
|
|
2163
|
+
* trait IterableLike[+A]
|
|
2164
|
+
* extends FromIterable ← 106
|
|
2165
|
+
* with IterableOps[A] ← 107
|
|
2166
|
+
* with IterablePolyTransforms[A] ← 108
|
|
2167
|
+
*
|
|
2168
|
+
* Stamping the clause's start line put IterableOps/PolyTransforms on 106, a
|
|
2169
|
+
* line they are not written on — 8 of 8 remaining scala oracle failures at the
|
|
2170
|
+
* 300-file scale, including the `trait T1 extends { ... } with Base` form where
|
|
2171
|
+
* the parent appears AFTER an early-init block. Same defect family as the call
|
|
2172
|
+
* site (`#825` half one) and the declared-type reference: the line belongs to
|
|
2173
|
+
* the NAME, never to the construct containing it.
|
|
2174
|
+
*/
|
|
2175
|
+
extractSupertypes(extendsClause) {
|
|
2176
|
+
const out = [];
|
|
2177
|
+
const push = (name, n) => {
|
|
2178
|
+
if (!name) return;
|
|
2179
|
+
if (out.some((x) => x.name === name)) return;
|
|
2180
|
+
out.push({ name, line: n.startPosition.row + 1 });
|
|
2181
|
+
};
|
|
2182
|
+
for (let i = 0; i < extendsClause.childCount; i++) {
|
|
2183
|
+
const c = extendsClause.child(i);
|
|
2184
|
+
if (!c || !c.isNamed) continue;
|
|
2185
|
+
if (c.type === "type_identifier" || c.type === "identifier") {
|
|
2186
|
+
push(c.text, c);
|
|
2187
|
+
} else if (c.type === "stable_type_identifier") {
|
|
2188
|
+
push(c.text, c);
|
|
2189
|
+
} else if (c.type === "generic_type") {
|
|
2190
|
+
let pushed = false;
|
|
2191
|
+
for (let j = 0; j < c.childCount; j++) {
|
|
2192
|
+
const ch = c.child(j);
|
|
2193
|
+
if (ch && ch.type === "stable_type_identifier") {
|
|
2194
|
+
push(ch.text, ch);
|
|
2195
|
+
pushed = true;
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
if (!pushed) {
|
|
2200
|
+
for (let j = 0; j < c.childCount; j++) {
|
|
2201
|
+
const ch = c.child(j);
|
|
2202
|
+
if (ch && ch.type === "type_identifier") {
|
|
2203
|
+
push(ch.text, ch);
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
} else if (c.type === "compound_type") {
|
|
2209
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
2210
|
+
const ch = c.namedChild(j);
|
|
2211
|
+
if (ch && (ch.type === "type_identifier" || ch.type === "generic_type" || ch.type === "annotated_type")) {
|
|
2212
|
+
if (ch.type === "type_identifier") push(ch.text, ch);
|
|
2213
|
+
else if (ch.type === "annotated_type") {
|
|
2214
|
+
const base = this.annotatedTypeBase(ch);
|
|
2215
|
+
if (base) push(base.name, base.node);
|
|
2216
|
+
} else {
|
|
2217
|
+
const inner = this.extractSupertypes(ch);
|
|
2218
|
+
if (inner.length > 0) push(inner[0].name, ch);
|
|
2219
|
+
else push(ch.text, ch);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
} else if (c.type === "annotated_type") {
|
|
2224
|
+
const base = this.annotatedTypeBase(c);
|
|
2225
|
+
if (base) push(base.name, base.node);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return out;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* ★ THE BASE TYPE UNDER AN ANNOTATION. `annotated_type` holds the TYPE and then the `annotation`,
|
|
2232
|
+
* so the base is its first named child — and it is the base that names the supertype.
|
|
2233
|
+
*
|
|
2234
|
+
* The shape is measured from the pinned build, not assumed: `annotated_type` ->
|
|
2235
|
+
* (`type_identifier` | `generic_type` | `stable_type_identifier`) + `annotation`.
|
|
2236
|
+
*/
|
|
2237
|
+
// SCALA-ANN-SUPERTYPE-BLOCK-C:BEGIN
|
|
2238
|
+
annotatedTypeBase(n) {
|
|
2239
|
+
for (let j = 0; j < n.namedChildCount; j++) {
|
|
2240
|
+
const ch = n.namedChild(j);
|
|
2241
|
+
if (!ch) continue;
|
|
2242
|
+
if (ch.type === "type_identifier" || ch.type === "identifier" || ch.type === "stable_type_identifier") {
|
|
2243
|
+
return { name: ch.text, node: ch };
|
|
2244
|
+
}
|
|
2245
|
+
if (ch.type === "generic_type") {
|
|
2246
|
+
for (let k = 0; k < ch.childCount; k++) {
|
|
2247
|
+
const g = ch.child(k);
|
|
2248
|
+
if (g && g.type === "stable_type_identifier") return { name: g.text, node: g };
|
|
2249
|
+
}
|
|
2250
|
+
for (let k = 0; k < ch.childCount; k++) {
|
|
2251
|
+
const g = ch.child(k);
|
|
2252
|
+
if (g && g.type === "type_identifier") return { name: g.text, node: g };
|
|
2253
|
+
}
|
|
2254
|
+
return null;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
return null;
|
|
2258
|
+
}
|
|
2259
|
+
// SCALA-ANN-SUPERTYPE-BLOCK-C:END
|
|
2260
|
+
handleClass(node, symbols, relationships, exports, patternsObj, parent) {
|
|
2261
|
+
const name = this.extractIdentifier(node, "name");
|
|
2262
|
+
if (!name) return null;
|
|
2263
|
+
const mods = this.collectModifiers(node);
|
|
2264
|
+
const isCase = this.isCaseClass(node);
|
|
2265
|
+
const isSealed = mods.has("sealed");
|
|
2266
|
+
const isAbstract = mods.has("abstract");
|
|
2267
|
+
const isFinal = mods.has("final");
|
|
2268
|
+
const isImplicit = mods.has("implicit");
|
|
2269
|
+
const classParamsNode = this.findChild(node, "class_parameters");
|
|
2270
|
+
const constructorParams = [];
|
|
2271
|
+
if (classParamsNode) {
|
|
2272
|
+
const ownerName = stripScalaQuoting(name);
|
|
2273
|
+
const idStub = { id: ownerName, name: ownerName };
|
|
2274
|
+
const ctor = this.createSymbol("constructor", ASTEngine.SymbolKind.Constructor, classParamsNode, idStub, {
|
|
2275
|
+
type: "constructor",
|
|
2276
|
+
ownerName,
|
|
2277
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
2278
|
+
scalaSpecific: true
|
|
2279
|
+
});
|
|
2280
|
+
ctor.ownerName = ownerName;
|
|
2281
|
+
const ctorParams = [];
|
|
2282
|
+
symbols.push(ctor);
|
|
2283
|
+
for (let i = 0; i < classParamsNode.namedChildCount; i++) {
|
|
2284
|
+
const cp = classParamsNode.namedChild(i);
|
|
2285
|
+
if (cp?.type !== "class_parameter") continue;
|
|
2286
|
+
const pName = cp.childForFieldName("name")?.text;
|
|
2287
|
+
if (!pName) continue;
|
|
2288
|
+
constructorParams.push(pName);
|
|
2289
|
+
const paramType = cp.childForFieldName("type")?.text;
|
|
2290
|
+
if (paramType) ctorParams.push({ name: pName, type: paramType, isImplicit: false });
|
|
2291
|
+
const pMods = this.collectModifiers(cp);
|
|
2292
|
+
const isVal = pMods.has("val");
|
|
2293
|
+
const isVar = pMods.has("var");
|
|
2294
|
+
if (isVal || isVar || isCase) {
|
|
2295
|
+
const prop = this.createSymbol(pName, ASTEngine.SymbolKind.Property, cp, idStub, {
|
|
2296
|
+
type: isVar ? "var" : "val",
|
|
2297
|
+
isConstructorProperty: true,
|
|
2298
|
+
ownerName,
|
|
2299
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
2300
|
+
isCaseClassParam: isCase
|
|
2301
|
+
});
|
|
2302
|
+
prop.ownerName = ownerName;
|
|
2303
|
+
symbols.push(prop);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
if (ctorParams.length > 0) ctor.parameters = ctorParams;
|
|
2307
|
+
}
|
|
2308
|
+
if (isSealed) patternsObj["Sealed Types"] = true;
|
|
2309
|
+
const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Class, node, parent, {
|
|
2310
|
+
type: "class",
|
|
2311
|
+
isObject: false,
|
|
2312
|
+
isCaseClass: isCase,
|
|
2313
|
+
isSealed,
|
|
2314
|
+
isAbstract,
|
|
2315
|
+
isFinal,
|
|
2316
|
+
isImplicit,
|
|
2317
|
+
constructorParams: constructorParams.length > 0 ? constructorParams : void 0
|
|
2318
|
+
});
|
|
2319
|
+
symbols.push(symbol);
|
|
2320
|
+
if (isImplicit) patternsObj["Implicit Classes (Extension Methods)"] = true;
|
|
2321
|
+
if (this.isPublic(node) && !parent) {
|
|
2322
|
+
exports.push({
|
|
2323
|
+
// `symbol.name`, not the raw `name`: createSymbol strips quoting syntax,
|
|
2324
|
+
// so the array used to publish `` `strip margin` `` where the symbol was
|
|
2325
|
+
// `strip margin` — one public surface with two spellings of every
|
|
2326
|
+
// backticked name in it. Measured on scala/scala (package.scala:40,
|
|
2327
|
+
// `implicit val `strip margin``), and the `exports` RELATIONSHIP derives
|
|
2328
|
+
// from this array, so the two spellings would have met in the graph.
|
|
2329
|
+
name: symbol.name,
|
|
2330
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
2331
|
+
range: symbol.range
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
const typeParams = this.extractScalaTypeParameters(node);
|
|
2335
|
+
if (typeParams.length > 0) {
|
|
2336
|
+
symbol.genericParameters = typeParams;
|
|
2337
|
+
symbol.metadata.typeParameters = typeParams;
|
|
2338
|
+
}
|
|
2339
|
+
const extendsClause = this.findChild(node, "extends_clause");
|
|
2340
|
+
if (extendsClause) {
|
|
2341
|
+
const supertypes = this.extractSupertypes(extendsClause);
|
|
2342
|
+
symbol.metadata.superTypes = supertypes.map((s) => s.name);
|
|
2343
|
+
for (let i = 0; i < supertypes.length; i++) {
|
|
2344
|
+
relationships.push({
|
|
2345
|
+
source: symbol.name,
|
|
2346
|
+
target: supertypes[i].name,
|
|
2347
|
+
// FLEET MIXIN RULING (2026-10-19, Lead): position 0 is the single
|
|
2348
|
+
// supertype `extends`; positions >=1 are MIXIN COMPOSITION and publish
|
|
2349
|
+
// `mixes_in`, the canonical value for `class A with M` / Ruby
|
|
2350
|
+
// `include/extend/prepend` / Dart `with`. They are NOT `implements`
|
|
2351
|
+
// conformance (the implements value is ruled NOT-APPLICABLE for scala;
|
|
2352
|
+
// the false-negative-if-conformance-without-composition falsifier is in
|
|
2353
|
+
// the lane's SCALA-CENSUS.md). This shape is the same for the
|
|
2354
|
+
// object handler below.
|
|
2355
|
+
type: i === 0 ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.MixesIn,
|
|
2356
|
+
// S1.4: explicit structural confidence (1.0) — undefined is
|
|
2357
|
+
// classifier-defaulted today but downstream >= 0.7 filters read
|
|
2358
|
+
// the field directly.
|
|
2359
|
+
confidence: 1,
|
|
2360
|
+
// M59 (#825): the parent's OWN line, not the clause's.
|
|
2361
|
+
line_number: supertypes[i].line,
|
|
2362
|
+
metadata: { isTrait: i > 0 }
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
return symbol;
|
|
2367
|
+
}
|
|
2368
|
+
handleObject(node, symbols, relationships, exports, patternsObj, parent) {
|
|
2369
|
+
const name = this.extractIdentifier(node, "name");
|
|
2370
|
+
if (!name) return null;
|
|
2371
|
+
const mods = this.collectModifiers(node);
|
|
2372
|
+
const isCase = this.isCaseClass(node);
|
|
2373
|
+
const isSealed = mods.has("sealed");
|
|
2374
|
+
const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Module, node, parent, {
|
|
2375
|
+
type: "object",
|
|
2376
|
+
isObject: true,
|
|
2377
|
+
isCaseObject: isCase,
|
|
2378
|
+
isSealed,
|
|
2379
|
+
scalaSpecific: true,
|
|
2380
|
+
isStatic: true
|
|
2381
|
+
});
|
|
2382
|
+
symbol.isStatic = true;
|
|
2383
|
+
symbols.push(symbol);
|
|
2384
|
+
if (this.isPublic(node) && !parent && !exports.some((e) => e.name === symbol.name)) {
|
|
2385
|
+
exports.push({
|
|
2386
|
+
name: symbol.name,
|
|
2387
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
2388
|
+
range: symbol.range
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
const typeParams = this.extractScalaTypeParameters(node);
|
|
2392
|
+
if (typeParams.length > 0) {
|
|
2393
|
+
symbol.genericParameters = typeParams;
|
|
2394
|
+
symbol.metadata.typeParameters = typeParams;
|
|
2395
|
+
}
|
|
2396
|
+
const extendsClause = this.findChild(node, "extends_clause");
|
|
2397
|
+
if (extendsClause) {
|
|
2398
|
+
const supertypes = this.extractSupertypes(extendsClause);
|
|
2399
|
+
symbol.metadata.superTypes = supertypes.map((s) => s.name);
|
|
2400
|
+
for (let i = 0; i < supertypes.length; i++) {
|
|
2401
|
+
relationships.push({
|
|
2402
|
+
source: symbol.name,
|
|
2403
|
+
target: supertypes[i].name,
|
|
2404
|
+
// FLEET MIXIN RULING (2026-10-19, Lead): position 0 is the single
|
|
2405
|
+
// supertype `extends`; positions >=1 are MIXIN COMPOSITION and publish
|
|
2406
|
+
// `mixes_in`, the canonical value for `class A with M` / Ruby
|
|
2407
|
+
// `include/extend/prepend` / Dart `with`. They are NOT `implements`
|
|
2408
|
+
// conformance (the implements value is ruled NOT-APPLICABLE for scala;
|
|
2409
|
+
// the false-negative-if-conformance-without-composition falsifier is in
|
|
2410
|
+
// the lane's SCALA-CENSUS.md). This shape is the same for the
|
|
2411
|
+
// object handler below.
|
|
2412
|
+
type: i === 0 ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.MixesIn,
|
|
2413
|
+
confidence: 1,
|
|
2414
|
+
line_number: extendsClause.startPosition.row + 1,
|
|
2415
|
+
metadata: { isTrait: i > 0 }
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
return symbol;
|
|
2420
|
+
}
|
|
2421
|
+
handleTrait(node, symbols, relationships, exports, patternsObj, parent) {
|
|
2422
|
+
const name = this.extractIdentifier(node, "name");
|
|
2423
|
+
if (!name) return null;
|
|
2424
|
+
const mods = this.collectModifiers(node);
|
|
2425
|
+
const isSealed = mods.has("sealed");
|
|
2426
|
+
if (isSealed) patternsObj["Sealed Types"] = true;
|
|
2427
|
+
const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Interface, node, parent, {
|
|
2428
|
+
type: "trait",
|
|
2429
|
+
isTrait: true,
|
|
2430
|
+
scalaSpecific: true,
|
|
2431
|
+
isSealed
|
|
2432
|
+
});
|
|
2433
|
+
symbols.push(symbol);
|
|
2434
|
+
if (this.isPublic(node) && !parent) {
|
|
2435
|
+
exports.push({
|
|
2436
|
+
name: symbol.name,
|
|
2437
|
+
kind: ASTEngine.SymbolKind.Interface,
|
|
2438
|
+
range: symbol.range
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
const typeParams = this.extractScalaTypeParameters(node);
|
|
2442
|
+
if (typeParams.length > 0) {
|
|
2443
|
+
symbol.genericParameters = typeParams;
|
|
2444
|
+
symbol.metadata.typeParameters = typeParams;
|
|
2445
|
+
}
|
|
2446
|
+
const extendsClause = this.findChild(node, "extends_clause");
|
|
2447
|
+
if (extendsClause) {
|
|
2448
|
+
const supertypes = this.extractSupertypes(extendsClause);
|
|
2449
|
+
symbol.metadata.superTypes = supertypes.map((s) => s.name);
|
|
2450
|
+
for (let i = 0; i < supertypes.length; i++) {
|
|
2451
|
+
relationships.push({
|
|
2452
|
+
source: symbol.name,
|
|
2453
|
+
target: supertypes[i].name,
|
|
2454
|
+
// FLEET MIXIN RULING (2026-10-19): a trait's own supertype list is
|
|
2455
|
+
// the same composition construct — position 0 stays `extends`, every
|
|
2456
|
+
// further trait in the list is `mixes_in` (it is not interface
|
|
2457
|
+
// conformance; implements is ruled NOT-APPLICABLE for scala).
|
|
2458
|
+
type: i === 0 ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.MixesIn,
|
|
2459
|
+
confidence: 1,
|
|
2460
|
+
// M59 (#825): the parent's OWN line, not the clause's.
|
|
2461
|
+
line_number: supertypes[i].line,
|
|
2462
|
+
metadata: { isTrait: i > 0 }
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return symbol;
|
|
2467
|
+
}
|
|
2468
|
+
handleEnum(node, symbols, relationships, exports, patternsObj, parent) {
|
|
2469
|
+
const name = this.extractIdentifier(node, "name");
|
|
2470
|
+
if (!name) return null;
|
|
2471
|
+
const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Enum, node, parent, { type: "enum", scala3: true });
|
|
2472
|
+
symbols.push(symbol);
|
|
2473
|
+
patternsObj["Scala 3 Enums"] = true;
|
|
2474
|
+
if (this.isPublic(node) && !parent) {
|
|
2475
|
+
exports.push({ name: symbol.name, kind: ASTEngine.SymbolKind.Enum, range: symbol.range });
|
|
2476
|
+
}
|
|
2477
|
+
const enumExtends = this.findChild(node, "extends_clause");
|
|
2478
|
+
if (enumExtends) {
|
|
2479
|
+
const supertypes = this.extractSupertypes(enumExtends);
|
|
2480
|
+
symbol.metadata.superTypes = supertypes.map((s) => s.name);
|
|
2481
|
+
for (let i = 0; i < supertypes.length; i++) {
|
|
2482
|
+
const st = supertypes[i];
|
|
2483
|
+
relationships.push({
|
|
2484
|
+
source: name,
|
|
2485
|
+
target: st.name,
|
|
2486
|
+
// FLEET MIXIN RULING (2026-10-19): same extends_clause composition
|
|
2487
|
+
// construct — first supertype extends, further ones mixes_in.
|
|
2488
|
+
type: i === 0 ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.MixesIn,
|
|
2489
|
+
confidence: 1,
|
|
2490
|
+
// M59 (#825): the parent's OWN line, not the clause's.
|
|
2491
|
+
line_number: st.line,
|
|
2492
|
+
metadata: { isTrait: true }
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
const enumParamsNode = this.findChild(node, "class_parameters");
|
|
2497
|
+
if (enumParamsNode) {
|
|
2498
|
+
const ownerName = stripScalaQuoting(name);
|
|
2499
|
+
const idStub = { id: ownerName, name: ownerName };
|
|
2500
|
+
const ctor = this.createSymbol("constructor", ASTEngine.SymbolKind.Constructor, enumParamsNode, idStub, {
|
|
2501
|
+
type: "constructor",
|
|
2502
|
+
ownerName,
|
|
2503
|
+
ownerKind: ASTEngine.SymbolKind.Enum,
|
|
2504
|
+
scalaSpecific: true
|
|
2505
|
+
});
|
|
2506
|
+
ctor.ownerName = ownerName;
|
|
2507
|
+
const ctorParams = [];
|
|
2508
|
+
symbols.push(ctor);
|
|
2509
|
+
for (let i = 0; i < enumParamsNode.namedChildCount; i++) {
|
|
2510
|
+
const cp = enumParamsNode.namedChild(i);
|
|
2511
|
+
if (cp?.type !== "class_parameter") continue;
|
|
2512
|
+
const pName = cp.childForFieldName("name")?.text;
|
|
2513
|
+
if (!pName) continue;
|
|
2514
|
+
const paramType = cp.childForFieldName("type")?.text;
|
|
2515
|
+
if (paramType) ctorParams.push({ name: pName, type: paramType, isImplicit: false });
|
|
2516
|
+
const pMods = this.collectModifiers(cp);
|
|
2517
|
+
const isVal = pMods.has("val");
|
|
2518
|
+
const isVar = pMods.has("var");
|
|
2519
|
+
if (isVal || isVar) {
|
|
2520
|
+
const prop = this.createSymbol(pName, ASTEngine.SymbolKind.Property, cp, idStub, {
|
|
2521
|
+
type: isVar ? "var" : "val",
|
|
2522
|
+
isConstructorProperty: true,
|
|
2523
|
+
ownerName,
|
|
2524
|
+
ownerKind: ASTEngine.SymbolKind.Enum
|
|
2525
|
+
});
|
|
2526
|
+
prop.ownerName = ownerName;
|
|
2527
|
+
symbols.push(prop);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
if (ctorParams.length > 0) ctor.parameters = ctorParams;
|
|
2531
|
+
}
|
|
2532
|
+
const walkEnumBody = (n) => {
|
|
2533
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2534
|
+
const c = n.namedChild(i);
|
|
2535
|
+
if (!c) continue;
|
|
2536
|
+
if (c.type === "simple_enum_case" || c.type === "full_enum_case") {
|
|
2537
|
+
const caseName = this.findChildIdentifier(c);
|
|
2538
|
+
if (caseName) {
|
|
2539
|
+
const member = this.createSymbol(caseName, ASTEngine.SymbolKind.EnumMember, c, symbol, {
|
|
2540
|
+
type: "enum_case",
|
|
2541
|
+
// stripped, like every other ownerName (see handleClass): a case
|
|
2542
|
+
// declared ``case `type` `` must not source its `contains` edge
|
|
2543
|
+
// from a spelling the symbol index does not hold.
|
|
2544
|
+
ownerName: stripScalaQuoting(name),
|
|
2545
|
+
ownerKind: ASTEngine.SymbolKind.Enum,
|
|
2546
|
+
scala3: true
|
|
2547
|
+
});
|
|
2548
|
+
member.ownerName = stripScalaQuoting(name);
|
|
2549
|
+
symbols.push(member);
|
|
2550
|
+
}
|
|
2551
|
+
} else {
|
|
2552
|
+
walkEnumBody(c);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
};
|
|
2556
|
+
const enumBody = this.findChild(node, "enum_body");
|
|
2557
|
+
if (enumBody) walkEnumBody(enumBody);
|
|
2558
|
+
return symbol;
|
|
2559
|
+
}
|
|
2560
|
+
handleTypeAlias(node, symbols, parent, exports) {
|
|
2561
|
+
const name = this.extractIdentifier(node, "name");
|
|
2562
|
+
if (!name) return;
|
|
2563
|
+
const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Type, node, parent, { isTypeAlias: true });
|
|
2564
|
+
symbols.push(symbol);
|
|
2565
|
+
if (this.isPublic(node) && !parent) exports.push({ name, kind: ASTEngine.SymbolKind.Type, range: symbol.range });
|
|
2566
|
+
}
|
|
2567
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
2568
|
+
// Scala 3 EXPORT CLAUSE -> `exports` relationships.
|
|
2569
|
+
//
|
|
2570
|
+
// The lane handover recorded `exports` as silent and the census measured the
|
|
2571
|
+
// construct present: `export_declaration` — 277 nodes in 158 files of the
|
|
2572
|
+
// 18,965-file population (script/m14/tmp/scala3-ontology-census.ts), e.g.
|
|
2573
|
+
// compiler/src/dotty/tools/dotc/core/Flags.scala, library/src/scala/
|
|
2574
|
+
// NamedTuple.scala. So this is a GAP, not an absence, and a prose ruling
|
|
2575
|
+
// ("Scala has no export construct") would have been wrong on 277 measured
|
|
2576
|
+
// nodes — the ontology gate's founding premise.
|
|
2577
|
+
//
|
|
2578
|
+
// THE RECONSTRUCTION, and why child position cannot do it. The natural first
|
|
2579
|
+
// rule — "everything before the last named child is the qualifier, the last
|
|
2580
|
+
// named child is the selection" — is wrong on real corpus shapes, measured in
|
|
2581
|
+
// script/m14/tmp/scala3-export-reconstruct-census.mts:
|
|
2582
|
+
//
|
|
2583
|
+
// * `export toppingPrice.apply, crustPrice.apply, crustPrice.unlift`
|
|
2584
|
+
// (tests/neg/i20511-1.scala) parses with SIX `field: path` children and
|
|
2585
|
+
// NO selection node at all. The positional rule reads the qualifier as
|
|
2586
|
+
// "toppingPrice.apply.crustPrice.apply.crustPrice" — three unrelated
|
|
2587
|
+
// exportees fused into one nonsense path. The FIELD is what separates
|
|
2588
|
+
// them: a `,` terminates the exportee and the path children after it
|
|
2589
|
+
// begin the next one.
|
|
2590
|
+
// * the anonymous `.` between path segments does NOT consistently carry
|
|
2591
|
+
// `field: path` (it does between two path segments, it does NOT before a
|
|
2592
|
+
// selection node), so it has to be skipped by node type, not by field.
|
|
2593
|
+
//
|
|
2594
|
+
// With that, the rule reconstructs 277 of 277 declarations and produces 280
|
|
2595
|
+
// exportees. The first version of the rule reconstructed 90 of 277; the
|
|
2596
|
+
// 187 it missed were all wildcards and selector lists, which is why the
|
|
2597
|
+
// coverage number is printed rather than asserted.
|
|
2598
|
+
//
|
|
2599
|
+
// WHAT AN EDGE IS. A Scala 3 export clause re-publishes members of another
|
|
2600
|
+
// object under the enclosing declaration's own name. So the edge reads
|
|
2601
|
+
// <enclosing declaration> exports <exported name>
|
|
2602
|
+
// which is the same shape makefile/python/zig emit (module -> name) and the
|
|
2603
|
+
// shape parallel-indexer's file-level `exports` section expects. The
|
|
2604
|
+
// QUALIFIER is carried in metadata rather than glued into the target: it is
|
|
2605
|
+
// the SOURCE path of the member, not part of the name the export publishes,
|
|
2606
|
+
// and gluing it would produce targets no symbol in the graph is named by.
|
|
2607
|
+
//
|
|
2608
|
+
// CONFIDENCE. 1.0 on a NAMED exportee: the name is written in the clause, so
|
|
2609
|
+
// there is nothing inferred. A wildcard exportee (`export X._`, 133 of the
|
|
2610
|
+
// 277 declarations) names no member, so an edge naming one would be
|
|
2611
|
+
// fabricated; it is published at 0.7 with `reasonCode: "export-wildcard"` and
|
|
2612
|
+
// an `isWildcard` flag, which makes it countable as a heuristic per
|
|
2613
|
+
// M14-HONESTY-CONTRACT §4 rather than silently indistinguishable from the
|
|
2614
|
+
// named ones.
|
|
2615
|
+
//
|
|
2616
|
+
// ENDPOINT RESOLUTION. The source is the enclosing declaration's name, which
|
|
2617
|
+
// is a symbol the same walk publishes, so it resolves. The target is a member
|
|
2618
|
+
// name that is usually NOT a symbol in this file (it lives on the qualifier's
|
|
2619
|
+
// object), so it resolves externally or not at all — which is the same
|
|
2620
|
+
// position every `references` import edge is already in, and is why the
|
|
2621
|
+
// qualifier goes in metadata where a consumer can still find it.
|
|
2622
|
+
handleExportClause(node, relationships, parent) {
|
|
2623
|
+
const SELECTION_TYPES = /* @__PURE__ */ new Set(["namespace_selectors", "namespace_wildcard", "as_renamed_identifier"]);
|
|
2624
|
+
const exportees = [];
|
|
2625
|
+
let pathParts = [];
|
|
2626
|
+
let pathNodes = [];
|
|
2627
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2628
|
+
const c = node.child(i);
|
|
2629
|
+
if (!c) return;
|
|
2630
|
+
const t = c.type;
|
|
2631
|
+
const field = node.fieldNameForChild(i) ?? "";
|
|
2632
|
+
if (t === "export") continue;
|
|
2633
|
+
if (t === ",") {
|
|
2634
|
+
if (pathParts.length === 0) continue;
|
|
2635
|
+
const leaf = pathParts.pop();
|
|
2636
|
+
const leafNode = pathNodes.pop() ?? c;
|
|
2637
|
+
exportees.push({ prefix: pathParts, selKind: "identifier", src: leafNode, names: [leaf] });
|
|
2638
|
+
pathParts = [];
|
|
2639
|
+
pathNodes = [];
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
if (t === ".") continue;
|
|
2643
|
+
if (c.isNamed && t === "block_comment") continue;
|
|
2644
|
+
if (c.isNamed && SELECTION_TYPES.has(t)) {
|
|
2645
|
+
if (field !== "") continue;
|
|
2646
|
+
if (t === "namespace_wildcard") {
|
|
2647
|
+
exportees.push({ prefix: pathParts.slice(), selKind: t, src: c, names: [] });
|
|
2648
|
+
} else if (t === "namespace_selectors") {
|
|
2649
|
+
const picked = [];
|
|
2650
|
+
let innerWildcard = false;
|
|
2651
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
2652
|
+
const s = c.namedChild(j);
|
|
2653
|
+
if (!s || s.isNamed && s.type === "block_comment") continue;
|
|
2654
|
+
if (s.type === "namespace_wildcard") {
|
|
2655
|
+
innerWildcard = true;
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
const nm = this.exportSelectorName(s);
|
|
2659
|
+
if (nm === null) continue;
|
|
2660
|
+
const alias = this.exportSelectorAlias(s);
|
|
2661
|
+
picked.push({ name: nm, node: s, ...alias && alias !== "*" ? { exportedAs: alias } : {} });
|
|
2662
|
+
}
|
|
2663
|
+
for (const pk of picked) {
|
|
2664
|
+
exportees.push({ prefix: pathParts.slice(), selKind: t, src: pk.node, names: [pk.name], ...pk.exportedAs ? { exportedAs: pk.exportedAs } : {} });
|
|
2665
|
+
}
|
|
2666
|
+
if (innerWildcard && pathParts.length > 0) {
|
|
2667
|
+
exportees.push({ prefix: pathParts.slice(), selKind: "namespace_wildcard", src: c, names: [] });
|
|
2668
|
+
}
|
|
2669
|
+
} else {
|
|
2670
|
+
const srcNode = c.namedChild(0);
|
|
2671
|
+
const srcName = srcNode ? this.exportSelectorName(srcNode) : null;
|
|
2672
|
+
if (srcName !== null) {
|
|
2673
|
+
const alias = this.exportSelectorAlias(c);
|
|
2674
|
+
exportees.push({ prefix: pathParts.slice(), selKind: t, src: srcNode ?? c, names: [srcName], ...alias && alias !== "*" ? { exportedAs: alias } : {} });
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
pathParts = [];
|
|
2678
|
+
pathNodes = [];
|
|
2679
|
+
continue;
|
|
2680
|
+
}
|
|
2681
|
+
if (field === "path") {
|
|
2682
|
+
if (c.isNamed) {
|
|
2683
|
+
pathParts.push(c.text);
|
|
2684
|
+
pathNodes.push(c);
|
|
2685
|
+
}
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
if (pathParts.length > 0) {
|
|
2691
|
+
const leaf = pathParts.pop();
|
|
2692
|
+
const leafNode = pathNodes.pop() ?? node;
|
|
2693
|
+
exportees.push({ prefix: pathParts, selKind: "identifier", src: leafNode, names: [leaf] });
|
|
2694
|
+
}
|
|
2695
|
+
if (exportees.length === 0) return;
|
|
2696
|
+
const source = parent?.name ?? _RealScalaAnalyzer.FILE_MODULE;
|
|
2697
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2698
|
+
for (const e of exportees) {
|
|
2699
|
+
const qualifier = e.prefix.join(".");
|
|
2700
|
+
if (e.names.length === 0) {
|
|
2701
|
+
if (qualifier.length === 0) continue;
|
|
2702
|
+
const key = `${source}|${qualifier}|*`;
|
|
2703
|
+
if (seen.has(key)) continue;
|
|
2704
|
+
seen.add(key);
|
|
2705
|
+
relationships.push({
|
|
2706
|
+
source,
|
|
2707
|
+
target: qualifier,
|
|
2708
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
2709
|
+
confidence: 0.7,
|
|
2710
|
+
line_number: e.src.startPosition.row + 1,
|
|
2711
|
+
metadata: { isExportClause: true, isWildcard: true, reasonCode: "export-wildcard", exportKind: "all_members", evidenceClass: "structural" }
|
|
2712
|
+
});
|
|
2713
|
+
continue;
|
|
2714
|
+
}
|
|
2715
|
+
for (const nm of e.names) {
|
|
2716
|
+
const published = e.exportedAs ?? nm;
|
|
2717
|
+
const target = stripScalaQuoting(published);
|
|
2718
|
+
if (target.length === 0 || target === "throws") continue;
|
|
2719
|
+
if (target === "_" || target === "*" || target === "unknown" || target === "?") continue;
|
|
2720
|
+
const key = `${source}|${target}`;
|
|
2721
|
+
if (seen.has(key)) continue;
|
|
2722
|
+
seen.add(key);
|
|
2723
|
+
relationships.push({
|
|
2724
|
+
source,
|
|
2725
|
+
target,
|
|
2726
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
2727
|
+
confidence: 1,
|
|
2728
|
+
// M59 (#825): the exported name is written in the clause, so the line
|
|
2729
|
+
// is where that NAME is written — the selector's own line inside a
|
|
2730
|
+
// selector list, the leaf segment's own line for `export a.b`, and
|
|
2731
|
+
// never the declaration's first line.
|
|
2732
|
+
line_number: e.src.startPosition.row + 1,
|
|
2733
|
+
metadata: {
|
|
2734
|
+
isExportClause: true,
|
|
2735
|
+
exportKind: e.exportedAs ? "renamed" : "named",
|
|
2736
|
+
...qualifier.length > 0 ? { exportFrom: qualifier } : {},
|
|
2737
|
+
...e.exportedAs ? { exportedAs: stripScalaQuoting(e.exportedAs) } : {},
|
|
2738
|
+
evidenceClass: "structural"
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
/** The RENAMING alias one selector publishes, or null when it does not
|
|
2745
|
+
* rename. `x as y` / `x => y` publish `y`; `x as _` / `x => _` publish `x`,
|
|
2746
|
+
* which is why `_` and `*` are never returned as an alias. */
|
|
2747
|
+
exportSelectorAlias(sel) {
|
|
2748
|
+
const t = sel.type;
|
|
2749
|
+
if (t !== "as_renamed_identifier" && t !== "arrow_renamed_identifier") return null;
|
|
2750
|
+
if (sel.namedChildCount < 2) return null;
|
|
2751
|
+
const last = sel.namedChild(sel.namedChildCount - 1);
|
|
2752
|
+
if (!last) return null;
|
|
2753
|
+
const txt = last.text;
|
|
2754
|
+
if (txt === "_" || txt === "*") return null;
|
|
2755
|
+
return this.exportSelectorName(last);
|
|
2756
|
+
}
|
|
2757
|
+
/** The member name ONE selector contributes, or null when it contributes
|
|
2758
|
+
* none. `as`/`=>` forms contribute their SOURCE name; the alias is read
|
|
2759
|
+
* separately so the caller can record the rename. */
|
|
2760
|
+
exportSelectorName(sel) {
|
|
2761
|
+
const t = sel.type;
|
|
2762
|
+
if (t === "identifier" || t === "operator_identifier" || t === "type_identifier") return sel.text;
|
|
2763
|
+
if (t === "generic_type") {
|
|
2764
|
+
const head = sel.namedChild(0);
|
|
2765
|
+
return head ? this.exportSelectorName(head) : null;
|
|
2766
|
+
}
|
|
2767
|
+
if (t === "as_renamed_identifier" || t === "arrow_renamed_identifier") {
|
|
2768
|
+
const src = sel.namedChild(0);
|
|
2769
|
+
return src ? this.exportSelectorName(src) : null;
|
|
2770
|
+
}
|
|
2771
|
+
if (t === "namespace_wildcard") return "*";
|
|
2772
|
+
return null;
|
|
2773
|
+
}
|
|
2774
|
+
handleGiven(node, symbols, relationships, exports, patternsObj, parent) {
|
|
2775
|
+
patternsObj["Given/Using (Scala 3)"] = true;
|
|
2776
|
+
patternsObj["Contextual Abstractions"] = true;
|
|
2777
|
+
const explicitName = node.childForFieldName("name")?.text ?? null;
|
|
2778
|
+
if (!explicitName) this.givenCounter += 1;
|
|
2779
|
+
const name = explicitName || `given_${this.givenCounter}`;
|
|
2780
|
+
const symbol = this.createSymbol(name, ASTEngine.SymbolKind.Constant, node, parent, {
|
|
2781
|
+
type: "given",
|
|
2782
|
+
isGiven: true,
|
|
2783
|
+
scala3: true,
|
|
2784
|
+
anonymous: !explicitName
|
|
2785
|
+
});
|
|
2786
|
+
symbols.push(symbol);
|
|
2787
|
+
if (explicitName && this.isPublic(node) && !parent) {
|
|
2788
|
+
exports.push({ name: symbol.name, kind: ASTEngine.SymbolKind.Constant, range: symbol.range });
|
|
2789
|
+
}
|
|
2790
|
+
return symbol;
|
|
2791
|
+
}
|
|
2792
|
+
handleFunction(node, symbols, relationships, exports, patternsObj, parent) {
|
|
2793
|
+
const name = this.extractIdentifier(node, "name");
|
|
2794
|
+
let extensionReceiverType;
|
|
2795
|
+
let extensionReceiverTypeNode = null;
|
|
2796
|
+
{
|
|
2797
|
+
let anc = node.parent;
|
|
2798
|
+
while (anc) {
|
|
2799
|
+
if (anc.type === "extension_definition") {
|
|
2800
|
+
for (let i = 0; i < anc.namedChildCount; i++) {
|
|
2801
|
+
const pc = anc.namedChild(i);
|
|
2802
|
+
if (!pc || pc.type !== "parameters") continue;
|
|
2803
|
+
if (pc.namedChildCount > 0 && pc.namedChild(0)) {
|
|
2804
|
+
const p0 = pc.namedChild(0);
|
|
2805
|
+
for (let j = 0; j < p0.namedChildCount; j++) {
|
|
2806
|
+
const tc = p0.namedChild(j);
|
|
2807
|
+
if (tc && this.isExtensionReceiverTypeNode(tc.type)) {
|
|
2808
|
+
const t = tc.text;
|
|
2809
|
+
extensionReceiverType = t.endsWith("?") ? t.slice(0, -1) : t;
|
|
2810
|
+
extensionReceiverTypeNode = tc;
|
|
2811
|
+
break;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
break;
|
|
2816
|
+
}
|
|
2817
|
+
break;
|
|
2818
|
+
}
|
|
2819
|
+
if (anc.type === "class_definition" || anc.type === "trait_definition" || anc.type === "object_definition") break;
|
|
2820
|
+
anc = anc.parent;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
if (!name) return null;
|
|
2824
|
+
if (name === "this" && parent && (parent.kind === ASTEngine.SymbolKind.Class || parent.kind === ASTEngine.SymbolKind.Module)) {
|
|
2825
|
+
let ctor;
|
|
2826
|
+
for (const s of symbols) {
|
|
2827
|
+
if (s.ownerName === parent.name && s.kind === ASTEngine.SymbolKind.Constructor && s.name === "constructor") {
|
|
2828
|
+
ctor = s;
|
|
2829
|
+
break;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
if (ctor) {
|
|
2833
|
+
const existing = new Set((ctor.parameters ?? []).map((p) => `${p.name}:${p.type}`));
|
|
2834
|
+
const ps = ctor.parameters ?? (ctor.parameters = []);
|
|
2835
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2836
|
+
const pn = node.namedChild(i);
|
|
2837
|
+
if (!pn || pn.type !== "parameters") continue;
|
|
2838
|
+
for (let j = 0; j < pn.namedChildCount; j++) {
|
|
2839
|
+
const p = pn.namedChild(j);
|
|
2840
|
+
if (!p || p.type !== "parameter") continue;
|
|
2841
|
+
const pname = p.childForFieldName("name")?.text || "";
|
|
2842
|
+
const ptype = p.childForFieldName("type")?.text || "";
|
|
2843
|
+
const key = `${pname}:${ptype}`;
|
|
2844
|
+
if (existing.has(key)) continue;
|
|
2845
|
+
existing.add(key);
|
|
2846
|
+
ps.push({ name: pname, type: ptype, isImplicit: false });
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
patternsObj["Auxiliary Constructors"] = true;
|
|
2851
|
+
return parent;
|
|
2852
|
+
}
|
|
2853
|
+
const isMethod = parent && (parent.metadata?.type === "class" || parent.metadata?.type === "trait" || parent.metadata?.type === "object" || parent.kind === ASTEngine.SymbolKind.Class || parent.kind === ASTEngine.SymbolKind.Interface || parent.kind === ASTEngine.SymbolKind.Module);
|
|
2854
|
+
const mods = this.collectModifiers(node);
|
|
2855
|
+
const isAbstract = node.type === "function_declaration" || mods.has("abstract");
|
|
2856
|
+
const isOverride = mods.has("override");
|
|
2857
|
+
const isImplicit = mods.has("implicit");
|
|
2858
|
+
const isInline = mods.has("inline_modifier") || mods.has("transparent_modifier");
|
|
2859
|
+
const parameters = [];
|
|
2860
|
+
let returnType = void 0;
|
|
2861
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2862
|
+
const child = node.child(i);
|
|
2863
|
+
if (!child) continue;
|
|
2864
|
+
if (child.type === "parameters") {
|
|
2865
|
+
let hasImplicit = false;
|
|
2866
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
2867
|
+
const paramChild = child.child(j);
|
|
2868
|
+
if (paramChild && (paramChild.type === "implicit" || paramChild.type === "using")) {
|
|
2869
|
+
hasImplicit = true;
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
2874
|
+
const paramNode = child.namedChild(j);
|
|
2875
|
+
if (!paramNode) continue;
|
|
2876
|
+
if (paramNode.type === "parameter") {
|
|
2877
|
+
const paramName = paramNode.childForFieldName("name")?.text || "";
|
|
2878
|
+
const paramType = paramNode.childForFieldName("type")?.text || "";
|
|
2879
|
+
parameters.push({
|
|
2880
|
+
name: paramName,
|
|
2881
|
+
type: paramType,
|
|
2882
|
+
isImplicit: hasImplicit
|
|
2883
|
+
});
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
const isTypeNode = paramNode.type.endsWith("_type") || paramNode.type.includes("type_identifier");
|
|
2887
|
+
if (isTypeNode) {
|
|
2888
|
+
parameters.push({ name: "", type: String(paramNode.text), isImplicit: true });
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
const returnTypeNode = node.childForFieldName("return_type") || node.childForFieldName("type");
|
|
2894
|
+
if (returnTypeNode) {
|
|
2895
|
+
returnType = returnTypeNode.text;
|
|
2896
|
+
}
|
|
2897
|
+
const isEnumMethod = parent?.kind === ASTEngine.SymbolKind.Enum || parent?.metadata?.type === "enum";
|
|
2898
|
+
const methodKind = isMethod || isEnumMethod ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function;
|
|
2899
|
+
const symbol = this.createSymbol(
|
|
2900
|
+
name,
|
|
2901
|
+
methodKind,
|
|
2902
|
+
node,
|
|
2903
|
+
parent,
|
|
2904
|
+
{
|
|
2905
|
+
type: "function",
|
|
2906
|
+
parameters: parameters.length > 0 ? parameters : void 0,
|
|
2907
|
+
returnType,
|
|
2908
|
+
isAbstract,
|
|
2909
|
+
isOverride,
|
|
2910
|
+
isImplicit,
|
|
2911
|
+
isInline,
|
|
2912
|
+
// Scala 3 extension methods: the receiver lives on the enclosing
|
|
2913
|
+
// extension_definition's parameters, NOT on the function node, and
|
|
2914
|
+
// the wrapper never becomes a parent symbol. Detect via ancestors.
|
|
2915
|
+
isExtension: !!extensionReceiverType,
|
|
2916
|
+
// M60/SDJ-9: the receiver is SEMANTIC — a Scala 3 extension method is
|
|
2917
|
+
// in scope only for its receiver type — and it was recorded NOWHERE.
|
|
2918
|
+
// Two `isPackage` methods, one on AbstractFile and one on JFile,
|
|
2919
|
+
// published byte-identical symbols: same id (`FileUtils.isPackage`),
|
|
2920
|
+
// same kind, same owner, no field to tell them apart. Storing the
|
|
2921
|
+
// receiver is pure information gain and is what lets the identity pass
|
|
2922
|
+
// below separate them.
|
|
2923
|
+
extensionReceiver: extensionReceiverType,
|
|
2924
|
+
complexity: this.calculateScalaFunctionComplexity(node)
|
|
2925
|
+
}
|
|
2926
|
+
);
|
|
2927
|
+
symbols.push(symbol);
|
|
2928
|
+
const fnTypeParams = this.extractScalaTypeParameters(node);
|
|
2929
|
+
if (fnTypeParams.length > 0) {
|
|
2930
|
+
symbol.genericParameters = fnTypeParams;
|
|
2931
|
+
symbol.metadata.typeParameters = fnTypeParams;
|
|
2932
|
+
}
|
|
2933
|
+
if (extensionReceiverType) {
|
|
2934
|
+
relationships.push({
|
|
2935
|
+
source: name,
|
|
2936
|
+
target: extensionReceiverType,
|
|
2937
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2938
|
+
confidence: 0.9,
|
|
2939
|
+
// M59 (#825): the line belongs to the NAME — the receiver TYPE node,
|
|
2940
|
+
// not the method that uses it. `extension (x: Bla)` on its own line
|
|
2941
|
+
// puts `Bla` there; the method body follows below.
|
|
2942
|
+
line_number: (extensionReceiverTypeNode ?? node).startPosition.row + 1,
|
|
2943
|
+
metadata: { relationshipType: "scala_extension_method", isExtension: true }
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
if (this.isPublic(node) && methodKind === ASTEngine.SymbolKind.Function && !parent) {
|
|
2947
|
+
exports.push({
|
|
2948
|
+
name: symbol.name,
|
|
2949
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
2950
|
+
range: symbol.range
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
if (name === "apply") patternsObj["Factory Pattern (apply)"] = true;
|
|
2954
|
+
if (name === "unapply" || name === "unapplySeq") patternsObj["Extractor Pattern (unapply)"] = true;
|
|
2955
|
+
return symbol;
|
|
2956
|
+
}
|
|
2957
|
+
handleVariable(node, symbols, exports, parent) {
|
|
2958
|
+
let ancestor = node.parent;
|
|
2959
|
+
while (ancestor) {
|
|
2960
|
+
if (ancestor.type === "function_definition" || ancestor.type === "function_declaration" || ancestor.type === "lambda_expression" || ancestor.type === "block" && ancestor.parent?.type === "function_definition") {
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
if (ancestor.type === "class_definition" || ancestor.type === "trait_definition" || ancestor.type === "object_definition" || ancestor.type === "enum_definition" || // M49/M37 #866: `template_body` is the body of an anonymous class
|
|
2964
|
+
// (`new Runnable { val field = target() }`). A val/var written
|
|
2965
|
+
// directly in it is a MEMBER of that type, not a local of the method
|
|
2966
|
+
// the literal happens to sit in — so the boundary check comes first,
|
|
2967
|
+
// exactly like the three named type forms above. Pre-fix the scan
|
|
2968
|
+
// walked past the body, reached the enclosing `def`, and dropped the
|
|
2969
|
+
// member entirely (java/groovy emit `property field
|
|
2970
|
+
// owner=anonymous_Runnable`). The body's PARENT must be the
|
|
2971
|
+
// instance_expression, otherwise this is a named/anonymous class body
|
|
2972
|
+
// handled by the branches above.
|
|
2973
|
+
ancestor.type === "template_body" && ancestor.parent?.type === "instance_expression") {
|
|
2974
|
+
break;
|
|
2975
|
+
}
|
|
2976
|
+
ancestor = ancestor.parent;
|
|
2977
|
+
}
|
|
2978
|
+
let patternNode = node.childForFieldName("pattern");
|
|
2979
|
+
if (!patternNode) {
|
|
2980
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2981
|
+
const child = node.child(i);
|
|
2982
|
+
if (child && child.isNamed && child.type === "identifier") {
|
|
2983
|
+
patternNode = child;
|
|
2984
|
+
break;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
if (!patternNode) return;
|
|
2989
|
+
if (patternNode.type === "tuple_pattern") return;
|
|
2990
|
+
if (patternNode.text === "this") return;
|
|
2991
|
+
if (patternNode.type !== "identifier" && patternNode.type !== "identifiers" && patternNode.type !== "operator_identifier") return;
|
|
2992
|
+
const kind = node.type === "val_definition" || node.type === "val_declaration" ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Variable;
|
|
2993
|
+
const declared = [];
|
|
2994
|
+
if (patternNode.type === "identifiers") {
|
|
2995
|
+
for (let i = 0; i < patternNode.childCount; i++) {
|
|
2996
|
+
const child = patternNode.child(i);
|
|
2997
|
+
if (child && child.isNamed && child.type === "identifier") declared.push(child.text);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
if (declared.length === 0) declared.push(patternNode.text);
|
|
3001
|
+
for (const name of declared) {
|
|
3002
|
+
const symbol = this.createSymbol(name, kind, node, parent, { type: node.type, isAbstract: node.type === "val_declaration" || node.type === "var_declaration" });
|
|
3003
|
+
symbols.push(symbol);
|
|
3004
|
+
if (this.isPublic(node) && !parent) {
|
|
3005
|
+
exports.push({
|
|
3006
|
+
name: symbol.name,
|
|
3007
|
+
kind,
|
|
3008
|
+
range: symbol.range
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
handleInstantiation(node, relationships, parent) {
|
|
3014
|
+
const sourceName = parent?.name ?? "(script)";
|
|
3015
|
+
const typeNode = node.namedChild(0);
|
|
3016
|
+
if (!typeNode) return;
|
|
3017
|
+
let typeName = this.scalaTypeHeadName(typeNode);
|
|
3018
|
+
let prevTypeName;
|
|
3019
|
+
do {
|
|
3020
|
+
prevTypeName = typeName;
|
|
3021
|
+
typeName = this.stripTypeArgs(typeName);
|
|
3022
|
+
} while (typeName !== prevTypeName);
|
|
3023
|
+
if (typeName && typeName !== "unknown") {
|
|
3024
|
+
relationships.push({
|
|
3025
|
+
source: sourceName,
|
|
3026
|
+
target: typeName,
|
|
3027
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
3028
|
+
confidence: 0.9,
|
|
3029
|
+
line_number: typeNode.startPosition.row + 1,
|
|
3030
|
+
metadata: { isInstantiation: true }
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
handleCall(node, relationships, patternsObj, parent) {
|
|
3035
|
+
const fnNode = node.childForFieldName("function");
|
|
3036
|
+
if (fnNode) {
|
|
3037
|
+
if (fnNode.text.startsWith("'") || fnNode.text.startsWith("`")) return;
|
|
3038
|
+
if (parent && fnNode.type !== "call_expression") {
|
|
3039
|
+
if (!parent.qualified_calls) parent.qualified_calls = [];
|
|
3040
|
+
parent.qualified_calls.push(this.buildScalaCallPath(fnNode));
|
|
3041
|
+
}
|
|
3042
|
+
if (fnNode.type === "call_expression") return;
|
|
3043
|
+
const CALLEE_NAME_SHAPES = /* @__PURE__ */ new Set([
|
|
3044
|
+
"identifier",
|
|
3045
|
+
"field_expression",
|
|
3046
|
+
"generic_function",
|
|
3047
|
+
"operator_identifier",
|
|
3048
|
+
"type_identifier",
|
|
3049
|
+
"stable_type_identifier"
|
|
3050
|
+
]);
|
|
3051
|
+
if (!CALLEE_NAME_SHAPES.has(fnNode.type)) return;
|
|
3052
|
+
let calledName = "";
|
|
3053
|
+
let isQualified = false;
|
|
3054
|
+
let calleeNode = null;
|
|
3055
|
+
if (fnNode.type === "generic_function") {
|
|
3056
|
+
const leaves = [];
|
|
3057
|
+
const findLeaves = (nn) => {
|
|
3058
|
+
if (nn.type === "identifier") {
|
|
3059
|
+
leaves.push(nn);
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
if (nn.type === "type_arguments") return;
|
|
3063
|
+
for (let i = 0; i < nn.namedChildCount; i++) {
|
|
3064
|
+
const c = nn.namedChild(i);
|
|
3065
|
+
if (c) findLeaves(c);
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
findLeaves(fnNode);
|
|
3069
|
+
if (leaves.length > 0) {
|
|
3070
|
+
calleeNode = leaves[leaves.length - 1];
|
|
3071
|
+
calledName = calleeNode.text;
|
|
3072
|
+
isQualified = leaves.length > 1;
|
|
3073
|
+
}
|
|
3074
|
+
} else {
|
|
3075
|
+
calledName = fnNode.text;
|
|
3076
|
+
calleeNode = fnNode;
|
|
3077
|
+
isQualified = fnNode.type === "field_expression";
|
|
3078
|
+
if (isQualified) {
|
|
3079
|
+
const leaves = [];
|
|
3080
|
+
for (let i = 0; i < fnNode.namedChildCount; i++) {
|
|
3081
|
+
const c = fnNode.namedChild(i);
|
|
3082
|
+
if (c && (c.type === "identifier" || c.type === "operator_identifier")) leaves.push(c);
|
|
3083
|
+
}
|
|
3084
|
+
if (leaves.length > 0) {
|
|
3085
|
+
calleeNode = leaves[leaves.length - 1];
|
|
3086
|
+
calledName = calleeNode.text;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
const calleeLine = (calleeNode ?? node).startPosition.row + 1;
|
|
3091
|
+
const sourceName = parent?.name ?? "(script)";
|
|
3092
|
+
if (calledName === "this") return;
|
|
3093
|
+
if (calledName) {
|
|
3094
|
+
const first = calledName.charCodeAt(0);
|
|
3095
|
+
const isCapitalized = first >= 65 && first <= 90;
|
|
3096
|
+
let prevCalled;
|
|
3097
|
+
do {
|
|
3098
|
+
prevCalled = calledName;
|
|
3099
|
+
calledName = this.stripBracketGroups(calledName).trim();
|
|
3100
|
+
} while (calledName !== prevCalled);
|
|
3101
|
+
if (isCapitalized && !isQualified) {
|
|
3102
|
+
relationships.push({
|
|
3103
|
+
source: sourceName,
|
|
3104
|
+
target: calledName,
|
|
3105
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
3106
|
+
// S1.4: explicit heuristic confidence — a bare PascalCase call is
|
|
3107
|
+
// a companion-apply GUESS (documented 0.7 contract), never an
|
|
3108
|
+
// implicit-1.0 structural edge.
|
|
3109
|
+
confidence: 0.7,
|
|
3110
|
+
line_number: calleeLine,
|
|
3111
|
+
metadata: { isInstantiation: true }
|
|
3112
|
+
});
|
|
3113
|
+
} else {
|
|
3114
|
+
relationships.push({
|
|
3115
|
+
source: sourceName,
|
|
3116
|
+
target: calledName,
|
|
3117
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
3118
|
+
// S1.4: method calls carry the documented 0.9 confidence (docs
|
|
3119
|
+
// promise 0.9 for calls; the classifier reads it explicitly).
|
|
3120
|
+
confidence: 0.9,
|
|
3121
|
+
line_number: calleeLine,
|
|
3122
|
+
metadata: {}
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
if (["map", "filter", "flatMap", "foreach", "fold", "reduce", "collect"].includes(calledName)) {
|
|
3126
|
+
patternsObj["Higher-Order Functions"] = true;
|
|
3127
|
+
patternsObj["Functional Programming"] = true;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
handleImport(node, imports) {
|
|
3133
|
+
if (node.type !== "import_declaration") return;
|
|
3134
|
+
const parts = [];
|
|
3135
|
+
const selectors = [];
|
|
3136
|
+
const groups = [];
|
|
3137
|
+
let groupParts = [];
|
|
3138
|
+
let moduleLine = node.startPosition.row + 1;
|
|
3139
|
+
const part = (text) => stripScalaQuoting(text);
|
|
3140
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3141
|
+
const child = node.child(i);
|
|
3142
|
+
if (!child) continue;
|
|
3143
|
+
if (child.type === "identifier" || child.type === ".") {
|
|
3144
|
+
if (child.type === "identifier") {
|
|
3145
|
+
parts.push(part(child.text));
|
|
3146
|
+
groupParts.push(part(child.text));
|
|
3147
|
+
moduleLine = child.startPosition.row + 1;
|
|
3148
|
+
}
|
|
3149
|
+
} else if (child.type === ",") {
|
|
3150
|
+
const completed = groupParts.join(".");
|
|
3151
|
+
if (completed.length > 0) groups.push({ prefix: completed, names: [] });
|
|
3152
|
+
groupParts = [];
|
|
3153
|
+
} else if (child.type === "namespace_wildcard") {
|
|
3154
|
+
selectors.push({ local: "*", imported: "*" });
|
|
3155
|
+
const gprefix = groupParts.join(".");
|
|
3156
|
+
if (gprefix.length > 0) groups.push({ prefix: gprefix, names: [] });
|
|
3157
|
+
groupParts = [];
|
|
3158
|
+
} else if (child.type === "namespace_selectors") {
|
|
3159
|
+
const groupPrefix = groupParts.join(".");
|
|
3160
|
+
const groupNames = [];
|
|
3161
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
3162
|
+
const sel = child.child(j);
|
|
3163
|
+
if (!sel) continue;
|
|
3164
|
+
if (sel.type === "identifier") {
|
|
3165
|
+
groupNames.push(part(sel.text));
|
|
3166
|
+
selectors.push({ local: sel.text, imported: sel.text });
|
|
3167
|
+
} else if (sel.type === "arrow_renamed_identifier") {
|
|
3168
|
+
const imported = sel.namedChild(0);
|
|
3169
|
+
const local = sel.namedChild(1);
|
|
3170
|
+
if (imported && local) {
|
|
3171
|
+
const isExclusion = local.type === "wildcard" || local.text === "_";
|
|
3172
|
+
parts.push(part(imported.text));
|
|
3173
|
+
moduleLine = imported.startPosition.row + 1;
|
|
3174
|
+
if (!isExclusion) groupNames.push(part(imported.text));
|
|
3175
|
+
selectors.push({ local: isExclusion ? "_" : local.text, imported: imported.text, excluded: isExclusion });
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
if (groupPrefix.length > 0) groups.push({ prefix: groupPrefix, names: groupNames });
|
|
3180
|
+
groupParts = [];
|
|
3181
|
+
} else if (child.type === "as_renamed_identifier") {
|
|
3182
|
+
const original = child.namedChild(0);
|
|
3183
|
+
const alias = child.namedChild(1);
|
|
3184
|
+
if (original && original.type === "identifier") {
|
|
3185
|
+
parts.push(part(original.text));
|
|
3186
|
+
groupParts.push(part(original.text));
|
|
3187
|
+
moduleLine = original.startPosition.row + 1;
|
|
3188
|
+
selectors.push({ local: alias && alias.type === "identifier" ? alias.text : original.text, imported: original.text });
|
|
3189
|
+
const gprefix = groupParts.join(".");
|
|
3190
|
+
if (gprefix.length > 0) groups.push({ prefix: gprefix, names: [] });
|
|
3191
|
+
groupParts = [];
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
const tailPrefix = groupParts.join(".");
|
|
3196
|
+
if (tailPrefix.length > 0) groups.push({ prefix: tailPrefix, names: [] });
|
|
3197
|
+
const pathStr = parts.join(".");
|
|
3198
|
+
if (selectors.length === 0 && pathStr.length > 0) {
|
|
3199
|
+
const idx = pathStr.lastIndexOf(".");
|
|
3200
|
+
const last = idx >= 0 ? pathStr.substring(idx + 1) : pathStr;
|
|
3201
|
+
if (last.length > 0) selectors.push({ local: last, imported: last });
|
|
3202
|
+
}
|
|
3203
|
+
imports.push({
|
|
3204
|
+
module: pathStr,
|
|
3205
|
+
// M59 (#825): the line the module's last segment is written on (see above).
|
|
3206
|
+
moduleLine,
|
|
3207
|
+
// S1.25 (M49 #715): family consumers read `items` (engine.ts
|
|
3208
|
+
// ImportInfo.items: string[]) — scala was the ONLY family member
|
|
3209
|
+
// populating the legacy `specifiers` shape, so downstream import
|
|
3210
|
+
// listing silently showed nothing for scala. Both shapes are filled:
|
|
3211
|
+
// items = imported names, specifiers kept for any legacy reader.
|
|
3212
|
+
items: selectors.map((s) => s.imported),
|
|
3213
|
+
specifiers: selectors,
|
|
3214
|
+
// Read by the `imports` edge builder; see handleImport's comment on why the
|
|
3215
|
+
// dependency edges and the module path are computed from different bases.
|
|
3216
|
+
...groups.length > 0 ? { metadata: { importGroups: groups } } : {},
|
|
3217
|
+
range: {
|
|
3218
|
+
startLine: node.startPosition.row + 1,
|
|
3219
|
+
startColumn: node.startPosition.column + 1,
|
|
3220
|
+
endLine: node.endPosition.row + 1,
|
|
3221
|
+
endColumn: node.endPosition.column + 1,
|
|
3222
|
+
startIndex: node.startIndex,
|
|
3223
|
+
endIndex: node.endIndex
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* M14 #419 G3/G8: the LEFTMOST identifier of a field_expression — for
|
|
3229
|
+
* a.b.c that is a (recursing through nested field_expressions); package
|
|
3230
|
+
* paths (com.foo.bar) yield com, so capitalized-receiver detection is
|
|
3231
|
+
* safe against package-qualified members.
|
|
3232
|
+
*/
|
|
3233
|
+
leftmostIdentifier(node) {
|
|
3234
|
+
if (node.type === "identifier") return node;
|
|
3235
|
+
const first = node.namedChild(0);
|
|
3236
|
+
return first ? this.leftmostIdentifier(first) : null;
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* The DECLARED NAME of a definition, read from its `name:` FIELD.
|
|
3240
|
+
*
|
|
3241
|
+
* M60/S8: this replaces "first child of type identifier", which is wrong for
|
|
3242
|
+
* an operator-named method. `def <&[B](that: IO[B]): IO[A]` carries its name
|
|
3243
|
+
* as an `operator_identifier`, so the first `identifier` child is the TYPE
|
|
3244
|
+
* PARAMETER `B` — and `resolveSource` then attributed every reference inside
|
|
3245
|
+
* that method to a type parameter named `B`. Two failures in one: the edge's
|
|
3246
|
+
* source is a binding rather than a declaration, and because the edge dedupe
|
|
3247
|
+
* key is (source, target, kind), every operator-named method in a file
|
|
3248
|
+
* collapses onto ONE bogus source, so the edge COUNT drops too.
|
|
3249
|
+
*
|
|
3250
|
+
* Measured on cats-effect/core/shared/src/main/scala/cats/effect/IO.scala,
|
|
3251
|
+
* where the operators `<*`, `<&`, `*>`, `>>`, `!>`, `&>` are the only sources
|
|
3252
|
+
* the derived truth attributes references to and the analyzer does not: 26
|
|
3253
|
+
* rows on the whole cats-effect corpus, and the whole of a 60-file rotation
|
|
3254
|
+
* read clean because the draw did not include this file.
|
|
3255
|
+
*
|
|
3256
|
+
* The lane's comparison matches on (label, target), so it cannot see a wrong
|
|
3257
|
+
* SOURCE at all — which is why this survived the `uses` audit at 100% too:
|
|
3258
|
+
* collectExpressionUses carries the same resolveSource.
|
|
3259
|
+
*/
|
|
3260
|
+
scalaDeclaredNameOf(node) {
|
|
3261
|
+
const f = node.childForFieldName?.("name");
|
|
3262
|
+
if (f) return f.text === "this" ? null : stripScalaQuoting(f.text);
|
|
3263
|
+
const i = this.findChild(node, "identifier")?.text;
|
|
3264
|
+
if (i === void 0) return null;
|
|
3265
|
+
return i === "this" ? null : stripScalaQuoting(i);
|
|
3266
|
+
}
|
|
3267
|
+
findChild(node, type) {
|
|
3268
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3269
|
+
const c = node.child(i);
|
|
3270
|
+
if (c && c.type === type) return c;
|
|
3271
|
+
}
|
|
3272
|
+
return null;
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* The declared name of a node that carries it as a bare `identifier` child —
|
|
3276
|
+
* an enum case (`case Red`, `case B(val x: Int) extends E`). Its `name:` field
|
|
3277
|
+
* would serve too, but the grammar's enum-case shape is the one place a
|
|
3278
|
+
* first-identifier scan and the field agree, and this helper is NOT safe for a
|
|
3279
|
+
* `given`: an anonymous given has no `name:` field and its BODY can be a bare
|
|
3280
|
+
* identifier, so the scan returns the given's VALUE. handleGiven reads the
|
|
3281
|
+
* field for that reason (M60/S6).
|
|
3282
|
+
*/
|
|
3283
|
+
findChildIdentifier(node) {
|
|
3284
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
3285
|
+
const c = node.child(i);
|
|
3286
|
+
if (c && c.isNamed && c.type === "identifier") return c.text;
|
|
3287
|
+
}
|
|
3288
|
+
return null;
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
export {
|
|
3292
|
+
RealScalaAnalyzer
|
|
3293
|
+
};
|