@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,2285 @@
|
|
|
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-groovy-analyzer.ts
|
|
17
|
+
var INTERPOLATION_OWNED_BY_MEMBER_WALK = /* @__PURE__ */ new Set([
|
|
18
|
+
"field_access",
|
|
19
|
+
"method_invocation",
|
|
20
|
+
"juxt_function_call",
|
|
21
|
+
"array_access",
|
|
22
|
+
"object_creation_expression"
|
|
23
|
+
]);
|
|
24
|
+
var GROOVY_BUILTINS = /* @__PURE__ */ new Set([
|
|
25
|
+
// Groovy language builtins
|
|
26
|
+
"println",
|
|
27
|
+
"print",
|
|
28
|
+
"printf",
|
|
29
|
+
"sprintf",
|
|
30
|
+
"assert",
|
|
31
|
+
"sleep",
|
|
32
|
+
"synchronized",
|
|
33
|
+
// JVM Object methods
|
|
34
|
+
"toString",
|
|
35
|
+
"hashCode",
|
|
36
|
+
"equals",
|
|
37
|
+
"getClass",
|
|
38
|
+
"notify",
|
|
39
|
+
"notifyAll",
|
|
40
|
+
"wait",
|
|
41
|
+
"clone",
|
|
42
|
+
"finalize",
|
|
43
|
+
// Common java.* entry points
|
|
44
|
+
"System.out.println",
|
|
45
|
+
"System.out.print",
|
|
46
|
+
"System.out.printf",
|
|
47
|
+
"System.err.println",
|
|
48
|
+
"System.err.print",
|
|
49
|
+
"System.exit",
|
|
50
|
+
"System.gc",
|
|
51
|
+
"System.currentTimeMillis",
|
|
52
|
+
"System.nanoTime",
|
|
53
|
+
"System.getProperty",
|
|
54
|
+
"System.getenv",
|
|
55
|
+
"System.identityHashCode",
|
|
56
|
+
"System.arraycopy",
|
|
57
|
+
"System.lineSeparator"
|
|
58
|
+
]);
|
|
59
|
+
var GROOVY_DECLARATION_TYPES = /* @__PURE__ */ new Set([
|
|
60
|
+
"method_declaration",
|
|
61
|
+
"constructor_declaration",
|
|
62
|
+
"function_definition",
|
|
63
|
+
"class_declaration",
|
|
64
|
+
"trait_declaration",
|
|
65
|
+
"enum_declaration"
|
|
66
|
+
]);
|
|
67
|
+
var TOP_LEVEL_TYPE_NODES = /* @__PURE__ */ new Set([
|
|
68
|
+
"class_declaration",
|
|
69
|
+
"interface_declaration",
|
|
70
|
+
"trait_declaration",
|
|
71
|
+
"enum_declaration",
|
|
72
|
+
"record_declaration",
|
|
73
|
+
"annotation_type_declaration"
|
|
74
|
+
]);
|
|
75
|
+
var RealGroovyAnalyzer = class _RealGroovyAnalyzer {
|
|
76
|
+
analyze(rootNode, content, filePath, context) {
|
|
77
|
+
const symbols = [];
|
|
78
|
+
const relationships = [];
|
|
79
|
+
const imports = [];
|
|
80
|
+
const exports = [];
|
|
81
|
+
const errors = [];
|
|
82
|
+
const symbolMap = /* @__PURE__ */ new Map();
|
|
83
|
+
const classHierarchy = /* @__PURE__ */ new Map();
|
|
84
|
+
const overrideTasks = [];
|
|
85
|
+
const scopeStack = [];
|
|
86
|
+
const stack = [{ node: rootNode }];
|
|
87
|
+
const walk = {
|
|
88
|
+
content,
|
|
89
|
+
symbols,
|
|
90
|
+
relationships,
|
|
91
|
+
imports,
|
|
92
|
+
symbolMap,
|
|
93
|
+
classHierarchy,
|
|
94
|
+
overrideTasks,
|
|
95
|
+
scopeStack,
|
|
96
|
+
parentContext: void 0,
|
|
97
|
+
newContext: void 0,
|
|
98
|
+
pushedScope: false,
|
|
99
|
+
currentPackage: ""
|
|
100
|
+
};
|
|
101
|
+
while (stack.length > 0) {
|
|
102
|
+
const item = stack.pop();
|
|
103
|
+
const { node, parentContext, isExit } = item;
|
|
104
|
+
if (isExit) {
|
|
105
|
+
if (parentContext) {
|
|
106
|
+
if (scopeStack.length > 0 && scopeStack[scopeStack.length - 1] === parentContext) {
|
|
107
|
+
scopeStack.pop();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (node.isMissing || node.hasError) {
|
|
113
|
+
if (node.type === "ERROR" || node.type === "MISSING") {
|
|
114
|
+
errors.push({
|
|
115
|
+
message: `Syntax ${node.type} detected.`,
|
|
116
|
+
range: this.nodeToRange(node),
|
|
117
|
+
severity: "warning"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
walk.parentContext = parentContext;
|
|
122
|
+
walk.newContext = parentContext;
|
|
123
|
+
walk.pushedScope = false;
|
|
124
|
+
const handler = this.nodeHandlers.get(node.type);
|
|
125
|
+
if (handler) handler(node, walk);
|
|
126
|
+
if (walk.pushedScope) {
|
|
127
|
+
stack.push({ node, parentContext: walk.newContext, isExit: true });
|
|
128
|
+
}
|
|
129
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
130
|
+
const child = node.namedChild(i);
|
|
131
|
+
if (child) {
|
|
132
|
+
stack.push({ node: child, parentContext: walk.newContext });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
this.recoverDegradedClassShapes(rootNode, symbols, relationships, symbolMap, walk.currentPackage);
|
|
137
|
+
this.resolveOverrides(overrideTasks, classHierarchy, symbolMap, relationships, context?.overrideCompanion);
|
|
138
|
+
const primaryType = symbols.find((s) => {
|
|
139
|
+
if (s.ownerName || s.scope) return false;
|
|
140
|
+
return s.kind === ASTEngine.SymbolKind.Class || s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Enum;
|
|
141
|
+
});
|
|
142
|
+
if (primaryType) {
|
|
143
|
+
for (const rel of relationships) {
|
|
144
|
+
const isImportMention = rel.type === ASTEngine.RelationshipType.References && rel.metadata?.isGroovyImport === true;
|
|
145
|
+
if (isImportMention && (rel.source === walk.currentPackage || rel.source === "(script)" || rel.source === "root")) {
|
|
146
|
+
rel.source = primaryType.name;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const s of symbols) {
|
|
151
|
+
const _owner = s.ownerName || s.scope;
|
|
152
|
+
if (_owner) {
|
|
153
|
+
relationships.push({
|
|
154
|
+
source: _owner,
|
|
155
|
+
target: s.name,
|
|
156
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
157
|
+
confidence: 1,
|
|
158
|
+
// M59 (#825): the member's DECLARATION line, and the same line the
|
|
159
|
+
// member's own range carries — a symbol and the edge naming it must
|
|
160
|
+
// never disagree (java/scala contract). Never 0 (#820/#821).
|
|
161
|
+
...s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
162
|
+
metadata: { isContained: true }
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this.collectFieldAccessUses(rootNode, relationships, symbols);
|
|
167
|
+
this.collectDeclaredTypeRefs(rootNode, relationships);
|
|
168
|
+
const fw = this.detectGroovyFrameworks(content, symbols, imports);
|
|
169
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
170
|
+
const s = symbols[i];
|
|
171
|
+
const nodeType = String(s.metadata?.nodeType ?? "");
|
|
172
|
+
const isTopLevelType = TOP_LEVEL_TYPE_NODES.has(nodeType);
|
|
173
|
+
const isTopLevelFunction = nodeType === "method_declaration" || nodeType === "function_definition";
|
|
174
|
+
if (!s.metadata?.isAnonymousClass && !s.metadata?.isPhantom && !s.ownerName && (isTopLevelType || isTopLevelFunction)) {
|
|
175
|
+
exports.push({
|
|
176
|
+
name: s.name,
|
|
177
|
+
kind: s.kind,
|
|
178
|
+
range: s.range,
|
|
179
|
+
visibility: s.visibility,
|
|
180
|
+
metadata: { exportForm: isTopLevelFunction ? "public_top_level_function" : "public_top_level_type" }
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let exportsWithNoPackage = 0;
|
|
185
|
+
let exportsNotImportable = 0;
|
|
186
|
+
if (exports.length > 0) {
|
|
187
|
+
const published = /* @__PURE__ */ new Set();
|
|
188
|
+
for (let i = 0; i < exports.length; i++) {
|
|
189
|
+
const e = exports[i];
|
|
190
|
+
const target = e.name;
|
|
191
|
+
if (!target) continue;
|
|
192
|
+
if (published.has(target)) continue;
|
|
193
|
+
published.add(target);
|
|
194
|
+
if (!walk.currentPackage) {
|
|
195
|
+
exportsWithNoPackage++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (e.visibility === "private" || e.visibility === "protected") {
|
|
199
|
+
exportsNotImportable++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const exportForm = String(e.metadata?.exportForm ?? "public_top_level_type");
|
|
203
|
+
relationships.push({
|
|
204
|
+
source: walk.currentPackage,
|
|
205
|
+
target,
|
|
206
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
207
|
+
confidence: 1,
|
|
208
|
+
// M59 (#825): the exported DECLARATION's own line — the name is
|
|
209
|
+
// written there, not at whatever construct encloses it. Absent rather
|
|
210
|
+
// than 0 when the symbol carried no range (#823).
|
|
211
|
+
...e.range?.startLine ? { line_number: e.range.startLine } : {},
|
|
212
|
+
metadata: {
|
|
213
|
+
// The vocabulary is the java lane's, and it is the adjudicated
|
|
214
|
+
// one (java/exported-types, task-9, 2026-10-18): `exported_type` was
|
|
215
|
+
// superseded by `<lang>_exportable_unit` + `exportForm` when java's
|
|
216
|
+
// export edge was folded into ONE post-pass, and the surviving
|
|
217
|
+
// channel is the one with the RICHER metadata — `kind` + `dottedName`
|
|
218
|
+
// + the `is<Lang>Export` flag. groovy carries the same shape, and
|
|
219
|
+
// groovy/exports-publication-edge pins it.
|
|
220
|
+
relationshipType: "groovy_exportable_unit",
|
|
221
|
+
exportForm,
|
|
222
|
+
isGroovyExport: true,
|
|
223
|
+
kind: String(e.kind ?? ""),
|
|
224
|
+
// The join key, stated because it is not obvious: an `exports` edge
|
|
225
|
+
// is `package -> simpleName`, so `source + "." + target`
|
|
226
|
+
// reconstructs the dotted path an `imports` edge targets and the
|
|
227
|
+
// two labels meet without a resolver (java's key, same meaning).
|
|
228
|
+
dottedName: `${walk.currentPackage}.${target}`
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const sym of symbols) {
|
|
234
|
+
const decs = sym.decorators;
|
|
235
|
+
if (!decs || decs.length === 0) continue;
|
|
236
|
+
const lines = sym.metadata?.decoratorLines;
|
|
237
|
+
for (let i = 0; i < decs.length; i++) {
|
|
238
|
+
const line = lines?.[i];
|
|
239
|
+
relationships.push({
|
|
240
|
+
source: sym.name,
|
|
241
|
+
target: decs[i],
|
|
242
|
+
type: ASTEngine.RelationshipType.Decorates,
|
|
243
|
+
confidence: 1,
|
|
244
|
+
...typeof line === "number" ? { line_number: line } : {},
|
|
245
|
+
metadata: {
|
|
246
|
+
isDecorator: true,
|
|
247
|
+
decoratorLine: typeof line === "number" ? line : null,
|
|
248
|
+
isGroovyDecorator: true
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
this.classifyEvidenceTier(relationships);
|
|
254
|
+
return {
|
|
255
|
+
symbols,
|
|
256
|
+
relationships,
|
|
257
|
+
imports,
|
|
258
|
+
exports,
|
|
259
|
+
errors,
|
|
260
|
+
metadata: {
|
|
261
|
+
// Real cyclomatic complexity — was the synthetic heuristic
|
|
262
|
+
// symbols.length + relationships.length, which is not a complexity
|
|
263
|
+
// metric at all (same defect class previously fixed on scala).
|
|
264
|
+
complexity: this.calculateGroovyMethodComplexity(rootNode),
|
|
265
|
+
cyclomaticComplexity: this.calculateGroovyMethodComplexity(rootNode),
|
|
266
|
+
linesOfCode: content.split("\n").length,
|
|
267
|
+
// Refusals, surfaced. A declaration whose name came back EMPTY (a MISSING node
|
|
268
|
+
// from error recovery) is dropped rather than published, because an empty name is
|
|
269
|
+
// not a name -- but a silent drop is a hole exactly like the one it closes, so the
|
|
270
|
+
// count travels with the result. Non-zero means the file had a parse that did not
|
|
271
|
+
// happen, which is worth knowing even when the symbol list looks clean.
|
|
272
|
+
emptyNameRefusals: walk.emptyNameRefusals ?? 0,
|
|
273
|
+
currentPackage: walk.currentPackage,
|
|
274
|
+
// DERIV_EXPORTS_GROOVY3: exportable units this file HAS but did not
|
|
275
|
+
// publish as an edge, because it declares no package and so has no
|
|
276
|
+
// truthful exporter to name. Zero means every exportable unit in the
|
|
277
|
+
// file reached the graph; non-zero is the count of export surface a
|
|
278
|
+
// reader of the relationship table cannot see, and it is reported
|
|
279
|
+
// rather than absorbed. Measured over the corpus: 266 of 2,817 files.
|
|
280
|
+
exportsWithNoPackage,
|
|
281
|
+
exportsNotImportable,
|
|
282
|
+
framework: fw.framework,
|
|
283
|
+
detectedFrameworks: fw.detected,
|
|
284
|
+
analysisVersion: "5.0.0-groovy-pure-ast"
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// ── #896: node-type dispatch table ────────────────────────────────────────
|
|
289
|
+
// `analyze` walks (push/visit/pop); these handlers extract. Each is a named
|
|
290
|
+
// method taking the walk state, so a handler can be exercised on its own —
|
|
291
|
+
// which is what the review asked for and what the if/else chain prevented.
|
|
292
|
+
// The misparse guards (§ juxt_function_call / field_declaration) are named
|
|
293
|
+
// handlers here for the same reason: they stay individually testable, and
|
|
294
|
+
// #841 rounds 3/14/22 measured them as load-bearing, so they are kept.
|
|
295
|
+
nodeHandlersCache;
|
|
296
|
+
get nodeHandlers() {
|
|
297
|
+
if (!this.nodeHandlersCache) {
|
|
298
|
+
const m = /* @__PURE__ */ new Map();
|
|
299
|
+
const reg = (types, h) => {
|
|
300
|
+
for (const t of types) m.set(t, h);
|
|
301
|
+
};
|
|
302
|
+
reg(["package_declaration"], (n, s) => this.handlePackageDeclaration(n, s));
|
|
303
|
+
reg(["import_declaration"], (n, s) => this.handleImportDeclaration(n, s));
|
|
304
|
+
reg(["class_declaration", "interface_declaration", "trait_declaration", "enum_declaration", "record_declaration", "annotation_type_declaration"], (n, s) => this.handleTypeDeclaration(n, s));
|
|
305
|
+
reg(["method_declaration", "constructor_declaration", "function_definition"], (n, s) => this.handleMethodDeclaration(n, s));
|
|
306
|
+
reg(["field_declaration", "constant_declaration"], (n, s) => this.handleFieldDeclaration(n, s));
|
|
307
|
+
reg(["enum_constant"], (n, s) => this.handleEnumConstant(n, s));
|
|
308
|
+
reg(["method_invocation"], (n, s) => this.handleMethodInvocation(n, s));
|
|
309
|
+
reg(["explicit_constructor_invocation"], (n, s) => this.handleExplicitConstructorInvocation(n, s));
|
|
310
|
+
reg(["juxt_function_call"], (n, s) => this.handleJuxtFunctionCall(n, s));
|
|
311
|
+
reg(["leading_dot_call"], (n, s) => this.handleLeadingDotCall(n, s));
|
|
312
|
+
reg(["object_creation_expression"], (n, s) => this.handleObjectCreationExpression(n, s));
|
|
313
|
+
this.nodeHandlersCache = m;
|
|
314
|
+
}
|
|
315
|
+
return this.nodeHandlersCache;
|
|
316
|
+
}
|
|
317
|
+
handlePackageDeclaration(node, s) {
|
|
318
|
+
const type = node.type;
|
|
319
|
+
const nameNode = this.findChild(node, (n) => n.type === "scoped_identifier" || n.type === "identifier");
|
|
320
|
+
if (nameNode) s.currentPackage = nameNode.text;
|
|
321
|
+
}
|
|
322
|
+
handleImportDeclaration(node, s) {
|
|
323
|
+
const { relationships, imports } = s;
|
|
324
|
+
const type = node.type;
|
|
325
|
+
const nameNode = this.findChild(node, (n) => n.type === "scoped_identifier" || n.type === "identifier");
|
|
326
|
+
if (nameNode) {
|
|
327
|
+
const moduleName = nameNode.text;
|
|
328
|
+
let isStatic = false;
|
|
329
|
+
let isWildcard = false;
|
|
330
|
+
let hasAlias = false;
|
|
331
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
332
|
+
const c = node.child(i);
|
|
333
|
+
if (!c) continue;
|
|
334
|
+
if (c.type === "static") isStatic = true;
|
|
335
|
+
else if (c.type === "asterisk") isWildcard = true;
|
|
336
|
+
else if (c.type === "as") hasAlias = true;
|
|
337
|
+
}
|
|
338
|
+
imports.push({
|
|
339
|
+
module: moduleName,
|
|
340
|
+
items: [],
|
|
341
|
+
isDefault: false,
|
|
342
|
+
range: this.nodeToRange(node),
|
|
343
|
+
isNamespace: isWildcard,
|
|
344
|
+
metadata: { isWildcard, isStatic }
|
|
345
|
+
});
|
|
346
|
+
const importMeta = { isGroovyImport: true, isWildcard, isStatic, hasAlias };
|
|
347
|
+
relationships.push({
|
|
348
|
+
source: s.currentPackage || "(script)",
|
|
349
|
+
target: moduleName,
|
|
350
|
+
type: ASTEngine.RelationshipType.References,
|
|
351
|
+
confidence: 0.9,
|
|
352
|
+
// M59 (#825): the line the imported MODULE is named on (java's rule).
|
|
353
|
+
...this.lineField(nameNode, node),
|
|
354
|
+
metadata: importMeta
|
|
355
|
+
});
|
|
356
|
+
relationships.push({
|
|
357
|
+
source: s.currentPackage || "(script)",
|
|
358
|
+
target: moduleName,
|
|
359
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
360
|
+
confidence: 0.9,
|
|
361
|
+
...this.lineField(nameNode, node),
|
|
362
|
+
metadata: {
|
|
363
|
+
...importMeta,
|
|
364
|
+
isImport: true,
|
|
365
|
+
relationshipType: isStatic ? isWildcard ? "static_on_demand" : "static_member" : isWildcard ? "on_demand_type" : "single_type"
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
relationships.push({
|
|
369
|
+
source: s.currentPackage || "(script)",
|
|
370
|
+
target: moduleName,
|
|
371
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
372
|
+
confidence: 1,
|
|
373
|
+
...this.lineField(nameNode, node),
|
|
374
|
+
metadata: {
|
|
375
|
+
relationshipType: isStatic ? "static_member_import" : isWildcard ? "type_on_demand_import" : "single_type_import",
|
|
376
|
+
isGroovyImport: true,
|
|
377
|
+
isWildcard,
|
|
378
|
+
isStatic,
|
|
379
|
+
fileLevel: true
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
handleTypeDeclaration(node, s) {
|
|
385
|
+
const { symbols, relationships, symbolMap, classHierarchy, scopeStack } = s;
|
|
386
|
+
const type = node.type;
|
|
387
|
+
const nameNode = this.findChild(node, (n) => n.type === "identifier");
|
|
388
|
+
if (nameNode) {
|
|
389
|
+
const name = nameNode.text;
|
|
390
|
+
if (name.trim() === "") {
|
|
391
|
+
s.emptyNameRefusals = (s.emptyNameRefusals ?? 0) + 1;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const kind = type === "interface_declaration" || type === "trait_declaration" ? ASTEngine.SymbolKind.Interface : type === "enum_declaration" ? ASTEngine.SymbolKind.Enum : ASTEngine.SymbolKind.Class;
|
|
395
|
+
const fqn = this.buildFqn(s.currentPackage, scopeStack, name);
|
|
396
|
+
const visibility = this.extractVisibility(node);
|
|
397
|
+
const parents = this.extractInheritance(node, type);
|
|
398
|
+
classHierarchy.set(fqn, parents);
|
|
399
|
+
const classAnnotationRefs = this.extractAnnotationRefs(node);
|
|
400
|
+
const classAnnotations = classAnnotationRefs.map((a) => a.name);
|
|
401
|
+
if (type === "class_declaration") {
|
|
402
|
+
const superClass = this.findChild(node, (n) => n.type === "superclass");
|
|
403
|
+
if (superClass) {
|
|
404
|
+
const sc = this.findChild(superClass, (n) => n.type === "type_identifier" || n.type === "identifier" || n.type === "generic_type" || n.type === "scoped_type_identifier");
|
|
405
|
+
if (sc) {
|
|
406
|
+
relationships.push({
|
|
407
|
+
source: name,
|
|
408
|
+
target: this.baseTypeName(sc),
|
|
409
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
410
|
+
confidence: 1,
|
|
411
|
+
// M59 (#825): the SUPERtype's own line, never the class header's.
|
|
412
|
+
...this.lineField(sc, node),
|
|
413
|
+
metadata: { relationshipType: "extends" }
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} else if (type === "interface_declaration") {
|
|
418
|
+
const ext = this.findChild(node, (n) => n.type === "extends_interfaces");
|
|
419
|
+
if (ext) {
|
|
420
|
+
for (const tid of this.collectTypeNodes(ext)) {
|
|
421
|
+
relationships.push({
|
|
422
|
+
source: name,
|
|
423
|
+
target: this.baseTypeName(tid),
|
|
424
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
425
|
+
confidence: 1,
|
|
426
|
+
// M59 (#825): each interface parent is named on its OWN line.
|
|
427
|
+
...this.lineField(tid, node),
|
|
428
|
+
metadata: { relationshipType: "extends_interface" }
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} else if (type === "trait_declaration") {
|
|
433
|
+
const superClass = this.findChild(node, (n) => n.type === "superclass");
|
|
434
|
+
if (superClass) {
|
|
435
|
+
const sc = this.findChild(superClass, (n) => n.type === "type_identifier" || n.type === "identifier" || n.type === "generic_type" || n.type === "scoped_type_identifier");
|
|
436
|
+
if (sc) {
|
|
437
|
+
relationships.push({
|
|
438
|
+
source: name,
|
|
439
|
+
target: this.baseTypeName(sc),
|
|
440
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
441
|
+
confidence: 1,
|
|
442
|
+
...this.lineField(sc, node),
|
|
443
|
+
metadata: { relationshipType: "extends_trait" }
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const superInterfaces = this.findChild(node, (n) => n.type === "super_interfaces");
|
|
449
|
+
if (superInterfaces) {
|
|
450
|
+
for (const tid of this.collectTypeNodes(superInterfaces)) {
|
|
451
|
+
relationships.push({
|
|
452
|
+
source: name,
|
|
453
|
+
target: this.baseTypeName(tid),
|
|
454
|
+
type: ASTEngine.RelationshipType.Implements,
|
|
455
|
+
confidence: 1,
|
|
456
|
+
// M59 (#825): each interface is named on its OWN line.
|
|
457
|
+
...this.lineField(tid, node),
|
|
458
|
+
metadata: { relationshipType: "implements" }
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const sym = {
|
|
463
|
+
name,
|
|
464
|
+
kind,
|
|
465
|
+
comments: (() => {
|
|
466
|
+
const d = this.extractGroovyDoc(node);
|
|
467
|
+
return d ? [d] : [];
|
|
468
|
+
})(),
|
|
469
|
+
genericParameters: (() => {
|
|
470
|
+
const tp = this.extractGroovyTypeParameters(node);
|
|
471
|
+
return tp.length > 0 ? tp : void 0;
|
|
472
|
+
})(),
|
|
473
|
+
// M59 (#825): annotations skipped — the class is written on its own line.
|
|
474
|
+
range: this.symbolRange(node),
|
|
475
|
+
visibility,
|
|
476
|
+
decorators: classAnnotations,
|
|
477
|
+
ownerName: scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0,
|
|
478
|
+
ownerKind: scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].kind : void 0,
|
|
479
|
+
metadata: { nodeType: type, annotations: classAnnotations, isRecord: type === "record_declaration", decoratorLines: classAnnotationRefs.map((a) => a.line) }
|
|
480
|
+
};
|
|
481
|
+
symbols.push(sym);
|
|
482
|
+
symbolMap.set(fqn, sym);
|
|
483
|
+
symbolMap.set(name, sym);
|
|
484
|
+
if (type === "record_declaration") {
|
|
485
|
+
const fp = this.findChild(node, (n) => n.type === "formal_parameters");
|
|
486
|
+
if (fp) {
|
|
487
|
+
for (let i = 0; i < fp.namedChildCount; i++) {
|
|
488
|
+
const p = fp.namedChild(i);
|
|
489
|
+
if (!p || p.type !== "formal_parameter") continue;
|
|
490
|
+
const pName = this.findChild(p, (n) => n.type === "identifier");
|
|
491
|
+
const pType = this.findChild(p, (n) => n.type === "type_identifier" || n.type === "integral_type" || n.type === "floating_point_type" || n.type === "boolean_type" || n.type === "generic_type" || n.type === "array_type" || n.type === "scoped_type_identifier");
|
|
492
|
+
if (pName) {
|
|
493
|
+
symbols.push({
|
|
494
|
+
name: pName.text,
|
|
495
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
496
|
+
range: this.symbolRange(p),
|
|
497
|
+
returnType: pType ? this.baseTypeName(pType) : void 0,
|
|
498
|
+
visibility: "public",
|
|
499
|
+
ownerName: name,
|
|
500
|
+
ownerKind: kind,
|
|
501
|
+
metadata: { nodeType: "record_component" }
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
s.newContext = {
|
|
508
|
+
node,
|
|
509
|
+
name,
|
|
510
|
+
kind,
|
|
511
|
+
fqn,
|
|
512
|
+
symbolRef: sym,
|
|
513
|
+
visibility,
|
|
514
|
+
isInterface: type === "interface_declaration" || type === "trait_declaration",
|
|
515
|
+
parents
|
|
516
|
+
};
|
|
517
|
+
scopeStack.push(s.newContext);
|
|
518
|
+
s.pushedScope = true;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
handleMethodDeclaration(node, s) {
|
|
522
|
+
const { symbols, symbolMap, overrideTasks, scopeStack } = s;
|
|
523
|
+
const type = node.type;
|
|
524
|
+
const isConstructorRaw = type === "constructor_declaration";
|
|
525
|
+
const nameNode = this.findChild(node, (n) => n.type === "identifier" || n.type === "string_literal");
|
|
526
|
+
if (nameNode) {
|
|
527
|
+
const rawName = nameNode.type === "string_literal" ? this.stripQuotes(nameNode.text) : nameNode.text;
|
|
528
|
+
if (rawName.trim() === "") {
|
|
529
|
+
s.emptyNameRefusals = (s.emptyNameRefusals ?? 0) + 1;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const name = "constructor";
|
|
533
|
+
const modifiers = this.extractModifiers(node);
|
|
534
|
+
const visibility = this.extractVisibility(node);
|
|
535
|
+
const annotationRefs = this.extractAnnotationRefs(node);
|
|
536
|
+
const annotations = annotationRefs.map((a) => a.name);
|
|
537
|
+
const enclosingScope = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : void 0;
|
|
538
|
+
const enclosingName = enclosingScope && (enclosingScope.kind === ASTEngine.SymbolKind.Class || enclosingScope.kind === ASTEngine.SymbolKind.Enum || enclosingScope.kind === ASTEngine.SymbolKind.Interface) ? enclosingScope.name : void 0;
|
|
539
|
+
const isMisnamedCtor = isConstructorRaw && enclosingName !== void 0 && rawName !== enclosingName;
|
|
540
|
+
const isConstructor = isConstructorRaw && !isMisnamedCtor;
|
|
541
|
+
const memberName = rawName;
|
|
542
|
+
const parameters = [];
|
|
543
|
+
const paramList = this.findChild(node, (n) => n.type === "formal_parameters");
|
|
544
|
+
if (paramList) {
|
|
545
|
+
for (let i = 0; i < paramList.namedChildCount; i++) {
|
|
546
|
+
const p = paramList.namedChild(i);
|
|
547
|
+
if (!p || p.type !== "formal_parameter" && p.type !== "spread_parameter") continue;
|
|
548
|
+
const isSpread = p.type === "spread_parameter";
|
|
549
|
+
let pName = this.findChild(p, (n) => n.type === "identifier");
|
|
550
|
+
if (isSpread) {
|
|
551
|
+
const decl = this.findChild(p, (n) => n.type === "variable_declarator");
|
|
552
|
+
pName = decl ? this.findChild(decl, (n) => n.type === "identifier") : null;
|
|
553
|
+
}
|
|
554
|
+
const pType = this.findChild(p, (n) => n.type === "type_identifier" || n.type === "integral_type" || n.type === "floating_point_type" || n.type === "boolean_type" || n.type === "void_type" || n.type === "generic_type" || n.type === "array_type" || n.type === "scoped_type_identifier");
|
|
555
|
+
parameters.push({
|
|
556
|
+
name: pName?.text ?? "anonymous",
|
|
557
|
+
type: pType ? this.baseTypeName(pType) + (isSpread ? "[]" : "") : void 0
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const signature = `(${parameters.map((p) => p.type ?? "").join(",")})`;
|
|
562
|
+
const ownerName = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0;
|
|
563
|
+
const ownerKind = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].kind : void 0;
|
|
564
|
+
const fqn = this.buildFqn(s.currentPackage, scopeStack, memberName);
|
|
565
|
+
const hasBody = !!this.findChild(node, (n) => n.type === "block" || n.type === "closure" || n.type === "constructor_body");
|
|
566
|
+
const isAbstract = !hasBody || !!s.newContext?.isInterface && !hasBody;
|
|
567
|
+
const declLine = (node.childForFieldName("name") ?? nameNode ?? this.declarationLineNode(node)).startPosition.row + 1;
|
|
568
|
+
if (annotations.indexOf("Override") !== -1 && s.newContext) {
|
|
569
|
+
overrideTasks.push({
|
|
570
|
+
fqn,
|
|
571
|
+
methodName: memberName,
|
|
572
|
+
signature,
|
|
573
|
+
ownerFqn: s.newContext.fqn,
|
|
574
|
+
explicit: true,
|
|
575
|
+
line: declLine
|
|
576
|
+
});
|
|
577
|
+
} else if (s.newContext && !isConstructor && modifiers.indexOf("static") === -1 && visibility !== "private") {
|
|
578
|
+
overrideTasks.push({
|
|
579
|
+
fqn,
|
|
580
|
+
methodName: memberName,
|
|
581
|
+
signature,
|
|
582
|
+
ownerFqn: s.newContext.fqn,
|
|
583
|
+
explicit: false,
|
|
584
|
+
line: declLine
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
const sym = {
|
|
588
|
+
name: memberName,
|
|
589
|
+
kind: isConstructor ? ASTEngine.SymbolKind.Constructor : ASTEngine.SymbolKind.Method,
|
|
590
|
+
comments: (() => {
|
|
591
|
+
const d = this.extractGroovyDoc(node);
|
|
592
|
+
return d ? [d] : [];
|
|
593
|
+
})(),
|
|
594
|
+
complexity: this.calculateGroovyMethodComplexity(node),
|
|
595
|
+
// The M59 (#825) anchor for a method is its NAME, and it is its name
|
|
596
|
+
// ALWAYS — the annotation branch that used to sit here is gone.
|
|
597
|
+
//
|
|
598
|
+
// It existed because the symbol's range was being read as the
|
|
599
|
+
// DECLARATION's line, and the hand-audited truth recorded the
|
|
600
|
+
// ANNOTATION's line for an annotated method (`@Override` L16 /
|
|
601
|
+
// `format` L17 → truth 16). With the name as the anchor, the line is
|
|
602
|
+
// the line the method is SPELLED on, which is the contract the M59 note
|
|
603
|
+
// states ("the line the thing is spelled on, not the declaration's
|
|
604
|
+
// opening line") — and it is what the whole live family does
|
|
605
|
+
// (script/m14/groovy-family-line-anchor-probe.ts: java/kotlin/scala all
|
|
606
|
+
// report the declaration's line for an annotated method, groovy
|
|
607
|
+
// reported the annotation's).
|
|
608
|
+
//
|
|
609
|
+
// THIS LANE'S OWN FIXTURES pin it and forbid the other:
|
|
610
|
+
// groovy/lines-825 contains[Wrapped->use] @26, @25 FORBIDDEN
|
|
611
|
+
// groovy/anonymous-body-overrides overrides[anonymous_TypeCodec.bsonType->bsonType] @23
|
|
612
|
+
// groovy/ctor-field-split-lines contains[Split->Split] @19 (the NAME's line)
|
|
613
|
+
//
|
|
614
|
+
// The tree-sitter-groovy field-absorb case is unchanged by this and is
|
|
615
|
+
// still the reason the NAME is the anchor: `final Distance distance` +
|
|
616
|
+
// `Sphere(...)` parse as one method_declaration starting at the field, so
|
|
617
|
+
// the node start is the field's line while the name is written 7 lines
|
|
618
|
+
// lower (measured: grails-core Sphere.groovy).
|
|
619
|
+
range: this.symbolRange(
|
|
620
|
+
node,
|
|
621
|
+
node.childForFieldName("name") ?? nameNode
|
|
622
|
+
),
|
|
623
|
+
visibility,
|
|
624
|
+
genericParameters: (() => {
|
|
625
|
+
const tp = this.extractGroovyTypeParameters(node);
|
|
626
|
+
return tp.length > 0 ? tp : void 0;
|
|
627
|
+
})(),
|
|
628
|
+
parameters,
|
|
629
|
+
decorators: annotations,
|
|
630
|
+
ownerName,
|
|
631
|
+
ownerKind,
|
|
632
|
+
metadata: { nodeType: type, modifiers, annotations, isAbstract, decoratorLines: annotationRefs.map((a) => a.line) }
|
|
633
|
+
};
|
|
634
|
+
symbols.push(sym);
|
|
635
|
+
symbolMap.set(fqn, sym);
|
|
636
|
+
const rawKey = ownerName ? `${ownerName}.${memberName}${signature}` : `${memberName}${signature}`;
|
|
637
|
+
symbolMap.set(rawKey, sym);
|
|
638
|
+
s.newContext = {
|
|
639
|
+
node,
|
|
640
|
+
name: memberName,
|
|
641
|
+
kind: isConstructor ? ASTEngine.SymbolKind.Constructor : ASTEngine.SymbolKind.Method,
|
|
642
|
+
fqn,
|
|
643
|
+
symbolRef: sym,
|
|
644
|
+
visibility,
|
|
645
|
+
isInterface: false,
|
|
646
|
+
parents: []
|
|
647
|
+
};
|
|
648
|
+
scopeStack.push(s.newContext);
|
|
649
|
+
s.pushedScope = true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
handleFieldDeclaration(node, s) {
|
|
653
|
+
const { symbols, scopeStack } = s;
|
|
654
|
+
const type = node.type;
|
|
655
|
+
const modifiers = this.extractModifiers(node);
|
|
656
|
+
const visibility = this.extractVisibility(node);
|
|
657
|
+
const isStatic = modifiers.indexOf("static") !== -1;
|
|
658
|
+
const isConstant = modifiers.indexOf("final") !== -1 && isStatic;
|
|
659
|
+
const fieldAnnotationRefs = this.extractAnnotationRefs(node);
|
|
660
|
+
const fieldAnnotations = fieldAnnotationRefs.map((a) => a.name);
|
|
661
|
+
const ownerName = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0;
|
|
662
|
+
const ownerKind = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].kind : void 0;
|
|
663
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
664
|
+
const child = node.namedChild(i);
|
|
665
|
+
if (child?.type !== "variable_declarator") continue;
|
|
666
|
+
const idNode = this.findChild(child, (n) => n.type === "identifier");
|
|
667
|
+
if (!idNode || !ownerName) continue;
|
|
668
|
+
if (idNode.text === "def") continue;
|
|
669
|
+
if (this.isDuplicateParseField(node, s.content)) continue;
|
|
670
|
+
const fieldName = idNode.text.trim();
|
|
671
|
+
if (fieldName === "") {
|
|
672
|
+
s.emptyNameRefusals = (s.emptyNameRefusals ?? 0) + 1;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
symbols.push({
|
|
676
|
+
name: fieldName,
|
|
677
|
+
kind: isConstant ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Property,
|
|
678
|
+
range: this.symbolRange(child),
|
|
679
|
+
visibility,
|
|
680
|
+
ownerName,
|
|
681
|
+
ownerKind,
|
|
682
|
+
decorators: fieldAnnotations,
|
|
683
|
+
metadata: { nodeType: type, modifiers, isConstant, annotations: fieldAnnotations, decoratorLines: fieldAnnotationRefs.map((a) => a.line) }
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
handleEnumConstant(node, s) {
|
|
688
|
+
const { symbols, scopeStack } = s;
|
|
689
|
+
const type = node.type;
|
|
690
|
+
const nameNode = this.findChild(node, (n) => n.type === "identifier");
|
|
691
|
+
const ownerName = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].name : void 0;
|
|
692
|
+
const enumMemberName = nameNode?.text.trim() ?? "";
|
|
693
|
+
if (enumMemberName !== "" && ownerName) {
|
|
694
|
+
symbols.push({
|
|
695
|
+
name: enumMemberName,
|
|
696
|
+
kind: ASTEngine.SymbolKind.EnumMember,
|
|
697
|
+
range: this.symbolRange(node),
|
|
698
|
+
visibility: "public",
|
|
699
|
+
ownerName,
|
|
700
|
+
ownerKind: ASTEngine.SymbolKind.Enum,
|
|
701
|
+
metadata: { nodeType: type }
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
handleMethodInvocation(node, s) {
|
|
706
|
+
const { relationships } = s;
|
|
707
|
+
const type = node.type;
|
|
708
|
+
const nameNode = node.childForFieldName("name");
|
|
709
|
+
if (nameNode) {
|
|
710
|
+
const methodName = nameNode.text;
|
|
711
|
+
const objectNode = node.childForFieldName("object");
|
|
712
|
+
const callExpr = this.buildGroovyQualifiedCall(objectNode, methodName);
|
|
713
|
+
const sourceName = s.parentContext?.name ?? "(script)";
|
|
714
|
+
if (!this.isGroovyBuiltin(callExpr)) {
|
|
715
|
+
if (s.parentContext?.symbolRef) {
|
|
716
|
+
if (!s.parentContext.symbolRef.qualified_calls) s.parentContext.symbolRef.qualified_calls = [];
|
|
717
|
+
s.parentContext.symbolRef.qualified_calls.push(callExpr);
|
|
718
|
+
}
|
|
719
|
+
relationships.push({
|
|
720
|
+
source: sourceName,
|
|
721
|
+
target: methodName,
|
|
722
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
723
|
+
confidence: 0.7,
|
|
724
|
+
// M59 (#825): the method NAME's line, not the receiver's — a
|
|
725
|
+
// multi-line chain opens on the receiver (scala's measured defect).
|
|
726
|
+
...this.lineField(nameNode, node),
|
|
727
|
+
metadata: { groovyCallExpression: callExpr }
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
handleExplicitConstructorInvocation(node, s) {
|
|
733
|
+
const { relationships } = s;
|
|
734
|
+
const type = node.type;
|
|
735
|
+
const first = node.namedChild(0);
|
|
736
|
+
if (first && (first.type === "super" || first.type === "this")) {
|
|
737
|
+
relationships.push({
|
|
738
|
+
source: s.parentContext?.name ?? "(script)",
|
|
739
|
+
target: first.type,
|
|
740
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
741
|
+
confidence: 1,
|
|
742
|
+
...this.lineField(first, node),
|
|
743
|
+
metadata: { relationshipType: "constructor_invocation" }
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
handleJuxtFunctionCall(node, s) {
|
|
748
|
+
const { relationships } = s;
|
|
749
|
+
const type = node.type;
|
|
750
|
+
const nameNode = node.childForFieldName("name");
|
|
751
|
+
if (nameNode && !this.isGroovyBuiltin(nameNode.text)) {
|
|
752
|
+
if (this.looksLikeTypedClosureMisparse(node, nameNode, s.content)) {
|
|
753
|
+
} else {
|
|
754
|
+
const leafNode = nameNode.type === "field_access" ? nameNode.childForFieldName("field") ?? nameNode : nameNode;
|
|
755
|
+
relationships.push({
|
|
756
|
+
source: s.parentContext?.name ?? "(script)",
|
|
757
|
+
target: leafNode.text,
|
|
758
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
759
|
+
confidence: 0.7,
|
|
760
|
+
...this.lineField(leafNode, node),
|
|
761
|
+
// The callee expression AS WRITTEN (the sibling method_invocation
|
|
762
|
+
// branch's payload) — kept so the dotted form is recoverable by a
|
|
763
|
+
// consumer even though the edge itself names the bare leaf.
|
|
764
|
+
metadata: { relationshipType: "juxt_call", groovyCallExpression: nameNode.text }
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
handleLeadingDotCall(node, s) {
|
|
770
|
+
const { relationships } = s;
|
|
771
|
+
const type = node.type;
|
|
772
|
+
const nameNode = node.childForFieldName("name");
|
|
773
|
+
if (nameNode && !this.isGroovyBuiltin(nameNode.text)) {
|
|
774
|
+
const args = node.childForFieldName("arguments") ?? this.findChild(node, (n) => n.type === "argument_list");
|
|
775
|
+
if (args) {
|
|
776
|
+
relationships.push({
|
|
777
|
+
source: s.parentContext?.name ?? "(script)",
|
|
778
|
+
target: nameNode.text,
|
|
779
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
780
|
+
confidence: 0.7,
|
|
781
|
+
...this.lineField(nameNode, node),
|
|
782
|
+
metadata: { relationshipType: "leading_dot_call" }
|
|
783
|
+
});
|
|
784
|
+
} else {
|
|
785
|
+
relationships.push({
|
|
786
|
+
source: s.parentContext?.name ?? "(script)",
|
|
787
|
+
target: nameNode.text,
|
|
788
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
789
|
+
confidence: 0.6,
|
|
790
|
+
...this.lineField(nameNode, node),
|
|
791
|
+
metadata: { isFieldAccess: true }
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
handleObjectCreationExpression(node, s) {
|
|
797
|
+
const { symbols, relationships, symbolMap, classHierarchy, scopeStack } = s;
|
|
798
|
+
const type = node.type;
|
|
799
|
+
const typeNode = this.findChild(node, (n) => n.type === "type_identifier" || n.type === "generic_type" || n.type === "scoped_type_identifier" || n.type === "identifier");
|
|
800
|
+
if (typeNode) {
|
|
801
|
+
const source = s.parentContext?.name || s.parentContext?.symbolRef?.name || "(script)";
|
|
802
|
+
relationships.push({
|
|
803
|
+
source,
|
|
804
|
+
target: this.baseTypeName(typeNode),
|
|
805
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
806
|
+
confidence: 0.95,
|
|
807
|
+
// M59 (#825): the constructed TYPE's own line.
|
|
808
|
+
...this.lineField(typeNode, node),
|
|
809
|
+
metadata: { relationshipType: "instantiates", isNew: true }
|
|
810
|
+
});
|
|
811
|
+
const classBody = this.findChild(node, (n) => n.type === "class_body");
|
|
812
|
+
if (classBody && s.parentContext) {
|
|
813
|
+
const superName = this.baseTypeName(typeNode);
|
|
814
|
+
const anonName = superName ? `anonymous_${superName}` : "anonymous_class";
|
|
815
|
+
const anonFqn = s.parentContext.fqn ? `${s.parentContext.fqn}.${anonName}` : anonName;
|
|
816
|
+
const anonSym = {
|
|
817
|
+
name: anonName,
|
|
818
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
819
|
+
range: this.symbolRange(node),
|
|
820
|
+
visibility: s.parentContext.visibility || "public",
|
|
821
|
+
ownerName: s.parentContext.name,
|
|
822
|
+
ownerKind: s.parentContext.kind,
|
|
823
|
+
// `synthesized: true` is a DECLARATION, not an exemption request. The name is
|
|
824
|
+
// invented from the supertype because an anonymous class has no spelling, and
|
|
825
|
+
// the M60 surface audit's "name not present in source" / "ghost target" rows
|
|
826
|
+
// cannot tell that from a name the analyzer made up and passed off. Declaring
|
|
827
|
+
// it moves both rows into the audit's own "named by contract, not by source
|
|
828
|
+
// spelling" category -- counted, disclosed, and still failing for any
|
|
829
|
+
// UNDECLARED synthesized name, which is the case that actually matters.
|
|
830
|
+
metadata: { nodeType: type, isAnonymousClass: true, synthesized: true, superTypes: superName ? [superName] : [] }
|
|
831
|
+
};
|
|
832
|
+
symbols.push(anonSym);
|
|
833
|
+
symbolMap.set(anonFqn, anonSym);
|
|
834
|
+
classHierarchy.set(anonFqn, superName ? [superName] : []);
|
|
835
|
+
s.newContext = {
|
|
836
|
+
node: classBody,
|
|
837
|
+
name: anonName,
|
|
838
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
839
|
+
fqn: anonFqn,
|
|
840
|
+
symbolRef: anonSym,
|
|
841
|
+
visibility: s.parentContext.visibility,
|
|
842
|
+
isInterface: false,
|
|
843
|
+
parents: superName ? [superName] : []
|
|
844
|
+
};
|
|
845
|
+
scopeStack.push(s.newContext);
|
|
846
|
+
s.pushedScope = true;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Framework detection from imports + annotation short names. The mapping is
|
|
852
|
+
* MEASURED against the grails-core corpus (top imported packages):
|
|
853
|
+
* the "grails" / "org.grails" / "org.apache.grails", "spock.lang" and
|
|
854
|
+
* "org.springframework" import families dominate real Groovy code.
|
|
855
|
+
* result.framework flows to
|
|
856
|
+
* brain_file.framework_type which patterns/rules.ts ranks on — Groovy
|
|
857
|
+
* files previously got NULL there, hiding Grails projects from pattern
|
|
858
|
+
* analysis. Primary selection follows a fixed priority so the same file
|
|
859
|
+
* always yields the same primary.
|
|
860
|
+
*/
|
|
861
|
+
/**
|
|
862
|
+
* M14 #299 - evidence-tier classification for EVERY emitted relationship.
|
|
863
|
+
* Tiers: structural (grammar clauses, conf 1.0) | usage_contextual (calls,
|
|
864
|
+
* field access, companion-apply idioms) | import_based | reference_qualified
|
|
865
|
+
* | inferred (sub-1.0 resolutions). Applied centrally at end of analyze()
|
|
866
|
+
* so no emission site can forget it.
|
|
867
|
+
*/
|
|
868
|
+
classifyEvidenceTier(relationships) {
|
|
869
|
+
for (const r of relationships) {
|
|
870
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
871
|
+
if (md.evidenceClass) continue;
|
|
872
|
+
const t = String(r.type ?? r.relationshipType ?? "");
|
|
873
|
+
const conf = typeof r.confidence === "number" ? r.confidence : 1;
|
|
874
|
+
if (t === "contains" || t === "exports") {
|
|
875
|
+
md.evidenceClass = "structural";
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (t === "imports") {
|
|
879
|
+
md.evidenceClass = "import_based";
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (t === "references") {
|
|
883
|
+
md.evidenceClass = md.isGroovyImport || md.isImport ? "import_based" : "reference_qualified";
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (t === "imports") {
|
|
887
|
+
md.evidenceClass = "import_based";
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (t === "extends" || t === "implements") {
|
|
891
|
+
md.evidenceClass = conf >= 1 ? "structural" : "inferred";
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (t === "overrides") {
|
|
895
|
+
md.evidenceClass = conf >= 1 ? "structural" : conf >= 0.85 ? "usage_contextual" : "inferred";
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (t === "instantiates") {
|
|
899
|
+
md.evidenceClass = md.isNew || conf >= 0.9 ? "structural" : "usage_contextual";
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (t === "decorates") {
|
|
903
|
+
md.evidenceClass = "structural";
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
md.evidenceClass = "usage_contextual";
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
detectGroovyFrameworks(content, symbols, imports) {
|
|
910
|
+
const frameworks = /* @__PURE__ */ new Set();
|
|
911
|
+
for (let i = 0; i < imports.length; i++) {
|
|
912
|
+
const mod = imports[i].module || "";
|
|
913
|
+
if (mod.startsWith("grails.") || mod.startsWith("org.grails.") || mod.startsWith("org.apache.grails.")) frameworks.add("grails");
|
|
914
|
+
else if (mod.startsWith("org.springframework.")) frameworks.add("spring");
|
|
915
|
+
else if (mod.startsWith("spock.lang.")) frameworks.add("spock");
|
|
916
|
+
else if (mod.startsWith("javax.persistence.") || mod.startsWith("jakarta.persistence.")) frameworks.add("jpa");
|
|
917
|
+
else if (mod.startsWith("org.hibernate.")) frameworks.add("hibernate");
|
|
918
|
+
else if (mod.startsWith("org.junit.") || mod.startsWith("org.spockframework.")) frameworks.add("junit");
|
|
919
|
+
}
|
|
920
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
921
|
+
const anns = symbols[i].metadata?.annotations ?? [];
|
|
922
|
+
for (let j = 0; j < anns.length; j++) {
|
|
923
|
+
const a = anns[j];
|
|
924
|
+
if (a === "Service" || a === "RestController" || a === "Controller" || a === "Repository" || a === "Component") frameworks.add("spring");
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
for (const priority of ["grails", "spring", "hibernate", "jpa", "spock", "junit"]) {
|
|
928
|
+
if (frameworks.has(priority)) return { framework: priority, detected: [...frameworks] };
|
|
929
|
+
}
|
|
930
|
+
return { framework: void 0, detected: [...frameworks] };
|
|
931
|
+
}
|
|
932
|
+
resolveOverrides(tasks, classHierarchy, symbolMap, relationships, companion) {
|
|
933
|
+
let suffixIndex = null;
|
|
934
|
+
for (let ti = 0; ti < tasks.length; ti++) {
|
|
935
|
+
const task = tasks[ti];
|
|
936
|
+
let targetFqn = null;
|
|
937
|
+
let confidence = 1;
|
|
938
|
+
let reasonCode = "exact-in-file";
|
|
939
|
+
if (!suffixIndex) {
|
|
940
|
+
suffixIndex = /* @__PURE__ */ new Map();
|
|
941
|
+
for (const fqn of classHierarchy.keys()) {
|
|
942
|
+
const bare = fqn.slice(fqn.lastIndexOf(".") + 1);
|
|
943
|
+
const arr = suffixIndex.get(bare);
|
|
944
|
+
if (arr) arr.push(fqn);
|
|
945
|
+
else suffixIndex.set(bare, [fqn]);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const queue = [];
|
|
949
|
+
const ownerH = classHierarchy.get(task.ownerFqn);
|
|
950
|
+
if (ownerH) {
|
|
951
|
+
for (let i = 0; i < ownerH.length; i++) queue.push(ownerH[i]);
|
|
952
|
+
}
|
|
953
|
+
const seen = /* @__PURE__ */ new Set();
|
|
954
|
+
let head = 0;
|
|
955
|
+
while (head < queue.length && !targetFqn) {
|
|
956
|
+
const parent = queue[head++];
|
|
957
|
+
if (seen.has(parent)) continue;
|
|
958
|
+
seen.add(parent);
|
|
959
|
+
const candidates = suffixIndex.get(parent) ?? [parent];
|
|
960
|
+
for (const cand of candidates) {
|
|
961
|
+
if (!classHierarchy.has(cand)) continue;
|
|
962
|
+
const ownerBare = cand.slice(cand.lastIndexOf(".") + 1);
|
|
963
|
+
const potential = `${ownerBare}.${task.methodName}${task.signature}`;
|
|
964
|
+
if (symbolMap.has(potential)) {
|
|
965
|
+
targetFqn = `${cand}.${task.methodName}${task.signature}`;
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (targetFqn) break;
|
|
970
|
+
for (const cand of candidates) {
|
|
971
|
+
const pH = classHierarchy.get(cand);
|
|
972
|
+
if (pH) {
|
|
973
|
+
for (let i = 0; i < pH.length; i++) queue.push(pH[i]);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (!targetFqn && task.explicit && companion && ownerH && ownerH.length > 0) {
|
|
978
|
+
const declaring = findDeclaringParent(
|
|
979
|
+
task.methodName,
|
|
980
|
+
ownerH,
|
|
981
|
+
companion,
|
|
982
|
+
(t) => classHierarchy.get(t) ?? []
|
|
983
|
+
);
|
|
984
|
+
if (declaring) {
|
|
985
|
+
targetFqn = `${declaring}.${task.methodName}${task.signature}`;
|
|
986
|
+
confidence = 0.85;
|
|
987
|
+
reasonCode = "companion-index";
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (!targetFqn && task.explicit && ownerH && ownerH.length > 0) {
|
|
991
|
+
targetFqn = `${ownerH[0]}.${task.methodName}${task.signature}`;
|
|
992
|
+
confidence = 0.5;
|
|
993
|
+
reasonCode = "first-parent-guess";
|
|
994
|
+
}
|
|
995
|
+
if (targetFqn) {
|
|
996
|
+
const localSource = localEdgeLabel(task.fqn);
|
|
997
|
+
const targetLabel = localEdgeLabel(targetFqn);
|
|
998
|
+
if (localSource === targetLabel) continue;
|
|
999
|
+
relationships.push({
|
|
1000
|
+
source: localSource,
|
|
1001
|
+
// THE TARGET IS THE DECLARED MEMBER, NOT `Owner.member` — this lane resolved 0 of 253 with
|
|
1002
|
+
// the dotted spelling, and the label helper it was using is the one the four-lane fix now
|
|
1003
|
+
// pairs with `localEdgeName`.
|
|
1004
|
+
target: localEdgeName(targetFqn),
|
|
1005
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
1006
|
+
confidence,
|
|
1007
|
+
// M59 (#825): the overriding declaration's line — captured while the
|
|
1008
|
+
// node was in hand, not resolved later by name (namesakes collide).
|
|
1009
|
+
...task.line ? { line_number: task.line } : {},
|
|
1010
|
+
metadata: {
|
|
1011
|
+
relationshipType: "method_override",
|
|
1012
|
+
isGroovyOverride: true,
|
|
1013
|
+
reasonCode,
|
|
1014
|
+
// THE OWNER IS CARRIED, not dropped — see the cpp site's note on why the four-lane fix
|
|
1015
|
+
// needed it and did not have it.
|
|
1016
|
+
overriddenOwner: targetLabel.includes(".") ? targetLabel.slice(0, targetLabel.lastIndexOf(".")) : ""
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Declared-type reference graph (M14). References edges from the enclosing
|
|
1024
|
+
* method/class to every named type in a declaration position. Collection
|
|
1025
|
+
* descends but stops at value boundaries (variable_declarator values,
|
|
1026
|
+
* closures, blocks) so initializer contents are never mistaken for the
|
|
1027
|
+
* declared type clause.
|
|
1028
|
+
*/
|
|
1029
|
+
collectDeclaredTypeRefs(rootNode, relationships) {
|
|
1030
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1031
|
+
const TYPE_STOP = /* @__PURE__ */ new Set(["variable_declarator", "closure", "block", "closure_expression"]);
|
|
1032
|
+
const isPlainTypeName = (t) => {
|
|
1033
|
+
if (t.length === 0) return false;
|
|
1034
|
+
for (let i = 0; i < t.length; i++) {
|
|
1035
|
+
const c = t.charCodeAt(i);
|
|
1036
|
+
const ok = c >= 97 && c <= 122 || // a-z
|
|
1037
|
+
c >= 65 && c <= 90 || // A-Z
|
|
1038
|
+
c >= 48 && c <= 57 || // 0-9
|
|
1039
|
+
c === 95 || // _
|
|
1040
|
+
c === 36 || // $ (a legal groovy/java identifier character)
|
|
1041
|
+
c === 46;
|
|
1042
|
+
if (!ok) return false;
|
|
1043
|
+
}
|
|
1044
|
+
return true;
|
|
1045
|
+
};
|
|
1046
|
+
const collectTypeNames = (n, out) => {
|
|
1047
|
+
if (n.type === "type_identifier") {
|
|
1048
|
+
if (n.text !== "def" && n.text !== "as" && isPlainTypeName(n.text) && !out.some((o) => o.name === n.text)) out.push({ name: n.text, node: n });
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (n.type === "record_pattern") {
|
|
1052
|
+
const t = n.namedChild(0);
|
|
1053
|
+
if (t && t.type === "identifier" && t.text !== "this" && !out.some((o) => o.name === t.text)) out.push({ name: t.text, node: t });
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (n.type === "type_arguments") {
|
|
1057
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1058
|
+
const c = n.namedChild(i);
|
|
1059
|
+
if (c) collectTypeNames(c, out);
|
|
1060
|
+
}
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (n.type === "scoped_type_identifier") {
|
|
1064
|
+
const text = n.text;
|
|
1065
|
+
if (!isPlainTypeName(text)) return;
|
|
1066
|
+
if (!out.some((o) => o.name === text)) out.push({ name: text, node: n });
|
|
1067
|
+
const args = this.findChild(n, (c) => c.type === "type_arguments");
|
|
1068
|
+
if (args) collectTypeNames(args, out);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (TYPE_STOP.has(n.type)) return;
|
|
1072
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1073
|
+
const c = n.namedChild(i);
|
|
1074
|
+
if (c) collectTypeNames(c, out);
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
const resolveSource = (start) => {
|
|
1078
|
+
let p = GROOVY_DECLARATION_TYPES.has(start.type) ? start : start.parent;
|
|
1079
|
+
while (p) {
|
|
1080
|
+
if (p.type === "method_declaration" || p.type === "constructor_declaration" || p.type === "function_definition") {
|
|
1081
|
+
const name = this.findChild(p, (c) => c.type === "identifier");
|
|
1082
|
+
if (name) return name.text;
|
|
1083
|
+
}
|
|
1084
|
+
if (p.type === "class_declaration" || p.type === "trait_declaration" || p.type === "enum_declaration") {
|
|
1085
|
+
const name = this.findChild(p, (c) => c.type === "identifier");
|
|
1086
|
+
if (name) return name.text;
|
|
1087
|
+
}
|
|
1088
|
+
p = p.parent;
|
|
1089
|
+
}
|
|
1090
|
+
return "(script)";
|
|
1091
|
+
};
|
|
1092
|
+
const emit = (decl, roots, kind, relType = "declared_type") => {
|
|
1093
|
+
const source = resolveSource(decl);
|
|
1094
|
+
for (const r of roots) {
|
|
1095
|
+
const names = [];
|
|
1096
|
+
collectTypeNames(r, names);
|
|
1097
|
+
for (const t of names) {
|
|
1098
|
+
const key = `${source}|${t.name}|${kind}`;
|
|
1099
|
+
if (emitted.has(key)) continue;
|
|
1100
|
+
emitted.add(key);
|
|
1101
|
+
relationships.push({
|
|
1102
|
+
source,
|
|
1103
|
+
target: t.name,
|
|
1104
|
+
type: ASTEngine.RelationshipType.References,
|
|
1105
|
+
confidence: 0.95,
|
|
1106
|
+
// M59 (#825): the line the TYPE is named on. A wrapped generic
|
|
1107
|
+
// (`List<\n Map<String, Int>>`) opens on an earlier line than its
|
|
1108
|
+
// arguments, so the enclosing type expression's line is wrong for
|
|
1109
|
+
// every nested name (scala's measured defect class).
|
|
1110
|
+
...this.lineField(t.node, decl),
|
|
1111
|
+
metadata: { relationshipType: relType, declaredOn: decl.type }
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1117
|
+
const walk = (n) => {
|
|
1118
|
+
if (visited.has(n.id)) return;
|
|
1119
|
+
visited.add(n.id);
|
|
1120
|
+
switch (n.type) {
|
|
1121
|
+
case "binary_expression": {
|
|
1122
|
+
const asType = n.childForFieldName("type");
|
|
1123
|
+
if (asType && n.childForFieldName("left")) {
|
|
1124
|
+
emit(n, [asType], "as_coercion", "cast_type");
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
case "field_declaration":
|
|
1130
|
+
case "formal_parameter":
|
|
1131
|
+
case "local_variable_declaration": {
|
|
1132
|
+
if (n.type === "local_variable_declaration") {
|
|
1133
|
+
let asKw = null;
|
|
1134
|
+
let asDecl = null;
|
|
1135
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1136
|
+
const c = n.namedChild(i);
|
|
1137
|
+
if (!c) continue;
|
|
1138
|
+
if ((c.type === "type_identifier" || c.type === "identifier") && c.text === "as") asKw = c;
|
|
1139
|
+
else if (c.type === "variable_declarator") asDecl = c;
|
|
1140
|
+
}
|
|
1141
|
+
if (asKw && asDecl) {
|
|
1142
|
+
const names = [];
|
|
1143
|
+
const tstack = [asDecl];
|
|
1144
|
+
while (tstack.length > 0) {
|
|
1145
|
+
const m = tstack.pop();
|
|
1146
|
+
if (m.type === "type_identifier" || m.type === "identifier") {
|
|
1147
|
+
if (m.text !== "as" && m.text !== "def" && !names.some((o) => o.name === m.text)) names.push({ name: m.text, node: m });
|
|
1148
|
+
} else {
|
|
1149
|
+
for (let i = 0; i < m.namedChildCount; i++) {
|
|
1150
|
+
const c = m.namedChild(i);
|
|
1151
|
+
if (c) tstack.push(c);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const source = resolveSource(n);
|
|
1156
|
+
for (const t of names) {
|
|
1157
|
+
const key = `${source}|${t.name}|as_cast`;
|
|
1158
|
+
if (emitted.has(key)) continue;
|
|
1159
|
+
emitted.add(key);
|
|
1160
|
+
relationships.push({
|
|
1161
|
+
source,
|
|
1162
|
+
target: t.name,
|
|
1163
|
+
type: ASTEngine.RelationshipType.References,
|
|
1164
|
+
confidence: 0.95,
|
|
1165
|
+
...this.lineField(t.node, n),
|
|
1166
|
+
metadata: { relationshipType: "cast_type", declaredOn: "as_cast" }
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const roots = [];
|
|
1173
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1174
|
+
const c = n.namedChild(i);
|
|
1175
|
+
if (!c || TYPE_STOP.has(c.type)) continue;
|
|
1176
|
+
if (c.type.endsWith("type") || c.type === "type_identifier" || c.type === "scoped_type_identifier") roots.push(c);
|
|
1177
|
+
}
|
|
1178
|
+
if (roots.length) emit(n, roots, n.type);
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
case "throws": {
|
|
1182
|
+
const kids = [];
|
|
1183
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1184
|
+
const c = n.namedChild(i);
|
|
1185
|
+
if (c) kids.push(c);
|
|
1186
|
+
}
|
|
1187
|
+
emit(n, kids, "throws", "throws_type");
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
case "catch_formal_parameter": {
|
|
1191
|
+
const catchType = this.findChild(n, (c) => c.type === "catch_type");
|
|
1192
|
+
if (catchType) emit(n, [catchType], "catch_formal_parameter", "declared_type");
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
case "instanceof_expression": {
|
|
1196
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1197
|
+
const c = n.namedChild(i);
|
|
1198
|
+
if (c) emit(n, [c], n.type, "instanceof_expression");
|
|
1199
|
+
}
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
case "cast_expression": {
|
|
1203
|
+
const first = n.namedChild(0);
|
|
1204
|
+
if (first) emit(n, [first], n.type, "cast_type");
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
case "switch_label": {
|
|
1208
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1209
|
+
const c = n.namedChild(i);
|
|
1210
|
+
if (c && c.type === "pattern") emit(n, [c], "switch_pattern", "case_pattern");
|
|
1211
|
+
}
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
case "method_reference": {
|
|
1215
|
+
const first = n.namedChild(0);
|
|
1216
|
+
if (first && first.type === "identifier" && first.text !== "this") {
|
|
1217
|
+
const source = resolveSource(n);
|
|
1218
|
+
const key = `${source}|${first.text}|method_reference`;
|
|
1219
|
+
if (!emitted.has(key)) {
|
|
1220
|
+
emitted.add(key);
|
|
1221
|
+
relationships.push({
|
|
1222
|
+
source,
|
|
1223
|
+
target: first.text,
|
|
1224
|
+
type: ASTEngine.RelationshipType.References,
|
|
1225
|
+
confidence: 0.9,
|
|
1226
|
+
...this.lineField(first, n),
|
|
1227
|
+
metadata: { relationshipType: "method_reference" }
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
case "juxt_function_call": {
|
|
1234
|
+
const first = n.namedChild(0);
|
|
1235
|
+
if (first && first.text === "as") {
|
|
1236
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1237
|
+
const c = n.namedChild(i);
|
|
1238
|
+
if (c && c.type === "argument_list") emit(n, [c], "as_expression", "cast_type");
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
case "method_declaration": {
|
|
1244
|
+
const roots = [];
|
|
1245
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1246
|
+
const c = n.namedChild(i);
|
|
1247
|
+
if (!c) continue;
|
|
1248
|
+
if (c.type === "identifier") break;
|
|
1249
|
+
if (c.type === "modifiers") continue;
|
|
1250
|
+
if (c.type.endsWith("_type") || c.type === "type_identifier" || c.type === "scoped_type_identifier") roots.push(c);
|
|
1251
|
+
else break;
|
|
1252
|
+
}
|
|
1253
|
+
if (roots.length) emit(n, roots, "return_type", "return_type");
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1258
|
+
const c = n.namedChild(i);
|
|
1259
|
+
if (c) walk(c);
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
walk(rootNode);
|
|
1263
|
+
}
|
|
1264
|
+
collectFieldAccessUses(rootNode, relationships, symbols) {
|
|
1265
|
+
const stack = [
|
|
1266
|
+
{ node: rootNode, owner: null, method: null }
|
|
1267
|
+
];
|
|
1268
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1269
|
+
const boundCache = /* @__PURE__ */ new Map();
|
|
1270
|
+
const boundNamesOf = (scope) => {
|
|
1271
|
+
const cached = boundCache.get(scope.id);
|
|
1272
|
+
if (cached) return cached;
|
|
1273
|
+
const out = /* @__PURE__ */ new Set();
|
|
1274
|
+
const push = (n) => {
|
|
1275
|
+
if (n && n.type === "identifier") out.add(n.text);
|
|
1276
|
+
};
|
|
1277
|
+
const work = [];
|
|
1278
|
+
for (let i = 0; i < scope.namedChildCount; i++) {
|
|
1279
|
+
const c = scope.namedChild(i);
|
|
1280
|
+
if (c) work.push(c);
|
|
1281
|
+
}
|
|
1282
|
+
while (work.length > 0) {
|
|
1283
|
+
const n = work.pop();
|
|
1284
|
+
if (n.type === "formal_parameter" || n.type === "catch_formal_parameter") push(n.childForFieldName("name"));
|
|
1285
|
+
else if (n.type === "variable_declarator") push(n.childForFieldName("name"));
|
|
1286
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1287
|
+
const c = n.namedChild(i);
|
|
1288
|
+
if (c) work.push(c);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (scope.type === "closure") out.add("it");
|
|
1292
|
+
boundCache.set(scope.id, out);
|
|
1293
|
+
return out;
|
|
1294
|
+
};
|
|
1295
|
+
const locallyBoundNames = (n) => {
|
|
1296
|
+
const acc = /* @__PURE__ */ new Set();
|
|
1297
|
+
let p = n.parent;
|
|
1298
|
+
let guard = 0;
|
|
1299
|
+
while (p && guard++ < 24) {
|
|
1300
|
+
if (p.type === "method_declaration" || p.type === "constructor_declaration" || p.type === "function_definition" || p.type === "lambda_expression" || p.type === "closure" || p.type === "program") {
|
|
1301
|
+
for (const t of boundNamesOf(p)) acc.add(t);
|
|
1302
|
+
}
|
|
1303
|
+
if (p.type === "class_declaration" || p.type === "interface_declaration" || p.type === "trait_declaration" || p.type === "enum_declaration") break;
|
|
1304
|
+
p = p.parent;
|
|
1305
|
+
}
|
|
1306
|
+
return acc;
|
|
1307
|
+
};
|
|
1308
|
+
while (stack.length > 0) {
|
|
1309
|
+
const item = stack.pop();
|
|
1310
|
+
const { node, owner, method } = item;
|
|
1311
|
+
if (node.type === "class_declaration" || node.type === "interface_declaration" || node.type === "trait_declaration" || node.type === "enum_declaration") {
|
|
1312
|
+
const nameNode = this.findChild(node, (n) => n.type === "identifier");
|
|
1313
|
+
const nextOwner = nameNode ? nameNode.text : owner;
|
|
1314
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
1315
|
+
const child = node.namedChild(i);
|
|
1316
|
+
if (child) stack.push({ node: child, owner: nextOwner, method });
|
|
1317
|
+
}
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
let nextMethod = method;
|
|
1321
|
+
if (node.type === "method_declaration" || node.type === "constructor_declaration" || node.type === "function_definition") {
|
|
1322
|
+
const nameNode = this.findChild(node, (n) => n.type === "identifier");
|
|
1323
|
+
const declaredName = nameNode?.text?.trim() ? nameNode.text : void 0;
|
|
1324
|
+
const isRealCtor = node.type === "constructor_declaration" && (owner === void 0 || declaredName === owner);
|
|
1325
|
+
nextMethod = isRealCtor ? "constructor" : declaredName ?? method;
|
|
1326
|
+
}
|
|
1327
|
+
if (node.type === "field_access") {
|
|
1328
|
+
const parts = node.text.split(".").map((p) => p.endsWith("?") || p.endsWith("*") ? p.slice(0, -1) : p);
|
|
1329
|
+
if (parts.length >= 2) {
|
|
1330
|
+
const first = this.fieldAccessReceiverName(node);
|
|
1331
|
+
const readOwner = method ?? owner;
|
|
1332
|
+
const source = first === "this" || first === "super" ? owner ?? first : readOwner ?? first ?? owner ?? "(script)";
|
|
1333
|
+
const pType = node.parent?.type;
|
|
1334
|
+
let isWrite = false;
|
|
1335
|
+
if (pType === "assignment_expression") {
|
|
1336
|
+
const lhs = node.parent.namedChild(0);
|
|
1337
|
+
isWrite = !!lhs && lhs.startIndex === node.startIndex && lhs.endIndex === node.endIndex;
|
|
1338
|
+
}
|
|
1339
|
+
const parentField = node.parent?.type === "juxt_function_call" ? node.parent.childForFieldName("name") : null;
|
|
1340
|
+
const isCalleePosition = !!parentField && parentField.startIndex === node.startIndex && parentField.endIndex === node.endIndex;
|
|
1341
|
+
const inConstructorBody = method === "constructor";
|
|
1342
|
+
const thisLhsInConstructor = inConstructorBody && isWrite && (first === "this" || first === "super");
|
|
1343
|
+
const leafNode = node.childForFieldName("field");
|
|
1344
|
+
const leafName = leafNode ? leafNode.type === "string_literal" || leafNode.type === "character_literal" ? this.stripQuotes(leafNode.text) : leafNode.text : parts[parts.length - 1];
|
|
1345
|
+
const key = `${source}.${leafName}`;
|
|
1346
|
+
if ((!isWrite || thisLhsInConstructor) && !isCalleePosition && !seen.has(key)) {
|
|
1347
|
+
seen.add(key);
|
|
1348
|
+
relationships.push({
|
|
1349
|
+
source,
|
|
1350
|
+
target: leafName,
|
|
1351
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1352
|
+
// 0.6 for an ordinary read, but 1.0 for a `this.x = …` in a constructor. The 0.6 is
|
|
1353
|
+
// a real claim-strength discount for an INFERRED read; this edge is not inferred.
|
|
1354
|
+
// `this.<field>` names a member of the enclosing type by construction, the type is
|
|
1355
|
+
// known statically, and the ground truth cites the exact source line for all four such
|
|
1356
|
+
// rows (User->id, User->name, Robot->name, Counter->count). At 0.6 the harness
|
|
1357
|
+
// classified the observed fact as state "unknown" -- anything below 0.7 is
|
|
1358
|
+
// emitted-but-not-claimed (script/m12 relationshipFacts) -- while the truth row is
|
|
1359
|
+
// "resolved", so a correct edge scored as a false NEGATIVE.
|
|
1360
|
+
confidence: thisLhsInConstructor ? 1 : 0.6,
|
|
1361
|
+
// M59 (#825): the `field` field of the access node names the
|
|
1362
|
+
// member — measured populated in the pinned grammar
|
|
1363
|
+
// (`repo.items` -> [identifier, identifier], field=identifier),
|
|
1364
|
+
// so a multi-line receiver chain does not drag the read onto the
|
|
1365
|
+
// receiver's line.
|
|
1366
|
+
...this.lineField(node.childForFieldName("field"), node),
|
|
1367
|
+
metadata: { isFieldAccess: true }
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (node.type === "array_access") {
|
|
1373
|
+
const parent = node.parent;
|
|
1374
|
+
const lhs = parent && parent.type === "assignment_expression" ? parent.namedChild(0) : null;
|
|
1375
|
+
const isWrite = !!lhs && lhs.startIndex === node.startIndex && lhs.endIndex === node.endIndex;
|
|
1376
|
+
const first = node.namedChild(0);
|
|
1377
|
+
if (!isWrite && first && first.type === "identifier") {
|
|
1378
|
+
const source = method ?? owner ?? first.text;
|
|
1379
|
+
const key = `arr:${source}.${first.text}`;
|
|
1380
|
+
if (!seen.has(key)) {
|
|
1381
|
+
seen.add(key);
|
|
1382
|
+
relationships.push({
|
|
1383
|
+
source,
|
|
1384
|
+
target: first.text,
|
|
1385
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1386
|
+
confidence: 0.6,
|
|
1387
|
+
...this.lineField(first, node),
|
|
1388
|
+
metadata: { isArrayAccess: true }
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (node.type === "interpolated_identifier" || node.type === "string_interpolation") {
|
|
1394
|
+
const source = method ?? owner;
|
|
1395
|
+
const candidates = [];
|
|
1396
|
+
if (node.type === "interpolated_identifier") {
|
|
1397
|
+
const parentType = node.parent?.type ?? "";
|
|
1398
|
+
if (parentType === "juxt_function_call" || parentType === "field_access") continue;
|
|
1399
|
+
candidates.push(node);
|
|
1400
|
+
} else {
|
|
1401
|
+
const collect = (n) => {
|
|
1402
|
+
if (n.type === "identifier") {
|
|
1403
|
+
const pt = n.parent?.type ?? "";
|
|
1404
|
+
if (!INTERPOLATION_OWNED_BY_MEMBER_WALK.has(pt)) candidates.push(n);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1408
|
+
const c = n.namedChild(i);
|
|
1409
|
+
if (c) collect(c);
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1413
|
+
const c = node.namedChild(i);
|
|
1414
|
+
if (c) collect(c);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
for (const n of candidates) {
|
|
1418
|
+
const name = n.text.startsWith("$") ? n.text.slice(1) : n.text;
|
|
1419
|
+
if (!name || !source) continue;
|
|
1420
|
+
if (locallyBoundNames(n).has(name)) continue;
|
|
1421
|
+
const key = `interp:${source}.${name}`;
|
|
1422
|
+
if (seen.has(key)) continue;
|
|
1423
|
+
seen.add(key);
|
|
1424
|
+
relationships.push({
|
|
1425
|
+
source,
|
|
1426
|
+
target: name,
|
|
1427
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1428
|
+
confidence: 0.9,
|
|
1429
|
+
...this.lineField(n, n),
|
|
1430
|
+
metadata: { isInterpolationRead: true }
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
1435
|
+
const child = node.namedChild(i);
|
|
1436
|
+
if (child) stack.push({ node: child, owner, method: nextMethod });
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* M60 ontology pass: the NAMEABLE receiver of a `field_access`, or `undefined`
|
|
1442
|
+
* when the receiver is an expression rather than a name.
|
|
1443
|
+
*
|
|
1444
|
+
* Nameable means the receiver's own text is an identifier the reader can look up:
|
|
1445
|
+
* `a` -> "a" `a.b` -> "a" `a.b.c` -> "a"
|
|
1446
|
+
* `this` -> "this" `super` -> "super" `i?.j` -> "i"
|
|
1447
|
+
* `k*.l` -> "k" (safe-nav and spread receivers are plain identifiers)
|
|
1448
|
+
*
|
|
1449
|
+
* NOT nameable, and deliberately `undefined` rather than a text fragment:
|
|
1450
|
+
* `arr[0].f` receiver is an `array_access` -> undefined
|
|
1451
|
+
* `new File(x).g` receiver is an `object_creation_expression`
|
|
1452
|
+
* `foo().h` receiver is a `method_invocation`
|
|
1453
|
+
* `(m).n` receiver is a `parenthesized_expression`
|
|
1454
|
+
*
|
|
1455
|
+
* Returning the raw text for those is what produced the measured defect: the
|
|
1456
|
+
* `node.text.split(".")[0]` this replaces yielded `arr[0]`, `new File(x` and
|
|
1457
|
+
* `(m)` as relationship SOURCES — strings no symbol carries and no consumer can
|
|
1458
|
+
* resolve. `undefined` is the honest answer, and the call site then applies the
|
|
1459
|
+
* rule the walk already used for an unresolvable receiver: attribute the read to
|
|
1460
|
+
* the enclosing method.
|
|
1461
|
+
*
|
|
1462
|
+
* Probe-verified node shapes (`object`/`field` are populated for every one,
|
|
1463
|
+
* including `?.` and `*.`): script/m14/tmp/gv-probe6.mts, and the corpus count is
|
|
1464
|
+
* `script/m60/groovy-r7-census.ts`'s `malformedNameEndpoints`.
|
|
1465
|
+
*/
|
|
1466
|
+
fieldAccessReceiverName(node) {
|
|
1467
|
+
let cur = node.childForFieldName("object");
|
|
1468
|
+
while (cur) {
|
|
1469
|
+
if (cur.type === "identifier" || cur.type === "this" || cur.type === "super") return cur.text;
|
|
1470
|
+
if (cur.type === "field_access") {
|
|
1471
|
+
cur = cur.childForFieldName("object");
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
return void 0;
|
|
1475
|
+
}
|
|
1476
|
+
return void 0;
|
|
1477
|
+
}
|
|
1478
|
+
isGroovyBuiltin(call) {
|
|
1479
|
+
if (GROOVY_BUILTINS.has(call)) return true;
|
|
1480
|
+
const firstDot = call.indexOf(".");
|
|
1481
|
+
const root = firstDot > 0 ? call.slice(0, firstDot) : call;
|
|
1482
|
+
const STDLIB_ROOTS = /* @__PURE__ */ new Set([
|
|
1483
|
+
"System",
|
|
1484
|
+
"String",
|
|
1485
|
+
"Integer",
|
|
1486
|
+
"Long",
|
|
1487
|
+
"Double",
|
|
1488
|
+
"Float",
|
|
1489
|
+
"Boolean",
|
|
1490
|
+
"Byte",
|
|
1491
|
+
"Short",
|
|
1492
|
+
"Character",
|
|
1493
|
+
"Math",
|
|
1494
|
+
"StrictMath",
|
|
1495
|
+
"Object",
|
|
1496
|
+
"Class",
|
|
1497
|
+
"Thread",
|
|
1498
|
+
"Runnable",
|
|
1499
|
+
"Runtime",
|
|
1500
|
+
"Process",
|
|
1501
|
+
"ProcessBuilder",
|
|
1502
|
+
"Throwable",
|
|
1503
|
+
"Error",
|
|
1504
|
+
"Exception",
|
|
1505
|
+
"StackTraceElement",
|
|
1506
|
+
"Enum",
|
|
1507
|
+
"Void",
|
|
1508
|
+
"Compiler",
|
|
1509
|
+
"Package",
|
|
1510
|
+
"ArrayList",
|
|
1511
|
+
"LinkedList",
|
|
1512
|
+
"HashMap",
|
|
1513
|
+
"HashSet",
|
|
1514
|
+
"TreeMap",
|
|
1515
|
+
"TreeSet",
|
|
1516
|
+
"LinkedHashMap",
|
|
1517
|
+
"LinkedHashSet",
|
|
1518
|
+
"ArrayDeque",
|
|
1519
|
+
"PriorityQueue",
|
|
1520
|
+
"Collections",
|
|
1521
|
+
"Arrays",
|
|
1522
|
+
"Objects",
|
|
1523
|
+
"Optional",
|
|
1524
|
+
"Comparator",
|
|
1525
|
+
"List",
|
|
1526
|
+
"Map",
|
|
1527
|
+
"Set",
|
|
1528
|
+
"Collection",
|
|
1529
|
+
"Iterator",
|
|
1530
|
+
"Iterable",
|
|
1531
|
+
"Calendar",
|
|
1532
|
+
"Date",
|
|
1533
|
+
"UUID",
|
|
1534
|
+
"Locale",
|
|
1535
|
+
"Currency",
|
|
1536
|
+
"File",
|
|
1537
|
+
"Path",
|
|
1538
|
+
"Paths",
|
|
1539
|
+
"Files",
|
|
1540
|
+
"BigDecimal",
|
|
1541
|
+
"BigInteger",
|
|
1542
|
+
"AtomicInteger",
|
|
1543
|
+
"AtomicLong",
|
|
1544
|
+
"StringBuilder",
|
|
1545
|
+
"StringBuffer",
|
|
1546
|
+
"Pattern",
|
|
1547
|
+
"Matcher",
|
|
1548
|
+
"Random",
|
|
1549
|
+
"Logger",
|
|
1550
|
+
"Level",
|
|
1551
|
+
"OutputStream",
|
|
1552
|
+
"InputStream",
|
|
1553
|
+
"Reader",
|
|
1554
|
+
"Writer",
|
|
1555
|
+
"URL",
|
|
1556
|
+
"URI",
|
|
1557
|
+
"Socket",
|
|
1558
|
+
"ServerSocket"
|
|
1559
|
+
]);
|
|
1560
|
+
if (STDLIB_ROOTS.has(root)) return true;
|
|
1561
|
+
return false;
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Groovydoc extraction: block_comment starting with '/**' immediately
|
|
1565
|
+
* preceding the declaration. Previously groovy symbols carried NO
|
|
1566
|
+
* comments (M14 #383).
|
|
1567
|
+
*/
|
|
1568
|
+
/**
|
|
1569
|
+
* Per-method cyclomatic complexity (1 + decision points) - java/kotlin
|
|
1570
|
+
* parity (M14 #301 prerequisite). Previously always absent.
|
|
1571
|
+
*/
|
|
1572
|
+
buildGroovyQualifiedCall(objectNode, methodName) {
|
|
1573
|
+
if (!objectNode) return methodName;
|
|
1574
|
+
if (objectNode.type === "method_invocation") {
|
|
1575
|
+
const innerName = objectNode.childForFieldName("name");
|
|
1576
|
+
const innerObject = objectNode.childForFieldName("object");
|
|
1577
|
+
const base = this.buildGroovyQualifiedCall(innerObject, innerName?.text ?? "");
|
|
1578
|
+
return `${base}.${methodName}`;
|
|
1579
|
+
}
|
|
1580
|
+
return `${objectNode.text}.${methodName}`;
|
|
1581
|
+
}
|
|
1582
|
+
calculateGroovyMethodComplexity(node) {
|
|
1583
|
+
let complexity = 1;
|
|
1584
|
+
const stack = [node];
|
|
1585
|
+
while (stack.length > 0) {
|
|
1586
|
+
const n = stack.pop();
|
|
1587
|
+
if (n.type === "if_statement" || n.type === "for_statement" || n.type === "enhanced_for_statement" || // for-in loops parsed as this, not for_statement
|
|
1588
|
+
n.type === "while_statement" || n.type === "do_statement" || // do-while (NOT "do_while")
|
|
1589
|
+
n.type === "switch_block_statement_group" || // per-case granularity (family contract)
|
|
1590
|
+
n.type === "catch_clause" || // NOT "catch_block"
|
|
1591
|
+
n.type === "ternary_expression") {
|
|
1592
|
+
complexity++;
|
|
1593
|
+
} else if (n.type === "binary_expression") {
|
|
1594
|
+
const op = n.childForFieldName("operator")?.text;
|
|
1595
|
+
if (op === "&&" || op === "||" || op === "?:") complexity++;
|
|
1596
|
+
}
|
|
1597
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1598
|
+
const child = n.namedChild(i);
|
|
1599
|
+
if (child) stack.push(child);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return complexity;
|
|
1603
|
+
}
|
|
1604
|
+
extractGroovyDoc(node) {
|
|
1605
|
+
const parent = node.parent;
|
|
1606
|
+
if (!parent) return void 0;
|
|
1607
|
+
let selfIdx = -1;
|
|
1608
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
1609
|
+
const c = parent.namedChild(i);
|
|
1610
|
+
if (c && c.startIndex === node.startIndex && c.endIndex === node.endIndex) {
|
|
1611
|
+
selfIdx = i;
|
|
1612
|
+
break;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
for (let i = selfIdx - 1; i >= 0; i--) {
|
|
1616
|
+
const sibling = parent.namedChild(i);
|
|
1617
|
+
if (!sibling) continue;
|
|
1618
|
+
if (sibling.type === "block_comment" && sibling.text.startsWith("/**")) return sibling.text;
|
|
1619
|
+
break;
|
|
1620
|
+
}
|
|
1621
|
+
return void 0;
|
|
1622
|
+
}
|
|
1623
|
+
extractInheritance(node, type) {
|
|
1624
|
+
const parents = [];
|
|
1625
|
+
if (type === "class_declaration" || type === "trait_declaration") {
|
|
1626
|
+
const superClass = this.findChild(node, (n) => n.type === "superclass");
|
|
1627
|
+
if (superClass) {
|
|
1628
|
+
const sc = this.findChild(superClass, (n) => n.type === "type_identifier" || n.type === "identifier" || n.type === "generic_type" || n.type === "scoped_type_identifier");
|
|
1629
|
+
if (sc) parents.push(this.baseTypeName(sc));
|
|
1630
|
+
}
|
|
1631
|
+
} else if (type === "interface_declaration") {
|
|
1632
|
+
const ext = this.findChild(node, (n) => n.type === "extends_interfaces");
|
|
1633
|
+
if (ext) {
|
|
1634
|
+
for (const tid of this.collectTypeNodes(ext)) {
|
|
1635
|
+
parents.push(this.baseTypeName(tid));
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
const superInterfaces = this.findChild(node, (n) => n.type === "super_interfaces");
|
|
1640
|
+
if (superInterfaces) {
|
|
1641
|
+
for (const tid of this.collectTypeNodes(superInterfaces)) {
|
|
1642
|
+
parents.push(this.baseTypeName(tid));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return parents;
|
|
1646
|
+
}
|
|
1647
|
+
/** Collect type nodes within a type_list/super_interfaces/etc (DFS).
|
|
1648
|
+
* scoped_type_identifier is accepted AS a complete type and NOT descended
|
|
1649
|
+
* into — its nested type_identifiers are package/class segments, never
|
|
1650
|
+
* independent types (collecting them fabricated garbage edges). */
|
|
1651
|
+
collectTypeNodes(node) {
|
|
1652
|
+
const out = [];
|
|
1653
|
+
const stack = [node];
|
|
1654
|
+
while (stack.length > 0) {
|
|
1655
|
+
const n = stack.pop();
|
|
1656
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1657
|
+
const c = n.namedChild(i);
|
|
1658
|
+
if (!c) continue;
|
|
1659
|
+
if (c.type === "type_identifier" || c.type === "generic_type" || c.type === "scoped_type_identifier") {
|
|
1660
|
+
out.push(c);
|
|
1661
|
+
} else {
|
|
1662
|
+
stack.push(c);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return out;
|
|
1667
|
+
}
|
|
1668
|
+
stripQuotes(text) {
|
|
1669
|
+
if (text.length >= 2) {
|
|
1670
|
+
const first = text[0];
|
|
1671
|
+
const last = text[text.length - 1];
|
|
1672
|
+
if (first === '"' && last === '"' || first === "'" && last === "'") {
|
|
1673
|
+
return text.slice(1, -1);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return text;
|
|
1677
|
+
}
|
|
1678
|
+
baseTypeName(node) {
|
|
1679
|
+
if (node.type === "generic_type") {
|
|
1680
|
+
const id = this.findChild(node, (n) => n.type === "type_identifier" || n.type === "identifier");
|
|
1681
|
+
return id ? id.text : node.text;
|
|
1682
|
+
}
|
|
1683
|
+
return node.text;
|
|
1684
|
+
}
|
|
1685
|
+
extractModifiers(node) {
|
|
1686
|
+
const mods = [];
|
|
1687
|
+
const modifiers = this.findChild(node, (n) => n.type === "modifiers");
|
|
1688
|
+
if (modifiers) {
|
|
1689
|
+
for (let i = 0; i < modifiers.childCount; i++) {
|
|
1690
|
+
const c = modifiers.child(i);
|
|
1691
|
+
if (!c || !c.isNamed) {
|
|
1692
|
+
if (c) mods.push(c.text);
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (c.type === "annotation" || c.type === "marker_annotation") continue;
|
|
1696
|
+
mods.push(c.text);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return mods;
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Generic type-parameter extraction from type_parameters nodes.
|
|
1703
|
+
* Previously groovy symbols carried NO generic information (#383 fix).
|
|
1704
|
+
*/
|
|
1705
|
+
extractGroovyTypeParameters(node) {
|
|
1706
|
+
const tpNode = node.namedChildren.find((c) => c.type === "type_parameters");
|
|
1707
|
+
if (!tpNode) return [];
|
|
1708
|
+
const out = [];
|
|
1709
|
+
for (let i = 0; i < tpNode.namedChildCount; i++) {
|
|
1710
|
+
const c = tpNode.namedChild(i);
|
|
1711
|
+
if (!c) continue;
|
|
1712
|
+
if (c.type.includes("identifier")) {
|
|
1713
|
+
out.push(c.text.trim());
|
|
1714
|
+
} else if (c.type === "type_parameter") {
|
|
1715
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
1716
|
+
const gc = c.namedChild(j);
|
|
1717
|
+
if (gc && gc.type.includes("identifier")) {
|
|
1718
|
+
out.push(gc.text.trim());
|
|
1719
|
+
break;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return out;
|
|
1725
|
+
}
|
|
1726
|
+
extractVisibility(node) {
|
|
1727
|
+
const mods = this.extractModifiers(node);
|
|
1728
|
+
if (mods.indexOf("private") !== -1) return "private";
|
|
1729
|
+
if (mods.indexOf("protected") !== -1) return "protected";
|
|
1730
|
+
return "public";
|
|
1731
|
+
}
|
|
1732
|
+
extractAnnotations(node) {
|
|
1733
|
+
return this.extractAnnotationRefs(node).map((a) => a.name);
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* THE ANNOTATIONS, WITH THE LINE EACH IS WRITTEN ON — 2026-10-22.
|
|
1737
|
+
*
|
|
1738
|
+
* The `decorates` edge is the family shape for "a decorator is applied to a
|
|
1739
|
+
* declaration" (src/ast/types.ts: the direction is the DECORATED DECLARATION
|
|
1740
|
+
* → THE DECORATOR, and the family's emitters are javascript
|
|
1741
|
+
* `src/ast/real-javascript-analyzer.ts:3003` and python
|
|
1742
|
+
* `src/ast/real-python-analyzer.ts:6112`):
|
|
1743
|
+
*
|
|
1744
|
+
* source = the decorated declaration's name
|
|
1745
|
+
* target = the decorator's NAME LEAF
|
|
1746
|
+
* line_number = the DECORATOR's own line
|
|
1747
|
+
* metadata = { isDecorator: true, decoratorLine }
|
|
1748
|
+
*
|
|
1749
|
+
* ⇒ SO THE NAME IS THE LEAF, NOT THE WRITTEN PATH: the same rule this
|
|
1750
|
+
* method already applies to the symbol's `decorators` list (the last dotted
|
|
1751
|
+
* segment), and the same rule the lane's own fixture pins
|
|
1752
|
+
* (groovy/fq-annotations: `expectDecorators` says the class `FqService`
|
|
1753
|
+
* "contains" `Service` for `@Service`, and the short-name normalizer is what
|
|
1754
|
+
* the fixture's own rationale records: "The short-name normalizer (last
|
|
1755
|
+
* dotted segment) must feed both"). ⇒ The EDGE and the SYMBOL LIST therefore
|
|
1756
|
+
* agree, which is what lets a consumer join them.
|
|
1757
|
+
*
|
|
1758
|
+
* ⇒ AND THE LINE IS THE ANNOTATION'S OWN LINE, not the declaration's: the
|
|
1759
|
+
* decorator is spelled there, and the family's emitters put the decorator's
|
|
1760
|
+
* line on the row (javascript: `decoratorLine`).
|
|
1761
|
+
*/
|
|
1762
|
+
extractAnnotationRefs(node) {
|
|
1763
|
+
const anns = [];
|
|
1764
|
+
const modifiers = this.findChild(node, (n) => n.type === "modifiers");
|
|
1765
|
+
if (modifiers) {
|
|
1766
|
+
for (let i = 0; i < modifiers.namedChildCount; i++) {
|
|
1767
|
+
const c = modifiers.namedChild(i);
|
|
1768
|
+
if (c && (c.type === "annotation" || c.type === "marker_annotation")) {
|
|
1769
|
+
const nameNode = this.findChild(c, (n) => n.type === "identifier" || n.type === "scoped_identifier");
|
|
1770
|
+
if (nameNode) {
|
|
1771
|
+
const text = nameNode.text;
|
|
1772
|
+
const dot = text.lastIndexOf(".");
|
|
1773
|
+
anns.push({
|
|
1774
|
+
name: dot === -1 ? text : text.slice(dot + 1),
|
|
1775
|
+
line: nameNode.startPosition.row + 1
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return anns;
|
|
1782
|
+
}
|
|
1783
|
+
buildFqn(currentPackage, scopeStack, name) {
|
|
1784
|
+
if (scopeStack.length > 0) {
|
|
1785
|
+
return `${scopeStack[scopeStack.length - 1].fqn}.${name}`;
|
|
1786
|
+
}
|
|
1787
|
+
return currentPackage ? `${currentPackage}.${name}` : name;
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* #841 round 14: does this juxt_function_call carry the typed-closure-
|
|
1791
|
+
* parameter misparse shape? Measured on the pinned grammar, not assumed:
|
|
1792
|
+
* `{ Test task -> … }` → name=Test, args="task"
|
|
1793
|
+
* `{ String key, Object value → }` → name=String, args="key, Object"
|
|
1794
|
+
* `{ Map<String,File> m -> … }` → name=Map, args="<String,File> m" (generic)
|
|
1795
|
+
*
|
|
1796
|
+
* The decisive evidence is the SOURCE, measured over the whole groovy corpus
|
|
1797
|
+
* (962 such juxt nodes, every one verified): what follows the node is at most
|
|
1798
|
+
* further parameter names (the multi-param node stops before the last name —
|
|
1799
|
+
* `String key, Object` then ` value`), commas, dots, generics, brackets and
|
|
1800
|
+
* whitespace, and then the closure arrow `->`. A REAL juxtaposed call is
|
|
1801
|
+
* never followed by an arrow — the arrow can only make the preceding text a
|
|
1802
|
+
* closure parameter list (Groovy has no other `->`). The tree compositions
|
|
1803
|
+
* (lambda sibling, arrow sibling, first child of closure/block) are the
|
|
1804
|
+
* fallback when no source text is supplied — the same 962 sites fall into
|
|
1805
|
+
* exactly those three compositions, with 139 of them ending before the last
|
|
1806
|
+
* parameter name (which is why a sibling-only test under-covers by ~5×: it
|
|
1807
|
+
* suppressed 115 of the 608 fabricated edges; the source test covers the
|
|
1808
|
+
* rest).
|
|
1809
|
+
*/
|
|
1810
|
+
/** The JS `\s` set, as a char test — kept exact so the char walks below are
|
|
1811
|
+
* behaviour-identical to the regexes they replace (M49.R2: the AST regex
|
|
1812
|
+
* census counted those three literals as review-class debt). */
|
|
1813
|
+
static isRegexSpace(c, code) {
|
|
1814
|
+
if (c === " " || c === " " || c === "\n" || c === "\r" || c === "\v" || c === "\f") return true;
|
|
1815
|
+
return code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
|
|
1816
|
+
}
|
|
1817
|
+
/** `/^[\s]*$/` as a char walk. */
|
|
1818
|
+
static isWhitespaceOnly(text) {
|
|
1819
|
+
for (let i = 0; i < text.length; i++) {
|
|
1820
|
+
if (!_RealGroovyAnalyzer.isRegexSpace(text[i], text.charCodeAt(i))) return false;
|
|
1821
|
+
}
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* `/^[A-Z][A-Za-z0-9_]*(\.[A-Z][A-Za-z0-9_]*)*$/` as a char walk: a
|
|
1826
|
+
* type-cased name, optionally dotted, every segment starting uppercase and
|
|
1827
|
+
* otherwise word characters. Lowercase juxt callees (`echo`, `sh`) never
|
|
1828
|
+
* match, so ordinary Jenkins-style juxtaposition is untouched.
|
|
1829
|
+
*/
|
|
1830
|
+
static isTypeCasedDottedName(text) {
|
|
1831
|
+
if (text.length === 0) return false;
|
|
1832
|
+
let i = 0;
|
|
1833
|
+
while (i < text.length) {
|
|
1834
|
+
const first = text[i];
|
|
1835
|
+
if (!(first >= "A" && first <= "Z")) return false;
|
|
1836
|
+
i++;
|
|
1837
|
+
while (i < text.length && text[i] !== ".") {
|
|
1838
|
+
const c = text[i];
|
|
1839
|
+
if (!(c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c >= "0" && c <= "9" || c === "_")) return false;
|
|
1840
|
+
i++;
|
|
1841
|
+
}
|
|
1842
|
+
if (i < text.length) {
|
|
1843
|
+
i++;
|
|
1844
|
+
if (i >= text.length) return false;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
return true;
|
|
1848
|
+
}
|
|
1849
|
+
/** Identifier start character (the regexes' `[A-Za-z_]`). */
|
|
1850
|
+
static isIdentStart(c) {
|
|
1851
|
+
return c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c === "_";
|
|
1852
|
+
}
|
|
1853
|
+
/** Identifier continuation character (the regexes' `[A-Za-z0-9_]`). */
|
|
1854
|
+
static isWordChar(c) {
|
|
1855
|
+
return c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c >= "0" && c <= "9" || c === "_";
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* `/^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*`-style list as a char
|
|
1859
|
+
* walk: comma-separated items, each an identifier optionally dotted, with
|
|
1860
|
+
* optional whitespace around the commas only (`Type name`, `a.b.C`,
|
|
1861
|
+
* `x, y.Z`). Mirrors the regex exactly — leading and trailing whitespace are
|
|
1862
|
+
* not accepted (the call site trims first), and a trailing comma fails.
|
|
1863
|
+
*/
|
|
1864
|
+
static isDottedIdentifierList(text) {
|
|
1865
|
+
let i = 0;
|
|
1866
|
+
const spaced = () => {
|
|
1867
|
+
while (i < text.length && _RealGroovyAnalyzer.isRegexSpace(text[i], text.charCodeAt(i))) i++;
|
|
1868
|
+
};
|
|
1869
|
+
const dotted = () => {
|
|
1870
|
+
if (!_RealGroovyAnalyzer.isIdentStart(text[i])) return false;
|
|
1871
|
+
i++;
|
|
1872
|
+
while (i < text.length && _RealGroovyAnalyzer.isWordChar(text[i])) i++;
|
|
1873
|
+
while (i < text.length && text[i] === ".") {
|
|
1874
|
+
i++;
|
|
1875
|
+
if (!_RealGroovyAnalyzer.isIdentStart(text[i])) return false;
|
|
1876
|
+
i++;
|
|
1877
|
+
while (i < text.length && _RealGroovyAnalyzer.isWordChar(text[i])) i++;
|
|
1878
|
+
}
|
|
1879
|
+
return true;
|
|
1880
|
+
};
|
|
1881
|
+
if (!dotted()) return false;
|
|
1882
|
+
while (i < text.length) {
|
|
1883
|
+
const save = i;
|
|
1884
|
+
spaced();
|
|
1885
|
+
if (i >= text.length) {
|
|
1886
|
+
i = save;
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
if (text[i] !== ",") return false;
|
|
1890
|
+
i++;
|
|
1891
|
+
spaced();
|
|
1892
|
+
if (!dotted()) return false;
|
|
1893
|
+
}
|
|
1894
|
+
return i >= text.length;
|
|
1895
|
+
}
|
|
1896
|
+
/** `/^[\s,.<>\[\]A-Za-z0-9_]*$/` as a char walk: parameter-list material only. */
|
|
1897
|
+
static isParamMaterialOnly(text) {
|
|
1898
|
+
for (let i = 0; i < text.length; i++) {
|
|
1899
|
+
const c = text[i];
|
|
1900
|
+
if (_RealGroovyAnalyzer.isRegexSpace(c, text.charCodeAt(i))) continue;
|
|
1901
|
+
if (c === "," || c === "." || c === "<" || c === ">" || c === "[" || c === "]") continue;
|
|
1902
|
+
if (c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c >= "0" && c <= "9" || c === "_") continue;
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
return true;
|
|
1906
|
+
}
|
|
1907
|
+
looksLikeTypedClosureMisparse(juxtNode, nameNode, content) {
|
|
1908
|
+
const nameText = nameNode.text.trim();
|
|
1909
|
+
if (!_RealGroovyAnalyzer.isTypeCasedDottedName(nameText)) return false;
|
|
1910
|
+
const argsNode = juxtNode.childForFieldName("args");
|
|
1911
|
+
if (!argsNode) return false;
|
|
1912
|
+
if (!_RealGroovyAnalyzer.isDottedIdentifierList(argsNode.text.trim())) return false;
|
|
1913
|
+
if (content != null) {
|
|
1914
|
+
const tail = content.slice(juxtNode.endIndex, juxtNode.endIndex + 160);
|
|
1915
|
+
const arrow = tail.indexOf("->");
|
|
1916
|
+
if (arrow >= 0) {
|
|
1917
|
+
const between = tail.slice(0, arrow);
|
|
1918
|
+
if (_RealGroovyAnalyzer.isParamMaterialOnly(between)) return true;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
const parent = juxtNode.parent;
|
|
1922
|
+
if (!parent) return false;
|
|
1923
|
+
if (juxtNode.nextSibling?.type === "->") return true;
|
|
1924
|
+
const siblings = [];
|
|
1925
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
1926
|
+
const c = parent.namedChild(i);
|
|
1927
|
+
if (c) siblings.push(c);
|
|
1928
|
+
}
|
|
1929
|
+
const idx = siblings.findIndex((s) => s.id === juxtNode.id);
|
|
1930
|
+
if (idx === -1) return false;
|
|
1931
|
+
const next = siblings[idx + 1];
|
|
1932
|
+
if (next != null && next.type === "lambda_expression") return true;
|
|
1933
|
+
if (idx === 0 && (parent.type === "closure" || parent.type === "block")) return true;
|
|
1934
|
+
return false;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* #841 round 14 (analyzer-side recovery): when a typed closure parameter
|
|
1938
|
+
* list collides inside a chained call, tree-sitter's recovery emits the
|
|
1939
|
+
* class as ROOT-LEVEL fragments — the `class` keyword token, the name
|
|
1940
|
+
* `identifier`, the `super_interfaces` node, and the member declarations
|
|
1941
|
+
* as siblings — instead of a class_declaration node. The walk therefore
|
|
1942
|
+
* never pushes a class scope, and the file loses its class symbol, its
|
|
1943
|
+
* implements edges, and every contains edge: methods extract but ride
|
|
1944
|
+
* ownerless/"(script)" (measured: 0 classes and 0 contains edges on
|
|
1945
|
+
* 160–470-line build-logic plugin files). The grammar fix for the
|
|
1946
|
+
* underlying construct is closed off by measurement (§7f–§7q), so this
|
|
1947
|
+
* pass reconstructs the ownership graph AFTER the walk:
|
|
1948
|
+
* 1. the class symbol, from the root `class` token + name fragment;
|
|
1949
|
+
* 2. implements edges + classHierarchy from the surviving
|
|
1950
|
+
* super_interfaces fragment;
|
|
1951
|
+
* 3. ownerName/ownerKind on the surviving member symbols inside the
|
|
1952
|
+
* body region (the existing contains loop then emits class → member);
|
|
1953
|
+
* 4. method symbols synthesized for degraded method headers (modifiers +
|
|
1954
|
+
* type + identifier + formal_parameters + `{` fragments) so the
|
|
1955
|
+
* methods whose wrapper was dropped rejoin the graph;
|
|
1956
|
+
* 5. "(script)"-sourced calls/uses edges inside the region re-parented
|
|
1957
|
+
* to the class.
|
|
1958
|
+
* Runs only on trees with a root-level ERROR and the class fragment
|
|
1959
|
+
* shape, so clean trees cannot enter (the shapes exist only where the
|
|
1960
|
+
* parser failed). Parse errors are NOT suppressed — the tree keeps its
|
|
1961
|
+
* ERROR nodes; only the ownership graph is restored.
|
|
1962
|
+
*/
|
|
1963
|
+
recoverDegradedClassShapes(rootNode, symbols, relationships, symbolMap, currentPackage) {
|
|
1964
|
+
if (!rootNode.hasError) return;
|
|
1965
|
+
const allKids = rootNode.children;
|
|
1966
|
+
const kids = [];
|
|
1967
|
+
for (const c of allKids) {
|
|
1968
|
+
if (c) kids.push(c);
|
|
1969
|
+
}
|
|
1970
|
+
if (!kids.some((k) => k.type === "ERROR")) return;
|
|
1971
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1972
|
+
if (kids[i].type !== "class") continue;
|
|
1973
|
+
const nameNode = kids[i + 1];
|
|
1974
|
+
if (!nameNode || nameNode.type !== "identifier") continue;
|
|
1975
|
+
const name = nameNode.text;
|
|
1976
|
+
const classToken = kids[i];
|
|
1977
|
+
const startLine = classToken.startPosition.row + 1;
|
|
1978
|
+
let j = i + 2;
|
|
1979
|
+
const headerFragments = [];
|
|
1980
|
+
while (j < kids.length && kids[j].type !== "{") {
|
|
1981
|
+
headerFragments.push(kids[j]);
|
|
1982
|
+
j++;
|
|
1983
|
+
}
|
|
1984
|
+
if (j >= kids.length) continue;
|
|
1985
|
+
const openBrace = kids[j];
|
|
1986
|
+
let closeIdx = -1;
|
|
1987
|
+
for (let k = kids.length - 1; k > j; k--) {
|
|
1988
|
+
if (kids[k].type === "}") {
|
|
1989
|
+
closeIdx = k;
|
|
1990
|
+
break;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const endLine = closeIdx >= 0 ? kids[closeIdx].endPosition.row + 1 : rootNode.endPosition.row + 1;
|
|
1994
|
+
const bodyChildren = kids.slice(j + 1, closeIdx >= 0 ? closeIdx : kids.length);
|
|
1995
|
+
if (symbols.some((s) => s.name === name && s.range?.startLine === startLine)) continue;
|
|
1996
|
+
const typeFragments = /* @__PURE__ */ new Set(["generic_type", "type_identifier", "scoped_type_identifier", "void_type", "integral_type", "floating_point_type", "boolean_type", "array_type"]);
|
|
1997
|
+
const memberKinds = /* @__PURE__ */ new Set([ASTEngine.SymbolKind.Method, ASTEngine.SymbolKind.Constructor, ASTEngine.SymbolKind.Property]);
|
|
1998
|
+
const members = symbols.filter((s) => !s.ownerName && !s.metadata?.isAnonymousClass && memberKinds.has(s.kind) && s.range?.startLine != null && s.range.startLine >= startLine && s.range.startLine <= endLine);
|
|
1999
|
+
const hasFieldDecl = bodyChildren.some((k) => k.type === "field_declaration");
|
|
2000
|
+
let hasDegradedHeader = false;
|
|
2001
|
+
for (let b = 0; b < bodyChildren.length - 1 && !hasDegradedHeader; b++) {
|
|
2002
|
+
let p = b;
|
|
2003
|
+
if (bodyChildren[b].type === "modifiers") p = b + 1;
|
|
2004
|
+
const typeFrag = bodyChildren[p];
|
|
2005
|
+
if (!typeFrag || !typeFragments.has(typeFrag.type)) continue;
|
|
2006
|
+
const nameFrag = bodyChildren[p + 1];
|
|
2007
|
+
if (!nameFrag || nameFrag.type !== "identifier") continue;
|
|
2008
|
+
const fp = bodyChildren[p + 2];
|
|
2009
|
+
const brace = bodyChildren[p + 3];
|
|
2010
|
+
if (fp && fp.type === "formal_parameters" && brace && brace.type === "{") hasDegradedHeader = true;
|
|
2011
|
+
}
|
|
2012
|
+
if (members.length === 0 && !hasDegradedHeader && !hasFieldDecl) continue;
|
|
2013
|
+
let visibility = "public";
|
|
2014
|
+
let annotations = [];
|
|
2015
|
+
const before = kids[i - 1];
|
|
2016
|
+
if (before && before.type === "modifiers") {
|
|
2017
|
+
for (let m = 0; m < before.namedChildCount; m++) {
|
|
2018
|
+
const mc = before.namedChild(m);
|
|
2019
|
+
if (!mc) continue;
|
|
2020
|
+
const t = mc.text ?? "";
|
|
2021
|
+
if (t === "private") visibility = "private";
|
|
2022
|
+
else if (t === "protected") visibility = "protected";
|
|
2023
|
+
if (mc.type === "annotation" || mc.type === "marker_annotation") {
|
|
2024
|
+
const annName = this.findChild(mc, (n) => n.type === "identifier" || n.type === "scoped_identifier");
|
|
2025
|
+
if (annName) {
|
|
2026
|
+
const text = annName.text;
|
|
2027
|
+
const dot = text.lastIndexOf(".");
|
|
2028
|
+
annotations.push(dot === -1 ? text : text.slice(dot + 1));
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
for (const frag of headerFragments) {
|
|
2034
|
+
if (frag.type !== "super_interfaces") continue;
|
|
2035
|
+
for (const tid of this.collectTypeNodes(frag)) {
|
|
2036
|
+
relationships.push({
|
|
2037
|
+
source: name,
|
|
2038
|
+
target: this.baseTypeName(tid),
|
|
2039
|
+
type: ASTEngine.RelationshipType.Implements,
|
|
2040
|
+
confidence: 1,
|
|
2041
|
+
...this.lineField(tid, frag),
|
|
2042
|
+
metadata: { relationshipType: "implements" }
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
const fqn = currentPackage ? `${currentPackage}.${name}` : name;
|
|
2047
|
+
const sym = {
|
|
2048
|
+
name,
|
|
2049
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
2050
|
+
comments: [],
|
|
2051
|
+
range: {
|
|
2052
|
+
// The measured body extent: from the opening brace (start overridden
|
|
2053
|
+
// to the class keyword's line, #825 rule) to the closing brace token
|
|
2054
|
+
// when one was found — NOT the whole root node (fragments are
|
|
2055
|
+
// root-level children, so the root's range is the entire file).
|
|
2056
|
+
...this.nodeToRange(closeIdx >= 0 ? kids[closeIdx] : openBrace.parent ?? rootNode),
|
|
2057
|
+
startLine,
|
|
2058
|
+
startColumn: classToken.startPosition.column + 1
|
|
2059
|
+
},
|
|
2060
|
+
visibility,
|
|
2061
|
+
decorators: annotations.length > 0 ? annotations : void 0,
|
|
2062
|
+
metadata: { nodeType: "class_declaration_recovered", annotations }
|
|
2063
|
+
};
|
|
2064
|
+
symbols.push(sym);
|
|
2065
|
+
symbolMap.set(fqn, sym);
|
|
2066
|
+
symbolMap.set(name, sym);
|
|
2067
|
+
for (const s of members) {
|
|
2068
|
+
s.ownerName = name;
|
|
2069
|
+
s.ownerKind = ASTEngine.SymbolKind.Class;
|
|
2070
|
+
if (!s.visibility) s.visibility = "public";
|
|
2071
|
+
}
|
|
2072
|
+
for (let b = 0; b < bodyChildren.length - 1; b++) {
|
|
2073
|
+
let p = b;
|
|
2074
|
+
const mods = bodyChildren[b].type === "modifiers" ? bodyChildren[b] : null;
|
|
2075
|
+
if (mods) p = b + 1;
|
|
2076
|
+
const typeFrag = bodyChildren[p];
|
|
2077
|
+
if (!typeFrag || !typeFragments.has(typeFrag.type)) continue;
|
|
2078
|
+
const nameFrag = bodyChildren[p + 1];
|
|
2079
|
+
if (!nameFrag || nameFrag.type !== "identifier") continue;
|
|
2080
|
+
const mName = nameFrag.text;
|
|
2081
|
+
const mLine = nameFrag.startPosition.row + 1;
|
|
2082
|
+
if (symbols.some((s) => s.name === mName && s.range?.startLine === mLine)) continue;
|
|
2083
|
+
let q = p + 2;
|
|
2084
|
+
let paramNode = null;
|
|
2085
|
+
const fp = bodyChildren[q];
|
|
2086
|
+
if (fp && fp.type === "formal_parameters") {
|
|
2087
|
+
paramNode = fp;
|
|
2088
|
+
q++;
|
|
2089
|
+
}
|
|
2090
|
+
const bodyBrace = bodyChildren[q];
|
|
2091
|
+
if (!bodyBrace || bodyBrace.type !== "{") continue;
|
|
2092
|
+
const parameters = [];
|
|
2093
|
+
if (paramNode) {
|
|
2094
|
+
for (let pc = 0; pc < paramNode.namedChildCount; pc++) {
|
|
2095
|
+
const par = paramNode.namedChild(pc);
|
|
2096
|
+
if (!par || par.type !== "formal_parameter" && par.type !== "spread_parameter") continue;
|
|
2097
|
+
const isSpread = par.type === "spread_parameter";
|
|
2098
|
+
let pName = this.findChild(par, (n) => n.type === "identifier");
|
|
2099
|
+
if (isSpread) {
|
|
2100
|
+
const decl = this.findChild(par, (n) => n.type === "variable_declarator");
|
|
2101
|
+
pName = decl ? this.findChild(decl, (n) => n.type === "identifier") : null;
|
|
2102
|
+
}
|
|
2103
|
+
const pType = this.findChild(par, (n) => n.type === "type_identifier" || n.type === "integral_type" || n.type === "floating_point_type" || n.type === "boolean_type" || n.type === "void_type" || n.type === "generic_type" || n.type === "array_type" || n.type === "scoped_type_identifier");
|
|
2104
|
+
parameters.push({
|
|
2105
|
+
name: pName?.text ?? "anonymous",
|
|
2106
|
+
type: pType ? this.baseTypeName(pType) + (isSpread ? "[]" : "") : void 0
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
let mVisibility = "public";
|
|
2111
|
+
if (mods) {
|
|
2112
|
+
for (let m2 = 0; m2 < mods.namedChildCount; m2++) {
|
|
2113
|
+
const mc2 = mods.namedChild(m2);
|
|
2114
|
+
const t2 = mc2?.text ?? "";
|
|
2115
|
+
if (t2 === "private") mVisibility = "private";
|
|
2116
|
+
else if (t2 === "protected") mVisibility = "protected";
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
symbols.push({
|
|
2120
|
+
name: mName,
|
|
2121
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
2122
|
+
comments: [],
|
|
2123
|
+
parameters: parameters.length > 0 ? parameters : void 0,
|
|
2124
|
+
returnType: typeFragments.has(typeFrag.type) ? this.baseTypeName(typeFrag) : void 0,
|
|
2125
|
+
range: {
|
|
2126
|
+
...this.nodeToRange(typeFrag),
|
|
2127
|
+
// The method is written on its NAME's line (#825 rule).
|
|
2128
|
+
startLine: mLine,
|
|
2129
|
+
startColumn: nameFrag.startPosition.column + 1
|
|
2130
|
+
},
|
|
2131
|
+
visibility: mVisibility,
|
|
2132
|
+
ownerName: name,
|
|
2133
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
2134
|
+
metadata: { nodeType: "method_declaration_recovered" }
|
|
2135
|
+
});
|
|
2136
|
+
b = q;
|
|
2137
|
+
}
|
|
2138
|
+
for (const rel of relationships) {
|
|
2139
|
+
if (rel.source !== "(script)") continue;
|
|
2140
|
+
if (rel.type !== ASTEngine.RelationshipType.Calls && rel.type !== ASTEngine.RelationshipType.Uses) continue;
|
|
2141
|
+
const line = rel.line_number;
|
|
2142
|
+
if (line == null || line < startLine || line > endLine) continue;
|
|
2143
|
+
rel.source = name;
|
|
2144
|
+
}
|
|
2145
|
+
break;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
findChild(node, predicate) {
|
|
2149
|
+
const count = node.namedChildCount;
|
|
2150
|
+
for (let i = 0; i < count; i++) {
|
|
2151
|
+
const child = node.namedChild(i);
|
|
2152
|
+
if (child && predicate(child)) return child;
|
|
2153
|
+
}
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
findAllChildren(node, predicate) {
|
|
2157
|
+
const out = [];
|
|
2158
|
+
const count = node.namedChildCount;
|
|
2159
|
+
for (let i = 0; i < count; i++) {
|
|
2160
|
+
const child = node.namedChild(i);
|
|
2161
|
+
if (child && predicate(child)) out.push(child);
|
|
2162
|
+
}
|
|
2163
|
+
return out;
|
|
2164
|
+
}
|
|
2165
|
+
nodeToRange(node) {
|
|
2166
|
+
return {
|
|
2167
|
+
startLine: node.startPosition.row + 1,
|
|
2168
|
+
// 1-based columns, consistent with java/kotlin/scala (M14 consistency gate)
|
|
2169
|
+
startColumn: node.startPosition.column + 1,
|
|
2170
|
+
endLine: node.endPosition.row + 1,
|
|
2171
|
+
endColumn: node.endPosition.column + 1,
|
|
2172
|
+
startIndex: node.startIndex,
|
|
2173
|
+
endIndex: node.endIndex
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* M59 (#825): the `line_number` field for a relationship, taken from the node
|
|
2178
|
+
* that NAMES the relationship's target — never from the construct that merely
|
|
2179
|
+
* contains it. Returns nothing at all when no node is available: #820/#821,
|
|
2180
|
+
* a stored 0 is a claim that the relationship occurs on line 0.
|
|
2181
|
+
*/
|
|
2182
|
+
lineField(nameNode, fallback) {
|
|
2183
|
+
const n = nameNode ?? fallback;
|
|
2184
|
+
return n ? { line_number: n.startPosition.row + 1 } : {};
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* M59 (#825): the node whose start is the DECLARATION's own line.
|
|
2188
|
+
*
|
|
2189
|
+
* An annotation is a modifier — `@Service` decorates the class, it is not
|
|
2190
|
+
* where the class is written. Measured on the pinned grammar, not assumed:
|
|
2191
|
+
* `class_declaration` for `@Service\nclass Svc` has children
|
|
2192
|
+
* `[modifiers@5, identifier@6, class_body@6]`, so the class symbol's line was
|
|
2193
|
+
* the ANNOTATION line. Same shape and same rule as java's
|
|
2194
|
+
* `declarationLineNode` and scala's `declarationLineNode` — one rule for the
|
|
2195
|
+
* whole JVM family rather than three dialects of it.
|
|
2196
|
+
*/
|
|
2197
|
+
/** True when the field node is the same declaration as a sibling method, parsed twice (M59 #825 follow-up: `static Sphere valueOf` + the `final Distance distance`/ctor split). Proof, not heuristic: two distinct same-line declarations must be `;`-separated, so a sibling method starting after the field with only whitespace between them is the same tokens parsed twice — the full-span method wins. */
|
|
2198
|
+
isDuplicateParseField(node, content) {
|
|
2199
|
+
const parent = node.parent;
|
|
2200
|
+
if (!parent) return false;
|
|
2201
|
+
for (let i = 0; i < parent.namedChildCount; i++) {
|
|
2202
|
+
const s = parent.namedChild(i);
|
|
2203
|
+
if (!s || s.id === node.id) continue;
|
|
2204
|
+
if (s.type !== "method_declaration" && s.type !== "constructor_declaration" && s.type !== "function_definition") continue;
|
|
2205
|
+
if (s.startPosition.row !== node.startPosition.row) continue;
|
|
2206
|
+
if (s.startIndex <= node.endIndex) continue;
|
|
2207
|
+
if (_RealGroovyAnalyzer.isWhitespaceOnly(content.slice(node.endIndex, s.startIndex))) return true;
|
|
2208
|
+
}
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
// A symbol is anchored at the DECLARATION, not at the annotation above it.
|
|
2212
|
+
//
|
|
2213
|
+
// THIS RULE WAS REVERSED ON 2026-10-22, and the reversal is measured, not
|
|
2214
|
+
// preferred. The old rule ("anchor at the FIRST ANNOTATION") was adopted
|
|
2215
|
+
// against a hand-audited truth file that records the annotation's line:
|
|
2216
|
+
// @Slf4j L18 / class Counter truth 18, was 19
|
|
2217
|
+
// @Override L16 / String format(…) truth 16, was 17
|
|
2218
|
+
// and it claimed the family convention — "the java analyzer anchors a symbol
|
|
2219
|
+
// at its annotation line via `annotationLine`, so groovy was the outlier".
|
|
2220
|
+
//
|
|
2221
|
+
// BOTH CLAIMS ARE MEASURED FALSE TODAY (script/m14/groovy-family-line-anchor-probe.ts,
|
|
2222
|
+
// the same annotated shape through all four JVM analyzers, the annotation on
|
|
2223
|
+
// the line BEFORE each declaration):
|
|
2224
|
+
//
|
|
2225
|
+
// java class@2 label@4 use@6 ← the DECLARATION's line
|
|
2226
|
+
// kotlin class@2 label@4 use@6 ← the DECLARATION's line
|
|
2227
|
+
// scala class@2 label@4 use@6 ← the DECLARATION's line
|
|
2228
|
+
// groovy class@1 label@4 use@5 ← the ANNOTATION's line
|
|
2229
|
+
//
|
|
2230
|
+
// java's `annotationLine` is not what java's SYMBOL range uses (java anchors
|
|
2231
|
+
// the symbol on the declaration and uses the annotation line for a
|
|
2232
|
+
// DIFFERENT edge), and the truth file the old rule cited is stale for the
|
|
2233
|
+
// whole JVM family — the same file is stale for the constructor name (see
|
|
2234
|
+
// the constructor comment) and `bun script/m12/verify-analyzer.ts java` /
|
|
2235
|
+
// `kotlin` report the constructor FN/FP pair at HEAD.
|
|
2236
|
+
//
|
|
2237
|
+
// THIS LANE'S OWN m14 FIXTURES record the DECLARATION's line and FORBID the
|
|
2238
|
+
// annotation's line, so the old anchor is pinned as the pre-fix shape:
|
|
2239
|
+
// groovy/lines-825 pins `contains[Wrapped->use] @26` and FORBIDS @25 (the
|
|
2240
|
+
// `@Deprecated` line), `contains[Wrapped->label] @23`, and the class at 19;
|
|
2241
|
+
// groovy/anonymous-body-overrides pins `overrides[anonymous_TypeCodec.bsonType->bsonType] @23`
|
|
2242
|
+
// and `contains[anonymous_TypeCodec->bsonType] @23` where the `@Override` is
|
|
2243
|
+
// on 22.
|
|
2244
|
+
//
|
|
2245
|
+
// A `modifiers` child is skipped in BOTH cases — annotation-carrying or not —
|
|
2246
|
+
// so `private String label` and `final int hex` stay on their own line, which
|
|
2247
|
+
// is what the already-correct property rows require.
|
|
2248
|
+
/** True when this declaration carries at least one annotation (directly or in `modifiers`). */
|
|
2249
|
+
declarationHasAnnotation(node) {
|
|
2250
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2251
|
+
const c = node.namedChild(i);
|
|
2252
|
+
if (!c) continue;
|
|
2253
|
+
if (c.type === "annotation" || c.type === "marker_annotation") return true;
|
|
2254
|
+
if (c.type === "modifiers" && this.findChild(c, (n) => n.type === "annotation" || n.type === "marker_annotation")) return true;
|
|
2255
|
+
}
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
declarationLineNode(node) {
|
|
2259
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2260
|
+
const c = node.namedChild(i);
|
|
2261
|
+
if (!c) continue;
|
|
2262
|
+
if (c.type === "modifiers") {
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
if (c.type === "annotation" || c.type === "marker_annotation") continue;
|
|
2266
|
+
if (c.type === "line_comment" || c.type === "block_comment") continue;
|
|
2267
|
+
return c;
|
|
2268
|
+
}
|
|
2269
|
+
return node;
|
|
2270
|
+
}
|
|
2271
|
+
/** The range of a SYMBOL (not of an import, an error or a raw node), anchored per `declarationLineNode`. */
|
|
2272
|
+
symbolRange(node, nameNode) {
|
|
2273
|
+
const decl = nameNode ?? this.declarationLineNode(node);
|
|
2274
|
+
const base = this.nodeToRange(node);
|
|
2275
|
+
return {
|
|
2276
|
+
...base,
|
|
2277
|
+
startLine: decl.startPosition.row + 1,
|
|
2278
|
+
startColumn: decl.startPosition.column + 1,
|
|
2279
|
+
startIndex: decl.startIndex
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
export {
|
|
2284
|
+
RealGroovyAnalyzer
|
|
2285
|
+
};
|