@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,4677 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findDeclaringParent,
|
|
3
|
+
localEdgeLabel,
|
|
4
|
+
localEdgeName
|
|
5
|
+
} from "./chunk-OWISBAOF.js";
|
|
6
|
+
import {
|
|
7
|
+
ASTEngine
|
|
8
|
+
} from "./chunk-V44PGPA6.js";
|
|
9
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
10
|
+
import "./chunk-SK6YVHFZ.js";
|
|
11
|
+
import "./chunk-2BSKD7O3.js";
|
|
12
|
+
import "./chunk-KAET47ZL.js";
|
|
13
|
+
import "./chunk-KCDECRJY.js";
|
|
14
|
+
import "./chunk-MLKGABMK.js";
|
|
15
|
+
|
|
16
|
+
// src/ast/real-java-analyzer.ts
|
|
17
|
+
function stringIncludes(str, val) {
|
|
18
|
+
return str.indexOf(val) !== -1;
|
|
19
|
+
}
|
|
20
|
+
function isPlainJavaIdentifier(name) {
|
|
21
|
+
if (name.length === 0) return false;
|
|
22
|
+
const isStart = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 36 || c === 95;
|
|
23
|
+
const isPart = (c) => isStart(c) || c >= 48 && c <= 57;
|
|
24
|
+
if (!isStart(name.charCodeAt(0))) return false;
|
|
25
|
+
for (let i = 1; i < name.length; i++) {
|
|
26
|
+
if (!isPart(name.charCodeAt(i))) return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
function javaSigHasTypeVar(sig) {
|
|
31
|
+
const n = sig.length;
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < n) {
|
|
34
|
+
const c = sig[i];
|
|
35
|
+
if (c === "(" || c === "," || c === ":" || c === "|" || c === "[" || c === "<" || c === "&" || i === 0) {
|
|
36
|
+
let j = i;
|
|
37
|
+
while (j < n && (sig[j] === "(" || sig[j] === "[" || sig[j] === " " || sig[j] === ":" || sig[j] === ",")) j++;
|
|
38
|
+
if (j < n && sig[j] >= "A" && sig[j] <= "Z") {
|
|
39
|
+
let end = j + 1;
|
|
40
|
+
while (end < n && (sig[end] >= "A" && sig[end] <= "Z" || sig[end] >= "a" && sig[end] <= "z" || sig[end] >= "0" && sig[end] <= "9" || sig[end] === "_")) end++;
|
|
41
|
+
if (end === j + 1) return true;
|
|
42
|
+
i = end;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
i = j > i ? j : i + 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function splitByChar(text, delim) {
|
|
53
|
+
const out = [];
|
|
54
|
+
let start = 0;
|
|
55
|
+
for (let i = 0; i < text.length; i++) {
|
|
56
|
+
if (text[i] === delim) {
|
|
57
|
+
out.push(text.slice(start, i));
|
|
58
|
+
start = i + 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
out.push(text.slice(start));
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
function tokenizeWhitespace(text) {
|
|
65
|
+
const out = [];
|
|
66
|
+
let cur = "";
|
|
67
|
+
const flush = () => {
|
|
68
|
+
if (cur) {
|
|
69
|
+
out.push(cur);
|
|
70
|
+
cur = "";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
for (let i = 0; i < text.length; i++) {
|
|
74
|
+
const ch = text[i];
|
|
75
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v") {
|
|
76
|
+
flush();
|
|
77
|
+
} else {
|
|
78
|
+
cur += ch;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
flush();
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function stripJavaAnnotations(text) {
|
|
85
|
+
let out = "";
|
|
86
|
+
let i = 0;
|
|
87
|
+
while (i < text.length) {
|
|
88
|
+
const ch = text[i];
|
|
89
|
+
if (ch === "@") {
|
|
90
|
+
i++;
|
|
91
|
+
while (i < text.length) {
|
|
92
|
+
const c = text[i];
|
|
93
|
+
const isWord = c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "_" || c === "." || c === "$";
|
|
94
|
+
if (!isWord) break;
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
while (i < text.length && (text[i] === " " || text[i] === " " || text[i] === "\n" || text[i] === "\r")) i++;
|
|
98
|
+
if (text[i] === "(") {
|
|
99
|
+
let depth = 0;
|
|
100
|
+
while (i < text.length) {
|
|
101
|
+
if (text[i] === "(") depth++;
|
|
102
|
+
else if (text[i] === ")") {
|
|
103
|
+
depth--;
|
|
104
|
+
if (depth === 0) {
|
|
105
|
+
i++;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
while (i < text.length && (text[i] === " " || text[i] === " " || text[i] === "\n" || text[i] === "\r")) i++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
out += ch;
|
|
116
|
+
i++;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
var JAVA_FRAMEWORK_PRIORITY = [
|
|
121
|
+
"spring_data",
|
|
122
|
+
"spring_mvc",
|
|
123
|
+
"spring_core",
|
|
124
|
+
"spring",
|
|
125
|
+
"jpa",
|
|
126
|
+
"hibernate",
|
|
127
|
+
"mybatis",
|
|
128
|
+
"quarkus",
|
|
129
|
+
"micronaut",
|
|
130
|
+
"jackson",
|
|
131
|
+
"slf4j"
|
|
132
|
+
];
|
|
133
|
+
var JAVA_RESERVED = /* @__PURE__ */ new Set([
|
|
134
|
+
"abstract",
|
|
135
|
+
"assert",
|
|
136
|
+
"boolean",
|
|
137
|
+
"break",
|
|
138
|
+
"byte",
|
|
139
|
+
"case",
|
|
140
|
+
"catch",
|
|
141
|
+
"char",
|
|
142
|
+
"class",
|
|
143
|
+
"const",
|
|
144
|
+
"continue",
|
|
145
|
+
"default",
|
|
146
|
+
"do",
|
|
147
|
+
"double",
|
|
148
|
+
"else",
|
|
149
|
+
"enum",
|
|
150
|
+
"extends",
|
|
151
|
+
"final",
|
|
152
|
+
"finally",
|
|
153
|
+
"float",
|
|
154
|
+
"for",
|
|
155
|
+
"goto",
|
|
156
|
+
"if",
|
|
157
|
+
"implements",
|
|
158
|
+
"import",
|
|
159
|
+
"instanceof",
|
|
160
|
+
"int",
|
|
161
|
+
"interface",
|
|
162
|
+
"long",
|
|
163
|
+
"native",
|
|
164
|
+
"new",
|
|
165
|
+
"package",
|
|
166
|
+
"private",
|
|
167
|
+
"protected",
|
|
168
|
+
"public",
|
|
169
|
+
"return",
|
|
170
|
+
"short",
|
|
171
|
+
"static",
|
|
172
|
+
"strictfp",
|
|
173
|
+
"super",
|
|
174
|
+
"switch",
|
|
175
|
+
"synchronized",
|
|
176
|
+
"this",
|
|
177
|
+
"throw",
|
|
178
|
+
"throws",
|
|
179
|
+
"transient",
|
|
180
|
+
"try",
|
|
181
|
+
"void",
|
|
182
|
+
"volatile",
|
|
183
|
+
"while",
|
|
184
|
+
"true",
|
|
185
|
+
"false",
|
|
186
|
+
"null",
|
|
187
|
+
"_",
|
|
188
|
+
"var",
|
|
189
|
+
"yield",
|
|
190
|
+
"record",
|
|
191
|
+
"sealed",
|
|
192
|
+
"permits",
|
|
193
|
+
"non-sealed"
|
|
194
|
+
]);
|
|
195
|
+
var JAVA_LANG_TYPES = /* @__PURE__ */ new Set([
|
|
196
|
+
"AbstractMethodError",
|
|
197
|
+
"AbstractStringBuilder",
|
|
198
|
+
"Appendable",
|
|
199
|
+
"ApplicationShutdownHooks",
|
|
200
|
+
"ArithmeticException",
|
|
201
|
+
"ArrayIndexOutOfBoundsException",
|
|
202
|
+
"ArrayStoreException",
|
|
203
|
+
"AssertionError",
|
|
204
|
+
"AssertionStatusDirectives",
|
|
205
|
+
"AutoCloseable",
|
|
206
|
+
"Boolean",
|
|
207
|
+
"BootstrapMethodError",
|
|
208
|
+
"Byte",
|
|
209
|
+
"Character",
|
|
210
|
+
"CharacterData",
|
|
211
|
+
"CharacterData00",
|
|
212
|
+
"CharacterData01",
|
|
213
|
+
"CharacterData02",
|
|
214
|
+
"CharacterData03",
|
|
215
|
+
"CharacterData0E",
|
|
216
|
+
"CharacterDataLatin1",
|
|
217
|
+
"CharacterDataPrivateUse",
|
|
218
|
+
"CharacterDataUndefined",
|
|
219
|
+
"CharacterName",
|
|
220
|
+
"CharSequence",
|
|
221
|
+
"Class",
|
|
222
|
+
"ClassCastException",
|
|
223
|
+
"ClassCircularityError",
|
|
224
|
+
"ClassFormatError",
|
|
225
|
+
"ClassLoader",
|
|
226
|
+
"ClassNotFoundException",
|
|
227
|
+
"ClassValue",
|
|
228
|
+
"Cloneable",
|
|
229
|
+
"CloneNotSupportedException",
|
|
230
|
+
"Comparable",
|
|
231
|
+
"Compiler",
|
|
232
|
+
"CompoundEnumeration",
|
|
233
|
+
"ConditionalSpecialCasing",
|
|
234
|
+
"Deprecated",
|
|
235
|
+
"Double",
|
|
236
|
+
"Enum",
|
|
237
|
+
"EnumConstantNotPresentException",
|
|
238
|
+
"Error",
|
|
239
|
+
"Exception",
|
|
240
|
+
"ExceptionInInitializerError",
|
|
241
|
+
"FdLibm",
|
|
242
|
+
"Float",
|
|
243
|
+
"FunctionalInterface",
|
|
244
|
+
"IllegalAccessError",
|
|
245
|
+
"IllegalAccessException",
|
|
246
|
+
"IllegalArgumentException",
|
|
247
|
+
"IllegalCallerException",
|
|
248
|
+
"IllegalMonitorStateException",
|
|
249
|
+
"IllegalStateException",
|
|
250
|
+
"IllegalThreadStateException",
|
|
251
|
+
"IncompatibleClassChangeError",
|
|
252
|
+
"IndexOutOfBoundsException",
|
|
253
|
+
"InheritableThreadLocal",
|
|
254
|
+
"InstantiationError",
|
|
255
|
+
"InstantiationException",
|
|
256
|
+
"Integer",
|
|
257
|
+
"InternalError",
|
|
258
|
+
"InterruptedException",
|
|
259
|
+
"Iterable",
|
|
260
|
+
"LayerInstantiationException",
|
|
261
|
+
"LinkageError",
|
|
262
|
+
"LiveStackFrame",
|
|
263
|
+
"LiveStackFrameInfo",
|
|
264
|
+
"Long",
|
|
265
|
+
"Math",
|
|
266
|
+
"Module",
|
|
267
|
+
"ModuleLayer",
|
|
268
|
+
"NamedPackage",
|
|
269
|
+
"NegativeArraySizeException",
|
|
270
|
+
"NoClassDefFoundError",
|
|
271
|
+
"NoSuchFieldError",
|
|
272
|
+
"NoSuchFieldException",
|
|
273
|
+
"NoSuchMethodError",
|
|
274
|
+
"NoSuchMethodException",
|
|
275
|
+
"NullPointerException",
|
|
276
|
+
"Number",
|
|
277
|
+
"NumberFormatException",
|
|
278
|
+
"Object",
|
|
279
|
+
"OutOfMemoryError",
|
|
280
|
+
"Override",
|
|
281
|
+
"Package",
|
|
282
|
+
"Process",
|
|
283
|
+
"ProcessBuilder",
|
|
284
|
+
"ProcessEnvironment",
|
|
285
|
+
"ProcessHandle",
|
|
286
|
+
"ProcessHandleImpl",
|
|
287
|
+
"ProcessImpl",
|
|
288
|
+
"PublicMethods",
|
|
289
|
+
"Readable",
|
|
290
|
+
"Record",
|
|
291
|
+
"ReflectiveOperationException",
|
|
292
|
+
"Runnable",
|
|
293
|
+
"Runtime",
|
|
294
|
+
"RuntimeException",
|
|
295
|
+
"RuntimePermission",
|
|
296
|
+
"SafeVarargs",
|
|
297
|
+
"SecurityException",
|
|
298
|
+
"SecurityManager",
|
|
299
|
+
"Short",
|
|
300
|
+
"Shutdown",
|
|
301
|
+
"StackFrameInfo",
|
|
302
|
+
"StackOverflowError",
|
|
303
|
+
"StackStreamFactory",
|
|
304
|
+
"StackTraceElement",
|
|
305
|
+
"StackWalker",
|
|
306
|
+
"StrictMath",
|
|
307
|
+
"String",
|
|
308
|
+
"StringBuffer",
|
|
309
|
+
"StringBuilder",
|
|
310
|
+
"StringCoding",
|
|
311
|
+
"StringConcatHelper",
|
|
312
|
+
"StringIndexOutOfBoundsException",
|
|
313
|
+
"StringLatin1",
|
|
314
|
+
"StringUTF16",
|
|
315
|
+
"SuppressWarnings",
|
|
316
|
+
"System",
|
|
317
|
+
"Terminator",
|
|
318
|
+
"Thread",
|
|
319
|
+
"ThreadDeath",
|
|
320
|
+
"ThreadGroup",
|
|
321
|
+
"ThreadLocal",
|
|
322
|
+
"Throwable",
|
|
323
|
+
"TypeNotPresentException",
|
|
324
|
+
"UnknownError",
|
|
325
|
+
"UnsatisfiedLinkError",
|
|
326
|
+
"UnsupportedClassVersionError",
|
|
327
|
+
"UnsupportedOperationException",
|
|
328
|
+
"VerifyError",
|
|
329
|
+
"VersionProps",
|
|
330
|
+
"VirtualMachineError",
|
|
331
|
+
"Void",
|
|
332
|
+
"WeakPairMap"
|
|
333
|
+
]);
|
|
334
|
+
function pickJavaFramework(frameworks) {
|
|
335
|
+
for (let i = 0; i < JAVA_FRAMEWORK_PRIORITY.length; i++) {
|
|
336
|
+
if (inArray(frameworks, JAVA_FRAMEWORK_PRIORITY[i])) return JAVA_FRAMEWORK_PRIORITY[i];
|
|
337
|
+
}
|
|
338
|
+
const sorted = [...frameworks].sort();
|
|
339
|
+
return sorted[0];
|
|
340
|
+
}
|
|
341
|
+
function inArray(arr, val) {
|
|
342
|
+
for (let i = 0; i < arr.length; i++) {
|
|
343
|
+
if (arr[i] === val) return true;
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
var JAVA_MODULE_DIRECTIVE_FIELDS = {
|
|
348
|
+
// requires [transitive|static] MODULE ;
|
|
349
|
+
requires_module_directive: [{ field: "module", role: "requires_module" }],
|
|
350
|
+
// exports PACKAGE [to field('modules', M) (, field('modules', M))*] ;
|
|
351
|
+
exports_module_directive: [
|
|
352
|
+
{ field: "package", role: "module_exports_package" },
|
|
353
|
+
{ field: "modules", role: "module_exports_package_to", unfieldedAfter: "package" }
|
|
354
|
+
],
|
|
355
|
+
// opens PACKAGE [to field('modules', M) (, field('modules', M))*] ;
|
|
356
|
+
opens_module_directive: [
|
|
357
|
+
{ field: "package", role: "module_opens_package" },
|
|
358
|
+
{ field: "modules", role: "module_opens_package_to", unfieldedAfter: "package" }
|
|
359
|
+
],
|
|
360
|
+
// uses TYPE ;
|
|
361
|
+
uses_module_directive: [{ field: "type", role: "module_uses_provided_type" }],
|
|
362
|
+
// provides field('provided', S) with I (, field('provider', I))* ; <- first I unfielded
|
|
363
|
+
provides_module_directive: [
|
|
364
|
+
{ field: "provided", role: "module_provides_service" },
|
|
365
|
+
{ field: "provider", role: "module_provides_service_with", unfieldedAfter: "provided" }
|
|
366
|
+
]
|
|
367
|
+
};
|
|
368
|
+
function isJavaNameNode(n) {
|
|
369
|
+
return !!n && (n.type === "identifier" || n.type === "scoped_identifier");
|
|
370
|
+
}
|
|
371
|
+
function javaTypeBaseName(node) {
|
|
372
|
+
if (!node) return "";
|
|
373
|
+
const t = node.type;
|
|
374
|
+
if (t === "type_identifier" || t === "identifier") return node.text;
|
|
375
|
+
if (t === "integral_type" || t === "floating_point_type" || t === "boolean_type" || t === "void_type") return node.text;
|
|
376
|
+
if (t === "generic_type") {
|
|
377
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
378
|
+
const c = node.namedChild(i);
|
|
379
|
+
if (!c) continue;
|
|
380
|
+
if (c.type === "type_identifier" || c.type === "scoped_type_identifier" || c.type === "generic_type") {
|
|
381
|
+
const b = javaTypeBaseName(c);
|
|
382
|
+
if (b) return b;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
if (t === "scoped_type_identifier") {
|
|
388
|
+
const parts = [];
|
|
389
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
390
|
+
const c = node.namedChild(i);
|
|
391
|
+
if (!c) continue;
|
|
392
|
+
if (c.type === "annotation" || c.type === "marker_annotation") continue;
|
|
393
|
+
const b = javaTypeBaseName(c);
|
|
394
|
+
if (b) parts.push(b);
|
|
395
|
+
}
|
|
396
|
+
return parts.join(".");
|
|
397
|
+
}
|
|
398
|
+
if (t === "annotated_type") {
|
|
399
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
400
|
+
const c = node.namedChild(i);
|
|
401
|
+
if (!c) continue;
|
|
402
|
+
if (c.type === "annotation" || c.type === "marker_annotation") continue;
|
|
403
|
+
const b = javaTypeBaseName(c);
|
|
404
|
+
if (b) return b;
|
|
405
|
+
}
|
|
406
|
+
return "";
|
|
407
|
+
}
|
|
408
|
+
if (t === "array_type") {
|
|
409
|
+
return javaTypeBaseName(node.childForFieldName("element"));
|
|
410
|
+
}
|
|
411
|
+
if (t === "field_access") {
|
|
412
|
+
const links = [];
|
|
413
|
+
let cur = node;
|
|
414
|
+
let guard = 0;
|
|
415
|
+
let done = false;
|
|
416
|
+
while (cur && guard++ < 64) {
|
|
417
|
+
if (cur.type === "field_access") {
|
|
418
|
+
const f = cur.childForFieldName("field");
|
|
419
|
+
const o = cur.childForFieldName("object");
|
|
420
|
+
if (!f || f.type !== "identifier") return "";
|
|
421
|
+
links.unshift(f.text);
|
|
422
|
+
cur = o;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (cur.type === "identifier" || cur.type === "type_identifier" || cur.type === "scoped_identifier") {
|
|
426
|
+
links.unshift(cur.text);
|
|
427
|
+
done = true;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
return "";
|
|
431
|
+
}
|
|
432
|
+
if (!done) return "";
|
|
433
|
+
return links.join(".");
|
|
434
|
+
}
|
|
435
|
+
return "";
|
|
436
|
+
}
|
|
437
|
+
var JAVA_LIBRARY_NAMESPACE_CITATIONS = ["jakarta", "androidx", "dotty.tools", "org.yaml"];
|
|
438
|
+
var JAVA_EXTERNAL_STATIC_IMPORT_ROOTS = ["java.", "javax.", "jakarta.", "jdk.", "sun.", "com.sun.", "androidx.", "dotty.tools.", "org.yaml."];
|
|
439
|
+
var RealJavaAnalyzer = class _RealJavaAnalyzer {
|
|
440
|
+
analyze(rootNode, content, filePath, context) {
|
|
441
|
+
const symbols = [];
|
|
442
|
+
const relationships = [];
|
|
443
|
+
const imports = [];
|
|
444
|
+
const exports = [];
|
|
445
|
+
const errors = [];
|
|
446
|
+
const frameworks = {};
|
|
447
|
+
const patterns = {};
|
|
448
|
+
let currentPackage = "";
|
|
449
|
+
let moduleName = "";
|
|
450
|
+
let cyclomaticComplexity = 0;
|
|
451
|
+
const symbolMap = /* @__PURE__ */ new Map();
|
|
452
|
+
const classHierarchy = /* @__PURE__ */ new Map();
|
|
453
|
+
const overrideTasks = [];
|
|
454
|
+
const methodsByOwner = /* @__PURE__ */ new Map();
|
|
455
|
+
const scopeStack = [];
|
|
456
|
+
const stack = [{ node: rootNode }];
|
|
457
|
+
const receiverTypes = this.collectJavaReceiverTypeIndex(rootNode);
|
|
458
|
+
while (stack.length > 0) {
|
|
459
|
+
const item = stack.pop();
|
|
460
|
+
const { node, parentContext, isExit } = item;
|
|
461
|
+
if (isExit) {
|
|
462
|
+
if (parentContext) {
|
|
463
|
+
this.processPhantomSymbols(parentContext, symbols, symbolMap, frameworks);
|
|
464
|
+
if (scopeStack.length > 0 && scopeStack[scopeStack.length - 1] === parentContext) {
|
|
465
|
+
scopeStack.pop();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (node.isMissing || node.hasError) {
|
|
471
|
+
if (node.type === "ERROR" || node.type === "MISSING") {
|
|
472
|
+
errors.push({
|
|
473
|
+
message: `Syntax ${node.type} detected.`,
|
|
474
|
+
range: this.nodeToRange(node),
|
|
475
|
+
severity: "warning"
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
let newContext = parentContext;
|
|
480
|
+
const type = node.type;
|
|
481
|
+
let pushedScope = false;
|
|
482
|
+
if (type === "if_statement" || type === "for_statement" || type === "enhanced_for_statement" || type === "while_statement" || type === "do_statement" || type === "catch_clause" || type === "switch_block_statement_group" || type === "switch_rule" || type === "ternary_expression") {
|
|
483
|
+
cyclomaticComplexity++;
|
|
484
|
+
} else if (type === "binary_expression") {
|
|
485
|
+
const op = node.childForFieldName("operator")?.text;
|
|
486
|
+
if (op === "&&" || op === "||") cyclomaticComplexity++;
|
|
487
|
+
}
|
|
488
|
+
if (type === "package_declaration") {
|
|
489
|
+
const pkgName = this.childText(node, "scoped_identifier") || this.childText(node, "identifier");
|
|
490
|
+
if (pkgName) currentPackage = pkgName;
|
|
491
|
+
} else if (type === "module_declaration") {
|
|
492
|
+
moduleName = node.childForFieldName("name")?.text ?? "";
|
|
493
|
+
this.emitJavaModuleDeclaration(node, symbols, relationships, imports);
|
|
494
|
+
} else if (type === "import_declaration") {
|
|
495
|
+
let isStatic = false;
|
|
496
|
+
let isAsterisk = false;
|
|
497
|
+
for (let k = 0; k < node.childCount; k++) {
|
|
498
|
+
const kid = node.child(k);
|
|
499
|
+
if (!kid) continue;
|
|
500
|
+
if (kid.type === "static") isStatic = true;
|
|
501
|
+
else if (kid.type === "asterisk") isAsterisk = true;
|
|
502
|
+
}
|
|
503
|
+
let moduleName2 = this.childText(node, "scoped_identifier") || this.childText(node, "identifier");
|
|
504
|
+
if (moduleName2) {
|
|
505
|
+
imports.push({
|
|
506
|
+
module: moduleName2,
|
|
507
|
+
items: [],
|
|
508
|
+
isDefault: false,
|
|
509
|
+
range: this.nodeToRange(node),
|
|
510
|
+
isNamespace: isAsterisk,
|
|
511
|
+
metadata: { isWildcard: isAsterisk, isStatic }
|
|
512
|
+
});
|
|
513
|
+
this.detectFrameworksFromImport(moduleName2, frameworks);
|
|
514
|
+
relationships.push({
|
|
515
|
+
source: currentPackage || "root",
|
|
516
|
+
target: moduleName2,
|
|
517
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
518
|
+
confidence: 1,
|
|
519
|
+
// M59 (#825): the line the imported module is NAMED on. 1.0 and not
|
|
520
|
+
// 0.9: `import_declaration` names the module in the grammar's own
|
|
521
|
+
// `scoped_identifier`, so this is the dependency the module pass reads,
|
|
522
|
+
// not a name matched from text. The `references` mention beside it keeps
|
|
523
|
+
// 0.9, which is the tier the family uses for a name-resolved mention.
|
|
524
|
+
//
|
|
525
|
+
// THE TARGET STAYS THE MODULE PATH, and the re-spelling tried here is
|
|
526
|
+
// MEASURED AND REJECTED rather than forgotten. `d5385c960` restated local
|
|
527
|
+
// imports to their leaf (+ `module` metadata) and measured "0 false locals" —
|
|
528
|
+
// a true statement about LOCALITY that is not this gate's question. Re-measured
|
|
529
|
+
// at cap 400 with `--index`: the gate indexes 4,020 qualifiedName aliases, so
|
|
530
|
+
// `com.example.core.Foo` ALREADY resolves to the declaration's qualified name,
|
|
531
|
+
// and the re-spelling moved `imports` resolved 104 → 64 with amb(decl) 0 → 40
|
|
532
|
+
// (unresolved unchanged at 263). A leaf whose FQN was unique can collide by
|
|
533
|
+
// name; the FQN cannot. The declaration is reached; the re-spelling lost it.
|
|
534
|
+
...this.lineField(this.findChildByType(node, "scoped_identifier") || this.findChildByType(node, "identifier"), node),
|
|
535
|
+
metadata: { relationshipType: "import_declaration", isJavaImport: true, isWildcard: isAsterisk, isStatic, isModuleDependency: true }
|
|
536
|
+
});
|
|
537
|
+
relationships.push({
|
|
538
|
+
source: currentPackage || "root",
|
|
539
|
+
target: moduleName2,
|
|
540
|
+
type: ASTEngine.RelationshipType.References,
|
|
541
|
+
confidence: 0.9,
|
|
542
|
+
// M59 (#825): the line the imported module is NAMED on.
|
|
543
|
+
...this.lineField(this.findChildByType(node, "scoped_identifier") || this.findChildByType(node, "identifier"), node),
|
|
544
|
+
metadata: { relationshipType: "import_declaration_mention", isJavaImport: true, isWildcard: isAsterisk, isStatic }
|
|
545
|
+
});
|
|
546
|
+
relationships.push({
|
|
547
|
+
source: currentPackage || "root",
|
|
548
|
+
target: moduleName2,
|
|
549
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
550
|
+
confidence: 1,
|
|
551
|
+
...this.lineField(this.findChildByType(node, "scoped_identifier") || this.findChildByType(node, "identifier"), node),
|
|
552
|
+
metadata: {
|
|
553
|
+
relationshipType: isStatic ? "static_member_import" : isAsterisk ? "type_on_demand_import" : "single_type_import",
|
|
554
|
+
isJavaImport: true,
|
|
555
|
+
isWildcard: isAsterisk,
|
|
556
|
+
isStatic,
|
|
557
|
+
fileLevel: true
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
} else if (type === "class_declaration" || type === "record_declaration" || type === "interface_declaration" || type === "enum_declaration" || type === "annotation_type_declaration") {
|
|
562
|
+
const name = this.childText(node, "identifier");
|
|
563
|
+
if (name) {
|
|
564
|
+
const kind = this.mapTypeToSymbolKind(type);
|
|
565
|
+
const modifiers = this.getJavaModifiers(node);
|
|
566
|
+
const annotations = this.getJavaAnnotations(node);
|
|
567
|
+
const visibility = this.getJavaVisibility(modifiers);
|
|
568
|
+
const fqn = this.buildFqn(currentPackage, scopeStack, name);
|
|
569
|
+
const inheritance = this.extractJavaInheritance(node);
|
|
570
|
+
const parents = [];
|
|
571
|
+
if (inheritance.extendsClass && inheritance.extendsClass.name) parents.push(inheritance.extendsClass.name);
|
|
572
|
+
for (let i = 0; i < inheritance.extendsInterfaces.length; i++) {
|
|
573
|
+
if (inheritance.extendsInterfaces[i].name) parents.push(inheritance.extendsInterfaces[i].name);
|
|
574
|
+
}
|
|
575
|
+
for (let i = 0; i < inheritance.implementsInterfaces.length; i++) {
|
|
576
|
+
if (inheritance.implementsInterfaces[i].name) parents.push(inheritance.implementsInterfaces[i].name);
|
|
577
|
+
}
|
|
578
|
+
classHierarchy.set(fqn, parents);
|
|
579
|
+
for (let k = 0; k < inheritance.permitsSubtypes.length; k++) {
|
|
580
|
+
if (!inheritance.permitsSubtypes[k].name) continue;
|
|
581
|
+
relationships.push({
|
|
582
|
+
source: name,
|
|
583
|
+
target: inheritance.permitsSubtypes[k].name,
|
|
584
|
+
type: ASTEngine.RelationshipType.References,
|
|
585
|
+
confidence: 1,
|
|
586
|
+
// M59 (#825): the subtype is named in the `permits` clause.
|
|
587
|
+
line_number: inheritance.permitsSubtypes[k].line,
|
|
588
|
+
metadata: { relationshipType: "permits" }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (inheritance.extendsClass && inheritance.extendsClass.name) {
|
|
592
|
+
relationships.push({
|
|
593
|
+
source: name,
|
|
594
|
+
target: inheritance.extendsClass.name,
|
|
595
|
+
type: ASTEngine.RelationshipType.Inheritance,
|
|
596
|
+
confidence: 1,
|
|
597
|
+
// M59 (#825): the parent's OWN line — a long `extends A\n implements B`
|
|
598
|
+
// list puts each parent on its own line, and the clause's start line
|
|
599
|
+
// named only the first of them.
|
|
600
|
+
line_number: inheritance.extendsClass.line,
|
|
601
|
+
metadata: { relationshipType: type === "interface_declaration" ? "extends_interface" : "extends" }
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
for (let k = 0; k < inheritance.extendsInterfaces.length; k++) {
|
|
605
|
+
const iface = inheritance.extendsInterfaces[k];
|
|
606
|
+
if (!iface.name) continue;
|
|
607
|
+
relationships.push({
|
|
608
|
+
source: name,
|
|
609
|
+
target: iface.name,
|
|
610
|
+
type: ASTEngine.RelationshipType.Inheritance,
|
|
611
|
+
confidence: 1,
|
|
612
|
+
line_number: iface.line,
|
|
613
|
+
metadata: { relationshipType: "extends_interface" }
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
for (let k = 0; k < inheritance.implementsInterfaces.length; k++) {
|
|
617
|
+
const iface = inheritance.implementsInterfaces[k];
|
|
618
|
+
if (!iface.name) continue;
|
|
619
|
+
relationships.push({
|
|
620
|
+
source: name,
|
|
621
|
+
target: iface.name,
|
|
622
|
+
type: ASTEngine.RelationshipType.Implementation,
|
|
623
|
+
confidence: 1,
|
|
624
|
+
line_number: iface.line,
|
|
625
|
+
metadata: { relationshipType: "implements" }
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
const annNames = this.getJavaAnnotationNames(node);
|
|
629
|
+
let isSpringBean = false;
|
|
630
|
+
let isEntity = false;
|
|
631
|
+
for (let i = 0; i < annNames.length; i++) {
|
|
632
|
+
const n = annNames[i];
|
|
633
|
+
if (n === "Service" || n === "Repository" || n === "RestController" || n === "Component") isSpringBean = true;
|
|
634
|
+
if (n === "Entity" || n === "Table") isEntity = true;
|
|
635
|
+
}
|
|
636
|
+
let tableName = name.toLowerCase();
|
|
637
|
+
let tableNameIsDeclared = false;
|
|
638
|
+
if (isEntity) {
|
|
639
|
+
frameworks["jpa"] = true;
|
|
640
|
+
patterns["entity_pattern"] = true;
|
|
641
|
+
if (inArray(annNames, "Table")) {
|
|
642
|
+
const n = this.getAnnotationValue(node, "Table", "name");
|
|
643
|
+
if (n) {
|
|
644
|
+
tableName = n;
|
|
645
|
+
tableNameIsDeclared = true;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
relationships.push({
|
|
649
|
+
source: name,
|
|
650
|
+
target: tableName,
|
|
651
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
652
|
+
// JD-J9 (M60 java deep review, 2026-09-27): a DECLARED table name
|
|
653
|
+
// is written in this file, so 1.0 is honest. The bare-`@Entity`
|
|
654
|
+
// default is not: `@Entity class Item` contains the string "item"
|
|
655
|
+
// nowhere, and JPA's documented default is the ENTITY NAME
|
|
656
|
+
// UNCHANGED (`AuctionItem`), so the lowercased form matches no
|
|
657
|
+
// standard naming strategy either. Publishing that at 1.0 with no
|
|
658
|
+
// marker states a certainty the source does not contain — and
|
|
659
|
+
// bb_graph, bb_impact and hover all repeat it verbatim as a real
|
|
660
|
+
// relationship to a table that may not exist.
|
|
661
|
+
//
|
|
662
|
+
// Measured consequence: it was also a standing FALSE FABRICATION in
|
|
663
|
+
// the corpus. `uses` joined java's compare set in this same wave
|
|
664
|
+
// (JD-J2), and the very first fresh draw reported
|
|
665
|
+
// `UNEXPLAINED Item.java :: uses Item -> item` at conf 1.0 — an
|
|
666
|
+
// unmatched CONFIDENT edge, which the harness counts as a
|
|
667
|
+
// fabrication, not a heuristic. No language-derived oracle can
|
|
668
|
+
// match it, because no language rule declares that name.
|
|
669
|
+
//
|
|
670
|
+
// So: declared stays 1.0; the inferred default drops below the
|
|
671
|
+
// 0.7 fabrication threshold and is marked, making it declared
|
|
672
|
+
// suspicion — visible in the heuristic bucket (surfaced by JD-J1)
|
|
673
|
+
// rather than counted as a relationship. Nothing is lost: the
|
|
674
|
+
// edge still exists, and a consumer can filter on `inferred`.
|
|
675
|
+
confidence: tableNameIsDeclared ? 1 : 0.6,
|
|
676
|
+
// M59 (#825): the table is named inside `@Table`, so that is the
|
|
677
|
+
// line the name is written on. Absent for the inferred default —
|
|
678
|
+
// there is no line to point at, and borrowing the class
|
|
679
|
+
// declaration's line would invent one (#823).
|
|
680
|
+
...this.annotationLine(node, "Table") ? { line_number: this.annotationLine(node, "Table") } : {},
|
|
681
|
+
metadata: { isDatabase: true, relationshipType: "mapped_table", table: tableName, inferred: !tableNameIsDeclared }
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
if (isSpringBean) {
|
|
685
|
+
frameworks["spring"] = true;
|
|
686
|
+
for (let i = 0; i < annNames.length; i++) {
|
|
687
|
+
const a = annNames[i];
|
|
688
|
+
if (a === "Service") {
|
|
689
|
+
patterns["service_layer"] = true;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
if (a === "Repository") {
|
|
693
|
+
patterns["repository_pattern"] = true;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
if (a === "RestController" || a === "Controller") {
|
|
697
|
+
patterns["mvc_pattern"] = true;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (inArray(annNames, "FeignClient")) {
|
|
703
|
+
const targetService = this.getAnnotationValue(node, "FeignClient", "name") || this.getAnnotationValue(node, "FeignClient", "value");
|
|
704
|
+
if (targetService) {
|
|
705
|
+
relationships.push({
|
|
706
|
+
source: name,
|
|
707
|
+
target: targetService,
|
|
708
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
709
|
+
confidence: 1,
|
|
710
|
+
// M59 (#825): the service is named inside `@FeignClient`.
|
|
711
|
+
...this.annotationLine(node, "FeignClient") ? { line_number: this.annotationLine(node, "FeignClient") } : {},
|
|
712
|
+
metadata: { isFeignClient: true, microservice: targetService }
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const javadoc = this.extractJavadocComment(node);
|
|
717
|
+
const sym = {
|
|
718
|
+
name,
|
|
719
|
+
kind,
|
|
720
|
+
// M14 INCOMING — the declaration's QUALIFIED name, published.
|
|
721
|
+
//
|
|
722
|
+
// The analyzer has always computed this (`buildFqn`, above, is the
|
|
723
|
+
// JLS name: package + nesting + own name) and used it internally for
|
|
724
|
+
// `classHierarchy`, then dropped it: no java symbol carried a
|
|
725
|
+
// `qualifiedName`, so `pool-worker.ts:1154` filled the field with its
|
|
726
|
+
// last-resort fallback and `script/m14/incoming-gate.ts` measured
|
|
727
|
+
// `qualifiedNameAliases: 0` for this lane — against 33,750 for
|
|
728
|
+
// typescript. What that costs is resolution, not tidiness: an edge
|
|
729
|
+
// whose target is spelled `com.acme.Foo` (a fully-qualified reference,
|
|
730
|
+
// an import's module, another package's type) has nothing to match,
|
|
731
|
+
// because the only key the pool publishes for this declaration is the
|
|
732
|
+
// leaf `Foo`.
|
|
733
|
+
qualifiedName: fqn,
|
|
734
|
+
// M59 (#825): the declaration's line, annotations skipped.
|
|
735
|
+
range: this.symbolRange(node),
|
|
736
|
+
visibility,
|
|
737
|
+
comments: javadoc ? [javadoc] : [],
|
|
738
|
+
decorators: annotations,
|
|
739
|
+
genericParameters: this.getJavaGenericParameters(node),
|
|
740
|
+
ownerName: parentContext?.name,
|
|
741
|
+
ownerKind: parentContext?.kind,
|
|
742
|
+
metadata: {
|
|
743
|
+
nodeType: type,
|
|
744
|
+
modifiers,
|
|
745
|
+
annotations,
|
|
746
|
+
isPublic: visibility === "public",
|
|
747
|
+
isAbstract: modifiers.indexOf("abstract") !== -1 || type === "interface_declaration",
|
|
748
|
+
...isSpringBean ? { spring_bean: true } : {}
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
if (type === "record_declaration") {
|
|
752
|
+
sym.parameters = this.extractJavaParameters(node, relationships, name);
|
|
753
|
+
const formalParams = this.findChildByType(node, "formal_parameters");
|
|
754
|
+
if (formalParams) {
|
|
755
|
+
for (let i = 0; i < formalParams.namedChildCount; i++) {
|
|
756
|
+
const comp = formalParams.namedChild(i);
|
|
757
|
+
if (!comp) continue;
|
|
758
|
+
if (comp.type !== "formal_parameter" && comp.type !== "spread_parameter") continue;
|
|
759
|
+
const compName = comp.type === "spread_parameter" ? (() => {
|
|
760
|
+
const decl = this.findChildByType(comp, "variable_declarator");
|
|
761
|
+
return decl ? this.findChildByType(decl, "identifier") : null;
|
|
762
|
+
})() : comp.childForFieldName("name");
|
|
763
|
+
if (!compName) continue;
|
|
764
|
+
let compReturnType = void 0;
|
|
765
|
+
if (comp.type === "spread_parameter") {
|
|
766
|
+
for (let cIdx = 0; cIdx < comp.namedChildCount; cIdx++) {
|
|
767
|
+
const c = comp.namedChild(cIdx);
|
|
768
|
+
if (c && c.type !== "modifiers" && !stringIncludes(c.type, "annotation") && c.type !== "variable_declarator") {
|
|
769
|
+
compReturnType = c.text + "[]";
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
compReturnType = this.extractJavaParameterType(comp);
|
|
775
|
+
}
|
|
776
|
+
const compSym = {
|
|
777
|
+
name: compName.text,
|
|
778
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
779
|
+
// M59 (#825): a record component's own line (its modifiers,
|
|
780
|
+
// which carry any parameter annotations, are skipped).
|
|
781
|
+
range: this.symbolRange(comp),
|
|
782
|
+
returnType: compReturnType,
|
|
783
|
+
visibility: "public",
|
|
784
|
+
ownerName: name,
|
|
785
|
+
ownerKind: kind,
|
|
786
|
+
metadata: { nodeType: "record_component" }
|
|
787
|
+
};
|
|
788
|
+
symbols.push(compSym);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
symbols.push(sym);
|
|
793
|
+
symbolMap.set(fqn, sym);
|
|
794
|
+
if (!symbolMap.has(name)) symbolMap.set(name, sym);
|
|
795
|
+
this.emitJavaTypeParameterSymbols(node, name, kind, type, symbols);
|
|
796
|
+
newContext = {
|
|
797
|
+
node,
|
|
798
|
+
name,
|
|
799
|
+
kind,
|
|
800
|
+
fqn,
|
|
801
|
+
symbolRef: sym,
|
|
802
|
+
visibility,
|
|
803
|
+
isAbstract: inArray(modifiers, "abstract"),
|
|
804
|
+
isInterface: type === "interface_declaration",
|
|
805
|
+
isRecord: type === "record_declaration",
|
|
806
|
+
annotations,
|
|
807
|
+
fields: [],
|
|
808
|
+
parents
|
|
809
|
+
};
|
|
810
|
+
scopeStack.push(newContext);
|
|
811
|
+
pushedScope = true;
|
|
812
|
+
}
|
|
813
|
+
} else if (type === "method_declaration" || type === "constructor_declaration" || type === "compact_constructor_declaration" || type === "annotation_type_element_declaration") {
|
|
814
|
+
const nameNode = this.findChildByType(node, "identifier");
|
|
815
|
+
const name = nameNode ? nameNode.text : null;
|
|
816
|
+
if (name) {
|
|
817
|
+
const isConstructor = type === "constructor_declaration" || type === "compact_constructor_declaration";
|
|
818
|
+
const isCompactCtor = type === "compact_constructor_declaration";
|
|
819
|
+
const modifiers = this.getJavaModifiers(node);
|
|
820
|
+
const annotations = this.getJavaAnnotations(node);
|
|
821
|
+
const visibility = this.getJavaVisibility(modifiers);
|
|
822
|
+
const parameters = isCompactCtor && parentContext ? this.extractJavaParameters(parentContext.node, relationships, parentContext.name) : this.extractJavaParameters(node, relationships, parentContext ? parentContext.name : void 0);
|
|
823
|
+
let sigParams = "";
|
|
824
|
+
for (let i = 0; i < parameters.length; i++) {
|
|
825
|
+
if (i > 0) sigParams += ",";
|
|
826
|
+
sigParams += parameters[i].type || "unknown";
|
|
827
|
+
}
|
|
828
|
+
const signature = `(${sigParams})`;
|
|
829
|
+
const fqn = parentContext ? `${parentContext.fqn}.${name}${signature}` : `${name}${signature}`;
|
|
830
|
+
const localOwnerPrefix = parentContext ? `${parentContext.name}.${name}${signature}` : `${name}${signature}`;
|
|
831
|
+
const returnType = isConstructor ? void 0 : this.extractJavaReturnType(node);
|
|
832
|
+
if (!isConstructor && parentContext) {
|
|
833
|
+
const retTypeNode = node.childForFieldName("type");
|
|
834
|
+
if (retTypeNode) {
|
|
835
|
+
for (const { name: t, line } of this.collectJavaTypeNames(retTypeNode)) {
|
|
836
|
+
if (t === "var") continue;
|
|
837
|
+
relationships.push({
|
|
838
|
+
source: parentContext.name,
|
|
839
|
+
target: t,
|
|
840
|
+
type: ASTEngine.RelationshipType.References,
|
|
841
|
+
confidence: 0.95,
|
|
842
|
+
// M59 (#825): the line the return TYPE is named on.
|
|
843
|
+
line_number: line,
|
|
844
|
+
metadata: { relationshipType: "declared_type", declaredOn: "return_type" }
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
let isApiEndpoint = false;
|
|
850
|
+
let fullRoute = "";
|
|
851
|
+
let mappingAnnotation = "";
|
|
852
|
+
const methodAnnNames = this.getJavaAnnotationNames(node);
|
|
853
|
+
for (let i = 0; i < methodAnnNames.length; i++) {
|
|
854
|
+
const n = methodAnnNames[i];
|
|
855
|
+
if (n === "GetMapping" || n === "PostMapping" || n === "PutMapping" || n === "DeleteMapping" || n === "PatchMapping" || n === "RequestMapping" || n === "Path") {
|
|
856
|
+
mappingAnnotation = n;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (mappingAnnotation) {
|
|
861
|
+
isApiEndpoint = true;
|
|
862
|
+
let httpMethod = "GET";
|
|
863
|
+
if (mappingAnnotation === "PostMapping") httpMethod = "POST";
|
|
864
|
+
else if (mappingAnnotation === "PutMapping") httpMethod = "PUT";
|
|
865
|
+
else if (mappingAnnotation === "DeleteMapping") httpMethod = "DELETE";
|
|
866
|
+
else if (mappingAnnotation === "PatchMapping") httpMethod = "PATCH";
|
|
867
|
+
else if (mappingAnnotation === "Path") httpMethod = "ANY";
|
|
868
|
+
else if (mappingAnnotation === "RequestMapping") {
|
|
869
|
+
const m = this.getAnnotationValue(node, "RequestMapping", "method");
|
|
870
|
+
if (m) {
|
|
871
|
+
let verb = "";
|
|
872
|
+
for (let ci = 0; ci < m.length; ci++) {
|
|
873
|
+
const c = m[ci];
|
|
874
|
+
if (c >= "A" && c <= "Z" || c >= "a" && c <= "z") verb += c;
|
|
875
|
+
}
|
|
876
|
+
verb = verb.toUpperCase();
|
|
877
|
+
if (verb === "GET" || verb === "POST" || verb === "PUT" || verb === "DELETE" || verb === "PATCH" || verb === "HEAD" || verb === "OPTIONS") httpMethod = verb;
|
|
878
|
+
else httpMethod = "ANY";
|
|
879
|
+
} else httpMethod = "ANY";
|
|
880
|
+
}
|
|
881
|
+
const methodRoute = this.getAnnotationValue(node, mappingAnnotation, "value") || this.getAnnotationValue(node, mappingAnnotation, "path") || "";
|
|
882
|
+
let basePath = "";
|
|
883
|
+
if (parentContext) {
|
|
884
|
+
const classAnnNames = this.getJavaAnnotationNames(parentContext.node);
|
|
885
|
+
let classMapping = "";
|
|
886
|
+
for (let i = 0; i < classAnnNames.length; i++) {
|
|
887
|
+
if (classAnnNames[i] === "RequestMapping" || classAnnNames[i] === "Path") {
|
|
888
|
+
classMapping = classAnnNames[i];
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (classMapping) {
|
|
893
|
+
basePath = this.getAnnotationValue(parentContext.node, classMapping, "value") || this.getAnnotationValue(parentContext.node, classMapping, "path") || "";
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (basePath || methodRoute) {
|
|
897
|
+
fullRoute = "";
|
|
898
|
+
for (let i = 0; i < basePath.length + methodRoute.length; i++) {
|
|
899
|
+
const ch = i < basePath.length ? basePath[i] : methodRoute[i - basePath.length];
|
|
900
|
+
if (ch === "/" && fullRoute.length > 0 && fullRoute[fullRoute.length - 1] === "/") continue;
|
|
901
|
+
fullRoute += ch;
|
|
902
|
+
}
|
|
903
|
+
if (!fullRoute.startsWith("/")) fullRoute = "/" + fullRoute;
|
|
904
|
+
relationships.push({
|
|
905
|
+
source: localOwnerPrefix,
|
|
906
|
+
target: fullRoute,
|
|
907
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
908
|
+
confidence: 1,
|
|
909
|
+
// M59 (#825): the route text is written inside the mapping
|
|
910
|
+
// annotation, so that is the line that carries the name.
|
|
911
|
+
...this.annotationLine(node, mappingAnnotation) ? { line_number: this.annotationLine(node, mappingAnnotation) } : {},
|
|
912
|
+
metadata: { isApiEndpoint: true, httpMethod, route: fullRoute, relationshipType: "api_endpoint" }
|
|
913
|
+
});
|
|
914
|
+
} else {
|
|
915
|
+
isApiEndpoint = false;
|
|
916
|
+
}
|
|
917
|
+
if (isApiEndpoint) frameworks["spring_mvc"] = true;
|
|
918
|
+
}
|
|
919
|
+
let dbMapping = "";
|
|
920
|
+
const methodDbAnnNames = this.getJavaAnnotationNames(node);
|
|
921
|
+
for (let idx = 0; idx < methodDbAnnNames.length; idx++) {
|
|
922
|
+
const n = methodDbAnnNames[idx];
|
|
923
|
+
if (n === "Column" || n === "OneToMany" || n === "ManyToOne" || n === "OneToOne" || n === "ManyToMany") {
|
|
924
|
+
dbMapping = n;
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (dbMapping && parentContext) {
|
|
929
|
+
let target = name.startsWith("get") ? name.charAt(3).toLowerCase() + name.slice(4) : name;
|
|
930
|
+
const explicit = this.getAnnotationValue(node, dbMapping, "name");
|
|
931
|
+
if (explicit) target = explicit;
|
|
932
|
+
relationships.push({
|
|
933
|
+
source: localOwnerPrefix,
|
|
934
|
+
target,
|
|
935
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
936
|
+
confidence: 1,
|
|
937
|
+
// M59 (#825): the column is named inside its mapping annotation.
|
|
938
|
+
...this.annotationLine(node, dbMapping) ? { line_number: this.annotationLine(node, dbMapping) } : {},
|
|
939
|
+
metadata: { isDatabase: true, relationshipType: "mapped_column" }
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const hasBody = !!this.findChildByType(node, "block");
|
|
943
|
+
const isAbstract = modifiers.indexOf("abstract") !== -1 || !!(parentContext?.isInterface && !hasBody && modifiers.indexOf("default") === -1);
|
|
944
|
+
const effectiveVisibility = visibility ?? (parentContext?.isInterface ? "public" : void 0);
|
|
945
|
+
const isAnnotatedOverride = annotations.indexOf("@Override") !== -1;
|
|
946
|
+
if (isAnnotatedOverride && parentContext) {
|
|
947
|
+
overrideTasks.push({
|
|
948
|
+
fqn,
|
|
949
|
+
methodName: name,
|
|
950
|
+
signature,
|
|
951
|
+
ownerFqn: parentContext.fqn,
|
|
952
|
+
arity: parameters.length,
|
|
953
|
+
explicit: true,
|
|
954
|
+
// M59 (#825): the OVERRIDING declaration's own line (annotations
|
|
955
|
+
// skipped, so an `@Override` method does not report the
|
|
956
|
+
// annotation) — captured here while the node is in hand rather
|
|
957
|
+
// than resolved later by name, which could match namesakes.
|
|
958
|
+
line: this.declarationLineNode(node).startPosition.row + 1
|
|
959
|
+
});
|
|
960
|
+
} else if (parentContext && !isConstructor && modifiers.indexOf("static") === -1 && visibility !== "private") {
|
|
961
|
+
overrideTasks.push({
|
|
962
|
+
fqn,
|
|
963
|
+
methodName: name,
|
|
964
|
+
signature,
|
|
965
|
+
ownerFqn: parentContext.fqn,
|
|
966
|
+
arity: parameters.length,
|
|
967
|
+
explicit: false,
|
|
968
|
+
line: this.declarationLineNode(node).startPosition.row + 1
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
const methodDoc = this.extractJavadocComment(node);
|
|
972
|
+
const memberName = isConstructor ? parentContext?.name ?? name : name;
|
|
973
|
+
const sym = {
|
|
974
|
+
name: memberName,
|
|
975
|
+
kind: isConstructor ? ASTEngine.SymbolKind.Constructor : ASTEngine.SymbolKind.Method,
|
|
976
|
+
// M14 INCOMING — the member's QUALIFIED name, WITHOUT the signature.
|
|
977
|
+
//
|
|
978
|
+
// `fqn` above carries it (`Owner.method(int,String)`) and that is the
|
|
979
|
+
// right key for the override BFS, but no edge spells a signature: an
|
|
980
|
+
// `overrides` target is `Owner.method` and a `calls` target is
|
|
981
|
+
// `receiver.method`. Publishing the signature-bearing form would add a
|
|
982
|
+
// key nothing can match. Overloads therefore share one qualified name —
|
|
983
|
+
// correct for the JLS (a method's qualified name is `Type.method`;
|
|
984
|
+
// overloading is a signature matter) and harmless to the candidate
|
|
985
|
+
// count, which counts DECLARING FILES, so N overloads in one file are
|
|
986
|
+
// still one candidate.
|
|
987
|
+
qualifiedName: parentContext ? `${parentContext.fqn}.${memberName}` : memberName,
|
|
988
|
+
// M59 (#825): the declaration's line, annotations skipped.
|
|
989
|
+
range: this.symbolRange(node),
|
|
990
|
+
comments: methodDoc ? [methodDoc] : [],
|
|
991
|
+
complexity: this.calculateJavaMethodComplexity(node),
|
|
992
|
+
visibility: effectiveVisibility,
|
|
993
|
+
parameters,
|
|
994
|
+
returnType,
|
|
995
|
+
qualified_calls: [],
|
|
996
|
+
apiContractPath: isApiEndpoint ? fullRoute : void 0,
|
|
997
|
+
isStatic: modifiers.indexOf("static") !== -1,
|
|
998
|
+
isAbstract,
|
|
999
|
+
decorators: annotations,
|
|
1000
|
+
genericParameters: this.getJavaGenericParameters(node),
|
|
1001
|
+
ownerName: parentContext?.name,
|
|
1002
|
+
ownerKind: parentContext?.kind,
|
|
1003
|
+
metadata: { nodeType: type, modifiers, annotations }
|
|
1004
|
+
};
|
|
1005
|
+
symbols.push(sym);
|
|
1006
|
+
symbolMap.set(fqn, sym);
|
|
1007
|
+
symbolMap.set(localOwnerPrefix, sym);
|
|
1008
|
+
if (parentContext?.fqn) {
|
|
1009
|
+
const leaf = parentContext.fqn.slice(parentContext.fqn.lastIndexOf(".") + 1);
|
|
1010
|
+
for (const key of parentContext.fqn === leaf ? [leaf] : [parentContext.fqn, leaf]) {
|
|
1011
|
+
let byName = methodsByOwner.get(key);
|
|
1012
|
+
if (!byName) {
|
|
1013
|
+
byName = /* @__PURE__ */ new Map();
|
|
1014
|
+
methodsByOwner.set(key, byName);
|
|
1015
|
+
}
|
|
1016
|
+
const arr = byName.get(name);
|
|
1017
|
+
if (arr) arr.push(sym);
|
|
1018
|
+
else byName.set(name, [sym]);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
this.emitJavaTypeParameterSymbols(node, name, ASTEngine.SymbolKind.Method, type, symbols);
|
|
1022
|
+
newContext = {
|
|
1023
|
+
node,
|
|
1024
|
+
name,
|
|
1025
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1026
|
+
fqn,
|
|
1027
|
+
symbolRef: sym,
|
|
1028
|
+
visibility: effectiveVisibility,
|
|
1029
|
+
isAbstract,
|
|
1030
|
+
isInterface: false,
|
|
1031
|
+
isRecord: false,
|
|
1032
|
+
annotations,
|
|
1033
|
+
fields: [],
|
|
1034
|
+
parents: []
|
|
1035
|
+
};
|
|
1036
|
+
scopeStack.push(newContext);
|
|
1037
|
+
pushedScope = true;
|
|
1038
|
+
}
|
|
1039
|
+
} else if (type === "constant_declaration") {
|
|
1040
|
+
const fieldType = this.extractJavaFieldType(node);
|
|
1041
|
+
for (let decIdx = 0; decIdx < node.namedChildCount; decIdx++) {
|
|
1042
|
+
const declarator = node.namedChild(decIdx);
|
|
1043
|
+
if (!declarator || declarator.type !== "variable_declarator") continue;
|
|
1044
|
+
const nameNode = this.findChildByType(declarator, "identifier");
|
|
1045
|
+
if (!nameNode || !parentContext) continue;
|
|
1046
|
+
symbols.push({
|
|
1047
|
+
name: nameNode.text,
|
|
1048
|
+
kind: ASTEngine.SymbolKind.Constant,
|
|
1049
|
+
range: this.nodeToRange(declarator),
|
|
1050
|
+
returnType: fieldType,
|
|
1051
|
+
visibility: "public",
|
|
1052
|
+
ownerName: parentContext.name,
|
|
1053
|
+
ownerKind: parentContext.kind,
|
|
1054
|
+
metadata: { nodeType: type }
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
const constTypeNode = node.childForFieldName("type");
|
|
1058
|
+
if (constTypeNode && parentContext) {
|
|
1059
|
+
for (const { name: t, line } of this.collectJavaTypeNames(constTypeNode)) {
|
|
1060
|
+
relationships.push({
|
|
1061
|
+
source: parentContext.name,
|
|
1062
|
+
target: t,
|
|
1063
|
+
type: ASTEngine.RelationshipType.References,
|
|
1064
|
+
confidence: 0.95,
|
|
1065
|
+
// M59 (#825): the line the constant's TYPE is named on.
|
|
1066
|
+
line_number: line,
|
|
1067
|
+
metadata: { relationshipType: "declared_type", declaredOn: type }
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
} else if (type === "field_declaration") {
|
|
1072
|
+
const modifiers = this.getJavaModifiers(node);
|
|
1073
|
+
const annotations = this.getJavaAnnotations(node);
|
|
1074
|
+
const visibility = this.getJavaVisibility(modifiers);
|
|
1075
|
+
const isStatic = inArray(modifiers, "static");
|
|
1076
|
+
const isConstant = modifiers.indexOf("final") !== -1 && isStatic;
|
|
1077
|
+
const fieldType = this.extractJavaFieldType(node);
|
|
1078
|
+
const fieldTypeNode = node.childForFieldName("type");
|
|
1079
|
+
if (fieldTypeNode && parentContext) {
|
|
1080
|
+
for (const { name: t, line } of this.collectJavaTypeNames(fieldTypeNode)) {
|
|
1081
|
+
relationships.push({
|
|
1082
|
+
source: parentContext.name,
|
|
1083
|
+
target: t,
|
|
1084
|
+
type: ASTEngine.RelationshipType.References,
|
|
1085
|
+
confidence: 0.95,
|
|
1086
|
+
// M59 (#825): the line the field's TYPE is named on.
|
|
1087
|
+
line_number: line,
|
|
1088
|
+
metadata: { relationshipType: "declared_type", declaredOn: type }
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const declarators = this.findAllChildrenByType(node, "variable_declarator");
|
|
1093
|
+
for (let decIdx = 0; decIdx < declarators.length; decIdx++) {
|
|
1094
|
+
const declarator = declarators[decIdx];
|
|
1095
|
+
const nameNode = this.findChildByType(declarator, "identifier");
|
|
1096
|
+
if (nameNode && parentContext) {
|
|
1097
|
+
const fieldName = nameNode.text;
|
|
1098
|
+
const localOwnerPrefix = `${parentContext.name}.${fieldName}`;
|
|
1099
|
+
const fieldDbAnnNames = this.getJavaAnnotationNames(node);
|
|
1100
|
+
let dbMapping = "";
|
|
1101
|
+
for (let idx = 0; idx < fieldDbAnnNames.length; idx++) {
|
|
1102
|
+
const n = fieldDbAnnNames[idx];
|
|
1103
|
+
if (n === "Column" || n === "OneToMany" || n === "ManyToOne" || n === "OneToOne" || n === "ManyToMany") {
|
|
1104
|
+
dbMapping = n;
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (dbMapping) {
|
|
1109
|
+
const explicit = this.getAnnotationValue(node, dbMapping, "name");
|
|
1110
|
+
const target = explicit ?? fieldName;
|
|
1111
|
+
const columnNameIsDeclared = !!explicit;
|
|
1112
|
+
relationships.push({
|
|
1113
|
+
source: localOwnerPrefix,
|
|
1114
|
+
target,
|
|
1115
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1116
|
+
confidence: columnNameIsDeclared ? 1 : 0.6,
|
|
1117
|
+
// M59 (#825): the column name is written inside its mapping annotation when explicit; the field line when implicit.
|
|
1118
|
+
...explicit ? { line_number: this.annotationLine(node, dbMapping, explicit) } : { line_number: nameNode.startPosition.row + 1 },
|
|
1119
|
+
metadata: { isDatabase: true, relationshipType: "mapped_column", inferred: !columnNameIsDeclared }
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
if (inArray(this.getJavaAnnotationNames(node), "Value")) {
|
|
1123
|
+
const valMatch = this.getAnnotationValue(node, "Value", "value");
|
|
1124
|
+
if (valMatch && valMatch.startsWith("${") && valMatch.endsWith("}")) {
|
|
1125
|
+
let configKey = valMatch.substring(2, valMatch.length - 1);
|
|
1126
|
+
if (stringIncludes(configKey, ":")) configKey = configKey.split(":")[0];
|
|
1127
|
+
relationships.push({
|
|
1128
|
+
source: localOwnerPrefix,
|
|
1129
|
+
target: configKey,
|
|
1130
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1131
|
+
confidence: 1,
|
|
1132
|
+
// M59 (#825): the key is written inside `@Value`.
|
|
1133
|
+
...this.annotationLine(node, "Value") ? { line_number: this.annotationLine(node, "Value") } : {},
|
|
1134
|
+
metadata: { isConfiguration: true, relationshipType: "config_property" }
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const fieldDoc = this.extractJavadocComment(node);
|
|
1139
|
+
const sym = {
|
|
1140
|
+
name: fieldName,
|
|
1141
|
+
kind: isConstant ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Property,
|
|
1142
|
+
range: this.nodeToRange(declarator),
|
|
1143
|
+
comments: fieldDoc ? [fieldDoc] : [],
|
|
1144
|
+
returnType: fieldType,
|
|
1145
|
+
visibility,
|
|
1146
|
+
isStatic,
|
|
1147
|
+
decorators: annotations,
|
|
1148
|
+
genericParameters: this.getJavaGenericParameters(node),
|
|
1149
|
+
ownerName: parentContext.name,
|
|
1150
|
+
ownerKind: parentContext.kind,
|
|
1151
|
+
metadata: { nodeType: type, modifiers, annotations, isConstant }
|
|
1152
|
+
};
|
|
1153
|
+
symbols.push(sym);
|
|
1154
|
+
parentContext.fields.push(sym);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} else if (type === "enum_constant") {
|
|
1158
|
+
const nameNode = this.findChildByType(node, "identifier");
|
|
1159
|
+
if (nameNode && parentContext) {
|
|
1160
|
+
symbols.push({
|
|
1161
|
+
name: nameNode.text,
|
|
1162
|
+
kind: ASTEngine.SymbolKind.EnumMember,
|
|
1163
|
+
// M59 (#825): the constant's own line, annotations skipped.
|
|
1164
|
+
range: this.symbolRange(node),
|
|
1165
|
+
visibility: "public",
|
|
1166
|
+
ownerName: parentContext.name,
|
|
1167
|
+
ownerKind: parentContext.kind,
|
|
1168
|
+
metadata: { nodeType: type }
|
|
1169
|
+
});
|
|
1170
|
+
const constantBody = this.findChildByType(node, "class_body");
|
|
1171
|
+
if (constantBody) {
|
|
1172
|
+
const anonName = `anonymous_${nameNode.text}`;
|
|
1173
|
+
const anonFqn = parentContext.fqn ? `${parentContext.fqn}.${anonName}` : anonName;
|
|
1174
|
+
const anonSym = {
|
|
1175
|
+
name: anonName,
|
|
1176
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1177
|
+
range: this.nodeToRange(constantBody),
|
|
1178
|
+
visibility: "package-private",
|
|
1179
|
+
ownerName: parentContext.name,
|
|
1180
|
+
ownerKind: parentContext.kind,
|
|
1181
|
+
metadata: { nodeType: type, isAnonymousClass: true, enumConstantBody: true, superTypes: [parentContext.name] }
|
|
1182
|
+
};
|
|
1183
|
+
symbols.push(anonSym);
|
|
1184
|
+
symbolMap.set(anonFqn, anonSym);
|
|
1185
|
+
classHierarchy.set(anonFqn, [parentContext.name]);
|
|
1186
|
+
newContext = {
|
|
1187
|
+
node: constantBody,
|
|
1188
|
+
name: anonName,
|
|
1189
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1190
|
+
fqn: anonFqn,
|
|
1191
|
+
symbolRef: anonSym,
|
|
1192
|
+
// A Scope's visibility union has no `package-private` member (the
|
|
1193
|
+
// SYMBOL can spell it; the scope carries the enclosing declaration's
|
|
1194
|
+
// visibility, exactly as the object_creation anonymous-class site does).
|
|
1195
|
+
visibility: parentContext.visibility,
|
|
1196
|
+
isAbstract: false,
|
|
1197
|
+
isInterface: false,
|
|
1198
|
+
isRecord: false,
|
|
1199
|
+
annotations: [],
|
|
1200
|
+
fields: [],
|
|
1201
|
+
parents: [parentContext.name]
|
|
1202
|
+
};
|
|
1203
|
+
scopeStack.push(newContext);
|
|
1204
|
+
pushedScope = true;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
} else if (type === "method_invocation") {
|
|
1208
|
+
const callExpression = this.buildJavaCallExpression(node);
|
|
1209
|
+
if (callExpression && !this.isJavaBuiltin(callExpression)) {
|
|
1210
|
+
const receiverOwner = this.javaReceiverOwnerType(node, receiverTypes, context?.projectIndex);
|
|
1211
|
+
const receiverMetadata = receiverOwner ? { owner: receiverOwner.type, ownerEvidence: receiverOwner.evidence } : {};
|
|
1212
|
+
if (parentContext && parentContext.kind === ASTEngine.SymbolKind.Method && parentContext.symbolRef) {
|
|
1213
|
+
if (!parentContext.symbolRef.qualified_calls) parentContext.symbolRef.qualified_calls = [];
|
|
1214
|
+
parentContext.symbolRef.qualified_calls.push(callExpression);
|
|
1215
|
+
let argumentCount;
|
|
1216
|
+
const argsNode = node.childForFieldName("arguments");
|
|
1217
|
+
if (argsNode) {
|
|
1218
|
+
argumentCount = 0;
|
|
1219
|
+
for (let ai = 0; ai < argsNode.namedChildCount; ai++) {
|
|
1220
|
+
const ac = argsNode.namedChild(ai);
|
|
1221
|
+
if (ac && ac.type !== "line_comment" && ac.type !== "block_comment") argumentCount++;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
relationships.push({
|
|
1225
|
+
source: parentContext.name || parentContext.symbolRef.name,
|
|
1226
|
+
target: callExpression.split(".").pop() || callExpression,
|
|
1227
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1228
|
+
confidence: 0.7,
|
|
1229
|
+
// M59 (#825): the line is where the CALL IS WRITTEN — the name
|
|
1230
|
+
// field of the method_invocation, not the invocation node's own
|
|
1231
|
+
// start. Measured with the M59 oracle on real java: a
|
|
1232
|
+
// `method_invocation` starts at its RECEIVER, so `a.b()\n .c()`
|
|
1233
|
+
// put c on the line a.b() opened on. java, kotlin and groovy
|
|
1234
|
+
// share this node shape, so this is the family pattern.
|
|
1235
|
+
...this.lineField(node.childForFieldName("name"), node),
|
|
1236
|
+
metadata: { javaCallExpression: callExpression, ...argumentCount !== void 0 ? { argumentCount } : {}, ...receiverMetadata }
|
|
1237
|
+
});
|
|
1238
|
+
} else {
|
|
1239
|
+
const typeScope = this.resolveEnclosingTypeScope(scopeStack);
|
|
1240
|
+
if (typeScope && typeScope.name) {
|
|
1241
|
+
relationships.push({
|
|
1242
|
+
source: typeScope.name,
|
|
1243
|
+
target: callExpression.split(".").pop() || callExpression,
|
|
1244
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1245
|
+
confidence: 0.7,
|
|
1246
|
+
// M59 (#825): same rule as the in-method branch above.
|
|
1247
|
+
...this.lineField(node.childForFieldName("name"), node),
|
|
1248
|
+
metadata: { javaCallExpression: callExpression, callSite: parentContext?.node?.type ?? node.parent?.type, inInitializer: true, ...receiverMetadata }
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
} else if (type === "explicit_constructor_invocation") {
|
|
1254
|
+
const text = node.text.trim();
|
|
1255
|
+
const source = parentContext?.name || parentContext?.symbolRef?.name || "global";
|
|
1256
|
+
let target = "super";
|
|
1257
|
+
let ctorKind = "super";
|
|
1258
|
+
if (text.startsWith("this")) {
|
|
1259
|
+
target = "this";
|
|
1260
|
+
ctorKind = "this";
|
|
1261
|
+
} else if (text.startsWith("super")) {
|
|
1262
|
+
target = "super";
|
|
1263
|
+
ctorKind = "super";
|
|
1264
|
+
} else {
|
|
1265
|
+
const q = text.split("(")[0].trim().split(".");
|
|
1266
|
+
target = q[0] + ".super";
|
|
1267
|
+
ctorKind = "qualified";
|
|
1268
|
+
}
|
|
1269
|
+
relationships.push({
|
|
1270
|
+
source,
|
|
1271
|
+
target,
|
|
1272
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1273
|
+
confidence: 1,
|
|
1274
|
+
// M59 (#825): the delegation is written exactly where the node is, and
|
|
1275
|
+
// `super`/`this` are unnamed keyword children — the invocation node's
|
|
1276
|
+
// own line is the only site that carries the name.
|
|
1277
|
+
line_number: node.startPosition.row + 1,
|
|
1278
|
+
metadata: { relationshipType: "constructor_invocation", constructorReceiver: ctorKind, callExpression: text }
|
|
1279
|
+
});
|
|
1280
|
+
} else if (type === "object_creation_expression") {
|
|
1281
|
+
const typeNode = this.findChildByType(node, "type_identifier") || this.findChildByType(node, "generic_type") || this.findChildByType(node, "scoped_type_identifier") || this.findChildByType(node, "identifier");
|
|
1282
|
+
if (typeNode) {
|
|
1283
|
+
const source = parentContext?.name || parentContext?.symbolRef?.name || "global";
|
|
1284
|
+
const typeName = typeNode.type === "identifier" ? typeNode.text : this.extractJavaTypeBaseName(typeNode);
|
|
1285
|
+
if (!typeName) continue;
|
|
1286
|
+
relationships.push({
|
|
1287
|
+
source,
|
|
1288
|
+
target: typeName,
|
|
1289
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1290
|
+
confidence: 0.95,
|
|
1291
|
+
// M59 (#825): the instantiated type is NAMED by its type node — that
|
|
1292
|
+
// node's line, not the statement's, which matters for
|
|
1293
|
+
// `new\n Foo()` and for a type named inside a multi-line argument.
|
|
1294
|
+
line_number: typeNode.startPosition.row + 1,
|
|
1295
|
+
metadata: { relationshipType: "instantiates", isNew: true }
|
|
1296
|
+
});
|
|
1297
|
+
const classBody = this.findChildByType(node, "class_body");
|
|
1298
|
+
if (classBody && parentContext) {
|
|
1299
|
+
const anonName = typeName ? `anonymous_${typeName}` : "anonymous_class";
|
|
1300
|
+
const anonFqn = parentContext.fqn ? `${parentContext.fqn}.${anonName}` : anonName;
|
|
1301
|
+
const anonSym = {
|
|
1302
|
+
name: anonName,
|
|
1303
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1304
|
+
range: this.nodeToRange(node),
|
|
1305
|
+
visibility: parentContext.visibility || "package-private",
|
|
1306
|
+
ownerName: parentContext.name,
|
|
1307
|
+
ownerKind: parentContext.kind,
|
|
1308
|
+
metadata: { nodeType: type, isAnonymousClass: true, superTypes: typeName ? [typeName] : [] }
|
|
1309
|
+
};
|
|
1310
|
+
symbols.push(anonSym);
|
|
1311
|
+
symbolMap.set(anonFqn, anonSym);
|
|
1312
|
+
classHierarchy.set(anonFqn, typeName ? [typeName] : []);
|
|
1313
|
+
newContext = {
|
|
1314
|
+
node: classBody,
|
|
1315
|
+
name: anonName,
|
|
1316
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1317
|
+
fqn: anonFqn,
|
|
1318
|
+
symbolRef: anonSym,
|
|
1319
|
+
visibility: parentContext.visibility,
|
|
1320
|
+
isAbstract: false,
|
|
1321
|
+
isInterface: false,
|
|
1322
|
+
isRecord: false,
|
|
1323
|
+
annotations: [],
|
|
1324
|
+
fields: [],
|
|
1325
|
+
parents: typeName ? [typeName] : []
|
|
1326
|
+
};
|
|
1327
|
+
scopeStack.push(newContext);
|
|
1328
|
+
pushedScope = true;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
} else if (type === "cast_expression" || type === "instanceof_expression" || type === "class_literal") {
|
|
1332
|
+
const typeNode = node.childForFieldName("type") || node.childForFieldName("right") || this.findChildByType(node, "type_identifier") || this.findChildByType(node, "generic_type") || this.findChildByType(node, "scoped_type_identifier") || // EXPORTS_JAVA2: `Foo[].class` — a class literal over an ARRAY type.
|
|
1333
|
+
// The `array_type` was missing from this list, and because the three
|
|
1334
|
+
// searches above only look at DIRECT children, `Foo[].class` resolved
|
|
1335
|
+
// to nothing and emitted NO edge at all (probe: `class X { Object o =
|
|
1336
|
+
// Foo[].class; }` produced only the `declared_type` edge for Object).
|
|
1337
|
+
// That is a missing edge rather than a wrong one, and it also left
|
|
1338
|
+
// `javaTypeBaseName`'s `array_type` branch unreachable from every
|
|
1339
|
+
// emission site — a branch that cannot match is dead code, which is
|
|
1340
|
+
// the question this lane is here to answer.
|
|
1341
|
+
this.findChildByType(node, "array_type");
|
|
1342
|
+
if (typeNode) {
|
|
1343
|
+
const typeName = typeNode.type === "type_identifier" ? typeNode.text : this.extractJavaTypeBaseName(typeNode);
|
|
1344
|
+
if (typeName && typeName !== "var") {
|
|
1345
|
+
const castTarget = typeName;
|
|
1346
|
+
relationships.push({
|
|
1347
|
+
source: parentContext?.name || parentContext?.symbolRef?.name || "global",
|
|
1348
|
+
// Cast arrays: `(String[]) o` — the EDGE TARGET is the element
|
|
1349
|
+
// type (String), not the JVM-erasure text with brackets
|
|
1350
|
+
// (probe 2026-09-08: `String[]` target joined nothing). The
|
|
1351
|
+
// declaredOn shape stays visible in metadata below.
|
|
1352
|
+
target: castTarget,
|
|
1353
|
+
type: ASTEngine.RelationshipType.References,
|
|
1354
|
+
confidence: 0.95,
|
|
1355
|
+
// M59 (#825): the cast/instanceof/class-literal type is named by
|
|
1356
|
+
// its type node.
|
|
1357
|
+
...this.lineField(typeNode, node),
|
|
1358
|
+
metadata: {
|
|
1359
|
+
relationshipType: type === "cast_expression" ? "cast_target" : type === "instanceof_expression" ? "instanceof_check" : "class_literal"
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
} else if (type === "record_pattern") {
|
|
1365
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1366
|
+
const c = node.namedChild(i);
|
|
1367
|
+
if (c && c.type !== "record_pattern_body") {
|
|
1368
|
+
const typeName = c.type === "type_identifier" ? c.text : this.extractJavaTypeBaseName(c);
|
|
1369
|
+
if (typeName && typeName !== "var") {
|
|
1370
|
+
relationships.push({
|
|
1371
|
+
source: parentContext?.name || parentContext?.symbolRef?.name || "global",
|
|
1372
|
+
target: typeName,
|
|
1373
|
+
type: ASTEngine.RelationshipType.References,
|
|
1374
|
+
confidence: 0.95,
|
|
1375
|
+
// M59 (#825): the pattern's type is named by the pattern node.
|
|
1376
|
+
...this.lineField(c, node),
|
|
1377
|
+
metadata: { relationshipType: "record_pattern" }
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
} else if (type === "array_creation_expression") {
|
|
1384
|
+
const elemNode = node.childForFieldName("type");
|
|
1385
|
+
if (elemNode) {
|
|
1386
|
+
const typeBearing = elemNode.type === "type_identifier" || elemNode.type === "scoped_type_identifier" || elemNode.type === "generic_type";
|
|
1387
|
+
const typeName = typeBearing ? elemNode.type === "type_identifier" ? elemNode.text : this.extractJavaTypeBaseName(elemNode) : null;
|
|
1388
|
+
if (typeName) {
|
|
1389
|
+
relationships.push({
|
|
1390
|
+
source: parentContext?.name || parentContext?.symbolRef?.name || "global",
|
|
1391
|
+
target: typeName,
|
|
1392
|
+
type: ASTEngine.RelationshipType.References,
|
|
1393
|
+
confidence: 0.95,
|
|
1394
|
+
// M59 (#825): `new T[...]` names T on the element-type node.
|
|
1395
|
+
...this.lineField(elemNode, node),
|
|
1396
|
+
metadata: { relationshipType: "array_creation" }
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
} else if (type === "method_reference") {
|
|
1401
|
+
const kids = [];
|
|
1402
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1403
|
+
const k = node.namedChild(i);
|
|
1404
|
+
if (k) kids.push(k);
|
|
1405
|
+
}
|
|
1406
|
+
const refSource = parentContext?.name || parentContext?.symbolRef?.name || "global";
|
|
1407
|
+
if (kids.length > 0) {
|
|
1408
|
+
const recv = kids[0];
|
|
1409
|
+
const recvType = recv.type;
|
|
1410
|
+
const recvName = recvType === "type_identifier" || recvType === "identifier" ? recv.text : recvType === "scoped_type_identifier" || recvType === "generic_type" || recvType === "field_access" ? this.extractJavaTypeBaseName(recv) : null;
|
|
1411
|
+
const isKeywordReceiver = recvName === "this" || recvName === "super";
|
|
1412
|
+
const isArrayTypeRef = recv.type === "array_type" || recv.text.endsWith("[]");
|
|
1413
|
+
const isQualifiedThis = recvName !== null && (recvName === "this" || recvName.endsWith(".this"));
|
|
1414
|
+
if (recvName && !isKeywordReceiver && !isArrayTypeRef && !isQualifiedThis && this.isJavaNameShape(recvName)) {
|
|
1415
|
+
relationships.push({
|
|
1416
|
+
source: refSource,
|
|
1417
|
+
target: recvName,
|
|
1418
|
+
type: ASTEngine.RelationshipType.References,
|
|
1419
|
+
confidence: 0.9,
|
|
1420
|
+
// M59 (#825): the referenced type is named by the receiver.
|
|
1421
|
+
...this.lineField(recv, node),
|
|
1422
|
+
metadata: { relationshipType: "method_reference_receiver", javaCallExpression: node.text }
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (kids.length >= 2) {
|
|
1427
|
+
relationships.push({
|
|
1428
|
+
source: refSource,
|
|
1429
|
+
target: kids[kids.length - 1].text,
|
|
1430
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1431
|
+
confidence: 0.8,
|
|
1432
|
+
// M59 (#825): the referenced method is named by its own node.
|
|
1433
|
+
...this.lineField(kids[kids.length - 1], node),
|
|
1434
|
+
metadata: { javaCallExpression: node.text }
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
} else if (type === "switch_label") {
|
|
1438
|
+
const pat = this.findChildByType(node, "pattern");
|
|
1439
|
+
if (pat) {
|
|
1440
|
+
const tp = this.findChildByType(pat, "type_pattern") || pat;
|
|
1441
|
+
const tnode = this.findChildByType(tp, "type_identifier") || this.findChildByType(tp, "generic_type") || this.findChildByType(tp, "scoped_type_identifier");
|
|
1442
|
+
if (tnode) {
|
|
1443
|
+
const typeName = tnode.type === "type_identifier" ? tnode.text : this.extractJavaTypeBaseName(tnode);
|
|
1444
|
+
if (typeName) {
|
|
1445
|
+
relationships.push({
|
|
1446
|
+
source: parentContext?.name || parentContext?.symbolRef?.name || "global",
|
|
1447
|
+
target: typeName,
|
|
1448
|
+
type: ASTEngine.RelationshipType.References,
|
|
1449
|
+
confidence: 0.95,
|
|
1450
|
+
// M59 (#825): the case pattern's type is named by its node.
|
|
1451
|
+
...this.lineField(tnode, node),
|
|
1452
|
+
metadata: { relationshipType: "switch_pattern" }
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
} else if (type === "local_variable_declaration" || type === "formal_parameter" || type === "spread_parameter" || type === "catch_formal_parameter" || // JD-J10 (M60 java deep review, 2026-09-27): two MORE declared-variable
|
|
1458
|
+
// positions were missing, so the branch above did not implement the rule
|
|
1459
|
+
// its own comment states. Both name a type in a declaration:
|
|
1460
|
+
// `for (Widget b : xs)` JLS 14.14 — the for-each variable
|
|
1461
|
+
// `try (Widget w = open())` JLS 14.20.3 — a try-with-resources
|
|
1462
|
+
// Both are `enhanced_for_statement` and `resource` in this grammar, and
|
|
1463
|
+
// NEITHER was in the list, so neither emitted a References edge.
|
|
1464
|
+
//
|
|
1465
|
+
// Found by turning `references` comparable for the first time (JD-J3):
|
|
1466
|
+
// the truth derives the for-each loop type, the analyzer had no edge for
|
|
1467
|
+
// it, and the differential reported it as MISSING. Verified with controls
|
|
1468
|
+
// so the finding could not be an artifact of the probe — `Widget` in a
|
|
1469
|
+
// local, a formal parameter and a catch parameter all emit, and `Widget`
|
|
1470
|
+
// appears ONLY in the loop variable and is silently absent. (An earlier
|
|
1471
|
+
// probe of mine appeared to show the for-each working; it was wrong —
|
|
1472
|
+
// the `Object` it saw came from a `List<Object>` in the method SIGNATURE,
|
|
1473
|
+
// emitted as `formal_parameter`. The loop type was missing in both
|
|
1474
|
+
// cases. Recording that because the first measurement was the misleading
|
|
1475
|
+
// one.)
|
|
1476
|
+
type === "enhanced_for_statement" || type === "resource") {
|
|
1477
|
+
const typeNode = node.childForFieldName("type") || this.findChildByType(node, "catch_type") || this.findChildByType(node, "type_identifier");
|
|
1478
|
+
if (typeNode) {
|
|
1479
|
+
for (const { name: t, line } of this.collectJavaTypeNames(typeNode)) {
|
|
1480
|
+
if (t === "var") continue;
|
|
1481
|
+
relationships.push({
|
|
1482
|
+
source: parentContext?.name || parentContext?.symbolRef?.name || "global",
|
|
1483
|
+
target: t,
|
|
1484
|
+
type: ASTEngine.RelationshipType.References,
|
|
1485
|
+
confidence: 0.95,
|
|
1486
|
+
// M59 (#825): the line the declared TYPE is named on.
|
|
1487
|
+
line_number: line,
|
|
1488
|
+
metadata: { relationshipType: "declared_type", declaredOn: type }
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
} else if (type === "annotation" || type === "marker_annotation") {
|
|
1493
|
+
const annotationNameNode = this.javaAnnotationNameNode(node);
|
|
1494
|
+
if (annotationNameNode) {
|
|
1495
|
+
const annotationSources = this.javaAnnotatedDeclarationSources(node, parentContext);
|
|
1496
|
+
for (let annotationIdx = 0; annotationIdx < annotationSources.length; annotationIdx++) {
|
|
1497
|
+
const annotationSource = annotationSources[annotationIdx];
|
|
1498
|
+
relationships.push({
|
|
1499
|
+
source: annotationSource.name,
|
|
1500
|
+
target: annotationNameNode.text,
|
|
1501
|
+
type: ASTEngine.RelationshipType.References,
|
|
1502
|
+
confidence: 0.95,
|
|
1503
|
+
// M59 (#825): the annotation TYPE is named by its name node — the
|
|
1504
|
+
// `@` sigil and an argument list are not the name.
|
|
1505
|
+
line_number: annotationNameNode.startPosition.row + 1,
|
|
1506
|
+
metadata: {
|
|
1507
|
+
relationshipType: "annotation",
|
|
1508
|
+
isAnnotation: true,
|
|
1509
|
+
annotatedKind: annotationSource.declKind,
|
|
1510
|
+
evidenceClass: "structural"
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
} else if (type === "type_parameter") {
|
|
1516
|
+
const boundNode = this.findChildByType(node, "type_bound");
|
|
1517
|
+
if (boundNode) {
|
|
1518
|
+
const paramNameNode = node.childForFieldName("name") || this.findChildByType(node, "type_identifier") || this.findChildByType(node, "identifier");
|
|
1519
|
+
const boundReadSource = parentContext?.name || parentContext?.symbolRef?.name || "global";
|
|
1520
|
+
for (const { name: boundType, line } of this.collectJavaTypeNames(boundNode)) {
|
|
1521
|
+
relationships.push({
|
|
1522
|
+
source: boundReadSource,
|
|
1523
|
+
target: boundType,
|
|
1524
|
+
type: ASTEngine.RelationshipType.References,
|
|
1525
|
+
confidence: 0.95,
|
|
1526
|
+
// M59 (#825): the line the BOUND type is named on.
|
|
1527
|
+
line_number: line,
|
|
1528
|
+
metadata: {
|
|
1529
|
+
relationshipType: "type_parameter_bound",
|
|
1530
|
+
...paramNameNode ? { boundFor: paramNameNode.text } : {},
|
|
1531
|
+
evidenceClass: "structural"
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
} else if (type === "throws") {
|
|
1537
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1538
|
+
const child = node.namedChild(i);
|
|
1539
|
+
if (!child) continue;
|
|
1540
|
+
for (const { name: t, line } of this.collectJavaTypeNames(child)) {
|
|
1541
|
+
relationships.push({
|
|
1542
|
+
source: parentContext?.name || parentContext?.symbolRef?.name || "global",
|
|
1543
|
+
target: t,
|
|
1544
|
+
type: ASTEngine.RelationshipType.References,
|
|
1545
|
+
confidence: 0.95,
|
|
1546
|
+
// M59 (#825): the line the THROWN type is named on.
|
|
1547
|
+
line_number: line,
|
|
1548
|
+
metadata: { relationshipType: "throws_type" }
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (type === "identifier") {
|
|
1554
|
+
const pt = node.parent?.type;
|
|
1555
|
+
if (pt === "class_declaration" || pt === "interface_declaration" || pt === "enum_declaration" || pt === "record_declaration") {
|
|
1556
|
+
const pat = node.text.endsWith("Factory") ? "factory" : node.text === "Singleton" ? "singleton" : node.text.endsWith("Builder") ? "builder" : null;
|
|
1557
|
+
if (pat) patterns[pat] = true;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (pushedScope) {
|
|
1561
|
+
stack.push({ node, parentContext: newContext, isExit: true });
|
|
1562
|
+
}
|
|
1563
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
1564
|
+
const child = node.namedChild(i);
|
|
1565
|
+
if (child) {
|
|
1566
|
+
stack.push({ node: child, parentContext: newContext });
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
this.resolveOverrides(overrideTasks, classHierarchy, symbolMap, relationships, context?.overrideCompanion, methodsByOwner);
|
|
1571
|
+
const primaryType = symbols.find((s) => {
|
|
1572
|
+
if (s.ownerName || s.scope) return false;
|
|
1573
|
+
return s.kind === ASTEngine.SymbolKind.Class || s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Enum;
|
|
1574
|
+
});
|
|
1575
|
+
if (primaryType) {
|
|
1576
|
+
for (const rel of relationships) {
|
|
1577
|
+
const isFileLevel = (rel.metadata ?? {}).fileLevel === true;
|
|
1578
|
+
if (!isFileLevel && (rel.type === ASTEngine.RelationshipType.References || rel.type === ASTEngine.RelationshipType.Imports) && (rel.source === currentPackage || rel.source === "root")) {
|
|
1579
|
+
rel.source = primaryType.name;
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
if (!isFileLevel && rel.type === ASTEngine.RelationshipType.Imports && (rel.metadata ?? {}).isJavaImport === true && (rel.source === currentPackage || rel.source === "root")) {
|
|
1583
|
+
rel.source = primaryType.name;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const finalFrameworks = [];
|
|
1588
|
+
for (const k in frameworks) finalFrameworks.push(k);
|
|
1589
|
+
const finalPatterns = [];
|
|
1590
|
+
for (const k in patterns) finalPatterns.push(k);
|
|
1591
|
+
let hasSpring = false;
|
|
1592
|
+
for (let i = 0; i < finalFrameworks.length; i++) {
|
|
1593
|
+
if (finalFrameworks[i] === "spring") {
|
|
1594
|
+
hasSpring = true;
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (hasSpring) {
|
|
1599
|
+
let hasSpringCore = false;
|
|
1600
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1601
|
+
if (this.hasJavaAnnotation(symbols[i], "Service") || this.hasJavaAnnotation(symbols[i], "Repository") || this.hasJavaAnnotation(symbols[i], "Component")) {
|
|
1602
|
+
hasSpringCore = true;
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
if (hasSpringCore) finalFrameworks.push("spring_core");
|
|
1607
|
+
}
|
|
1608
|
+
for (const s of symbols) {
|
|
1609
|
+
const _owner = s.ownerName || s.scope;
|
|
1610
|
+
if (_owner) {
|
|
1611
|
+
const isPhantomMember = s.metadata?.isPhantom === true;
|
|
1612
|
+
relationships.push({
|
|
1613
|
+
source: _owner,
|
|
1614
|
+
target: s.name,
|
|
1615
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1616
|
+
confidence: isPhantomMember ? 0.6 : 1,
|
|
1617
|
+
// M59 (#825): a containment edge has no call site, but it does have a
|
|
1618
|
+
// navigable one — the member's own declaration line, taken from the
|
|
1619
|
+
// symbol so the edge and the symbol cannot disagree. Omitted, never
|
|
1620
|
+
// zeroed, when the symbol has no line (#820/#821).
|
|
1621
|
+
//
|
|
1622
|
+
// A SYNTHESISED member has no declaration at all: java's phantoms
|
|
1623
|
+
// (Spring-Data repository methods carry the INTERFACE's range, lombok
|
|
1624
|
+
// getters/setters carry the FIELD's) name methods that are written
|
|
1625
|
+
// nowhere in the source, so the class/field line can never be the site
|
|
1626
|
+
// of the name. Same contract and same reasoning as kotlin's data-class
|
|
1627
|
+
// phantoms — the line is omitted, not guessed.
|
|
1628
|
+
...s.metadata?.isPhantom !== true && s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
1629
|
+
metadata: { isContained: true, ...isPhantomMember ? { inferred: true } : {} }
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
this.collectFieldAccessUses(rootNode, relationships, symbols, receiverTypes, context?.projectIndex);
|
|
1634
|
+
this.attachJavaImportedFrom(relationships, receiverTypes, symbols);
|
|
1635
|
+
this.attachJavaLibraryNamespaceCitations(relationships, symbols, receiverTypes, context?.projectIndex);
|
|
1636
|
+
this.attachJavaImportedBindingClaims(relationships, symbols, receiverTypes);
|
|
1637
|
+
this.attachJavaSingleTypeImportBindingClaims(relationships, symbols, receiverTypes, context?.projectIndex);
|
|
1638
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
1639
|
+
const s = symbols[i];
|
|
1640
|
+
const exportNodeType = String(s.metadata?.nodeType ?? "");
|
|
1641
|
+
const isTopLevelTypeDecl = exportNodeType === "class_declaration" || exportNodeType === "interface_declaration" || exportNodeType === "enum_declaration" || exportNodeType === "record_declaration" || exportNodeType === "annotation_type_declaration" || // JLS 7.7 — a MODULE is an importable unit too, named by another
|
|
1642
|
+
// module's `requires M` clause: `module com.example.widget` is written
|
|
1643
|
+
// once, in this file, and that is the name a `requires` directive binds.
|
|
1644
|
+
// java/module-exports-jpms pins it in `exports[]` with `kind: module`.
|
|
1645
|
+
// (The module symbol carries no package, so the export-EDGE post-pass
|
|
1646
|
+
// below declines it — `exportsWithNoPackage` — rather than fabricating a
|
|
1647
|
+
// `package -> module` row; the Namespace symbols it owns are members and
|
|
1648
|
+
// ride `contains`.)
|
|
1649
|
+
exportNodeType === "module_declaration";
|
|
1650
|
+
if (!s.metadata?.isPhantom && !s.ownerName && isTopLevelTypeDecl) {
|
|
1651
|
+
exports.push({ name: s.name, kind: s.kind, range: s.range, visibility: s.visibility });
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
let exportsWithNoPackage = 0;
|
|
1655
|
+
if (exports.length > 0) {
|
|
1656
|
+
const exported = /* @__PURE__ */ new Set();
|
|
1657
|
+
for (let i = 0; i < exports.length; i++) {
|
|
1658
|
+
const e = exports[i];
|
|
1659
|
+
const target = e.name;
|
|
1660
|
+
if (!target) continue;
|
|
1661
|
+
if (exported.has(target)) continue;
|
|
1662
|
+
exported.add(target);
|
|
1663
|
+
const isPublic = e.visibility === "public";
|
|
1664
|
+
if (!currentPackage) {
|
|
1665
|
+
exportsWithNoPackage++;
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
relationships.push({
|
|
1669
|
+
source: currentPackage,
|
|
1670
|
+
target,
|
|
1671
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
1672
|
+
confidence: isPublic ? 1 : 0.6,
|
|
1673
|
+
// M59 (#825): the exported DECLARATION's own line — the name is
|
|
1674
|
+
// written there, not at whatever construct encloses it. Absent rather
|
|
1675
|
+
// than 0 when the symbol carried no range (#823).
|
|
1676
|
+
...e.range?.startLine ? { line_number: e.range.startLine } : {},
|
|
1677
|
+
metadata: {
|
|
1678
|
+
relationshipType: "java_exportable_unit",
|
|
1679
|
+
exportForm: isPublic ? "public_top_level_type" : "package_private_top_level_type",
|
|
1680
|
+
...isPublic ? {} : { reasonCode: "package_private_top_level_type" },
|
|
1681
|
+
// The folded site's keys (see the note at the type-declaration
|
|
1682
|
+
// branch, where this edge used to be emitted a second time).
|
|
1683
|
+
//
|
|
1684
|
+
// `isJavaExport` is TRUE only on the importable tier: JLS 7.6 makes a
|
|
1685
|
+
// PUBLIC top-level type importable from another package, while a
|
|
1686
|
+
// package-private one is reachable by name only from its own package
|
|
1687
|
+
// (JLS 6.6.1) — which is exactly the claim the 0.6 tier and its
|
|
1688
|
+
// `reasonCode` discount, and marking it as an export outright would
|
|
1689
|
+
// assert the stronger fact.
|
|
1690
|
+
...isPublic ? { isJavaExport: true } : {},
|
|
1691
|
+
kind: String(e.kind ?? ""),
|
|
1692
|
+
// The join key, stated because it is not obvious: an `exports` edge is
|
|
1693
|
+
// `package -> simpleName`, so `source + "." + target` reconstructs the
|
|
1694
|
+
// dotted path an `imports` edge targets and the two labels meet without
|
|
1695
|
+
// a resolver.
|
|
1696
|
+
dottedName: `${currentPackage}.${target}`
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
this.classifyEvidenceTier(relationships);
|
|
1702
|
+
this.markExternalTargets(relationships, symbols, imports, context);
|
|
1703
|
+
return {
|
|
1704
|
+
symbols,
|
|
1705
|
+
relationships,
|
|
1706
|
+
imports,
|
|
1707
|
+
exports,
|
|
1708
|
+
errors,
|
|
1709
|
+
metadata: {
|
|
1710
|
+
// +1 base: the walk above counts raw decision points; the stored
|
|
1711
|
+
// metric is textbook cyclomatic complexity (family contract).
|
|
1712
|
+
complexity: cyclomaticComplexity + 1,
|
|
1713
|
+
linesOfCode: content.split("\n").length,
|
|
1714
|
+
cyclomaticComplexity: cyclomaticComplexity + 1,
|
|
1715
|
+
// Deterministic single-framework pick (hygiene 2026-09-08): the old
|
|
1716
|
+
// finalFrameworks[0] depended on import ORDER. Fixed priority list,
|
|
1717
|
+
// alphabetical tiebreak — same file always reports the same value.
|
|
1718
|
+
framework: pickJavaFramework(finalFrameworks),
|
|
1719
|
+
patterns: finalPatterns,
|
|
1720
|
+
currentPackage,
|
|
1721
|
+
// EXPORTS_JAVA2: the JPMS module this file declares ("" when the file is
|
|
1722
|
+
// not a module-info.java). Absent means "no module declared"; it never
|
|
1723
|
+
// means "a module nobody read" — that distinction is the whole reason the
|
|
1724
|
+
// field exists.
|
|
1725
|
+
...moduleName ? { moduleName } : {},
|
|
1726
|
+
detectedFrameworks: finalFrameworks,
|
|
1727
|
+
frameworkPatterns: this.getJavaFrameworkPatterns(finalFrameworks, symbols),
|
|
1728
|
+
hasTypeInference: true,
|
|
1729
|
+
hasFrameworkIntelligence: true,
|
|
1730
|
+
analysisVersion: "4.0.0-java-single-pass-stateless"
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* M14 #299 - evidence-tier classification for EVERY emitted relationship.
|
|
1736
|
+
* Tiers: structural (grammar clauses, conf 1.0) | usage_contextual (calls,
|
|
1737
|
+
* field access, companion-apply idioms) | import_based | reference_qualified
|
|
1738
|
+
* | inferred (sub-1.0 resolutions). Applied centrally at end of analyze()
|
|
1739
|
+
* so no emission site can forget it.
|
|
1740
|
+
*/
|
|
1741
|
+
classifyEvidenceTier(relationships) {
|
|
1742
|
+
for (const r of relationships) {
|
|
1743
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
1744
|
+
if (md.evidenceClass) continue;
|
|
1745
|
+
const t = String(r.type ?? r.relationshipType ?? "");
|
|
1746
|
+
const conf = typeof r.confidence === "number" ? r.confidence : 1;
|
|
1747
|
+
if (t === "contains" || t === "exports") {
|
|
1748
|
+
md.evidenceClass = "structural";
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
if (t === "imports" || t === "brain_import") {
|
|
1752
|
+
md.evidenceClass = "import_based";
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
if (t === "references") {
|
|
1756
|
+
md.evidenceClass = md.isJavaImport || md.isGroovyImport || md.isImport || md.isModuleDirective ? "import_based" : "reference_qualified";
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
if (t === "extends" || t === "implements") {
|
|
1760
|
+
md.evidenceClass = conf >= 1 ? "structural" : "inferred";
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
if (t === "overrides") {
|
|
1764
|
+
md.evidenceClass = conf >= 1 ? "structural" : conf >= 0.85 ? "usage_contextual" : "inferred";
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (t === "instantiates") {
|
|
1768
|
+
md.evidenceClass = md.isNew || conf >= 0.9 ? "structural" : "usage_contextual";
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
if (t === "imports") {
|
|
1772
|
+
md.evidenceClass = "import_based";
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
md.evidenceClass = "usage_contextual";
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* EXPORTS_JAVA2: one JPMS module-directive edge.
|
|
1780
|
+
*
|
|
1781
|
+
* `source` is the module that writes the directive and `target` is the name the
|
|
1782
|
+
* directive names — a package for `exports`/`opens`, a module for `requires`, a
|
|
1783
|
+
* type for `uses`, a service and its implementations for `provides`. The line is
|
|
1784
|
+
* the DIRECTIVE's own line, because that is where the name is written; the
|
|
1785
|
+
* grammar gives the named target a span inside it, and a `to a, b` list puts
|
|
1786
|
+
* every target on the directive's line anyway.
|
|
1787
|
+
*
|
|
1788
|
+
* Confidence 1.0 and `reasonCode: "module_directive"`: the dependency is
|
|
1789
|
+
* written in the file. Nothing here is inferred — the one thing a static reader
|
|
1790
|
+
* cannot know (whether the required module is on the module path at compile
|
|
1791
|
+
* time) is not what this edge claims.
|
|
1792
|
+
*/
|
|
1793
|
+
pushModuleDirective(relationships, module, targetNode, role) {
|
|
1794
|
+
const target = targetNode.text;
|
|
1795
|
+
if (!module || !target) return;
|
|
1796
|
+
const line = targetNode.startPosition.row >= 0 ? { line_number: targetNode.startPosition.row + 1 } : {};
|
|
1797
|
+
relationships.push({
|
|
1798
|
+
source: module,
|
|
1799
|
+
target,
|
|
1800
|
+
type: ASTEngine.RelationshipType.References,
|
|
1801
|
+
confidence: 1,
|
|
1802
|
+
...line,
|
|
1803
|
+
metadata: {
|
|
1804
|
+
relationshipType: role,
|
|
1805
|
+
isModuleDirective: true,
|
|
1806
|
+
isJavaModuleDirective: true,
|
|
1807
|
+
reasonCode: "module_directive",
|
|
1808
|
+
moduleName: module
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
findChild(node, predicate) {
|
|
1813
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1814
|
+
const child = node.child(i);
|
|
1815
|
+
if (child && predicate(child)) return child;
|
|
1816
|
+
}
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
resolveOverrides(tasks, classHierarchy, symbolMap, relationships, companion, methodsByOwner) {
|
|
1820
|
+
let suffixIndex = null;
|
|
1821
|
+
for (let taskIdx = 0; taskIdx < tasks.length; taskIdx++) {
|
|
1822
|
+
const task = tasks[taskIdx];
|
|
1823
|
+
let targetFqn = null;
|
|
1824
|
+
let confidence = 1;
|
|
1825
|
+
let reasonCode = "exact-in-file";
|
|
1826
|
+
if (!suffixIndex) {
|
|
1827
|
+
suffixIndex = /* @__PURE__ */ new Map();
|
|
1828
|
+
for (const fqn of classHierarchy.keys()) {
|
|
1829
|
+
const bare = fqn.slice(fqn.lastIndexOf(".") + 1);
|
|
1830
|
+
const arr = suffixIndex.get(bare);
|
|
1831
|
+
if (arr) arr.push(fqn);
|
|
1832
|
+
else suffixIndex.set(bare, [fqn]);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const queue = [];
|
|
1836
|
+
const ownerH = classHierarchy.get(task.ownerFqn);
|
|
1837
|
+
if (ownerH) {
|
|
1838
|
+
for (let i = 0; i < ownerH.length; i++) queue.push(ownerH[i]);
|
|
1839
|
+
}
|
|
1840
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1841
|
+
let head = 0;
|
|
1842
|
+
while (head < queue.length && !targetFqn) {
|
|
1843
|
+
const parent = queue[head++];
|
|
1844
|
+
if (seen.has(parent)) continue;
|
|
1845
|
+
seen.add(parent);
|
|
1846
|
+
const candidates = suffixIndex.get(parent) ?? [parent];
|
|
1847
|
+
for (const cand of candidates) {
|
|
1848
|
+
const potentialTargetFqn = `${cand}.${task.methodName}${task.signature}`;
|
|
1849
|
+
const baseSym = symbolMap.get(potentialTargetFqn);
|
|
1850
|
+
if (baseSym) {
|
|
1851
|
+
if (baseSym.visibility !== "private") {
|
|
1852
|
+
targetFqn = potentialTargetFqn;
|
|
1853
|
+
break;
|
|
1854
|
+
}
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
const members = methodsByOwner?.get(cand)?.get(task.methodName);
|
|
1858
|
+
if (members) {
|
|
1859
|
+
for (const member of members) {
|
|
1860
|
+
if (member.visibility === "private") continue;
|
|
1861
|
+
const params = member.parameters ?? [];
|
|
1862
|
+
if (params.length !== task.arity) continue;
|
|
1863
|
+
const baseSig = `(${params.map((p) => p.type || "unknown").join(",")})`;
|
|
1864
|
+
if (!javaSigHasTypeVar(baseSig) && !javaSigHasTypeVar(task.signature)) continue;
|
|
1865
|
+
targetFqn = `${cand}.${task.methodName}${task.signature}`;
|
|
1866
|
+
confidence = 0.9;
|
|
1867
|
+
reasonCode = "generic-substitution";
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
if (targetFqn) break;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
if (targetFqn) break;
|
|
1874
|
+
for (const cand of candidates) {
|
|
1875
|
+
const pH = classHierarchy.get(cand);
|
|
1876
|
+
if (pH) {
|
|
1877
|
+
for (let i = 0; i < pH.length; i++) queue.push(pH[i]);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (!targetFqn && companion && ownerH && ownerH.length > 0) {
|
|
1882
|
+
const declaring = findDeclaringParent(
|
|
1883
|
+
task.methodName,
|
|
1884
|
+
ownerH,
|
|
1885
|
+
companion,
|
|
1886
|
+
(t) => classHierarchy.get(t) ?? []
|
|
1887
|
+
);
|
|
1888
|
+
if (declaring) {
|
|
1889
|
+
targetFqn = `${declaring}.${task.methodName}${task.signature}`;
|
|
1890
|
+
confidence = 0.85;
|
|
1891
|
+
reasonCode = "companion-index";
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
if (!targetFqn && task.explicit && ownerH && ownerH.length > 0) {
|
|
1895
|
+
targetFqn = `${ownerH[0]}.${task.methodName}${task.signature}`;
|
|
1896
|
+
confidence = 0.5;
|
|
1897
|
+
reasonCode = "first-parent-guess";
|
|
1898
|
+
}
|
|
1899
|
+
if (targetFqn) {
|
|
1900
|
+
const localSource = localEdgeLabel(task.fqn);
|
|
1901
|
+
const localTarget = localEdgeName(targetFqn);
|
|
1902
|
+
const localQualified = localEdgeLabel(targetFqn);
|
|
1903
|
+
const declaringType = localQualified.lastIndexOf(".") > 0 ? localQualified.slice(0, localQualified.lastIndexOf(".")) : "";
|
|
1904
|
+
relationships.push({
|
|
1905
|
+
source: localSource,
|
|
1906
|
+
target: localTarget,
|
|
1907
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
1908
|
+
confidence,
|
|
1909
|
+
// M59 (#825): the override occurs at the OVERRIDING declaration, which
|
|
1910
|
+
// the task carries from the node itself. Omitted, never zeroed, when it
|
|
1911
|
+
// is somehow absent (#821).
|
|
1912
|
+
...task.line ? { line_number: task.line } : {},
|
|
1913
|
+
metadata: {
|
|
1914
|
+
relationshipType: "method_override",
|
|
1915
|
+
isJavaOverride: true,
|
|
1916
|
+
reasonCode,
|
|
1917
|
+
...declaringType ? { declaringType } : {}
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
processPhantomSymbols(scope, symbols, symbolMap, frameworks) {
|
|
1924
|
+
const isSpringData = frameworks["spring_data"] || frameworks["spring"];
|
|
1925
|
+
if (isSpringData && scope.isInterface) {
|
|
1926
|
+
let isRepo = false;
|
|
1927
|
+
for (let i = 0; i < scope.parents.length; i++) {
|
|
1928
|
+
if (scope.parents[i].indexOf("JpaRepository") !== -1 || scope.parents[i].indexOf("CrudRepository") !== -1) {
|
|
1929
|
+
isRepo = true;
|
|
1930
|
+
break;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if (isRepo) {
|
|
1934
|
+
const defaultMethods = ["save", "findById", "findAll", "deleteById", "delete", "count"];
|
|
1935
|
+
for (let j = 0; j < defaultMethods.length; j++) {
|
|
1936
|
+
const method = defaultMethods[j];
|
|
1937
|
+
const sym = {
|
|
1938
|
+
name: method,
|
|
1939
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1940
|
+
scope: scope.name,
|
|
1941
|
+
ownerName: scope.name,
|
|
1942
|
+
ownerKind: ASTEngine.SymbolKind.Interface,
|
|
1943
|
+
modifiers: ["phantom", "framework_generated", "spring-data"],
|
|
1944
|
+
range: scope.node ? this.nodeToRange(scope.node) : { startLine: 0, startColumn: 0, endLine: 0, endColumn: 0 },
|
|
1945
|
+
// Payload consistency (claim 21): the symbolMap key carries the
|
|
1946
|
+
// REAL signature (findById(Object), save(Object)) while the symbol
|
|
1947
|
+
// payload said parameters:[] — resolution and display disagreed.
|
|
1948
|
+
// A phantom method with an arity-carrying key has arity.
|
|
1949
|
+
parameters: method === "findById" || method === "deleteById" || method === "save" || method === "delete" ? [{ name: "entity", type: "Object", isOptional: false }] : [],
|
|
1950
|
+
returnType: "Object",
|
|
1951
|
+
metadata: { isPhantom: true }
|
|
1952
|
+
};
|
|
1953
|
+
symbols.push(sym);
|
|
1954
|
+
let phantomSig = "()";
|
|
1955
|
+
if (method === "findById" || method === "deleteById" || method === "save" || method === "delete") {
|
|
1956
|
+
phantomSig = "(Object)";
|
|
1957
|
+
}
|
|
1958
|
+
symbolMap.set(`${scope.fqn}.${method}${phantomSig}`, sym);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (!scope.isInterface && !scope.isEnum) {
|
|
1963
|
+
let isDataOrGetter = false;
|
|
1964
|
+
let isDataOrSetter = false;
|
|
1965
|
+
for (let i = 0; i < scope.annotations.length; i++) {
|
|
1966
|
+
const d = scope.annotations[i];
|
|
1967
|
+
let shortName = d.startsWith("@") ? d.slice(1) : d;
|
|
1968
|
+
const paren = shortName.indexOf("(");
|
|
1969
|
+
if (paren !== -1) shortName = shortName.slice(0, paren);
|
|
1970
|
+
const lastDot = shortName.lastIndexOf(".");
|
|
1971
|
+
if (lastDot !== -1) shortName = shortName.slice(lastDot + 1);
|
|
1972
|
+
if (shortName === "Data" || shortName === "Getter") isDataOrGetter = true;
|
|
1973
|
+
if (shortName === "Data" || shortName === "Setter") isDataOrSetter = true;
|
|
1974
|
+
}
|
|
1975
|
+
if (isDataOrGetter || isDataOrSetter) {
|
|
1976
|
+
for (let idx = 0; idx < scope.fields.length; idx++) {
|
|
1977
|
+
const prop = scope.fields[idx];
|
|
1978
|
+
const capitalizedName = prop.name.charAt(0).toUpperCase() + prop.name.slice(1);
|
|
1979
|
+
if (isDataOrGetter) {
|
|
1980
|
+
const sym = {
|
|
1981
|
+
name: `get${capitalizedName}`,
|
|
1982
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1983
|
+
scope: scope.name,
|
|
1984
|
+
ownerName: scope.name,
|
|
1985
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
1986
|
+
modifiers: ["phantom", "lombok_generated"],
|
|
1987
|
+
range: prop.range,
|
|
1988
|
+
parameters: [],
|
|
1989
|
+
returnType: prop.returnType || "Object",
|
|
1990
|
+
metadata: { isPhantom: true }
|
|
1991
|
+
};
|
|
1992
|
+
symbols.push(sym);
|
|
1993
|
+
symbolMap.set(`${scope.fqn}.get${capitalizedName}()`, sym);
|
|
1994
|
+
}
|
|
1995
|
+
if (isDataOrSetter) {
|
|
1996
|
+
const sym = {
|
|
1997
|
+
name: `set${capitalizedName}`,
|
|
1998
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1999
|
+
scope: scope.name,
|
|
2000
|
+
ownerName: scope.name,
|
|
2001
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
2002
|
+
modifiers: ["phantom", "lombok_generated"],
|
|
2003
|
+
range: prop.range,
|
|
2004
|
+
parameters: [{ name: "value", type: prop.returnType || "Object" }],
|
|
2005
|
+
returnType: "void",
|
|
2006
|
+
metadata: { isPhantom: true }
|
|
2007
|
+
};
|
|
2008
|
+
symbols.push(sym);
|
|
2009
|
+
symbolMap.set(`${scope.fqn}.set${capitalizedName}(${prop.returnType || "Object"})`, sym);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
detectFrameworksFromImport(mod, frameworks) {
|
|
2016
|
+
if (mod.startsWith("org.springframework")) frameworks["spring"] = true;
|
|
2017
|
+
if (mod.startsWith("org.springframework.web")) frameworks["spring_mvc"] = true;
|
|
2018
|
+
if (mod.startsWith("org.springframework.data")) frameworks["spring_data"] = true;
|
|
2019
|
+
if (mod.startsWith("javax.persistence") || mod.startsWith("jakarta.persistence")) frameworks["jpa"] = true;
|
|
2020
|
+
if (mod.startsWith("org.hibernate")) frameworks["hibernate"] = true;
|
|
2021
|
+
if (mod.startsWith("org.mybatis")) frameworks["mybatis"] = true;
|
|
2022
|
+
if (mod.startsWith("io.quarkus")) frameworks["quarkus"] = true;
|
|
2023
|
+
if (mod.startsWith("io.micronaut")) frameworks["micronaut"] = true;
|
|
2024
|
+
if (mod.startsWith("org.junit")) frameworks["junit"] = true;
|
|
2025
|
+
if (mod.startsWith("org.mockito")) frameworks["mockito"] = true;
|
|
2026
|
+
if (mod.startsWith("javax.servlet") || mod.startsWith("jakarta.servlet")) frameworks["servlet"] = true;
|
|
2027
|
+
if (mod.startsWith("javax.ws.rs") || mod.startsWith("jakarta.ws.rs")) frameworks["jax_rs"] = true;
|
|
2028
|
+
}
|
|
2029
|
+
mapTypeToSymbolKind(type) {
|
|
2030
|
+
if (type === "class_declaration" || type === "record_declaration") return ASTEngine.SymbolKind.Class;
|
|
2031
|
+
if (type === "interface_declaration") return ASTEngine.SymbolKind.Interface;
|
|
2032
|
+
if (type === "enum_declaration") return ASTEngine.SymbolKind.Enum;
|
|
2033
|
+
return ASTEngine.SymbolKind.Class;
|
|
2034
|
+
}
|
|
2035
|
+
buildFqn(pkg, scopeStack, name) {
|
|
2036
|
+
const parts = [];
|
|
2037
|
+
if (pkg) parts.push(pkg);
|
|
2038
|
+
for (let idx = 0; idx < scopeStack.length; idx++) {
|
|
2039
|
+
const scope = scopeStack[idx];
|
|
2040
|
+
if (scope.kind !== ASTEngine.SymbolKind.Method) {
|
|
2041
|
+
parts.push(scope.name);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
parts.push(name);
|
|
2045
|
+
return parts.join(".");
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Javadoc extraction: block_comment starting with '/**' immediately
|
|
2049
|
+
* preceding the declaration (previous sibling). Previously java symbols
|
|
2050
|
+
* carried NO comments - docstrings were silently lost (M14 #383).
|
|
2051
|
+
*/
|
|
2052
|
+
/**
|
|
2053
|
+
* Per-method cyclomatic complexity (1 + decision points). Previously java
|
|
2054
|
+
* method symbols stored complexity=1 always - only the file-level counter
|
|
2055
|
+
* was real. Kotlin parity (M14 #301 prerequisite).
|
|
2056
|
+
*/
|
|
2057
|
+
calculateJavaMethodComplexity(node) {
|
|
2058
|
+
let complexity = 1;
|
|
2059
|
+
const stack = [node];
|
|
2060
|
+
while (stack.length > 0) {
|
|
2061
|
+
const n = stack.pop();
|
|
2062
|
+
if (n.type === "if_statement" || n.type === "for_statement" || n.type === "enhanced_for_statement" || n.type === "while_statement" || n.type === "do_statement" || n.type === "switch_rule" || n.type === "switch_block_statement_group" || n.type === "catch_clause" || n.type === "ternary_expression") {
|
|
2063
|
+
complexity++;
|
|
2064
|
+
} else if (n.type === "binary_expression") {
|
|
2065
|
+
const op = n.childForFieldName("operator")?.text;
|
|
2066
|
+
if (op === "&&" || op === "||") complexity++;
|
|
2067
|
+
}
|
|
2068
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2069
|
+
const child = n.namedChild(i);
|
|
2070
|
+
if (child) stack.push(child);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return complexity;
|
|
2074
|
+
}
|
|
2075
|
+
extractJavadocComment(node) {
|
|
2076
|
+
const parent = node.parent;
|
|
2077
|
+
if (!parent) return void 0;
|
|
2078
|
+
let selfIdx = -1;
|
|
2079
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
2080
|
+
const c = parent.namedChild(i);
|
|
2081
|
+
if (c && c.startIndex === node.startIndex && c.endIndex === node.endIndex) {
|
|
2082
|
+
selfIdx = i;
|
|
2083
|
+
break;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
for (let i = selfIdx - 1; i >= 0; i--) {
|
|
2087
|
+
const sibling = parent.namedChild(i);
|
|
2088
|
+
if (!sibling) continue;
|
|
2089
|
+
if (sibling.type === "block_comment" && sibling.text.startsWith("/**")) return sibling.text;
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
return void 0;
|
|
2093
|
+
}
|
|
2094
|
+
nodeToRange(node) {
|
|
2095
|
+
return {
|
|
2096
|
+
startLine: node.startPosition.row + 1,
|
|
2097
|
+
startColumn: node.startPosition.column + 1,
|
|
2098
|
+
endLine: node.endPosition.row + 1,
|
|
2099
|
+
endColumn: node.endPosition.column + 1,
|
|
2100
|
+
startIndex: node.startIndex,
|
|
2101
|
+
endIndex: node.endIndex
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* M59 (#825): the `line_number` field for a relationship, taken from the node
|
|
2106
|
+
* that CARRIES THE NAME the edge is about.
|
|
2107
|
+
*
|
|
2108
|
+
* java shared scala's defect class in full: a `method_invocation` starts at
|
|
2109
|
+
* its receiver, a type expression's start is not the line of the types inside
|
|
2110
|
+
* it, and a definition node can start at its modifiers. The rule that holds in
|
|
2111
|
+
* all cases is one sentence — the line belongs to the name, never to the
|
|
2112
|
+
* construct that contains it — so callers pass the NAME node and this returns
|
|
2113
|
+
* nothing at all when there is none (never 0: #820/#821, 0 means "unknown" to
|
|
2114
|
+
* every reader, so unknown must be absent).
|
|
2115
|
+
*
|
|
2116
|
+
* `fallback` exists for sites where the analyzed construct has no name-bearing
|
|
2117
|
+
* child at all (a `this(...)`/`super(...)` delegation, a `new` with a diamond).
|
|
2118
|
+
*/
|
|
2119
|
+
lineField(nameNode, fallback) {
|
|
2120
|
+
const n = nameNode ?? fallback;
|
|
2121
|
+
return n ? { line_number: n.startPosition.row + 1 } : {};
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* M59 (#825): the node whose start is the DECLARATION's own line.
|
|
2125
|
+
*
|
|
2126
|
+
* An annotation is a modifier — `@SuppressWarnings("unused")` decorates the
|
|
2127
|
+
* class, it is not where the class is written. Measured, not assumed: on a
|
|
2128
|
+
* 300-file real java sample, **25 of 93 symbols (27%)** had their line on an
|
|
2129
|
+
* annotation, so a tool navigating to a java symbol landed on `@Override` or
|
|
2130
|
+
* `@SuppressWarnings` instead of the declaration. tree-sitter-java nests the
|
|
2131
|
+
* annotations in a `modifiers` child, which is what is skipped here; the
|
|
2132
|
+
* contract is scala's (settled first and pinned there), applied to java's node
|
|
2133
|
+
* shape so kotlin and groovy can copy one rule instead of two.
|
|
2134
|
+
*
|
|
2135
|
+
* The symbol range and its `contains` edge move together, exactly as in scala:
|
|
2136
|
+
* a symbol and the edge that names it must never disagree about the line.
|
|
2137
|
+
*/
|
|
2138
|
+
declarationLineNode(node) {
|
|
2139
|
+
const named = this.declarationNameNode(node);
|
|
2140
|
+
if (named) return named;
|
|
2141
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2142
|
+
const c = node.namedChild(i);
|
|
2143
|
+
if (!c) continue;
|
|
2144
|
+
if (c.type === "modifiers") continue;
|
|
2145
|
+
if (c.type === "line_comment" || c.type === "block_comment") continue;
|
|
2146
|
+
return c;
|
|
2147
|
+
}
|
|
2148
|
+
return node;
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* The NAME node of a declaration, or null when the shape has none.
|
|
2152
|
+
*
|
|
2153
|
+
* `childForFieldName("name")` is the grammar's own answer for every shape that
|
|
2154
|
+
* has one — class/interface/enum/record/annotation-type declarations, methods,
|
|
2155
|
+
* constructors, enum constants and type parameters. A FIELD is the exception:
|
|
2156
|
+
* its name lives in a nested `variable_declarator`, so the declarator is asked
|
|
2157
|
+
* instead. Multi-declarator fields (`int a, b;`) carry one declarator each; the
|
|
2158
|
+
* FIRST one is the declaration's own line, which is also the line the field
|
|
2159
|
+
* symbol is published under.
|
|
2160
|
+
*/
|
|
2161
|
+
declarationNameNode(node) {
|
|
2162
|
+
const field = node.childForFieldName?.("name");
|
|
2163
|
+
if (field && (field.type === "identifier" || field.type === "type_identifier")) return field;
|
|
2164
|
+
if (node.type === "field_declaration") {
|
|
2165
|
+
const declarator = this.findChildByType(node, "variable_declarator");
|
|
2166
|
+
const name = declarator?.childForFieldName?.("name");
|
|
2167
|
+
if (name) return name;
|
|
2168
|
+
}
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* M59 (#825): the line of the ANNOTATION that names this edge's target.
|
|
2173
|
+
*
|
|
2174
|
+
* Annotation-derived edges (a JPA table, a mapped column, a FeignClients
|
|
2175
|
+
* service, a `@Value` config key, a route) carry a target whose NAME IS WRITTEN
|
|
2176
|
+
* INSIDE THE ANNOTATION. The declaration's line is not where that name is, so
|
|
2177
|
+
* pointing the edge at the declaration would fail the very source check this
|
|
2178
|
+
* milestone exists for — the check looks for the target name on the claimed
|
|
2179
|
+
* line. Returns undefined when the annotation is absent, and callers omit the
|
|
2180
|
+
* field rather than writing 0 (#821).
|
|
2181
|
+
*/
|
|
2182
|
+
annotationLine(node, name, targetName) {
|
|
2183
|
+
const mods = this.findChildByType(node, "modifiers") ?? node;
|
|
2184
|
+
for (let i = 0; i < mods.namedChildCount; i++) {
|
|
2185
|
+
const c = mods.namedChild(i);
|
|
2186
|
+
if (!c) continue;
|
|
2187
|
+
if (c.type !== "annotation" && c.type !== "marker_annotation") continue;
|
|
2188
|
+
const id = c.childForFieldName?.("name") ?? this.findChildByType(c, "identifier") ?? this.findChildByType(c, "scoped_identifier");
|
|
2189
|
+
const leaf = id ? String(id.text).split(".").pop() : "";
|
|
2190
|
+
if (leaf === name) {
|
|
2191
|
+
if (targetName) {
|
|
2192
|
+
const quoted = `"${targetName}"`;
|
|
2193
|
+
const found = this.findStringToken(c, quoted);
|
|
2194
|
+
if (found) return found.startPosition.row + 1;
|
|
2195
|
+
}
|
|
2196
|
+
return c.startPosition.row + 1;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return void 0;
|
|
2200
|
+
}
|
|
2201
|
+
/** First descendant whose text is exactly the quoted token (the string literal that carries an annotation target name). */
|
|
2202
|
+
findStringToken(node, quoted) {
|
|
2203
|
+
if (node.text === quoted) return node;
|
|
2204
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2205
|
+
const c = node.namedChild(i);
|
|
2206
|
+
if (!c) continue;
|
|
2207
|
+
const hit = this.findStringToken(c, quoted);
|
|
2208
|
+
if (hit) return hit;
|
|
2209
|
+
}
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
/** The range of a SYMBOL (not of an import or an error): annotations skipped. */
|
|
2213
|
+
symbolRange(node) {
|
|
2214
|
+
const decl = this.declarationLineNode(node);
|
|
2215
|
+
const base = this.nodeToRange(node);
|
|
2216
|
+
return {
|
|
2217
|
+
...base,
|
|
2218
|
+
startLine: decl.startPosition.row + 1,
|
|
2219
|
+
startColumn: decl.startPosition.column + 1,
|
|
2220
|
+
startIndex: decl.startIndex
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
findChildByType(node, type) {
|
|
2224
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2225
|
+
const child = node.namedChild(i);
|
|
2226
|
+
if (child && child.type === type) return child;
|
|
2227
|
+
}
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
findAllChildrenByType(node, type) {
|
|
2231
|
+
const results = [];
|
|
2232
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2233
|
+
const child = node.namedChild(i);
|
|
2234
|
+
if (child && child.type === type) results.push(child);
|
|
2235
|
+
}
|
|
2236
|
+
return results;
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* All named types referenced inside a type subtree: the base of a generic
|
|
2240
|
+
* (`List<X>` → List), each type argument recursively (`Map<String,
|
|
2241
|
+
* List<X>>` → Map, String, X), scoped names in full dotted form per the
|
|
2242
|
+
* M14 dotted-name convention (`java.util.List` stays dotted), and array
|
|
2243
|
+
* element types. Union catch types are visited via their catch_type parent.
|
|
2244
|
+
* Primitives never match these node shapes and are correctly excluded.
|
|
2245
|
+
*/
|
|
2246
|
+
/**
|
|
2247
|
+
* M59 (#825): `{name, line}` pairs, not bare names.
|
|
2248
|
+
*
|
|
2249
|
+
* Every edge these names feed is a `references` edge, and its line must be the
|
|
2250
|
+
* line the TYPE IS NAMED — not the line the enclosing type expression starts
|
|
2251
|
+
* on. java writes long type expressions across lines (`Map<String,\n List<X>>`),
|
|
2252
|
+
* so the two differ in exactly the places a tool would point at the wrong code.
|
|
2253
|
+
* The shape is the one scala settled on for the same reason: collect the node
|
|
2254
|
+
* with the name, then the line cannot be taken from the parent by accident.
|
|
2255
|
+
*/
|
|
2256
|
+
collectJavaTypeNames(node) {
|
|
2257
|
+
const out = [];
|
|
2258
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2259
|
+
const visit = (n) => {
|
|
2260
|
+
if (n.type === "type_identifier" || n.type === "scoped_type_identifier") {
|
|
2261
|
+
const nm = n.type === "type_identifier" ? n.text : javaTypeBaseName(n);
|
|
2262
|
+
if (nm && !seen.has(nm)) {
|
|
2263
|
+
seen.add(nm);
|
|
2264
|
+
out.push({ name: nm, line: n.startPosition.row + 1 });
|
|
2265
|
+
}
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2269
|
+
const child = n.namedChild(i);
|
|
2270
|
+
if (child) visit(child);
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
2273
|
+
visit(node);
|
|
2274
|
+
return out;
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* THE IMPORTED-BINDING CLAIM FOR STATIC IMPORTS (Lead claim kind, live 2026-10-19).
|
|
2278
|
+
*
|
|
2279
|
+
* Two halves, and BOTH are needed because the arbiter verifies one against the other:
|
|
2280
|
+
*
|
|
2281
|
+
* 1. EVIDENCE ON THE IMPORT EDGE. For every `import static pkg.Type.member;` the analyzer
|
|
2282
|
+
* publishes `metadata.importedBindings = [member]` and `metadata.module = pkg.Type` (the
|
|
2283
|
+
* CONTAINER as written — the gate's evidence map is keyed by exactly that specifier). The
|
|
2284
|
+
* written target stays the full member path; this is additive metadata.
|
|
2285
|
+
*
|
|
2286
|
+
* 2. THE CLAIM ON THE USE EDGE. A calls row whose call expression is a BARE name (no receiver)
|
|
2287
|
+
* or a uses row that is a BARE READ, whose target is the bound name and whose import's
|
|
2288
|
+
* container root is EXTERNAL, carries
|
|
2289
|
+
* metadata.importedBinding = { specifier: pkg.Type, binding: member }.
|
|
2290
|
+
* The arbiter verifies it against the evidence map (own column: bindOK), never folds it into
|
|
2291
|
+
* resolved, and refutes a claim whose evidence contradicts it.
|
|
2292
|
+
*
|
|
2293
|
+
* REFUSALS, each a way the claim could become FALSE: a container that is NOT external (a project
|
|
2294
|
+
* class's static member is project code and must resolve internally); a name the FILE itself
|
|
2295
|
+
* declares (the declaration shadows the import — the call/read is not the imported binding); a
|
|
2296
|
+
* receiver-carrying call (its target is a member leaf, not the bound name); an on-demand static
|
|
2297
|
+
* import (`import static X.*` binds no name this file states).
|
|
2298
|
+
*/
|
|
2299
|
+
attachJavaImportedBindingClaims(relationships, symbols, index) {
|
|
2300
|
+
if (index.staticImportPaths.size === 0) return;
|
|
2301
|
+
const fileDeclared = /* @__PURE__ */ new Set();
|
|
2302
|
+
for (const s of symbols) if (s && s.name) fileDeclared.add(s.name);
|
|
2303
|
+
const isExternalContainer = (container) => {
|
|
2304
|
+
for (const root of JAVA_EXTERNAL_STATIC_IMPORT_ROOTS) if (container.startsWith(root)) return true;
|
|
2305
|
+
for (const root of JAVA_LIBRARY_NAMESPACE_CITATIONS) if (container.startsWith(`${root}.`)) return true;
|
|
2306
|
+
return false;
|
|
2307
|
+
};
|
|
2308
|
+
for (const r of relationships) {
|
|
2309
|
+
if (String(r.type ?? "") !== "imports") continue;
|
|
2310
|
+
const md = r.metadata;
|
|
2311
|
+
if (!md || typeof md !== "object") continue;
|
|
2312
|
+
if (md.isStatic !== true || md.isWildcard === true) continue;
|
|
2313
|
+
const target = String(r.target ?? "");
|
|
2314
|
+
const dot = target.lastIndexOf(".");
|
|
2315
|
+
if (dot <= 0 || dot >= target.length - 1) continue;
|
|
2316
|
+
const container = target.slice(0, dot);
|
|
2317
|
+
const member = target.slice(dot + 1);
|
|
2318
|
+
if (!isExternalContainer(container)) continue;
|
|
2319
|
+
md.importedBindings = [member];
|
|
2320
|
+
md.module = container;
|
|
2321
|
+
}
|
|
2322
|
+
for (const r of relationships) {
|
|
2323
|
+
const label = String(r.type ?? "");
|
|
2324
|
+
if (label !== "calls" && label !== "uses") continue;
|
|
2325
|
+
const md = r.metadata;
|
|
2326
|
+
if (!md || typeof md !== "object") continue;
|
|
2327
|
+
if (md.importedBinding !== void 0) continue;
|
|
2328
|
+
const target = String(r.target ?? "");
|
|
2329
|
+
if (!isPlainJavaIdentifier(target)) continue;
|
|
2330
|
+
if (fileDeclared.has(target)) continue;
|
|
2331
|
+
const container = index.staticImportPaths.get(target);
|
|
2332
|
+
if (!container || !isExternalContainer(container)) continue;
|
|
2333
|
+
if (label === "calls") {
|
|
2334
|
+
const callExpr = typeof md.javaCallExpression === "string" ? md.javaCallExpression : "";
|
|
2335
|
+
if (callExpr.length === 0 || callExpr.indexOf(".") !== -1) continue;
|
|
2336
|
+
} else if (md.isBareRead !== true) {
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
md.importedBinding = { specifier: container, binding: target };
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* THE PROJECT'S OWN KEY SPACE, read the way the ARBITER reads it: how many declarations answer
|
|
2344
|
+
* to a name. The arbiter's key space counts every published symbol by its SIMPLE name — a type,
|
|
2345
|
+
* a method, a field, a constant, an enum constant, a constructor — and a declaration's simple
|
|
2346
|
+
* name is exactly what the name-keyed resolver looks up.
|
|
2347
|
+
*
|
|
2348
|
+
* THE INDEX'S TWO CHANNELS ARE THE SAME COUNT, SPLIT. `symbolCounts` holds the TYPES by name;
|
|
2349
|
+
* `typeMembers` holds the MEMBERS by owner. A name the project declares ONCE as a type and ONCE
|
|
2350
|
+
* as a field — the `public static final Foo Foo = new Foo();` idiom, which is what most of the
|
|
2351
|
+
* lane's AMBIGUOUS type-shaped rows are (measured: 34,306 of 39,779) — answers to the name
|
|
2352
|
+
* TWICE, and the type channel alone would say once. So the count is the sum of the two, and it
|
|
2353
|
+
* is cached per index because the index is per REPOSITORY and the answer is per name.
|
|
2354
|
+
*/
|
|
2355
|
+
static javaDeclCountCache = /* @__PURE__ */ new WeakMap();
|
|
2356
|
+
static javaDeclCounts(index) {
|
|
2357
|
+
const hit = _RealJavaAnalyzer.javaDeclCountCache.get(index);
|
|
2358
|
+
if (hit) return hit;
|
|
2359
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2360
|
+
for (const [name, n] of index.symbolCounts) counts.set(name, (counts.get(name) ?? 0) + n);
|
|
2361
|
+
for (const members of index.typeMembers.values()) {
|
|
2362
|
+
for (const name of members) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
2363
|
+
}
|
|
2364
|
+
_RealJavaAnalyzer.javaDeclCountCache.set(index, counts);
|
|
2365
|
+
return counts;
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* THE EXTERNAL SINGLE-TYPE-IMPORT BINDING CLAIM (java lane, 2026-10-20).
|
|
2369
|
+
*
|
|
2370
|
+
* THE SHAPE IS THE ARBITER'S, NOT THIS LANE'S. `metadata.importedBinding = { specifier, binding }`
|
|
2371
|
+
* says *"the name `binding` is bound, in this file, by the import `specifier`"*, and the arbiter
|
|
2372
|
+
* verifies it against the run's OWN import evidence — the `importedBindings` + `module` metadata
|
|
2373
|
+
* an `imports` edge publishes — in its OWN column (`bindOK`), never in `resolved`. The
|
|
2374
|
+
* static-import arm proved the shape on this lane: **391 verified claims, bindFAIL 0** (the
|
|
2375
|
+
* `java/library-import-binding` fixture, 76/0). This method applies the SAME shape to the OTHER
|
|
2376
|
+
* import form java has: the SINGLE-TYPE import, `import java.util.List;`, which binds the simple
|
|
2377
|
+
* name `List`.
|
|
2378
|
+
*
|
|
2379
|
+
* WHY IT IS A TRUE STATEMENT (JLS 6.4.1). A single-type-import declaration d in a compilation unit
|
|
2380
|
+
* c of package p that imports a type named n shadows, throughout c, the declarations of any top
|
|
2381
|
+
* level type named n declared in another compilation unit of p, and any type named n imported on
|
|
2382
|
+
* demand in c. A type in another package is not in scope at all unless it is imported. So INSIDE
|
|
2383
|
+
* THE FILE the simple name is UNAMBIGUOUS: it names the imported type and nothing else. The
|
|
2384
|
+
* store's key space is NAME-KEYED and repo-wide, so it answers a different question ("how many
|
|
2385
|
+
* declarations in this repo are called `List`?") — which is why the row stays a tie, and why the
|
|
2386
|
+
* tie is a NAME-MODEL artefact rather than a fact about the file.
|
|
2387
|
+
*
|
|
2388
|
+
* THE CLAIM IS A TIE-BREAKER, AND IT IS ONLY EMITTED WHERE A TIE EXISTS. Three conditions, each
|
|
2389
|
+
* one a way the claim could become FALSE or pointless:
|
|
2390
|
+
*
|
|
2391
|
+
* 1. **THE PROJECT'S KEY SPACE GIVES THE SIMPLE NAME MORE THAN ONE ANSWER.** This is the wall
|
|
2392
|
+
* the claim exists to close. With EXACTLY ONE answer the name model RESOLVES the row, and a
|
|
2393
|
+
* claim there would trade a resolution for a claim — the campaign's "claims are never
|
|
2394
|
+
* merged into resolution", read in the direction that keeps the strongest class. With NO
|
|
2395
|
+
* answer the analyzer's own external claim ("the project declares no such name") already
|
|
2396
|
+
* verifies the row, and a second claim on it would replace one verified claim with another
|
|
2397
|
+
* and move the row between two verified columns for no gain at all. So the claim is emitted
|
|
2398
|
+
* for the TIE and for nothing else.
|
|
2399
|
+
* **A COMPLETE PROJECT INDEX IS REQUIRED TO SEE A TIE AT ALL**, so the rule is fail-closed:
|
|
2400
|
+
* no complete index ⇒ no claim ⇒ the no-index arm is byte-identical to its previous value.
|
|
2401
|
+
* 2. **THE IMPORTED TYPE IS OUTSIDE THE PROJECT.** With the complete index in hand, the
|
|
2402
|
+
* project must NOT declare the qualified path: the index saw the project's whole admitted
|
|
2403
|
+
* file set, so "the project declares no such type" is a STATEMENT and not a guess — and it
|
|
2404
|
+
* REFUTES the claim when the project DOES declare it. A project-internal import names
|
|
2405
|
+
* project code, which must resolve through the owner/name model.
|
|
2406
|
+
* 3. **THE FILE DOES NOT DECLARE THE NAME, AND NO STATIC IMPORT BINDS IT.** A name the file
|
|
2407
|
+
* declares shadows the import (JLS 6.4.1); a name a static import also binds is the static
|
|
2408
|
+
* arm's shape (two imports of one name is a compile error).
|
|
2409
|
+
*
|
|
2410
|
+
* AND THE CLAIM IS ONLY PUT ON A ROW WHERE THE TARGET IS THE BOUND NAME, IN A TYPE POSITION.
|
|
2411
|
+
* `references` / `instantiates` / `extends` / `implements` only: a read or a call target is a
|
|
2412
|
+
* member leaf, and a local variable may shadow a type name in an expression (JLS 6.5.5.1), so
|
|
2413
|
+
* only a type position is safe. A dotted target is not the bound name either.
|
|
2414
|
+
*/
|
|
2415
|
+
attachJavaSingleTypeImportBindingClaims(relationships, symbols, index, projectIndex) {
|
|
2416
|
+
if (projectIndex?.complete !== true) return;
|
|
2417
|
+
if (index.importSinglePaths.size === 0) return;
|
|
2418
|
+
const fileDeclared = /* @__PURE__ */ new Set();
|
|
2419
|
+
for (const s of symbols) if (s && s.name) fileDeclared.add(s.name);
|
|
2420
|
+
const declCounts = _RealJavaAnalyzer.javaDeclCounts(projectIndex);
|
|
2421
|
+
const eligible = /* @__PURE__ */ new Map();
|
|
2422
|
+
for (const [leaf, path] of index.importSinglePaths) {
|
|
2423
|
+
if (leaf.length === 0 || path.length === 0) continue;
|
|
2424
|
+
if (fileDeclared.has(leaf)) continue;
|
|
2425
|
+
if (index.staticImportPaths.has(leaf)) continue;
|
|
2426
|
+
if ((declCounts.get(leaf) ?? 0) < 2) continue;
|
|
2427
|
+
if (projectIndex.typeKinds.has(path) || projectIndex.symbolNames.has(path)) continue;
|
|
2428
|
+
eligible.set(leaf, path);
|
|
2429
|
+
}
|
|
2430
|
+
if (eligible.size === 0) return;
|
|
2431
|
+
for (const r of relationships) {
|
|
2432
|
+
if (String(r.type ?? "") !== "imports") continue;
|
|
2433
|
+
const md = r.metadata;
|
|
2434
|
+
if (!md || typeof md !== "object") continue;
|
|
2435
|
+
if (md.isStatic === true || md.isWildcard === true) continue;
|
|
2436
|
+
const target = String(r.target ?? "");
|
|
2437
|
+
const dot = target.lastIndexOf(".");
|
|
2438
|
+
if (dot <= 0 || dot >= target.length - 1) continue;
|
|
2439
|
+
const leaf = target.slice(dot + 1);
|
|
2440
|
+
const path = eligible.get(leaf);
|
|
2441
|
+
if (!path || path !== target) continue;
|
|
2442
|
+
md.importedBindings = [leaf];
|
|
2443
|
+
md.module = path;
|
|
2444
|
+
}
|
|
2445
|
+
const TYPE_LABELS = /* @__PURE__ */ new Set(["references", "instantiates", "extends", "implements"]);
|
|
2446
|
+
for (const r of relationships) {
|
|
2447
|
+
const label = String(r.type ?? "");
|
|
2448
|
+
if (!TYPE_LABELS.has(label)) continue;
|
|
2449
|
+
const md = r.metadata;
|
|
2450
|
+
if (!md || typeof md !== "object") continue;
|
|
2451
|
+
if (md.importedBinding !== void 0) continue;
|
|
2452
|
+
const target = String(r.target ?? "");
|
|
2453
|
+
if (!isPlainJavaIdentifier(target)) continue;
|
|
2454
|
+
const path = eligible.get(target);
|
|
2455
|
+
if (!path) continue;
|
|
2456
|
+
md.importedBinding = { specifier: path, binding: target };
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* LIBRARY-NAMESPACE CITATIONS — attach the arbiter's `languageNamespace` claim to targets whose
|
|
2461
|
+
* LEADING SEGMENTS name a root in the shared registry `java-platform-libraries`.
|
|
2462
|
+
*
|
|
2463
|
+
* WHAT IT MOVES AND WHAT IT CANNOT. The arbiter's namespace claim is a CITATION: a target whose
|
|
2464
|
+
* leading segments equal a declared root is `externalVerified` (`verifiedBy: "namespace"`), is
|
|
2465
|
+
* counted in `extOK`, and is NEVER folded into `resolved`. It is refutable by construction (a
|
|
2466
|
+
* root not in the declared list REFUTES), which is why the declared list is the registry's
|
|
2467
|
+
* subset and nothing else. A BARE target (a type name with no dots) can never match a prefix
|
|
2468
|
+
* segment and is therefore NOT claimed here — those rows need the imported-binding claim kind
|
|
2469
|
+
* (Lead task-19), which is why the depth wave leaves them as a sized residual.
|
|
2470
|
+
*
|
|
2471
|
+
* Measured before this: the depth residual `ext?` 2,446 carried 1,160 non-reserved dotted rows
|
|
2472
|
+
* (jakarta 1,114 · androidx.media3 10 · org.yaml 4 · dotty.tools 2) whose roots are exactly this
|
|
2473
|
+
* registry entry. Nothing is claimed for roots outside it.
|
|
2474
|
+
*/
|
|
2475
|
+
attachJavaLibraryNamespaceCitations(relationships, symbols, index, projectIndex) {
|
|
2476
|
+
if (projectIndex?.complete !== true) return;
|
|
2477
|
+
const projectOwnsQualified = (qualified) => projectIndex.typeKinds.has(qualified) || projectIndex.symbolNames.has(qualified);
|
|
2478
|
+
const fileDeclared = /* @__PURE__ */ new Set();
|
|
2479
|
+
for (const s of symbols) if (s && s.name) fileDeclared.add(s.name);
|
|
2480
|
+
const roots = [...JAVA_LIBRARY_NAMESPACE_CITATIONS].sort((a, b) => b.length - a.length);
|
|
2481
|
+
for (const r of relationships) {
|
|
2482
|
+
const md = r.metadata;
|
|
2483
|
+
if (!md || typeof md !== "object") continue;
|
|
2484
|
+
if (md.languageNamespace !== void 0) continue;
|
|
2485
|
+
const target = String(r.target ?? "");
|
|
2486
|
+
if (target.length === 0) continue;
|
|
2487
|
+
for (const root of roots) {
|
|
2488
|
+
if (target !== root && !target.startsWith(`${root}.`)) continue;
|
|
2489
|
+
if (projectOwnsQualified(target)) return;
|
|
2490
|
+
md.languageNamespace = [root];
|
|
2491
|
+
break;
|
|
2492
|
+
}
|
|
2493
|
+
if (md.languageNamespace !== void 0) continue;
|
|
2494
|
+
if (fileDeclared.has(target)) continue;
|
|
2495
|
+
const importedPath = index.importSinglePaths.get(target);
|
|
2496
|
+
if (!importedPath) continue;
|
|
2497
|
+
for (const root of roots) {
|
|
2498
|
+
if (!importedPath.startsWith(`${root}.`)) continue;
|
|
2499
|
+
if (projectOwnsQualified(importedPath)) return;
|
|
2500
|
+
md.languageNamespace = [target];
|
|
2501
|
+
break;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* THE IMPORTED-NAME SEAM — `metadata.importedFrom` on type-shaped reference edges whose
|
|
2507
|
+
* first name segment is a KNOWN single-type import of this file.
|
|
2508
|
+
*
|
|
2509
|
+
* WHAT IT IS FOR (task-17 item 2 / D7): the wall `references type-shaped` is 84,751
|
|
2510
|
+
* ambiguous+unresolved rows whose targets are bare type names; for 32,176 of them the
|
|
2511
|
+
* FILE's own import states which package the name comes from. The store's name-keyed
|
|
2512
|
+
* resolver cannot see the file's imports, so the row stays a tie — but joining the
|
|
2513
|
+
* import path to the target yields the declaration's QUALIFIED NAME, which the store's
|
|
2514
|
+
* symbol table already carries (`qualifiedName` aliases are indexed). This method is the
|
|
2515
|
+
* ANALYZER HALF of that instrument: it publishes the seam; the resolver change that
|
|
2516
|
+
* consumes it is Lead-owned (`parallel-indexer.resolveRelationships`), and the sizing
|
|
2517
|
+
* comes from the lane's ambiguity classifier (same-file 81,448 · single-type-import
|
|
2518
|
+
* 39,828 over all labels).
|
|
2519
|
+
*
|
|
2520
|
+
* ADDITIVE ONLY: the target spelling is untouched (JD20's bare-leaf contract and the
|
|
2521
|
+
* differential oracle both stay), the key is not in any claim channel, and a row that
|
|
2522
|
+
* already carries `importedFrom` keeps it. Nested spellings join the remainder too —
|
|
2523
|
+
* `Map.Entry` under `import java.util.Map` publishes `java.util.Map.Entry`, which is
|
|
2524
|
+
* exactly the qualified form of the nested type.
|
|
2525
|
+
*/
|
|
2526
|
+
attachJavaImportedFrom(relationships, index, symbols) {
|
|
2527
|
+
if (index.importSinglePaths.size === 0) return;
|
|
2528
|
+
const declared = /* @__PURE__ */ new Set();
|
|
2529
|
+
for (const s of symbols) if (s && s.name) declared.add(s.name);
|
|
2530
|
+
const TYPE_LABELS = /* @__PURE__ */ new Set(["references", "instantiates", "extends", "implements"]);
|
|
2531
|
+
const MEMBER_LABELS = /* @__PURE__ */ new Set(["calls", "uses"]);
|
|
2532
|
+
for (const r of relationships) {
|
|
2533
|
+
const label = String(r.type ?? "");
|
|
2534
|
+
if (!TYPE_LABELS.has(label) && !MEMBER_LABELS.has(label)) continue;
|
|
2535
|
+
const md = r.metadata;
|
|
2536
|
+
if (!md || typeof md !== "object" || md.importedFrom !== void 0) continue;
|
|
2537
|
+
const target = String(r.target ?? "");
|
|
2538
|
+
if (target.length === 0) continue;
|
|
2539
|
+
if (MEMBER_LABELS.has(label) && index.staticImportPaths.has(target) && !declared.has(target)) {
|
|
2540
|
+
md.importedFrom = `${index.staticImportPaths.get(target)}.${target}`;
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
if (!TYPE_LABELS.has(label)) continue;
|
|
2544
|
+
const first = target.split(".")[0];
|
|
2545
|
+
if (first.length === 0 || declared.has(first)) continue;
|
|
2546
|
+
const imported = index.importSinglePaths.get(first);
|
|
2547
|
+
if (!imported) continue;
|
|
2548
|
+
const remainder = target.length > first.length ? target.slice(first.length) : "";
|
|
2549
|
+
if (remainder.length > 0) {
|
|
2550
|
+
let shapeOk = remainder.startsWith(".");
|
|
2551
|
+
for (let i = 1; shapeOk && i < remainder.length; i++) {
|
|
2552
|
+
const code = remainder.charCodeAt(i);
|
|
2553
|
+
if (code === 46) continue;
|
|
2554
|
+
const isStart = code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 36 || code === 95;
|
|
2555
|
+
const isPart = isStart || code >= 48 && code <= 57;
|
|
2556
|
+
if (!isPart) {
|
|
2557
|
+
shapeOk = false;
|
|
2558
|
+
break;
|
|
2559
|
+
}
|
|
2560
|
+
if (i > 0 && code !== 46) {
|
|
2561
|
+
const prev = remainder.charCodeAt(i - 1);
|
|
2562
|
+
const prevIsDot = prev === 46;
|
|
2563
|
+
if (prevIsDot && !isStart) {
|
|
2564
|
+
shapeOk = false;
|
|
2565
|
+
break;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
if (!shapeOk) continue;
|
|
2570
|
+
}
|
|
2571
|
+
md.importedFrom = remainder.length > 0 ? `${imported}${remainder}` : imported;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Collect Uses relationships from field_access expressions (this.name, obj.method)
|
|
2576
|
+
*/
|
|
2577
|
+
collectFieldAccessUses(rootNode, relationships, symbols, receiverTypes, projectIndex) {
|
|
2578
|
+
const membersByOwner = /* @__PURE__ */ new Map();
|
|
2579
|
+
for (const s of symbols) {
|
|
2580
|
+
const o = s.ownerName || s.scope;
|
|
2581
|
+
if (!o) continue;
|
|
2582
|
+
const set = membersByOwner.get(o);
|
|
2583
|
+
if (set) set.add(s.name);
|
|
2584
|
+
else membersByOwner.set(o, /* @__PURE__ */ new Set([s.name]));
|
|
2585
|
+
}
|
|
2586
|
+
const stack = [
|
|
2587
|
+
{ node: rootNode, owner: null, method: null, locals: /* @__PURE__ */ new Set() }
|
|
2588
|
+
];
|
|
2589
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2590
|
+
while (stack.length > 0) {
|
|
2591
|
+
const item = stack.pop();
|
|
2592
|
+
const { node, owner, method, locals } = item;
|
|
2593
|
+
if (node.type === "class_declaration" || node.type === "interface_declaration" || node.type === "enum_declaration" || node.type === "record_declaration" || // M37.21 C17: member reads INSIDE an @interface body previously kept
|
|
2594
|
+
// the outer owner — @interface is a type declaration like the others,
|
|
2595
|
+
// so its member declarations must rebind the read owner.
|
|
2596
|
+
node.type === "annotation_type_declaration") {
|
|
2597
|
+
const nameNode = this.findChildByType(node, "identifier");
|
|
2598
|
+
const nextOwner = nameNode ? nameNode.text : owner;
|
|
2599
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
2600
|
+
const child = node.namedChild(i);
|
|
2601
|
+
if (child) stack.push({ node: child, owner: nextOwner, method: null, locals: /* @__PURE__ */ new Set() });
|
|
2602
|
+
}
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
let nextMethod = method;
|
|
2606
|
+
let nextLocals = locals;
|
|
2607
|
+
if (node.type === "method_declaration" || node.type === "constructor_declaration" || node.type === "compact_constructor_declaration") {
|
|
2608
|
+
const nameNode = node.childForFieldName("name") || this.findChildByType(node, "identifier");
|
|
2609
|
+
nextMethod = node.type === "constructor_declaration" || node.type === "compact_constructor_declaration" ? owner ?? "constructor" : nameNode?.text ?? method;
|
|
2610
|
+
nextLocals = this.collectBoundNames(node);
|
|
2611
|
+
}
|
|
2612
|
+
if (node.type === "field_access") {
|
|
2613
|
+
const parts = node.text.split(".");
|
|
2614
|
+
if (parts.length >= 2) {
|
|
2615
|
+
const first = parts[0];
|
|
2616
|
+
const readOwner = method ?? owner;
|
|
2617
|
+
const source = first === "this" || first === "super" ? method ?? owner ?? first : readOwner ?? first;
|
|
2618
|
+
const parent = node.parent;
|
|
2619
|
+
const lhs = parent && parent.type === "assignment_expression" ? parent.namedChild(0) : null;
|
|
2620
|
+
const isWrite = !!lhs && lhs.startIndex === node.startIndex && lhs.endIndex === node.endIndex;
|
|
2621
|
+
const key = `${source}.${parts[parts.length - 1]}`;
|
|
2622
|
+
if (!isWrite && !seen.has(key)) {
|
|
2623
|
+
seen.add(key);
|
|
2624
|
+
const readReceiver = this.javaReceiverOwnerType(node, receiverTypes, projectIndex, "uses");
|
|
2625
|
+
relationships.push({
|
|
2626
|
+
source,
|
|
2627
|
+
target: parts[parts.length - 1],
|
|
2628
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2629
|
+
confidence: 0.6,
|
|
2630
|
+
// M59 (#825): the READ is where the field NAME is written — the
|
|
2631
|
+
// `field` node, not the field_access (which starts at its
|
|
2632
|
+
// receiver, the same defect class as the call site).
|
|
2633
|
+
...this.lineField(node.childForFieldName("field"), node),
|
|
2634
|
+
metadata: {
|
|
2635
|
+
isFieldAccess: true,
|
|
2636
|
+
...readReceiver ? { owner: readReceiver.type, ownerEvidence: readReceiver.evidence } : {}
|
|
2637
|
+
}
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
if (node.type === "array_access") {
|
|
2643
|
+
const arrayNode = node.childForFieldName("array");
|
|
2644
|
+
if (arrayNode) {
|
|
2645
|
+
const parent = node.parent;
|
|
2646
|
+
const lhs = parent && parent.type === "assignment_expression" ? parent.namedChild(0) : null;
|
|
2647
|
+
const isWrite = !!lhs && lhs.startIndex === node.startIndex && lhs.endIndex === node.endIndex;
|
|
2648
|
+
const at = arrayNode.type;
|
|
2649
|
+
let source = null;
|
|
2650
|
+
let target = null;
|
|
2651
|
+
if (at === "identifier") {
|
|
2652
|
+
const readOwner = method ?? owner;
|
|
2653
|
+
source = readOwner ?? arrayNode.text;
|
|
2654
|
+
target = arrayNode.text;
|
|
2655
|
+
} else if (at === "field_access" || at === "scoped_identifier") {
|
|
2656
|
+
const parts = arrayNode.text.split(".");
|
|
2657
|
+
if (parts.length >= 2) {
|
|
2658
|
+
const first = parts[0];
|
|
2659
|
+
const readOwner = method ?? owner;
|
|
2660
|
+
source = first === "this" || first === "super" ? method ?? owner ?? first : readOwner ?? first;
|
|
2661
|
+
target = parts[parts.length - 1];
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
if (source && target && !isWrite) {
|
|
2665
|
+
const key = `${source}.${target}`;
|
|
2666
|
+
if (!seen.has(key)) {
|
|
2667
|
+
seen.add(key);
|
|
2668
|
+
relationships.push({
|
|
2669
|
+
source,
|
|
2670
|
+
target,
|
|
2671
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2672
|
+
confidence: 0.6,
|
|
2673
|
+
// M59 (#825): the array read is named by the array expression.
|
|
2674
|
+
...this.lineField(arrayNode, node),
|
|
2675
|
+
metadata: { isArrayAccess: true }
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
if (node.type === "identifier") {
|
|
2682
|
+
const txt = node.text;
|
|
2683
|
+
if (txt !== "this" && txt !== "super") {
|
|
2684
|
+
const parent = node.parent;
|
|
2685
|
+
const memberish = parent?.type === "field_access" || parent?.type === "method_invocation" || parent?.type === "scoped_identifier" || parent?.type === "array_access";
|
|
2686
|
+
const annotationName = parent?.type === "marker_annotation" || parent?.type === "annotation";
|
|
2687
|
+
const declName = (parent?.type === "variable_declarator" || parent?.type === "formal_parameter" || parent?.type === "method_declaration" || parent?.type === "constructor_declaration" || parent?.type === "class_declaration" || parent?.type === "interface_declaration" || parent?.type === "enum_declaration" || parent?.type === "record_declaration" || parent?.type === "annotation_type_declaration" || parent?.type === "enum_constant" || parent?.type === "catch_formal_parameter" || parent?.type === "enhanced_for_statement" || // A COMPACT CONSTRUCTOR (JLS 8.10.4) and an ANNOTATION TYPE ELEMENT
|
|
2688
|
+
// (JLS 9.6.1) both NAME a member: `R { ... }` and `Class<?> value();`.
|
|
2689
|
+
// The first is the constructor's own name and the second is an element
|
|
2690
|
+
// declaration, so neither is a read of an enclosing member — but the
|
|
2691
|
+
// list above did not include them and probe on the pinned grammar
|
|
2692
|
+
// showed both fabricated a bare read: record R(int x) { R { ... } }
|
|
2693
|
+
// emitted `uses R -> R` and `@interface Ann { List<String> list(); }`
|
|
2694
|
+
// emitted `uses Ann -> list`. A name is not a read; GATE A below then
|
|
2695
|
+
// let the fabricated edge through because the member map carried it.
|
|
2696
|
+
parent?.type === "compact_constructor_declaration" || parent?.type === "annotation_type_element_declaration") && parent.childForFieldName("name")?.startIndex === node.startIndex;
|
|
2697
|
+
let isWrite = false;
|
|
2698
|
+
if (parent?.type === "assignment_expression") {
|
|
2699
|
+
const lhs = parent.childForFieldName("left") ?? parent.namedChild(0);
|
|
2700
|
+
isWrite = !!lhs && lhs.startIndex === node.startIndex;
|
|
2701
|
+
}
|
|
2702
|
+
const isDeclaredMember = !!owner && (membersByOwner.get(owner)?.has(txt) ?? false);
|
|
2703
|
+
const readSource = method ?? owner;
|
|
2704
|
+
const key = readSource ? `${readSource}.#${txt}` : null;
|
|
2705
|
+
if (!memberish && !annotationName && !declName && !isWrite && !locals.has(txt) && isDeclaredMember && readSource && key && !seen.has(key)) {
|
|
2706
|
+
seen.add(key);
|
|
2707
|
+
let bareOwner = null;
|
|
2708
|
+
if (owner && this.javaTypeEligible(owner, receiverTypes, projectIndex) && this.javaOwnerResolvable(owner, txt, "uses", receiverTypes, projectIndex)) {
|
|
2709
|
+
bareOwner = owner;
|
|
2710
|
+
}
|
|
2711
|
+
relationships.push({
|
|
2712
|
+
source: readSource,
|
|
2713
|
+
target: txt,
|
|
2714
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2715
|
+
// 1.0, not the 0.6 an inferred read carries. This edge is gated on
|
|
2716
|
+
// `isDeclaredMember`, i.e. the owner was CHECKED to declare the member
|
|
2717
|
+
// (`membersByOwner`), so "this type declares this field" is verified rather
|
|
2718
|
+
// than guessed. At 0.6 the harness classified it as emitted-but-not-claimed
|
|
2719
|
+
// (below the 0.7 threshold) while truth says "resolved" -- a false negative
|
|
2720
|
+
// for a correct edge.
|
|
2721
|
+
confidence: 1,
|
|
2722
|
+
// M59 (#825): the bare read IS its identifier node.
|
|
2723
|
+
line_number: node.startPosition.row + 1,
|
|
2724
|
+
metadata: { isBareRead: true, ...bareOwner ? { owner: bareOwner, ownerEvidence: "this" } : {} }
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
2730
|
+
const child = node.namedChild(i);
|
|
2731
|
+
if (child) stack.push({ node: child, owner, method: nextMethod, locals: nextLocals });
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* M49 #739 GATE B: the names a function body BINDS — parameters, locals,
|
|
2737
|
+
* catch/enhanced-for/lambda/resource bindings. A bare read of any of these
|
|
2738
|
+
* is not a member read (CGD15 family rule shared with c/cpp).
|
|
2739
|
+
*
|
|
2740
|
+
* Deliberately method-wide rather than block-scoped: a local declared AFTER
|
|
2741
|
+
* a use still suppresses that use, which can only LOSE an edge, never
|
|
2742
|
+
* fabricate one — the correct direction under the precision rule. Nested
|
|
2743
|
+
* type bodies reset this set (the class branch above), so a nested class's
|
|
2744
|
+
* member reads are never suppressed by the enclosing method.
|
|
2745
|
+
*/
|
|
2746
|
+
collectBoundNames(fnNode) {
|
|
2747
|
+
const names = /* @__PURE__ */ new Set();
|
|
2748
|
+
const stack = [fnNode];
|
|
2749
|
+
while (stack.length > 0) {
|
|
2750
|
+
const n = stack.pop();
|
|
2751
|
+
if (n.type === "variable_declarator" || n.type === "formal_parameter" || n.type === "catch_formal_parameter" || n.type === "enhanced_for_statement" || n.type === "resource") {
|
|
2752
|
+
const nm = n.childForFieldName("name") || this.findChildByType(n, "identifier");
|
|
2753
|
+
if (nm) names.add(nm.text);
|
|
2754
|
+
} else if (n.type === "lambda_expression") {
|
|
2755
|
+
const params = n.childForFieldName("parameters");
|
|
2756
|
+
if (params) {
|
|
2757
|
+
if (params.type === "identifier") names.add(params.text);
|
|
2758
|
+
else for (let i = 0; i < params.namedChildCount; i++) {
|
|
2759
|
+
const c = params.namedChild(i);
|
|
2760
|
+
if (c) names.add(c.text);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2765
|
+
const c = n.namedChild(i);
|
|
2766
|
+
if (c) stack.push(c);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
return names;
|
|
2770
|
+
}
|
|
2771
|
+
childText(node, type) {
|
|
2772
|
+
const child = this.findChildByType(node, type);
|
|
2773
|
+
return child ? child.text : null;
|
|
2774
|
+
}
|
|
2775
|
+
getJavaModifiers(node) {
|
|
2776
|
+
const modifiers = [];
|
|
2777
|
+
const modifiersNode = this.findChildByType(node, "modifiers");
|
|
2778
|
+
if (modifiersNode) {
|
|
2779
|
+
for (let i = 0; i < modifiersNode.childCount; i++) {
|
|
2780
|
+
const modifier = modifiersNode.child(i);
|
|
2781
|
+
if (modifier && !stringIncludes(modifier.type, "annotation")) {
|
|
2782
|
+
modifiers.push(modifier.text);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
return modifiers;
|
|
2787
|
+
}
|
|
2788
|
+
getJavaAnnotations(node) {
|
|
2789
|
+
const annotations = [];
|
|
2790
|
+
const modifiersNode = this.findChildByType(node, "modifiers");
|
|
2791
|
+
const target = modifiersNode || node;
|
|
2792
|
+
for (let i = 0; i < target.namedChildCount; i++) {
|
|
2793
|
+
const child = target.namedChild(i);
|
|
2794
|
+
if (child && (child.type === "annotation" || child.type === "marker_annotation")) {
|
|
2795
|
+
annotations.push(child.text);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
return annotations;
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* The NAME child of an annotation / marker_annotation node — the annotation
|
|
2802
|
+
* TYPE as the file wrote it. The grammar serializes it as
|
|
2803
|
+
* `$._name = choice(identifier, scoped_identifier)` (probe-verified: `@Override`
|
|
2804
|
+
* has a direct `identifier`, `@org.springframework.stereotype.Service` a
|
|
2805
|
+
* `scoped_identifier`), and `findChildByType` only inspects DIRECT children, so
|
|
2806
|
+
* an identifier inside an argument list can never be mistaken for the name.
|
|
2807
|
+
*/
|
|
2808
|
+
javaAnnotationNameNode(ann) {
|
|
2809
|
+
return this.findChildByType(ann, "identifier") || this.findChildByType(ann, "scoped_identifier");
|
|
2810
|
+
}
|
|
2811
|
+
/** The declaration kinds whose annotations this lane attributes to a published
|
|
2812
|
+
* name. Every entry is a node type of the pinned tree-sitter-java grammar; the
|
|
2813
|
+
* list is exhaustive for the positions Java permits a type annotation in
|
|
2814
|
+
* (`modifiers`, type-use, parameter, type-parameter, receiver, record
|
|
2815
|
+
* component, enum constant). */
|
|
2816
|
+
static JAVA_ANNOTATED_DECLARATIONS = /* @__PURE__ */ new Set([
|
|
2817
|
+
"class_declaration",
|
|
2818
|
+
"interface_declaration",
|
|
2819
|
+
"enum_declaration",
|
|
2820
|
+
"record_declaration",
|
|
2821
|
+
"annotation_type_declaration",
|
|
2822
|
+
"module_declaration",
|
|
2823
|
+
"method_declaration",
|
|
2824
|
+
"constructor_declaration",
|
|
2825
|
+
"compact_constructor_declaration",
|
|
2826
|
+
"annotation_type_element_declaration",
|
|
2827
|
+
"field_declaration",
|
|
2828
|
+
"constant_declaration",
|
|
2829
|
+
"local_variable_declaration",
|
|
2830
|
+
"enum_constant",
|
|
2831
|
+
"formal_parameter",
|
|
2832
|
+
"spread_parameter",
|
|
2833
|
+
"catch_formal_parameter",
|
|
2834
|
+
"receiver_parameter",
|
|
2835
|
+
"type_parameter"
|
|
2836
|
+
]);
|
|
2837
|
+
/** Positions whose annotated thing is NOT a published symbol of its own: the
|
|
2838
|
+
* annotation belongs to the declaration that carries the position. */
|
|
2839
|
+
static JAVA_ANNOTATION_CARRIER_POSITIONS = /* @__PURE__ */ new Set([
|
|
2840
|
+
"formal_parameter",
|
|
2841
|
+
"spread_parameter",
|
|
2842
|
+
"catch_formal_parameter",
|
|
2843
|
+
"receiver_parameter",
|
|
2844
|
+
"type_parameter",
|
|
2845
|
+
"local_variable_declaration"
|
|
2846
|
+
]);
|
|
2847
|
+
/**
|
|
2848
|
+
* The declaration an annotation node decorates, found by climbing ancestors to
|
|
2849
|
+
* the first node type in JAVA_ANNOTATED_DECLARATIONS.
|
|
2850
|
+
*
|
|
2851
|
+
* WHY A CLIMB AND NOT `parent.parent`: a declaration annotation sits under the
|
|
2852
|
+
* declaration's `modifiers` child (one hop), but a TYPE-USE annotation
|
|
2853
|
+
* (`List<@Nullable String>`, JLS 9.7.4) is nested inside the type node
|
|
2854
|
+
* (`annotated_type` > `type_arguments` > `generic_type` > …), and a parameter /
|
|
2855
|
+
* record-component annotation sits under `formal_parameter`'s modifiers. The
|
|
2856
|
+
* fixed-hop version would work for exactly one of those shapes and silently
|
|
2857
|
+
* emit nothing for the others.
|
|
2858
|
+
*/
|
|
2859
|
+
javaAnnotatedDeclarationNode(ann) {
|
|
2860
|
+
let cur = ann.parent;
|
|
2861
|
+
let guard = 0;
|
|
2862
|
+
while (cur && guard++ < 64) {
|
|
2863
|
+
if (_RealJavaAnalyzer.JAVA_ANNOTATED_DECLARATIONS.has(cur.type)) return cur;
|
|
2864
|
+
cur = cur.parent;
|
|
2865
|
+
}
|
|
2866
|
+
return null;
|
|
2867
|
+
}
|
|
2868
|
+
/** The name of the nearest enclosing named declaration (method / constructor /
|
|
2869
|
+
* type), used for positions that publish no symbol of their own. */
|
|
2870
|
+
javaEnclosingDeclarationName(node) {
|
|
2871
|
+
let cur = node.parent;
|
|
2872
|
+
let guard = 0;
|
|
2873
|
+
while (cur && guard++ < 64) {
|
|
2874
|
+
const named = this.findChildByType(cur, "type_identifier");
|
|
2875
|
+
if (cur.type === "class_declaration" || cur.type === "interface_declaration" || cur.type === "enum_declaration" || cur.type === "record_declaration" || cur.type === "annotation_type_declaration" || cur.type === "module_declaration" || cur.type === "method_declaration" || cur.type === "constructor_declaration" || cur.type === "compact_constructor_declaration" || cur.type === "annotation_type_element_declaration") {
|
|
2876
|
+
const namedChild = cur.childForFieldName("name") || this.findChildByType(cur, "identifier") || named;
|
|
2877
|
+
if (namedChild) return namedChild.text;
|
|
2878
|
+
}
|
|
2879
|
+
cur = cur.parent;
|
|
2880
|
+
}
|
|
2881
|
+
return void 0;
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* The names an annotation edge is emitted FROM, with the annotated declaration's
|
|
2885
|
+
* node type beside each. See the `annotation` branch in the walk for the
|
|
2886
|
+
* contract; this helper exists so the branch reads as one edge shape.
|
|
2887
|
+
*/
|
|
2888
|
+
javaAnnotatedDeclarationSources(ann, parentContext) {
|
|
2889
|
+
const fallback = parentContext?.name ? [{ name: String(parentContext.name), declKind: "enclosing_scope" }] : [];
|
|
2890
|
+
const decl = this.javaAnnotatedDeclarationNode(ann);
|
|
2891
|
+
if (!decl) {
|
|
2892
|
+
let root = ann;
|
|
2893
|
+
let rootGuard = 0;
|
|
2894
|
+
while (root.parent && root.type !== "program" && rootGuard++ < 64) root = root.parent;
|
|
2895
|
+
if (root.type === "program") {
|
|
2896
|
+
for (let i = 0; i < root.namedChildCount; i++) {
|
|
2897
|
+
const top = root.namedChild(i);
|
|
2898
|
+
if (!top || top.type !== "package_declaration") continue;
|
|
2899
|
+
const pkgName = this.findChildByType(top, "scoped_identifier") || this.findChildByType(top, "identifier");
|
|
2900
|
+
if (pkgName) return [{ name: pkgName.text, declKind: "package_declaration" }];
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
return fallback;
|
|
2904
|
+
}
|
|
2905
|
+
const declKind = decl.type;
|
|
2906
|
+
if (declKind === "field_declaration" || declKind === "constant_declaration") {
|
|
2907
|
+
const names = [];
|
|
2908
|
+
for (let i = 0; i < decl.namedChildCount; i++) {
|
|
2909
|
+
const c = decl.namedChild(i);
|
|
2910
|
+
if (!c || c.type !== "variable_declarator") continue;
|
|
2911
|
+
const n = c.childForFieldName("name") || this.findChildByType(c, "identifier");
|
|
2912
|
+
if (n) names.push({ name: n.text, declKind });
|
|
2913
|
+
}
|
|
2914
|
+
return names.length > 0 ? names : fallback;
|
|
2915
|
+
}
|
|
2916
|
+
if (_RealJavaAnalyzer.JAVA_ANNOTATION_CARRIER_POSITIONS.has(declKind)) {
|
|
2917
|
+
const carrier = this.javaEnclosingDeclarationName(decl);
|
|
2918
|
+
return carrier ? [{ name: carrier, declKind }] : fallback;
|
|
2919
|
+
}
|
|
2920
|
+
const ownName = decl.childForFieldName("name") || this.findChildByType(decl, "identifier") || this.findChildByType(decl, "type_identifier");
|
|
2921
|
+
return ownName ? [{ name: ownName.text, declKind }] : fallback;
|
|
2922
|
+
}
|
|
2923
|
+
// ── RECEIVER TYPING — the pre-pass and the resolver ─────────────────────────────────────────────
|
|
2924
|
+
/**
|
|
2925
|
+
* Build the per-file receiver-type index in ONE pre-pass over the same tree.
|
|
2926
|
+
*
|
|
2927
|
+
* WHY A PRE-PASS. The main walk is single-pass and visits a call site before it
|
|
2928
|
+
* visits declarations written BELOW it — `x.foo()` above `void foo()` is the
|
|
2929
|
+
* normal shape, not the exception — and Java allows fields and methods in any
|
|
2930
|
+
* order. Reading what the file states therefore has to happen before the
|
|
2931
|
+
* emission that consumes it. The pre-pass visits the same tree and emits nothing.
|
|
2932
|
+
*
|
|
2933
|
+
* WHAT IS INDEXED, and each entry is a DECLARATION, not an inference:
|
|
2934
|
+
* * type scopes (class/interface/enum/record/@interface bodies, plus anonymous
|
|
2935
|
+
* class bodies) with their FIELDS;
|
|
2936
|
+
* * method/constructor scopes with their FORMAL PARAMETERS, and their LOCALS
|
|
2937
|
+
* (a local is recorded with its enclosing block's range, so a declaration in
|
|
2938
|
+
* a sibling block cannot bind a use outside it);
|
|
2939
|
+
* * method name -> declared return type, only when this file declares it and
|
|
2940
|
+
* the name has ONE distinct return type in the file (an overload set with
|
|
2941
|
+
* differing return types is refused, not guessed);
|
|
2942
|
+
* * type names declared in the file and single-type-import local names, which
|
|
2943
|
+
* is how a CLASS-QUALIFIED static call (`Helper.help()`) is told apart from a
|
|
2944
|
+
* variable read.
|
|
2945
|
+
*/
|
|
2946
|
+
collectJavaReceiverTypeIndex(rootNode) {
|
|
2947
|
+
const index = {
|
|
2948
|
+
methodScopes: [],
|
|
2949
|
+
typeScopes: [],
|
|
2950
|
+
typeNames: /* @__PURE__ */ new Set(),
|
|
2951
|
+
typeKindsByName: /* @__PURE__ */ new Map(),
|
|
2952
|
+
importSingleTypes: /* @__PURE__ */ new Set(),
|
|
2953
|
+
importSinglePaths: /* @__PURE__ */ new Map(),
|
|
2954
|
+
methodReturns: /* @__PURE__ */ new Map(),
|
|
2955
|
+
typeScopesByName: /* @__PURE__ */ new Map(),
|
|
2956
|
+
staticImportLeaves: /* @__PURE__ */ new Set(),
|
|
2957
|
+
hasStaticOnDemand: false,
|
|
2958
|
+
staticImportPaths: /* @__PURE__ */ new Map()
|
|
2959
|
+
};
|
|
2960
|
+
const TYPE_DECLARATIONS = /* @__PURE__ */ new Set([
|
|
2961
|
+
"class_declaration",
|
|
2962
|
+
"interface_declaration",
|
|
2963
|
+
"enum_declaration",
|
|
2964
|
+
"record_declaration",
|
|
2965
|
+
"annotation_type_declaration"
|
|
2966
|
+
]);
|
|
2967
|
+
const BLOCK_NODES = /* @__PURE__ */ new Set([
|
|
2968
|
+
"block",
|
|
2969
|
+
"constructor_body",
|
|
2970
|
+
"switch_block",
|
|
2971
|
+
"for_statement",
|
|
2972
|
+
"enhanced_for_statement",
|
|
2973
|
+
"try_statement",
|
|
2974
|
+
"catch_clause",
|
|
2975
|
+
"finally_clause",
|
|
2976
|
+
"synchronized_statement",
|
|
2977
|
+
"lambda_expression",
|
|
2978
|
+
"static_initializer"
|
|
2979
|
+
]);
|
|
2980
|
+
const addFields = (scope, body) => {
|
|
2981
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
2982
|
+
const child = body.namedChild(i);
|
|
2983
|
+
if (!child) continue;
|
|
2984
|
+
if (child.type === "enum_body_declarations") {
|
|
2985
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
2986
|
+
const inner = child.namedChild(j);
|
|
2987
|
+
if (inner && (inner.type === "field_declaration" || inner.type === "constant_declaration")) {
|
|
2988
|
+
this.addJavaDeclaredMembers(inner, "field", scope, body, index);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
if (child.type === "field_declaration" || child.type === "constant_declaration") {
|
|
2994
|
+
this.addJavaDeclaredMembers(child, "field", scope, body, index);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
};
|
|
2998
|
+
const addMemberNamesAndEnumConstants = (scope, body) => {
|
|
2999
|
+
const names = scope.memberNames;
|
|
3000
|
+
const methods = scope.methodNames;
|
|
3001
|
+
if (!names) return;
|
|
3002
|
+
const addName = (decl) => {
|
|
3003
|
+
if (!decl) return;
|
|
3004
|
+
const nameNode = decl.childForFieldName("name");
|
|
3005
|
+
if (nameNode && nameNode.text) {
|
|
3006
|
+
names.add(nameNode.text);
|
|
3007
|
+
if (decl.type === "method_declaration" || decl.type === "constructor_declaration" || decl.type === "annotation_type_element_declaration") {
|
|
3008
|
+
methods?.add(nameNode.text);
|
|
3009
|
+
}
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
if (decl.type === "field_declaration" || decl.type === "constant_declaration") {
|
|
3013
|
+
for (let k = 0; k < decl.namedChildCount; k++) {
|
|
3014
|
+
const d = decl.namedChild(k);
|
|
3015
|
+
if (!d || d.type !== "variable_declarator") continue;
|
|
3016
|
+
const n = d.childForFieldName("name");
|
|
3017
|
+
if (n && n.text) names.add(n.text);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
3022
|
+
const child = body.namedChild(i);
|
|
3023
|
+
if (!child) continue;
|
|
3024
|
+
if (child.type === "enum_body_declarations") {
|
|
3025
|
+
for (let j = 0; j < child.namedChildCount; j++) addName(child.namedChild(j));
|
|
3026
|
+
continue;
|
|
3027
|
+
}
|
|
3028
|
+
if (child.type === "method_declaration" || child.type === "constructor_declaration" || child.type === "compact_constructor_declaration" || child.type === "field_declaration" || child.type === "constant_declaration" || TYPE_DECLARATIONS.has(child.type)) {
|
|
3029
|
+
addName(child);
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
if (child.type === "enum_constant") {
|
|
3033
|
+
const nameNode = child.childForFieldName("name") || this.findChildByType(child, "identifier");
|
|
3034
|
+
if (!nameNode || !nameNode.text) continue;
|
|
3035
|
+
names.add(nameNode.text);
|
|
3036
|
+
if (scope.name) {
|
|
3037
|
+
scope.members.set(nameNode.text, {
|
|
3038
|
+
type: scope.name,
|
|
3039
|
+
evidence: "field",
|
|
3040
|
+
declStart: child.startIndex,
|
|
3041
|
+
scopeStart: scope.start,
|
|
3042
|
+
scopeEnd: scope.end
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
};
|
|
3048
|
+
const makeTypeScope = (body, name, parents) => {
|
|
3049
|
+
const scope = {
|
|
3050
|
+
start: body.startIndex,
|
|
3051
|
+
end: body.endIndex,
|
|
3052
|
+
members: /* @__PURE__ */ new Map(),
|
|
3053
|
+
name,
|
|
3054
|
+
parents,
|
|
3055
|
+
memberNames: /* @__PURE__ */ new Set(),
|
|
3056
|
+
methodNames: /* @__PURE__ */ new Set()
|
|
3057
|
+
};
|
|
3058
|
+
addFields(scope, body);
|
|
3059
|
+
addMemberNamesAndEnumConstants(scope, body);
|
|
3060
|
+
if (name) index.typeScopesByName.set(name, scope);
|
|
3061
|
+
index.typeScopes.push(scope);
|
|
3062
|
+
return scope;
|
|
3063
|
+
};
|
|
3064
|
+
const makeBodyScope = (n) => {
|
|
3065
|
+
const body = this.findChildByType(n, "block") || this.findChildByType(n, "constructor_body");
|
|
3066
|
+
const scope = {
|
|
3067
|
+
start: body ? body.startIndex : n.startIndex,
|
|
3068
|
+
end: body ? body.endIndex : n.endIndex,
|
|
3069
|
+
members: /* @__PURE__ */ new Map()
|
|
3070
|
+
};
|
|
3071
|
+
const params = this.findChildByType(n, "formal_parameters");
|
|
3072
|
+
if (params) {
|
|
3073
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
3074
|
+
const p = params.namedChild(i);
|
|
3075
|
+
if (!p) continue;
|
|
3076
|
+
if (p.type !== "formal_parameter" && p.type !== "spread_parameter") continue;
|
|
3077
|
+
const typeNode = p.childForFieldName("type");
|
|
3078
|
+
const nameNode = p.childForFieldName("name") || this.findChildByType(p, "identifier");
|
|
3079
|
+
const typeName = this.javaDeclaredTypeBaseName(typeNode);
|
|
3080
|
+
if (typeName && nameNode) {
|
|
3081
|
+
scope.members.set(nameNode.text, {
|
|
3082
|
+
type: typeName,
|
|
3083
|
+
evidence: "param",
|
|
3084
|
+
declStart: p.startIndex,
|
|
3085
|
+
scopeStart: scope.start,
|
|
3086
|
+
scopeEnd: scope.end
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
index.methodScopes.push(scope);
|
|
3092
|
+
return scope;
|
|
3093
|
+
};
|
|
3094
|
+
const visit = (n, methodScope, typeScopes, block) => {
|
|
3095
|
+
const t = n.type;
|
|
3096
|
+
let nextMethod = methodScope;
|
|
3097
|
+
let nextTypeScopes = typeScopes;
|
|
3098
|
+
const nextBlock = BLOCK_NODES.has(t) ? n : block;
|
|
3099
|
+
if (TYPE_DECLARATIONS.has(t)) {
|
|
3100
|
+
const nameNode = n.childForFieldName("name");
|
|
3101
|
+
const typeName = nameNode?.text;
|
|
3102
|
+
if (typeName) {
|
|
3103
|
+
index.typeNames.add(typeName);
|
|
3104
|
+
index.typeKindsByName.set(typeName, t);
|
|
3105
|
+
}
|
|
3106
|
+
const body = this.findChildByType(n, "class_body") || this.findChildByType(n, "enum_body") || this.findChildByType(n, "interface_body") || this.findChildByType(n, "annotation_type_body");
|
|
3107
|
+
if (body) {
|
|
3108
|
+
nextTypeScopes = [...typeScopes, makeTypeScope(body, typeName, typeName ? this.javaDeclaredParentNames(n) : void 0)];
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
if (t === "object_creation_expression") {
|
|
3112
|
+
const body = this.findChildByType(n, "class_body");
|
|
3113
|
+
if (body) {
|
|
3114
|
+
const superNode = this.findChildByType(n, "type_identifier") || this.findChildByType(n, "generic_type") || this.findChildByType(n, "scoped_type_identifier");
|
|
3115
|
+
const superName = superNode ? this.javaDeclaredTypeBaseName(superNode) : null;
|
|
3116
|
+
nextTypeScopes = [...typeScopes, makeTypeScope(body, void 0, superName ? [superName] : void 0)];
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
if (t === "method_declaration" || t === "constructor_declaration" || t === "compact_constructor_declaration") {
|
|
3120
|
+
nextMethod = makeBodyScope(n);
|
|
3121
|
+
if (t === "method_declaration") {
|
|
3122
|
+
const nameNode = n.childForFieldName("name");
|
|
3123
|
+
const returnType = this.javaDeclaredTypeBaseName(n.childForFieldName("type"));
|
|
3124
|
+
if (nameNode && returnType) {
|
|
3125
|
+
const set = index.methodReturns.get(nameNode.text) ?? /* @__PURE__ */ new Set();
|
|
3126
|
+
set.add(returnType);
|
|
3127
|
+
index.methodReturns.set(nameNode.text, set);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
if (t === "import_declaration") {
|
|
3132
|
+
let isStatic = false;
|
|
3133
|
+
for (let ci = 0; ci < n.childCount; ci++) {
|
|
3134
|
+
const childNode = n.child(ci);
|
|
3135
|
+
if (childNode && childNode.type === "static") {
|
|
3136
|
+
isStatic = true;
|
|
3137
|
+
break;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
const isWildcard = this.findChildByType(n, "asterisk") !== null;
|
|
3141
|
+
if (isStatic) {
|
|
3142
|
+
const staticName = this.findChildByType(n, "scoped_identifier") || this.findChildByType(n, "identifier");
|
|
3143
|
+
if (staticName) {
|
|
3144
|
+
const text = staticName.text;
|
|
3145
|
+
const leaf = text.slice(text.lastIndexOf(".") + 1);
|
|
3146
|
+
if (isWildcard) index.hasStaticOnDemand = true;
|
|
3147
|
+
else if (leaf) {
|
|
3148
|
+
index.staticImportLeaves.add(leaf);
|
|
3149
|
+
const container = text.slice(0, text.lastIndexOf("."));
|
|
3150
|
+
if (container.length > 0) index.staticImportPaths.set(leaf, container);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
if (!isStatic && !isWildcard) {
|
|
3155
|
+
const nameNode = this.findChildByType(n, "scoped_identifier") || this.findChildByType(n, "identifier");
|
|
3156
|
+
if (nameNode) {
|
|
3157
|
+
const text = nameNode.text;
|
|
3158
|
+
const leaf = text.slice(text.lastIndexOf(".") + 1);
|
|
3159
|
+
if (leaf) {
|
|
3160
|
+
index.importSingleTypes.add(leaf);
|
|
3161
|
+
index.importSinglePaths.set(leaf, text);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
if (t === "lambda_expression") {
|
|
3167
|
+
const lambdaScope = { start: n.startIndex, end: n.endIndex, members: /* @__PURE__ */ new Map() };
|
|
3168
|
+
const paramsNode = n.childForFieldName("parameters");
|
|
3169
|
+
const paramChildren = paramsNode ? paramsNode.type === "identifier" ? [paramsNode] : Array.from({ length: paramsNode.namedChildCount }, (_, i) => paramsNode.namedChild(i)) : [];
|
|
3170
|
+
for (const p of paramChildren) {
|
|
3171
|
+
if (!p) continue;
|
|
3172
|
+
if (p.type !== "formal_parameter" && p.type !== "spread_parameter" && p.type !== "identifier") continue;
|
|
3173
|
+
const typeNode = p.type === "identifier" ? null : p.childForFieldName("type");
|
|
3174
|
+
const nameNode = p.type === "identifier" ? p : p.childForFieldName("name") || this.findChildByType(p, "identifier");
|
|
3175
|
+
const lambdaType = this.javaDeclaredTypeBaseName(typeNode);
|
|
3176
|
+
if (lambdaType && nameNode) {
|
|
3177
|
+
lambdaScope.members.set(nameNode.text, {
|
|
3178
|
+
type: lambdaType,
|
|
3179
|
+
evidence: "param",
|
|
3180
|
+
declStart: p.startIndex,
|
|
3181
|
+
scopeStart: lambdaScope.start,
|
|
3182
|
+
scopeEnd: lambdaScope.end
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
index.methodScopes.push(lambdaScope);
|
|
3187
|
+
nextMethod = lambdaScope;
|
|
3188
|
+
}
|
|
3189
|
+
if (t === "local_variable_declaration" && methodScope) {
|
|
3190
|
+
const scopeNode = block ?? (n.parent ?? n);
|
|
3191
|
+
this.addJavaDeclaredMembers(n, "local", methodScope, scopeNode, index);
|
|
3192
|
+
}
|
|
3193
|
+
if (methodScope && t === "catch_formal_parameter") {
|
|
3194
|
+
const typeNode = n.childForFieldName("type") || this.findChildByType(n, "catch_type");
|
|
3195
|
+
const nameNode = n.childForFieldName("name") || this.findChildByType(n, "identifier");
|
|
3196
|
+
const catchNames = typeNode && typeNode.type === "catch_type" ? this.collectJavaTypeNames(typeNode) : [];
|
|
3197
|
+
const catchType = catchNames.length === 1 ? catchNames[0].name : this.javaDeclaredTypeBaseName(typeNode);
|
|
3198
|
+
if (catchType && nameNode) {
|
|
3199
|
+
let catchScopeNode = n;
|
|
3200
|
+
let catchClimb = n.parent;
|
|
3201
|
+
let catchGuard = 0;
|
|
3202
|
+
while (catchClimb && catchGuard++ < 8) {
|
|
3203
|
+
if (catchClimb.type === "catch_clause") {
|
|
3204
|
+
catchScopeNode = catchClimb;
|
|
3205
|
+
break;
|
|
3206
|
+
}
|
|
3207
|
+
catchClimb = catchClimb.parent;
|
|
3208
|
+
}
|
|
3209
|
+
methodScope.members.set(nameNode.text, {
|
|
3210
|
+
type: catchType,
|
|
3211
|
+
evidence: "local",
|
|
3212
|
+
declStart: n.startIndex,
|
|
3213
|
+
scopeStart: catchScopeNode.startIndex,
|
|
3214
|
+
scopeEnd: catchScopeNode.endIndex
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (methodScope && t === "enhanced_for_statement") {
|
|
3219
|
+
const typeNode = n.childForFieldName("type");
|
|
3220
|
+
const nameNode = n.childForFieldName("name");
|
|
3221
|
+
const forType = this.javaDeclaredTypeBaseName(typeNode);
|
|
3222
|
+
if (forType && nameNode) {
|
|
3223
|
+
methodScope.members.set(nameNode.text, {
|
|
3224
|
+
type: forType,
|
|
3225
|
+
evidence: "local",
|
|
3226
|
+
declStart: n.startIndex,
|
|
3227
|
+
scopeStart: n.startIndex,
|
|
3228
|
+
scopeEnd: n.endIndex
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
if (methodScope && t === "resource") {
|
|
3233
|
+
const typeNode = n.childForFieldName("type");
|
|
3234
|
+
const nameNode = n.childForFieldName("name") || this.findChildByType(n, "identifier");
|
|
3235
|
+
const resType = this.javaDeclaredTypeBaseName(typeNode);
|
|
3236
|
+
if (resType && nameNode) {
|
|
3237
|
+
let scopeNode = n;
|
|
3238
|
+
let climb = n.parent;
|
|
3239
|
+
let guard = 0;
|
|
3240
|
+
while (climb && guard++ < 8) {
|
|
3241
|
+
if (climb.type === "try_statement" || climb.type === "try_with_resources_statement") {
|
|
3242
|
+
scopeNode = climb;
|
|
3243
|
+
break;
|
|
3244
|
+
}
|
|
3245
|
+
climb = climb.parent;
|
|
3246
|
+
}
|
|
3247
|
+
methodScope.members.set(nameNode.text, {
|
|
3248
|
+
type: resType,
|
|
3249
|
+
evidence: "local",
|
|
3250
|
+
declStart: n.startIndex,
|
|
3251
|
+
scopeStart: scopeNode.startIndex,
|
|
3252
|
+
scopeEnd: scopeNode.endIndex
|
|
3253
|
+
});
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
3257
|
+
const child = n.namedChild(i);
|
|
3258
|
+
if (child) visit(child, nextMethod, nextTypeScopes, nextBlock);
|
|
3259
|
+
}
|
|
3260
|
+
};
|
|
3261
|
+
visit(rootNode, null, [], null);
|
|
3262
|
+
return index;
|
|
3263
|
+
}
|
|
3264
|
+
/** Record every declarator a field/constant/local declaration binds, with the
|
|
3265
|
+
* declaration's written type and the range in which the binding is legal. A
|
|
3266
|
+
* declaration whose type is not a named type (a primitive, `var`) records
|
|
3267
|
+
* nothing: a primitive receiver has no type whose members could be resolved. */
|
|
3268
|
+
addJavaDeclaredMembers(decl, evidence, scope, scopeNode, index) {
|
|
3269
|
+
const typeNode = decl.childForFieldName("type");
|
|
3270
|
+
const isVarForm = typeNode?.type === "type_identifier" && typeNode.text === "var";
|
|
3271
|
+
let arrayElement = null;
|
|
3272
|
+
if (typeNode?.type === "array_type") {
|
|
3273
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
3274
|
+
const c = typeNode.namedChild(i);
|
|
3275
|
+
if (!c) continue;
|
|
3276
|
+
if (c.type === "type_identifier" || c.type === "generic_type" || c.type === "scoped_type_identifier") {
|
|
3277
|
+
const base = this.javaDeclaredTypeBaseName(c);
|
|
3278
|
+
if (base) {
|
|
3279
|
+
arrayElement = base;
|
|
3280
|
+
break;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
const typeName = isVarForm ? null : arrayElement ?? this.javaDeclaredTypeBaseName(typeNode);
|
|
3286
|
+
if (!typeName && !isVarForm) return;
|
|
3287
|
+
for (let i = 0; i < decl.namedChildCount; i++) {
|
|
3288
|
+
const declarator = decl.namedChild(i);
|
|
3289
|
+
if (!declarator || declarator.type !== "variable_declarator") continue;
|
|
3290
|
+
const nameNode = declarator.childForFieldName("name") || this.findChildByType(declarator, "identifier");
|
|
3291
|
+
if (!nameNode) continue;
|
|
3292
|
+
let memberType = typeName;
|
|
3293
|
+
let memberEvidence = evidence;
|
|
3294
|
+
if (isVarForm) {
|
|
3295
|
+
if (!index) continue;
|
|
3296
|
+
const inferred = this.javaInferVarInitializerType(declarator, index);
|
|
3297
|
+
if (!inferred) continue;
|
|
3298
|
+
memberType = inferred;
|
|
3299
|
+
memberEvidence = "initializer";
|
|
3300
|
+
}
|
|
3301
|
+
if (!memberType) continue;
|
|
3302
|
+
scope.members.set(nameNode.text, {
|
|
3303
|
+
type: memberType,
|
|
3304
|
+
evidence: memberEvidence,
|
|
3305
|
+
declStart: declarator.startIndex,
|
|
3306
|
+
scopeStart: evidence === "field" ? scope.start : scopeNode.startIndex,
|
|
3307
|
+
scopeEnd: evidence === "field" ? scope.end : scopeNode.endIndex,
|
|
3308
|
+
...arrayElement !== null ? { isArray: true } : {}
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
/**
|
|
3313
|
+
* The statically stated type of a `var` declarator's INITIALIZER (JLS 14.4 / 10.3.2:
|
|
3314
|
+
* `var` takes the type of its initializer, it is not inferred from later use).
|
|
3315
|
+
*
|
|
3316
|
+
* Only shapes whose type the FILE WRITES are read: `new Foo()`, a cast's type, and a
|
|
3317
|
+
* method invocation whose name has exactly ONE declared return type in this file (an
|
|
3318
|
+
* overload set with differing returns is refused, the same rule the `return` evidence
|
|
3319
|
+
* class already states). String literals, chained calls on foreign types, lambdas and
|
|
3320
|
+
* arrays are refused: their type is not written in this file, and a wrong owner is
|
|
3321
|
+
* worse than none.
|
|
3322
|
+
*/
|
|
3323
|
+
javaInferVarInitializerType(declarator, index) {
|
|
3324
|
+
let value = declarator.childForFieldName("value");
|
|
3325
|
+
for (let guard = 0; guard < 8 && value; guard++) {
|
|
3326
|
+
if (value.type !== "parenthesized_expression") break;
|
|
3327
|
+
value = value.namedChild(0);
|
|
3328
|
+
}
|
|
3329
|
+
if (!value) return null;
|
|
3330
|
+
if (value.type === "object_creation_expression") {
|
|
3331
|
+
const typeNode = this.findChildByType(value, "type_identifier") || this.findChildByType(value, "generic_type") || this.findChildByType(value, "scoped_type_identifier");
|
|
3332
|
+
return typeNode ? this.javaDeclaredTypeBaseName(typeNode) : null;
|
|
3333
|
+
}
|
|
3334
|
+
if (value.type === "cast_expression") {
|
|
3335
|
+
return this.javaDeclaredTypeBaseName(value.childForFieldName("type"));
|
|
3336
|
+
}
|
|
3337
|
+
if (value.type === "method_invocation") {
|
|
3338
|
+
const nameNode = value.childForFieldName("name");
|
|
3339
|
+
const name = nameNode ? nameNode.text : "";
|
|
3340
|
+
if (!name || this.isJavaBuiltin(name)) return null;
|
|
3341
|
+
const returns = index.methodReturns.get(name);
|
|
3342
|
+
return returns && returns.size === 1 ? [...returns][0] : null;
|
|
3343
|
+
}
|
|
3344
|
+
return null;
|
|
3345
|
+
}
|
|
3346
|
+
/** The type name a declared-type node writes, or null when the node is a
|
|
3347
|
+
* primitive / `var` / malformed. `javaTypeBaseName` keeps a qualified spelling
|
|
3348
|
+
* (`java.util.List`) as written, which is what "the receiver's statically known
|
|
3349
|
+
* type" means to this lane. */
|
|
3350
|
+
javaDeclaredTypeBaseName(typeNode) {
|
|
3351
|
+
if (!typeNode) return null;
|
|
3352
|
+
const t = typeNode.type;
|
|
3353
|
+
if (t === "integral_type" || t === "floating_point_type" || t === "boolean_type" || t === "void_type") return null;
|
|
3354
|
+
const base = javaTypeBaseName(typeNode);
|
|
3355
|
+
if (!base || base === "var" || base === "void") return null;
|
|
3356
|
+
return base;
|
|
3357
|
+
}
|
|
3358
|
+
/** The innermost scope of `scopes` whose range contains `pos` (innermost = the one
|
|
3359
|
+
* that starts latest; equal starts keep the first seen, which is deterministic
|
|
3360
|
+
* over one tree). */
|
|
3361
|
+
javaInnermostScope(scopes, pos) {
|
|
3362
|
+
let best = null;
|
|
3363
|
+
for (const scope of scopes) {
|
|
3364
|
+
if (scope.start <= pos && pos <= scope.end) {
|
|
3365
|
+
if (!best || scope.start >= best.start) best = scope;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
return best;
|
|
3369
|
+
}
|
|
3370
|
+
/** The base names a type declaration writes in its heritage clauses (JLS 8.1.4/8.1.5,
|
|
3371
|
+
* 9.1.3, 8.10.3): superclass first, then the interface list. Used ONLY by the depth
|
|
3372
|
+
* campaign's `super` receiver class — ancestry is otherwise never inferred. */
|
|
3373
|
+
javaDeclaredParentNames(declNode) {
|
|
3374
|
+
const inheritance = this.extractJavaInheritance(declNode);
|
|
3375
|
+
const names = [];
|
|
3376
|
+
if (inheritance.extendsClass?.name) names.push(inheritance.extendsClass.name);
|
|
3377
|
+
for (const p of inheritance.extendsInterfaces) if (p.name) names.push(p.name);
|
|
3378
|
+
for (const p of inheritance.implementsInterfaces) if (p.name) names.push(p.name);
|
|
3379
|
+
return names;
|
|
3380
|
+
}
|
|
3381
|
+
/** The declared type a bare identifier is BOUND to: method/lambda scopes first (innermost
|
|
3382
|
+
* first, with the declaration-before-use rule of JLS 6.3), then the enclosing types' fields.
|
|
3383
|
+
* Extracted from the receiver resolver so the array-element class can reuse the SAME schema
|
|
3384
|
+
* walk — a second, divergent copy is what this file's audits exist to catch. */
|
|
3385
|
+
javaResolveIdentifierBinding(name, pos, index) {
|
|
3386
|
+
const coveringMethods = [];
|
|
3387
|
+
for (const scope of index.methodScopes) {
|
|
3388
|
+
if (scope.start <= pos && pos <= scope.end) coveringMethods.push(scope);
|
|
3389
|
+
}
|
|
3390
|
+
coveringMethods.sort((a, b) => b.start - a.start);
|
|
3391
|
+
for (const methodScope of coveringMethods) {
|
|
3392
|
+
const hit = methodScope.members.get(name);
|
|
3393
|
+
if (hit && hit.declStart < pos && hit.scopeStart <= pos && pos <= hit.scopeEnd) return hit;
|
|
3394
|
+
}
|
|
3395
|
+
const coveringTypes = [];
|
|
3396
|
+
for (const scope of index.typeScopes) {
|
|
3397
|
+
if (scope.start <= pos && pos <= scope.end) coveringTypes.push(scope);
|
|
3398
|
+
}
|
|
3399
|
+
coveringTypes.sort((a, b) => b.start - a.start);
|
|
3400
|
+
for (const scope of coveringTypes) {
|
|
3401
|
+
const hit = scope.members.get(name);
|
|
3402
|
+
if (hit) return hit;
|
|
3403
|
+
}
|
|
3404
|
+
return null;
|
|
3405
|
+
}
|
|
3406
|
+
/** The innermost TYPE scope containing `pos` that has a NAME. An anonymous class body
|
|
3407
|
+
* deliberately has none: it is not a project declaration, so it can never be an owner. */
|
|
3408
|
+
javaEnclosingTypeScope(index, pos) {
|
|
3409
|
+
let best = null;
|
|
3410
|
+
for (const scope of index.typeScopes) {
|
|
3411
|
+
if (!scope.name) continue;
|
|
3412
|
+
if (scope.start <= pos && pos <= scope.end) {
|
|
3413
|
+
if (!best || scope.start >= best.start) best = scope;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
return best;
|
|
3417
|
+
}
|
|
3418
|
+
/** WOULD THE OWNER CHANNEL RESOLVE THIS ROW? — the guard, now shaped exactly like
|
|
3419
|
+
* `member-owner-resolution.ts` rather than merely "some member of that name exists".
|
|
3420
|
+
*
|
|
3421
|
+
* The channel's candidate model (its own code, mirrored):
|
|
3422
|
+
* 1. candidates are the owner's members with the relationship's KIND filter —
|
|
3423
|
+
* `calls` accepts function/method/constructor, `uses` accepts any member;
|
|
3424
|
+
* 2. if the OWNER declares the name, the row resolves there (even when an ancestor
|
|
3425
|
+
* also declares it: the direct hit wins);
|
|
3426
|
+
* 3. otherwise the members collected across the ancestor chain must be EXACTLY ONE —
|
|
3427
|
+
* two ancestors declaring the name is `ambiguousInChain`, a decline.
|
|
3428
|
+
*
|
|
3429
|
+
* Java's ProjectIndex CARRIES ancestry since 2026-10-20 — the spec-local `baseResolver` in
|
|
3430
|
+
* `JAVA_DECLARATION_SPEC` reads each type's DIRECT bases (measured: java's `typeBases` went
|
|
3431
|
+
* from EMPTY to populated; the fix is confined to the java spec block, and the other
|
|
3432
|
+
* languages' `typeBases` are byte-identical before and after). **THIS CHANNEL STILL WALKS
|
|
3433
|
+
* ONLY IN-FILE ANCESTRY**: the walk below reads `index.typeScopesByName`, and when a parent
|
|
3434
|
+
* is not in this file it consults the project's MEMBERS for that parent but does not walk
|
|
3435
|
+
* the PROJECT's ancestry. So a row whose only chance is a CROSS-FILE inherited member is
|
|
3436
|
+
* still refused (missing, never wrong).
|
|
3437
|
+
*
|
|
3438
|
+
* **THE PROJECT ANCESTRY IS NOW AVAILABLE TO BE USED HERE — AND THAT IS A SEPARATE INSTRUMENT,
|
|
3439
|
+
* NOT THIS COMMENT.** Walking `projectIndex.typeBases` would let this channel decide the
|
|
3440
|
+
* cross-file inherited-member rows the project index now knows the shape of; it would also
|
|
3441
|
+
* change the lane's recorded arm, so it is not a lane's unilateral change. Until it is
|
|
3442
|
+
* decided, the behaviour is exactly what it was. */
|
|
3443
|
+
javaOwnerResolvable(ownerName, memberName, rowType, index, projectIndex) {
|
|
3444
|
+
if (!ownerName || !memberName) return false;
|
|
3445
|
+
const wantsMethod = rowType === "calls";
|
|
3446
|
+
const fileScope = index.typeScopesByName.get(ownerName);
|
|
3447
|
+
const directFile = wantsMethod ? fileScope?.methodNames?.has(memberName) ?? false : fileScope?.memberNames?.has(memberName) ?? false;
|
|
3448
|
+
const directProject = projectIndex?.complete === true ? projectIndex.typeMembers.get(ownerName)?.has(memberName) ?? false : false;
|
|
3449
|
+
if (directFile || directProject) return true;
|
|
3450
|
+
let hits = 0;
|
|
3451
|
+
const walk = (name, depth) => {
|
|
3452
|
+
if (depth > 8) return;
|
|
3453
|
+
const scope = index.typeScopesByName.get(name);
|
|
3454
|
+
for (const parent of scope?.parents ?? []) {
|
|
3455
|
+
const parentScope = index.typeScopesByName.get(parent);
|
|
3456
|
+
if (parentScope) {
|
|
3457
|
+
const has = wantsMethod ? parentScope.methodNames?.has(memberName) ?? false : parentScope.memberNames?.has(memberName) ?? false;
|
|
3458
|
+
if (has) hits++;
|
|
3459
|
+
walk(parent, depth + 1);
|
|
3460
|
+
continue;
|
|
3461
|
+
}
|
|
3462
|
+
if (projectIndex?.complete === true && (projectIndex.typeMembers.get(parent)?.has(memberName) ?? false)) hits++;
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
walk(ownerName, 0);
|
|
3466
|
+
return hits === 1;
|
|
3467
|
+
}
|
|
3468
|
+
/** Can an owner spelled `ownerName` be attached at all?
|
|
3469
|
+
*
|
|
3470
|
+
* A type DECLARED IN THIS FILE is eligible: the enclosing-type classes are exactly the
|
|
3471
|
+
* declarations this file contains. A type declared only in the PROJECT is eligible when a
|
|
3472
|
+
* COMPLETE index declares it exactly once; completeness is the bit that turns "declared
|
|
3473
|
+
* once in the project" from a fragment's opinion into a statement. When the complete
|
|
3474
|
+
* index shows the simple name declared MORE THAN ONCE the owner is refused: the channel
|
|
3475
|
+
* reports `ownerAmbiguous` and would withdraw the name binding for nothing. */
|
|
3476
|
+
javaTypeEligible(ownerName, index, projectIndex) {
|
|
3477
|
+
if (!ownerName) return false;
|
|
3478
|
+
const inFile = index.typeNames.has(ownerName);
|
|
3479
|
+
if (projectIndex?.complete === true) {
|
|
3480
|
+
const count = projectIndex.symbolCounts.get(ownerName) ?? 0;
|
|
3481
|
+
if (count > 1) return false;
|
|
3482
|
+
if (inFile) return true;
|
|
3483
|
+
return projectIndex.typeKinds.has(ownerName) && count === 1;
|
|
3484
|
+
}
|
|
3485
|
+
return inFile;
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3488
|
+
* The declared type of a call's receiver — the depth campaign's evidence classes.
|
|
3489
|
+
*
|
|
3490
|
+
* The 2026-10-18 contract classes (field / param / local / return) are read from THE
|
|
3491
|
+
* FILE'S OWN DECLARATIONS by `javaReceiverOwnerTypeFromFile` and are byte-identical to
|
|
3492
|
+
* that contract.
|
|
3493
|
+
*
|
|
3494
|
+
* The 2026-10-19 depth classes are evidence the file states, each guarded by
|
|
3495
|
+
* `javaOwnerResolvable` (the owner channel's own kind + ancestor rule) and
|
|
3496
|
+
* `javaTypeEligible`:
|
|
3497
|
+
* * `this` — an implicit-`this` call (JLS 15.12.1) and an explicit `this.m()`;
|
|
3498
|
+
* also implicit-`this` READS. Refused when an unqualified name could
|
|
3499
|
+
* come from a static import (JLS 7.5.3/7.5.4).
|
|
3500
|
+
* * `super` — `super.m()`: the first IN-FILE base that declares the member.
|
|
3501
|
+
* * `enum` — `Mode.ONE.act()` and a READ of the constant: its static type IS the
|
|
3502
|
+
* enum (JLS 8.9.1).
|
|
3503
|
+
* * `initializer` — `var` whose initializer states the type (JLS 14.4).
|
|
3504
|
+
* * `new` — a receiver that IS an object-creation expression (`new Foo().m()`):
|
|
3505
|
+
* the created type is written.
|
|
3506
|
+
* * `cast` — a receiver that is a cast (`((Foo) x).m()`): the cast type is written.
|
|
3507
|
+
* * `element` — an array-element receiver (`foos[i].m()`): the declared element type
|
|
3508
|
+
* of the array binding is written (the binding comes from the schema
|
|
3509
|
+
* classes above).
|
|
3510
|
+
*
|
|
3511
|
+
* Returns null — never a guess — for a receiver whose type no static evidence here states:
|
|
3512
|
+
* an inherited field, a chained call on a foreign/unknown return type, a library-typed
|
|
3513
|
+
* parameter, an untyped lambda parameter, multi-catch, and a CLASS-QUALIFIED STATIC CALL
|
|
3514
|
+
* (`Helper.help()` — its receiver already names its owner, which the 2026-10-18 contract
|
|
3515
|
+
* and its fixture deliberately keep owner-less).
|
|
3516
|
+
*/
|
|
3517
|
+
javaReceiverOwnerType(callNode, index, projectIndex, rowType = "calls") {
|
|
3518
|
+
const fieldLocal = this.javaReceiverOwnerTypeFromFile(callNode, index);
|
|
3519
|
+
const targetNode = callNode.childForFieldName("name") ?? callNode.childForFieldName("field");
|
|
3520
|
+
const targetName = targetNode ? targetNode.text : "";
|
|
3521
|
+
if (fieldLocal) {
|
|
3522
|
+
if (fieldLocal.evidence !== "initializer") return fieldLocal;
|
|
3523
|
+
if (!this.javaTypeEligible(fieldLocal.type, index, projectIndex)) return null;
|
|
3524
|
+
if (!this.javaOwnerResolvable(fieldLocal.type, targetName, rowType, index, projectIndex)) return null;
|
|
3525
|
+
return fieldLocal;
|
|
3526
|
+
}
|
|
3527
|
+
if (!targetName) return null;
|
|
3528
|
+
const objectNode = callNode.childForFieldName("object");
|
|
3529
|
+
const pos = callNode.startIndex;
|
|
3530
|
+
const enclosing = this.javaEnclosingTypeScope(index, pos);
|
|
3531
|
+
const emit = (type, evidence) => {
|
|
3532
|
+
if (!this.javaTypeEligible(type, index, projectIndex)) return null;
|
|
3533
|
+
if (!this.javaOwnerResolvable(type, targetName, rowType, index, projectIndex)) return null;
|
|
3534
|
+
return { type, evidence };
|
|
3535
|
+
};
|
|
3536
|
+
if (!objectNode) {
|
|
3537
|
+
if (!enclosing?.name) return null;
|
|
3538
|
+
if (index.staticImportLeaves.has(targetName) || index.hasStaticOnDemand) return null;
|
|
3539
|
+
return emit(enclosing.name, "this");
|
|
3540
|
+
}
|
|
3541
|
+
const ot = objectNode.type;
|
|
3542
|
+
if (ot === "this") {
|
|
3543
|
+
if (!enclosing?.name) return null;
|
|
3544
|
+
return emit(enclosing.name, "this");
|
|
3545
|
+
}
|
|
3546
|
+
if (ot === "super") {
|
|
3547
|
+
if (!enclosing?.name) return null;
|
|
3548
|
+
for (const parent of enclosing.parents ?? []) {
|
|
3549
|
+
const hit = emit(parent, "super");
|
|
3550
|
+
if (hit) return hit;
|
|
3551
|
+
}
|
|
3552
|
+
return null;
|
|
3553
|
+
}
|
|
3554
|
+
if (ot === "object_creation_expression") {
|
|
3555
|
+
const createdNode = this.findChildByType(objectNode, "type_identifier") || this.findChildByType(objectNode, "generic_type") || this.findChildByType(objectNode, "scoped_type_identifier");
|
|
3556
|
+
const created = createdNode ? this.javaDeclaredTypeBaseName(createdNode) : null;
|
|
3557
|
+
if (!created) return null;
|
|
3558
|
+
return emit(created, "new");
|
|
3559
|
+
}
|
|
3560
|
+
if (ot === "cast_expression" || ot === "parenthesized_expression") {
|
|
3561
|
+
let expr = objectNode;
|
|
3562
|
+
for (let guard = 0; guard < 8 && expr && expr.type === "parenthesized_expression"; guard++) {
|
|
3563
|
+
expr = expr.namedChild(0);
|
|
3564
|
+
}
|
|
3565
|
+
if (expr && expr.type === "cast_expression") {
|
|
3566
|
+
const castType = this.javaDeclaredTypeBaseName(expr.childForFieldName("type"));
|
|
3567
|
+
if (!castType) return null;
|
|
3568
|
+
return emit(castType, "cast");
|
|
3569
|
+
}
|
|
3570
|
+
return null;
|
|
3571
|
+
}
|
|
3572
|
+
if (ot === "array_access") {
|
|
3573
|
+
const arrayNode = objectNode.childForFieldName("array");
|
|
3574
|
+
if (!arrayNode || arrayNode.type !== "identifier") return null;
|
|
3575
|
+
const binding = this.javaResolveIdentifierBinding(arrayNode.text, pos, index);
|
|
3576
|
+
if (!binding || !binding.isArray) return null;
|
|
3577
|
+
return emit(binding.type, "element");
|
|
3578
|
+
}
|
|
3579
|
+
if (ot === "identifier" && callNode.type === "field_access") {
|
|
3580
|
+
const enumName = objectNode.text;
|
|
3581
|
+
if (index.typeKindsByName.get(enumName) !== "enum_declaration") return null;
|
|
3582
|
+
return emit(enumName, "enum") ?? null;
|
|
3583
|
+
}
|
|
3584
|
+
if (ot === "field_access") {
|
|
3585
|
+
const parts = objectNode.text.split(".");
|
|
3586
|
+
if (parts.length === 2 && index.typeKindsByName.get(parts[0]) === "enum_declaration") {
|
|
3587
|
+
if (!index.typeScopesByName.get(parts[0])?.memberNames?.has(parts[1])) return null;
|
|
3588
|
+
const enumName = parts[0];
|
|
3589
|
+
return emit(enumName, "enum");
|
|
3590
|
+
}
|
|
3591
|
+
return null;
|
|
3592
|
+
}
|
|
3593
|
+
return null;
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* The 2026-10-18 contract: the declared type of a receiver from THE FILE'S OWN
|
|
3597
|
+
* DECLARATIONS. Returns null — never a guess — for every receiver whose type this file
|
|
3598
|
+
* does not state: an implicit `this`, a class-qualified static call, `super`, chained
|
|
3599
|
+
* access on an untyped receiver, an inherited field, and a `var` local. The four
|
|
3600
|
+
* evidence classes are the declarations the contract names: field / param /
|
|
3601
|
+
* local / return.
|
|
3602
|
+
*/
|
|
3603
|
+
javaReceiverOwnerTypeFromFile(callNode, index) {
|
|
3604
|
+
const objectNode = callNode.childForFieldName("object");
|
|
3605
|
+
if (!objectNode) return null;
|
|
3606
|
+
const pos = callNode.startIndex;
|
|
3607
|
+
if (objectNode.type === "identifier") {
|
|
3608
|
+
const binding = this.javaResolveIdentifierBinding(objectNode.text, pos, index);
|
|
3609
|
+
if (binding && !binding.isArray) return { type: binding.type, evidence: binding.evidence };
|
|
3610
|
+
return null;
|
|
3611
|
+
}
|
|
3612
|
+
if (objectNode.type === "field_access") {
|
|
3613
|
+
const parts = objectNode.text.split(".");
|
|
3614
|
+
if (parts.length === 2 && parts[0] === "this") {
|
|
3615
|
+
const name = parts[1];
|
|
3616
|
+
for (const scope of index.typeScopes) {
|
|
3617
|
+
if (!(scope.start <= pos && pos <= scope.end)) continue;
|
|
3618
|
+
const hit = scope.members.get(name);
|
|
3619
|
+
if (hit) return { type: hit.type, evidence: "field" };
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
return null;
|
|
3623
|
+
}
|
|
3624
|
+
if (objectNode.type === "method_invocation") {
|
|
3625
|
+
const nameNode = objectNode.childForFieldName("name");
|
|
3626
|
+
const name = nameNode ? nameNode.text : null;
|
|
3627
|
+
if (!name || this.isJavaBuiltin(name)) return null;
|
|
3628
|
+
const returns = index.methodReturns.get(name);
|
|
3629
|
+
if (returns && returns.size === 1) {
|
|
3630
|
+
const only = [...returns][0];
|
|
3631
|
+
return { type: only, evidence: "return" };
|
|
3632
|
+
}
|
|
3633
|
+
return null;
|
|
3634
|
+
}
|
|
3635
|
+
return null;
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* Short annotation name from an annotation/marker_annotation node's name
|
|
3639
|
+
* child. The pinned grammar serializes the name as $._name =
|
|
3640
|
+
* choice(identifier, scoped_identifier) — probe-verified: `@Override` has a
|
|
3641
|
+
* direct identifier, `@org.springframework.stereotype.Service` has a
|
|
3642
|
+
* scoped_identifier. The short name is the last dotted segment; bare names
|
|
3643
|
+
* pass through unchanged.
|
|
3644
|
+
*
|
|
3645
|
+
* F6 audit: a third `_reserved_identifier` fallback had been added here on
|
|
3646
|
+
* the claim that the grammar's `_name` was a triple choice including it.
|
|
3647
|
+
* That name is an underscore-prefixed HIDDEN rule, so `.type` can never
|
|
3648
|
+
* equal it and `findChildByType` can never match it — a branch that could
|
|
3649
|
+
* not fire. Removed; the two real shapes cover every annotation name.
|
|
3650
|
+
*/
|
|
3651
|
+
getJavaAnnotationShortName(ann) {
|
|
3652
|
+
const nameNode = this.findChildByType(ann, "identifier") || this.findChildByType(ann, "scoped_identifier");
|
|
3653
|
+
if (!nameNode) return null;
|
|
3654
|
+
const text = nameNode.text;
|
|
3655
|
+
const lastDot = text.lastIndexOf(".");
|
|
3656
|
+
return lastDot === -1 ? text : text.slice(lastDot + 1);
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Bare annotation names extracted from the AST (@Service → Service), read
|
|
3660
|
+
* from the identifier field of each annotation node — never from text.
|
|
3661
|
+
*/
|
|
3662
|
+
getJavaAnnotationNames(node) {
|
|
3663
|
+
const names = [];
|
|
3664
|
+
const modifiersNode = this.findChildByType(node, "modifiers");
|
|
3665
|
+
const target = modifiersNode || node;
|
|
3666
|
+
for (let i = 0; i < target.namedChildCount; i++) {
|
|
3667
|
+
const child = target.namedChild(i);
|
|
3668
|
+
if (child && (child.type === "annotation" || child.type === "marker_annotation")) {
|
|
3669
|
+
const name = this.getJavaAnnotationShortName(child);
|
|
3670
|
+
if (name) names.push(name);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
return names;
|
|
3674
|
+
}
|
|
3675
|
+
/**
|
|
3676
|
+
* Semantic text of a value node: for string_literal nodes the concatenated
|
|
3677
|
+
* string_fragment children (unquoted) instead of raw quoted text; for all
|
|
3678
|
+
* other node types the raw text.
|
|
3679
|
+
*/
|
|
3680
|
+
extractStringValue(node) {
|
|
3681
|
+
if (!node) return void 0;
|
|
3682
|
+
if (node.type === "string_literal") {
|
|
3683
|
+
let fragmentText = "";
|
|
3684
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3685
|
+
const c = node.namedChild(i);
|
|
3686
|
+
if (c && (c.type === "string_fragment" || c.type === "multiline_string_fragment")) fragmentText += c.text;
|
|
3687
|
+
}
|
|
3688
|
+
if (fragmentText) return fragmentText;
|
|
3689
|
+
}
|
|
3690
|
+
if (node.type === "binary_expression") {
|
|
3691
|
+
return this.extractJavaConstantString(node);
|
|
3692
|
+
}
|
|
3693
|
+
return node.text;
|
|
3694
|
+
}
|
|
3695
|
+
/** Strict constant-string folder: literals/text blocks/parens/+ chains
|
|
3696
|
+
* only. Any non-literal operand makes the whole expression unresolved. */
|
|
3697
|
+
extractJavaConstantString(node) {
|
|
3698
|
+
if (node.type === "parenthesized_expression") {
|
|
3699
|
+
const inner = node.namedChild(0);
|
|
3700
|
+
return inner ? this.extractJavaConstantString(inner) : void 0;
|
|
3701
|
+
}
|
|
3702
|
+
if (node.type === "string_literal") {
|
|
3703
|
+
let fragmentText = "";
|
|
3704
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3705
|
+
const c = node.namedChild(i);
|
|
3706
|
+
if (c && (c.type === "string_fragment" || c.type === "multiline_string_fragment")) fragmentText += c.text;
|
|
3707
|
+
}
|
|
3708
|
+
return fragmentText || void 0;
|
|
3709
|
+
}
|
|
3710
|
+
if (node.type === "binary_expression") {
|
|
3711
|
+
const op = node.childForFieldName("operator")?.text;
|
|
3712
|
+
if (op !== "+") return void 0;
|
|
3713
|
+
const l = node.namedChild(0);
|
|
3714
|
+
const r = node.namedChild(1);
|
|
3715
|
+
if (!l || !r) return void 0;
|
|
3716
|
+
const lt = this.extractJavaConstantString(l);
|
|
3717
|
+
const rt = this.extractJavaConstantString(r);
|
|
3718
|
+
if (lt === void 0 || rt === void 0) return void 0;
|
|
3719
|
+
const joined = lt + rt;
|
|
3720
|
+
return joined || void 0;
|
|
3721
|
+
}
|
|
3722
|
+
return void 0;
|
|
3723
|
+
}
|
|
3724
|
+
getJavaVisibility(modifiers) {
|
|
3725
|
+
if (modifiers.indexOf("public") !== -1) return "public";
|
|
3726
|
+
if (modifiers.indexOf("private") !== -1) return "private";
|
|
3727
|
+
if (modifiers.indexOf("protected") !== -1) return "protected";
|
|
3728
|
+
return void 0;
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* Extract the base type name from a type node, stripping type arguments so
|
|
3732
|
+
* inheritance targets are clean symbol names (Repository<T> → Repository).
|
|
3733
|
+
* A scoped type (BaseService.Entity) keeps its full dotted name.
|
|
3734
|
+
*/
|
|
3735
|
+
/**
|
|
3736
|
+
* M14 #419 G4: nearest enclosing type scope for call attribution when a
|
|
3737
|
+
* method_invocation sits outside any method body (initializer blocks,
|
|
3738
|
+
* field initializers, enum constant bodies). Scans the scope stack from
|
|
3739
|
+
* the top, skipping member-level scopes (Property/Constant/EnumMember),
|
|
3740
|
+
* and returns the nearest type-level scope. Defined in-class so the
|
|
3741
|
+
* handler above can reach it without exposing Scope publicly.
|
|
3742
|
+
*/
|
|
3743
|
+
resolveEnclosingTypeScope(scopeStack) {
|
|
3744
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
3745
|
+
const s = scopeStack[i];
|
|
3746
|
+
if (s.kind === ASTEngine.SymbolKind.Class || s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Enum || s.kind === ASTEngine.SymbolKind.Type) {
|
|
3747
|
+
return { name: s.name, kind: s.kind };
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
return void 0;
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* Can this string be a java NAME? Letters, digits, `_`, `$`, and `.` for a
|
|
3754
|
+
* qualified name. No regex (analyzers carry no regex literals, R2 register) —
|
|
3755
|
+
* a character walk, the same shape as the census instrument's.
|
|
3756
|
+
*
|
|
3757
|
+
* Used to refuse an edge whose target is not a name at all, rather than to
|
|
3758
|
+
* invent one. `newArrayList(v3(), v4())` and `"com.google.common."` both fail
|
|
3759
|
+
* it; `V`, `com.google.common.collect`, `ImmutableList.Builder` and
|
|
3760
|
+
* `CollectionFeature.NONE` all pass.
|
|
3761
|
+
*/
|
|
3762
|
+
isJavaNameShape(s) {
|
|
3763
|
+
if (!s) return false;
|
|
3764
|
+
for (let i = 0; i < s.length; i++) {
|
|
3765
|
+
const c = s[i];
|
|
3766
|
+
const ok = c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "_" || c === "$" || c === ".";
|
|
3767
|
+
if (!ok) return false;
|
|
3768
|
+
}
|
|
3769
|
+
return true;
|
|
3770
|
+
}
|
|
3771
|
+
extractJavaTypeBaseName(typeNode) {
|
|
3772
|
+
return javaTypeBaseName(typeNode);
|
|
3773
|
+
}
|
|
3774
|
+
/**
|
|
3775
|
+
* M59 (#825): every parent is `{name, line}` — the line the PARENT IS NAMED on.
|
|
3776
|
+
*
|
|
3777
|
+
* java writes a long heritage list on separate lines
|
|
3778
|
+
* (`class A extends B\n implements C, D`), so a clause-start line named only
|
|
3779
|
+
* the first entry. Same rule as scala's `extractSupertypes` and the reason the
|
|
3780
|
+
* shape is pairs rather than names.
|
|
3781
|
+
*/
|
|
3782
|
+
extractJavaInheritance(classNode) {
|
|
3783
|
+
const inheritance = {
|
|
3784
|
+
extendsClass: null,
|
|
3785
|
+
// interface_declaration > extends_interfaces > type_list: EVERY parent is an extends (JLS 9.1.3)
|
|
3786
|
+
extendsInterfaces: [],
|
|
3787
|
+
implementsInterfaces: [],
|
|
3788
|
+
// sealed class permits clause (JLS 8.1.1.2): permitted subtypes are
|
|
3789
|
+
// CHILDREN of the sealed class, not ancestors — kept out of parents[]
|
|
3790
|
+
// and surfaced separately so callers can emit directional edges.
|
|
3791
|
+
permitsSubtypes: []
|
|
3792
|
+
};
|
|
3793
|
+
const superclass = this.findChildByType(classNode, "superclass");
|
|
3794
|
+
if (superclass) {
|
|
3795
|
+
const typeIdentifier = this.findChildByType(superclass, "scoped_type_identifier") || this.findChildByType(superclass, "generic_type") || this.findChildByType(superclass, "type_identifier");
|
|
3796
|
+
if (typeIdentifier) inheritance.extendsClass = { name: this.extractJavaTypeBaseName(typeIdentifier), line: typeIdentifier.startPosition.row + 1 };
|
|
3797
|
+
}
|
|
3798
|
+
const superInterfaces = this.findChildByType(classNode, "super_interfaces") || this.findChildByType(classNode, "extends_interfaces");
|
|
3799
|
+
if (superInterfaces) {
|
|
3800
|
+
const targetList = this.findChildByType(superInterfaces, "type_list") || superInterfaces;
|
|
3801
|
+
const isInterfaceExtends = superInterfaces.type === "extends_interfaces";
|
|
3802
|
+
const bucket = isInterfaceExtends ? inheritance.extendsInterfaces : inheritance.implementsInterfaces;
|
|
3803
|
+
for (let i = 0; i < targetList.namedChildCount; i++) {
|
|
3804
|
+
const interfaceType = targetList.namedChild(i);
|
|
3805
|
+
if (interfaceType && (interfaceType.type === "type_identifier" || interfaceType.type === "generic_type" || interfaceType.type === "scoped_type_identifier")) {
|
|
3806
|
+
bucket.push({ name: this.extractJavaTypeBaseName(interfaceType), line: interfaceType.startPosition.row + 1 });
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
const permitsNode = this.findChildByType(classNode, "permits");
|
|
3811
|
+
if (permitsNode) {
|
|
3812
|
+
const permitsList = this.findChildByType(permitsNode, "type_list") || permitsNode;
|
|
3813
|
+
for (let i = 0; i < permitsList.namedChildCount; i++) {
|
|
3814
|
+
const subtype = permitsList.namedChild(i);
|
|
3815
|
+
if (subtype && (subtype.type === "type_identifier" || subtype.type === "generic_type" || subtype.type === "scoped_type_identifier")) {
|
|
3816
|
+
inheritance.permitsSubtypes.push({ name: this.extractJavaTypeBaseName(subtype), line: subtype.startPosition.row + 1 });
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
return inheritance;
|
|
3821
|
+
}
|
|
3822
|
+
extractJavaParameters(node, relationships, ownerPrefix) {
|
|
3823
|
+
const parameters = [];
|
|
3824
|
+
const formalParams = this.findChildByType(node, "formal_parameters");
|
|
3825
|
+
const targetNode = formalParams ? formalParams : node;
|
|
3826
|
+
for (let i = 0; i < targetNode.namedChildCount; i++) {
|
|
3827
|
+
const param = targetNode.namedChild(i);
|
|
3828
|
+
if (param && param.type === "formal_parameter") {
|
|
3829
|
+
const nameNode = param.childForFieldName("name") || this.findChildByType(param, "identifier");
|
|
3830
|
+
if (nameNode) {
|
|
3831
|
+
const paramType = this.extractJavaParameterType(param);
|
|
3832
|
+
parameters.push({ name: nameNode.text, type: paramType, isOptional: false });
|
|
3833
|
+
if (relationships && ownerPrefix) {
|
|
3834
|
+
const valueAnn = inArray(this.getJavaAnnotationNames(param), "Value");
|
|
3835
|
+
if (valueAnn) {
|
|
3836
|
+
const valMatch = this.getAnnotationValue(param, "Value", "value");
|
|
3837
|
+
if (valMatch && valMatch.startsWith("${") && valMatch.endsWith("}")) {
|
|
3838
|
+
relationships.push({
|
|
3839
|
+
source: ownerPrefix + "." + nameNode.text,
|
|
3840
|
+
target: valMatch.substring(2, valMatch.length - 1).split(":")[0],
|
|
3841
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
3842
|
+
confidence: 1,
|
|
3843
|
+
// M59 (#825): the key is written in the PARAMETER's own
|
|
3844
|
+
// `@Value` — the parameter is the node that carries it.
|
|
3845
|
+
...this.annotationLine(param, "Value") ? { line_number: this.annotationLine(param, "Value") } : {},
|
|
3846
|
+
metadata: { isConfiguration: true, relationshipType: "config_property" }
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
} else if (param && param.type === "spread_parameter") {
|
|
3853
|
+
const decl = this.findChildByType(param, "variable_declarator");
|
|
3854
|
+
const nameNode = decl ? this.findChildByType(decl, "identifier") : null;
|
|
3855
|
+
if (nameNode) {
|
|
3856
|
+
let spreadType = void 0;
|
|
3857
|
+
for (let cIdx = 0; cIdx < param.namedChildCount; cIdx++) {
|
|
3858
|
+
const c = param.namedChild(cIdx);
|
|
3859
|
+
if (c && c.type !== "modifiers" && !stringIncludes(c.type, "annotation") && c.type !== "variable_declarator") {
|
|
3860
|
+
spreadType = c.text;
|
|
3861
|
+
break;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
parameters.push({ name: nameNode.text, type: (spreadType || "unknown") + "[]", isOptional: false });
|
|
3865
|
+
}
|
|
3866
|
+
} else if (param && param.type === "ERROR") {
|
|
3867
|
+
let rawText = param.text;
|
|
3868
|
+
if (rawText.startsWith(",")) rawText = rawText.slice(1);
|
|
3869
|
+
rawText = rawText.trim();
|
|
3870
|
+
if (rawText) {
|
|
3871
|
+
const parts = splitByChar(rawText, ",");
|
|
3872
|
+
for (const p of parts) {
|
|
3873
|
+
const pt = p.trim();
|
|
3874
|
+
if (!pt) continue;
|
|
3875
|
+
const tokens = tokenizeWhitespace(pt);
|
|
3876
|
+
let name = tokens[tokens.length - 1];
|
|
3877
|
+
let cleanName = "";
|
|
3878
|
+
for (let ci = 0; ci < name.length; ci++) {
|
|
3879
|
+
const ch = name[ci];
|
|
3880
|
+
if (ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_") cleanName += ch;
|
|
3881
|
+
}
|
|
3882
|
+
name = cleanName;
|
|
3883
|
+
if (name) {
|
|
3884
|
+
const lastToken = tokens[tokens.length - 1];
|
|
3885
|
+
let typeGuess = pt.length > lastToken.length ? pt.slice(0, pt.length - lastToken.length).trim() : "";
|
|
3886
|
+
typeGuess = stripJavaAnnotations(typeGuess).split("...").join("[]").trim() || "unknown";
|
|
3887
|
+
parameters.push({ name, type: typeGuess, isOptional: false });
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
return parameters;
|
|
3894
|
+
}
|
|
3895
|
+
extractJavaParameterType(paramNode) {
|
|
3896
|
+
const typeNode = paramNode.childForFieldName("type");
|
|
3897
|
+
if (typeNode) return typeNode.text;
|
|
3898
|
+
for (let i = 0; i < paramNode.namedChildCount; i++) {
|
|
3899
|
+
const child = paramNode.namedChild(i);
|
|
3900
|
+
if (child && child.type !== "identifier" && child.type !== "modifiers" && !stringIncludes(child.type, "annotation")) return child.text;
|
|
3901
|
+
}
|
|
3902
|
+
return void 0;
|
|
3903
|
+
}
|
|
3904
|
+
extractJavaReturnType(node) {
|
|
3905
|
+
const typeNode = node.childForFieldName("type");
|
|
3906
|
+
if (typeNode) return typeNode.text;
|
|
3907
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3908
|
+
const child = node.namedChild(i);
|
|
3909
|
+
if (child && (child.type === "type_identifier" || child.type === "generic_type" || child.type === "array_type" || child.type === "scoped_type_identifier" || child.type === "void_type" || child.type === "integral_type" || child.type === "floating_point_type" || child.type === "boolean_type")) {
|
|
3910
|
+
return child.text;
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
return void 0;
|
|
3914
|
+
}
|
|
3915
|
+
extractJavaFieldType(fieldNode) {
|
|
3916
|
+
return this.extractJavaReturnType(fieldNode);
|
|
3917
|
+
}
|
|
3918
|
+
buildJavaCallExpression(invocationNode) {
|
|
3919
|
+
const objectNode = invocationNode.childForFieldName("object");
|
|
3920
|
+
const nameNode = invocationNode.childForFieldName("name");
|
|
3921
|
+
if (!nameNode) {
|
|
3922
|
+
const text = invocationNode.text;
|
|
3923
|
+
const paren = text.indexOf("(");
|
|
3924
|
+
return paren === -1 ? text : text.slice(0, paren);
|
|
3925
|
+
}
|
|
3926
|
+
if (objectNode) {
|
|
3927
|
+
const objectText = objectNode.type === "method_invocation" ? this.buildJavaCallExpression(objectNode) : objectNode.text;
|
|
3928
|
+
return `${objectText}.${nameNode.text}`;
|
|
3929
|
+
}
|
|
3930
|
+
return nameNode.text;
|
|
3931
|
+
}
|
|
3932
|
+
isJavaBuiltin(call) {
|
|
3933
|
+
const builtins = [
|
|
3934
|
+
"System.out.println",
|
|
3935
|
+
"System.out.print",
|
|
3936
|
+
"System.out.printf",
|
|
3937
|
+
"System.err.println",
|
|
3938
|
+
"System.err.print",
|
|
3939
|
+
"System.exit",
|
|
3940
|
+
"System.gc",
|
|
3941
|
+
"System.currentTimeMillis",
|
|
3942
|
+
"System.nanoTime",
|
|
3943
|
+
"System.getProperty",
|
|
3944
|
+
"System.getenv",
|
|
3945
|
+
"System.identityHashCode",
|
|
3946
|
+
"System.arraycopy",
|
|
3947
|
+
"toString",
|
|
3948
|
+
"hashCode",
|
|
3949
|
+
"equals",
|
|
3950
|
+
"getClass",
|
|
3951
|
+
"notify",
|
|
3952
|
+
"notifyAll",
|
|
3953
|
+
"wait",
|
|
3954
|
+
"clone",
|
|
3955
|
+
"finalize"
|
|
3956
|
+
];
|
|
3957
|
+
for (let i = 0; i < builtins.length; i++) {
|
|
3958
|
+
if (builtins[i] === call) return true;
|
|
3959
|
+
}
|
|
3960
|
+
const firstDot = call.indexOf(".");
|
|
3961
|
+
const root = firstDot > 0 ? call.slice(0, firstDot) : call;
|
|
3962
|
+
const STDLIB_ROOTS = /* @__PURE__ */ new Set([
|
|
3963
|
+
// java.lang
|
|
3964
|
+
"System",
|
|
3965
|
+
"String",
|
|
3966
|
+
"Integer",
|
|
3967
|
+
"Long",
|
|
3968
|
+
"Double",
|
|
3969
|
+
"Float",
|
|
3970
|
+
"Boolean",
|
|
3971
|
+
"Byte",
|
|
3972
|
+
"Short",
|
|
3973
|
+
"Character",
|
|
3974
|
+
"Math",
|
|
3975
|
+
"StrictMath",
|
|
3976
|
+
"Object",
|
|
3977
|
+
"Class",
|
|
3978
|
+
"Thread",
|
|
3979
|
+
"Runnable",
|
|
3980
|
+
"Runtime",
|
|
3981
|
+
"Process",
|
|
3982
|
+
"ProcessBuilder",
|
|
3983
|
+
"Throwable",
|
|
3984
|
+
"Error",
|
|
3985
|
+
"Exception",
|
|
3986
|
+
"StackTraceElement",
|
|
3987
|
+
"Enum",
|
|
3988
|
+
"Void",
|
|
3989
|
+
"Compiler",
|
|
3990
|
+
"Package",
|
|
3991
|
+
// java.util
|
|
3992
|
+
"ArrayList",
|
|
3993
|
+
"LinkedList",
|
|
3994
|
+
"HashMap",
|
|
3995
|
+
"HashSet",
|
|
3996
|
+
"TreeMap",
|
|
3997
|
+
"TreeSet",
|
|
3998
|
+
"LinkedHashMap",
|
|
3999
|
+
"LinkedHashSet",
|
|
4000
|
+
"WeakHashMap",
|
|
4001
|
+
"IdentityHashMap",
|
|
4002
|
+
"Collections",
|
|
4003
|
+
"Arrays",
|
|
4004
|
+
"Objects",
|
|
4005
|
+
"Optional",
|
|
4006
|
+
"OptionalInt",
|
|
4007
|
+
"OptionalLong",
|
|
4008
|
+
"OptionalDouble",
|
|
4009
|
+
"Stream",
|
|
4010
|
+
"Collectors",
|
|
4011
|
+
"Comparator",
|
|
4012
|
+
"Iterator",
|
|
4013
|
+
"ListIterator",
|
|
4014
|
+
"Spliterator",
|
|
4015
|
+
"Random",
|
|
4016
|
+
"UUID",
|
|
4017
|
+
"Date",
|
|
4018
|
+
"Calendar",
|
|
4019
|
+
"TimeZone",
|
|
4020
|
+
"Locale",
|
|
4021
|
+
"Currency",
|
|
4022
|
+
"Base64",
|
|
4023
|
+
"Scanner",
|
|
4024
|
+
"Formatter",
|
|
4025
|
+
"StringJoiner",
|
|
4026
|
+
"AbstractList",
|
|
4027
|
+
"AbstractMap",
|
|
4028
|
+
"AbstractSet",
|
|
4029
|
+
"AbstractCollection",
|
|
4030
|
+
"EnumMap",
|
|
4031
|
+
"EnumSet",
|
|
4032
|
+
"Queue",
|
|
4033
|
+
"Deque",
|
|
4034
|
+
"Stack",
|
|
4035
|
+
"Vector",
|
|
4036
|
+
"Hashtable",
|
|
4037
|
+
"Properties",
|
|
4038
|
+
"BitSet",
|
|
4039
|
+
"Dictionary",
|
|
4040
|
+
"List",
|
|
4041
|
+
"Map",
|
|
4042
|
+
"Set",
|
|
4043
|
+
"Collection",
|
|
4044
|
+
"Iterable",
|
|
4045
|
+
// java.io / java.nio
|
|
4046
|
+
"File",
|
|
4047
|
+
"Files",
|
|
4048
|
+
"Path",
|
|
4049
|
+
"Paths",
|
|
4050
|
+
"InputStream",
|
|
4051
|
+
"OutputStream",
|
|
4052
|
+
"Reader",
|
|
4053
|
+
"Writer",
|
|
4054
|
+
"BufferedReader",
|
|
4055
|
+
"BufferedWriter",
|
|
4056
|
+
"FileInputStream",
|
|
4057
|
+
"FileOutputStream",
|
|
4058
|
+
"FileReader",
|
|
4059
|
+
"FileWriter",
|
|
4060
|
+
"ByteArrayInputStream",
|
|
4061
|
+
"ByteArrayOutputStream",
|
|
4062
|
+
"StringReader",
|
|
4063
|
+
"StringWriter",
|
|
4064
|
+
"PrintWriter",
|
|
4065
|
+
"PrintStream",
|
|
4066
|
+
"IOException",
|
|
4067
|
+
"FileNotFoundException",
|
|
4068
|
+
"EOFException",
|
|
4069
|
+
// java.net / java.sql / javax
|
|
4070
|
+
"URL",
|
|
4071
|
+
"URI",
|
|
4072
|
+
"HttpURLConnection",
|
|
4073
|
+
"URLEncoder",
|
|
4074
|
+
"URLDecoder",
|
|
4075
|
+
"Connection",
|
|
4076
|
+
"DriverManager",
|
|
4077
|
+
"ResultSet",
|
|
4078
|
+
"Statement",
|
|
4079
|
+
"PreparedStatement",
|
|
4080
|
+
"SQLException",
|
|
4081
|
+
"DataSource",
|
|
4082
|
+
"javax",
|
|
4083
|
+
"jakarta",
|
|
4084
|
+
// kotlin stdlib
|
|
4085
|
+
"kotlin",
|
|
4086
|
+
"Unit",
|
|
4087
|
+
"Nothing",
|
|
4088
|
+
"Any",
|
|
4089
|
+
"Int",
|
|
4090
|
+
"String",
|
|
4091
|
+
// Common JDK prefixes
|
|
4092
|
+
"java",
|
|
4093
|
+
"jdk",
|
|
4094
|
+
"sun",
|
|
4095
|
+
// Logger frameworks (always stdlib when called by these names)
|
|
4096
|
+
"Logger",
|
|
4097
|
+
"LoggerFactory",
|
|
4098
|
+
"LogManager",
|
|
4099
|
+
"Level",
|
|
4100
|
+
"LogRecord",
|
|
4101
|
+
"Marker",
|
|
4102
|
+
"MDC",
|
|
4103
|
+
"NDC",
|
|
4104
|
+
// Common annotations (never architectural)
|
|
4105
|
+
"Override",
|
|
4106
|
+
"Deprecated",
|
|
4107
|
+
"SuppressWarnings",
|
|
4108
|
+
"SafeVarargs",
|
|
4109
|
+
"FunctionalInterface",
|
|
4110
|
+
"Nullable",
|
|
4111
|
+
"NotNull",
|
|
4112
|
+
"NonNls",
|
|
4113
|
+
"Test",
|
|
4114
|
+
"Before",
|
|
4115
|
+
"After",
|
|
4116
|
+
"BeforeClass",
|
|
4117
|
+
"AfterClass",
|
|
4118
|
+
"Ignore",
|
|
4119
|
+
"RunWith",
|
|
4120
|
+
"Suite",
|
|
4121
|
+
"Parameterized",
|
|
4122
|
+
// org.slf4j / logging
|
|
4123
|
+
"slf4j",
|
|
4124
|
+
"log4j",
|
|
4125
|
+
"logback"
|
|
4126
|
+
]);
|
|
4127
|
+
const segments = call.split(".");
|
|
4128
|
+
if (segments.length > 0 && STDLIB_ROOTS.has(segments[0])) return true;
|
|
4129
|
+
if (STDLIB_ROOTS.has(call)) return true;
|
|
4130
|
+
return false;
|
|
4131
|
+
}
|
|
4132
|
+
/**
|
|
4133
|
+
* M14 incoming — the external-target marker, and only for the PROVABLE cases.
|
|
4134
|
+
*
|
|
4135
|
+
* `script/m14/incoming-gate.ts` counts an edge as resolved (one declaration), ambiguous
|
|
4136
|
+
* (several), or unresolved. A JDK symbol is a fourth thing — legitimately outside the
|
|
4137
|
+
* corpus — and an unmarked edge of that kind is indistinguishable from a broken one, so it
|
|
4138
|
+
* counts against the lane. Measured before this marker existed on java (200 files,
|
|
4139
|
+
* `npx tsx script/m14/incoming-gate.ts java java 200 --json`): **16,021 unresolved of 26,095
|
|
4140
|
+
* edges, `markedExternal 0` on EVERY type**, and `imports` 2,716 of 2,716 unresolved.
|
|
4141
|
+
*
|
|
4142
|
+
* Only two facts are provable from a single file, so only two are asserted:
|
|
4143
|
+
* - the target carries a JVM-platform / Kotlin-stdlib qualifier (`java.`, `javax.`,
|
|
4144
|
+
* `jakarta.`, `jdk.`, `sun.`, `com.sun.`, `kotlin.`, `kotlinx.`);
|
|
4145
|
+
* - the target's ROOT segment is a local name the file IMPORTS from such a module, and the
|
|
4146
|
+
* file does not declare that name itself.
|
|
4147
|
+
*
|
|
4148
|
+
* Deliberately NOT marked, because the prefix proves nothing about a single file:
|
|
4149
|
+
* - `org.` / `com.` third-party packages — in a multi-module build `com.acme` IS project
|
|
4150
|
+
* source, the same reason `com.sun` and `com.mycompany` were separated in `isJavaBuiltin`;
|
|
4151
|
+
* - wildcard imports (`import java.util.*`) — the file never names the type;
|
|
4152
|
+
* - java.lang simple names used with no import (`String`, `Object`, `System`). They are
|
|
4153
|
+
* *probably* the platform types, and "probably" is what this marker must not say: an
|
|
4154
|
+
* unproven marker hides a genuine unresolved edge behind a claim the file cannot make.
|
|
4155
|
+
* They stay counted against the lane until a rule can prove them.
|
|
4156
|
+
*/
|
|
4157
|
+
markExternalTargets(relationships, symbols, imports, context) {
|
|
4158
|
+
const JVM_ROOTS = ["java.", "javax.", "jakarta.", "jdk.", "sun.", "com.sun."];
|
|
4159
|
+
const isJvmModule = (name) => JVM_ROOTS.some((r) => name.startsWith(r));
|
|
4160
|
+
const declared = /* @__PURE__ */ new Set();
|
|
4161
|
+
for (const s of symbols) {
|
|
4162
|
+
const n = String(s?.name ?? "");
|
|
4163
|
+
if (n.length > 0) declared.add(n);
|
|
4164
|
+
const q = String(s?.qualifiedName ?? "");
|
|
4165
|
+
if (q.length > 0) declared.add(q);
|
|
4166
|
+
}
|
|
4167
|
+
const importedFromJvm = /* @__PURE__ */ new Map();
|
|
4168
|
+
for (const im of imports) {
|
|
4169
|
+
const module = String(im?.module ?? "");
|
|
4170
|
+
if (module.length === 0 || !isJvmModule(module)) continue;
|
|
4171
|
+
if (im?.isNamespace === true || im?.metadata?.isWildcard === true || module.endsWith(".*")) continue;
|
|
4172
|
+
const items = Array.isArray(im?.items) ? im.items : [];
|
|
4173
|
+
let named = false;
|
|
4174
|
+
for (const it of items) {
|
|
4175
|
+
const n = String(it?.name ?? it?.alias ?? "");
|
|
4176
|
+
if (n.length > 0) {
|
|
4177
|
+
importedFromJvm.set(n, module);
|
|
4178
|
+
named = true;
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
if (!named) {
|
|
4182
|
+
const leaf = module.slice(module.lastIndexOf(".") + 1);
|
|
4183
|
+
if (leaf.length > 0 && leaf !== "*") importedFromJvm.set(leaf, module);
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
let marked = 0;
|
|
4187
|
+
for (const r of relationships) {
|
|
4188
|
+
const target = String(r?.target ?? "");
|
|
4189
|
+
if (target.length === 0) continue;
|
|
4190
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
4191
|
+
if (md.isExternalTarget === true) continue;
|
|
4192
|
+
if (declared.has(target)) continue;
|
|
4193
|
+
if (String(r?.type ?? "") === "references" && md.relationshipType === "import_declaration_mention" && isJvmModule(target) && !declared.has(target)) {
|
|
4194
|
+
md.languageNamespace = [target];
|
|
4195
|
+
marked++;
|
|
4196
|
+
continue;
|
|
4197
|
+
}
|
|
4198
|
+
const CLAIMED_KINDS = /* @__PURE__ */ new Set(["references", "extends", "implements", "instantiates"]);
|
|
4199
|
+
if (JAVA_LANG_TYPES.has(target) && !importedFromJvm.has(target) && CLAIMED_KINDS.has(String(r?.type ?? ""))) {
|
|
4200
|
+
md.languageNamespace = [target];
|
|
4201
|
+
marked++;
|
|
4202
|
+
continue;
|
|
4203
|
+
}
|
|
4204
|
+
const colon = target.indexOf(":");
|
|
4205
|
+
if (colon > 0 && !target.startsWith("/") && !target.includes("/")) {
|
|
4206
|
+
const module2 = target.slice(0, colon);
|
|
4207
|
+
if (isJvmModule(module2)) {
|
|
4208
|
+
md.isExternalTarget = true;
|
|
4209
|
+
md.externalReason = "builtin-module";
|
|
4210
|
+
md.externalSpecifier = module2;
|
|
4211
|
+
marked++;
|
|
4212
|
+
}
|
|
4213
|
+
continue;
|
|
4214
|
+
}
|
|
4215
|
+
if (isJvmModule(target)) {
|
|
4216
|
+
md.isExternalTarget = true;
|
|
4217
|
+
md.externalReason = "builtin-lib-type";
|
|
4218
|
+
md.externalSpecifier = JVM_ROOTS.find((p) => target.startsWith(p)) ?? "";
|
|
4219
|
+
marked++;
|
|
4220
|
+
continue;
|
|
4221
|
+
}
|
|
4222
|
+
const root = target.split(".")[0];
|
|
4223
|
+
if (root.length === 0 || declared.has(root)) continue;
|
|
4224
|
+
const module = importedFromJvm.get(root);
|
|
4225
|
+
if (module !== void 0) {
|
|
4226
|
+
md.isExternalTarget = true;
|
|
4227
|
+
md.externalReason = "builtin-lib-type";
|
|
4228
|
+
md.externalReceiver = root;
|
|
4229
|
+
md.externalSpecifier = module;
|
|
4230
|
+
marked++;
|
|
4231
|
+
continue;
|
|
4232
|
+
}
|
|
4233
|
+
if (md.isExternalTarget === true) continue;
|
|
4234
|
+
if (context?.projectIndex?.complete !== true) continue;
|
|
4235
|
+
if (String(r?.type ?? "") === "imports" || String(r?.type ?? "") === "brain_import") continue;
|
|
4236
|
+
if (!isPlainJavaIdentifier(target)) continue;
|
|
4237
|
+
if (JAVA_RESERVED.has(target)) continue;
|
|
4238
|
+
if (context.projectIndex.symbolNames.has(target)) continue;
|
|
4239
|
+
md.isExternalTarget = true;
|
|
4240
|
+
md.externalReason = "project-scope-bare-name";
|
|
4241
|
+
md.externalProof = `ProjectIndex.complete === true; symbolNames has no "${target}"`;
|
|
4242
|
+
marked++;
|
|
4243
|
+
}
|
|
4244
|
+
return marked;
|
|
4245
|
+
}
|
|
4246
|
+
getAnnotationValue(node, annotationName, key) {
|
|
4247
|
+
const findAllAnnotations = (n) => {
|
|
4248
|
+
const results = [];
|
|
4249
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
4250
|
+
const child = n.namedChild(i);
|
|
4251
|
+
if (child && (child.type === "annotation" || child.type === "marker_annotation")) results.push(child);
|
|
4252
|
+
}
|
|
4253
|
+
return results;
|
|
4254
|
+
};
|
|
4255
|
+
let annotations = findAllAnnotations(node);
|
|
4256
|
+
const modifiersNode = (() => {
|
|
4257
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4258
|
+
const child = node.namedChild(i);
|
|
4259
|
+
if (child && child.type === "modifiers") return child;
|
|
4260
|
+
}
|
|
4261
|
+
return null;
|
|
4262
|
+
})();
|
|
4263
|
+
if (modifiersNode) {
|
|
4264
|
+
annotations = annotations.concat(findAllAnnotations(modifiersNode));
|
|
4265
|
+
}
|
|
4266
|
+
for (let j = 0; j < annotations.length; j++) {
|
|
4267
|
+
const ann = annotations[j];
|
|
4268
|
+
const name = this.getJavaAnnotationShortName(ann);
|
|
4269
|
+
if (name && name === annotationName) {
|
|
4270
|
+
const argsNode = this.findChildByType(ann, "annotation_argument_list");
|
|
4271
|
+
if (!argsNode) return void 0;
|
|
4272
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
4273
|
+
const arg = argsNode.namedChild(i);
|
|
4274
|
+
if (!arg) continue;
|
|
4275
|
+
if (arg.type === "element_value_pair") {
|
|
4276
|
+
const keyNode = this.findChildByType(arg, "identifier");
|
|
4277
|
+
if (keyNode && keyNode.text === key) {
|
|
4278
|
+
const valNode = arg.namedChild(arg.namedChildCount - 1);
|
|
4279
|
+
return this.extractStringValue(valNode);
|
|
4280
|
+
}
|
|
4281
|
+
} else if (key === "value" && arg.type === "element_value_array_initializer") {
|
|
4282
|
+
const first = arg.namedChild(0);
|
|
4283
|
+
if (first) return this.extractStringValue(first);
|
|
4284
|
+
return void 0;
|
|
4285
|
+
} else if (key === "value" && arg.type !== "element_value_pair") {
|
|
4286
|
+
return this.extractStringValue(arg);
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
return void 0;
|
|
4292
|
+
}
|
|
4293
|
+
getJavaGenericParameters(node) {
|
|
4294
|
+
const typeParamsNode = this.findChildByType(node, "type_parameters");
|
|
4295
|
+
if (!typeParamsNode) return void 0;
|
|
4296
|
+
const params = [];
|
|
4297
|
+
for (let i = 0; i < typeParamsNode.namedChildCount; i++) {
|
|
4298
|
+
const child = typeParamsNode.namedChild(i);
|
|
4299
|
+
if (child && child.type === "type_parameter") {
|
|
4300
|
+
const nameChild = child.childForFieldName("name") || this.findChildByType(child, "type_identifier");
|
|
4301
|
+
params.push(nameChild ? nameChild.text : child.text);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
return params.length > 0 ? params : void 0;
|
|
4305
|
+
}
|
|
4306
|
+
/**
|
|
4307
|
+
* JLS 7.7 — a `module-info.java` compilation unit. The ONE implementation.
|
|
4308
|
+
*
|
|
4309
|
+
* WHAT IS EMITTED, PER DIRECTIVE SHAPE, and every row below is pinned by a
|
|
4310
|
+
* fixture (three of them are INCOMING_JAVA fixtures written for this file):
|
|
4311
|
+
*
|
|
4312
|
+
* `module M { … }` a `Module` symbol. It carries no `ownerName`, so
|
|
4313
|
+
* the existing post-pass also lists it in `exports[]`
|
|
4314
|
+
* — correct, because `requires M` is how another
|
|
4315
|
+
* module names it (java/module-exports-jpms).
|
|
4316
|
+
* `exports P [to M…];` a `Namespace` symbol for P owned by M, an `exports`
|
|
4317
|
+
* edge M -> P (`module_exports`, with the `to` list in
|
|
4318
|
+
* `qualifiedTo`), a `references` MENTION of P
|
|
4319
|
+
* (`module_exports_mention`), and a `references` row
|
|
4320
|
+
* per `to` module (`module_exports_package_to`).
|
|
4321
|
+
* `requires [t|s] M2;` an `imports` edge M -> M2 (`module_requires`), a
|
|
4322
|
+
* `references` mention (`module_requires_mention`),
|
|
4323
|
+
* and an `imports[]` entry (ImportInfo) — the channel
|
|
4324
|
+
* `brain_import` reads.
|
|
4325
|
+
* `opens`/`uses`/`provides` one `references` row per named target, labelled
|
|
4326
|
+
* from JAVA_MODULE_DIRECTIVE_FIELDS.
|
|
4327
|
+
*
|
|
4328
|
+
* BOTH LABELS PER DIRECTIVE is the family rule this lane already states for
|
|
4329
|
+
* `import` declarations at the analyzer's import site ("`imports` is the
|
|
4330
|
+
* DEPENDENCY … and `references` is the MENTION"), and erlang's analyzer states
|
|
4331
|
+
* it for its own `-import`. java/jpms-module-exports pins it row by row; the
|
|
4332
|
+
* `mentions` are what the reference-resolution surface joins against, the
|
|
4333
|
+
* dependencies are what the module-dependency pass reads.
|
|
4334
|
+
*
|
|
4335
|
+
* WHY `requires` IS AN `imports` EDGE AND `exports` AN `exports` EDGE. JLS 7.7.1
|
|
4336
|
+
* makes `requires M` a compile-time MODULE DEPENDENCY, which is the same fact an
|
|
4337
|
+
* `import` states, so it takes the same kind — the two halves of JPMS must not
|
|
4338
|
+
* share a label or "what does this file depend on" becomes unanswerable
|
|
4339
|
+
* (java/jpms-module-exports forbids an `imports` row for an exported package,
|
|
4340
|
+
* and an `exports` row for a required module). JLS 7.7.2 makes `exports P`
|
|
4341
|
+
* "this unit makes P visible to importers", which is exactly the `exports`
|
|
4342
|
+
* kind's claim, and java/module-info + java/jpms-module-exports pin the row.
|
|
4343
|
+
*
|
|
4344
|
+
* `opens` grants deep REFLECTIVE access and is not an export in any sense the
|
|
4345
|
+
* `exports` kind asserts, so no `exports` row is emitted for it (pinned by
|
|
4346
|
+
* java/jpms-module-exports' forbid); it keeps its `references` row. The `to`
|
|
4347
|
+
* clause RESTRICTS who may read P, so an `exports` edge from M to a `to` target
|
|
4348
|
+
* would assert the opposite of what the clause says: it stays a mention plus
|
|
4349
|
+
* `qualifiedTo` metadata.
|
|
4350
|
+
*
|
|
4351
|
+
* `requires static` is an OPTIONAL dependency at compile time and REQUIRED at
|
|
4352
|
+
* run time, which is a different claim from a plain `requires`, so it is
|
|
4353
|
+
* published at 0.9 with `inferred: true` and
|
|
4354
|
+
* `reasonCode: "optional-module-dependency"` (java/module-info pins
|
|
4355
|
+
* confidenceMin AND confidenceMax at 0.9 for exactly that row) rather than
|
|
4356
|
+
* being spelled as a second label.
|
|
4357
|
+
*
|
|
4358
|
+
* `provides`/`uses` rows are the two remaining directive shapes in the grammar.
|
|
4359
|
+
* This corpus contains 0 of both (probe: 0 `opens_module_directive`, 0
|
|
4360
|
+
* `provides_module_directive`, 0 `uses_module_directive` over 3,275 files), so
|
|
4361
|
+
* their labels are pinned by fixtures rather than by corpus frequency and are
|
|
4362
|
+
* recorded as such.
|
|
4363
|
+
*
|
|
4364
|
+
* Node shapes are probe-verified against the pinned wasm, not assumed
|
|
4365
|
+
* (java-node-shape-probe.mts):
|
|
4366
|
+
* module_declaration field `name` -> scoped_identifier | identifier,
|
|
4367
|
+
* field `body` -> module_body
|
|
4368
|
+
* exports_module_directive FIRST named child is the package; any `to`
|
|
4369
|
+
* clause follows the anonymous `to` token
|
|
4370
|
+
* requires_module_directive first named child is the module name, UNLESS a
|
|
4371
|
+
* `requires_modifier` (`transitive`/`static`)
|
|
4372
|
+
* comes first — so the modifier is skipped by node
|
|
4373
|
+
* type, never by position
|
|
4374
|
+
*/
|
|
4375
|
+
emitJavaModuleDeclaration(declNode, symbols, relationships, imports) {
|
|
4376
|
+
const nameNode = declNode.childForFieldName("name") || this.findChildByType(declNode, "scoped_identifier") || this.findChildByType(declNode, "identifier");
|
|
4377
|
+
if (!nameNode) return;
|
|
4378
|
+
const moduleName = nameNode.text;
|
|
4379
|
+
if (!moduleName) return;
|
|
4380
|
+
symbols.push({
|
|
4381
|
+
name: moduleName,
|
|
4382
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
4383
|
+
range: this.nodeToRange(nameNode),
|
|
4384
|
+
visibility: "public",
|
|
4385
|
+
comments: [],
|
|
4386
|
+
metadata: { nodeType: "module_declaration", isJavaModule: true }
|
|
4387
|
+
});
|
|
4388
|
+
const body = declNode.childForFieldName("body") || this.findChildByType(declNode, "module_body");
|
|
4389
|
+
if (!body) return;
|
|
4390
|
+
const emittedPackages = /* @__PURE__ */ new Set();
|
|
4391
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
4392
|
+
const n = body.namedChild(i);
|
|
4393
|
+
if (!n) continue;
|
|
4394
|
+
if (n.type === "exports_module_directive") {
|
|
4395
|
+
const pkgNode = this.findChildByType(n, "scoped_identifier") || this.findChildByType(n, "identifier");
|
|
4396
|
+
if (pkgNode && pkgNode.text && !emittedPackages.has(pkgNode.text)) {
|
|
4397
|
+
const pkgName = pkgNode.text;
|
|
4398
|
+
emittedPackages.add(pkgName);
|
|
4399
|
+
const qualifiedTo = this.readJavaQualifiedExportTargets(n);
|
|
4400
|
+
symbols.push({
|
|
4401
|
+
name: pkgName,
|
|
4402
|
+
kind: ASTEngine.SymbolKind.Namespace,
|
|
4403
|
+
// The package is named on the `exports` line, not on `module`.
|
|
4404
|
+
range: this.nodeToRange(pkgNode),
|
|
4405
|
+
visibility: "public",
|
|
4406
|
+
ownerName: moduleName,
|
|
4407
|
+
ownerKind: ASTEngine.SymbolKind.Module,
|
|
4408
|
+
metadata: { nodeType: "exports_module_directive", isExportedPackage: true }
|
|
4409
|
+
});
|
|
4410
|
+
relationships.push({
|
|
4411
|
+
source: moduleName,
|
|
4412
|
+
target: pkgName,
|
|
4413
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
4414
|
+
// JLS 7.7.2 is structural syntax: the directive is written, so this
|
|
4415
|
+
// is a parse fact and not a heuristic.
|
|
4416
|
+
confidence: 1,
|
|
4417
|
+
// The line the PACKAGE is named on — the directive's own name node.
|
|
4418
|
+
line_number: pkgNode.startPosition.row + 1,
|
|
4419
|
+
metadata: {
|
|
4420
|
+
relationshipType: "module_exports",
|
|
4421
|
+
isModuleDirective: true,
|
|
4422
|
+
isJavaModuleDirective: true,
|
|
4423
|
+
moduleName,
|
|
4424
|
+
...qualifiedTo ? { qualifiedTo } : {}
|
|
4425
|
+
}
|
|
4426
|
+
});
|
|
4427
|
+
relationships.push({
|
|
4428
|
+
source: moduleName,
|
|
4429
|
+
target: pkgName,
|
|
4430
|
+
type: ASTEngine.RelationshipType.References,
|
|
4431
|
+
confidence: 1,
|
|
4432
|
+
line_number: pkgNode.startPosition.row + 1,
|
|
4433
|
+
metadata: {
|
|
4434
|
+
relationshipType: "module_exports_mention",
|
|
4435
|
+
isModuleDirective: true,
|
|
4436
|
+
isJavaModuleDirective: true,
|
|
4437
|
+
reasonCode: "module_directive",
|
|
4438
|
+
moduleName
|
|
4439
|
+
}
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
} else if (n.type === "requires_module_directive") {
|
|
4443
|
+
let modNode = null;
|
|
4444
|
+
let transitive = false;
|
|
4445
|
+
let isStatic = false;
|
|
4446
|
+
for (let j = 0; j < n.namedChildCount; j++) {
|
|
4447
|
+
const c = n.namedChild(j);
|
|
4448
|
+
if (!c) continue;
|
|
4449
|
+
if (c.type === "requires_modifier") {
|
|
4450
|
+
if (c.text.trim() === "transitive") transitive = true;
|
|
4451
|
+
else if (c.text.trim() === "static") isStatic = true;
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
if (isJavaNameNode(c)) {
|
|
4455
|
+
modNode = c;
|
|
4456
|
+
break;
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
const required = modNode?.text ?? "";
|
|
4460
|
+
if (!required) continue;
|
|
4461
|
+
const line = modNode.startPosition.row + 1;
|
|
4462
|
+
const shared = {
|
|
4463
|
+
isModuleDirective: true,
|
|
4464
|
+
isJavaModuleDirective: true,
|
|
4465
|
+
moduleName,
|
|
4466
|
+
isModuleRequires: true
|
|
4467
|
+
};
|
|
4468
|
+
relationships.push({
|
|
4469
|
+
source: moduleName,
|
|
4470
|
+
target: required,
|
|
4471
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
4472
|
+
// 0.9 for `requires static`: the module is OPTIONAL at compile time and
|
|
4473
|
+
// required at run time, so the dependency claim is discounted and the
|
|
4474
|
+
// reason is machine-readable rather than silent (honesty contract §4).
|
|
4475
|
+
confidence: isStatic ? 0.9 : 1,
|
|
4476
|
+
line_number: line,
|
|
4477
|
+
metadata: {
|
|
4478
|
+
relationshipType: "module_requires",
|
|
4479
|
+
...shared,
|
|
4480
|
+
isStatic,
|
|
4481
|
+
isTransitive: transitive,
|
|
4482
|
+
fileLevel: true,
|
|
4483
|
+
...isStatic ? { inferred: true, reasonCode: "optional-module-dependency" } : { reasonCode: "module_directive" }
|
|
4484
|
+
}
|
|
4485
|
+
});
|
|
4486
|
+
relationships.push({
|
|
4487
|
+
source: moduleName,
|
|
4488
|
+
target: required,
|
|
4489
|
+
type: ASTEngine.RelationshipType.References,
|
|
4490
|
+
confidence: 1,
|
|
4491
|
+
line_number: line,
|
|
4492
|
+
metadata: {
|
|
4493
|
+
relationshipType: "module_requires_mention",
|
|
4494
|
+
...shared,
|
|
4495
|
+
reasonCode: "module_directive"
|
|
4496
|
+
}
|
|
4497
|
+
});
|
|
4498
|
+
imports.push({
|
|
4499
|
+
module: required,
|
|
4500
|
+
items: [],
|
|
4501
|
+
isDefault: false,
|
|
4502
|
+
range: this.nodeToRange(modNode),
|
|
4503
|
+
metadata: {
|
|
4504
|
+
isModuleRequires: true,
|
|
4505
|
+
...transitive ? { transitive: true } : {},
|
|
4506
|
+
...isStatic ? { isStatic: true } : {}
|
|
4507
|
+
},
|
|
4508
|
+
importType: transitive ? "java-module-requires-transitive" : "java-module-requires"
|
|
4509
|
+
});
|
|
4510
|
+
continue;
|
|
4511
|
+
}
|
|
4512
|
+
const shape = JAVA_MODULE_DIRECTIVE_FIELDS[n.type];
|
|
4513
|
+
if (!shape) continue;
|
|
4514
|
+
for (let k = 0; k < shape.length; k++) {
|
|
4515
|
+
const { field, role, unfieldedAfter } = shape[k];
|
|
4516
|
+
if (role === "module_exports_package") continue;
|
|
4517
|
+
const targets = n.childrenForFieldName?.(field) ?? [];
|
|
4518
|
+
if (unfieldedAfter) {
|
|
4519
|
+
const anchor = n.childForFieldName(unfieldedAfter);
|
|
4520
|
+
for (let j = 0; j < n.namedChildCount; j++) {
|
|
4521
|
+
const c = n.namedChild(j);
|
|
4522
|
+
if (isJavaNameNode(c) && c.startIndex !== anchor?.startIndex && c.endIndex !== anchor?.endIndex) {
|
|
4523
|
+
targets.push(c);
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
for (let j = 0; j < targets.length; j++) {
|
|
4528
|
+
const t = targets[j];
|
|
4529
|
+
if (t) this.pushModuleDirective(relationships, moduleName, t, role);
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
/** The `to M1, M2` target list of a qualified export, or null when unqualified. */
|
|
4535
|
+
readJavaQualifiedExportTargets(directive) {
|
|
4536
|
+
const out = [];
|
|
4537
|
+
let seenPackage = false;
|
|
4538
|
+
for (let i = 0; i < directive.namedChildCount; i++) {
|
|
4539
|
+
const c = directive.namedChild(i);
|
|
4540
|
+
if (!c) continue;
|
|
4541
|
+
const isName = c.type === "scoped_identifier" || c.type === "identifier";
|
|
4542
|
+
if (!isName) continue;
|
|
4543
|
+
if (!seenPackage) {
|
|
4544
|
+
seenPackage = true;
|
|
4545
|
+
continue;
|
|
4546
|
+
}
|
|
4547
|
+
out.push(c.text);
|
|
4548
|
+
}
|
|
4549
|
+
return out.length > 0 ? out : null;
|
|
4550
|
+
}
|
|
4551
|
+
/**
|
|
4552
|
+
* JLS 4.4 — publish each DECLARED type variable as a `SymbolKind.Type` symbol.
|
|
4553
|
+
*
|
|
4554
|
+
* WHY. The analyzer already read every one of them: `getJavaGenericParameters`
|
|
4555
|
+
* above put `["K","V"]` on the class symbol's `genericParameters`, and the
|
|
4556
|
+
* declared-type walk emitted a `references` edge to each string it met in a
|
|
4557
|
+
* field type, parameter type, return type, cast or `throws` clause. So the
|
|
4558
|
+
* GRAPH already asserted that `Box` references `K` — and no symbol named `K`
|
|
4559
|
+
* existed anywhere, so the assertion had nothing to bind to. The rule-R7 /
|
|
4560
|
+
* endpoint question ("do both ends resolve to a real symbol?") could not be
|
|
4561
|
+
* answered for those edges at any rate above zero, and the honest cause was
|
|
4562
|
+
* not cross-file reach: the declaration was in the SAME FILE and was simply
|
|
4563
|
+
* never published.
|
|
4564
|
+
*
|
|
4565
|
+
* MEASURED, not assumed (instrument
|
|
4566
|
+
* `validation/m14/measurements/java/java-construct-probe.mts`, whole corpus,
|
|
4567
|
+
* 3,275 `.java` files, all parsed, 0 failed): 9,780 `type_parameter` nodes
|
|
4568
|
+
* over 1,543 files (47.1% of the corpus), 55 distinct declared variable
|
|
4569
|
+
* names. On the 250-file store slice this produced 1,588 `references` rows
|
|
4570
|
+
* naming a declaration that existed nowhere in the index (`V` 815, `K` 773).
|
|
4571
|
+
*
|
|
4572
|
+
* WHY `Type` AND NOT SOMETHING ELSE. `SymbolKind.Type = "type"` is declared
|
|
4573
|
+
* at `src/ast/types.ts:18`, `CONTAINER_TYPES_LOOKUP` in parallel-indexer
|
|
4574
|
+
* already treats `type` as a container kind, and this analyzer's own
|
|
4575
|
+
* evidence classifier at `resolveEnclosingTypeScope` already tests for it.
|
|
4576
|
+
*
|
|
4577
|
+
* THE CONTAINMENT EDGE IS NOT INVENTED HERE. The symbol carries `ownerName`,
|
|
4578
|
+
* so the existing post-pass (`for (const s of symbols) { const _owner =
|
|
4579
|
+
* s.ownerName || s.scope; … }`) emits `owner -> variable` at confidence 1.0
|
|
4580
|
+
* with the variable's own declaration line — one code path for every member,
|
|
4581
|
+
* not a second spelling of containment.
|
|
4582
|
+
*
|
|
4583
|
+
* WHY THE NAME STAYS BARE. A `references -> V` edge names `V`. Publishing
|
|
4584
|
+
* the symbol as `Box.V` would leave every one of those edges unresolved
|
|
4585
|
+
* again, which is the defect being fixed. The cost is honest and recorded:
|
|
4586
|
+
* a class type variable and a method type variable that share a spelling in
|
|
4587
|
+
* one file (`class A<T> { <T> T m(T t) {} }`) are two symbols with one name,
|
|
4588
|
+
* and the store's name resolution picks one. That is a genuine ambiguity, not
|
|
4589
|
+
* a fabrication — both declarations are real, and JLS 6.3 scopes both to the
|
|
4590
|
+
* body that names them.
|
|
4591
|
+
*
|
|
4592
|
+
* The `contains` post-pass skips nothing here: `ownerName` is set, so these
|
|
4593
|
+
* do NOT reach the `exports[]` channel (a type variable is not an importable
|
|
4594
|
+
* top-level unit — `requires Box.V` is not a thing any module can write).
|
|
4595
|
+
*/
|
|
4596
|
+
emitJavaTypeParameterSymbols(declNode, ownerName, ownerKind, declaredOn, symbols) {
|
|
4597
|
+
const typeParamsNode = this.findChildByType(declNode, "type_parameters");
|
|
4598
|
+
if (!typeParamsNode) return;
|
|
4599
|
+
for (let i = 0; i < typeParamsNode.namedChildCount; i++) {
|
|
4600
|
+
const tp = typeParamsNode.namedChild(i);
|
|
4601
|
+
if (!tp || tp.type !== "type_parameter") continue;
|
|
4602
|
+
const nameNode = tp.childForFieldName("name") || this.findChildByType(tp, "type_identifier");
|
|
4603
|
+
if (!nameNode) continue;
|
|
4604
|
+
const tpName = nameNode.text;
|
|
4605
|
+
if (!tpName) continue;
|
|
4606
|
+
const bound = this.findChildByType(tp, "type_bound");
|
|
4607
|
+
symbols.push({
|
|
4608
|
+
name: tpName,
|
|
4609
|
+
kind: ASTEngine.SymbolKind.Type,
|
|
4610
|
+
// The VARIABLE's own line, so the symbol and the `contains` edge that
|
|
4611
|
+
// names it cannot disagree (M59 #825's rule, applied to the position).
|
|
4612
|
+
range: this.nodeToRange(nameNode),
|
|
4613
|
+
ownerName,
|
|
4614
|
+
ownerKind,
|
|
4615
|
+
metadata: {
|
|
4616
|
+
nodeType: "type_parameter",
|
|
4617
|
+
isTypeParameter: true,
|
|
4618
|
+
declaredOn,
|
|
4619
|
+
declarationOrder: i,
|
|
4620
|
+
...bound ? { bound: bound.text } : {}
|
|
4621
|
+
}
|
|
4622
|
+
});
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
/**
|
|
4626
|
+
* Exact short-name annotation test on a symbol's decorators. Decorator text
|
|
4627
|
+
* is raw source ("@Service", "@org.x.Service", "@Service(...)") — normalize
|
|
4628
|
+
* to the short name before comparing so fully-qualified and parameterized
|
|
4629
|
+
* annotations match exactly like bare ones.
|
|
4630
|
+
*/
|
|
4631
|
+
hasJavaAnnotation(symbol, annotationName) {
|
|
4632
|
+
if (!symbol.decorators) return false;
|
|
4633
|
+
for (let i = 0; i < symbol.decorators.length; i++) {
|
|
4634
|
+
let t = symbol.decorators[i];
|
|
4635
|
+
if (t.startsWith("@")) t = t.slice(1);
|
|
4636
|
+
const paren = t.indexOf("(");
|
|
4637
|
+
if (paren !== -1) t = t.slice(0, paren);
|
|
4638
|
+
const dot = t.lastIndexOf(".");
|
|
4639
|
+
if (dot !== -1) t = t.slice(dot + 1);
|
|
4640
|
+
if (t === annotationName) return true;
|
|
4641
|
+
}
|
|
4642
|
+
return false;
|
|
4643
|
+
}
|
|
4644
|
+
getJavaFrameworkPatterns(frameworks, symbols) {
|
|
4645
|
+
const patterns = [];
|
|
4646
|
+
if (inArray(frameworks, "spring")) {
|
|
4647
|
+
const springPatterns = [];
|
|
4648
|
+
let hasService = false, hasRepo = false, hasController = false;
|
|
4649
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
4650
|
+
if (!hasService && this.hasJavaAnnotation(symbols[i], "Service")) hasService = true;
|
|
4651
|
+
if (!hasRepo && this.hasJavaAnnotation(symbols[i], "Repository")) hasRepo = true;
|
|
4652
|
+
if (!hasController && (this.hasJavaAnnotation(symbols[i], "Controller") || this.hasJavaAnnotation(symbols[i], "RestController"))) hasController = true;
|
|
4653
|
+
}
|
|
4654
|
+
if (hasService) springPatterns.push("service_layer");
|
|
4655
|
+
if (hasRepo) springPatterns.push("repository_pattern");
|
|
4656
|
+
if (hasController) {
|
|
4657
|
+
springPatterns.push("mvc_pattern");
|
|
4658
|
+
springPatterns.push("api_endpoints");
|
|
4659
|
+
}
|
|
4660
|
+
if (springPatterns.length > 0) patterns.push({ framework: "Spring Framework", patterns: springPatterns });
|
|
4661
|
+
}
|
|
4662
|
+
if (inArray(frameworks, "spring_mvc")) {
|
|
4663
|
+
patterns.push({ framework: "Spring MVC", patterns: ["mvc_pattern", "api_endpoints"] });
|
|
4664
|
+
}
|
|
4665
|
+
if (inArray(frameworks, "spring_data")) {
|
|
4666
|
+
patterns.push({ framework: "Spring Data", patterns: ["repository_pattern", "orm_mapping"] });
|
|
4667
|
+
} else if (inArray(frameworks, "jpa")) {
|
|
4668
|
+
patterns.push({ framework: "JPA", patterns: ["orm_mapping"] });
|
|
4669
|
+
} else if (inArray(frameworks, "hibernate")) {
|
|
4670
|
+
patterns.push({ framework: "Hibernate", patterns: ["orm_mapping"] });
|
|
4671
|
+
}
|
|
4672
|
+
return patterns;
|
|
4673
|
+
}
|
|
4674
|
+
};
|
|
4675
|
+
export {
|
|
4676
|
+
RealJavaAnalyzer
|
|
4677
|
+
};
|