@backbencher/brain 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-KY7K552E.js +17 -0
- package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2RTN2DFU.js +711 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3LFTSMIV.js +174 -0
- package/dist/chunk-47HKX4AJ.js +930 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5SD45FLC.js +818 -0
- package/dist/chunk-66A5ZFYA.js +1758 -0
- package/dist/chunk-6AIRWAO2.js +247 -0
- package/dist/chunk-6WNGD3NN.js +196 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-BHB7BYYI.js +626 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-CW4XXSNN.js +1479 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-FVVQTSIM.js +3427 -0
- package/dist/chunk-FWSL4VG6.js +1290 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HOWAQXJO.js +236 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-IFZNR5UJ.js +2863 -0
- package/dist/chunk-J4PMBXQQ.js +336 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-KLK3PBTO.js +1119 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-PJTNGD3Z.js +6924 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RH7KC5HU.js +12050 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UFE77L67.js +782 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-V44PGPA6.js +2104 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-VURYFIHW.js +201 -0
- package/dist/chunk-XJKJYIPP.js +7436 -0
- package/dist/chunk-YI6FF2ZR.js +340 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-642F7E5V.js +593 -0
- package/dist/database-TCTZDLE5.js +9 -0
- package/dist/doctor-GXCKSJOX.js +395 -0
- package/dist/edit-engine-KM7MPFEC.js +51 -0
- package/dist/engine-42TZFNIZ.js +11 -0
- package/dist/engine-LO6L4JLQ.js +18 -0
- package/dist/fingerprint-XDXFRWO6.js +33 -0
- package/dist/framework-intelligence-DWFCKV44.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-PL5YA7BA.js +21 -0
- package/dist/graph_request-MM4REP6H.js +350 -0
- package/dist/impact_request-WDE47IZ5.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-6BD7UBHR.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-4PW3AFXP.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-ZS5TS2Z6.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-DGZBIZS7.js +355 -0
- package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
- package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
- package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
- package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
- package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
- package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
- package/dist/real-css-analyzer-HPU277Q6.js +15 -0
- package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
- package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
- package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
- package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
- package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
- package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
- package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
- package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
- package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
- package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
- package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
- package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
- package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
- package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
- package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
- package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
- package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
- package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
- package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
- package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
- package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
- package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
- package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
- package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
- package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
- package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
- package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
- package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
- package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
- package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
- package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
- package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
- package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
- package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
- package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
- package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
- package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
- package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
- package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
- package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
- package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
- package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
- package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
- package/dist/regex-search-OPTLKUN5.js +239 -0
- package/dist/rules-I453JTRC.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-Y5GB5KXA.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-KB4MU4SI.js +16 -0
- package/dist/taint-YOTN5X55.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-G4NAGJIH.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-RF675A3C.js +32 -0
- package/dist/type-inference-4Y7WTMFE.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-LXGNFO37.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,3956 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findDeclaringParent
|
|
3
|
+
} from "./chunk-OWISBAOF.js";
|
|
4
|
+
import {
|
|
5
|
+
ASTEngine
|
|
6
|
+
} from "./chunk-V44PGPA6.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-kotlin-analyzer.ts
|
|
15
|
+
function stringIncludes(str, val) {
|
|
16
|
+
if (!str) return false;
|
|
17
|
+
return str.indexOf(val) !== -1;
|
|
18
|
+
}
|
|
19
|
+
function someSymbol(arr, predicate) {
|
|
20
|
+
if (!arr) return false;
|
|
21
|
+
for (let i = 0; i < arr.length; i++) {
|
|
22
|
+
if (predicate(arr[i])) return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function stripKotlinQuoting(text) {
|
|
27
|
+
if (!text.includes("`")) return text;
|
|
28
|
+
let out = "";
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < text.length) {
|
|
31
|
+
if (text.charCodeAt(i) === 96) {
|
|
32
|
+
const close = text.indexOf("`", i + 1);
|
|
33
|
+
if (close === -1) {
|
|
34
|
+
out += text.slice(i);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
out += text.slice(i + 1, close);
|
|
38
|
+
i = close + 1;
|
|
39
|
+
} else {
|
|
40
|
+
out += text[i];
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function kotlinSigHasTypeVar(sig) {
|
|
47
|
+
const n = sig.length;
|
|
48
|
+
let i = 0;
|
|
49
|
+
while (i < n) {
|
|
50
|
+
const c = sig[i];
|
|
51
|
+
if (c === "(" || c === "," || c === ":" || c === "|" || c === "[" || c === "<" || c === "&" || i === 0) {
|
|
52
|
+
let j = i;
|
|
53
|
+
while (j < n && (sig[j] === "(" || sig[j] === "[" || sig[j] === " " || sig[j] === ":" || sig[j] === ",")) j++;
|
|
54
|
+
if (j < n && sig[j] >= "A" && sig[j] <= "Z") {
|
|
55
|
+
let end = j + 1;
|
|
56
|
+
while (end < n && (sig[end] >= "A" && sig[end] <= "Z" || sig[end] >= "a" && sig[end] <= "z" || sig[end] >= "0" && sig[end] <= "9" || sig[end] === "_")) end++;
|
|
57
|
+
if (end === j + 1) return true;
|
|
58
|
+
i = end;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
i = j > i ? j : i + 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function kotlinCompilationUnitName(filePath) {
|
|
69
|
+
if (!filePath) return "";
|
|
70
|
+
let norm = "";
|
|
71
|
+
for (let i = 0; i < filePath.length; i++) {
|
|
72
|
+
const c = filePath[i];
|
|
73
|
+
norm += c === "\\" ? "/" : c;
|
|
74
|
+
}
|
|
75
|
+
const base = norm.slice(norm.lastIndexOf("/") + 1);
|
|
76
|
+
const dot = base.lastIndexOf(".");
|
|
77
|
+
const stem = dot > 0 ? base.slice(0, dot) : base;
|
|
78
|
+
return stem ? stripKotlinQuoting(stem) : "";
|
|
79
|
+
}
|
|
80
|
+
var KOTLIN_EXTERNAL_IMPORT_ROOTS = [
|
|
81
|
+
"java.",
|
|
82
|
+
"javax.",
|
|
83
|
+
"jakarta.",
|
|
84
|
+
"jdk.",
|
|
85
|
+
"sun.",
|
|
86
|
+
"com.sun.",
|
|
87
|
+
"kotlin.",
|
|
88
|
+
"android.",
|
|
89
|
+
"androidx."
|
|
90
|
+
];
|
|
91
|
+
var RealKotlinAnalyzer = class _RealKotlinAnalyzer {
|
|
92
|
+
findChildByType(node, types) {
|
|
93
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
94
|
+
const c = node.namedChild(i);
|
|
95
|
+
if (c) {
|
|
96
|
+
for (let j = 0; j < types.length; j++) {
|
|
97
|
+
if (types[j] === c.type) return c;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Is this candidate TYPE name actually a Kotlin keyword the recovery handed us?
|
|
105
|
+
*
|
|
106
|
+
* MEASURED DEFECT (kotlin corpus, 405-file census). IntelliJ context-parameter
|
|
107
|
+
* test markup breaks a class header so badly that the real name ends up INSIDE
|
|
108
|
+
* an ERROR node while the constructor keyword is serialised as a direct
|
|
109
|
+
* `type_identifier` child of the class_declaration:
|
|
110
|
+
*
|
|
111
|
+
* analysis/low-level-api-fir/testData/lazyResolve/errors/
|
|
112
|
+
* constructorDuplicatedContextParameter.kt
|
|
113
|
+
* `class Foo { context(c<caret>: Int) context(c: String) constructor() }`
|
|
114
|
+
*
|
|
115
|
+
* class_declaration
|
|
116
|
+
* class (anon)
|
|
117
|
+
* ERROR <- contains simple_identifier "Foo"
|
|
118
|
+
* simple_identifier "Foo"
|
|
119
|
+
* ...
|
|
120
|
+
* type_identifier "constructor" <- the KEYWORD, a direct named child
|
|
121
|
+
* primary_constructor "()"
|
|
122
|
+
*
|
|
123
|
+
* A first-name-child scan therefore published a CLASS named `constructor` for a
|
|
124
|
+
* file that declares `Foo`, and the census then found `exports (file) ->
|
|
125
|
+
* constructor` and `contains constructor -> constructor` rows pointing at it.
|
|
126
|
+
*
|
|
127
|
+
* The rejection is TYPE-DECLARATION-ONLY on purpose: `constructor` is a SOFT
|
|
128
|
+
* keyword in Kotlin, and `fun constructor()` is legal — the corpus has one
|
|
129
|
+
* (compiler/psi/psi-impl/testData/psi/secondaryConstructors/basic.kt declares
|
|
130
|
+
* both `constructor() {}` and `@constructor fun constructor() {}` inside class
|
|
131
|
+
* A). Applying this to functions would delete a real declaration. No Kotlin
|
|
132
|
+
* TYPE may be named with any token below, which is what makes the list safe
|
|
133
|
+
* here: they are declaration keywords, and the recovery only ever offers one
|
|
134
|
+
* where a name belongs.
|
|
135
|
+
*/
|
|
136
|
+
static KOTLIN_TYPE_NAME_KEYWORDS = /* @__PURE__ */ new Set([
|
|
137
|
+
"class",
|
|
138
|
+
"interface",
|
|
139
|
+
"object",
|
|
140
|
+
"fun",
|
|
141
|
+
"val",
|
|
142
|
+
"var",
|
|
143
|
+
"constructor",
|
|
144
|
+
"companion",
|
|
145
|
+
"enum",
|
|
146
|
+
"annotation",
|
|
147
|
+
"typealias",
|
|
148
|
+
"init"
|
|
149
|
+
]);
|
|
150
|
+
isKotlinTypeNameKeyword(text) {
|
|
151
|
+
return _RealKotlinAnalyzer.KOTLIN_TYPE_NAME_KEYWORDS.has(text);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The NAME node of a declaration, or null when the declaration has none.
|
|
155
|
+
*
|
|
156
|
+
* `findChildByType` returns the first child whose TYPE matches, and a
|
|
157
|
+
* zero-length node matches. Tree-sitter produces exactly that under
|
|
158
|
+
* ERROR recovery, and Kotlin's corpus carries the recovery in real files, not
|
|
159
|
+
* only in broken ones — measured on the kotlin corpus (script/m14/kotlin-census.ts,
|
|
160
|
+
* stride 40, 405 files admitted), two declarations published an EMPTY name:
|
|
161
|
+
*
|
|
162
|
+
* analysis/analysis-api/testData/.../ambiguouslyInheritedGeneric.kt
|
|
163
|
+
* `abstract class <caret>Child : A<String>, B<CharSequence>`
|
|
164
|
+
* -> source_file child is a class_declaration whose only name-bearing child
|
|
165
|
+
* is a ZERO-LENGTH simple_identifier, so `className` was "".
|
|
166
|
+
* analysis/low-level-api-fir/testData/.../memberPropertyWithTypeInDelegateScript.kts
|
|
167
|
+
* `fun <expr>doSmth</expr>(i: String) = 4`
|
|
168
|
+
* -> same shape, an empty-name function symbol owned by class A.
|
|
169
|
+
*
|
|
170
|
+
* An empty name is not a name. It became a symbol row with `name = ""`, and
|
|
171
|
+
* because the same "" flowed into the derivations that read `ownerName` /
|
|
172
|
+
* `cls.name`, it also became FIVE EDGES THAT NAMED NOBODY: 2 `implements` and
|
|
173
|
+
* 3 `references` rows whose `source` is the empty string. Those rows reach
|
|
174
|
+
* `brain_relationship` with `source_symbol_id = NULL` and `source_external = ''`
|
|
175
|
+
* — an edge that asserts a relationship and identifies no participant.
|
|
176
|
+
*
|
|
177
|
+
* The fix is ABSENCE, not a placeholder (standing rule R7, ledger.json): the
|
|
178
|
+
* declaration is not published, and the file's ParseError already reports the
|
|
179
|
+
* ERROR node's line, so the malformed syntax is disclosed rather than
|
|
180
|
+
* silently renamed. Reading the ERROR node's own text instead would recover
|
|
181
|
+
* the name `Child` — deliberately not done: a name read out of an error
|
|
182
|
+
* recovery is a guess about what the author meant, and publishing it at the
|
|
183
|
+
* confidence of a real declaration is the same defect one node over.
|
|
184
|
+
*
|
|
185
|
+
* The scan is over NAMED children in child order, exactly like
|
|
186
|
+
* findChildByType, and only skips zero-length matches — so a declaration
|
|
187
|
+
* whose name node is present but empty is dropped while every other
|
|
188
|
+
* declaration's resolution is unchanged.
|
|
189
|
+
*/
|
|
190
|
+
static NAME_NODE_TYPES = /* @__PURE__ */ new Set(["simple_identifier", "type_identifier"]);
|
|
191
|
+
findNameNode(node) {
|
|
192
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
193
|
+
const c = node.namedChild(i);
|
|
194
|
+
if (!c) continue;
|
|
195
|
+
if (!_RealKotlinAnalyzer.NAME_NODE_TYPES.has(c.type)) {
|
|
196
|
+
if (c.type === "variable_declaration") {
|
|
197
|
+
const inner = this.findNameNode(c);
|
|
198
|
+
if (inner) return inner;
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (c.text.length === 0) continue;
|
|
203
|
+
return c;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* An endpoint is publishable when it names something. R7 forbids naming a
|
|
209
|
+
* symbol with a placeholder literal, and an empty string is the same failure
|
|
210
|
+
* with the placeholder removed: the row exists and identifies no participant.
|
|
211
|
+
*
|
|
212
|
+
* `unknown` is NOT special-cased here, and the reason is measured rather than
|
|
213
|
+
* stylistic: `unknown` is a legal Kotlin identifier, and the corpus declares
|
|
214
|
+
* one. `localClassWithUnresolvedSuperType.kt` contains
|
|
215
|
+
* `override fun unknown() {}` and calls `Bar().unknown()`, so the `unknown`
|
|
216
|
+
* endpoint on those two edges is a REAL member that really is called. A gate
|
|
217
|
+
* that convicted it would be wrong, and a gate with false positives gets
|
|
218
|
+
* disabled — so the rule stays on emptiness, which has no such collision.
|
|
219
|
+
*/
|
|
220
|
+
static publishableEndpoint(name) {
|
|
221
|
+
if (typeof name !== "string") return null;
|
|
222
|
+
return name.length === 0 ? null : name;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* THE KOTLIN PLATFORM'S OWN TYPE NAMES — the DEFAULT-IMPORTED set, as a CLOSED LIST.
|
|
226
|
+
*
|
|
227
|
+
* ## What this answers, and what it deliberately does not
|
|
228
|
+
*
|
|
229
|
+
* A bare `String`, `Int`, `List`, `Unit`, `Throwable` target names a declaration in
|
|
230
|
+
* `kotlin.*` (or one of the other default-imported packages: kotlin.collections,
|
|
231
|
+
* kotlin.text, kotlin.ranges, kotlin.sequences, kotlin.annotation, kotlin.comparisons,
|
|
232
|
+
* kotlin.io) — NOT a declaration in the project. MEASURED over this lane's 400-file gate
|
|
233
|
+
* population (2,067-file corpus, 6 projects), the unresolved edges whose target is one of
|
|
234
|
+
* these spellings number 2,936: 2,734 `references`, 172 `instantiates`, 27 `extends`,
|
|
235
|
+
* 3 `implements`. The top spellings are `String` 685, `Boolean` 332, `Int` 316, `Any` 315,
|
|
236
|
+
* `List` 269, `Unit` 238, `Long` 136, `Result` 96, `Throwable` 91, `Map` 68, `Double` 48,
|
|
237
|
+
* `Pair` 45, `Function` 40, `Array` 30. This is the largest ANALYZER-facing cause in the
|
|
238
|
+
* lane's residual; the largest cause overall (6,753 `calls` edges) is the 400-file stride
|
|
239
|
+
* hiding a declaration that does exist — an instrument artifact, not an analyzer defect.
|
|
240
|
+
*
|
|
241
|
+
* The membership test is DECIDABLE from a closed list, which is why it may be claimed at
|
|
242
|
+
* all: there is no lookup and no corpus fact behind it — a bare identifier of one of these
|
|
243
|
+
* spellings that is not this file's own is the platform's declaration of that name. The
|
|
244
|
+
* model is graphql's closed built-in scalar set.
|
|
245
|
+
*
|
|
246
|
+
* ## The three guards, each one measured rather than assumed
|
|
247
|
+
*
|
|
248
|
+
* 1. BARE SPELLINGS ONLY. A DOTTED target is not marked, and the reason is a measurement of
|
|
249
|
+
* the arbiter's own classifier: `incoming-rule.ts` `pathLikeTarget` reads a dotted target
|
|
250
|
+
* whose last segment is a source-file extension as a PATH, so `kotlin.js` would land in
|
|
251
|
+
* `externalUnverified` (`ext?`) — a claim no check can reach, which blocks a PASS instead
|
|
252
|
+
* of earning one. The dotted language-FQN channel (1,667 further edges: `kotlin.test` 228,
|
|
253
|
+
* `kotlin.coroutines` 141, `java.util.concurrent` 66, `java.util` 60, `kotlin.time` 33,
|
|
254
|
+
* `java.util.UUID` 18 …) is therefore PRICED AND LEFT, not guessed at.
|
|
255
|
+
* 2. NOT A NAME THIS FILE COULD MEAN ITSELF. The corpus refutes the naive version: Exposed
|
|
256
|
+
* declares `org.jetbrains.exposed.v1.core.Sequence` and `...core.Function`, and
|
|
257
|
+
* kotlinx-coroutines declares package `kotlin.coroutines.jvm.internal` — so a bare
|
|
258
|
+
* `Sequence` in an Exposed file that imports the project's own `Sequence` is NOT the
|
|
259
|
+
* platform's type, and marking it would assert something false about what the edge means.
|
|
260
|
+
* The marker is attached only where the file neither declares that name nor imports a
|
|
261
|
+
* declaration whose simple name or alias is that name. Both lists are already in hand at
|
|
262
|
+
* the point of the check — no new walk, no new parse. MEASURED: 40 edges vetoed.
|
|
263
|
+
* 3. A WILDCARD IMPORT VETOES EVERY BARE NAME. `MariaDBDialect.kt` carries
|
|
264
|
+
* `import org.jetbrains.exposed.v1.core.*` and writes `nextVal(seq: Sequence, …)`; an
|
|
265
|
+
* explicit star import beats the DEFAULT star import in Kotlin, so that `Sequence` is
|
|
266
|
+
* Exposed's own class. What a wildcard names cannot be enumerated from this file, so the
|
|
267
|
+
* file claims nothing. MEASURED: 1,518 candidate edges vetoed (54% of the naive 2,936);
|
|
268
|
+
* the 1,386 that remain are the change's whole yield.
|
|
269
|
+
*
|
|
270
|
+
* ## Why the marker is safe wherever it is attached
|
|
271
|
+
*
|
|
272
|
+
* The arbiter reads an external claim ONLY where the target names nothing in the project
|
|
273
|
+
* (`classifyEdge`, the `decls === 0` branch): on an edge whose target IS a project
|
|
274
|
+
* declaration the marker is inert, so it can never hide a resolution and never turn one into
|
|
275
|
+
* a gap. On a bare target the claim is verified by looking the target up and finding
|
|
276
|
+
* nothing — which is exactly the state in which it is read.
|
|
277
|
+
*
|
|
278
|
+
* TWO SPELLINGS ARE EXCLUDED, AND THE EXCLUSION IS A MEASUREMENT RATHER THAN A TASTE:
|
|
279
|
+
* `Sequence` and `Function` are default-imported (`kotlin.sequences.Sequence`, `kotlin.Function`)
|
|
280
|
+
* and this corpus declares both UNDER ITS OWN PACKAGE (`org.jetbrains.exposed.v1.core`). A
|
|
281
|
+
* same-package declaration needs NO import, so no per-file guard can see it: in the 400-file
|
|
282
|
+
* draw the marker was attached to `Alias.kt:139` — `object : Function<T>(columnType)`, which is
|
|
283
|
+
* Exposed's own class — and a false external is the one defect this programme has reverted a fix
|
|
284
|
+
* over (hcl's 29, protobuf's 37). Excluding the two spellings costs exactly TWO edges of the
|
|
285
|
+
* 1,388 (one of them that false one) and removes the only measured instance; the hazard CLASS
|
|
286
|
+
* remains declarable — a name redeclared in the same package of an unseen file — and for these
|
|
287
|
+
* 53 spellings a scan of all 2,346 `.kt`/`.kts` files found no other instance.
|
|
288
|
+
*/
|
|
289
|
+
static KOTLIN_DEFAULT_IMPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
290
|
+
// kotlin.* (NOTE: `Function` is deliberately absent — see the exclusion note above.)
|
|
291
|
+
"Any",
|
|
292
|
+
"Nothing",
|
|
293
|
+
"Unit",
|
|
294
|
+
"Boolean",
|
|
295
|
+
"Byte",
|
|
296
|
+
"Char",
|
|
297
|
+
"Double",
|
|
298
|
+
"Float",
|
|
299
|
+
"Int",
|
|
300
|
+
"Long",
|
|
301
|
+
"Short",
|
|
302
|
+
"String",
|
|
303
|
+
"Array",
|
|
304
|
+
"CharSequence",
|
|
305
|
+
"Comparable",
|
|
306
|
+
"Enum",
|
|
307
|
+
"Lazy",
|
|
308
|
+
"Pair",
|
|
309
|
+
"Triple",
|
|
310
|
+
"Result",
|
|
311
|
+
"Throwable",
|
|
312
|
+
"Exception",
|
|
313
|
+
"Error",
|
|
314
|
+
"RuntimeException",
|
|
315
|
+
"IllegalArgumentException",
|
|
316
|
+
"IllegalStateException",
|
|
317
|
+
"UnsupportedOperationException",
|
|
318
|
+
"IndexOutOfBoundsException",
|
|
319
|
+
"ArithmeticException",
|
|
320
|
+
"ClassCastException",
|
|
321
|
+
"NullPointerException",
|
|
322
|
+
"NoSuchElementException",
|
|
323
|
+
"NumberFormatException",
|
|
324
|
+
"ConcurrentModificationException",
|
|
325
|
+
"Number",
|
|
326
|
+
"Annotation",
|
|
327
|
+
"Deprecated",
|
|
328
|
+
"Suppress",
|
|
329
|
+
"OptIn",
|
|
330
|
+
"Comparator",
|
|
331
|
+
"UInt",
|
|
332
|
+
"ULong",
|
|
333
|
+
"UByte",
|
|
334
|
+
"UShort",
|
|
335
|
+
// kotlin.collections.*
|
|
336
|
+
"Iterable",
|
|
337
|
+
"MutableIterable",
|
|
338
|
+
"Iterator",
|
|
339
|
+
"MutableIterator",
|
|
340
|
+
"ListIterator",
|
|
341
|
+
"Collection",
|
|
342
|
+
"MutableCollection",
|
|
343
|
+
"List",
|
|
344
|
+
"MutableList",
|
|
345
|
+
"Set",
|
|
346
|
+
"MutableSet",
|
|
347
|
+
"Map",
|
|
348
|
+
"MutableMap",
|
|
349
|
+
"ArrayList",
|
|
350
|
+
"HashMap",
|
|
351
|
+
"HashSet",
|
|
352
|
+
"LinkedHashMap",
|
|
353
|
+
"LinkedHashSet",
|
|
354
|
+
"ArrayDeque",
|
|
355
|
+
// kotlin.text.* / kotlin.ranges.* (NOTE: `Sequence` is deliberately absent — same reason.)
|
|
356
|
+
"StringBuilder",
|
|
357
|
+
"Regex",
|
|
358
|
+
"MatchResult",
|
|
359
|
+
"IntRange",
|
|
360
|
+
"LongRange",
|
|
361
|
+
"CharRange",
|
|
362
|
+
"ClosedRange"
|
|
363
|
+
]);
|
|
364
|
+
/**
|
|
365
|
+
* ATTACH THE PLATFORM MARKER — one pass over the finished edge list, so a new emission site
|
|
366
|
+
* cannot forget it (the defect class that produced this lane's two import shapes).
|
|
367
|
+
*
|
|
368
|
+
* `isStdlib` is the spelling the arbiter already recognises as an external claim
|
|
369
|
+
* (`incoming-rule.ts` `EXTERNAL_KEYS`), and no kotlin edge carried it before this change, so
|
|
370
|
+
* the column it feeds read 0 for this lane while these edges were counted as silent holes.
|
|
371
|
+
* Nothing else about an edge is touched: target, source, line and every pre-existing
|
|
372
|
+
* metadata key are byte-identical.
|
|
373
|
+
*/
|
|
374
|
+
markPlatformTargets(relationships, symbols, imports) {
|
|
375
|
+
let hasWildcardImport = false;
|
|
376
|
+
const fileOwned = /* @__PURE__ */ new Set();
|
|
377
|
+
for (const s of symbols) {
|
|
378
|
+
const n = s?.name;
|
|
379
|
+
if (typeof n === "string" && n.length > 0) fileOwned.add(n);
|
|
380
|
+
}
|
|
381
|
+
for (const imp of imports) {
|
|
382
|
+
const mod = imp?.module;
|
|
383
|
+
if (typeof mod !== "string" || mod.length === 0) continue;
|
|
384
|
+
if (mod.endsWith("*") || imp?.isNamespaceImport === true) {
|
|
385
|
+
hasWildcardImport = true;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const dot = mod.lastIndexOf(".");
|
|
389
|
+
const leaf = dot >= 0 ? mod.slice(dot + 1) : mod;
|
|
390
|
+
if (leaf.length > 0) fileOwned.add(leaf);
|
|
391
|
+
if (typeof imp.alias === "string" && imp.alias.length > 0) fileOwned.add(imp.alias);
|
|
392
|
+
}
|
|
393
|
+
if (hasWildcardImport) return;
|
|
394
|
+
for (const rel of relationships) {
|
|
395
|
+
const t = rel.type;
|
|
396
|
+
if (t !== ASTEngine.RelationshipType.References && t !== ASTEngine.RelationshipType.Instantiates && t !== ASTEngine.RelationshipType.Extends && t !== ASTEngine.RelationshipType.Implements) continue;
|
|
397
|
+
const target = rel.target;
|
|
398
|
+
if (typeof target !== "string") continue;
|
|
399
|
+
if (!_RealKotlinAnalyzer.KOTLIN_DEFAULT_IMPORTED_TYPES.has(target)) continue;
|
|
400
|
+
if (fileOwned.has(target)) continue;
|
|
401
|
+
const md = rel.metadata;
|
|
402
|
+
if (!md || typeof md !== "object") continue;
|
|
403
|
+
if (md.isStdlib === true) continue;
|
|
404
|
+
md.isStdlib = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
analyze(rootNode, content, filePath, context) {
|
|
408
|
+
const ctx = {
|
|
409
|
+
content,
|
|
410
|
+
currentFile: filePath,
|
|
411
|
+
packageStack: [],
|
|
412
|
+
classStack: []
|
|
413
|
+
};
|
|
414
|
+
const symbols = [];
|
|
415
|
+
const relationships = [];
|
|
416
|
+
const imports = [];
|
|
417
|
+
const exports = [];
|
|
418
|
+
const errors = [];
|
|
419
|
+
const importClaimEnabled = process.env.BB_KOTLIN_IMPORT_CLAIM === "1";
|
|
420
|
+
if (rootNode.hasError) {
|
|
421
|
+
const collectErrors = (node) => {
|
|
422
|
+
if (node.type === "ERROR" || node.type === "MISSING") {
|
|
423
|
+
errors.push({
|
|
424
|
+
message: `Syntax ${node.type} detected. The Brain Engine is bypassing this broken node.`,
|
|
425
|
+
// 1-based rows/columns, consistent with nodeToRange and every
|
|
426
|
+
// other analyzer (was 0-based: error locations off-by-one).
|
|
427
|
+
range: {
|
|
428
|
+
startLine: node.startPosition.row + 1,
|
|
429
|
+
startColumn: node.startPosition.column + 1,
|
|
430
|
+
endLine: node.endPosition.row + 1,
|
|
431
|
+
endColumn: node.endPosition.column + 1
|
|
432
|
+
},
|
|
433
|
+
severity: `warning`
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
437
|
+
const child = node.child(i);
|
|
438
|
+
if (child) collectErrors(child);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
collectErrors(rootNode);
|
|
442
|
+
}
|
|
443
|
+
this.extractSymbolsWithScoping(rootNode, ctx, symbols, content, void 0, relationships);
|
|
444
|
+
this.analyzeRelationships(rootNode, relationships, symbols, ctx);
|
|
445
|
+
this.processImportsExports(rootNode, imports, exports, relationships, symbols, ctx);
|
|
446
|
+
const frameworks = this.detectKotlinFrameworks(content, symbols, imports);
|
|
447
|
+
const patterns = this.detectKotlinPatterns(symbols, relationships, content, rootNode);
|
|
448
|
+
this.injectPhantomSymbols(rootNode, symbols, frameworks, ctx);
|
|
449
|
+
try {
|
|
450
|
+
const extractKotlinCalls = (n) => {
|
|
451
|
+
const resolveCallSource = (start) => {
|
|
452
|
+
let p = start;
|
|
453
|
+
while (p) {
|
|
454
|
+
if (p.type === "secondary_constructor" || p.type === "primary_constructor") {
|
|
455
|
+
return this.kotlinConstructorOwnerName(p) ?? "";
|
|
456
|
+
}
|
|
457
|
+
if (p.type === "object_literal") {
|
|
458
|
+
return this.anonymousObjectName(p);
|
|
459
|
+
}
|
|
460
|
+
if (p.type === "function_declaration" || p.type === "class_declaration" || p.type === "object_declaration") {
|
|
461
|
+
const nameNode = this.findNameNode(p);
|
|
462
|
+
if (nameNode) return nameNode.text;
|
|
463
|
+
}
|
|
464
|
+
const topLevelProp = this.kotlinTopLevelPropertyName(p);
|
|
465
|
+
if (topLevelProp) return topLevelProp;
|
|
466
|
+
p = p.parent;
|
|
467
|
+
}
|
|
468
|
+
return "Global";
|
|
469
|
+
};
|
|
470
|
+
if (this.isKotlinContextParameterHeader(n)) return;
|
|
471
|
+
if (n.type === "call_expression") {
|
|
472
|
+
const callExpr = this.buildKotlinCallExpression(n);
|
|
473
|
+
const targetName = callExpr;
|
|
474
|
+
const leaf = targetName.split(".").pop() || targetName;
|
|
475
|
+
const isQualified = targetName.includes(".");
|
|
476
|
+
const isStdlib = this.kotlinStdlibExact.has(targetName) || !isQualified && this.kotlinStdlibExact.has(leaf);
|
|
477
|
+
if (targetName && targetName !== "print" && targetName !== "println" && !isStdlib) {
|
|
478
|
+
const sourceName = resolveCallSource(n.parent);
|
|
479
|
+
const endpointsName = _RealKotlinAnalyzer.publishableEndpoint(leaf) !== null && _RealKotlinAnalyzer.publishableEndpoint(sourceName) !== null;
|
|
480
|
+
if (endpointsName) {
|
|
481
|
+
const isPascal = leaf.length > 0 && leaf[0] >= "A" && leaf[0] <= "Z";
|
|
482
|
+
if (isPascal && !isQualified) {
|
|
483
|
+
relationships.push({
|
|
484
|
+
source: sourceName,
|
|
485
|
+
target: leaf,
|
|
486
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
487
|
+
confidence: 0.7,
|
|
488
|
+
// (#825): the constructed type is named by the callee leaf.
|
|
489
|
+
...this.lineField(this.calleeLeafNode(n, leaf), n),
|
|
490
|
+
metadata: { kotlinSpecific: true, isInstantiation: true }
|
|
491
|
+
});
|
|
492
|
+
} else {
|
|
493
|
+
relationships.push({
|
|
494
|
+
source: sourceName,
|
|
495
|
+
target: leaf,
|
|
496
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
497
|
+
confidence: 0.7,
|
|
498
|
+
// (#825): the call is written at the callee leaf.
|
|
499
|
+
...this.lineField(this.calleeLeafNode(n, leaf), n),
|
|
500
|
+
metadata: {
|
|
501
|
+
kotlinSpecific: true,
|
|
502
|
+
// cut-1 claim eligibility: only a RECEIVER-LESS call can be the imported binding itself
|
|
503
|
+
// (a receiver-carrying call's target is a member leaf, not the bound name).
|
|
504
|
+
...importClaimEnabled && !isQualified ? { kotlinBareCallee: true } : {}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} else if (n.type === "constructor_delegation_call") {
|
|
511
|
+
const text = n.text.trim();
|
|
512
|
+
const sourceName = resolveCallSource(n.parent);
|
|
513
|
+
let target = "super";
|
|
514
|
+
if (text.startsWith("this")) target = "this";
|
|
515
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(sourceName)) {
|
|
516
|
+
} else relationships.push({
|
|
517
|
+
source: sourceName,
|
|
518
|
+
target,
|
|
519
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
520
|
+
confidence: 1,
|
|
521
|
+
// (#825): `super(...)`/`this(...)` are unnamed keyword children,
|
|
522
|
+
// so the delegation node's own line is the site that carries it.
|
|
523
|
+
line_number: n.startPosition.row + 1,
|
|
524
|
+
metadata: {
|
|
525
|
+
relationshipType: "constructor_invocation",
|
|
526
|
+
constructorReceiver: target,
|
|
527
|
+
callExpression: text
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
532
|
+
const child = n.namedChild(i);
|
|
533
|
+
if (child) extractKotlinCalls(child);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
extractKotlinCalls(rootNode);
|
|
537
|
+
} catch (e) {
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
this.extractDeclaredTypeRefs(rootNode, relationships);
|
|
541
|
+
this.collectExpressionUses(rootNode, relationships, symbols);
|
|
542
|
+
} catch (e) {
|
|
543
|
+
}
|
|
544
|
+
const typeIndex = /* @__PURE__ */ new Map();
|
|
545
|
+
for (const s of symbols) {
|
|
546
|
+
if (s.kind === ASTEngine.SymbolKind.Class || s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Enum) {
|
|
547
|
+
if (!typeIndex.has(s.name)) typeIndex.set(s.name, s);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const declSigs = /* @__PURE__ */ new Map();
|
|
551
|
+
for (const t of symbols) {
|
|
552
|
+
if (t.kind !== ASTEngine.SymbolKind.Method || !t.ownerName || t.metadata?.isPhantom) continue;
|
|
553
|
+
let nameMap = declSigs.get(t.ownerName);
|
|
554
|
+
if (!nameMap) {
|
|
555
|
+
nameMap = /* @__PURE__ */ new Map();
|
|
556
|
+
declSigs.set(t.ownerName, nameMap);
|
|
557
|
+
}
|
|
558
|
+
let set = nameMap.get(t.name);
|
|
559
|
+
if (!set) {
|
|
560
|
+
set = /* @__PURE__ */ new Set();
|
|
561
|
+
nameMap.set(t.name, set);
|
|
562
|
+
}
|
|
563
|
+
const tSig = "(" + (t.parameters ?? []).map((p) => p.type ?? "").join(",") + ")";
|
|
564
|
+
set.add(tSig);
|
|
565
|
+
}
|
|
566
|
+
const propSigs = /* @__PURE__ */ new Map();
|
|
567
|
+
for (const t of symbols) {
|
|
568
|
+
if (t.kind !== ASTEngine.SymbolKind.Property || !t.ownerName || t.metadata?.isPhantom) continue;
|
|
569
|
+
let nameMap = propSigs.get(t.ownerName);
|
|
570
|
+
if (!nameMap) {
|
|
571
|
+
nameMap = /* @__PURE__ */ new Map();
|
|
572
|
+
propSigs.set(t.ownerName, nameMap);
|
|
573
|
+
}
|
|
574
|
+
let set = nameMap.get(t.name);
|
|
575
|
+
if (!set) {
|
|
576
|
+
set = /* @__PURE__ */ new Set();
|
|
577
|
+
nameMap.set(t.name, set);
|
|
578
|
+
}
|
|
579
|
+
set.add("(" + (t.returnType ?? "") + ")");
|
|
580
|
+
}
|
|
581
|
+
for (const s of symbols) {
|
|
582
|
+
const _owner = s.ownerName;
|
|
583
|
+
if (_owner) {
|
|
584
|
+
const isSynthetic = s.metadata?.isPhantom === true;
|
|
585
|
+
relationships.push({
|
|
586
|
+
source: _owner,
|
|
587
|
+
target: s.name,
|
|
588
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
589
|
+
// K1 (M60 kotlin deep review, 2026-09-27): a SYNTHESIZED member's
|
|
590
|
+
// name is written nowhere in the file. `data class C(val a: Int)`
|
|
591
|
+
// produces component1 / copy / equals / hashCode / toString, and the
|
|
592
|
+
// code above already knows it — `isSynthetic` is computed, and the
|
|
593
|
+
// LINE is correctly omitted for exactly this reason. The confidence
|
|
594
|
+
// was the one field left claiming otherwise.
|
|
595
|
+
//
|
|
596
|
+
// This is the same defect the java pass closed as JD-J13, verbatim in
|
|
597
|
+
// shape: an inference published at maximum confidence, on a branch
|
|
598
|
+
// that already carries the knowledge it is wrong about. Probed here:
|
|
599
|
+
//
|
|
600
|
+
// data class C(val a: Int)
|
|
601
|
+
// C->a conf=1.0 line=1 (declared — correct)
|
|
602
|
+
// C->component1 conf=1.0 line=- (synthesized)
|
|
603
|
+
// C->copy conf=1.0 line=- (synthesized)
|
|
604
|
+
// C->equals conf=1.0 line=- (synthesized)
|
|
605
|
+
// C->hashCode conf=1.0 line=- (synthesized)
|
|
606
|
+
// C->toString conf=1.0 line=- (synthesized)
|
|
607
|
+
//
|
|
608
|
+
// Five of six containment edges name a method with no declaration, at
|
|
609
|
+
// maximum confidence, while the one real member sits beside them at the
|
|
610
|
+
// same confidence with a real line. A consumer cannot tell them apart
|
|
611
|
+
// without knowing the `isPhantom` convention.
|
|
612
|
+
//
|
|
613
|
+
// Fixed the way java's JD-J13 was: keep the edge (the containment is
|
|
614
|
+
// real and useful — `C.component1` does exist), mark it, and demote
|
|
615
|
+
// below the 0.7 fabrication threshold so it reads as declared
|
|
616
|
+
// suspicion and a consumer can filter on it.
|
|
617
|
+
confidence: isSynthetic ? 0.6 : 1,
|
|
618
|
+
...!isSynthetic && s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
619
|
+
metadata: { isContained: true, ...isSynthetic ? { inferred: true } : {} }
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
const overrideKind = s.kind === ASTEngine.SymbolKind.Method ? "method" : s.kind === ASTEngine.SymbolKind.Property ? "property" : "";
|
|
623
|
+
if (overrideKind !== "" && s.ownerName && !s.metadata?.isPhantom && s.metadata?.modifiers?.includes?.("override")) {
|
|
624
|
+
const isMethodOverride = overrideKind === "method";
|
|
625
|
+
const ownerSym = typeIndex.get(s.ownerName);
|
|
626
|
+
const parents = ownerSym?.metadata?.superTypes ?? [];
|
|
627
|
+
const overrideSig = isMethodOverride ? "(" + (s.parameters ?? []).map((p) => p.type ?? "").join(",") + ")" : "(" + (s.returnType ?? "") + ")";
|
|
628
|
+
let overrideParent = parents.length > 0 ? parents[0] : "";
|
|
629
|
+
let confidence = 0.5;
|
|
630
|
+
let reasonCode = "first-supertype-guess";
|
|
631
|
+
let evidence = "inferred";
|
|
632
|
+
if (parents.length > 0) {
|
|
633
|
+
const declared = (typeName) => {
|
|
634
|
+
const nameMap = (isMethodOverride ? declSigs : propSigs).get(typeName);
|
|
635
|
+
if (!nameMap) return false;
|
|
636
|
+
const sigs = nameMap.get(s.name);
|
|
637
|
+
if (!sigs || sigs.size === 0) return false;
|
|
638
|
+
if (overrideSig && sigs.has(overrideSig)) return true;
|
|
639
|
+
const hasTypeVar = (sig) => kotlinSigHasTypeVar(sig);
|
|
640
|
+
if (!overrideSig) return true;
|
|
641
|
+
for (const s2 of sigs) if (hasTypeVar(s2) || hasTypeVar(overrideSig)) return true;
|
|
642
|
+
return false;
|
|
643
|
+
};
|
|
644
|
+
const queue = [...parents];
|
|
645
|
+
const seen = /* @__PURE__ */ new Set();
|
|
646
|
+
let depth = 0;
|
|
647
|
+
while (queue.length > 0 && depth < 10) {
|
|
648
|
+
depth++;
|
|
649
|
+
const parent = queue.shift();
|
|
650
|
+
if (seen.has(parent)) continue;
|
|
651
|
+
seen.add(parent);
|
|
652
|
+
if (declared(parent)) {
|
|
653
|
+
overrideParent = parent;
|
|
654
|
+
confidence = 0.9;
|
|
655
|
+
reasonCode = "in-file-verified";
|
|
656
|
+
evidence = "structural";
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
const gp = typeIndex.get(parent)?.metadata?.superTypes ?? [];
|
|
660
|
+
for (const g of gp) if (!seen.has(g)) queue.push(g);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (isMethodOverride && context?.overrideCompanion && parents.length > 0) {
|
|
664
|
+
const declaring = findDeclaringParent(
|
|
665
|
+
s.name,
|
|
666
|
+
parents,
|
|
667
|
+
context.overrideCompanion,
|
|
668
|
+
(t) => typeIndex.get(t)?.metadata?.superTypes ?? []
|
|
669
|
+
);
|
|
670
|
+
if (declaring) {
|
|
671
|
+
overrideParent = declaring;
|
|
672
|
+
confidence = 0.9;
|
|
673
|
+
reasonCode = "companion-index";
|
|
674
|
+
evidence = "structural";
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (overrideParent) {
|
|
678
|
+
relationships.push({
|
|
679
|
+
source: `${s.ownerName}.${s.name}`,
|
|
680
|
+
target: `${overrideParent}.${s.name}`,
|
|
681
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
682
|
+
confidence,
|
|
683
|
+
// (#825): the override occurs at the OVERRIDING declaration.
|
|
684
|
+
...s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
685
|
+
metadata: {
|
|
686
|
+
isOverride: true,
|
|
687
|
+
overrideParent,
|
|
688
|
+
reasonCode,
|
|
689
|
+
evidenceClass: evidence,
|
|
690
|
+
overrideSig,
|
|
691
|
+
overrideMemberKind: overrideKind
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const compilationUnit = kotlinCompilationUnitName(filePath);
|
|
698
|
+
for (const imp of imports) {
|
|
699
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(imp.module)) continue;
|
|
700
|
+
relationships.push({
|
|
701
|
+
source: "root",
|
|
702
|
+
target: imp.module,
|
|
703
|
+
type: ASTEngine.RelationshipType.References,
|
|
704
|
+
confidence: 0.9,
|
|
705
|
+
// (#825): the imported module is named on the import statement.
|
|
706
|
+
...imp.range?.startLine ? { line_number: imp.range.startLine } : {},
|
|
707
|
+
metadata: { isImportDep: true }
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
this.canonicalizePublishedNames(symbols, relationships);
|
|
711
|
+
const primaryType = this.primaryTopLevelType(symbols);
|
|
712
|
+
if (primaryType) {
|
|
713
|
+
for (const rel of relationships) {
|
|
714
|
+
if (rel.type === ASTEngine.RelationshipType.References && rel.source === "root") {
|
|
715
|
+
rel.source = primaryType.name;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
} else if (compilationUnit) {
|
|
719
|
+
for (const rel of relationships) {
|
|
720
|
+
if (rel.type === ASTEngine.RelationshipType.References && rel.source === "root") {
|
|
721
|
+
rel.source = compilationUnit;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const exportedSymbols = [];
|
|
726
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
727
|
+
const s = symbols[i];
|
|
728
|
+
if (s.metadata?.isPhantom || s.metadata?.isAnonymousObject) continue;
|
|
729
|
+
if (s.metadata?.isFileLevelDeclaration !== true) continue;
|
|
730
|
+
if (s.visibility !== "public" && s.visibility !== void 0) continue;
|
|
731
|
+
if (s.ownerName) continue;
|
|
732
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(s.name)) continue;
|
|
733
|
+
const mods = s.metadata?.modifiers ?? [];
|
|
734
|
+
const explicitPublic = mods.indexOf("public") >= 0;
|
|
735
|
+
exportedSymbols.push({
|
|
736
|
+
name: s.name,
|
|
737
|
+
kind: s.kind,
|
|
738
|
+
range: s.range,
|
|
739
|
+
visibility: s.visibility || "public",
|
|
740
|
+
via: explicitPublic ? "explicit-public" : "default-public"
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
const unitIdentity = this.primaryTopLevelType(symbols);
|
|
744
|
+
const fileLevelSource = unitIdentity ? "(file)" : kotlinCompilationUnitName(filePath) || "(file)";
|
|
745
|
+
for (const e of exportedSymbols) {
|
|
746
|
+
exports.push({ name: e.name, kind: e.kind, range: e.range, visibility: e.visibility });
|
|
747
|
+
relationships.push({
|
|
748
|
+
source: fileLevelSource,
|
|
749
|
+
target: e.name,
|
|
750
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
751
|
+
confidence: 1,
|
|
752
|
+
// (#825): the exported declaration's own line — the same site rule
|
|
753
|
+
// go's exports emitter uses (real-go-analyzer.ts:463).
|
|
754
|
+
...e.range?.startLine ? { line_number: e.range.startLine } : {},
|
|
755
|
+
metadata: {
|
|
756
|
+
kotlinSpecific: true,
|
|
757
|
+
// Which fact put this name on the file's surface: the modifier that is
|
|
758
|
+
// written, or Kotlin's public-by-default rule. A reader can tell an
|
|
759
|
+
// explicit `public` from an implicit one without re-parsing.
|
|
760
|
+
via: e.via,
|
|
761
|
+
visibility: e.visibility,
|
|
762
|
+
symbolKind: String(e.kind),
|
|
763
|
+
// M60/#910: the source is the FILE, not a symbol.
|
|
764
|
+
fileLevel: true
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
this.classifyEvidenceTier(relationships);
|
|
769
|
+
this.markPlatformTargets(relationships, symbols, imports);
|
|
770
|
+
if (importClaimEnabled) {
|
|
771
|
+
this.attachKotlinImportedBindingClaims(relationships, symbols, imports);
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
symbols,
|
|
775
|
+
relationships,
|
|
776
|
+
imports,
|
|
777
|
+
exports,
|
|
778
|
+
errors,
|
|
779
|
+
metadata: {
|
|
780
|
+
complexity: this.calculateRealComplexity(symbols, relationships),
|
|
781
|
+
linesOfCode: content.split("\n").length,
|
|
782
|
+
cyclomaticComplexity: this.calculateCyclomaticComplexity(rootNode),
|
|
783
|
+
framework: frameworks[0],
|
|
784
|
+
// Primary framework
|
|
785
|
+
patterns,
|
|
786
|
+
// Kotlin intelligence metadata
|
|
787
|
+
detectedFrameworks: frameworks,
|
|
788
|
+
frameworkPatterns: this.getKotlinFrameworkPatterns(frameworks, symbols),
|
|
789
|
+
architecturalInsights: this.generateArchitecturalInsights(symbols, relationships),
|
|
790
|
+
architecturalScore: this.calculateArchitecturalScore(symbols, relationships, patterns),
|
|
791
|
+
codeQuality: this.assessKotlinCodeQuality(symbols, relationships, patterns),
|
|
792
|
+
hasTypeInference: true,
|
|
793
|
+
hasFrameworkIntelligence: true,
|
|
794
|
+
// ABSENT when the file has a module identity, a COUNT when it does not:
|
|
795
|
+
// a zero would be a claim that the count was measured to be zero, while
|
|
796
|
+
// an absent field is an unknown. See the unit-identity note above.
|
|
797
|
+
...unitIdentity ? {} : { exportsWithheldNoModuleIdentity: exportedSymbols.length },
|
|
798
|
+
analysisVersion: "3.0.0-infallible-brain"
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* PHASE 1: Real Kotlin Symbol Extraction with Scoping
|
|
804
|
+
* Understands Kotlin semantics: classes, data classes, sealed classes, objects, functions
|
|
805
|
+
*/
|
|
806
|
+
injectPhantomSymbols(rootNode, symbols, frameworks, ctx) {
|
|
807
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
808
|
+
const symbol = symbols[i];
|
|
809
|
+
let hasData = false;
|
|
810
|
+
const symModifiers = symbol.metadata?.modifiers ?? [];
|
|
811
|
+
for (let j = 0; j < symModifiers.length; j++) {
|
|
812
|
+
if (symModifiers[j] === "data") {
|
|
813
|
+
hasData = true;
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (symbol.kind === ASTEngine.SymbolKind.Class && hasData) {
|
|
818
|
+
const properties = [];
|
|
819
|
+
for (let j = 0; j < symbols.length; j++) {
|
|
820
|
+
const s = symbols[j];
|
|
821
|
+
if (s.ownerName === symbol.name && s.kind === ASTEngine.SymbolKind.Property && s.metadata?.isConstructorProperty === true) properties.push(s);
|
|
822
|
+
}
|
|
823
|
+
for (let index = 0; index < properties.length; index++) {
|
|
824
|
+
const prop = properties[index];
|
|
825
|
+
symbols.push({
|
|
826
|
+
name: `component${index + 1}`,
|
|
827
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
828
|
+
scope: symbol.name,
|
|
829
|
+
ownerName: symbol.name,
|
|
830
|
+
modifiers: ["phantom", "data_class_generated", "operator"],
|
|
831
|
+
range: symbol.range,
|
|
832
|
+
parameters: [],
|
|
833
|
+
returnType: prop.returnType || prop.type || "Any",
|
|
834
|
+
metadata: { isPhantom: true }
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
const copyParams = [];
|
|
838
|
+
for (let j = 0; j < properties.length; j++) {
|
|
839
|
+
copyParams.push({ name: properties[j].name, type: properties[j].returnType || properties[j].type || "Any" });
|
|
840
|
+
}
|
|
841
|
+
symbols.push({
|
|
842
|
+
name: "copy",
|
|
843
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
844
|
+
scope: symbol.name,
|
|
845
|
+
// (#825): ownerName is what makes the member CONTAINED by its class
|
|
846
|
+
// — the injector set it for componentN but not for copy/equals/hashCode/
|
|
847
|
+
// toString, so `contains Money -> copy` never existed while
|
|
848
|
+
// `contains Money -> component1` did. Same class, same synthesis route,
|
|
849
|
+
// opposite outcomes on a real corpus file (measured on
|
|
850
|
+
// JsonTestsData.kt: 7 componentN containment edges, 0 for the other four
|
|
851
|
+
// members). java's phantoms (lombok/Spring-Data, processPhantomSymbols)
|
|
852
|
+
// have always carried ownerName.
|
|
853
|
+
ownerName: symbol.name,
|
|
854
|
+
modifiers: ["phantom", "data_class_generated"],
|
|
855
|
+
range: symbol.range,
|
|
856
|
+
parameters: copyParams,
|
|
857
|
+
returnType: symbol.name,
|
|
858
|
+
metadata: { isPhantom: true }
|
|
859
|
+
});
|
|
860
|
+
const stdMethods = [
|
|
861
|
+
{ name: "equals", type: "Boolean", params: [{ name: "other", type: "Any?" }] },
|
|
862
|
+
{ name: "hashCode", type: "Int", params: [] },
|
|
863
|
+
{ name: "toString", type: "String", params: [] }
|
|
864
|
+
];
|
|
865
|
+
for (let j = 0; j < stdMethods.length; j++) {
|
|
866
|
+
const method = stdMethods[j];
|
|
867
|
+
symbols.push({
|
|
868
|
+
name: method.name,
|
|
869
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
870
|
+
scope: symbol.name,
|
|
871
|
+
// Same ownerName gap as `copy` above (#825): without it these
|
|
872
|
+
// three generated overrides were the only members of the data class
|
|
873
|
+
// with no containment edge at all.
|
|
874
|
+
ownerName: symbol.name,
|
|
875
|
+
modifiers: ["phantom", "data_class_generated", "override"],
|
|
876
|
+
range: symbol.range,
|
|
877
|
+
parameters: method.params,
|
|
878
|
+
returnType: method.type,
|
|
879
|
+
metadata: { isPhantom: true }
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* The TYPE that owns a constructor, by name — for attributing edges written
|
|
887
|
+
* inside one.
|
|
888
|
+
*
|
|
889
|
+
* MEASURED DEFECT this closes (kotlin corpus, 405-file census,
|
|
890
|
+
* script/m14/kotlin-census.ts). The M14 #419 K11 rule is "constructor and init
|
|
891
|
+
* calls attribute to the CLASS NAME", and it was implemented as
|
|
892
|
+
* `cls = node.parent; findNameNode(cls)`. That is TRUE for a
|
|
893
|
+
* `primary_constructor` — its parent IS the class_declaration — and FALSE for a
|
|
894
|
+
* `secondary_constructor`, whose parent is the `class_body`. So every
|
|
895
|
+
* `secondary_constructor` in the corpus fell through to the literal fallback
|
|
896
|
+
* and published `parentClass || "constructor"`: 60 edges across 405 files
|
|
897
|
+
* (37 calls, 13 uses, 4 instantiates, 3 exports targets, 2 references, 1
|
|
898
|
+
* contains) attributed to or naming a symbol spelled `constructor`, which no
|
|
899
|
+
* Kotlin source can declare. The rule was documented as closed and was half
|
|
900
|
+
* implemented.
|
|
901
|
+
*
|
|
902
|
+
* One walk fixes both shapes: skip the `class_body` and take the nearest
|
|
903
|
+
* enclosing type declaration's name. Returns undefined when there is none (an
|
|
904
|
+
* ERROR-recovered declaration whose name node is zero-length — see
|
|
905
|
+
* findNameNode), and the callers then publish nothing rather than a
|
|
906
|
+
* placeholder (R7).
|
|
907
|
+
*/
|
|
908
|
+
kotlinConstructorOwnerName(node) {
|
|
909
|
+
let p = node.parent;
|
|
910
|
+
while (p) {
|
|
911
|
+
if (p.type === "class_declaration" || p.type === "object_declaration") {
|
|
912
|
+
const n = this.findNameNode(p);
|
|
913
|
+
if (!n || this.isKotlinTypeNameKeyword(n.text)) return void 0;
|
|
914
|
+
return n.text;
|
|
915
|
+
}
|
|
916
|
+
if (p.type === "source_file") break;
|
|
917
|
+
p = p.parent;
|
|
918
|
+
}
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
extractSymbolsWithScoping(node, ctx, symbols, content, parentClass, relationships) {
|
|
922
|
+
const stack = [{ n: node, pClass: parentClass }];
|
|
923
|
+
while (stack.length > 0) {
|
|
924
|
+
const entry = stack.pop();
|
|
925
|
+
if (entry.isPop) {
|
|
926
|
+
if (entry.name) {
|
|
927
|
+
ctx.classStack.pop();
|
|
928
|
+
}
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
const n = entry.n;
|
|
932
|
+
const pClass = entry.pClass;
|
|
933
|
+
let nextParentClass = pClass;
|
|
934
|
+
let pushedName = void 0;
|
|
935
|
+
const symbolsBefore = symbols.length;
|
|
936
|
+
const atFileLevel = n.parent?.type === "source_file";
|
|
937
|
+
switch (n.type) {
|
|
938
|
+
case "function_declaration":
|
|
939
|
+
{
|
|
940
|
+
const pt = n.parent?.type;
|
|
941
|
+
const isMemberPosition = pt === "class_body" || pt === "enum_class_body";
|
|
942
|
+
this.extractFunction(n, symbols, ctx, isMemberPosition ? pClass : void 0);
|
|
943
|
+
}
|
|
944
|
+
break;
|
|
945
|
+
case "class_declaration": {
|
|
946
|
+
const isInterface = someSymbol(n.children, (c) => c?.type === "interface");
|
|
947
|
+
const isEnum = someSymbol(n.children, (c) => c?.type === "enum");
|
|
948
|
+
const cNameNode = this.findNameNode(n);
|
|
949
|
+
const cName = cNameNode && !this.isKotlinTypeNameKeyword(cNameNode.text) ? cNameNode.text : void 0;
|
|
950
|
+
if (cName) {
|
|
951
|
+
ctx.classStack.push(cName);
|
|
952
|
+
pushedName = cName;
|
|
953
|
+
nextParentClass = cName;
|
|
954
|
+
}
|
|
955
|
+
{
|
|
956
|
+
const pt = n.parent?.type;
|
|
957
|
+
const isMemberPosition = pt === "class_body" || pt === "enum_class_body";
|
|
958
|
+
const ownerForDecl = isMemberPosition ? pClass : void 0;
|
|
959
|
+
if (isInterface) {
|
|
960
|
+
this.extractInterface(n, symbols, ctx, ownerForDecl);
|
|
961
|
+
} else if (isEnum) {
|
|
962
|
+
this.extractEnumClass(n, symbols, ctx, ownerForDecl);
|
|
963
|
+
} else {
|
|
964
|
+
this.extractClass(n, symbols, ctx, ownerForDecl);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
case "object_declaration": {
|
|
970
|
+
const oNameNode = this.findNameNode(n);
|
|
971
|
+
const oName = oNameNode && !this.isKotlinTypeNameKeyword(oNameNode.text) ? oNameNode.text : void 0;
|
|
972
|
+
if (oName) {
|
|
973
|
+
ctx.classStack.push(oName);
|
|
974
|
+
pushedName = oName;
|
|
975
|
+
nextParentClass = oName;
|
|
976
|
+
}
|
|
977
|
+
{
|
|
978
|
+
const pt = n.parent?.type;
|
|
979
|
+
const isMemberPosition = pt === "class_body" || pt === "enum_class_body";
|
|
980
|
+
this.extractObject(n, symbols, ctx, isMemberPosition ? pClass : void 0);
|
|
981
|
+
}
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
case "property_declaration":
|
|
985
|
+
if (this.isKotlinLocalDeclaration(n)) break;
|
|
986
|
+
{
|
|
987
|
+
const pt = n.parent?.type;
|
|
988
|
+
const isMemberPosition = pt === "class_body" || pt === "enum_class_body";
|
|
989
|
+
this.extractProperty(n, symbols, ctx, isMemberPosition ? pClass : void 0, relationships);
|
|
990
|
+
}
|
|
991
|
+
break;
|
|
992
|
+
case "primary_constructor":
|
|
993
|
+
case "constructor_declaration":
|
|
994
|
+
case "secondary_constructor":
|
|
995
|
+
this.extractConstructor(n, symbols, ctx, pClass);
|
|
996
|
+
break;
|
|
997
|
+
case "package_header":
|
|
998
|
+
this.extractPackage(n, ctx);
|
|
999
|
+
break;
|
|
1000
|
+
case "import_header":
|
|
1001
|
+
this.extractImport(n, ctx);
|
|
1002
|
+
break;
|
|
1003
|
+
case "annotation":
|
|
1004
|
+
this.extractAnnotation(n, symbols, ctx);
|
|
1005
|
+
break;
|
|
1006
|
+
case "type_alias": {
|
|
1007
|
+
const pt = n.parent?.type;
|
|
1008
|
+
const isMemberPosition = pt === "class_body" || pt === "enum_class_body" || pt === "source_file";
|
|
1009
|
+
const tName = this.findNameNode(n);
|
|
1010
|
+
if (tName) {
|
|
1011
|
+
const tModifiers = this.extractModifiers(n, ctx);
|
|
1012
|
+
symbols.push({
|
|
1013
|
+
name: tName.text,
|
|
1014
|
+
kind: ASTEngine.SymbolKind.Type,
|
|
1015
|
+
range: this.symbolRange(n),
|
|
1016
|
+
scope: this.currentScope(ctx),
|
|
1017
|
+
visibility: this.determineVisibility(tModifiers, ctx),
|
|
1018
|
+
// M60 M3 (master review): same owner rule, same sweep. A type
|
|
1019
|
+
// alias is a MEMBER of a class exactly when it is declared in that
|
|
1020
|
+
// class's body. One declared inside a function is local to it, and
|
|
1021
|
+
// the class does not contain it:
|
|
1022
|
+
//
|
|
1023
|
+
// class Host3 { fun f() { typealias T = Int; val x: T = 1 } }
|
|
1024
|
+
// published: T[owner=Host3], contains Host3->T
|
|
1025
|
+
//
|
|
1026
|
+
// This is the same phantom containment as aa6a2beb's init-block
|
|
1027
|
+
// local and the local-class fix, in the one remaining ungated
|
|
1028
|
+
// extractor call site. Found by enumerating every site that passes
|
|
1029
|
+
// a parentClass, not by looking for this shape.
|
|
1030
|
+
ownerName: isMemberPosition ? pClass : void 0,
|
|
1031
|
+
ownerKind: isMemberPosition && pClass ? ASTEngine.SymbolKind.Class : void 0,
|
|
1032
|
+
decorators: this.extractAnnotations(n, ctx),
|
|
1033
|
+
metadata: {
|
|
1034
|
+
modifiers: tModifiers,
|
|
1035
|
+
annotations: this.extractAnnotations(n, ctx).map((a) => this.kotlinShortAnnotationName(a)),
|
|
1036
|
+
isTypeAlias: true,
|
|
1037
|
+
kotlinSpecific: true
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case "object_literal": {
|
|
1044
|
+
const deleg = this.findChildByType(n, ["delegation_specifier"]);
|
|
1045
|
+
let superName;
|
|
1046
|
+
if (deleg) {
|
|
1047
|
+
const findType = (node2) => {
|
|
1048
|
+
if (node2.type === "type_identifier") return node2;
|
|
1049
|
+
for (let i = 0; i < node2.namedChildCount; i++) {
|
|
1050
|
+
const c = node2.namedChild(i);
|
|
1051
|
+
if (c) {
|
|
1052
|
+
const r = findType(c);
|
|
1053
|
+
if (r) return r;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
};
|
|
1058
|
+
const tId = findType(deleg);
|
|
1059
|
+
if (tId) superName = tId.text;
|
|
1060
|
+
}
|
|
1061
|
+
const anonName = this.anonymousObjectName(n);
|
|
1062
|
+
const anonOwner = this.anonymousObjectOwner(n, pClass);
|
|
1063
|
+
ctx.classStack.push(anonName);
|
|
1064
|
+
pushedName = anonName;
|
|
1065
|
+
nextParentClass = anonName;
|
|
1066
|
+
symbols.push({
|
|
1067
|
+
name: anonName,
|
|
1068
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1069
|
+
range: this.symbolRange(n),
|
|
1070
|
+
scope: this.currentScope(ctx),
|
|
1071
|
+
visibility: this.determineVisibility(this.extractModifiers(n, ctx), ctx),
|
|
1072
|
+
ownerName: anonOwner.name,
|
|
1073
|
+
ownerKind: anonOwner.kind,
|
|
1074
|
+
decorators: this.extractAnnotations(n, ctx),
|
|
1075
|
+
metadata: {
|
|
1076
|
+
modifiers: [],
|
|
1077
|
+
annotations: this.extractAnnotations(n, ctx).map((a) => this.kotlinShortAnnotationName(a)),
|
|
1078
|
+
isObject: true,
|
|
1079
|
+
isAnonymousObject: true,
|
|
1080
|
+
superTypes: superName ? [superName] : [],
|
|
1081
|
+
superTypesAreConstructorCalls: superName ? [false] : [],
|
|
1082
|
+
kotlinSpecific: true
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (atFileLevel) {
|
|
1089
|
+
for (let i = symbolsBefore; i < symbols.length; i++) {
|
|
1090
|
+
const sym = symbols[i];
|
|
1091
|
+
sym.metadata = { ...sym.metadata ?? {}, isFileLevelDeclaration: true };
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (pushedName) {
|
|
1095
|
+
stack.push({ n, isPop: true, name: pushedName });
|
|
1096
|
+
}
|
|
1097
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1098
|
+
const child = n.namedChild(i);
|
|
1099
|
+
if (child) {
|
|
1100
|
+
stack.push({ n: child, pClass: nextParentClass });
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Extract Kotlin function with extension functions and suspend functions
|
|
1107
|
+
*/
|
|
1108
|
+
extractFunction(node, symbols, ctx, parentClass) {
|
|
1109
|
+
const nameNode = this.findNameNode(node);
|
|
1110
|
+
if (!nameNode) return;
|
|
1111
|
+
const functionName = nameNode.text;
|
|
1112
|
+
const qualifiedName = this.buildQualifiedName(functionName, ctx, parentClass);
|
|
1113
|
+
const modifiers = this.extractModifiers(node, ctx);
|
|
1114
|
+
const parameters = this.extractKotlinParameters(node, ctx);
|
|
1115
|
+
const returnType = this.extractReturnType(node, ctx);
|
|
1116
|
+
const receiverType = this.extractReceiverType(node, ctx);
|
|
1117
|
+
const qualifiedCalls = this.findKotlinFunctionCalls(node, ctx);
|
|
1118
|
+
const annotations = this.extractAnnotations(node, ctx);
|
|
1119
|
+
const kdoc = this.extractKDocComment(node, ctx);
|
|
1120
|
+
const visibility = this.determineVisibility(modifiers, ctx);
|
|
1121
|
+
let isInline = false, isInfix = false, isOperator = false, isSuspend = false;
|
|
1122
|
+
for (let i = 0; i < modifiers.length; i++) {
|
|
1123
|
+
if (modifiers[i] === "inline") isInline = true;
|
|
1124
|
+
else if (modifiers[i] === "infix") isInfix = true;
|
|
1125
|
+
else if (modifiers[i] === "operator") isOperator = true;
|
|
1126
|
+
else if (modifiers[i] === "suspend") isSuspend = true;
|
|
1127
|
+
}
|
|
1128
|
+
const isExtension = !!receiverType;
|
|
1129
|
+
const apiContract = this.extractKotlinApiContract(node, ctx);
|
|
1130
|
+
const apiContractPath = apiContract.path;
|
|
1131
|
+
const apiMethod = apiContract.method;
|
|
1132
|
+
const fnTypeParameters = this.extractTypeParameters(node, ctx);
|
|
1133
|
+
const symbol = {
|
|
1134
|
+
name: functionName,
|
|
1135
|
+
kind: parentClass ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function,
|
|
1136
|
+
range: this.symbolRange(node),
|
|
1137
|
+
scope: this.currentScope(ctx),
|
|
1138
|
+
visibility,
|
|
1139
|
+
isAsync: isSuspend,
|
|
1140
|
+
parameters,
|
|
1141
|
+
returnType,
|
|
1142
|
+
decorators: annotations,
|
|
1143
|
+
// Top-level per family contract: the DB ingester reads
|
|
1144
|
+
// symbol.complexity (|| 1) for gravity scoring — metadata-only values
|
|
1145
|
+
// left every kotlin symbol at complexity 1 in the index.
|
|
1146
|
+
complexity: this.calculateFunctionComplexity(node, ctx),
|
|
1147
|
+
comments: kdoc ? [kdoc] : [],
|
|
1148
|
+
qualified_calls: qualifiedCalls,
|
|
1149
|
+
apiContractPath,
|
|
1150
|
+
genericParameters: fnTypeParameters.length > 0 ? fnTypeParameters : void 0,
|
|
1151
|
+
ownerName: parentClass,
|
|
1152
|
+
ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
|
|
1153
|
+
metadata: {
|
|
1154
|
+
modifiers,
|
|
1155
|
+
annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
|
|
1156
|
+
receiverType,
|
|
1157
|
+
apiMethod,
|
|
1158
|
+
isInline,
|
|
1159
|
+
isInfix,
|
|
1160
|
+
isOperator,
|
|
1161
|
+
isSuspend,
|
|
1162
|
+
isExtension,
|
|
1163
|
+
complexity: this.calculateFunctionComplexity(node, ctx),
|
|
1164
|
+
parameterCount: parameters.length,
|
|
1165
|
+
hasKDoc: !!kdoc,
|
|
1166
|
+
...fnTypeParameters.length > 0 ? { typeParameters: fnTypeParameters } : {},
|
|
1167
|
+
kotlinSpecific: true
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
symbols.push(symbol);
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Spring mapping annotation extraction. Returns the route PATH ONLY and
|
|
1174
|
+
* the HTTP method separately: brain_symbol.api_contract_path is an exact
|
|
1175
|
+
* match key shared across languages (doc-engine queries
|
|
1176
|
+
* s.api_contract_path=?), so embedding "GET " into it breaks every
|
|
1177
|
+
* cross-language route join against analyzers that store bare paths.
|
|
1178
|
+
*/
|
|
1179
|
+
extractKotlinApiContract(node, ctx) {
|
|
1180
|
+
const MAPPINGS = ["@GetMapping", "@PostMapping", "@PutMapping", "@DeleteMapping", "@PatchMapping", "@RequestMapping"];
|
|
1181
|
+
const candidates = [];
|
|
1182
|
+
const modifiersNode = this.findChildByType(node, ["modifiers"]);
|
|
1183
|
+
if (modifiersNode) {
|
|
1184
|
+
const stack = [modifiersNode];
|
|
1185
|
+
while (stack.length > 0) {
|
|
1186
|
+
const n = stack.pop();
|
|
1187
|
+
if (n.type === "annotation") candidates.push({ text: n.text, args: this.getValueArguments(n) });
|
|
1188
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1189
|
+
const c = n.namedChild(i);
|
|
1190
|
+
if (c) stack.push(c);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (candidates.length === 0 && modifiersNode.text.includes("Mapping")) {
|
|
1194
|
+
candidates.push({ text: modifiersNode.text });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1198
|
+
const mod = node.namedChild(i);
|
|
1199
|
+
if (mod?.type === "annotation") candidates.push({ text: mod.text, args: this.getValueArguments(mod) });
|
|
1200
|
+
}
|
|
1201
|
+
for (const { text, args } of candidates) {
|
|
1202
|
+
const shortName = this.kotlinShortAnnotationName(text);
|
|
1203
|
+
const mapping = MAPPINGS.find((m) => shortName === m.slice(1));
|
|
1204
|
+
if (!mapping) continue;
|
|
1205
|
+
const method = mapping.slice(1).replace("Mapping", "").toUpperCase();
|
|
1206
|
+
let path = "/";
|
|
1207
|
+
if (args && args.namedChildCount > 0) {
|
|
1208
|
+
let methodStr = "ANY";
|
|
1209
|
+
const paths = [];
|
|
1210
|
+
for (let j = 0; j < args.namedChildCount; j++) {
|
|
1211
|
+
const arg = args.namedChild(j);
|
|
1212
|
+
if (!arg || arg.type !== "value_argument") continue;
|
|
1213
|
+
let name = "";
|
|
1214
|
+
let expr = null;
|
|
1215
|
+
for (let k = 0; k < arg.namedChildCount; k++) {
|
|
1216
|
+
const c = arg.namedChild(k);
|
|
1217
|
+
if (!c) continue;
|
|
1218
|
+
if (c.type === "simple_identifier" && !expr) name = c.text;
|
|
1219
|
+
else if (!expr) expr = c;
|
|
1220
|
+
}
|
|
1221
|
+
const isRequestMethod = name === "method" || !name && stringIncludes(arg.text, "method");
|
|
1222
|
+
if (isRequestMethod) {
|
|
1223
|
+
const segs = arg.text.split(".");
|
|
1224
|
+
if (segs.length > 1) methodStr = segs[segs.length - 1].toUpperCase();
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
if (name && name !== "value" && name !== "path") continue;
|
|
1228
|
+
if (!expr) continue;
|
|
1229
|
+
if (expr.type === "string_literal") {
|
|
1230
|
+
paths.push(expr.text.split('"').join(""));
|
|
1231
|
+
} else if (expr.type === "collection_literal") {
|
|
1232
|
+
for (let k = 0; k < expr.namedChildCount; k++) {
|
|
1233
|
+
const e = expr.namedChild(k);
|
|
1234
|
+
if (e?.type === "string_literal") paths.push(e.text.split('"').join(""));
|
|
1235
|
+
}
|
|
1236
|
+
} else if (expr.type === "call_expression") {
|
|
1237
|
+
for (let k = 0; k < expr.namedChildCount; k++) {
|
|
1238
|
+
const inner = expr.namedChild(k);
|
|
1239
|
+
if (inner?.type !== "call_suffix") continue;
|
|
1240
|
+
for (let m2 = 0; m2 < inner.namedChildCount; m2++) {
|
|
1241
|
+
const va = inner.namedChild(m2);
|
|
1242
|
+
if (va?.type !== "value_arguments") continue;
|
|
1243
|
+
for (let m3 = 0; m3 < va.namedChildCount; m3++) {
|
|
1244
|
+
const e = va.namedChild(m3);
|
|
1245
|
+
if (e?.type === "string_literal") paths.push(e.text.split('"').join(""));
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (paths.length > 0) path = paths[0];
|
|
1252
|
+
if (methodStr !== "ANY") {
|
|
1253
|
+
return { path, method: methodStr };
|
|
1254
|
+
}
|
|
1255
|
+
return { path, method };
|
|
1256
|
+
}
|
|
1257
|
+
const afterName = text.slice(mapping.length);
|
|
1258
|
+
const firstQuote = afterName.indexOf('"');
|
|
1259
|
+
if (firstQuote !== -1) {
|
|
1260
|
+
const close = afterName.indexOf('"', firstQuote + 1);
|
|
1261
|
+
if (close !== -1) path = afterName.slice(firstQuote + 1, close);
|
|
1262
|
+
}
|
|
1263
|
+
if (mapping !== "@RequestMapping") return { path, method };
|
|
1264
|
+
const rmIdx = afterName.indexOf("RequestMethod.");
|
|
1265
|
+
if (rmIdx !== -1) {
|
|
1266
|
+
let end = rmIdx + "RequestMethod.".length;
|
|
1267
|
+
while (end < afterName.length) {
|
|
1268
|
+
const ch = afterName[end];
|
|
1269
|
+
const isAlpha = ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z";
|
|
1270
|
+
if (!isAlpha) break;
|
|
1271
|
+
end++;
|
|
1272
|
+
}
|
|
1273
|
+
const mname = afterName.slice(rmIdx + "RequestMethod.".length, end);
|
|
1274
|
+
if (mname) return { path, method: mname.toUpperCase() };
|
|
1275
|
+
}
|
|
1276
|
+
return { path, method: "ANY" };
|
|
1277
|
+
}
|
|
1278
|
+
return {};
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Extract Kotlin class with data classes, sealed classes, and inner classes
|
|
1282
|
+
*/
|
|
1283
|
+
extractClass(node, symbols, ctx, parentClass) {
|
|
1284
|
+
const nameNode = this.findNameNode(node);
|
|
1285
|
+
if (!nameNode) return;
|
|
1286
|
+
if (this.isKotlinTypeNameKeyword(nameNode.text)) return;
|
|
1287
|
+
const className = nameNode.text;
|
|
1288
|
+
const qualifiedName = this.buildQualifiedName(className, ctx, parentClass);
|
|
1289
|
+
const modifiers = this.extractModifiers(node, ctx);
|
|
1290
|
+
const superTypes = this.extractSuperTypes(node, ctx);
|
|
1291
|
+
const superTypeKinds = this.extractSuperTypesWithKinds(node);
|
|
1292
|
+
const typeParameters = this.extractTypeParameters(node, ctx);
|
|
1293
|
+
const annotations = this.extractAnnotations(node, ctx);
|
|
1294
|
+
const kdoc = this.extractKDocComment(node, ctx);
|
|
1295
|
+
const visibility = this.determineVisibility(modifiers, ctx);
|
|
1296
|
+
let isData = false, isSealed = false, isAbstract = false, isInner = false, isOpen = false;
|
|
1297
|
+
for (let i = 0; i < modifiers.length; i++) {
|
|
1298
|
+
if (modifiers[i] === "data") isData = true;
|
|
1299
|
+
else if (modifiers[i] === "sealed") isSealed = true;
|
|
1300
|
+
else if (modifiers[i] === "abstract") isAbstract = true;
|
|
1301
|
+
else if (modifiers[i] === "inner") isInner = true;
|
|
1302
|
+
else if (modifiers[i] === "open") isOpen = true;
|
|
1303
|
+
}
|
|
1304
|
+
const symbol = {
|
|
1305
|
+
name: className,
|
|
1306
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1307
|
+
range: this.symbolRange(node),
|
|
1308
|
+
// Scope = package + enclosing types, EXCLUDING this class's own name,
|
|
1309
|
+
// so a class is never a container of itself.
|
|
1310
|
+
scope: this.enclosingScope(ctx),
|
|
1311
|
+
visibility,
|
|
1312
|
+
isAbstract,
|
|
1313
|
+
ownerName: parentClass,
|
|
1314
|
+
ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
|
|
1315
|
+
decorators: annotations,
|
|
1316
|
+
comments: kdoc ? [kdoc] : [],
|
|
1317
|
+
metadata: {
|
|
1318
|
+
modifiers,
|
|
1319
|
+
// Short annotation names (last dotted segment) so framework pattern
|
|
1320
|
+
// detection matches fully-qualified annotations exactly like bare ones.
|
|
1321
|
+
annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
|
|
1322
|
+
superTypes,
|
|
1323
|
+
superTypesAreConstructorCalls: superTypeKinds.map((s) => s.isConstructorCall),
|
|
1324
|
+
// (#825): parallel line array — one line per supertype, in the
|
|
1325
|
+
// same order. Heritage edges take their line from here, not from the
|
|
1326
|
+
// class header (which names only the first supertype).
|
|
1327
|
+
superTypeLines: superTypeKinds.map((s) => s.line),
|
|
1328
|
+
inheritance: superTypes,
|
|
1329
|
+
typeParameters,
|
|
1330
|
+
isData,
|
|
1331
|
+
isDataClass: isData,
|
|
1332
|
+
isSealed,
|
|
1333
|
+
isInner,
|
|
1334
|
+
isOpen,
|
|
1335
|
+
methodCount: this.countMethods(node, ctx),
|
|
1336
|
+
propertyCount: this.countProperties(node, ctx),
|
|
1337
|
+
hasCompanionObject: this.hasCompanionObject(node, ctx),
|
|
1338
|
+
kotlinSpecific: true
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
symbols.push(symbol);
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Extract Kotlin object (singleton)
|
|
1345
|
+
*/
|
|
1346
|
+
extractObject(node, symbols, ctx, parentClass) {
|
|
1347
|
+
const nameNode = this.findNameNode(node);
|
|
1348
|
+
if (!nameNode) return;
|
|
1349
|
+
if (this.isKotlinTypeNameKeyword(nameNode.text)) return;
|
|
1350
|
+
const objectName = nameNode.text;
|
|
1351
|
+
const modifiers = this.extractModifiers(node, ctx);
|
|
1352
|
+
const superTypes = this.extractSuperTypes(node, ctx);
|
|
1353
|
+
const superTypeKinds = this.extractSuperTypesWithKinds(node);
|
|
1354
|
+
const annotations = this.extractAnnotations(node, ctx);
|
|
1355
|
+
const symbol = {
|
|
1356
|
+
name: objectName,
|
|
1357
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1358
|
+
// Using Class for objects
|
|
1359
|
+
range: this.symbolRange(node),
|
|
1360
|
+
scope: this.enclosingScope(ctx),
|
|
1361
|
+
visibility: this.determineVisibility(modifiers, ctx),
|
|
1362
|
+
ownerName: parentClass,
|
|
1363
|
+
ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
|
|
1364
|
+
decorators: annotations,
|
|
1365
|
+
metadata: {
|
|
1366
|
+
modifiers,
|
|
1367
|
+
annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
|
|
1368
|
+
superTypes,
|
|
1369
|
+
superTypesAreConstructorCalls: superTypeKinds.map((s) => s.isConstructorCall),
|
|
1370
|
+
// (#825): parallel line array — one line per supertype.
|
|
1371
|
+
superTypeLines: superTypeKinds.map((s) => s.line),
|
|
1372
|
+
isObject: true,
|
|
1373
|
+
isCompanionObject: (() => {
|
|
1374
|
+
for (let i = 0; i < modifiers.length; i++) if (modifiers[i] === "companion") return true;
|
|
1375
|
+
return false;
|
|
1376
|
+
})(),
|
|
1377
|
+
kotlinSpecific: true
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
symbols.push(symbol);
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* M49/M37 #867: the DECLARATION that owns an anonymous type — the innermost
|
|
1384
|
+
* enclosing `function_declaration` when the literal sits inside one, and the
|
|
1385
|
+
* enclosing class (`pClass`) otherwise.
|
|
1386
|
+
*
|
|
1387
|
+
* Ruled language-wide (not just here): `ownerName` = innermost enclosing
|
|
1388
|
+
* declaration. Three of the four JVM analyzers already did that before this
|
|
1389
|
+
* ruling — java's own source comment claimed to "mirror the kotlin family
|
|
1390
|
+
* convention exactly" while its code used the enclosing declaration, so the
|
|
1391
|
+
* comment and the majority disagreed and kotlin was the outlier. Lambdas and
|
|
1392
|
+
* `init` blocks are not declarations: a literal inside one is still owned by
|
|
1393
|
+
* the enclosing function/class, matching java (which pushes no context for a
|
|
1394
|
+
* lambda either).
|
|
1395
|
+
*/
|
|
1396
|
+
anonymousObjectOwner(node, pClass) {
|
|
1397
|
+
let ancestor = node.parent;
|
|
1398
|
+
while (ancestor && ancestor.type !== "source_file") {
|
|
1399
|
+
if (ancestor.type === "function_declaration") {
|
|
1400
|
+
const nameNode = this.findNameNode(ancestor);
|
|
1401
|
+
if (nameNode) return { name: nameNode.text, kind: ASTEngine.SymbolKind.Method };
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
if (ancestor.type === "class_declaration" || ancestor.type === "object_declaration") break;
|
|
1405
|
+
ancestor = ancestor.parent;
|
|
1406
|
+
}
|
|
1407
|
+
return { name: pClass, kind: pClass ? ASTEngine.SymbolKind.Class : void 0 };
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* M49/M37 #866: the name an anonymous object's members — and the calls made
|
|
1411
|
+
* from them — attribute to. ONE derivation, shared by the symbol emitter and
|
|
1412
|
+
* the call-source resolver, so a field symbol and its initialiser call can
|
|
1413
|
+
* never disagree about which type they belong to (java/groovy convention:
|
|
1414
|
+
* `anonymous_<SuperName>`).
|
|
1415
|
+
*/
|
|
1416
|
+
anonymousObjectName(node) {
|
|
1417
|
+
const deleg = this.findChildByType(node, ["delegation_specifier"]);
|
|
1418
|
+
let superName;
|
|
1419
|
+
if (deleg) {
|
|
1420
|
+
const findType = (n) => {
|
|
1421
|
+
if (n.type === "type_identifier") return n;
|
|
1422
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1423
|
+
const c = n.namedChild(i);
|
|
1424
|
+
if (c) {
|
|
1425
|
+
const r = findType(c);
|
|
1426
|
+
if (r) return r;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return null;
|
|
1430
|
+
};
|
|
1431
|
+
const tId = findType(deleg);
|
|
1432
|
+
if (tId) superName = tId.text;
|
|
1433
|
+
}
|
|
1434
|
+
return superName ? `anonymous_${superName}` : "anonymous_object";
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Extract Kotlin property with getters/setters and delegation
|
|
1438
|
+
*/
|
|
1439
|
+
extractProperty(node, symbols, ctx, parentClass, relationships) {
|
|
1440
|
+
let ancestor = node.parent;
|
|
1441
|
+
while (ancestor && ancestor.type !== "class_declaration" && ancestor.type !== "object_declaration" && ancestor.type !== "object_literal" && ancestor.type !== "source_file") {
|
|
1442
|
+
if (ancestor.type === "function_declaration") return;
|
|
1443
|
+
ancestor = ancestor.parent;
|
|
1444
|
+
}
|
|
1445
|
+
const declarationNode = this.findChildByType(node, ["variable_declaration"]);
|
|
1446
|
+
const nameNode = declarationNode ? this.findNameNode(declarationNode) : this.findNameNode(node);
|
|
1447
|
+
if (!nameNode) return;
|
|
1448
|
+
const propertyName = nameNode.text;
|
|
1449
|
+
const modifiers = this.extractModifiers(node, ctx);
|
|
1450
|
+
const propertyType = this.extractPropertyType(node, ctx);
|
|
1451
|
+
const annotations = this.extractAnnotations(node, ctx);
|
|
1452
|
+
const isVal = this.isValProperty(node);
|
|
1453
|
+
const isVar = this.isVarProperty(node);
|
|
1454
|
+
let isLateInit = false, isConst = false;
|
|
1455
|
+
for (let i = 0; i < modifiers.length; i++) {
|
|
1456
|
+
if (modifiers[i] === "lateinit") isLateInit = true;
|
|
1457
|
+
if (modifiers[i] === "const") isConst = true;
|
|
1458
|
+
}
|
|
1459
|
+
const hasDelegate = this.hasDelegate(node);
|
|
1460
|
+
let extensionReceiver;
|
|
1461
|
+
let extensionReceiverNode;
|
|
1462
|
+
{
|
|
1463
|
+
let receiverNode = node.childForFieldName("receiver");
|
|
1464
|
+
for (let i = 0; receiverNode == null && i < node.namedChildCount; i++) {
|
|
1465
|
+
const c = node.namedChild(i);
|
|
1466
|
+
if (c?.type === "simple_identifier") break;
|
|
1467
|
+
if (c?.type === "receiver_type" || c?.type === "user_type" || c?.type === "nullable_type" || c?.type === "type_identifier") {
|
|
1468
|
+
receiverNode = c;
|
|
1469
|
+
break;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (receiverNode) {
|
|
1473
|
+
const t = receiverNode.text;
|
|
1474
|
+
extensionReceiver = t.endsWith("?") ? t.slice(0, -1) : t;
|
|
1475
|
+
extensionReceiverNode = receiverNode;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
const symbol = {
|
|
1479
|
+
name: propertyName,
|
|
1480
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1481
|
+
range: this.symbolRange(node),
|
|
1482
|
+
scope: this.currentScope(ctx),
|
|
1483
|
+
visibility: this.determineVisibility(modifiers, ctx),
|
|
1484
|
+
decorators: annotations,
|
|
1485
|
+
returnType: propertyType,
|
|
1486
|
+
ownerName: parentClass,
|
|
1487
|
+
ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
|
|
1488
|
+
metadata: {
|
|
1489
|
+
modifiers,
|
|
1490
|
+
annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
|
|
1491
|
+
propertyType,
|
|
1492
|
+
isVal,
|
|
1493
|
+
isVar,
|
|
1494
|
+
isLateInit,
|
|
1495
|
+
isConst,
|
|
1496
|
+
hasDelegate,
|
|
1497
|
+
hasCustomGetter: this.hasCustomGetter(node),
|
|
1498
|
+
hasCustomSetter: this.hasCustomSetter(node),
|
|
1499
|
+
...extensionReceiver ? { isExtension: true, extensionReceiver } : {},
|
|
1500
|
+
kotlinSpecific: true
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
symbols.push(symbol);
|
|
1504
|
+
if (extensionReceiver && relationships) {
|
|
1505
|
+
relationships.push({
|
|
1506
|
+
source: propertyName,
|
|
1507
|
+
target: extensionReceiver,
|
|
1508
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1509
|
+
confidence: 0.9,
|
|
1510
|
+
// (#825): the receiver type is named by its receiver node.
|
|
1511
|
+
...this.lineField(extensionReceiverNode, node),
|
|
1512
|
+
metadata: {
|
|
1513
|
+
relationshipType: "kotlin_extension_property",
|
|
1514
|
+
isExtension: true
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Kotlin-specific framework detection
|
|
1521
|
+
*/
|
|
1522
|
+
detectKotlinFrameworks(content, symbols, imports) {
|
|
1523
|
+
const frameworks = {};
|
|
1524
|
+
const hasImport = (pkg) => {
|
|
1525
|
+
for (let i = 0; i < imports.length; i++) {
|
|
1526
|
+
const mod = imports[i].module;
|
|
1527
|
+
if (mod && (mod.startsWith(pkg) || mod === pkg)) return true;
|
|
1528
|
+
}
|
|
1529
|
+
return false;
|
|
1530
|
+
};
|
|
1531
|
+
if (hasImport("org.springframework") || hasImport("org.springframework.boot")) {
|
|
1532
|
+
frameworks["Spring Boot"] = true;
|
|
1533
|
+
frameworks["Spring Framework"] = true;
|
|
1534
|
+
}
|
|
1535
|
+
if (hasImport("io.ktor")) frameworks["Ktor"] = true;
|
|
1536
|
+
if (hasImport("io.micronaut")) frameworks["Micronaut"] = true;
|
|
1537
|
+
if (hasImport("io.reactivex.rxjava2") || hasImport("io.reactivex.rxjava3")) frameworks["RxJava"] = true;
|
|
1538
|
+
if (hasImport("org.hibernate") || hasImport("javax.persistence") || hasImport("jakarta.persistence"))
|
|
1539
|
+
frameworks["Hibernate / JPA"] = true;
|
|
1540
|
+
if (hasImport("kotlinx.coroutines")) frameworks["Kotlin Coroutines"] = true;
|
|
1541
|
+
if (hasImport("org.junit") || hasImport("kotlin.test")) frameworks["JUnit"] = true;
|
|
1542
|
+
if (hasImport("io.mockk") || hasImport("org.mockito")) frameworks["Mocking Framework"] = true;
|
|
1543
|
+
if (hasImport("androidx.compose")) frameworks["Jetpack Compose"] = true;
|
|
1544
|
+
if (hasImport("android.app") || hasImport("androidx")) frameworks["Android SDK"] = true;
|
|
1545
|
+
const annotationNames = /* @__PURE__ */ new Set();
|
|
1546
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1547
|
+
const anns = symbols[i].metadata?.annotations ?? [];
|
|
1548
|
+
for (let j = 0; j < anns.length; j++) annotationNames.add(anns[j]);
|
|
1549
|
+
}
|
|
1550
|
+
const hasAnnotation = (...names) => names.some((n) => annotationNames.has(n));
|
|
1551
|
+
if (hasAnnotation("Service", "RestController", "Controller", "Repository", "Component", "SpringBootApplication")) {
|
|
1552
|
+
frameworks["Spring Boot"] = true;
|
|
1553
|
+
frameworks["Spring Framework"] = true;
|
|
1554
|
+
}
|
|
1555
|
+
if (hasAnnotation("Entity", "Table")) frameworks["Hibernate / JPA"] = true;
|
|
1556
|
+
if (hasAnnotation("Dao", "Database")) frameworks["Room"] = true;
|
|
1557
|
+
if (hasAnnotation("Composable")) frameworks["Jetpack Compose"] = true;
|
|
1558
|
+
if (hasAnnotation("Test", "RunWith", "ExtendWith")) frameworks["JUnit"] = true;
|
|
1559
|
+
const result = [];
|
|
1560
|
+
for (const key in frameworks) {
|
|
1561
|
+
if (Object.prototype.hasOwnProperty.call(frameworks, key)) {
|
|
1562
|
+
result.push(key);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
return result;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Detect Kotlin-specific patterns
|
|
1569
|
+
*/
|
|
1570
|
+
detectKotlinPatterns(symbols, relationships, content, rootNode) {
|
|
1571
|
+
const patterns = [];
|
|
1572
|
+
let hasData = false, hasSealed = false, hasExtension = false, hasSuspend = false, hasDelegate = false, hasCompanion = false, hasOperator = false;
|
|
1573
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1574
|
+
const s = symbols[i];
|
|
1575
|
+
if (s.metadata?.isData) hasData = true;
|
|
1576
|
+
if (s.metadata?.isSealed) hasSealed = true;
|
|
1577
|
+
if (s.metadata?.isExtension) hasExtension = true;
|
|
1578
|
+
if (s.metadata?.isSuspend) hasSuspend = true;
|
|
1579
|
+
if (s.metadata?.hasDelegate) hasDelegate = true;
|
|
1580
|
+
if (s.metadata?.hasCompanionObject || s.metadata?.isCompanionObject) hasCompanion = true;
|
|
1581
|
+
if (s.metadata?.isOperator) hasOperator = true;
|
|
1582
|
+
}
|
|
1583
|
+
if (hasData) patterns.push("Data Classes");
|
|
1584
|
+
if (hasSealed) patterns.push("Sealed Classes");
|
|
1585
|
+
if (hasExtension) patterns.push("Extension Functions");
|
|
1586
|
+
if (hasSuspend) patterns.push("Coroutines");
|
|
1587
|
+
const astHas = /* @__PURE__ */ new Map();
|
|
1588
|
+
const scanAst = (n) => {
|
|
1589
|
+
switch (n.type) {
|
|
1590
|
+
case "check_expression":
|
|
1591
|
+
astHas.set("is", true);
|
|
1592
|
+
break;
|
|
1593
|
+
case "elvis_expression":
|
|
1594
|
+
astHas.set("elvis", true);
|
|
1595
|
+
break;
|
|
1596
|
+
case "lambda_literal":
|
|
1597
|
+
case "anonymous_function":
|
|
1598
|
+
astHas.set("lambda", true);
|
|
1599
|
+
break;
|
|
1600
|
+
}
|
|
1601
|
+
if (n.type === "navigation_expression") {
|
|
1602
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1603
|
+
if (n.namedChild(i)?.type === "quest") astHas.set("nullsafe", true);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
if (n.type === "postfix_expression" && n.text.endsWith("!!")) astHas.set("notnull", true);
|
|
1607
|
+
if (n.type === "delegation_specifier" && stringIncludes(n.text, " by ")) astHas.set("by", true);
|
|
1608
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1609
|
+
const c = n.namedChild(i);
|
|
1610
|
+
if (c) scanAst(c);
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
if (rootNode) scanAst(rootNode);
|
|
1614
|
+
if (astHas.get("nullsafe") || astHas.get("notnull") || astHas.get("elvis")) patterns.push("Null Safety");
|
|
1615
|
+
if (astHas.get("is")) patterns.push("Smart Casts");
|
|
1616
|
+
if (astHas.get("lambda")) patterns.push("Higher-Order Functions");
|
|
1617
|
+
if (hasDelegate || astHas.get("by")) {
|
|
1618
|
+
patterns.push("Delegation");
|
|
1619
|
+
patterns.push("Property Delegation");
|
|
1620
|
+
}
|
|
1621
|
+
if (stringIncludes(content, "buildString {")) patterns.push("DSL Patterns");
|
|
1622
|
+
if (stringIncludes(content, "@GetMapping") || stringIncludes(content, "@PostMapping") || stringIncludes(content, "@RequestMapping")) patterns.push("REST API");
|
|
1623
|
+
if (hasCompanion) patterns.push("Companion Objects");
|
|
1624
|
+
if (hasOperator) patterns.push("Operator Overloading");
|
|
1625
|
+
return patterns;
|
|
1626
|
+
}
|
|
1627
|
+
extractKotlinParameters(node, ctx) {
|
|
1628
|
+
const parameters = [];
|
|
1629
|
+
let paramsNode = this.findChildByType(node, ["function_value_parameters"]);
|
|
1630
|
+
if (!paramsNode) return parameters;
|
|
1631
|
+
const kids = [];
|
|
1632
|
+
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
1633
|
+
const c = paramsNode.namedChild(i);
|
|
1634
|
+
if (c) kids.push(c);
|
|
1635
|
+
}
|
|
1636
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1637
|
+
const paramNode = kids[i];
|
|
1638
|
+
if (paramNode.type !== "parameter") continue;
|
|
1639
|
+
const next = kids[i + 1];
|
|
1640
|
+
const defaultNode = next && next.type !== "parameter" ? next : null;
|
|
1641
|
+
const nameNode = this.findNameNode(paramNode);
|
|
1642
|
+
const typeNode = this.findChildByType(paramNode, ["user_type", "nullable_type", "type_identifier"]);
|
|
1643
|
+
if (nameNode && nameNode.text.length > 0) {
|
|
1644
|
+
parameters.push({
|
|
1645
|
+
name: nameNode.text,
|
|
1646
|
+
type: typeNode ? this.cleanKotlinType(typeNode.text) : void 0,
|
|
1647
|
+
isOptional: !!defaultNode,
|
|
1648
|
+
defaultValue: defaultNode ? defaultNode.text : void 0
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
return parameters;
|
|
1653
|
+
}
|
|
1654
|
+
buildQualifiedName(name, ctx, parentClass) {
|
|
1655
|
+
const scope = parentClass || this.currentScope(ctx);
|
|
1656
|
+
return scope ? `${scope}.${name}` : name;
|
|
1657
|
+
}
|
|
1658
|
+
currentScope(ctx) {
|
|
1659
|
+
return [...ctx.packageStack, ...ctx.classStack].join(".");
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Scope of a type declaration = package + ENCLOSING types, excluding the
|
|
1663
|
+
* declaration's own name (which has already been pushed onto classStack by
|
|
1664
|
+
* the time the extractor runs). A container must never contain itself.
|
|
1665
|
+
*/
|
|
1666
|
+
enclosingScope(ctx) {
|
|
1667
|
+
const enclosing = ctx.classStack.slice(0, ctx.classStack.length - 1);
|
|
1668
|
+
return [...ctx.packageStack, ...enclosing].join(".");
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* M14 #299 - evidence-tier classification for EVERY emitted relationship.
|
|
1672
|
+
* Tiers: structural (grammar clauses, conf 1.0) | usage_contextual (calls,
|
|
1673
|
+
* field access, companion-apply idioms) | import_based | reference_qualified
|
|
1674
|
+
* | inferred (sub-1.0 resolutions). Applied centrally at end of analyze()
|
|
1675
|
+
* so no emission site can forget it.
|
|
1676
|
+
*/
|
|
1677
|
+
classifyEvidenceTier(relationships) {
|
|
1678
|
+
for (const r of relationships) {
|
|
1679
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
1680
|
+
if (md.evidenceClass) continue;
|
|
1681
|
+
const t = String(r.type ?? r.relationshipType ?? "");
|
|
1682
|
+
const conf = typeof r.confidence === "number" ? r.confidence : 1;
|
|
1683
|
+
if (t === "contains" || t === "exports") {
|
|
1684
|
+
md.evidenceClass = md.relationshipType === "compose_ui_tree" ? "heuristic" : "structural";
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (t === "references") {
|
|
1688
|
+
md.evidenceClass = md.isImport || md.isImportDep ? "import_based" : "reference_qualified";
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
if (t === "imports") {
|
|
1692
|
+
md.evidenceClass = "import_based";
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (t === "extends" || t === "implements") {
|
|
1696
|
+
md.evidenceClass = conf >= 1 ? "structural" : "inferred";
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
if (t === "overrides") {
|
|
1700
|
+
md.evidenceClass = conf >= 1 ? "structural" : conf >= 0.85 ? "usage_contextual" : "inferred";
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
if (t === "instantiates") {
|
|
1704
|
+
md.evidenceClass = md.isNew || conf >= 0.9 ? "structural" : "usage_contextual";
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
md.evidenceClass = "usage_contextual";
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* (#825): the `line_number` field for a relationship, taken from the node
|
|
1712
|
+
* that CARRIES THE NAME the edge is about. java and scala settled this rule
|
|
1713
|
+
* (the line belongs to the name, never to the construct that contains it) and
|
|
1714
|
+
* kotlin shares their node shapes, so it is the same contract, ported.
|
|
1715
|
+
*
|
|
1716
|
+
* Returns nothing at all when no node is available — never 0: #820/#821, a
|
|
1717
|
+
* stored 0 is a claim that the relationship occurs on line 0.
|
|
1718
|
+
*/
|
|
1719
|
+
lineField(nameNode, fallback) {
|
|
1720
|
+
const n = nameNode ?? fallback;
|
|
1721
|
+
return n ? { line_number: n.startPosition.row + 1 } : {};
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Is this `call_expression` a KOTLIN 2.2 CONTEXT-PARAMETER HEADER rather than a
|
|
1725
|
+
* call?
|
|
1726
|
+
*
|
|
1727
|
+
* `context(session: FirSession)` is a DECLARATION FORM with no node of its own
|
|
1728
|
+
* in this grammar, so it arrives as an ordinary call and was published as one:
|
|
1729
|
+
* `calls[Global -> context]` at 0.7 for every header — a real statement
|
|
1730
|
+
* attributed to a symbol no kotlin file declares. Measured on a 400-file
|
|
1731
|
+
* stratified draw of the pinned fixture corpus (kotlin@9b80c8ff): 20 headers in 4
|
|
1732
|
+
* files, 28 of the sample's 524 `Global` call edges (the other 496 are Gradle
|
|
1733
|
+
* `.kts` script statements, which have no owner either).
|
|
1734
|
+
*
|
|
1735
|
+
* The guard tests for an ERROR inside the `call_suffix`, NOT for the name
|
|
1736
|
+
* `context` alone, and that distinction is the whole rule: a `value_argument`
|
|
1737
|
+
* cannot carry an annotation, so the parameter list does not parse (the dumped
|
|
1738
|
+
* tree ends `ERROR ": FirSession"`), while a real `context(session)`
|
|
1739
|
+
* INVOCATION parses cleanly. `context` is a SOFT keyword — `fun context(x: Int)`
|
|
1740
|
+
* is a legal declaration and a call to it is a real call — so a name-only test
|
|
1741
|
+
* would delete live edges, which `kotlin/context-parameters` pins against.
|
|
1742
|
+
*/
|
|
1743
|
+
/**
|
|
1744
|
+
* Is this declaration LOCAL — written inside a BODY rather than in a class body
|
|
1745
|
+
* or at file level?
|
|
1746
|
+
*
|
|
1747
|
+
* The test walks up to the nearest enclosing position and answers at the FIRST
|
|
1748
|
+
* of them: a body (`function_body`, `lambda_literal`, `anonymous_function`) is
|
|
1749
|
+
* a local; a declaration position (`class_body`, `enum_class_body`,
|
|
1750
|
+
* `source_file`) is not. `anonymous_initializer` is treated as a declaration
|
|
1751
|
+
* position on purpose — an `init` block's locals are M60 K2's ruling, not this
|
|
1752
|
+
* one, and folding them in here would move a pin that already exists.
|
|
1753
|
+
*
|
|
1754
|
+
* This is deliberately ancestry-only: the same question "is this declaration
|
|
1755
|
+
* only reachable from inside a body" is what `kotlinDeclarationPlacement`
|
|
1756
|
+
* answers for the exports surface, and that helper is line-keyed because it
|
|
1757
|
+
* works on symbol ranges after the tree is gone. Both exist because they run at
|
|
1758
|
+
* different stages; a declaration that is local by ancestry is local by line.
|
|
1759
|
+
*/
|
|
1760
|
+
/**
|
|
1761
|
+
* The file's module identity: its first TOP-LEVEL TYPE, or null.
|
|
1762
|
+
*
|
|
1763
|
+
* One predicate, three readers (the `references` source rewrite, the `imports`
|
|
1764
|
+
* source, and the `exports` unit source) so the three cannot disagree about
|
|
1765
|
+
* which name stands for the compilation unit — the failure mode that made the
|
|
1766
|
+
* old file emit one `exports` edge sourced at the package, a second at `(file)`
|
|
1767
|
+
* and a third spelling in the fixtures.
|
|
1768
|
+
*
|
|
1769
|
+
* A top-level type is a class/interface/enum with no owner, which excludes a
|
|
1770
|
+
* nested type, a member, a top-level function and a top-level property. Phantom
|
|
1771
|
+
* symbols (data-class generated members) and anonymous objects are excluded
|
|
1772
|
+
* explicitly: neither can be named by an import.
|
|
1773
|
+
*/
|
|
1774
|
+
primaryTopLevelType(symbols) {
|
|
1775
|
+
return symbols.find((s) => {
|
|
1776
|
+
if (s.ownerName) return false;
|
|
1777
|
+
if (s.metadata?.isPhantom || s.metadata?.isAnonymousObject) return false;
|
|
1778
|
+
return s.kind === ASTEngine.SymbolKind.Class || s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Enum;
|
|
1779
|
+
}) ?? null;
|
|
1780
|
+
}
|
|
1781
|
+
isKotlinLocalDeclaration(node) {
|
|
1782
|
+
let p = node.parent;
|
|
1783
|
+
while (p) {
|
|
1784
|
+
const t = p.type;
|
|
1785
|
+
if (t === "class_body" || t === "enum_class_body" || t === "source_file" || t === "anonymous_initializer") return false;
|
|
1786
|
+
if (t === "function_body" || t === "lambda_literal" || t === "anonymous_function") return true;
|
|
1787
|
+
p = p.parent;
|
|
1788
|
+
}
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
isKotlinContextParameterHeader(node) {
|
|
1792
|
+
const nameChild = node.namedChild(0);
|
|
1793
|
+
if (!nameChild || nameChild.type !== "simple_identifier" || nameChild.text !== "context") return false;
|
|
1794
|
+
const suffix = this.findChildByType(node, ["call_suffix"]);
|
|
1795
|
+
return !!suffix && suffix.hasError;
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* (#825): the node that NAMES a kotlin call.
|
|
1799
|
+
*
|
|
1800
|
+
* Measured from the pinned wasm rather than assumed: a kotlin
|
|
1801
|
+
* `call_expression`'s named children are `navigation_expression` and
|
|
1802
|
+
* `call_suffix`, and the navigation expression SPANS the receiver — for
|
|
1803
|
+
* `s\n .trim()\n .toUpperCase()` the node opens on line 5 while
|
|
1804
|
+
* `.toUpperCase()` (the name, and the `()` that follows it) is written on 7.
|
|
1805
|
+
* The last identifier inside the navigation expression is the callee leaf,
|
|
1806
|
+
* which is where the call is written.
|
|
1807
|
+
*/
|
|
1808
|
+
calleeLeafNode(callNode, leafText) {
|
|
1809
|
+
const nav = callNode.namedChild(0);
|
|
1810
|
+
if (!nav) return null;
|
|
1811
|
+
let last = null;
|
|
1812
|
+
let legacyLast = null;
|
|
1813
|
+
let leafHit = null;
|
|
1814
|
+
const visit = (n, recordLeaf) => {
|
|
1815
|
+
if (n.type === "simple_identifier" || n.type === "type_identifier") {
|
|
1816
|
+
last = n;
|
|
1817
|
+
if (recordLeaf && leafText !== void 0 && n.text === leafText) leafHit = n;
|
|
1818
|
+
}
|
|
1819
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1820
|
+
const c = n.namedChild(i);
|
|
1821
|
+
if (c) visit(c, recordLeaf);
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
visit(nav, true);
|
|
1825
|
+
legacyLast = last;
|
|
1826
|
+
if (!leafHit && leafText !== void 0) visit(callNode, true);
|
|
1827
|
+
if (!leafHit && leafText !== void 0) {
|
|
1828
|
+
const navChild = this.findChildByType(callNode, ["navigation_expression"]);
|
|
1829
|
+
if (navChild && navChild.id !== nav.id) visit(navChild, true);
|
|
1830
|
+
}
|
|
1831
|
+
return leafHit ?? legacyLast ?? last ?? nav;
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* (#825): the node whose start is the DECLARATION's own line.
|
|
1835
|
+
*
|
|
1836
|
+
* An annotation is a modifier — `@Test`/`@Bean` decorate the declaration, they
|
|
1837
|
+
* are not where it is written. Measured on the 40-file kotlin oracle sample:
|
|
1838
|
+
* with the node's own start, `contains` and `overrides` edges landed on the
|
|
1839
|
+
* ANNOTATION line for every annotated member (102 of the sample's 241
|
|
1840
|
+
* inconsistent claims started at an `@Test`/`@Bean` line). java measured the
|
|
1841
|
+
* same defect at 27% of its symbols and scala settled the contract; kotlin
|
|
1842
|
+
* inherits one rule, not a third variant.
|
|
1843
|
+
*
|
|
1844
|
+
* The symbol range and the edges that name it move together, so a symbol and
|
|
1845
|
+
* its `contains`/`overrides` lines can never disagree.
|
|
1846
|
+
*/
|
|
1847
|
+
declarationLineNode(node) {
|
|
1848
|
+
const name = this.findNameNode(node);
|
|
1849
|
+
if (name) return name;
|
|
1850
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1851
|
+
const c = node.namedChild(i);
|
|
1852
|
+
if (!c) continue;
|
|
1853
|
+
if (c.type === "modifiers" || c.type === "annotation") continue;
|
|
1854
|
+
if (c.type === "line_comment" || c.type === "multiline_comment") continue;
|
|
1855
|
+
return c;
|
|
1856
|
+
}
|
|
1857
|
+
return node;
|
|
1858
|
+
}
|
|
1859
|
+
/** The range of a SYMBOL: annotations skipped (see declarationLineNode). */
|
|
1860
|
+
symbolRange(node) {
|
|
1861
|
+
const decl = this.declarationLineNode(node);
|
|
1862
|
+
const base = this.nodeToRange(node);
|
|
1863
|
+
return {
|
|
1864
|
+
...base,
|
|
1865
|
+
startLine: decl.startPosition.row + 1,
|
|
1866
|
+
startColumn: decl.startPosition.column + 1,
|
|
1867
|
+
startIndex: decl.startIndex
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
nodeToRange(node) {
|
|
1871
|
+
return {
|
|
1872
|
+
startLine: node.startPosition.row + 1,
|
|
1873
|
+
startColumn: node.startPosition.column + 1,
|
|
1874
|
+
endLine: node.endPosition.row + 1,
|
|
1875
|
+
endColumn: node.endPosition.column + 1,
|
|
1876
|
+
startIndex: node.startIndex,
|
|
1877
|
+
endIndex: node.endIndex
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
// Additional helper methods (stubs for now)...
|
|
1881
|
+
/**
|
|
1882
|
+
* Kotlin Interface Extraction
|
|
1883
|
+
*/
|
|
1884
|
+
extractInterface(node, symbols, ctx, parentClass) {
|
|
1885
|
+
const nameNode = this.findNameNode(node);
|
|
1886
|
+
if (!nameNode) return;
|
|
1887
|
+
if (this.isKotlinTypeNameKeyword(nameNode.text)) return;
|
|
1888
|
+
const interfaceName = nameNode.text;
|
|
1889
|
+
const modifiers = this.extractModifiers(node, ctx);
|
|
1890
|
+
const superTypes = this.extractSuperTypes(node, ctx);
|
|
1891
|
+
const superTypeKinds = this.extractSuperTypesWithKinds(node);
|
|
1892
|
+
const typeParameters = this.extractTypeParameters(node, ctx);
|
|
1893
|
+
const annotations = this.extractAnnotations(node, ctx);
|
|
1894
|
+
const symbol = {
|
|
1895
|
+
name: interfaceName,
|
|
1896
|
+
kind: ASTEngine.SymbolKind.Interface,
|
|
1897
|
+
range: this.symbolRange(node),
|
|
1898
|
+
scope: this.enclosingScope(ctx),
|
|
1899
|
+
visibility: this.determineVisibility(modifiers, ctx),
|
|
1900
|
+
isAbstract: true,
|
|
1901
|
+
// All interfaces are abstract
|
|
1902
|
+
ownerName: parentClass,
|
|
1903
|
+
ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
|
|
1904
|
+
decorators: annotations,
|
|
1905
|
+
metadata: {
|
|
1906
|
+
modifiers,
|
|
1907
|
+
annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
|
|
1908
|
+
superTypes,
|
|
1909
|
+
superTypesAreConstructorCalls: superTypeKinds.map((s) => s.isConstructorCall),
|
|
1910
|
+
// (#825): parallel line array — one line per supertype, in the
|
|
1911
|
+
// same order. Heritage edges take their line from here, not from the
|
|
1912
|
+
// class header (which names only the first supertype).
|
|
1913
|
+
superTypeLines: superTypeKinds.map((s) => s.line),
|
|
1914
|
+
inheritance: superTypes,
|
|
1915
|
+
typeParameters,
|
|
1916
|
+
methodCount: this.countMethods(node, ctx),
|
|
1917
|
+
isKotlinInterface: true,
|
|
1918
|
+
kotlinSpecific: true
|
|
1919
|
+
}
|
|
1920
|
+
};
|
|
1921
|
+
symbols.push(symbol);
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Kotlin Enum Class Extraction
|
|
1925
|
+
*/
|
|
1926
|
+
extractEnumClass(node, symbols, ctx, parentClass) {
|
|
1927
|
+
const nameNode = this.findNameNode(node);
|
|
1928
|
+
if (!nameNode) return;
|
|
1929
|
+
if (this.isKotlinTypeNameKeyword(nameNode.text)) return;
|
|
1930
|
+
const enumName = nameNode.text;
|
|
1931
|
+
const modifiers = this.extractModifiers(node, ctx);
|
|
1932
|
+
const annotations = this.extractAnnotations(node, ctx);
|
|
1933
|
+
const enumValues = this.extractKotlinEnumValues(node);
|
|
1934
|
+
const superTypes = this.extractSuperTypes(node, ctx);
|
|
1935
|
+
const superTypeKinds = this.extractSuperTypesWithKinds(node);
|
|
1936
|
+
const symbol = {
|
|
1937
|
+
name: enumName,
|
|
1938
|
+
kind: ASTEngine.SymbolKind.Enum,
|
|
1939
|
+
range: this.symbolRange(node),
|
|
1940
|
+
scope: this.enclosingScope(ctx),
|
|
1941
|
+
visibility: this.determineVisibility(modifiers, ctx),
|
|
1942
|
+
ownerName: parentClass,
|
|
1943
|
+
ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
|
|
1944
|
+
decorators: annotations,
|
|
1945
|
+
metadata: {
|
|
1946
|
+
modifiers,
|
|
1947
|
+
annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
|
|
1948
|
+
superTypes,
|
|
1949
|
+
superTypesAreConstructorCalls: superTypeKinds.map((s) => s.isConstructorCall),
|
|
1950
|
+
// (#825): parallel line array — one line per supertype.
|
|
1951
|
+
superTypeLines: superTypeKinds.map((s) => s.line),
|
|
1952
|
+
enumValues,
|
|
1953
|
+
valueCount: enumValues.length,
|
|
1954
|
+
hasEnumMethods: this.countMethods(node, ctx) > 0,
|
|
1955
|
+
isKotlinEnum: true,
|
|
1956
|
+
kotlinSpecific: true
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
symbols.push(symbol);
|
|
1960
|
+
const enumBody = this.findChildByType(node, ["enum_class_body", "class_body"]);
|
|
1961
|
+
const entryNodes = [];
|
|
1962
|
+
if (enumBody) {
|
|
1963
|
+
for (let i = 0; i < enumBody.namedChildCount; i++) {
|
|
1964
|
+
const c = enumBody.namedChild(i);
|
|
1965
|
+
if (c?.type === "enum_entry") entryNodes.push(c);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
for (let i = 0; i < enumValues.length; i++) {
|
|
1969
|
+
const enumValue = enumValues[i];
|
|
1970
|
+
const entryNode = entryNodes[i] ?? node;
|
|
1971
|
+
symbols.push({
|
|
1972
|
+
name: enumValue,
|
|
1973
|
+
kind: ASTEngine.SymbolKind.EnumMember,
|
|
1974
|
+
range: this.symbolRange(entryNode),
|
|
1975
|
+
scope: this.currentScope(ctx),
|
|
1976
|
+
visibility: "public",
|
|
1977
|
+
ownerName: enumName,
|
|
1978
|
+
ownerKind: ASTEngine.SymbolKind.Enum,
|
|
1979
|
+
metadata: {
|
|
1980
|
+
isEnumValue: true,
|
|
1981
|
+
kotlinSpecific: true
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Kotlin Constructor Extraction
|
|
1988
|
+
*/
|
|
1989
|
+
extractConstructor(node, symbols, ctx, parentClass) {
|
|
1990
|
+
const parameters = this.extractKotlinParameters(node, ctx);
|
|
1991
|
+
const modifiers = this.extractModifiers(node, ctx);
|
|
1992
|
+
const annotations = this.extractAnnotations(node, ctx);
|
|
1993
|
+
const isPrimaryConstructor = node.type === "primary_constructor";
|
|
1994
|
+
const constructorProperties = [];
|
|
1995
|
+
if (isPrimaryConstructor) {
|
|
1996
|
+
parameters.length = 0;
|
|
1997
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1998
|
+
const paramNode = node.namedChild(i);
|
|
1999
|
+
if (paramNode && paramNode.type === "class_parameter") {
|
|
2000
|
+
const nameNode = this.findNameNode(paramNode);
|
|
2001
|
+
const typeNode = this.findChildByType(paramNode, ["user_type", "nullable_type", "type_identifier"]);
|
|
2002
|
+
let exists = false;
|
|
2003
|
+
if (nameNode && nameNode.text.length > 0) {
|
|
2004
|
+
for (let j = 0; j < parameters.length; j++) {
|
|
2005
|
+
if (parameters[j].name === nameNode.text) {
|
|
2006
|
+
exists = true;
|
|
2007
|
+
break;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (nameNode && !exists) {
|
|
2012
|
+
parameters.push({
|
|
2013
|
+
name: nameNode.text,
|
|
2014
|
+
type: typeNode ? this.cleanKotlinType(typeNode.text) : "Any"
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
const bindingKind = this.findChildByType(paramNode, ["binding_pattern_kind"]);
|
|
2018
|
+
if (bindingKind && (bindingKind.text === "val" || bindingKind.text === "var")) {
|
|
2019
|
+
constructorProperties.push(paramNode);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
let hasDefaults = false;
|
|
2025
|
+
for (let i = 0; i < parameters.length; i++) {
|
|
2026
|
+
if (parameters[i].defaultValue !== void 0) {
|
|
2027
|
+
hasDefaults = true;
|
|
2028
|
+
break;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(parentClass)) return;
|
|
2032
|
+
const constructorName = parentClass;
|
|
2033
|
+
let ctorDeclNode = node;
|
|
2034
|
+
let ancestor = node.parent;
|
|
2035
|
+
while (ancestor && ancestor.type === "class_body") ancestor = ancestor.parent;
|
|
2036
|
+
if (ancestor && (ancestor.type === "class_declaration" || ancestor.type === "object_declaration")) ctorDeclNode = ancestor;
|
|
2037
|
+
const symbol = {
|
|
2038
|
+
name: constructorName,
|
|
2039
|
+
kind: ASTEngine.SymbolKind.Constructor,
|
|
2040
|
+
range: this.symbolRange(ctorDeclNode),
|
|
2041
|
+
scope: this.currentScope(ctx),
|
|
2042
|
+
visibility: this.determineVisibility(modifiers, ctx),
|
|
2043
|
+
parameters,
|
|
2044
|
+
decorators: annotations,
|
|
2045
|
+
ownerName: parentClass,
|
|
2046
|
+
ownerKind: parentClass ? ASTEngine.SymbolKind.Class : void 0,
|
|
2047
|
+
metadata: {
|
|
2048
|
+
modifiers,
|
|
2049
|
+
annotations: annotations.map((a) => this.kotlinShortAnnotationName(a)),
|
|
2050
|
+
isPrimaryConstructor,
|
|
2051
|
+
parameterCount: parameters.length,
|
|
2052
|
+
hasDefaultParameters: hasDefaults,
|
|
2053
|
+
kotlinSpecific: true
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
symbols.push(symbol);
|
|
2057
|
+
if (parentClass) {
|
|
2058
|
+
for (let i = 0; i < constructorProperties.length; i++) {
|
|
2059
|
+
const paramNode = constructorProperties[i];
|
|
2060
|
+
const nameNode = this.findNameNode(paramNode);
|
|
2061
|
+
const typeNode = this.findChildByType(paramNode, ["user_type", "nullable_type", "type_identifier"]);
|
|
2062
|
+
const bindingKind = this.findChildByType(paramNode, ["binding_pattern_kind"]);
|
|
2063
|
+
if (!nameNode || !bindingKind) continue;
|
|
2064
|
+
symbols.push({
|
|
2065
|
+
name: nameNode.text,
|
|
2066
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
2067
|
+
range: this.symbolRange(paramNode),
|
|
2068
|
+
scope: this.currentScope(ctx),
|
|
2069
|
+
visibility: this.determineVisibility(this.extractModifiers(paramNode, ctx), ctx),
|
|
2070
|
+
ownerName: parentClass,
|
|
2071
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
2072
|
+
returnType: typeNode ? this.cleanKotlinType(typeNode.text) : void 0,
|
|
2073
|
+
metadata: {
|
|
2074
|
+
isVal: bindingKind.text === "val",
|
|
2075
|
+
isVar: bindingKind.text === "var",
|
|
2076
|
+
isConstructorProperty: true,
|
|
2077
|
+
kotlinSpecific: true
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Kotlin Package Extraction
|
|
2085
|
+
*/
|
|
2086
|
+
extractPackage(node, ctx) {
|
|
2087
|
+
const packageNameNode = this.findChildByType(node, ["identifier", "simple_identifier", "type_identifier"]);
|
|
2088
|
+
if (packageNameNode) {
|
|
2089
|
+
const packageName = packageNameNode.text;
|
|
2090
|
+
ctx.packageStack = packageName.split(".");
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Kotlin Import Extraction
|
|
2095
|
+
*/
|
|
2096
|
+
extractImport(node, ctx) {
|
|
2097
|
+
void ctx;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Kotlin Annotation Extraction
|
|
2101
|
+
*/
|
|
2102
|
+
extractAnnotation(node, symbols, ctx) {
|
|
2103
|
+
void symbols;
|
|
2104
|
+
void ctx;
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Kotlin Modifier Extraction
|
|
2108
|
+
*/
|
|
2109
|
+
extractModifiers(node, ctx) {
|
|
2110
|
+
const modifiers = [];
|
|
2111
|
+
const modifiersNode = this.findChildByType(node, ["modifiers"]);
|
|
2112
|
+
if (modifiersNode) {
|
|
2113
|
+
for (let j = 0; j < modifiersNode.namedChildCount; j++) {
|
|
2114
|
+
const modifier = modifiersNode.namedChild(j);
|
|
2115
|
+
if (modifier && modifier.type !== "annotation") {
|
|
2116
|
+
modifiers.push(modifier.text.trim());
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return modifiers;
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Kotlin Return Type Extraction
|
|
2124
|
+
*/
|
|
2125
|
+
extractReturnType(node, ctx) {
|
|
2126
|
+
const typeNode = this.findChildByType(node, ["user_type", "nullable_type", "function_type", "type_identifier"]);
|
|
2127
|
+
if (typeNode) {
|
|
2128
|
+
return this.cleanKotlinType(typeNode.text);
|
|
2129
|
+
}
|
|
2130
|
+
const functionBody = this.findChildByType(node, ["function_body"]);
|
|
2131
|
+
if (functionBody && !functionBody.text.startsWith("{")) {
|
|
2132
|
+
return void 0;
|
|
2133
|
+
}
|
|
2134
|
+
return "Unit";
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Kotlin Receiver Type Extraction (for extension functions)
|
|
2138
|
+
*/
|
|
2139
|
+
extractReceiverType(node, ctx) {
|
|
2140
|
+
const fieldReceiver = node.childForFieldName("receiver");
|
|
2141
|
+
if (fieldReceiver) return this.cleanKotlinType(fieldReceiver.text);
|
|
2142
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2143
|
+
const c = node.namedChild(i);
|
|
2144
|
+
if (c?.type === "simple_identifier") break;
|
|
2145
|
+
if (c?.type === "receiver_type" || c?.type === "user_type" || c?.type === "nullable_type" || c?.type === "type_identifier") {
|
|
2146
|
+
return this.cleanKotlinType(c.text);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return void 0;
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Kotlin Function Call Detection
|
|
2153
|
+
* Analyzes Kotlin method calls, extension functions, and Android/Spring patterns
|
|
2154
|
+
*/
|
|
2155
|
+
findKotlinFunctionCalls(node, ctx) {
|
|
2156
|
+
const calls = [];
|
|
2157
|
+
const stack = [node];
|
|
2158
|
+
while (stack.length > 0) {
|
|
2159
|
+
const n = stack.pop();
|
|
2160
|
+
if (this.isKotlinContextParameterHeader(n)) continue;
|
|
2161
|
+
if (n.type === "call_expression") {
|
|
2162
|
+
const callName = this.buildKotlinCallExpression(n);
|
|
2163
|
+
if (callName && !this.isKotlinBuiltin(callName)) {
|
|
2164
|
+
calls.push(callName);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2168
|
+
const child = n.namedChild(i);
|
|
2169
|
+
if (child) stack.push(child);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return calls;
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Kotlin Annotations Extraction (multiple)
|
|
2176
|
+
*/
|
|
2177
|
+
extractAnnotations(node, ctx) {
|
|
2178
|
+
const annotations = [];
|
|
2179
|
+
const modifiers = this.extractModifiers(node, ctx);
|
|
2180
|
+
if (modifiers.includes("expect")) annotations.push("@expect");
|
|
2181
|
+
if (modifiers.includes("actual")) annotations.push("@actual");
|
|
2182
|
+
const collect = (n) => {
|
|
2183
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2184
|
+
const c = n.namedChild(i);
|
|
2185
|
+
if (c && c.type === "annotation") {
|
|
2186
|
+
annotations.push(c.text.startsWith("@") ? c.text.slice(1) : c.text);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
if (node) {
|
|
2191
|
+
collect(node);
|
|
2192
|
+
const modifiersNode = this.findChildByType(node, ["modifiers"]);
|
|
2193
|
+
if (modifiersNode) collect(modifiersNode);
|
|
2194
|
+
}
|
|
2195
|
+
return annotations;
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Kotlin KDoc Comment Extraction
|
|
2199
|
+
*/
|
|
2200
|
+
extractKDocComment(node, ctx) {
|
|
2201
|
+
const parent = node.parent;
|
|
2202
|
+
if (!parent) return void 0;
|
|
2203
|
+
let selfIdx = -1;
|
|
2204
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
2205
|
+
const c = parent.namedChild(i);
|
|
2206
|
+
if (c && c.startIndex === node.startIndex && c.endIndex === node.endIndex) {
|
|
2207
|
+
selfIdx = i;
|
|
2208
|
+
break;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
for (let i = selfIdx - 1; i >= 0; i--) {
|
|
2212
|
+
const sibling = parent.namedChild(i);
|
|
2213
|
+
if (!sibling) continue;
|
|
2214
|
+
if ((sibling.type === "multiline_comment" || sibling.type === "line_comment") && sibling.text.startsWith("/**")) {
|
|
2215
|
+
return sibling.text;
|
|
2216
|
+
}
|
|
2217
|
+
break;
|
|
2218
|
+
}
|
|
2219
|
+
return void 0;
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Kotlin Visibility Determination
|
|
2223
|
+
*/
|
|
2224
|
+
determineVisibility(modifiers, ctx) {
|
|
2225
|
+
for (let i = 0; i < modifiers.length; i++) {
|
|
2226
|
+
const mod = modifiers[i];
|
|
2227
|
+
if (mod === "private") return "private";
|
|
2228
|
+
if (mod === "protected") return "protected";
|
|
2229
|
+
if (mod === "internal") return "internal";
|
|
2230
|
+
}
|
|
2231
|
+
return "public";
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Kotlin Function Complexity Calculation
|
|
2235
|
+
*/
|
|
2236
|
+
calculateFunctionComplexity(node, ctx) {
|
|
2237
|
+
let complexity = 1;
|
|
2238
|
+
const stack = [node];
|
|
2239
|
+
while (stack.length > 0) {
|
|
2240
|
+
const n = stack.pop();
|
|
2241
|
+
if (n.type === "if_expression" || n.type === "when_entry" || // per-case, matching java/TS switch_case granularity
|
|
2242
|
+
n.type === "for_statement" || n.type === "while_statement" || n.type === "do_while_statement" || n.type === "catch_block" || n.type === "elvis_expression") {
|
|
2243
|
+
complexity++;
|
|
2244
|
+
} else if (n.type === "conjunction_expression" || n.type === "disjunction_expression") {
|
|
2245
|
+
complexity++;
|
|
2246
|
+
}
|
|
2247
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2248
|
+
const child = n.namedChild(i);
|
|
2249
|
+
if (child) stack.push(child);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
return complexity;
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Kotlin Super Types Extraction
|
|
2256
|
+
*/
|
|
2257
|
+
/**
|
|
2258
|
+
* Extract the clean base type name of every supertype. A delegation_specifier
|
|
2259
|
+
* contains either a user_type directly (Repository<User>) or a
|
|
2260
|
+
* constructor_invocation (BaseService<User>()) wrapping one. The base name
|
|
2261
|
+
* strips type arguments and nullability so relationship targets are valid
|
|
2262
|
+
* symbol leaves (pool-worker's noise filter rejects targets containing <>).
|
|
2263
|
+
*/
|
|
2264
|
+
extractSuperTypes(node, ctx) {
|
|
2265
|
+
return this.extractSuperTypesWithKinds(node).map((s) => s.name);
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Kotlin semantics (Kotlin docs, Inheritance): a supertype written with a
|
|
2269
|
+
* constructor invocation `Base()` is an extended CLASS; a bare type `Repo`
|
|
2270
|
+
* is an IMPLEMENTED interface. Position alone is wrong - a class whose
|
|
2271
|
+
* first (or only) supertype is an interface was labelled extends.
|
|
2272
|
+
*
|
|
2273
|
+
* (#825): each entry also carries the line the SUPERtype is NAMED on.
|
|
2274
|
+
* A kotlin supertype list wraps (`class A :\n B(),\n C`), so the
|
|
2275
|
+
* class header's line names only the first entry. Same rule as java's
|
|
2276
|
+
* `extractJavaInheritance` and scala's `extractSupertypes`.
|
|
2277
|
+
*/
|
|
2278
|
+
extractSuperTypesWithKinds(node) {
|
|
2279
|
+
const out = [];
|
|
2280
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2281
|
+
const c = node.namedChild(i);
|
|
2282
|
+
if (c?.type === "delegation_specifier") {
|
|
2283
|
+
const invocation = this.findChildByType(c, ["constructor_invocation"]);
|
|
2284
|
+
let userType = this.findChildByType(c, ["user_type"]);
|
|
2285
|
+
if (!userType && invocation) userType = this.findChildByType(invocation, ["user_type"]);
|
|
2286
|
+
if (userType) {
|
|
2287
|
+
out.push({
|
|
2288
|
+
name: this.cleanKotlinBaseType(userType.text),
|
|
2289
|
+
isConstructorCall: !!invocation,
|
|
2290
|
+
line: userType.startPosition.row + 1
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
return out;
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Reduce a (possibly generic/nullable) type to its base name:
|
|
2299
|
+
* "BaseService<User>" → "BaseService", "User?" → "User",
|
|
2300
|
+
* "Map<String, List<Int>>" → "Map".
|
|
2301
|
+
*/
|
|
2302
|
+
cleanKotlinBaseType(typeText) {
|
|
2303
|
+
let t = typeText;
|
|
2304
|
+
while (t.endsWith("?")) t = t.slice(0, -1);
|
|
2305
|
+
const lt = t.indexOf("<");
|
|
2306
|
+
if (lt !== -1) t = t.slice(0, lt);
|
|
2307
|
+
return t.trim();
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Kotlin Type Parameters Extraction
|
|
2311
|
+
*/
|
|
2312
|
+
/**
|
|
2313
|
+
* Positional `value_arguments` lookup. The pinned kotlin wasm exposes only
|
|
2314
|
+
* four fields (receiver/alternative/condition/consequence — verified
|
|
2315
|
+
* 2026-09-16), none of which is `value_arguments`, so this helper remains the
|
|
2316
|
+
* mechanism for annotation/route argument extraction (M14.1). The earlier
|
|
2317
|
+
* "fieldCount=0" claim in this comment was stale: the 1852ea1 rebuild added
|
|
2318
|
+
* field info, but only `receiver` among the fields this analyzer would want.
|
|
2319
|
+
*/
|
|
2320
|
+
getValueArguments(node) {
|
|
2321
|
+
const stack = [node];
|
|
2322
|
+
while (stack.length > 0) {
|
|
2323
|
+
const n = stack.pop();
|
|
2324
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2325
|
+
const c = n.namedChild(i);
|
|
2326
|
+
if (!c) continue;
|
|
2327
|
+
if (c.type === "value_arguments") return c;
|
|
2328
|
+
stack.push(c);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
return void 0;
|
|
2332
|
+
}
|
|
2333
|
+
// KAD-MR3 K13 (M49.K1): getCallFunctionName, extractKotlinAnnotationArguments
|
|
2334
|
+
// and extractKotlinAnnotationTarget were dead (grep-proven zero call sites) —
|
|
2335
|
+
// deleted. extractKotlinAnnotationArguments's arg-walk lives on in the
|
|
2336
|
+
// E6 route extractor as a real consumer.
|
|
2337
|
+
extractTypeParameters(node, ctx) {
|
|
2338
|
+
const typeParameters = [];
|
|
2339
|
+
const typeParamList = this.findChildByType(node, ["type_parameters"]);
|
|
2340
|
+
if (typeParamList) {
|
|
2341
|
+
for (let i = 0; i < typeParamList.namedChildCount; i++) {
|
|
2342
|
+
const typeParam = typeParamList.namedChild(i);
|
|
2343
|
+
if (typeParam?.type === "type_parameter") {
|
|
2344
|
+
const paramName = typeParam.namedChildren.find(
|
|
2345
|
+
(c) => c.type === "simple_identifier" || c.type === "type_identifier"
|
|
2346
|
+
)?.text;
|
|
2347
|
+
if (paramName) {
|
|
2348
|
+
typeParameters.push(paramName);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return typeParameters;
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Kotlin Method Counting — DIRECT members only.
|
|
2357
|
+
* KAD-MR3 K14 (M49.K1): the subtree-wide count misattributed nested
|
|
2358
|
+
* structure — a nested class's members inflated the OUTER class's counts
|
|
2359
|
+
* (class Outer { class Nested { fun nm() } } reported methodCount=2 for
|
|
2360
|
+
* Outer). Probed containers: class/object bodies are class_body, enum
|
|
2361
|
+
* members live in enum_class_body.
|
|
2362
|
+
*/
|
|
2363
|
+
countMethods(node, ctx) {
|
|
2364
|
+
let count = 0;
|
|
2365
|
+
for (const bt of ["class_body", "enum_class_body"]) {
|
|
2366
|
+
const body = this.findChildByType(node, [bt]);
|
|
2367
|
+
if (!body) continue;
|
|
2368
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
2369
|
+
const m = body.namedChild(i);
|
|
2370
|
+
if (m?.type === "function_declaration") count++;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
return count;
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Kotlin Property Counting — DIRECT members only (KAD-MR3 K14, see
|
|
2377
|
+
* countMethods). Companion-object properties belong to the companion, not
|
|
2378
|
+
* the outer class.
|
|
2379
|
+
*/
|
|
2380
|
+
countProperties(node, ctx) {
|
|
2381
|
+
let count = 0;
|
|
2382
|
+
for (const bt of ["class_body", "enum_class_body"]) {
|
|
2383
|
+
const body = this.findChildByType(node, [bt]);
|
|
2384
|
+
if (!body) continue;
|
|
2385
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
2386
|
+
const m = body.namedChild(i);
|
|
2387
|
+
if (m?.type === "property_declaration") count++;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return count;
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Kotlin Companion Object Detection — DIRECT member of THIS type only
|
|
2394
|
+
* (KAD-MR3 K14: the subtree-wide scan credited a nested class's companion
|
|
2395
|
+
* to the outer class).
|
|
2396
|
+
*/
|
|
2397
|
+
hasCompanionObject(node, ctx) {
|
|
2398
|
+
for (const bt of ["class_body", "enum_class_body"]) {
|
|
2399
|
+
const body = this.findChildByType(node, [bt]);
|
|
2400
|
+
if (!body) continue;
|
|
2401
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
2402
|
+
const m = body.namedChild(i);
|
|
2403
|
+
if (m?.type !== "companion_object" && m?.type !== "object_declaration") continue;
|
|
2404
|
+
if (m.type === "companion_object") return true;
|
|
2405
|
+
const modifiers = this.extractModifiers(m, ctx);
|
|
2406
|
+
for (let j = 0; j < modifiers.length; j++) {
|
|
2407
|
+
if (modifiers[j] === "companion") return true;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return false;
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Kotlin Property Type Extraction
|
|
2415
|
+
*/
|
|
2416
|
+
extractPropertyType(node, ctx) {
|
|
2417
|
+
const declarationNode = this.findChildByType(node, ["variable_declaration"]);
|
|
2418
|
+
const typeNode = this.findChildByType(node, ["user_type", "nullable_type", "function_type", "type_identifier"]) || (declarationNode ? this.findChildByType(declarationNode, ["user_type", "nullable_type", "function_type", "type_identifier"]) : null);
|
|
2419
|
+
if (typeNode) {
|
|
2420
|
+
return this.cleanKotlinType(typeNode.text);
|
|
2421
|
+
}
|
|
2422
|
+
const LITERAL_TYPES = ["integer_literal", "real_literal", "string_literal", "character_literal", "boolean_literal", "call_expression"];
|
|
2423
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2424
|
+
const c = node.namedChild(i);
|
|
2425
|
+
if (c && LITERAL_TYPES.indexOf(c.type) !== -1) {
|
|
2426
|
+
return this.inferKotlinTypeFromInitializer(c);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return void 0;
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Kotlin Val Property Detection
|
|
2433
|
+
*/
|
|
2434
|
+
isValProperty(node) {
|
|
2435
|
+
const binding = this.findChildByType(node, ["binding_pattern_kind"]);
|
|
2436
|
+
if (binding) return binding.text === "val";
|
|
2437
|
+
return stringIncludes(node.text, "val ") && !stringIncludes(node.text, "var ");
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Kotlin Var Property Detection
|
|
2441
|
+
*/
|
|
2442
|
+
isVarProperty(node) {
|
|
2443
|
+
const binding = this.findChildByType(node, ["binding_pattern_kind"]);
|
|
2444
|
+
if (binding) return binding.text === "var";
|
|
2445
|
+
return stringIncludes(node.text, "var ");
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Kotlin Property Delegation Detection
|
|
2449
|
+
*/
|
|
2450
|
+
hasDelegate(node) {
|
|
2451
|
+
return !!this.findChildByType(node, ["property_delegate"]);
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Kotlin Custom Getter Detection
|
|
2455
|
+
*/
|
|
2456
|
+
hasCustomGetter(node) {
|
|
2457
|
+
return this.hasPropertyAccessor(node, "getter");
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Kotlin accessor run: the pinned grammar emits accessors as SIBLINGS of
|
|
2461
|
+
* property_declaration (class_body > [property_declaration, getter,
|
|
2462
|
+
* setter]) — probe-verified, and the run is CONTIGUOUS (a setter follows
|
|
2463
|
+
* the getter, not the declaration). Text matching fabricated both ("get()")
|
|
2464
|
+
* hit `val t = m.get()` (a plain call named get); "set("+"value") hit
|
|
2465
|
+
* `val u = s.set(1, value)`). Span-locate the declaration among its
|
|
2466
|
+
* parent's children, then walk the contiguous accessor run that follows.
|
|
2467
|
+
*/
|
|
2468
|
+
hasPropertyAccessor(node, accessorType) {
|
|
2469
|
+
const parent = node.parent;
|
|
2470
|
+
if (!parent) return false;
|
|
2471
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
2472
|
+
const c = parent.namedChild(i);
|
|
2473
|
+
if (c && c.startIndex === node.startIndex && c.endIndex === node.endIndex) {
|
|
2474
|
+
for (let j = i + 1; j < parent.namedChildCount; j++) {
|
|
2475
|
+
const sib = parent.namedChild(j);
|
|
2476
|
+
if (sib?.type === "getter" || sib?.type === "setter") {
|
|
2477
|
+
if (sib.type === accessorType) return true;
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
break;
|
|
2481
|
+
}
|
|
2482
|
+
return false;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
return false;
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Kotlin Custom Setter Detection
|
|
2489
|
+
*/
|
|
2490
|
+
hasCustomSetter(node) {
|
|
2491
|
+
return this.hasPropertyAccessor(node, "setter");
|
|
2492
|
+
}
|
|
2493
|
+
// ======================== ADDITIONAL KOTLIN HELPER METHODS ========================
|
|
2494
|
+
/**
|
|
2495
|
+
* Declared-type reference graph (M14). Emits References edges from the
|
|
2496
|
+
* enclosing function/class to every named type used in a declaration
|
|
2497
|
+
* position. Exact-triple dedupe guards against nested-declaration overlap
|
|
2498
|
+
* (e.g. lambda params inside a property initializer).
|
|
2499
|
+
*/
|
|
2500
|
+
extractDeclaredTypeRefs(rootNode, relationships) {
|
|
2501
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2502
|
+
const collectTypeNames = (n, out) => {
|
|
2503
|
+
if (n.type === "type_identifier") {
|
|
2504
|
+
if (n.text.length === 0) return;
|
|
2505
|
+
if (!out.some((x) => x.name === n.text)) out.push({ name: n.text, line: n.startPosition.row + 1 });
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2509
|
+
const c = n.namedChild(i);
|
|
2510
|
+
if (c) collectTypeNames(c, out);
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
const resolveSource = (start) => {
|
|
2514
|
+
let p = start;
|
|
2515
|
+
while (p) {
|
|
2516
|
+
if (p.type === "function_declaration" || p.type === "class_declaration" || p.type === "object_declaration") {
|
|
2517
|
+
const name = this.findNameNode(p);
|
|
2518
|
+
if (name) return name.text;
|
|
2519
|
+
}
|
|
2520
|
+
const topLevelProp = this.kotlinTopLevelPropertyName(p);
|
|
2521
|
+
if (topLevelProp) return topLevelProp;
|
|
2522
|
+
p = p.parent;
|
|
2523
|
+
}
|
|
2524
|
+
return "Global";
|
|
2525
|
+
};
|
|
2526
|
+
const emit = (decl, typeRoots, kind, relType = "declared_type", sourceOverride) => {
|
|
2527
|
+
const source = sourceOverride ?? resolveSource(decl);
|
|
2528
|
+
for (const tr of typeRoots) {
|
|
2529
|
+
if (!tr) continue;
|
|
2530
|
+
const names = [];
|
|
2531
|
+
collectTypeNames(tr, names);
|
|
2532
|
+
for (const { name: t, line } of names) {
|
|
2533
|
+
const key = `${source}|${t}|${kind}`;
|
|
2534
|
+
if (emitted.has(key)) continue;
|
|
2535
|
+
emitted.add(key);
|
|
2536
|
+
relationships.push({
|
|
2537
|
+
source,
|
|
2538
|
+
target: t,
|
|
2539
|
+
type: ASTEngine.RelationshipType.References,
|
|
2540
|
+
confidence: 0.95,
|
|
2541
|
+
// (#825): the line the referenced TYPE is named on.
|
|
2542
|
+
line_number: line,
|
|
2543
|
+
metadata: { relationshipType: relType, declaredOn: decl.type }
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
const walk = (n) => {
|
|
2549
|
+
switch (n.type) {
|
|
2550
|
+
case "class_parameter":
|
|
2551
|
+
case "parameter": {
|
|
2552
|
+
emit(n, [n], n.type);
|
|
2553
|
+
break;
|
|
2554
|
+
}
|
|
2555
|
+
case "variable_declaration": {
|
|
2556
|
+
emit(n, [n], "variable_declaration");
|
|
2557
|
+
break;
|
|
2558
|
+
}
|
|
2559
|
+
case "as_expression":
|
|
2560
|
+
case "check_expression": {
|
|
2561
|
+
if (n.namedChildCount >= 2) {
|
|
2562
|
+
const right = n.namedChild(n.namedChildCount - 1);
|
|
2563
|
+
emit(n, [right], n.type, n.type);
|
|
2564
|
+
}
|
|
2565
|
+
break;
|
|
2566
|
+
}
|
|
2567
|
+
case "callable_reference": {
|
|
2568
|
+
const tId = this.findNameNode(n);
|
|
2569
|
+
if (tId) emit(n, [tId], "callable_reference", "callable_reference");
|
|
2570
|
+
break;
|
|
2571
|
+
}
|
|
2572
|
+
case "type_alias": {
|
|
2573
|
+
const tName = this.findNameNode(n);
|
|
2574
|
+
if (tName) {
|
|
2575
|
+
const rhs = [];
|
|
2576
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2577
|
+
const c = n.namedChild(i);
|
|
2578
|
+
if (c && c.type !== "type_identifier" && c.type !== "type_parameters") rhs.push(c);
|
|
2579
|
+
}
|
|
2580
|
+
emit(n, rhs, "type_alias", "type_alias", tName.text);
|
|
2581
|
+
}
|
|
2582
|
+
break;
|
|
2583
|
+
}
|
|
2584
|
+
case "object_literal": {
|
|
2585
|
+
const deleg = this.findChildByType(n, ["delegation_specifier"]);
|
|
2586
|
+
if (deleg) emit(n, [deleg], "object_literal", "object_literal");
|
|
2587
|
+
break;
|
|
2588
|
+
}
|
|
2589
|
+
case "function_declaration": {
|
|
2590
|
+
let sawParams = false;
|
|
2591
|
+
let sawName = false;
|
|
2592
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2593
|
+
const c = n.namedChild(i);
|
|
2594
|
+
if (!c) continue;
|
|
2595
|
+
if (c.type === "function_value_parameters") {
|
|
2596
|
+
sawParams = true;
|
|
2597
|
+
continue;
|
|
2598
|
+
}
|
|
2599
|
+
if (c.type === "simple_identifier" || c.type === "type_identifier") {
|
|
2600
|
+
if (!sawName && !sawParams && !c.text.startsWith("`")) {
|
|
2601
|
+
if (i > 0 && n.namedChild(i - 1)?.type === "user_type") {
|
|
2602
|
+
emit(n, [c], "receiver_type");
|
|
2603
|
+
}
|
|
2604
|
+
sawName = true;
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
continue;
|
|
2608
|
+
}
|
|
2609
|
+
if (c.type === "receiver_type" || c.type === "user_type" || c.type === "nullable_type" || c.type === "function_type") {
|
|
2610
|
+
if (!sawName && !sawParams) {
|
|
2611
|
+
emit(n, [c], "receiver_type");
|
|
2612
|
+
sawName = true;
|
|
2613
|
+
} else if (sawParams) {
|
|
2614
|
+
emit(n, [c], "return_type", "return_type");
|
|
2615
|
+
break;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
break;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2623
|
+
const c = n.namedChild(i);
|
|
2624
|
+
if (c) walk(c);
|
|
2625
|
+
}
|
|
2626
|
+
};
|
|
2627
|
+
walk(rootNode);
|
|
2628
|
+
}
|
|
2629
|
+
/**
|
|
2630
|
+
* M14 #419 K1/K4: expression-level Uses edges.
|
|
2631
|
+
* - indexing_expression / directly_assignable_expression (with an
|
|
2632
|
+
* indexing_suffix child): `xs[i]` reads and writes USE the indexed
|
|
2633
|
+
* expression — previously only contains edges were emitted, so the array
|
|
2634
|
+
* reference was invisible to the use graph.
|
|
2635
|
+
* - interpolated_identifier: "hi $name" string templates USE the
|
|
2636
|
+
* referenced identifier. Kotlin's grammar produces a real template node
|
|
2637
|
+
* (unlike groovy's opaque GStrings — the groovy S1 ruling), so the read
|
|
2638
|
+
* is visible and must be captured.
|
|
2639
|
+
* - CONV-K-1 (#740): BARE and DOTTED member reads. Property reads are the
|
|
2640
|
+
* most common read form in kotlin and produced NOTHING — `return x` and
|
|
2641
|
+
* `return this.x` both emitted zero uses (probe-confirmed live), while
|
|
2642
|
+
* only the indexing (K1) and template (K4) forms were ever claimed. Gate
|
|
2643
|
+
* A: the name must resolve to a DECLARED MEMBER of the enclosing type.
|
|
2644
|
+
* Gate B (CGD15): a name the enclosing function/lambda binds as a
|
|
2645
|
+
* parameter or local is not a member read — without it `val x = 9` inside
|
|
2646
|
+
* a method fabricates uses(method -> x) on a property it only shadows.
|
|
2647
|
+
* Both gates are the real work; the emission is a line.
|
|
2648
|
+
* Deduped per (source, target) so repeated indexing in one function emits
|
|
2649
|
+
* one honest edge, not a fan of identical edges.
|
|
2650
|
+
*/
|
|
2651
|
+
collectExpressionUses(rootNode, relationships, symbols) {
|
|
2652
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2653
|
+
const membersByOwner = /* @__PURE__ */ new Map();
|
|
2654
|
+
for (const s of symbols) {
|
|
2655
|
+
if (!s.ownerName) continue;
|
|
2656
|
+
if (s.kind === ASTEngine.SymbolKind.Constructor || s.kind === ASTEngine.SymbolKind.Module) continue;
|
|
2657
|
+
let set = membersByOwner.get(s.ownerName);
|
|
2658
|
+
if (!set) {
|
|
2659
|
+
set = /* @__PURE__ */ new Set();
|
|
2660
|
+
membersByOwner.set(s.ownerName, set);
|
|
2661
|
+
}
|
|
2662
|
+
set.add(s.name);
|
|
2663
|
+
}
|
|
2664
|
+
const resolveSource = (start) => {
|
|
2665
|
+
let p = start;
|
|
2666
|
+
while (p) {
|
|
2667
|
+
if (p.type === "secondary_constructor" || p.type === "primary_constructor") {
|
|
2668
|
+
return this.kotlinConstructorOwnerName(p) ?? "";
|
|
2669
|
+
}
|
|
2670
|
+
if (p.type === "function_declaration" || p.type === "class_declaration" || p.type === "object_declaration") {
|
|
2671
|
+
const name = this.findNameNode(p);
|
|
2672
|
+
if (name) return name.text;
|
|
2673
|
+
}
|
|
2674
|
+
const topLevelProp = this.kotlinTopLevelPropertyName(p);
|
|
2675
|
+
if (topLevelProp) return topLevelProp;
|
|
2676
|
+
p = p.parent;
|
|
2677
|
+
}
|
|
2678
|
+
return "Global";
|
|
2679
|
+
};
|
|
2680
|
+
const emitUse = (n, target) => {
|
|
2681
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(target)) return;
|
|
2682
|
+
const source = resolveSource(n.parent);
|
|
2683
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(source)) return;
|
|
2684
|
+
const key = `${source}|${target}|uses`;
|
|
2685
|
+
if (emitted.has(key)) return;
|
|
2686
|
+
emitted.add(key);
|
|
2687
|
+
relationships.push({
|
|
2688
|
+
source,
|
|
2689
|
+
target,
|
|
2690
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2691
|
+
confidence: 0.9,
|
|
2692
|
+
// (#825): a read happens where the name is written. `n` is the
|
|
2693
|
+
// expression the walk found, which is the site that carries it.
|
|
2694
|
+
line_number: n.startPosition.row + 1,
|
|
2695
|
+
metadata: { relationshipType: "expression_use" }
|
|
2696
|
+
});
|
|
2697
|
+
};
|
|
2698
|
+
const resolveOwner = (start) => {
|
|
2699
|
+
let p = start;
|
|
2700
|
+
while (p) {
|
|
2701
|
+
if (p.type === "class_declaration" || p.type === "object_declaration") {
|
|
2702
|
+
const nm = this.findNameNode(p);
|
|
2703
|
+
return nm ? nm.text : null;
|
|
2704
|
+
}
|
|
2705
|
+
p = p.parent;
|
|
2706
|
+
}
|
|
2707
|
+
return null;
|
|
2708
|
+
};
|
|
2709
|
+
const boundCache = /* @__PURE__ */ new Map();
|
|
2710
|
+
const boundOf = (scope) => {
|
|
2711
|
+
const hit = boundCache.get(scope.id);
|
|
2712
|
+
if (hit) return hit;
|
|
2713
|
+
const out = /* @__PURE__ */ new Set();
|
|
2714
|
+
const w = [scope];
|
|
2715
|
+
while (w.length > 0) {
|
|
2716
|
+
const m = w.pop();
|
|
2717
|
+
if (m.type === "simple_identifier") {
|
|
2718
|
+
const pt = m.parent?.type ?? "";
|
|
2719
|
+
if (pt === "parameter" || pt === "class_parameter" || pt === "variable_declaration") {
|
|
2720
|
+
out.add(m.text);
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
for (let i = m.namedChildCount - 1; i >= 0; i--) {
|
|
2724
|
+
const c = m.namedChild(i);
|
|
2725
|
+
if (c) w.push(c);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
boundCache.set(scope.id, out);
|
|
2729
|
+
return out;
|
|
2730
|
+
};
|
|
2731
|
+
const isLocallyBound = (n, name) => {
|
|
2732
|
+
let p = n.parent;
|
|
2733
|
+
while (p) {
|
|
2734
|
+
if (p.type === "function_declaration" || p.type === "lambda_literal" || p.type === "primary_constructor" || p.type === "secondary_constructor") {
|
|
2735
|
+
if (boundOf(p).has(name)) return true;
|
|
2736
|
+
}
|
|
2737
|
+
if (p.type === "class_declaration" || p.type === "object_declaration") break;
|
|
2738
|
+
p = p.parent;
|
|
2739
|
+
}
|
|
2740
|
+
return false;
|
|
2741
|
+
};
|
|
2742
|
+
const isWriteLhs = (n) => {
|
|
2743
|
+
let a = n.parent;
|
|
2744
|
+
while (a && (a.type === "directly_assignable_expression" || a.type === "navigation_expression" || a.type === "navigation_suffix")) a = a.parent;
|
|
2745
|
+
if (a && a.type === "assignment") {
|
|
2746
|
+
const lhs = a.namedChild(0);
|
|
2747
|
+
return !!lhs && lhs.startIndex <= n.startIndex && lhs.endIndex >= n.endIndex;
|
|
2748
|
+
}
|
|
2749
|
+
return false;
|
|
2750
|
+
};
|
|
2751
|
+
const NOT_A_READ = /* @__PURE__ */ new Set([
|
|
2752
|
+
"parameter",
|
|
2753
|
+
"class_parameter",
|
|
2754
|
+
"variable_declaration",
|
|
2755
|
+
"type_identifier",
|
|
2756
|
+
"user_type",
|
|
2757
|
+
"nullable_type",
|
|
2758
|
+
"import_header",
|
|
2759
|
+
"package_header",
|
|
2760
|
+
"function_declaration",
|
|
2761
|
+
"class_declaration",
|
|
2762
|
+
"object_declaration",
|
|
2763
|
+
"property_declaration",
|
|
2764
|
+
"value_parameter"
|
|
2765
|
+
]);
|
|
2766
|
+
const emitMemberRead = (n, target) => {
|
|
2767
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(target)) return;
|
|
2768
|
+
const source = resolveSource(n.parent);
|
|
2769
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(source)) return;
|
|
2770
|
+
const key = `${source}|${target}|uses`;
|
|
2771
|
+
if (emitted.has(key)) return;
|
|
2772
|
+
emitted.add(key);
|
|
2773
|
+
relationships.push({
|
|
2774
|
+
source,
|
|
2775
|
+
target,
|
|
2776
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2777
|
+
confidence: 0.6,
|
|
2778
|
+
// (#825): a bare member read IS its identifier node.
|
|
2779
|
+
line_number: n.startPosition.row + 1,
|
|
2780
|
+
metadata: { isBareRead: true, relationshipType: "expression_use" }
|
|
2781
|
+
});
|
|
2782
|
+
};
|
|
2783
|
+
const walk = (n) => {
|
|
2784
|
+
if (n.type === "indexing_expression" || n.type === "directly_assignable_expression") {
|
|
2785
|
+
const recv = n.namedChild(0);
|
|
2786
|
+
let hasIndexing = false;
|
|
2787
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2788
|
+
if (n.namedChild(i)?.type === "indexing_suffix") {
|
|
2789
|
+
hasIndexing = true;
|
|
2790
|
+
break;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
const parent = n.parent;
|
|
2794
|
+
const lhs = parent && parent.type === "assignment" ? parent.namedChild(0) : null;
|
|
2795
|
+
const isWrite = !!lhs && lhs.startIndex === n.startIndex && lhs.endIndex === n.endIndex;
|
|
2796
|
+
if (recv && hasIndexing && !isWrite) {
|
|
2797
|
+
const receiverPath = this.kotlinIdentifierPath(recv);
|
|
2798
|
+
if (receiverPath) emitUse(n, receiverPath);
|
|
2799
|
+
else if (this.kotlinNameShapedText(recv.text)) emitUse(n, recv.text);
|
|
2800
|
+
}
|
|
2801
|
+
} else if (n.type === "interpolated_identifier") {
|
|
2802
|
+
emitUse(n, n.text);
|
|
2803
|
+
} else if (n.type === "simple_identifier") {
|
|
2804
|
+
const txt = n.text;
|
|
2805
|
+
const pt = n.parent?.type ?? "";
|
|
2806
|
+
const owner = resolveOwner(n.parent);
|
|
2807
|
+
const isMember = !!owner && (membersByOwner.get(owner)?.has(txt) ?? false);
|
|
2808
|
+
if (!NOT_A_READ.has(pt) && pt !== "call_expression" && !isWriteLhs(n) && !isLocallyBound(n, txt) && isMember) {
|
|
2809
|
+
emitMemberRead(n, txt);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2813
|
+
const c = n.namedChild(i);
|
|
2814
|
+
if (c) walk(c);
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
walk(rootNode);
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Kotlin Call Expression Builder
|
|
2821
|
+
*/
|
|
2822
|
+
/**
|
|
2823
|
+
* M60 (#924, kotlin half): canonicalize the PUBLISHED name surface.
|
|
2824
|
+
*
|
|
2825
|
+
* Kotlin has no symbol factory (symbols are pushed inline by the extractors),
|
|
2826
|
+
* so the strip cannot be applied at construction the way swift's createSymbol
|
|
2827
|
+
* does it; it is applied here, at the publish boundary — after every analysis
|
|
2828
|
+
* pass and before exports/imports are derived from these same objects, so all
|
|
2829
|
+
* published spellings agree with each other (a `contains` target still equals
|
|
2830
|
+
* the symbol name it points at) and with what a reference can spell.
|
|
2831
|
+
*
|
|
2832
|
+
* Internal keys are deliberately NOT rewritten past this point: the analysis
|
|
2833
|
+
* resolves references in the source's own spelling (a kotlin reference to a
|
|
2834
|
+
* quoted name is itself quoted), so the quoted form is the correct lookup key
|
|
2835
|
+
* inside the analysis and the stripped form is the correct published name.
|
|
2836
|
+
*/
|
|
2837
|
+
canonicalizePublishedNames(symbols, relationships) {
|
|
2838
|
+
for (const s of symbols) {
|
|
2839
|
+
if (typeof s.name === "string") s.name = stripKotlinQuoting(s.name);
|
|
2840
|
+
if (typeof s.ownerName === "string") s.ownerName = stripKotlinQuoting(s.ownerName);
|
|
2841
|
+
if (typeof s.id === "string") s.id = stripKotlinQuoting(s.id);
|
|
2842
|
+
}
|
|
2843
|
+
for (const r of relationships) {
|
|
2844
|
+
if (typeof r.source === "string") r.source = stripKotlinQuoting(r.source);
|
|
2845
|
+
if (typeof r.target === "string") r.target = stripKotlinQuoting(r.target);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
buildKotlinCallExpression(node) {
|
|
2849
|
+
switch (node.type) {
|
|
2850
|
+
case "simple_identifier":
|
|
2851
|
+
case "identifier":
|
|
2852
|
+
return node.text;
|
|
2853
|
+
case "navigation_expression":
|
|
2854
|
+
return this.buildKotlinNavigationExpression(node);
|
|
2855
|
+
case "call_expression": {
|
|
2856
|
+
const nav = this.findChildByType(node, ["navigation_expression"]);
|
|
2857
|
+
if (nav) return this.buildKotlinNavigationExpression(nav);
|
|
2858
|
+
const functionName = this.findChildByType(node, ["simple_identifier", "identifier"]);
|
|
2859
|
+
return functionName ? functionName.text : node.text.split("(")[0];
|
|
2860
|
+
}
|
|
2861
|
+
default:
|
|
2862
|
+
return node.text;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Kotlin Navigation Expression Builder (for method calls)
|
|
2867
|
+
*/
|
|
2868
|
+
/**
|
|
2869
|
+
* M60 (#924, kotlin E half): the NAME of an expression, when it has one.
|
|
2870
|
+
*
|
|
2871
|
+
* The uses walk published an indexing receiver's raw TEXT, so a read of
|
|
2872
|
+
* `dest.selectAll().where { dest.id eq 1 }.single()[dest.id]` was published as
|
|
2873
|
+
* that whole expression — arguments, lambda body and brackets included. Every
|
|
2874
|
+
* one of the 27 such strings the census collected on kotlin is an indexing
|
|
2875
|
+
* receiver (script/m14/tmp/c924-name-surface-probe.ts). A name is the chain of
|
|
2876
|
+
* IDENTIFIERS along the callee/receiver spine; arguments and lambda bodies are
|
|
2877
|
+
* never part of it. Returns undefined when the spine carries no identifier, and
|
|
2878
|
+
* the caller then decides by the old text's SHAPE (see kotlinNameShapedText).
|
|
2879
|
+
*/
|
|
2880
|
+
kotlinIdentifierPath(node) {
|
|
2881
|
+
if (!node) return void 0;
|
|
2882
|
+
switch (node.type) {
|
|
2883
|
+
case "simple_identifier":
|
|
2884
|
+
case "identifier":
|
|
2885
|
+
case "type_identifier":
|
|
2886
|
+
return stripKotlinQuoting(node.text);
|
|
2887
|
+
case "navigation_expression": {
|
|
2888
|
+
const receiver = node.namedChild(0);
|
|
2889
|
+
let selector = node.namedChild(node.namedChildCount - 1);
|
|
2890
|
+
if (!receiver || !selector) return void 0;
|
|
2891
|
+
if (selector.type === "navigation_suffix") {
|
|
2892
|
+
const inner = selector.namedChild(selector.namedChildCount - 1);
|
|
2893
|
+
if (!inner) return void 0;
|
|
2894
|
+
selector = inner;
|
|
2895
|
+
}
|
|
2896
|
+
const rt = this.kotlinIdentifierPath(receiver);
|
|
2897
|
+
const st = this.kotlinIdentifierPath(selector);
|
|
2898
|
+
return rt && st ? `${rt}.${st}` : void 0;
|
|
2899
|
+
}
|
|
2900
|
+
case "call_expression":
|
|
2901
|
+
case "indexing_expression":
|
|
2902
|
+
return this.kotlinIdentifierPath(node.namedChild(0));
|
|
2903
|
+
default:
|
|
2904
|
+
return void 0;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* M60 (#924, kotlin E half): is this published TEXT itself name-shaped? A name
|
|
2909
|
+
* carries no argument, lambda, index or separator punctuation — the same rule
|
|
2910
|
+
* the census's E class measures. Used only as a FALLBACK: a receiver whose spine
|
|
2911
|
+
* has no identifier (`this`, a type parameter, a quoted name) keeps its old
|
|
2912
|
+
* spelling rather than losing the edge, because those spellings are names and
|
|
2913
|
+
* the defect being fixed is an EXPRESSION published as one.
|
|
2914
|
+
*/
|
|
2915
|
+
kotlinNameShapedText(text) {
|
|
2916
|
+
if (!text) return false;
|
|
2917
|
+
if (text.includes("\n")) return false;
|
|
2918
|
+
for (const ch of ["(", ")", "{", "}", "[", "]", ","]) {
|
|
2919
|
+
if (text.includes(ch)) return false;
|
|
2920
|
+
}
|
|
2921
|
+
return text.trim() === text;
|
|
2922
|
+
}
|
|
2923
|
+
buildKotlinNavigationExpression(node) {
|
|
2924
|
+
const receiver = node.namedChild(0);
|
|
2925
|
+
let selector = node.namedChild(node.namedChildCount - 1);
|
|
2926
|
+
if (!receiver || !selector) return node.text;
|
|
2927
|
+
if (selector.type === "navigation_suffix") {
|
|
2928
|
+
const inner = selector.namedChild(selector.namedChildCount - 1);
|
|
2929
|
+
if (!inner) return node.text;
|
|
2930
|
+
selector = inner;
|
|
2931
|
+
}
|
|
2932
|
+
const receiverText = receiver.type === "navigation_expression" ? this.buildKotlinNavigationExpression(receiver) : receiver.type === "call_expression" ? this.buildKotlinCallExpression(receiver) : receiver.text;
|
|
2933
|
+
return `${receiverText}.${selector.text}`;
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Check if function call is Kotlin builtin
|
|
2937
|
+
*/
|
|
2938
|
+
/**
|
|
2939
|
+
* Stdlib check for qualified_calls. EXACT matching only - the previous
|
|
2940
|
+
* substring loop classified any user call whose name merely CONTAINED a
|
|
2941
|
+
* builtin word (getUserById -> "get", runMigration -> "run") as stdlib,
|
|
2942
|
+
* silently emptying symbol.qualified_calls (M14.1 fix). Qualified calls are
|
|
2943
|
+
* suppressed only by full-name match or stdlib package prefix; unqualified
|
|
2944
|
+
* calls by exact leaf against the stdlib set + scope-function extras.
|
|
2945
|
+
*/
|
|
2946
|
+
isKotlinBuiltin(call) {
|
|
2947
|
+
if (!call) return false;
|
|
2948
|
+
for (const prefix of _RealKotlinAnalyzer.stdlibPackagePrefixes) {
|
|
2949
|
+
if (call.startsWith(prefix)) return true;
|
|
2950
|
+
}
|
|
2951
|
+
if (this.kotlinStdlibExact.has(call)) return true;
|
|
2952
|
+
if (!call.includes(".")) {
|
|
2953
|
+
return _RealKotlinAnalyzer.kotlinBuiltinExtras.has(call);
|
|
2954
|
+
}
|
|
2955
|
+
return false;
|
|
2956
|
+
}
|
|
2957
|
+
static stdlibPackagePrefixes = [
|
|
2958
|
+
"kotlin.",
|
|
2959
|
+
"kotlinx.",
|
|
2960
|
+
"java.",
|
|
2961
|
+
"javax.",
|
|
2962
|
+
"android.",
|
|
2963
|
+
"androidx."
|
|
2964
|
+
];
|
|
2965
|
+
static kotlinBuiltinExtras = /* @__PURE__ */ new Set([
|
|
2966
|
+
"println",
|
|
2967
|
+
"print",
|
|
2968
|
+
"readLine",
|
|
2969
|
+
"require",
|
|
2970
|
+
"check",
|
|
2971
|
+
"error",
|
|
2972
|
+
"let",
|
|
2973
|
+
"run",
|
|
2974
|
+
"with",
|
|
2975
|
+
"apply",
|
|
2976
|
+
"also",
|
|
2977
|
+
"takeIf",
|
|
2978
|
+
"takeUnless",
|
|
2979
|
+
"toString",
|
|
2980
|
+
"equals",
|
|
2981
|
+
"hashCode",
|
|
2982
|
+
"copy",
|
|
2983
|
+
"componentN",
|
|
2984
|
+
"repeat",
|
|
2985
|
+
"uppercase",
|
|
2986
|
+
"lowercase",
|
|
2987
|
+
"trim",
|
|
2988
|
+
"mutableMapOf",
|
|
2989
|
+
"mapOf",
|
|
2990
|
+
"mutableListOf",
|
|
2991
|
+
"listOf",
|
|
2992
|
+
"setOf",
|
|
2993
|
+
"mutableSetOf",
|
|
2994
|
+
"arrayOf",
|
|
2995
|
+
"emptyList",
|
|
2996
|
+
"emptyMap",
|
|
2997
|
+
"emptySet"
|
|
2998
|
+
]);
|
|
2999
|
+
/**
|
|
3000
|
+
* Exact stdlib names used by the call-graph filter (leaf-level). Operators
|
|
3001
|
+
* like plus/minus/times are deliberately excluded so in-file operator
|
|
3002
|
+
* overloads produce real Calls edges.
|
|
3003
|
+
*/
|
|
3004
|
+
kotlinStdlibExact = /* @__PURE__ */ new Set([
|
|
3005
|
+
"println",
|
|
3006
|
+
"print",
|
|
3007
|
+
"readLine",
|
|
3008
|
+
"require",
|
|
3009
|
+
"check",
|
|
3010
|
+
"error",
|
|
3011
|
+
"let",
|
|
3012
|
+
"run",
|
|
3013
|
+
"with",
|
|
3014
|
+
"apply",
|
|
3015
|
+
"also",
|
|
3016
|
+
"takeIf",
|
|
3017
|
+
"takeUnless",
|
|
3018
|
+
"toString",
|
|
3019
|
+
"equals",
|
|
3020
|
+
"hashCode",
|
|
3021
|
+
"copy",
|
|
3022
|
+
"componentN",
|
|
3023
|
+
"mutableMapOf",
|
|
3024
|
+
"mapOf",
|
|
3025
|
+
"mutableListOf",
|
|
3026
|
+
"listOf",
|
|
3027
|
+
"setOf",
|
|
3028
|
+
"mutableSetOf",
|
|
3029
|
+
"arrayOf",
|
|
3030
|
+
"emptyList",
|
|
3031
|
+
"emptyMap",
|
|
3032
|
+
"emptySet",
|
|
3033
|
+
"repeat",
|
|
3034
|
+
"uppercase",
|
|
3035
|
+
"lowercase",
|
|
3036
|
+
"trim",
|
|
3037
|
+
"startsWith",
|
|
3038
|
+
"endsWith",
|
|
3039
|
+
"contains",
|
|
3040
|
+
"substring",
|
|
3041
|
+
"map",
|
|
3042
|
+
"filter",
|
|
3043
|
+
"reduce",
|
|
3044
|
+
"fold",
|
|
3045
|
+
"forEach",
|
|
3046
|
+
"sorted",
|
|
3047
|
+
"sortedBy",
|
|
3048
|
+
"distinct",
|
|
3049
|
+
"first",
|
|
3050
|
+
"last",
|
|
3051
|
+
"add",
|
|
3052
|
+
"remove",
|
|
3053
|
+
"size",
|
|
3054
|
+
"isEmpty",
|
|
3055
|
+
"isNotEmpty",
|
|
3056
|
+
"put",
|
|
3057
|
+
"get",
|
|
3058
|
+
"getOrElse",
|
|
3059
|
+
"getOrDefault",
|
|
3060
|
+
"any",
|
|
3061
|
+
"all",
|
|
3062
|
+
"none",
|
|
3063
|
+
"count",
|
|
3064
|
+
"sum",
|
|
3065
|
+
"max",
|
|
3066
|
+
"min",
|
|
3067
|
+
"take",
|
|
3068
|
+
"drop"
|
|
3069
|
+
]);
|
|
3070
|
+
/**
|
|
3071
|
+
* Kotlin Annotation Name Extraction
|
|
3072
|
+
*/
|
|
3073
|
+
getKotlinAnnotationName(node) {
|
|
3074
|
+
const direct = this.findNameNode(node);
|
|
3075
|
+
if (direct) return `@${direct.text}`;
|
|
3076
|
+
const userType = this.findChildByType(node, ["user_type"]);
|
|
3077
|
+
if (userType) {
|
|
3078
|
+
let last = null;
|
|
3079
|
+
for (let i = 0; i < userType.namedChildCount; i++) {
|
|
3080
|
+
const c = userType.namedChild(i);
|
|
3081
|
+
if (c && (c.type === "type_identifier" || c.type === "simple_identifier")) last = c;
|
|
3082
|
+
}
|
|
3083
|
+
if (last) return `@${last.text}`;
|
|
3084
|
+
}
|
|
3085
|
+
return void 0;
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Reduce raw annotation text to its short name: "@org.x.GetMapping(\"/u\")"
|
|
3089
|
+
* → "GetMapping". Decorators keep the raw source text; anything comparing
|
|
3090
|
+
* annotation NAMES must compare through this normalization.
|
|
3091
|
+
*/
|
|
3092
|
+
kotlinShortAnnotationName(raw) {
|
|
3093
|
+
let t = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
3094
|
+
const paren = t.indexOf("(");
|
|
3095
|
+
if (paren !== -1) t = t.slice(0, paren);
|
|
3096
|
+
const dot = t.lastIndexOf(".");
|
|
3097
|
+
return dot === -1 ? t : t.slice(dot + 1);
|
|
3098
|
+
}
|
|
3099
|
+
/**
|
|
3100
|
+
* Kotlin Annotation Arguments Extraction
|
|
3101
|
+
*/
|
|
3102
|
+
extractKotlinAnnotationArguments(node) {
|
|
3103
|
+
const args = [];
|
|
3104
|
+
const argsNode = this.getValueArguments(node);
|
|
3105
|
+
if (argsNode) {
|
|
3106
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
3107
|
+
const arg = argsNode.namedChild(i);
|
|
3108
|
+
if (arg) {
|
|
3109
|
+
args.push(arg.text);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
return args;
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Kotlin Annotation Target Extraction
|
|
3117
|
+
*/
|
|
3118
|
+
extractKotlinAnnotationTarget(node) {
|
|
3119
|
+
const targetNode = this.findChildByType(node, ["use_site_target"]);
|
|
3120
|
+
if (targetNode) {
|
|
3121
|
+
const text = targetNode.text;
|
|
3122
|
+
return text.endsWith(":") ? text.slice(0, -1) : text;
|
|
3123
|
+
}
|
|
3124
|
+
return void 0;
|
|
3125
|
+
}
|
|
3126
|
+
/**
|
|
3127
|
+
* Kotlin Type Cleaning
|
|
3128
|
+
*/
|
|
3129
|
+
cleanKotlinType(typeText) {
|
|
3130
|
+
const parts = typeText.split(" ");
|
|
3131
|
+
const validParts = [];
|
|
3132
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3133
|
+
if (parts[i]) validParts.push(parts[i]);
|
|
3134
|
+
}
|
|
3135
|
+
return validParts.join(" ").replace("??", "?").replace("? ?", "?").trim();
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Kotlin Type Inference from Initializer
|
|
3139
|
+
*/
|
|
3140
|
+
/**
|
|
3141
|
+
* Infer the type of a property from its initializer using the initializer
|
|
3142
|
+
* NODE TYPE — never by regex-testing its text. Kotlin literal nodes carry
|
|
3143
|
+
* their kind in the grammar: integer_literal, real_literal, float_literal,
|
|
3144
|
+
* string_literal, character_literal, boolean_literal, call_expression.
|
|
3145
|
+
*/
|
|
3146
|
+
inferKotlinTypeFromInitializer(initNode) {
|
|
3147
|
+
const text = initNode.text.trim();
|
|
3148
|
+
switch (initNode.type) {
|
|
3149
|
+
case "integer_literal":
|
|
3150
|
+
return text.endsWith("L") ? "Long" : "Int";
|
|
3151
|
+
case "real_literal":
|
|
3152
|
+
return text.endsWith("f") || text.endsWith("F") ? "Float" : "Double";
|
|
3153
|
+
case "float_literal":
|
|
3154
|
+
return "Float";
|
|
3155
|
+
case "string_literal":
|
|
3156
|
+
return "String";
|
|
3157
|
+
case "character_literal":
|
|
3158
|
+
return "Char";
|
|
3159
|
+
case "boolean_literal":
|
|
3160
|
+
return "Boolean";
|
|
3161
|
+
case "call_expression": {
|
|
3162
|
+
const callee = this.buildKotlinCallExpression(initNode);
|
|
3163
|
+
const name = callee.split(".").pop() || callee;
|
|
3164
|
+
if (name === "listOf" || name === "emptyList") return "List<*>";
|
|
3165
|
+
if (name === "mutableListOf") return "MutableList<*>";
|
|
3166
|
+
if (name === "setOf" || name === "emptySet") return "Set<*>";
|
|
3167
|
+
if (name === "mapOf" || name === "emptyMap") return "Map<*, *>";
|
|
3168
|
+
if (name === "arrayOf") return "Array<*>";
|
|
3169
|
+
return name;
|
|
3170
|
+
}
|
|
3171
|
+
default:
|
|
3172
|
+
return "Any";
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Kotlin Enum Values Extraction
|
|
3177
|
+
*/
|
|
3178
|
+
extractKotlinEnumValues(node) {
|
|
3179
|
+
const values = [];
|
|
3180
|
+
const enumBody = this.findChildByType(node, ["enum_class_body", "class_body"]);
|
|
3181
|
+
if (!enumBody) return values;
|
|
3182
|
+
for (let i = 0; i < enumBody.namedChildCount; i++) {
|
|
3183
|
+
const child = enumBody.namedChild(i);
|
|
3184
|
+
if (child?.type === "enum_entry") {
|
|
3185
|
+
const nameNode = this.findNameNode(child);
|
|
3186
|
+
if (nameNode) {
|
|
3187
|
+
values.push(nameNode.text);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
return values;
|
|
3192
|
+
}
|
|
3193
|
+
calculateRealComplexity(symbols, relationships) {
|
|
3194
|
+
return symbols.length + relationships.length;
|
|
3195
|
+
}
|
|
3196
|
+
calculateCyclomaticComplexity(node) {
|
|
3197
|
+
let complexity = 1;
|
|
3198
|
+
const stack = [node];
|
|
3199
|
+
while (stack.length > 0) {
|
|
3200
|
+
const n = stack.pop();
|
|
3201
|
+
switch (n.type) {
|
|
3202
|
+
case "if_expression":
|
|
3203
|
+
// else_clause deliberately NOT counted: an `else` is not an
|
|
3204
|
+
// independent decision (family contract). Counting both if and
|
|
3205
|
+
// else double-counted every if/else.
|
|
3206
|
+
case "for_statement":
|
|
3207
|
+
case "while_statement":
|
|
3208
|
+
case "do_while_statement":
|
|
3209
|
+
case "when_entry":
|
|
3210
|
+
case "catch_block":
|
|
3211
|
+
case "elvis_expression":
|
|
3212
|
+
complexity++;
|
|
3213
|
+
break;
|
|
3214
|
+
case "conjunction_expression":
|
|
3215
|
+
case "disjunction_expression":
|
|
3216
|
+
complexity++;
|
|
3217
|
+
break;
|
|
3218
|
+
case "lambda_literal":
|
|
3219
|
+
break;
|
|
3220
|
+
}
|
|
3221
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
3222
|
+
const child = n.namedChild(i);
|
|
3223
|
+
if (child) stack.push(child);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
return complexity;
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* (#825): `{name, node}` pairs, not bare names.
|
|
3230
|
+
*
|
|
3231
|
+
* The compose containment edges are derived from the CALLS inside a
|
|
3232
|
+
* `@Composable` body, and their target is the called name — so the line must
|
|
3233
|
+
* come from the call node that carries it. Returning the node is the cheapest
|
|
3234
|
+
* way to keep that true as the walk changes; the previous string-only return
|
|
3235
|
+
* left those edges with no line at all (unknown, which is honest but useless).
|
|
3236
|
+
*/
|
|
3237
|
+
extractFunctionCalls(node) {
|
|
3238
|
+
const calls = [];
|
|
3239
|
+
const stack = [node];
|
|
3240
|
+
while (stack.length > 0) {
|
|
3241
|
+
const n = stack.pop();
|
|
3242
|
+
if (this.isKotlinContextParameterHeader(n)) continue;
|
|
3243
|
+
if (n.type === "call_expression") {
|
|
3244
|
+
const full = this.buildKotlinCallExpression(n);
|
|
3245
|
+
calls.push({ name: full.split(".").pop() || full, node: this.calleeLeafNode(n) ?? n });
|
|
3246
|
+
}
|
|
3247
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
3248
|
+
const child = n.namedChild(i);
|
|
3249
|
+
if (child) stack.push(child);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
return calls;
|
|
3253
|
+
}
|
|
3254
|
+
analyzeRelationships(node, relationships, symbols, ctx) {
|
|
3255
|
+
const stack = [node];
|
|
3256
|
+
while (stack.length > 0) {
|
|
3257
|
+
const n = stack.pop();
|
|
3258
|
+
if (n.type === "function_declaration") {
|
|
3259
|
+
let receiverNode = void 0;
|
|
3260
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
3261
|
+
const c = n.namedChild(i);
|
|
3262
|
+
if (c?.type === "simple_identifier") break;
|
|
3263
|
+
if (c?.type === "receiver_type" || c?.type === "user_type" || c?.type === "nullable_type" || c?.type === "type_identifier") {
|
|
3264
|
+
receiverNode = c;
|
|
3265
|
+
break;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
const nameNode = this.findNameNode(n);
|
|
3269
|
+
if (receiverNode && nameNode) {
|
|
3270
|
+
const receiverType = receiverNode.text.endsWith("?") ? receiverNode.text.slice(0, -1) : receiverNode.text;
|
|
3271
|
+
relationships.push({
|
|
3272
|
+
source: nameNode.text,
|
|
3273
|
+
target: receiverType,
|
|
3274
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
3275
|
+
confidence: 0.9,
|
|
3276
|
+
// (#825): the receiver type is named by its receiver node.
|
|
3277
|
+
...this.lineField(receiverNode, n),
|
|
3278
|
+
metadata: {
|
|
3279
|
+
relationshipType: "kotlin_extension_function",
|
|
3280
|
+
isExtension: true
|
|
3281
|
+
}
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
const isComposable = this.extractAnnotations(n, ctx).some((a) => this.kotlinShortAnnotationName(a) === "Composable");
|
|
3285
|
+
if (isComposable && nameNode) {
|
|
3286
|
+
const bodyNode = this.findChildByType(n, ["function_body", "class_body"]);
|
|
3287
|
+
if (bodyNode) {
|
|
3288
|
+
const bodyCalls = this.extractFunctionCalls(bodyNode);
|
|
3289
|
+
for (let i = 0; i < bodyCalls.length; i++) {
|
|
3290
|
+
const call = bodyCalls[i].name;
|
|
3291
|
+
if (call.length > 0 && call[0] >= "A" && call[0] <= "Z") {
|
|
3292
|
+
relationships.push({
|
|
3293
|
+
source: nameNode.text,
|
|
3294
|
+
target: call,
|
|
3295
|
+
// (#825): the UI component is named at its call site.
|
|
3296
|
+
line_number: bodyCalls[i].node.startPosition.row + 1,
|
|
3297
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
3298
|
+
confidence: 0.8,
|
|
3299
|
+
metadata: {
|
|
3300
|
+
relationshipType: "compose_ui_tree",
|
|
3301
|
+
isUiComponent: true,
|
|
3302
|
+
// KAD-MR3 K18/K4 (M49.K1): a PascalCase-name heuristic is
|
|
3303
|
+
// NOT verified containment — the tier classifier stamped
|
|
3304
|
+
// every contains edge "structural" (probe: Intent(...)
|
|
3305
|
+
// identical to Text(...)). Honest label: heuristic — same
|
|
3306
|
+
// tier as a 0.5 override guess. The compose edge keeps
|
|
3307
|
+
// its 0.8 (patterns.ts filters >=0.7), but consumers can
|
|
3308
|
+
// now distinguish it from verified containment.
|
|
3309
|
+
evidenceClass: "heuristic"
|
|
3310
|
+
}
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
if (isComposable && nameNode) {
|
|
3317
|
+
const renderBody = this.findChildByType(n, ["function_body"]);
|
|
3318
|
+
if (renderBody) {
|
|
3319
|
+
const composableNames = /* @__PURE__ */ new Set();
|
|
3320
|
+
for (let si = 0; si < symbols.length; si++) {
|
|
3321
|
+
const sym = symbols[si];
|
|
3322
|
+
const symAnns = sym.metadata?.annotations;
|
|
3323
|
+
if (!Array.isArray(symAnns) || symAnns.indexOf("Composable") < 0) continue;
|
|
3324
|
+
if (sym.name === nameNode.text) continue;
|
|
3325
|
+
if (sym.kind !== ASTEngine.SymbolKind.Function && sym.kind !== ASTEngine.SymbolKind.Method) continue;
|
|
3326
|
+
composableNames.add(sym.name);
|
|
3327
|
+
}
|
|
3328
|
+
if (composableNames.size > 0) {
|
|
3329
|
+
const renderCalls = this.extractFunctionCalls(renderBody);
|
|
3330
|
+
for (let ri = 0; ri < renderCalls.length; ri++) {
|
|
3331
|
+
if (!composableNames.has(renderCalls[ri].name)) continue;
|
|
3332
|
+
relationships.push({
|
|
3333
|
+
source: nameNode.text,
|
|
3334
|
+
target: renderCalls[ri].name,
|
|
3335
|
+
// The rendered component is named at its call site.
|
|
3336
|
+
line_number: renderCalls[ri].node.startPosition.row + 1,
|
|
3337
|
+
type: ASTEngine.RelationshipType.Renders,
|
|
3338
|
+
confidence: 0.9,
|
|
3339
|
+
metadata: {
|
|
3340
|
+
relationshipType: "compose_render",
|
|
3341
|
+
isComposableMarker: true,
|
|
3342
|
+
isComponentCall: true,
|
|
3343
|
+
// The marker on the source and the target's OWN declaration are both
|
|
3344
|
+
// read from the tree — structural, unlike the PascalCase heuristic above.
|
|
3345
|
+
evidenceClass: "structural"
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
if (n.type === "call_expression") {
|
|
3354
|
+
const full = this.buildKotlinCallExpression(n);
|
|
3355
|
+
const callName = full.split(".").pop() || "";
|
|
3356
|
+
if (callName === "launch" || callName === "async" || callName === "withContext") {
|
|
3357
|
+
let parentFunc = n.parent;
|
|
3358
|
+
while (parentFunc && parentFunc?.type !== "function_declaration") {
|
|
3359
|
+
parentFunc = parentFunc.parent;
|
|
3360
|
+
}
|
|
3361
|
+
if (parentFunc) {
|
|
3362
|
+
const parentName = this.findNameNode(parentFunc)?.text;
|
|
3363
|
+
if (parentName) {
|
|
3364
|
+
relationships.push({
|
|
3365
|
+
source: parentName,
|
|
3366
|
+
target: callName,
|
|
3367
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
3368
|
+
confidence: 1,
|
|
3369
|
+
// (#825): the dispatcher is named at the call itself.
|
|
3370
|
+
...this.lineField(this.calleeLeafNode(n, callName), n),
|
|
3371
|
+
metadata: {
|
|
3372
|
+
relationshipType: "coroutine_dispatch",
|
|
3373
|
+
isAsync: true
|
|
3374
|
+
}
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
3381
|
+
const child = n.namedChild(i);
|
|
3382
|
+
if (child) stack.push(child);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
3386
|
+
const cls = symbols[i];
|
|
3387
|
+
if (cls.kind === ASTEngine.SymbolKind.Class || cls.kind === ASTEngine.SymbolKind.Interface || cls.kind === ASTEngine.SymbolKind.Enum) {
|
|
3388
|
+
if (cls.metadata?.isAnonymousObject) continue;
|
|
3389
|
+
if (cls.metadata?.superTypes) {
|
|
3390
|
+
const ctorCalls = cls.metadata?.superTypesAreConstructorCalls;
|
|
3391
|
+
const superLines = cls.metadata?.superTypeLines;
|
|
3392
|
+
for (let j = 0; j < cls.metadata.superTypes.length; j++) {
|
|
3393
|
+
const isExtends = cls.kind === ASTEngine.SymbolKind.Interface || ctorCalls?.[j] === true;
|
|
3394
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(cls.name)) continue;
|
|
3395
|
+
if (!_RealKotlinAnalyzer.publishableEndpoint(cls.metadata.superTypes[j])) continue;
|
|
3396
|
+
relationships.push({
|
|
3397
|
+
source: cls.name,
|
|
3398
|
+
target: cls.metadata.superTypes[j],
|
|
3399
|
+
type: isExtends ? ASTEngine.RelationshipType.Extends : ASTEngine.RelationshipType.Implements,
|
|
3400
|
+
confidence: 1,
|
|
3401
|
+
...typeof superLines?.[j] === "number" ? { line_number: superLines[j] } : {},
|
|
3402
|
+
metadata: { relationshipType: isExtends ? "kotlin_extends" : "kotlin_implements" }
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
/**
|
|
3410
|
+
* The name of a node that is a TOP-LEVEL property declaration, or undefined.
|
|
3411
|
+
*
|
|
3412
|
+
* ONTOLOGY (kotlin, 2026-10-03) — the `Global` fallback, and why it fires on
|
|
3413
|
+
* ordinary, well-formed kotlin.
|
|
3414
|
+
*
|
|
3415
|
+
* The three attribution resolvers (`resolveCallSource` for `calls` /
|
|
3416
|
+
* `instantiates`, `extractDeclaredTypeRefs.resolveSource` for the declared-type
|
|
3417
|
+
* `references` graph, and the `uses` resolver) all walk up the parent chain
|
|
3418
|
+
* looking for a declaration to own the edge, and all three listed the same
|
|
3419
|
+
* three node types:
|
|
3420
|
+
*
|
|
3421
|
+
* function_declaration | class_declaration | object_declaration
|
|
3422
|
+
*
|
|
3423
|
+
* `property_declaration` was missing from all three. So every edge written
|
|
3424
|
+
* inside a TOP-LEVEL property's own type clause or initializer had no
|
|
3425
|
+
* declaration to stop at, the walk reached `source_file`, and the fallback
|
|
3426
|
+
* returned the literal `"Global"` — a symbol name that appears nowhere in
|
|
3427
|
+
* kotlin and in no file of the corpus.
|
|
3428
|
+
*
|
|
3429
|
+
* MEASURED, by script/m14/kotlin-r7-endpoint-probe.ts on a 1,200-file
|
|
3430
|
+
* every-Nth draw of the pinned fixture corpus (kotlin@9b80c8ff): 1,290 `Global`
|
|
3431
|
+
* sources in files the analyzer reported as CLEAN, i.e. where the grammar had
|
|
3432
|
+
* no trouble at all and the fallback was standing in for nothing. Two real
|
|
3433
|
+
* files, both ordinary shipped `.kt`:
|
|
3434
|
+
*
|
|
3435
|
+
* CommonJsAndWasmArgumentProviders.kt:101
|
|
3436
|
+
* private val testBaseDir: Path = Paths.get("").toAbsolutePath()
|
|
3437
|
+
* ^^^^^^^^^^^^^ call sourced to Global
|
|
3438
|
+
* KaFirSymbol.kt:65
|
|
3439
|
+
* private val FirBasedSymbol<*>.realPsi: PsiElement?
|
|
3440
|
+
* ^^^^^^^^^^^ declared_type ref sourced to Global
|
|
3441
|
+
*
|
|
3442
|
+
* WHY ONLY WHEN `parent.type === "source_file"`, and this is the load-bearing
|
|
3443
|
+
* condition rather than a detail. A property inside a `class_body` is owned by
|
|
3444
|
+
* its CLASS, and the walk reaches the class first, so it never reaches this
|
|
3445
|
+
* branch — which is the existing, fixture-pinned contract ("calls from
|
|
3446
|
+
* class-level contexts (property initializers, init blocks) belong to the
|
|
3447
|
+
* enclosing class", kotlin/init-attribution). Treating every property as a
|
|
3448
|
+
* boundary would move those edges off the class and onto the property, which is
|
|
3449
|
+
* a different and wrong answer. A TOP-LEVEL property has no enclosing class, so
|
|
3450
|
+
* there is nothing else that could own it and it owns itself — and the
|
|
3451
|
+
* analyzer already publishes it as a `Property` symbol with exactly this name,
|
|
3452
|
+
* so the edge's source resolves to a real symbol.
|
|
3453
|
+
*
|
|
3454
|
+
* `type_alias` is deliberately NOT handled here. A top-level typealias declares
|
|
3455
|
+
* no runtime symbol (the specification expands it at compile time), and the
|
|
3456
|
+
* `exports` rule already excludes it for that reason; giving it edges as an
|
|
3457
|
+
* owner would put a name in the graph that no file can navigate to.
|
|
3458
|
+
*/
|
|
3459
|
+
kotlinTopLevelPropertyName(node) {
|
|
3460
|
+
if (node.type === "getter" || node.type === "setter") {
|
|
3461
|
+
const parent = node.parent;
|
|
3462
|
+
if (!parent || parent.type !== "source_file") return void 0;
|
|
3463
|
+
let best;
|
|
3464
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
3465
|
+
const c = parent.namedChild(i);
|
|
3466
|
+
if (!c || c.type !== "property_declaration") continue;
|
|
3467
|
+
if (c.startPosition.row > node.startPosition.row) continue;
|
|
3468
|
+
if (!best || c.startPosition.row > best.startPosition.row) best = c;
|
|
3469
|
+
}
|
|
3470
|
+
return best ? this.kotlinTopLevelPropertyName(best) : void 0;
|
|
3471
|
+
}
|
|
3472
|
+
if (node.type !== "property_declaration") return void 0;
|
|
3473
|
+
if (node.parent?.type !== "source_file") return void 0;
|
|
3474
|
+
const decl = this.findChildByType(node, ["variable_declaration"]);
|
|
3475
|
+
if (!decl) return void 0;
|
|
3476
|
+
return this.findChildByType(decl, ["simple_identifier", "type_identifier"])?.text;
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* THE IMPORTED-BINDING CLAIM FOR KOTLIN IMPORTS .
|
|
3480
|
+
*
|
|
3481
|
+
* Two halves, both needed because the arbiter verifies one against the other:
|
|
3482
|
+
* 1. EVIDENCE on the imports edge: `metadata.importedBindings = [binding]` + `metadata.module = <the
|
|
3483
|
+
* module path as written>` for EXTERNAL imports only. The gate builds file -> specifier -> bound
|
|
3484
|
+
* names from exactly this pair and verifies claims against it.
|
|
3485
|
+
* 2. CLAIM on the use row: `metadata.importedBinding = { specifier, binding }` on a calls row whose
|
|
3486
|
+
* callee is BARE (no receiver) or a uses row that is a BARE READ (`isBareRead`), whose target IS the
|
|
3487
|
+
* imported binding and whose file does not declare a shadowing name.
|
|
3488
|
+
*
|
|
3489
|
+
* REFUSALS (each a way the claim could become false, fail-closed):
|
|
3490
|
+
* - container root NOT in the EXTERNAL allowlist (only JVM/platform-reserved roots are listed; a
|
|
3491
|
+
* project-local import is project-resolvable and claiming it would launder — that is why the list is
|
|
3492
|
+
* the reserved vocabulary and not "anything that looks like a package");
|
|
3493
|
+
* - an on-demand import (`import a.b.*`) binds no name this file states;
|
|
3494
|
+
* - the FILE declares the name (the declaration shadows the import — the use is not the binding);
|
|
3495
|
+
* - a receiver-carrying call / field access row (its target is a member leaf, cut 2);
|
|
3496
|
+
* - a row that already carries a claim.
|
|
3497
|
+
*/
|
|
3498
|
+
attachKotlinImportedBindingClaims(relationships, symbols, imports) {
|
|
3499
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
3500
|
+
const isExternal = (module) => {
|
|
3501
|
+
for (const root of KOTLIN_EXTERNAL_IMPORT_ROOTS) if (module === root || module.startsWith(root)) return true;
|
|
3502
|
+
return false;
|
|
3503
|
+
};
|
|
3504
|
+
for (const imp of imports) {
|
|
3505
|
+
const module = String(imp?.module ?? "");
|
|
3506
|
+
if (module.length === 0) continue;
|
|
3507
|
+
if (imp.isNamespaceImport === true) continue;
|
|
3508
|
+
const dot = module.lastIndexOf(".");
|
|
3509
|
+
const leaf = dot > 0 ? module.slice(dot + 1) : module;
|
|
3510
|
+
const binding = String(imp.metadata?.alias ?? imp.alias ?? "") || leaf;
|
|
3511
|
+
if (!binding || binding === "*") continue;
|
|
3512
|
+
if (!isExternal(module)) continue;
|
|
3513
|
+
bindings.set(binding, module);
|
|
3514
|
+
}
|
|
3515
|
+
if (bindings.size === 0) return;
|
|
3516
|
+
for (const r of relationships) {
|
|
3517
|
+
if (String(r.type ?? "") !== "imports") continue;
|
|
3518
|
+
const specifier = String(r.target ?? "");
|
|
3519
|
+
if (!isExternal(specifier)) continue;
|
|
3520
|
+
const dot = specifier.lastIndexOf(".");
|
|
3521
|
+
const leaf = dot > 0 ? specifier.slice(dot + 1) : specifier;
|
|
3522
|
+
let binding = leaf;
|
|
3523
|
+
for (const [b, spec] of bindings) if (spec === specifier) {
|
|
3524
|
+
binding = b;
|
|
3525
|
+
break;
|
|
3526
|
+
}
|
|
3527
|
+
if (!specifier || !binding || binding === "*") continue;
|
|
3528
|
+
const md = r.metadata = r.metadata ?? {};
|
|
3529
|
+
if (md.importedBindings !== void 0) continue;
|
|
3530
|
+
md.importedBindings = [binding];
|
|
3531
|
+
md.module = specifier;
|
|
3532
|
+
}
|
|
3533
|
+
const fileDeclared = /* @__PURE__ */ new Set();
|
|
3534
|
+
for (const sym of symbols) if (sym?.name) fileDeclared.add(sym.name);
|
|
3535
|
+
for (const r of relationships) {
|
|
3536
|
+
const label = String(r.type ?? "");
|
|
3537
|
+
if (label !== "calls" && label !== "uses") continue;
|
|
3538
|
+
const md = r.metadata ?? {};
|
|
3539
|
+
if (md.importedBinding !== void 0) continue;
|
|
3540
|
+
const target = String(r.target ?? "");
|
|
3541
|
+
if (!target || !bindings.has(target)) continue;
|
|
3542
|
+
if (fileDeclared.has(target)) continue;
|
|
3543
|
+
if (label === "calls") {
|
|
3544
|
+
if (md.kotlinBareCallee !== true) continue;
|
|
3545
|
+
} else if (md.isBareRead !== true) {
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
r.metadata = { ...md, importedBinding: { specifier: bindings.get(target), binding: target } };
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
processImportsExports(node, imports, exports, relationships, symbols, ctx) {
|
|
3552
|
+
const stack = [node];
|
|
3553
|
+
const packageName = ctx.packageStack.join(".");
|
|
3554
|
+
const unitIdentityForImports = this.primaryTopLevelType(symbols);
|
|
3555
|
+
const importSource = unitIdentityForImports?.name || kotlinCompilationUnitName(ctx.currentFile) || packageName || "root";
|
|
3556
|
+
while (stack.length > 0) {
|
|
3557
|
+
const n = stack.pop();
|
|
3558
|
+
if (n.type === "import_header") {
|
|
3559
|
+
const pathNode = this.findChildByType(n, ["identifier"]);
|
|
3560
|
+
if (pathNode && pathNode.text.length > 0) {
|
|
3561
|
+
const importPath = pathNode.text;
|
|
3562
|
+
const aliasNode = this.findChildByType(n, ["import_alias"]);
|
|
3563
|
+
const isWildcard = !!this.findChildByType(n, ["wildcard_import"]);
|
|
3564
|
+
const aliasText = aliasNode ? aliasNode.namedChild(aliasNode.namedChildCount - 1)?.text ?? aliasNode.text : void 0;
|
|
3565
|
+
imports.push({
|
|
3566
|
+
module: importPath,
|
|
3567
|
+
items: isWildcard ? ["*"] : [aliasText ?? (importPath.split(".").pop() || importPath)],
|
|
3568
|
+
isDefaultImport: false,
|
|
3569
|
+
isNamespaceImport: isWildcard,
|
|
3570
|
+
range: this.nodeToRange(n),
|
|
3571
|
+
metadata: {
|
|
3572
|
+
isKotlinImport: true,
|
|
3573
|
+
alias: aliasText
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
const importShape = isWildcard ? "imported_package_wildcard" : aliasText ? "imported_declaration_aliased" : "imported_declaration";
|
|
3577
|
+
relationships.push({
|
|
3578
|
+
source: importSource,
|
|
3579
|
+
target: importPath,
|
|
3580
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
3581
|
+
confidence: 0.9,
|
|
3582
|
+
// (#825): the imported path is named on the import statement.
|
|
3583
|
+
...this.lineField(pathNode, n),
|
|
3584
|
+
metadata: {
|
|
3585
|
+
isKotlinImport: true,
|
|
3586
|
+
// `isImport` is the flag the JVM-family evidence classifier keys on
|
|
3587
|
+
// (see classifyEvidenceTier). Without it an `imports` edge would be
|
|
3588
|
+
// filed as `usage_contextual` and be indistinguishable from a
|
|
3589
|
+
// heuristic in a payload.
|
|
3590
|
+
isImport: true,
|
|
3591
|
+
relationshipType: importShape,
|
|
3592
|
+
importShape,
|
|
3593
|
+
isWildcard,
|
|
3594
|
+
// The same fact under the ontology's own spelling
|
|
3595
|
+
// (kotlin/imports-exports-ontology pins `meta.isWildcardImport`),
|
|
3596
|
+
// beside the lane's `isWildcard`: the wildcard form IS the fact
|
|
3597
|
+
// that separates `import a.b.*` from `import a.b.C`, and a consumer
|
|
3598
|
+
// reading either name must not have to guess which one this lane
|
|
3599
|
+
// chose. Emitted only when true — an absent flag is not a false one.
|
|
3600
|
+
...isWildcard ? { isWildcardImport: true } : {},
|
|
3601
|
+
...aliasText ? { alias: aliasText } : {},
|
|
3602
|
+
...packageName ? {} : { rootPackageFile: true },
|
|
3603
|
+
reasonCode: "declared-import-path"
|
|
3604
|
+
}
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
3609
|
+
const child = n.namedChild(i);
|
|
3610
|
+
if (child) stack.push(child);
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
this.emitKotlinExportRelationships(symbols, relationships, ctx, node);
|
|
3614
|
+
}
|
|
3615
|
+
/**
|
|
3616
|
+
* ONTOLOGY (kotlin, 2026-10-03): the `exports` LABEL — a relationship behind
|
|
3617
|
+
* an export surface that already existed as data.
|
|
3618
|
+
*
|
|
3619
|
+
* BEFORE: the loop in `analyze()` filled `ParseResult.exports` and NONE of its
|
|
3620
|
+
* rows reached a relationship, so the lane published a public-API list and no
|
|
3621
|
+
* graph edge. Measured on 300 tracked files of
|
|
3622
|
+
* the pinned fixture corpus (kotlin@9b80c8ff) before this change: 0 `exports`
|
|
3623
|
+
* edges. On a 1,500-file window the `ExportInfo` list held 2,809 rows and the
|
|
3624
|
+
* relationship list held none.
|
|
3625
|
+
*
|
|
3626
|
+
* THE RULE, and every clause of it is a kotlin language fact:
|
|
3627
|
+
*
|
|
3628
|
+
* 1. TOP-LEVEL ONLY — `!s.ownerName`. A member is reached through its
|
|
3629
|
+
* container, and membership is already published as `contains`; publishing
|
|
3630
|
+
* it twice would make `contains` and `exports` two names for one edge.
|
|
3631
|
+
* Measured on the window: 16,694 nested declarations against 7,554
|
|
3632
|
+
* top-level ones, so this clause governs the majority of the population.
|
|
3633
|
+
* 2. NOT LEXICALLY LOCAL — see `isKotlinLocalDeclaration`. "No ownerName" is
|
|
3634
|
+
* NOT the same predicate as "top level": a class or function declared
|
|
3635
|
+
* inside a function body gets no owner in this analyzer, because
|
|
3636
|
+
* `isMemberPosition` is only true inside a `class_body`. Measured: 148 of
|
|
3637
|
+
* 2,809 ownerless public symbols on the window are local declarations
|
|
3638
|
+
* (`KaFirCompilerFacility.kt:467 function register`, `:584 mapModule`,
|
|
3639
|
+
* `:594 appendDanglingChunk`), and the pre-existing `ExportInfo` loop
|
|
3640
|
+
* published every one of them. A local function is callable from exactly
|
|
3641
|
+
* one lexical scope, so an `exports` edge claiming a module publishes it
|
|
3642
|
+
* would be a fabricated edge in `bb_migrate`.
|
|
3643
|
+
* 3. PUBLIC ONLY — `visibility === "public" || undefined`. Kotlin's default
|
|
3644
|
+
* visibility is public, `private` is file-scoped and `internal` is
|
|
3645
|
+
* module-scoped; neither of the latter is reachable by name from another
|
|
3646
|
+
* module, which is the reachability an `imports` edge asserts from the
|
|
3647
|
+
* other side. Measured on the window: 1,719 `internal` and 657 `private`
|
|
3648
|
+
* top-level declarations excluded.
|
|
3649
|
+
* 4. NOT SYNTHESIZED — `!isPhantom && !isAnonymousObject`. A data class's
|
|
3650
|
+
* `componentN`/`copy`/`equals` are written nowhere; an anonymous object is
|
|
3651
|
+
* named by no import.
|
|
3652
|
+
* 5. NOT A SCRIPT — a `.kts` file's top-level declarations become members of
|
|
3653
|
+
* the script's class and are not importable by any other file, so a script
|
|
3654
|
+
* publishes no API. Measured: 12 script top-level declarations in a
|
|
3655
|
+
* 3,000-file window, all of them also excluded by clause 6.
|
|
3656
|
+
* 6. THE FILE MUST NAME A PACKAGE — the source of the edge IS the package, and
|
|
3657
|
+
* kotlin's root package has no name to publish it under. THIS IS THE ONE
|
|
3658
|
+
* CLAUSE THAT IS NOT A LANGUAGE RULE. Kotlin, unlike java (JLS 7.4.2), does
|
|
3659
|
+
* not forbid importing from the unnamed package, so excluding these claims
|
|
3660
|
+
* a restriction the language does not make; it is a LIMITATION of the edge
|
|
3661
|
+
* shape, declared as one, with its cost measured rather than assumed: 693 of
|
|
3662
|
+
* 2,613 top-level ownerless public declarations on the window sit in the
|
|
3663
|
+
* root package, 3 of them in `.kt` files and the rest in `testData`
|
|
3664
|
+
* fragments (the corpus is 71% `testData/`). On the production subset
|
|
3665
|
+
* 1,045 of 1,056 files carry a package header and 0 of 1,748 top-level
|
|
3666
|
+
* public declarations are excluded by this clause. Publishing them under a
|
|
3667
|
+
* placeholder source instead would put a fabricated symbol name into
|
|
3668
|
+
* `bb_graph` for a quarter of the population, which is the worse of the
|
|
3669
|
+
* two failures and is the reason standing rule R7 forbids it.
|
|
3670
|
+
*
|
|
3671
|
+
* Confidence 1.0: the edge is a modifier read plus a position test with no
|
|
3672
|
+
* inference in it, so it is a fact about the source rather than a heuristic.
|
|
3673
|
+
* `evidenceClass` is left to `classifyEvidenceTier`, which already files
|
|
3674
|
+
* `exports` as `structural` — one place, so the two cannot disagree.
|
|
3675
|
+
*/
|
|
3676
|
+
emitKotlinExportRelationships(symbols, relationships, ctx, root) {
|
|
3677
|
+
const packageName = ctx.packageStack.join(".");
|
|
3678
|
+
if (!packageName) return;
|
|
3679
|
+
if (ctx.currentFile.toLowerCase().endsWith(".kts")) return;
|
|
3680
|
+
const unitType = this.primaryTopLevelType(symbols);
|
|
3681
|
+
if (!unitType) return;
|
|
3682
|
+
const placement = this.kotlinDeclarationPlacement(root, ctx.content.split("\n").length);
|
|
3683
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
3684
|
+
const s = symbols[i];
|
|
3685
|
+
if (s.metadata?.isPhantom || s.metadata?.isAnonymousObject) continue;
|
|
3686
|
+
if (s.visibility !== "public" && s.visibility !== void 0) continue;
|
|
3687
|
+
if (s.ownerName) continue;
|
|
3688
|
+
if (!s.name) continue;
|
|
3689
|
+
const declLine = s.range?.startLine ?? 0;
|
|
3690
|
+
if (declLine < 1 || declLine >= placement.local.length) continue;
|
|
3691
|
+
if (placement.local[declLine]) continue;
|
|
3692
|
+
if (!placement.top[declLine]) continue;
|
|
3693
|
+
relationships.push({
|
|
3694
|
+
source: unitType.name,
|
|
3695
|
+
target: s.name,
|
|
3696
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
3697
|
+
confidence: 1,
|
|
3698
|
+
// (#825): the exported name is written on the declaration.
|
|
3699
|
+
...s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
3700
|
+
metadata: {
|
|
3701
|
+
isKotlinExport: true,
|
|
3702
|
+
relationshipType: "exported_declaration",
|
|
3703
|
+
// The PUBLISHER of this row is the compilation unit, spelled as the
|
|
3704
|
+
// file's top-level type. `moduleIdentity` says which rule chose it, so
|
|
3705
|
+
// a consumer reading three exports spellings in this lane can tell
|
|
3706
|
+
// which one answers which question without matching on the source text.
|
|
3707
|
+
moduleIdentity: "primary-top-level-type",
|
|
3708
|
+
kind: String(s.kind),
|
|
3709
|
+
// `exportKind` is the same fact under the name the ontology fixtures
|
|
3710
|
+
// pin (kotlin/imports-exports-ontology), kept beside `kind` rather than
|
|
3711
|
+
// replacing it: `kind` is the symbol's own predicate and `exportKind`
|
|
3712
|
+
// is what the surface calls it, and a consumer reading either must not
|
|
3713
|
+
// have to guess which spelling this lane chose.
|
|
3714
|
+
exportKind: String(s.kind),
|
|
3715
|
+
visibility: s.visibility || "public",
|
|
3716
|
+
// The join key with `imports`, stated because it is not obvious: an
|
|
3717
|
+
// `exports` edge is `package -> simpleName`, so `source + "." + target`
|
|
3718
|
+
// is exactly the dotted path an `imports` edge targets.
|
|
3719
|
+
dottedName: `${packageName}.${s.name}`
|
|
3720
|
+
}
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Which start lines carry a TOP-LEVEL declaration, and which carry one nested
|
|
3726
|
+
* inside a body — a function body, a lambda, an initializer, a class body.
|
|
3727
|
+
*
|
|
3728
|
+
* WHY A SEPARATE PASS RATHER THAN A PER-SYMBOL WALK. The rule "no ownerName
|
|
3729
|
+
* means top level" is wrong, and the way it is wrong is that a declaration
|
|
3730
|
+
* inside a function body carries no owner in this analyzer (`isMemberPosition`
|
|
3731
|
+
* is only true inside a `class_body`). Measured on a 1,500-file window: 148 of
|
|
3732
|
+
* 2,809 ownerless public symbols are such local declarations, and the
|
|
3733
|
+
* pre-existing `ExportInfo` loop published every one of them as an export. A
|
|
3734
|
+
* local function is callable from exactly one lexical scope, so publishing it
|
|
3735
|
+
* as module API would be a fabricated edge in `bb_migrate`.
|
|
3736
|
+
*
|
|
3737
|
+
* A per-symbol walk up from the symbol's own line would be an
|
|
3738
|
+
* O(symbols x tree) scan, and this lane's complexity envelope forbids that in a
|
|
3739
|
+
* hot path. So the tree is walked ONCE, carrying a boolean for "am I inside a
|
|
3740
|
+
* body", and the answer is recorded per declaration start line. Each symbol is
|
|
3741
|
+
* then two Set lookups. The walk is iterative, like the rest of this analyzer:
|
|
3742
|
+
* a kotlin file nests deeper than a worker thread's JS stack should be asked
|
|
3743
|
+
* for.
|
|
3744
|
+
*
|
|
3745
|
+
* Two earlier versions of the measuring probe got this wrong in ways that would
|
|
3746
|
+
* have certified a fabricated rule as measured, and both are recorded because
|
|
3747
|
+
* both produced a confident zero:
|
|
3748
|
+
* - one took the first ancestor whose TEXT contained the symbol's name, which
|
|
3749
|
+
* is `source_file` for every symbol, so it answered "top level" 64 times out
|
|
3750
|
+
* of 64;
|
|
3751
|
+
* - one used tree DEPTH, and `tree-sitter-kotlin` wraps a file in
|
|
3752
|
+
* `source_file`, so a top-level declaration sits at depth 2 and the probe
|
|
3753
|
+
* read 0 top-level declarations on a corpus where every file has some.
|
|
3754
|
+
*/
|
|
3755
|
+
kotlinDeclarationPlacement(root, lineCount) {
|
|
3756
|
+
const DECL = /* @__PURE__ */ new Set([
|
|
3757
|
+
"class_declaration",
|
|
3758
|
+
"object_declaration",
|
|
3759
|
+
"function_declaration",
|
|
3760
|
+
"property_declaration",
|
|
3761
|
+
"type_alias"
|
|
3762
|
+
]);
|
|
3763
|
+
const BODY = /* @__PURE__ */ new Set([
|
|
3764
|
+
"function_body",
|
|
3765
|
+
"lambda_literal",
|
|
3766
|
+
"anonymous_initializer",
|
|
3767
|
+
"secondary_constructor",
|
|
3768
|
+
"class_body",
|
|
3769
|
+
"object_literal",
|
|
3770
|
+
"catch_block",
|
|
3771
|
+
"do_while_statement",
|
|
3772
|
+
"while_statement",
|
|
3773
|
+
"for_statement"
|
|
3774
|
+
]);
|
|
3775
|
+
const rows = Math.max(1, lineCount) + 2;
|
|
3776
|
+
const local = new Uint8Array(rows);
|
|
3777
|
+
const top = new Uint8Array(rows);
|
|
3778
|
+
const nameRow = (n) => {
|
|
3779
|
+
for (let k = 0; k < n.namedChildCount; k++) {
|
|
3780
|
+
const c = n.namedChild(k);
|
|
3781
|
+
if (!c || c.type === "modifiers") continue;
|
|
3782
|
+
if (c.type === "type_identifier" || c.type === "simple_identifier") return c.startPosition.row + 1;
|
|
3783
|
+
if (c.type === "variable_declaration") {
|
|
3784
|
+
for (let m = 0; m < c.namedChildCount; m++) {
|
|
3785
|
+
const g = c.namedChild(m);
|
|
3786
|
+
if (g && g.type === "simple_identifier") return g.startPosition.row + 1;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
return null;
|
|
3791
|
+
};
|
|
3792
|
+
const stack = [{ n: root, inBody: false }];
|
|
3793
|
+
while (stack.length > 0) {
|
|
3794
|
+
const { n, inBody } = stack.pop();
|
|
3795
|
+
const here = inBody || BODY.has(n.type);
|
|
3796
|
+
if (DECL.has(n.type)) {
|
|
3797
|
+
const startRow = n.startPosition.row + 1;
|
|
3798
|
+
const named = nameRow(n);
|
|
3799
|
+
const endRow = named && named > startRow ? named : startRow;
|
|
3800
|
+
const into = here ? local : top;
|
|
3801
|
+
for (let r = startRow; r <= endRow && r < rows; r++) into[r] = 1;
|
|
3802
|
+
}
|
|
3803
|
+
for (let k = n.namedChildCount - 1; k >= 0; k--) {
|
|
3804
|
+
const c = n.namedChild(k);
|
|
3805
|
+
if (c) stack.push({ n: c, inBody: here });
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
return { local, top };
|
|
3809
|
+
}
|
|
3810
|
+
// KAD-MR3 K6 (M49.K1): performKotlinTypeInference was DEAD MACHINERY — it
|
|
3811
|
+
// gated on SymbolKind.Variable, but the extractor emits Property symbols
|
|
3812
|
+
// and no Variable kind, so every branch (is/has→Boolean,
|
|
3813
|
+
// remember/mutableStateOf→MutableState) was unreachable (probe: kinds
|
|
3814
|
+
// [class, method], zero Variables). Deleted rather than fed — fabricating
|
|
3815
|
+
// return types from NAME PREFIXES ("isValid→Boolean") is a guessing
|
|
3816
|
+
// heuristic masquerading as inference. metadata.hasTypeInference stays
|
|
3817
|
+
// (declared-type extraction IS real inference input).
|
|
3818
|
+
getKotlinFrameworkPatterns(frameworks, symbols) {
|
|
3819
|
+
const patterns = [];
|
|
3820
|
+
for (let i = 0; i < frameworks.length; i++) {
|
|
3821
|
+
const fw = frameworks[i];
|
|
3822
|
+
if (fw === "Jetpack Compose") {
|
|
3823
|
+
let hasUI = false, hasState = false;
|
|
3824
|
+
for (let j = 0; j < symbols.length; j++) {
|
|
3825
|
+
const s = symbols[j];
|
|
3826
|
+
if (s.metadata?.annotations) {
|
|
3827
|
+
for (let k = 0; k < s.metadata.annotations.length; k++) {
|
|
3828
|
+
if (s.metadata.annotations[k] === "Composable") hasUI = true;
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
if (s.qualified_calls) {
|
|
3832
|
+
for (let k = 0; k < s.qualified_calls.length; k++) {
|
|
3833
|
+
if (s.qualified_calls[k] === "remember" || s.qualified_calls[k] === "mutableStateOf") hasState = true;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
const fwPatterns = [];
|
|
3838
|
+
if (hasUI) fwPatterns.push("ui_components");
|
|
3839
|
+
if (hasState) fwPatterns.push("state_management");
|
|
3840
|
+
patterns.push({ framework: fw, patterns: fwPatterns });
|
|
3841
|
+
} else if (fw === "Android SDK") {
|
|
3842
|
+
let hasMvvm = false, hasLifecycle = false;
|
|
3843
|
+
for (let j = 0; j < symbols.length; j++) {
|
|
3844
|
+
const s = symbols[j];
|
|
3845
|
+
if (s.metadata?.superTypes) {
|
|
3846
|
+
for (let k = 0; k < s.metadata.superTypes.length; k++) {
|
|
3847
|
+
if (s.metadata.superTypes[k] === "ViewModel") hasMvvm = true;
|
|
3848
|
+
if (s.metadata.superTypes[k] === "Activity" || s.metadata.superTypes[k] === "Fragment") hasLifecycle = true;
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
const fwPatterns = [];
|
|
3853
|
+
if (hasMvvm) fwPatterns.push("mvvm_architecture");
|
|
3854
|
+
if (hasLifecycle) fwPatterns.push("android_lifecycle");
|
|
3855
|
+
patterns.push({ framework: fw, patterns: fwPatterns });
|
|
3856
|
+
} else if (fw === "Room") {
|
|
3857
|
+
let hasEntities = false, hasDaos = false;
|
|
3858
|
+
for (let j = 0; j < symbols.length; j++) {
|
|
3859
|
+
const s = symbols[j];
|
|
3860
|
+
if (s.metadata?.annotations) {
|
|
3861
|
+
for (let k = 0; k < s.metadata.annotations.length; k++) {
|
|
3862
|
+
if (s.metadata.annotations[k] === "Entity") hasEntities = true;
|
|
3863
|
+
if (s.metadata.annotations[k] === "Dao") hasDaos = true;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
const fwPatterns = [];
|
|
3868
|
+
if (hasEntities) fwPatterns.push("database_entities");
|
|
3869
|
+
if (hasDaos) fwPatterns.push("data_access_objects");
|
|
3870
|
+
patterns.push({ framework: fw, patterns: fwPatterns });
|
|
3871
|
+
} else if (fw === "Spring Boot") {
|
|
3872
|
+
let hasRest = false, hasService = false;
|
|
3873
|
+
for (let j = 0; j < symbols.length; j++) {
|
|
3874
|
+
const s = symbols[j];
|
|
3875
|
+
if (s.metadata?.annotations) {
|
|
3876
|
+
for (let k = 0; k < s.metadata.annotations.length; k++) {
|
|
3877
|
+
if (s.metadata.annotations[k] === "RestController") hasRest = true;
|
|
3878
|
+
if (s.metadata.annotations[k] === "Service") hasService = true;
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
const fwPatterns = [];
|
|
3883
|
+
if (hasRest) fwPatterns.push("rest_controllers");
|
|
3884
|
+
if (hasService) fwPatterns.push("services");
|
|
3885
|
+
patterns.push({ framework: fw, patterns: fwPatterns });
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
return patterns;
|
|
3889
|
+
}
|
|
3890
|
+
generateArchitecturalInsights(symbols, relationships) {
|
|
3891
|
+
const insights = [];
|
|
3892
|
+
let viewModelsCount = 0, composablesCount = 0, suspendCount = 0;
|
|
3893
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
3894
|
+
const s = symbols[i];
|
|
3895
|
+
if (s.metadata?.superTypes) {
|
|
3896
|
+
for (let j = 0; j < s.metadata.superTypes.length; j++) {
|
|
3897
|
+
if (s.metadata.superTypes[j] === "ViewModel") viewModelsCount++;
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
if (s.metadata?.annotations) {
|
|
3901
|
+
for (let j = 0; j < s.metadata.annotations.length; j++) {
|
|
3902
|
+
if (s.metadata.annotations[j] === "Composable") composablesCount++;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
if (s.metadata?.modifiers) {
|
|
3906
|
+
for (let j = 0; j < s.metadata.modifiers.length; j++) {
|
|
3907
|
+
if (s.metadata.modifiers[j] === "suspend") suspendCount++;
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
if (viewModelsCount > 0) {
|
|
3912
|
+
insights.push({
|
|
3913
|
+
type: "architecture",
|
|
3914
|
+
category: "android_mvvm",
|
|
3915
|
+
message: `Detected ${viewModelsCount} ViewModels, indicating MVVM architecture.`,
|
|
3916
|
+
severity: "info"
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
if (composablesCount > 0) {
|
|
3920
|
+
insights.push({
|
|
3921
|
+
type: "ui_framework",
|
|
3922
|
+
category: "compose",
|
|
3923
|
+
message: `Jetpack Compose UI detected with ${composablesCount} composable functions.`,
|
|
3924
|
+
severity: "info"
|
|
3925
|
+
});
|
|
3926
|
+
}
|
|
3927
|
+
if (suspendCount > 0) {
|
|
3928
|
+
insights.push({
|
|
3929
|
+
type: "concurrency",
|
|
3930
|
+
category: "coroutines",
|
|
3931
|
+
message: `Coroutine usage detected with ${suspendCount} suspend functions.`,
|
|
3932
|
+
severity: "info"
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
return insights;
|
|
3936
|
+
}
|
|
3937
|
+
calculateArchitecturalScore(symbols, relationships, patterns) {
|
|
3938
|
+
let score = 80;
|
|
3939
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
3940
|
+
if (patterns[i] === "Jetpack Compose") score += 5;
|
|
3941
|
+
if (patterns[i] === "Coroutines") score += 5;
|
|
3942
|
+
if (patterns[i] === "Data Classes") score += 5;
|
|
3943
|
+
}
|
|
3944
|
+
return Math.min(100, score);
|
|
3945
|
+
}
|
|
3946
|
+
assessKotlinCodeQuality(symbols, relationships, patterns) {
|
|
3947
|
+
const score = this.calculateArchitecturalScore(symbols, relationships, patterns);
|
|
3948
|
+
if (score >= 90) return "excellent";
|
|
3949
|
+
if (score >= 80) return "good";
|
|
3950
|
+
if (score >= 60) return "fair";
|
|
3951
|
+
return "poor";
|
|
3952
|
+
}
|
|
3953
|
+
};
|
|
3954
|
+
export {
|
|
3955
|
+
RealKotlinAnalyzer
|
|
3956
|
+
};
|