@backbencher/brain 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-Q5APTJQL.js +17 -0
- package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-22V37YTK.js +1758 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3ZTOHHSW.js +2104 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5BVAH26D.js +340 -0
- package/dist/chunk-62DHLYW6.js +3427 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-7DRPNVIN.js +782 -0
- package/dist/chunk-7MBYT777.js +174 -0
- package/dist/chunk-AHDWGM7Q.js +236 -0
- package/dist/chunk-B5GEAAYF.js +818 -0
- package/dist/chunk-BEIZZ73B.js +1479 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DWKXZ6IF.js +336 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-ENW4BTBA.js +930 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-EY5ZZUUA.js +626 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HJEWO5EV.js +247 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NO3U3FJQ.js +201 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-QKTRECYS.js +1119 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SGGCSHAX.js +12050 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UQNQZXHY.js +711 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-WIG36E67.js +196 -0
- package/dist/chunk-XHK3WWGB.js +7436 -0
- package/dist/chunk-XTQM2IP3.js +2863 -0
- package/dist/chunk-ZE2LG76D.js +1296 -0
- package/dist/chunk-ZETTGPAW.js +6950 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-7XIP555Q.js +593 -0
- package/dist/database-76RP44JG.js +9 -0
- package/dist/doctor-ANGETYMN.js +395 -0
- package/dist/edit-engine-CG72LDZD.js +51 -0
- package/dist/engine-BKEOGN7N.js +11 -0
- package/dist/engine-VIW6NSKO.js +18 -0
- package/dist/fingerprint-OBLIMZLP.js +33 -0
- package/dist/framework-intelligence-3LORTPOX.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-HBSQNPTB.js +21 -0
- package/dist/graph_request-Q6NKB6N2.js +350 -0
- package/dist/impact_request-LYP672WE.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-7OVGYSGW.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-FK2XKZXY.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-6XPENVAA.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-ZJ3DVUGO.js +355 -0
- package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
- package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
- package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
- package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
- package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
- package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
- package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
- package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
- package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
- package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
- package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
- package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
- package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
- package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
- package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
- package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
- package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
- package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
- package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
- package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
- package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
- package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
- package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
- package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
- package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
- package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
- package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
- package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
- package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
- package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
- package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
- package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
- package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
- package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
- package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
- package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
- package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
- package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
- package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
- package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
- package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
- package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
- package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
- package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
- package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
- package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
- package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
- package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
- package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
- package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
- package/dist/regex-search-NJKHMZNO.js +239 -0
- package/dist/rules-ACHPB7QU.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-NT5ZHYNE.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-GZJIM2J2.js +16 -0
- package/dist/taint-KXUHHPIE.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-OJDC35DI.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-Q57YMMBX.js +32 -0
- package/dist/type-inference-QMT2RMLV.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-ABP2TUL2.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,4126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASTEngine
|
|
3
|
+
} from "./chunk-3ZTOHHSW.js";
|
|
4
|
+
import "./chunk-BQ3OVQ3P.js";
|
|
5
|
+
import "./chunk-SK6YVHFZ.js";
|
|
6
|
+
import "./chunk-2BSKD7O3.js";
|
|
7
|
+
import "./chunk-KAET47ZL.js";
|
|
8
|
+
import "./chunk-KCDECRJY.js";
|
|
9
|
+
import "./chunk-MLKGABMK.js";
|
|
10
|
+
|
|
11
|
+
// src/ast/real-go-analyzer.ts
|
|
12
|
+
function stringIncludes(str, val) {
|
|
13
|
+
if (!str) return false;
|
|
14
|
+
return str.indexOf(val) !== -1;
|
|
15
|
+
}
|
|
16
|
+
function inArray(arr, val) {
|
|
17
|
+
if (!arr) return false;
|
|
18
|
+
for (let i = 0; i < arr.length; i++) {
|
|
19
|
+
if (arr[i] === val) return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function filterSymbols(symbols, predicate) {
|
|
24
|
+
const result = [];
|
|
25
|
+
if (!symbols) return result;
|
|
26
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
27
|
+
if (predicate(symbols[i])) result.push(symbols[i]);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function mapSymbols(arr, mapper) {
|
|
32
|
+
const result = [];
|
|
33
|
+
if (!arr) return result;
|
|
34
|
+
for (let i = 0; i < arr.length; i++) {
|
|
35
|
+
result.push(mapper(arr[i]));
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function someSymbol(arr, predicate) {
|
|
40
|
+
if (!arr) return false;
|
|
41
|
+
for (let i = 0; i < arr.length; i++) {
|
|
42
|
+
if (predicate(arr[i])) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
function findSymbol(arr, predicate) {
|
|
47
|
+
if (!arr) return void 0;
|
|
48
|
+
for (let i = 0; i < arr.length; i++) {
|
|
49
|
+
if (predicate(arr[i])) return arr[i];
|
|
50
|
+
}
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
function forEachSymbol(arr, fn) {
|
|
54
|
+
if (!arr) return;
|
|
55
|
+
for (let i = 0; i < arr.length; i++) {
|
|
56
|
+
fn(arr[i]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function getObjectKeysLength(obj) {
|
|
60
|
+
if (!obj) return 0;
|
|
61
|
+
let count = 0;
|
|
62
|
+
for (const key in obj) {
|
|
63
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
64
|
+
count++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return count;
|
|
68
|
+
}
|
|
69
|
+
var GO_SUB_UNIT_REASON = {
|
|
70
|
+
// The callee's LEAF name is recorded; the receiver or package that owns it is
|
|
71
|
+
// not, and a leaf is a per-file-analyzer limit rather than a property of the
|
|
72
|
+
// call. Measured endpoint resolution: 19.4% of `calls` targets bind to a
|
|
73
|
+
// symbol in the same file.
|
|
74
|
+
"calls|go_function_call": "callee_recorded_as_a_leaf_name_whose_owner_is_not_resolved_per_file",
|
|
75
|
+
"calls|go_package_scope_call": "callee_recorded_as_a_leaf_name_whose_owner_is_not_resolved_per_file",
|
|
76
|
+
// A selector read names the FIELD leaf. Whether the receiver is the declared
|
|
77
|
+
// type, an embedded one, or an interface value is not resolved here.
|
|
78
|
+
"uses|go_selector_use": "field_leaf_read_whose_receiver_type_is_not_resolved_per_file",
|
|
79
|
+
// Channel send/receive records the channel's leaf name; the direction is in
|
|
80
|
+
// `operationType` and the element type is not read.
|
|
81
|
+
"uses|go_channel_operation": "channel_leaf_read_without_its_element_type",
|
|
82
|
+
// A declared/return type position records the type's leaf. Measured: 32.0% of
|
|
83
|
+
// `references` targets bind within the file.
|
|
84
|
+
"references|declared_type": "declared_type_leaf_whose_declaring_file_is_out_of_scope",
|
|
85
|
+
"references|return_type": "declared_type_leaf_whose_declaring_file_is_out_of_scope",
|
|
86
|
+
// `struct { E }` promotes E's method set. Go has no inheritance declaration,
|
|
87
|
+
// so the edge records a promotion that the compiler resolves, not an
|
|
88
|
+
// extends-clause the author wrote. THE STORED TYPE IS `mixes_in` because that
|
|
89
|
+
// same fact IS the mixin contract — composition of E's IMPLEMENTATION into
|
|
90
|
+
// T's method set, no single base, no inheritance clause. The
|
|
91
|
+
// `go_struct_embedding` construct id is KEPT: metadata identifies the
|
|
92
|
+
// construct, so the label move is 1:1 and this reason still states the fact.
|
|
93
|
+
"mixes_in|go_struct_embedding": "promotion_by_embedding_not_a_declared_supertype",
|
|
94
|
+
"extends|go_interface_embedding": "interface_method_set_inclusion_not_a_declared_supertype",
|
|
95
|
+
// The target is an import PATH (`net/http`, `encoding/json`), which names a
|
|
96
|
+
// module and not any symbol this analyzer can bind.
|
|
97
|
+
"imports|*": "target_is_a_module_path_not_a_symbol",
|
|
98
|
+
// A composite literal's type is a real declared type, read from the tree.
|
|
99
|
+
"instantiates|go_composite_literal_construction": "type_read_from_a_composite_literal_whose_declaration_may_be_in_another_file",
|
|
100
|
+
// PascalCase naming is Go's constructor CONVENTION with no declaration behind
|
|
101
|
+
// it. Measured false-positive class: `instantiates bitSizeError -> Itoa` at
|
|
102
|
+
// src/strconv/number.go:285 is `strconv.Itoa`, a formatting function whose
|
|
103
|
+
// name merely starts upper-case.
|
|
104
|
+
"instantiates|go_instantiation": "pascal_case_name_is_a_constructor_convention_with_no_declaration",
|
|
105
|
+
"instantiates|go_package_scope_instantiation": "pascal_case_name_is_a_constructor_convention_with_no_declaration",
|
|
106
|
+
// ── THE BIND REASONS — the same eleven facts, asked of a second question.
|
|
107
|
+
//
|
|
108
|
+
// The table above answers "WHY IS THIS EDGE BELOW 1.0". The D7 wall needs a
|
|
109
|
+
// different answer: "WHY DID THE ARBITER NOT BIND THIS EDGE's TARGET" — and
|
|
110
|
+
// those are NOT the same question, because an edge can be at confidence 1.0
|
|
111
|
+
// (the analyzer is certain the edge is real) and still have a target the
|
|
112
|
+
// arbiter cannot resolve to a declaration.
|
|
113
|
+
//
|
|
114
|
+
// MEASURED on the gql-gqlgen chunk (888 files, the gate's own run at the
|
|
115
|
+
// pre-claim analyzer): 3,820 wall rows carried NO reasonCode, and EVERY ONE
|
|
116
|
+
// OF THEM WAS confidence 1.0 — so the reasonCode publication loop, which
|
|
117
|
+
// skips `conf >= 1.0`, CANNOT reach them, and a reasonCode stamped there
|
|
118
|
+
// would break this analyzer's own invariant (a confidence-1.0 row must carry
|
|
119
|
+
// no code — see the census at script/m14/go9-census.ts).
|
|
120
|
+
//
|
|
121
|
+
// SO THE BIND REASON IS A SECOND FIELD, `metadata.bindReason`, published on
|
|
122
|
+
// every edge whose target is a REFERENCE to something rather than the
|
|
123
|
+
// declaration itself. `contains` is excluded: its target IS the declaration
|
|
124
|
+
// it contains, so there is no binding question to answer.
|
|
125
|
+
//
|
|
126
|
+
// The two fields share this table on purpose. One vocabulary, two questions.
|
|
127
|
+
// A per-type `*` fallback makes the publication COMPLETE BY CONSTRUCTION:
|
|
128
|
+
// no edge of a covered type can escape a named class, which is the property
|
|
129
|
+
// the D7 census needs.
|
|
130
|
+
//
|
|
131
|
+
// A used identifier is a plain name; the file that declares it is not read.
|
|
132
|
+
"uses|identifier_usage": "used_identifier_recorded_as_a_leaf_whose_declaring_file_is_out_of_scope",
|
|
133
|
+
// A goroutine's call and a deferred call are calls; their owner is not
|
|
134
|
+
// resolved per file, exactly as for a direct call.
|
|
135
|
+
"uses|go_goroutine": "goroutine_call_recorded_as_a_leaf_name_whose_owner_is_not_resolved_per_file",
|
|
136
|
+
"uses|go_defer": "deferred_call_recorded_as_a_leaf_name_whose_owner_is_not_resolved_per_file",
|
|
137
|
+
// An exported name is the package's own; the edge records the name alone, so
|
|
138
|
+
// which of the project's same-named exports it is, is not carried.
|
|
139
|
+
"exports|go_exported_identifier": "exported_name_recorded_as_a_leaf_whose_owning_package_is_not_the_target",
|
|
140
|
+
// An implemented interface is named by its leaf; the file that declares it is
|
|
141
|
+
// not read here.
|
|
142
|
+
"implements|go_interface_implementation": "implemented_interface_recorded_as_a_leaf_whose_declaring_file_is_out_of_scope",
|
|
143
|
+
// A promoted method is recorded with its qualified owner (`Type.Method`);
|
|
144
|
+
// the file declaring that type is not read.
|
|
145
|
+
"overrides|go_method_promotion_shadowing": "promoted_method_recorded_with_its_qualified_owner_whose_declaring_file_is_out_of_scope",
|
|
146
|
+
// A qualified type reference names a package and a type; the package's own
|
|
147
|
+
// file is not carried on the edge.
|
|
148
|
+
"references|reference_qualified": "qualified_type_reference_whose_declaring_file_is_out_of_scope",
|
|
149
|
+
// ── THE PER-TYPE FALLBACKS: the general truth for the type, used when a
|
|
150
|
+
// label of that type has no specific entry above. They make the bind
|
|
151
|
+
// publication complete: a NEW label of a covered type still gets a named
|
|
152
|
+
// class instead of escaping unclassified.
|
|
153
|
+
"calls|*": "callee_recorded_as_a_leaf_name_whose_owner_is_not_resolved_per_file",
|
|
154
|
+
"uses|*": "used_name_recorded_as_a_leaf_whose_declaring_file_is_out_of_scope",
|
|
155
|
+
"references|*": "declared_type_leaf_whose_declaring_file_is_out_of_scope",
|
|
156
|
+
"exports|*": "exported_name_recorded_as_a_leaf_whose_owning_package_is_not_the_target",
|
|
157
|
+
"extends|*": "embedded_name_recorded_as_a_promotion_rather_than_a_declared_supertype",
|
|
158
|
+
"implements|*": "implemented_interface_recorded_as_a_leaf_whose_declaring_file_is_out_of_scope",
|
|
159
|
+
"overrides|*": "promoted_method_recorded_with_its_qualified_owner_whose_declaring_file_is_out_of_scope",
|
|
160
|
+
"instantiates|*": "instantiated_name_recorded_as_a_leaf_whose_declaring_file_is_out_of_scope",
|
|
161
|
+
"mixes_in|*": "embedded_name_recorded_as_a_promotion_rather_than_a_declared_supertype",
|
|
162
|
+
// The rendered component is a template SYMBOL whose template-ness is read from
|
|
163
|
+
// its own constructor call (tree-sitter-go has no type resolution), and the
|
|
164
|
+
// execution is a method call — the render relation is inferred from those two
|
|
165
|
+
// facts rather than declared by the language, which is why this row is 0.9.
|
|
166
|
+
"renders|go_template_render": "render_relation_inferred_from_an_html_template_execution_and_the_template_symbols_own_construction"
|
|
167
|
+
};
|
|
168
|
+
var GO_STDLIB_SINGLE_SEGMENT = /* @__PURE__ */ new Set([
|
|
169
|
+
"bufio",
|
|
170
|
+
"bytes",
|
|
171
|
+
"cmp",
|
|
172
|
+
"context",
|
|
173
|
+
"crypto",
|
|
174
|
+
"embed",
|
|
175
|
+
"encoding",
|
|
176
|
+
"errors",
|
|
177
|
+
"expvar",
|
|
178
|
+
"flag",
|
|
179
|
+
"fmt",
|
|
180
|
+
"hash",
|
|
181
|
+
"html",
|
|
182
|
+
"image",
|
|
183
|
+
"io",
|
|
184
|
+
"iter",
|
|
185
|
+
"log",
|
|
186
|
+
"maps",
|
|
187
|
+
"math",
|
|
188
|
+
"mime",
|
|
189
|
+
"net",
|
|
190
|
+
"os",
|
|
191
|
+
"path",
|
|
192
|
+
"plugin",
|
|
193
|
+
"reflect",
|
|
194
|
+
"regexp",
|
|
195
|
+
"runtime",
|
|
196
|
+
"slices",
|
|
197
|
+
"sort",
|
|
198
|
+
"strconv",
|
|
199
|
+
"strings",
|
|
200
|
+
"structs",
|
|
201
|
+
"sync",
|
|
202
|
+
"syscall",
|
|
203
|
+
"testing",
|
|
204
|
+
"time",
|
|
205
|
+
"unicode",
|
|
206
|
+
"unique",
|
|
207
|
+
"unsafe",
|
|
208
|
+
"weak"
|
|
209
|
+
]);
|
|
210
|
+
var GO_STDLIB_TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
211
|
+
"archive",
|
|
212
|
+
"bufio",
|
|
213
|
+
"bytes",
|
|
214
|
+
"cmp",
|
|
215
|
+
"compress",
|
|
216
|
+
"container",
|
|
217
|
+
"context",
|
|
218
|
+
"crypto",
|
|
219
|
+
"database",
|
|
220
|
+
"debug",
|
|
221
|
+
"embed",
|
|
222
|
+
"encoding",
|
|
223
|
+
"errors",
|
|
224
|
+
"expvar",
|
|
225
|
+
"flag",
|
|
226
|
+
"fmt",
|
|
227
|
+
"go",
|
|
228
|
+
"hash",
|
|
229
|
+
"html",
|
|
230
|
+
"image",
|
|
231
|
+
"index",
|
|
232
|
+
"io",
|
|
233
|
+
"iter",
|
|
234
|
+
"log",
|
|
235
|
+
"maps",
|
|
236
|
+
"math",
|
|
237
|
+
"mime",
|
|
238
|
+
"net",
|
|
239
|
+
"os",
|
|
240
|
+
"path",
|
|
241
|
+
"plugin",
|
|
242
|
+
"reflect",
|
|
243
|
+
"regexp",
|
|
244
|
+
"runtime",
|
|
245
|
+
"slices",
|
|
246
|
+
"sort",
|
|
247
|
+
"strconv",
|
|
248
|
+
"strings",
|
|
249
|
+
"structs",
|
|
250
|
+
"sync",
|
|
251
|
+
"syscall",
|
|
252
|
+
"testing",
|
|
253
|
+
"text",
|
|
254
|
+
"time",
|
|
255
|
+
"unicode",
|
|
256
|
+
"unique",
|
|
257
|
+
"unsafe",
|
|
258
|
+
"weak"
|
|
259
|
+
]);
|
|
260
|
+
function isGoStdlibModulePath(module) {
|
|
261
|
+
const first = module.split("/")[0];
|
|
262
|
+
return first.length > 0 && GO_STDLIB_TOP_LEVEL.has(first);
|
|
263
|
+
}
|
|
264
|
+
var GO_FRAMEWORK_RULES = [
|
|
265
|
+
{
|
|
266
|
+
name: "gin",
|
|
267
|
+
confidence: 1,
|
|
268
|
+
listed: true,
|
|
269
|
+
matches: (p) => stringIncludes(p, "gin-gonic/gin")
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "echo",
|
|
273
|
+
confidence: 1,
|
|
274
|
+
listed: true,
|
|
275
|
+
matches: (p) => stringIncludes(p, "labstack/echo")
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "fiber",
|
|
279
|
+
confidence: 1,
|
|
280
|
+
listed: true,
|
|
281
|
+
matches: (p) => stringIncludes(p, "gofiber/fiber")
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "net_http",
|
|
285
|
+
// The only standard-library entry, and the only row that must NOT be a
|
|
286
|
+
// substring test: `net/http` is a complete path, and a substring match
|
|
287
|
+
// would also fire on a third-party module whose path merely contains it.
|
|
288
|
+
confidence: 0.8,
|
|
289
|
+
listed: true,
|
|
290
|
+
matches: (p) => p === "net/http" || stringIncludes(p, "net/http/")
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: "gorilla_mux",
|
|
294
|
+
confidence: 1,
|
|
295
|
+
listed: true,
|
|
296
|
+
matches: (p) => stringIncludes(p, "gorilla/mux")
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "grpc",
|
|
300
|
+
confidence: 1,
|
|
301
|
+
listed: true,
|
|
302
|
+
matches: (p) => stringIncludes(p, "google.golang.org/grpc")
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "testify",
|
|
306
|
+
confidence: 1,
|
|
307
|
+
listed: true,
|
|
308
|
+
matches: (p) => stringIncludes(p, "stretchr/testify")
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: "gorm",
|
|
312
|
+
confidence: 1,
|
|
313
|
+
listed: true,
|
|
314
|
+
matches: (p) => stringIncludes(p, "gorm.io/gorm")
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "sqlx",
|
|
318
|
+
confidence: 1,
|
|
319
|
+
listed: true,
|
|
320
|
+
matches: (p) => stringIncludes(p, "jmoiron/sqlx")
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "ent",
|
|
324
|
+
confidence: 1,
|
|
325
|
+
listed: true,
|
|
326
|
+
matches: (p) => stringIncludes(p, "entgo.io/ent")
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "fx",
|
|
330
|
+
confidence: 1,
|
|
331
|
+
listed: true,
|
|
332
|
+
matches: (p) => p === "go.uber.org/fx" || stringIncludes(p, "go.uber.org/fx/")
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "wire",
|
|
336
|
+
confidence: 1,
|
|
337
|
+
listed: true,
|
|
338
|
+
matches: (p) => stringIncludes(p, "google/wire")
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "zap",
|
|
342
|
+
confidence: 1,
|
|
343
|
+
listed: true,
|
|
344
|
+
matches: (p) => p === "go.uber.org/zap" || stringIncludes(p, "go.uber.org/zap/")
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "logrus",
|
|
348
|
+
confidence: 1,
|
|
349
|
+
listed: true,
|
|
350
|
+
matches: (p) => stringIncludes(p, "sirupsen/logrus")
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "golang_extended",
|
|
354
|
+
// Category marker, not a framework — see the table's comment.
|
|
355
|
+
confidence: 1,
|
|
356
|
+
listed: false,
|
|
357
|
+
matches: (p) => p === "golang.org/x" || stringIncludes(p, "golang.org/x/")
|
|
358
|
+
}
|
|
359
|
+
];
|
|
360
|
+
function getUniqueStrings(arr) {
|
|
361
|
+
const seen = /* @__PURE__ */ Object.create(null);
|
|
362
|
+
const result = [];
|
|
363
|
+
if (!arr) return result;
|
|
364
|
+
for (let i = 0; i < arr.length; i++) {
|
|
365
|
+
if (!seen[arr[i]]) {
|
|
366
|
+
seen[arr[i]] = true;
|
|
367
|
+
result.push(arr[i]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
var RealGoAnalyzer = class {
|
|
373
|
+
/**
|
|
374
|
+
* the CALLEE node of each deduped call, keyed by the enclosing
|
|
375
|
+
* function's `startIndex` and then by the call text. The calls lane iterates
|
|
376
|
+
* `symbol.qualified_calls` — a deduped STRING list by design (M37 #604, GOD9)
|
|
377
|
+
* — so the node an edge's line comes from has to be recorded where the scan
|
|
378
|
+
* still holds it. Reset at the top of every `analyze()`: a `startIndex` is
|
|
379
|
+
* file-local, so a retained map would attribute another file's line.
|
|
380
|
+
*/
|
|
381
|
+
goCallSites = /* @__PURE__ */ new Map();
|
|
382
|
+
/**
|
|
383
|
+
* The canonical signature text of each method symbol, keyed by the method
|
|
384
|
+
* node's `startIndex` (file-local, so it cannot collide) — see `goCallSites`
|
|
385
|
+
* for the same discipline. The pre-fix code re-derived it on EVERY
|
|
386
|
+
* comparison: `checkGoInterfaceImplementation` compares each interface method
|
|
387
|
+
* against every candidate method, and `collectGoPromotedMethods` re-derives it
|
|
388
|
+
* again in each of its two shadowing tests, so one pass over a file with I
|
|
389
|
+
* interfaces and C candidates rebuilt the same string O(I × C × methods) times
|
|
390
|
+
* — the second half of the superlinear implements cost described at the hoist
|
|
391
|
+
* site below. Reset per `analyze()` with the same reason: a `startIndex` is
|
|
392
|
+
* file-local, so a retained map would leak one file's signatures into
|
|
393
|
+
* another's comparison.
|
|
394
|
+
*/
|
|
395
|
+
goSignatures = /* @__PURE__ */ new Map();
|
|
396
|
+
analyze(rootNode, content, filePath) {
|
|
397
|
+
const ctx = { filePath, content };
|
|
398
|
+
this.goCallSites = /* @__PURE__ */ new Map();
|
|
399
|
+
this.goSignatures = /* @__PURE__ */ new Map();
|
|
400
|
+
const symbols = [];
|
|
401
|
+
const relationships = [];
|
|
402
|
+
const imports = [];
|
|
403
|
+
const exports = [];
|
|
404
|
+
const errors = [];
|
|
405
|
+
if (rootNode.hasError) {
|
|
406
|
+
const stack = [rootNode];
|
|
407
|
+
while (stack.length > 0) {
|
|
408
|
+
const n = stack.pop();
|
|
409
|
+
if (n.type === "ERROR" || n.isMissing) {
|
|
410
|
+
errors.push({
|
|
411
|
+
message: "Brain Engine (Go): Bypassed resilient parser error node",
|
|
412
|
+
range: {
|
|
413
|
+
startLine: n.startPosition.row + 1,
|
|
414
|
+
startColumn: n.startPosition.column + 1,
|
|
415
|
+
endLine: n.endPosition.row + 1,
|
|
416
|
+
endColumn: n.endPosition.column + 1
|
|
417
|
+
},
|
|
418
|
+
severity: `warning`
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
for (let i = n.childCount - 1; i >= 0; i--) {
|
|
422
|
+
const child = n.child(i);
|
|
423
|
+
if (child) stack.push(child);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const packageClause = this.findGoPackageClause(rootNode);
|
|
428
|
+
const packageName = packageClause ? this.goPackageClauseName(packageClause) : void 0;
|
|
429
|
+
if (packageName && packageClause) {
|
|
430
|
+
symbols.push({
|
|
431
|
+
name: packageName,
|
|
432
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
433
|
+
// The PACKAGE CLAUSE's own range, read from the node. The pre-fix code
|
|
434
|
+
// hand-built the range from the NAME LENGTH — `startLine: 1,
|
|
435
|
+
// startColumn: 1, startIndex: 0, endIndex: packageName.length + 8` —
|
|
436
|
+
// which is only right when the clause is the first thing in the file.
|
|
437
|
+
// Go puts a `//go:build` / `// +build` header ABOVE the clause, and
|
|
438
|
+
// 158 of the pinned corpora's 3,924 files and 2,462 of kubernetes's
|
|
439
|
+
// 17,848 do, so every byte of the published range pointed into the
|
|
440
|
+
// build-tag comment: the symbol claimed line 1, column 1, bytes 0..8.
|
|
441
|
+
// GO4's finding stands and is now structural rather than arithmetic —
|
|
442
|
+
// `nodeToRange` derives the exclusive `endIndex` and the 1-based
|
|
443
|
+
// `endColumn` from the node's own end position, and for a clause at
|
|
444
|
+
// byte 0 those are byte-identical to the old hand-built values
|
|
445
|
+
// (`endIndex: len+8`, `endColumn: len+9`), so the common case is
|
|
446
|
+
// unchanged output and the header case becomes correct.
|
|
447
|
+
range: this.nodeToRange(packageClause),
|
|
448
|
+
visibility: "public",
|
|
449
|
+
metadata: {
|
|
450
|
+
nodeType: "package_clause",
|
|
451
|
+
isGoPackage: true
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
this.extractGoSymbols(rootNode, symbols);
|
|
456
|
+
this.processGoImports(rootNode, imports);
|
|
457
|
+
const stdlibQualifiers = this.collectGoStdlibQualifiers(imports);
|
|
458
|
+
this.analyzeGoRelationships(rootNode, relationships, symbols, ctx, stdlibQualifiers);
|
|
459
|
+
this.analyzeGoTemplateRenders(rootNode, imports, relationships);
|
|
460
|
+
let invalidImportPathCount = 0;
|
|
461
|
+
for (const imp of imports) {
|
|
462
|
+
if ((imp.module ?? "").length === 0) {
|
|
463
|
+
invalidImportPathCount++;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const importModule = String(imp.module ?? "");
|
|
467
|
+
const moduleSegments = importModule.split("/");
|
|
468
|
+
const stdlibRoot = moduleSegments.length >= 2 ? moduleSegments[0] ?? "" : "";
|
|
469
|
+
const isMultiSegmentStdlib = stdlibRoot.length > 0 && stdlibRoot !== "." && stdlibRoot !== ".." && GO_STDLIB_TOP_LEVEL.has(stdlibRoot);
|
|
470
|
+
relationships.push({
|
|
471
|
+
source: packageName || filePath,
|
|
472
|
+
target: imp.module || "",
|
|
473
|
+
// The FAMILY'S first-class `imports` kind, not a `references` edge
|
|
474
|
+
// (#799 round). An import names a MODULE, not a symbol: with the edge
|
|
475
|
+
// labelled `references`, every import path landed in the lane's
|
|
476
|
+
// references target surface, and an import path is a string literal
|
|
477
|
+
// (`net/http`, `github.com/google/go-cmp/cmp`) whose separators — `/`,
|
|
478
|
+
// `.`, `-` — no Go declaration may contain. R10 asks whether a target is
|
|
479
|
+
// a NAME; the answer for a module specifier is that it is a dependency,
|
|
480
|
+
// and the family's label for a dependency is `imports` (c moved to it in
|
|
481
|
+
// #910; go's own requiredKinds has named `imports` since the wave). The
|
|
482
|
+
// references surface is now separator-free, so a future foreign spelling
|
|
483
|
+
// there fails R10 immediately instead of hiding behind a recorded
|
|
484
|
+
// import-path convention.
|
|
485
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
486
|
+
confidence: 0.9,
|
|
487
|
+
// isImport:true + isGoImportDep keep the edge pinnable as the "imports"
|
|
488
|
+
// relationship kind by completeness section B, which reads the MARKER
|
|
489
|
+
// (script/m14/completeness-inventory.ts, section B) — so the kind is
|
|
490
|
+
// claimed twice over and the evidence tier stays import_based.
|
|
491
|
+
// the import_spec, whose line is the path's line for a
|
|
492
|
+
// single import and for each entry of a grouped one (the spec starts
|
|
493
|
+
// at its path — an alias would share the line).
|
|
494
|
+
...imp.range?.startLine ? { line_number: imp.range.startLine } : {},
|
|
495
|
+
metadata: {
|
|
496
|
+
isGoImportDep: true,
|
|
497
|
+
isImport: true,
|
|
498
|
+
packageName: imp.metadata?.packageName,
|
|
499
|
+
// THE ONE EXTERNAL CLAIM AN IMPORT EDGE CAN CARRY, and it is a claim about the NAME'S
|
|
500
|
+
// ORIGIN that the corpus cannot falsify because it is not about the corpus: the module is
|
|
501
|
+
// a member of the Go standard library's own package set (`GO_STDLIB_SINGLE_SEGMENT`
|
|
502
|
+
// above). `isStdlib` is one of the gate's recognised EXTERNAL_KEYS, so this converts a
|
|
503
|
+
// silent hole into a STATED, verifiable claim — never into a resolution, and the rule
|
|
504
|
+
// reads it only where the target names nothing, so no `resolved` row can move.
|
|
505
|
+
//
|
|
506
|
+
// ATTACHED ONLY TO SINGLE-SEGMENT PATHS, because the rule reaches a multi-segment one
|
|
507
|
+
// (`encoding/json`) as a PATH: `pathLikeTarget` sends it to `externalUnverified`, which
|
|
508
|
+
// is a clause-1 failure exactly as `unresolved` is. Claiming there would move an edge
|
|
509
|
+
// from one failure column into another and make the table look better without answering
|
|
510
|
+
// anything — so it is not done. MEASURED on the 400-file stride: this covers 830 of the
|
|
511
|
+
// lane's 25,215 unresolved edges and all 830 verify; the other 24,385 are untouched.
|
|
512
|
+
...GO_STDLIB_SINGLE_SEGMENT.has(imp.module ?? "") ? { isStdlib: true } : {},
|
|
513
|
+
// Multi-segment stdlib citation (ruled mechanism above; imports edges only). The root
|
|
514
|
+
// listed is the MATCHED first segment, so the claim's vocabulary is exactly what the
|
|
515
|
+
// closed set proved; `stdlibModule` is evidence kept beside the claim for a reviewer
|
|
516
|
+
// (not a claim key). `isStdlib` is deliberately NOT added here: one claim family per
|
|
517
|
+
// edge, and `externalClaimOf` resolves the namespace claim first anyway.
|
|
518
|
+
...isMultiSegmentStdlib ? {
|
|
519
|
+
languageNamespace: [stdlibRoot],
|
|
520
|
+
languageNamespacePolicy: "strict-absent",
|
|
521
|
+
languageNamespaceSlash: true,
|
|
522
|
+
stdlibModule: importModule
|
|
523
|
+
} : {}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const frameworks = this.detectGoFrameworks(imports);
|
|
528
|
+
const patterns = this.detectGoPatterns(rootNode, symbols);
|
|
529
|
+
const seenExportNames = /* @__PURE__ */ new Set();
|
|
530
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
531
|
+
const s = symbols[i];
|
|
532
|
+
const isPrivate = s.visibility === "private";
|
|
533
|
+
const isVar = s.kind === ASTEngine.SymbolKind.Variable;
|
|
534
|
+
const isMethod = s.kind === ASTEngine.SymbolKind.Method;
|
|
535
|
+
if (!isPrivate && !isVar && !isMethod && !s.ownerName && !seenExportNames.has(s.name)) {
|
|
536
|
+
seenExportNames.add(s.name);
|
|
537
|
+
exports.push({ name: s.name, kind: s.kind, range: s.range, visibility: s.visibility || "public" });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (packageName) {
|
|
541
|
+
const packageScopeNames = this.collectGoPackageScopeNames(rootNode);
|
|
542
|
+
for (const e of exports) {
|
|
543
|
+
if (!e.name || e.name === packageName) continue;
|
|
544
|
+
if (!packageScopeNames.has(e.name)) continue;
|
|
545
|
+
relationships.push({
|
|
546
|
+
source: packageName,
|
|
547
|
+
target: e.name,
|
|
548
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
549
|
+
confidence: 1,
|
|
550
|
+
// the exported declaration's own line.
|
|
551
|
+
...e.range?.startLine ? { line_number: e.range.startLine } : {},
|
|
552
|
+
metadata: {
|
|
553
|
+
relationshipType: "go_exported_identifier",
|
|
554
|
+
exportForm: "capitalized_package_scope_name",
|
|
555
|
+
symbolKind: String(e.kind ?? "")
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
this.extractDeclaredTypeRefs(rootNode, relationships, stdlibQualifiers, symbols);
|
|
561
|
+
let r7DroppedEdges = 0;
|
|
562
|
+
{
|
|
563
|
+
const kept = [];
|
|
564
|
+
for (const r of relationships) {
|
|
565
|
+
if (String(r.source ?? "").length > 0 && String(r.target ?? "").length > 0) kept.push(r);
|
|
566
|
+
else r7DroppedEdges++;
|
|
567
|
+
}
|
|
568
|
+
if (r7DroppedEdges > 0) relationships.splice(0, relationships.length, ...kept);
|
|
569
|
+
}
|
|
570
|
+
this.classifyEvidenceTier(relationships);
|
|
571
|
+
const cyclomaticComplexity = this.calculateCyclomaticComplexity(rootNode);
|
|
572
|
+
return {
|
|
573
|
+
language: "go",
|
|
574
|
+
symbols,
|
|
575
|
+
relationships,
|
|
576
|
+
imports,
|
|
577
|
+
exports,
|
|
578
|
+
errors,
|
|
579
|
+
metadata: {
|
|
580
|
+
// GO6 (go review): `complexity` was the synthetic
|
|
581
|
+
// symbols+relationships heuristic while a real per-file
|
|
582
|
+
// cyclomaticComplexity sat beside it — the exact disease the
|
|
583
|
+
// family removed everywhere else. One contract: both report the
|
|
584
|
+
// real cyclomatic number.
|
|
585
|
+
complexity: cyclomaticComplexity,
|
|
586
|
+
linesOfCode: ctx.content.split("\n").length,
|
|
587
|
+
// GO26 (round 5): computed ONCE. The pre-fix code called the counter
|
|
588
|
+
// twice on the same node, and each call is a full recursive walk of the
|
|
589
|
+
// tree — one wasted whole-file traversal per file, for two keys of the
|
|
590
|
+
// same object literal that must agree by construction.
|
|
591
|
+
cyclomaticComplexity,
|
|
592
|
+
framework: frameworks[0]?.name,
|
|
593
|
+
// Primary framework
|
|
594
|
+
patterns,
|
|
595
|
+
// Go intelligence metadata
|
|
596
|
+
detectedFrameworks: mapSymbols(frameworks, (f) => f.name),
|
|
597
|
+
frameworkPatterns: this.getGoFrameworkPatterns(
|
|
598
|
+
mapSymbols(frameworks, (f) => f.name),
|
|
599
|
+
symbols
|
|
600
|
+
),
|
|
601
|
+
architecturalInsights: this.generateGoArchitecturalInsights(symbols, relationships),
|
|
602
|
+
architecturalScore: this.calculateArchitecturalScore(symbols, relationships, patterns),
|
|
603
|
+
codeQuality: this.assessCodeQuality(symbols, relationships, patterns),
|
|
604
|
+
hasTypeInference: true,
|
|
605
|
+
hasFrameworkIntelligence: true,
|
|
606
|
+
analysisVersion: "3.0.1-go-iterative-stateless",
|
|
607
|
+
// R7's two counters, published because a suppression a consumer cannot see is a
|
|
608
|
+
// suppression it cannot trust: `r7DroppedEdges` is every relationship the choke point
|
|
609
|
+
// refused for an empty source or target, `invalidImportPathCount` is the import subset of
|
|
610
|
+
// them. A valid file reports 0/0, which is what makes a non-zero value evidence.
|
|
611
|
+
r7DroppedEdges,
|
|
612
|
+
invalidImportPathCount
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* #419-D evidence-tier classification for EVERY emitted relationship
|
|
618
|
+
* (family rules — mirrors rust/c/cpp so the go analyzer is not the one
|
|
619
|
+
* sibling that leaves edges untagged, keeping completeness section D
|
|
620
|
+
* achievable). Metadata tags already set by the emitter are left intact.
|
|
621
|
+
*/
|
|
622
|
+
classifyEvidenceTier(relationships) {
|
|
623
|
+
for (const r of relationships) {
|
|
624
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
625
|
+
if (md.evidenceClass) continue;
|
|
626
|
+
const t = String(r.type ?? "");
|
|
627
|
+
const conf = typeof r.confidence === "number" ? r.confidence : 1;
|
|
628
|
+
if (t === "contains" || t === "exports") {
|
|
629
|
+
md.evidenceClass = "structural";
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (t === "imports") {
|
|
633
|
+
md.evidenceClass = "import_based";
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (t === "references") {
|
|
637
|
+
md.evidenceClass = md.relationshipType === "declared_type" || md.relationshipType === "return_type" ? "reference_qualified" : "usage_contextual";
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (t === "extends" || t === "implements" || t === "overrides" || t === "mixes_in") {
|
|
641
|
+
md.evidenceClass = conf >= 1 ? "structural" : conf >= 0.85 ? "usage_contextual" : "inferred";
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (t === "instantiates") {
|
|
645
|
+
md.evidenceClass = "structural";
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
md.evidenceClass = "usage_contextual";
|
|
649
|
+
}
|
|
650
|
+
for (const r of relationships) {
|
|
651
|
+
const conf = typeof r.confidence === "number" ? r.confidence : 1;
|
|
652
|
+
if (conf >= 1) continue;
|
|
653
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
654
|
+
if (typeof md.reasonCode === "string" && md.reasonCode.length) continue;
|
|
655
|
+
const t = String(r.type ?? "");
|
|
656
|
+
const label = String(md.relationshipType ?? "");
|
|
657
|
+
const code = GO_SUB_UNIT_REASON[`${t}|${label}`] ?? GO_SUB_UNIT_REASON[`${t}|*`];
|
|
658
|
+
if (code) md.reasonCode = code;
|
|
659
|
+
}
|
|
660
|
+
for (const r of relationships) {
|
|
661
|
+
const t = String(r.type ?? "");
|
|
662
|
+
if (t === "contains" || t === "renders") continue;
|
|
663
|
+
const md = r.metadata ?? (r.metadata = {});
|
|
664
|
+
if (typeof md.bindReason === "string" && md.bindReason.length) continue;
|
|
665
|
+
const label = String(md.relationshipType ?? "");
|
|
666
|
+
const code = GO_SUB_UNIT_REASON[`${t}|${label}`] ?? GO_SUB_UNIT_REASON[`${t}|*`];
|
|
667
|
+
if (code) md.bindReason = code;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* The names this file declares at PACKAGE SCOPE, read from the grammar.
|
|
672
|
+
*
|
|
673
|
+
* WHY POSITION AND NOT THE SYMBOL TABLE. The lane publishes a function-local
|
|
674
|
+
* `type` as a symbol on purpose (the declared-type pass attributes that type's
|
|
675
|
+
* field types to it, so dropping the symbol would publish a `references` edge
|
|
676
|
+
* whose SOURCE names nothing), and such a symbol carries no `ownerName`. So
|
|
677
|
+
* the symbol table cannot answer "is this name in package scope" — nothing in a
|
|
678
|
+
* `type_spec` symbol distinguishes line 10 from line 2946. The declaration's
|
|
679
|
+
* position can, and position is what Go's own scoping rule is written about.
|
|
680
|
+
*
|
|
681
|
+
* WHY THE DIRECT CHILDREN OF `source_file`. That is the grammar's own
|
|
682
|
+
* statement of package scope: `package_clause`, `import_declaration`, and the
|
|
683
|
+
* declarations are its direct named children, and everything else is reachable
|
|
684
|
+
* only by descending into a `function_declaration`, `method_declaration` or
|
|
685
|
+
* `func_literal` body — a `block`, which is not a direct child of `source_file`
|
|
686
|
+
* by construction.
|
|
687
|
+
*
|
|
688
|
+
* WHY ONE LEVEL OF SPEC DESCENT AND NO FURTHER. `type_declaration`,
|
|
689
|
+
* `var_declaration` and `const_declaration` nest their `type_spec`,
|
|
690
|
+
* `type_alias`, `var_spec`/`const_spec` children directly, and those two
|
|
691
|
+
* levels are all a package-scope declaration needs. A nested
|
|
692
|
+
* `function_declaration` is unreachable from here because it would be a child
|
|
693
|
+
* of a `block`.
|
|
694
|
+
*
|
|
695
|
+
* `type_alias` IS a sibling of `type_spec`, not a variant of it:
|
|
696
|
+
* `type_declaration` holds a choice of the two, so `type WidgetList =
|
|
697
|
+
* []*Widget` arrives as `type_alias` and `type Widget struct{}` as
|
|
698
|
+
* `type_spec`. Handling only `type_spec` dropped every exported ALIAS, which
|
|
699
|
+
* the pre-existing fixture `exports-contract` caught on the first run — see
|
|
700
|
+
* the commit for that row.
|
|
701
|
+
*
|
|
702
|
+
* `_` is the BLANK identifier. It has no name and names no graph node, so it is
|
|
703
|
+
* left out: the answer to "is this name at package scope" is not yes.
|
|
704
|
+
*
|
|
705
|
+
* `Set<string>`, so a declaration named `constructor` or `toString` cannot be
|
|
706
|
+
* confused with an inherited member — the same Object.prototype hazard the
|
|
707
|
+
* interface and promotion passes defend against separately.
|
|
708
|
+
*/
|
|
709
|
+
collectGoPackageScopeNames(root) {
|
|
710
|
+
const names = /* @__PURE__ */ new Set();
|
|
711
|
+
const addSpecNames = (spec) => {
|
|
712
|
+
for (let i = 0; i < spec.namedChildCount; i++) {
|
|
713
|
+
const c = spec.namedChild(i);
|
|
714
|
+
if (!c) continue;
|
|
715
|
+
if (c.type !== "identifier" && c.type !== "type_identifier") continue;
|
|
716
|
+
const text = String(c.text ?? "");
|
|
717
|
+
if (text.length === 0 || text === "_") continue;
|
|
718
|
+
names.add(text);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
for (let i = 0; i < root.namedChildCount; i++) {
|
|
722
|
+
const d = root.namedChild(i);
|
|
723
|
+
if (!d) continue;
|
|
724
|
+
if (d.type === "function_declaration" || d.type === "method_declaration") {
|
|
725
|
+
const nm = d.childForFieldName?.("name");
|
|
726
|
+
if (nm) {
|
|
727
|
+
const text = String(nm.text ?? "");
|
|
728
|
+
if (text.length > 0) names.add(text);
|
|
729
|
+
}
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (d.type !== "type_declaration" && d.type !== "var_declaration" && d.type !== "const_declaration") continue;
|
|
733
|
+
for (let j = 0; j < d.namedChildCount; j++) {
|
|
734
|
+
const spec = d.namedChild(j);
|
|
735
|
+
if (!spec) continue;
|
|
736
|
+
if (spec.type === "type_spec" || spec.type === "type_alias") {
|
|
737
|
+
const nm = spec.childForFieldName?.("name");
|
|
738
|
+
if (nm) {
|
|
739
|
+
const text = String(nm.text ?? "");
|
|
740
|
+
if (text.length > 0) names.add(text);
|
|
741
|
+
}
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (spec.type === "var_spec" || spec.type === "const_spec") {
|
|
745
|
+
addSpecNames(spec);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return names;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* M14 — declared-type reference graph. Go type positions: struct fields,
|
|
753
|
+
* parameter declarations (params, receivers, and the SECOND parameter_list
|
|
754
|
+
* holding return values), var/const specs, and composite literal types.
|
|
755
|
+
* qualified_type keeps its dotted pkg.Type form per the M14 dotted-name
|
|
756
|
+
* convention; pointer/array/map/etc. wrappers descend; builtins excluded.
|
|
757
|
+
*/
|
|
758
|
+
extractDeclaredTypeRefs(rootNode, relationships, stdlibQualifiers = /* @__PURE__ */ new Map(), symbols = []) {
|
|
759
|
+
const ownPackageName = this.goOwnPackageName(rootNode);
|
|
760
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
761
|
+
const BUILTINS = /* @__PURE__ */ new Set(["int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "float32", "float64", "complex64", "complex128", "string", "bool", "byte", "rune", "error", "any", "comparable"]);
|
|
762
|
+
const collectTypeNameNodes = (n, out, qualified) => {
|
|
763
|
+
if (n.type === "qualified_type" || n.type === "type_identifier") {
|
|
764
|
+
if (!BUILTINS.has(n.text) && !out.some((x) => x.text === n.text)) out.push(n);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (n.type === "block" || n.type === "expression_list") return;
|
|
768
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
769
|
+
const c = n.namedChild(i);
|
|
770
|
+
if (c) collectTypeNameNodes(c, out, qualified);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
const resolveSource = (start) => {
|
|
774
|
+
let p = start.type === "field_declaration" ? start : start.parent;
|
|
775
|
+
let sawTypeSpec = false;
|
|
776
|
+
while (p) {
|
|
777
|
+
if (!sawTypeSpec && (p.type === "type_spec" || p.type === "type_alias")) {
|
|
778
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
779
|
+
const c = p.namedChild(i);
|
|
780
|
+
if (c && c.type === "type_identifier") return c.text;
|
|
781
|
+
}
|
|
782
|
+
sawTypeSpec = true;
|
|
783
|
+
}
|
|
784
|
+
if (p.type === "function_declaration" || p.type === "method_declaration") {
|
|
785
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
786
|
+
const c = p.namedChild(i);
|
|
787
|
+
if (!c) continue;
|
|
788
|
+
if (p.type === "method_declaration" && c.type === "field_identifier" || p.type === "function_declaration" && c.type === "identifier") return c.text;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
p = p.parent;
|
|
792
|
+
}
|
|
793
|
+
return "global";
|
|
794
|
+
};
|
|
795
|
+
const emit = (source, roots, relType) => {
|
|
796
|
+
for (const r of roots) {
|
|
797
|
+
if (!r) continue;
|
|
798
|
+
const names = [];
|
|
799
|
+
collectTypeNameNodes(r, names);
|
|
800
|
+
for (const t of names) {
|
|
801
|
+
const qualified = t.type === "qualified_type" ? t.text : void 0;
|
|
802
|
+
const dot = qualified ? qualified.lastIndexOf(".") : -1;
|
|
803
|
+
const target = qualified && dot > 0 ? qualified.slice(dot + 1) : t.text;
|
|
804
|
+
const key = `${source}|${target}|${relType}`;
|
|
805
|
+
if (emitted.has(key)) continue;
|
|
806
|
+
emitted.add(key);
|
|
807
|
+
const stdlibModule = qualified !== void 0 ? this.goStdlibTypeClaim(qualified, target, symbols, stdlibQualifiers, ownPackageName) : null;
|
|
808
|
+
relationships.push({
|
|
809
|
+
source,
|
|
810
|
+
target,
|
|
811
|
+
type: ASTEngine.RelationshipType.References,
|
|
812
|
+
confidence: 0.95,
|
|
813
|
+
...this.nodeLine(t) ? { line_number: this.nodeLine(t) } : {},
|
|
814
|
+
metadata: {
|
|
815
|
+
relationshipType: relType,
|
|
816
|
+
...qualified ? { qualifiedType: qualified } : {},
|
|
817
|
+
...stdlibModule !== null ? { isStdlib: true, stdlibPath: stdlibModule } : {}
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
const isGoResultParameter = (decl) => {
|
|
824
|
+
const list = decl.parent;
|
|
825
|
+
if (!list || list.type !== "parameter_list") return false;
|
|
826
|
+
const owner = list.parent;
|
|
827
|
+
if (!owner) return false;
|
|
828
|
+
if (owner.type !== "function_declaration" && owner.type !== "method_declaration" && owner.type !== "method_spec" && owner.type !== "func_literal") return false;
|
|
829
|
+
const result = owner.childForFieldName("result");
|
|
830
|
+
return !!result && result.id === list.id;
|
|
831
|
+
};
|
|
832
|
+
const walk = (n) => {
|
|
833
|
+
switch (n.type) {
|
|
834
|
+
case "field_declaration": {
|
|
835
|
+
emit(resolveSource(n), [n], "declared_type");
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
case "function_declaration":
|
|
839
|
+
case "method_declaration":
|
|
840
|
+
case "func_literal":
|
|
841
|
+
case "method_spec": {
|
|
842
|
+
const result = n.childForFieldName("result");
|
|
843
|
+
if (result && result.type !== "parameter_list") emit(resolveSource(result), [result], "return_type");
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
case "parameter_declaration":
|
|
847
|
+
case "variadic_parameter_declaration": {
|
|
848
|
+
emit(resolveSource(n), [n], isGoResultParameter(n) ? "return_type" : "declared_type");
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
case "var_spec":
|
|
852
|
+
case "const_spec": {
|
|
853
|
+
const inFunction = (x) => {
|
|
854
|
+
for (let q = x; q; q = q.parent) {
|
|
855
|
+
if (q.type === "function_declaration" || q.type === "method_declaration" || q.type === "func_literal") return true;
|
|
856
|
+
}
|
|
857
|
+
return false;
|
|
858
|
+
};
|
|
859
|
+
if (!inFunction(n)) {
|
|
860
|
+
const declared = [];
|
|
861
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
862
|
+
const c = n.namedChild(i);
|
|
863
|
+
if (c && c.type === "identifier" && String(c.text ?? "") !== "_") declared.push(String(c.text ?? ""));
|
|
864
|
+
}
|
|
865
|
+
if (declared.length === 0) emit(resolveSource(n), [n], "declared_type");
|
|
866
|
+
else for (const name of declared) emit(name, [n], "declared_type");
|
|
867
|
+
} else {
|
|
868
|
+
emit(resolveSource(n), [n], "declared_type");
|
|
869
|
+
}
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
874
|
+
const c = n.namedChild(i);
|
|
875
|
+
if (c) walk(c);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
walk(rootNode);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* PHASE 1: Extract Go symbols with proper Go semantics understanding
|
|
882
|
+
*/
|
|
883
|
+
extractGoSymbols(node, symbols) {
|
|
884
|
+
const stack = [{ node, inFunction: false }];
|
|
885
|
+
while (stack.length > 0) {
|
|
886
|
+
const { node: current, inFunction } = stack.pop();
|
|
887
|
+
switch (current.type) {
|
|
888
|
+
case "function_declaration":
|
|
889
|
+
this.extractGoFunction(current, symbols);
|
|
890
|
+
break;
|
|
891
|
+
case "method_declaration":
|
|
892
|
+
this.extractGoMethod(current, symbols);
|
|
893
|
+
break;
|
|
894
|
+
case "type_declaration":
|
|
895
|
+
this.extractGoTypeDeclaration(current, symbols);
|
|
896
|
+
break;
|
|
897
|
+
case "var_declaration":
|
|
898
|
+
case "const_declaration":
|
|
899
|
+
if (!inFunction) this.extractGoVariables(current, symbols);
|
|
900
|
+
break;
|
|
901
|
+
case "import_declaration":
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
const nextInFunction = inFunction || current.type === "block" || current.type === "function_declaration" || current.type === "method_declaration" || current.type === "func_literal";
|
|
905
|
+
for (let i = current.namedChildCount - 1; i >= 0; i--) {
|
|
906
|
+
const child = current.namedChild(i);
|
|
907
|
+
if (child) stack.push({ node: child, inFunction: nextInFunction });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Extract Go function declarations
|
|
913
|
+
*/
|
|
914
|
+
extractGoFunction(node, symbols) {
|
|
915
|
+
const nameNode = node.childForFieldName("name");
|
|
916
|
+
if (!nameNode) return;
|
|
917
|
+
const functionName = nameNode.text;
|
|
918
|
+
const parameters = this.extractGoParameters(node);
|
|
919
|
+
const returnType = this.extractGoReturnType(node);
|
|
920
|
+
const qualifiedCalls = this.findGoFunctionCalls(node);
|
|
921
|
+
const bodyText = node.childForFieldName("body")?.text || "";
|
|
922
|
+
const apiContractPath = this.extractGoApiContractPathFromComments(node);
|
|
923
|
+
symbols.push({
|
|
924
|
+
name: functionName,
|
|
925
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
926
|
+
range: this.nodeToRange(node),
|
|
927
|
+
parameters,
|
|
928
|
+
returnType,
|
|
929
|
+
qualified_calls: qualifiedCalls,
|
|
930
|
+
apiContractPath,
|
|
931
|
+
// Per-symbol cyclomatic complexity (family contract — rust/c/cpp all
|
|
932
|
+
// emit symbol.complexity; go was the only sibling that didn't, so the
|
|
933
|
+
// completeness section-C payload pin was impossible. Added go wave
|
|
934
|
+
// 2026-08-28, same counter as calculateCyclomaticComplexity).
|
|
935
|
+
complexity: this.calculateCyclomaticComplexity(node),
|
|
936
|
+
visibility: this.isGoExported(functionName) ? "public" : "private",
|
|
937
|
+
// A package-scope Go function has no owner, and that is a property of
|
|
938
|
+
// the language rather than of a missing argument: Go declares functions
|
|
939
|
+
// and methods at package scope only, and a nested function is a
|
|
940
|
+
// `func_literal`, which LocalsAreNotSymbols keeps out of the symbol set
|
|
941
|
+
// entirely. The two `!s.ownerName` constructor filters below are
|
|
942
|
+
// therefore correct on a fact about Go, not by accident of a dead
|
|
943
|
+
// parameter (GO26).
|
|
944
|
+
metadata: {
|
|
945
|
+
nodeType: node.type,
|
|
946
|
+
isGoFunction: true,
|
|
947
|
+
isExported: this.isGoExported(functionName),
|
|
948
|
+
hasErrorHandling: this.hasGoErrorHandling(bodyText),
|
|
949
|
+
isInit: functionName === "init",
|
|
950
|
+
hasGenerics: !!findSymbol(node.children, (c) => c && c.type === "type_parameter_list")
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Extract Go method declarations (methods with receivers)
|
|
956
|
+
*/
|
|
957
|
+
extractGoMethod(node, symbols) {
|
|
958
|
+
const nameNode = node.childForFieldName("name");
|
|
959
|
+
if (!nameNode) return;
|
|
960
|
+
const methodName = nameNode.text;
|
|
961
|
+
const receiver = this.extractGoReceiver(node);
|
|
962
|
+
const parameters = this.extractGoParameters(node);
|
|
963
|
+
const returnType = this.extractGoReturnType(node);
|
|
964
|
+
const qualifiedCalls = this.findGoFunctionCalls(node);
|
|
965
|
+
const bodyText = node.childForFieldName("body")?.text || "";
|
|
966
|
+
const apiContractPath = this.extractGoApiContractPathFromComments(node);
|
|
967
|
+
symbols.push({
|
|
968
|
+
name: methodName,
|
|
969
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
970
|
+
range: this.nodeToRange(node),
|
|
971
|
+
parameters,
|
|
972
|
+
returnType,
|
|
973
|
+
qualified_calls: qualifiedCalls,
|
|
974
|
+
apiContractPath,
|
|
975
|
+
// Per-symbol cyclomatic complexity (family contract, go wave 2026-08-28).
|
|
976
|
+
complexity: this.calculateCyclomaticComplexity(node),
|
|
977
|
+
visibility: this.isGoExported(methodName) ? "public" : "private",
|
|
978
|
+
ownerName: receiver?.typeName,
|
|
979
|
+
ownerKind: ASTEngine.SymbolKind.Struct,
|
|
980
|
+
metadata: {
|
|
981
|
+
nodeType: node.type,
|
|
982
|
+
isGoMethod: true,
|
|
983
|
+
receiver,
|
|
984
|
+
receiverType: receiver?.typeName,
|
|
985
|
+
receiverKind: receiver?.isPointer ? "pointer" : "value",
|
|
986
|
+
isPointerReceiver: receiver?.isPointer || false,
|
|
987
|
+
isExported: this.isGoExported(methodName),
|
|
988
|
+
hasErrorHandling: this.hasGoErrorHandling(bodyText),
|
|
989
|
+
hasGenerics: !!findSymbol(node.children, (c) => c && c.type === "type_parameter_list")
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Extract Go type declarations (structs, interfaces, type aliases)
|
|
995
|
+
*/
|
|
996
|
+
extractGoTypeDeclaration(node, symbols) {
|
|
997
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
998
|
+
const typeSpec = node.namedChild(i);
|
|
999
|
+
if (typeSpec?.type === "type_spec" || typeSpec?.type === "type_alias") {
|
|
1000
|
+
this.extractGoTypeSpec(typeSpec, symbols);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Extract individual type specifications (struct, interface, etc.)
|
|
1006
|
+
*/
|
|
1007
|
+
extractGoTypeSpec(node, symbols) {
|
|
1008
|
+
const nameNode = node.childForFieldName("name");
|
|
1009
|
+
if (!nameNode) return;
|
|
1010
|
+
const typeName = nameNode.text;
|
|
1011
|
+
const typeNode = node.childForFieldName("type");
|
|
1012
|
+
if (!typeNode) return;
|
|
1013
|
+
const typeParamsNode = findSymbol(node.children, (c) => c && c.type === "type_parameter_list");
|
|
1014
|
+
const hasGenerics = !!typeParamsNode;
|
|
1015
|
+
let kind;
|
|
1016
|
+
let isInterface = false;
|
|
1017
|
+
switch (typeNode.type) {
|
|
1018
|
+
case "struct_type":
|
|
1019
|
+
kind = ASTEngine.SymbolKind.Struct;
|
|
1020
|
+
break;
|
|
1021
|
+
case "interface_type":
|
|
1022
|
+
kind = ASTEngine.SymbolKind.Interface;
|
|
1023
|
+
isInterface = true;
|
|
1024
|
+
break;
|
|
1025
|
+
default:
|
|
1026
|
+
kind = ASTEngine.SymbolKind.Type;
|
|
1027
|
+
}
|
|
1028
|
+
symbols.push({
|
|
1029
|
+
name: typeName,
|
|
1030
|
+
kind,
|
|
1031
|
+
range: this.nodeToRange(node),
|
|
1032
|
+
visibility: this.isGoExported(typeName) ? "public" : "private",
|
|
1033
|
+
metadata: {
|
|
1034
|
+
nodeType: node.type,
|
|
1035
|
+
goType: typeNode.type,
|
|
1036
|
+
isInterface,
|
|
1037
|
+
isStruct: typeNode.type === "struct_type",
|
|
1038
|
+
isGoStruct: typeNode.type === "struct_type",
|
|
1039
|
+
isExported: this.isGoExported(typeName),
|
|
1040
|
+
isTypeAlias: node.type === "type_alias",
|
|
1041
|
+
hasGenerics
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
if (typeNode.type === "struct_type") {
|
|
1045
|
+
this.extractStructFields(typeNode, symbols, typeName);
|
|
1046
|
+
} else if (typeNode.type === "interface_type") {
|
|
1047
|
+
this.extractInterfaceMethods(typeNode, symbols, typeName);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Extract struct fields
|
|
1052
|
+
*/
|
|
1053
|
+
extractStructFields(structNode, symbols, structName) {
|
|
1054
|
+
for (let i = 0; i < structNode.namedChildCount; i++) {
|
|
1055
|
+
const child = structNode.namedChild(i);
|
|
1056
|
+
if (child?.type === "field_declaration_list") {
|
|
1057
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1058
|
+
const fieldDecl = child.namedChild(j);
|
|
1059
|
+
if (fieldDecl?.type === "field_declaration") {
|
|
1060
|
+
const fieldTypeNode = fieldDecl.childForFieldName("type");
|
|
1061
|
+
const declaredNameNodes = [];
|
|
1062
|
+
const seenFieldStart = /* @__PURE__ */ new Set();
|
|
1063
|
+
const addDeclaredName = (n) => {
|
|
1064
|
+
if (!n) return;
|
|
1065
|
+
const at = n.startIndex ?? -1;
|
|
1066
|
+
if (seenFieldStart.has(at)) return;
|
|
1067
|
+
seenFieldStart.add(at);
|
|
1068
|
+
declaredNameNodes.push(n);
|
|
1069
|
+
};
|
|
1070
|
+
addDeclaredName(fieldDecl.childForFieldName("name"));
|
|
1071
|
+
for (let k = 0; k < fieldDecl.namedChildCount; k++) {
|
|
1072
|
+
const c = fieldDecl.namedChild(k);
|
|
1073
|
+
if (c && c.type === "field_identifier") addDeclaredName(c);
|
|
1074
|
+
}
|
|
1075
|
+
const fieldNameNodes = declaredNameNodes.filter((n) => n.text !== "_");
|
|
1076
|
+
const hasDeclaredName = declaredNameNodes.length > 0;
|
|
1077
|
+
const tagNode = fieldDecl.childForFieldName("tag");
|
|
1078
|
+
let structTags = /* @__PURE__ */ Object.create(null);
|
|
1079
|
+
if (tagNode) {
|
|
1080
|
+
const tagText = this.stripRawStringDelimiters(tagNode.text);
|
|
1081
|
+
structTags = this.parseStructTags(tagText);
|
|
1082
|
+
}
|
|
1083
|
+
if (!hasDeclaredName && fieldTypeNode) {
|
|
1084
|
+
const embeddedName = this.goEmbeddedFieldName(fieldTypeNode);
|
|
1085
|
+
if (embeddedName) {
|
|
1086
|
+
symbols.push({
|
|
1087
|
+
name: embeddedName,
|
|
1088
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1089
|
+
range: this.nodeToRange(fieldTypeNode),
|
|
1090
|
+
returnType: fieldTypeNode.text,
|
|
1091
|
+
visibility: this.isGoExported(embeddedName) ? "public" : "private",
|
|
1092
|
+
ownerName: structName,
|
|
1093
|
+
ownerKind: ASTEngine.SymbolKind.Struct,
|
|
1094
|
+
metadata: {
|
|
1095
|
+
structTags: getObjectKeysLength(structTags) > 0 ? structTags : void 0,
|
|
1096
|
+
nodeType: fieldDecl.type,
|
|
1097
|
+
isStructField: true,
|
|
1098
|
+
fieldType: fieldTypeNode.text,
|
|
1099
|
+
isExported: this.isGoExported(embeddedName),
|
|
1100
|
+
isEmbeddedField: true,
|
|
1101
|
+
embeddedField: fieldTypeNode.text
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
for (const fieldNameNode of fieldNameNodes) {
|
|
1107
|
+
symbols.push({
|
|
1108
|
+
name: fieldNameNode.text,
|
|
1109
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1110
|
+
// The FIELD's own span, not the whole declaration: `A, B int`
|
|
1111
|
+
// names two fields at two positions, and a consumer that jumps
|
|
1112
|
+
// to `B` must land on `B` (#919's range rule).
|
|
1113
|
+
range: this.nodeToRange(fieldNameNode),
|
|
1114
|
+
returnType: fieldTypeNode?.text,
|
|
1115
|
+
visibility: this.isGoExported(fieldNameNode.text) ? "public" : "private",
|
|
1116
|
+
ownerName: structName,
|
|
1117
|
+
ownerKind: ASTEngine.SymbolKind.Struct,
|
|
1118
|
+
metadata: {
|
|
1119
|
+
structTags: getObjectKeysLength(structTags) > 0 ? structTags : void 0,
|
|
1120
|
+
nodeType: fieldDecl.type,
|
|
1121
|
+
isStructField: true,
|
|
1122
|
+
fieldType: fieldTypeNode?.text,
|
|
1123
|
+
isExported: this.isGoExported(fieldNameNode.text),
|
|
1124
|
+
// #889: this field shares its declaration with others
|
|
1125
|
+
groupedFieldDeclaration: fieldNameNodes.length > 1
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* The NAME an embedded field declares.
|
|
1137
|
+
*
|
|
1138
|
+
* Go's spec: "A field declared with a type but no explicit field name is
|
|
1139
|
+
* called an embedded field ... The unqualified type name acts as the field
|
|
1140
|
+
* name." So `io.Reader` declares a field `Reader`, `*S2` declares `S2`, and
|
|
1141
|
+
* `T[P]` declares `T` — the same LEAF rule the embedded-INTERFACE path in
|
|
1142
|
+
* `extractInterfaceMethods` already applies. Wrappers are transparent; only
|
|
1143
|
+
* the leaf names the field.
|
|
1144
|
+
*/
|
|
1145
|
+
goEmbeddedFieldName(typeNode) {
|
|
1146
|
+
let cur = typeNode;
|
|
1147
|
+
for (let guard = 0; guard < 8 && cur; guard++) {
|
|
1148
|
+
const t = cur.type;
|
|
1149
|
+
if (t === "pointer_type") {
|
|
1150
|
+
cur = (cur.namedChildCount > 0 ? cur.namedChild(0) : void 0) ?? void 0;
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (t === "generic_type") {
|
|
1154
|
+
cur = (cur.namedChildCount > 0 ? cur.namedChild(0) : void 0) ?? void 0;
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
if (!cur) return void 0;
|
|
1160
|
+
if (cur.type !== "type_identifier" && cur.type !== "qualified_type") return void 0;
|
|
1161
|
+
const raw = cur.text;
|
|
1162
|
+
const dot = raw.lastIndexOf(".");
|
|
1163
|
+
const leaf = dot !== -1 ? raw.slice(dot + 1) : raw;
|
|
1164
|
+
return this.isPlainGoIdentifier(leaf) ? leaf : void 0;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Extract interface methods
|
|
1168
|
+
*/
|
|
1169
|
+
extractInterfaceMethods(interfaceNode, symbols, interfaceName) {
|
|
1170
|
+
for (let i = 0; i < interfaceNode.namedChildCount; i++) {
|
|
1171
|
+
const method = interfaceNode.namedChild(i);
|
|
1172
|
+
if (method?.type === "method_spec") {
|
|
1173
|
+
const methodName = method.childForFieldName("name");
|
|
1174
|
+
if (methodName) {
|
|
1175
|
+
const parameters = this.extractGoMethodSpecParameters(method);
|
|
1176
|
+
const returnType = this.extractGoMethodSpecReturnType(method);
|
|
1177
|
+
symbols.push({
|
|
1178
|
+
name: methodName.text,
|
|
1179
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1180
|
+
range: this.nodeToRange(method),
|
|
1181
|
+
isAbstract: true,
|
|
1182
|
+
// Interface methods are abstract
|
|
1183
|
+
parameters,
|
|
1184
|
+
returnType,
|
|
1185
|
+
visibility: this.isGoExported(methodName.text) ? "public" : "private",
|
|
1186
|
+
ownerName: interfaceName,
|
|
1187
|
+
ownerKind: ASTEngine.SymbolKind.Interface,
|
|
1188
|
+
metadata: {
|
|
1189
|
+
nodeType: method.type,
|
|
1190
|
+
isInterfaceMethod: true,
|
|
1191
|
+
isExported: this.isGoExported(methodName.text)
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (method?.type === "constraint_elem") {
|
|
1198
|
+
const embeddedNode = method.namedChildCount > 0 ? method.namedChild(0) : void 0;
|
|
1199
|
+
if (embeddedNode && (embeddedNode.type === "type_identifier" || embeddedNode.type === "qualified_type")) {
|
|
1200
|
+
const rawText = embeddedNode.text;
|
|
1201
|
+
const leaf = rawText.lastIndexOf(".") !== -1 ? rawText.slice(rawText.lastIndexOf(".") + 1) : rawText;
|
|
1202
|
+
if (leaf && this.isPlainGoIdentifier(leaf)) {
|
|
1203
|
+
symbols.push({
|
|
1204
|
+
name: leaf,
|
|
1205
|
+
kind: ASTEngine.SymbolKind.Interface,
|
|
1206
|
+
range: this.nodeToRange(method),
|
|
1207
|
+
visibility: "public",
|
|
1208
|
+
ownerName: interfaceName,
|
|
1209
|
+
ownerKind: ASTEngine.SymbolKind.Interface,
|
|
1210
|
+
metadata: {
|
|
1211
|
+
nodeType: method.type,
|
|
1212
|
+
isEmbeddedInterface: true,
|
|
1213
|
+
embeddedInterface: rawText
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Extract Go variables and constants
|
|
1223
|
+
*/
|
|
1224
|
+
extractGoVariables(node, symbols) {
|
|
1225
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1226
|
+
const spec = node.namedChild(i);
|
|
1227
|
+
if (spec?.type === "var_spec" || spec?.type === "const_spec") {
|
|
1228
|
+
const nameNodes = [];
|
|
1229
|
+
for (let j = 0; j < spec.namedChildCount; j++) {
|
|
1230
|
+
const c = spec.namedChild(j);
|
|
1231
|
+
if (c && c.type === "identifier" && c.text !== "_") nameNodes.push(c);
|
|
1232
|
+
}
|
|
1233
|
+
if (nameNodes.length > 0) {
|
|
1234
|
+
const isConst = spec.type === "const_spec";
|
|
1235
|
+
for (const nameNode of nameNodes) {
|
|
1236
|
+
if (!nameNode) continue;
|
|
1237
|
+
symbols.push({
|
|
1238
|
+
name: nameNode.text,
|
|
1239
|
+
kind: isConst ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Variable,
|
|
1240
|
+
// The NAME's own span, not the whole `var_spec` — the same
|
|
1241
|
+
// #919 range rule the struct-field path applies (and the reason
|
|
1242
|
+
// it exists: `A, B int` names two fields at two positions, and a
|
|
1243
|
+
// consumer that jumps to `B` must land on `B`). The var/const
|
|
1244
|
+
// path kept the declaration's range, so on
|
|
1245
|
+
// `var alpha,\n\tbeta int` BOTH names reported `3:5` — `beta`'s
|
|
1246
|
+
// own position never appeared anywhere in the index.
|
|
1247
|
+
range: this.nodeToRange(nameNode),
|
|
1248
|
+
visibility: this.isGoExported(nameNode.text) ? "public" : "private",
|
|
1249
|
+
metadata: {
|
|
1250
|
+
nodeType: spec.type,
|
|
1251
|
+
isConst,
|
|
1252
|
+
isExported: this.isGoExported(nameNode.text)
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
// HELPER METHODS
|
|
1261
|
+
/** GO3: file has a dot-import (`import . "fmt"`) — bare identifiers may
|
|
1262
|
+
* name dot-imported package members, which no local symbol can resolve.
|
|
1263
|
+
* Calls resolve leniently (documented); bare READS stay strict. */
|
|
1264
|
+
hasGoDotImport(rootNode) {
|
|
1265
|
+
const stack = [rootNode];
|
|
1266
|
+
while (stack.length > 0) {
|
|
1267
|
+
const n = stack.pop();
|
|
1268
|
+
if (n.type === "import_spec") {
|
|
1269
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
1270
|
+
const c = n.namedChild(i);
|
|
1271
|
+
if (c && c.type === "dot") return true;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1275
|
+
const c = n.namedChild(i);
|
|
1276
|
+
if (c) stack.push(c);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
/** GO1: identifiers under a qualified_type (`pkg.Foo`) name types, never
|
|
1282
|
+
* values — the composite-literal and declared-type passes own those. */
|
|
1283
|
+
hasGoQualifiedAncestor(node) {
|
|
1284
|
+
let cur = node.parent;
|
|
1285
|
+
while (cur) {
|
|
1286
|
+
if (cur.type === "qualified_type") return true;
|
|
1287
|
+
if (cur.type === "call_expression" || cur.type === "field_declaration" || cur.type === "function_declaration" || cur.type === "method_declaration" || cur.type === "block" || cur.type === "composite_literal" || cur.type === "var_spec" || cur.type === "const_spec" || cur.type === "short_var_declaration" || cur.type === "assignment_statement") return false;
|
|
1288
|
+
cur = cur.parent;
|
|
1289
|
+
}
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
nodeToRange(node) {
|
|
1293
|
+
return {
|
|
1294
|
+
startLine: node.startPosition.row + 1,
|
|
1295
|
+
startColumn: node.startPosition.column + 1,
|
|
1296
|
+
endLine: node.endPosition.row + 1,
|
|
1297
|
+
endColumn: node.endPosition.column + 1,
|
|
1298
|
+
startIndex: node.startIndex,
|
|
1299
|
+
endIndex: node.endIndex
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Is a Go identifier EXPORTED?
|
|
1304
|
+
*
|
|
1305
|
+
* Go's spec: "An identifier is exported if the first character of the
|
|
1306
|
+
* identifier's name is a Unicode upper case letter" — the rule is
|
|
1307
|
+
* `unicode.IsUpper`, NOT an ASCII range. The old test here was
|
|
1308
|
+
* `name[0] >= "A" && name[0] <= "Z"`, so every legal Go identifier with a
|
|
1309
|
+
* non-ASCII head (`Ünï`, `Ö`, `Ñ`) was published `visibility: "private"`,
|
|
1310
|
+
* which fails the PHASE-5 export predicate below and removed it from the
|
|
1311
|
+
* public-API list the product serves. Go identifiers may use any Unicode
|
|
1312
|
+
* letter (spec, Identifiers), so this was a real hole in the export
|
|
1313
|
+
* surface, not a cosmetic classification.
|
|
1314
|
+
*
|
|
1315
|
+
* The implementation is a no-regex upper-case test on the FIRST CODE POINT
|
|
1316
|
+
* using the JS runtime's own Unicode case mappings (the same ICU tables the
|
|
1317
|
+
* grammar's identifier rule is written against):
|
|
1318
|
+
*
|
|
1319
|
+
* c is an UPPER CASE letter <=> toUpperCase(c) === c AND toLowerCase(c) !== c
|
|
1320
|
+
*
|
|
1321
|
+
* which is exactly `unicode.IsUpper`'s definition — Go's own spec words the
|
|
1322
|
+
* category test as "the uppercase version of the letter is different from
|
|
1323
|
+
* its lowercase version". Spot-checked against the categories that matter:
|
|
1324
|
+
* `A` upper, `a` not, `Ü` upper, `ü` not, `ß` not (Go: Ll, not Lu), CJK
|
|
1325
|
+
* `U+4E2D` not (Lo, not Lu), titlecase `Dž` not (Lt, not Lu). The only residual is
|
|
1326
|
+
* a code point the runtime's tables do not know, which is then not-upper —
|
|
1327
|
+
* the conservative direction (a symbol stays private rather than being
|
|
1328
|
+
* published as public API on a guess).
|
|
1329
|
+
*/
|
|
1330
|
+
isGoExported(name) {
|
|
1331
|
+
if (!name) return false;
|
|
1332
|
+
const c = String.fromCodePoint(name.codePointAt(0));
|
|
1333
|
+
if (c.length === 0) return false;
|
|
1334
|
+
if (c === "\uFFFD" && name.charCodeAt(0) >= 55296 && name.charCodeAt(0) <= 57343) return false;
|
|
1335
|
+
const upper = c.toUpperCase();
|
|
1336
|
+
if (upper !== c) return false;
|
|
1337
|
+
return c.toLowerCase() !== c;
|
|
1338
|
+
}
|
|
1339
|
+
extractGoApiContractPathFromComments(node) {
|
|
1340
|
+
let prevNode = node.previousNamedSibling;
|
|
1341
|
+
const comments = [];
|
|
1342
|
+
while (prevNode && prevNode.type === "comment") {
|
|
1343
|
+
comments.unshift(prevNode.text);
|
|
1344
|
+
prevNode = prevNode.previousNamedSibling;
|
|
1345
|
+
}
|
|
1346
|
+
for (const comment of comments) {
|
|
1347
|
+
const router = this.parseRouterAnnotation(comment);
|
|
1348
|
+
if (router) return router;
|
|
1349
|
+
}
|
|
1350
|
+
return void 0;
|
|
1351
|
+
}
|
|
1352
|
+
extractGoReceiver(methodNode) {
|
|
1353
|
+
let receiverList = null;
|
|
1354
|
+
for (let i = 0; i < methodNode.namedChildCount; i++) {
|
|
1355
|
+
const child = methodNode.namedChild(i);
|
|
1356
|
+
if (child?.type === "parameter_list") {
|
|
1357
|
+
receiverList = child;
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (!receiverList) return void 0;
|
|
1362
|
+
const receiverParam = receiverList.namedChild(0);
|
|
1363
|
+
if (!receiverParam || receiverParam.type !== "parameter_declaration") return void 0;
|
|
1364
|
+
const receiverName = receiverParam.childForFieldName("name");
|
|
1365
|
+
const receiverType = receiverParam.childForFieldName("type");
|
|
1366
|
+
if (!receiverType) return void 0;
|
|
1367
|
+
const typeName = receiverType.text;
|
|
1368
|
+
const isPointer = typeName.startsWith("*");
|
|
1369
|
+
let actualTypeName = isPointer ? typeName.substring(1) : typeName;
|
|
1370
|
+
const lt = actualTypeName.indexOf("[");
|
|
1371
|
+
if (lt > 0) actualTypeName = actualTypeName.slice(0, lt);
|
|
1372
|
+
return {
|
|
1373
|
+
name: receiverName?.text || "",
|
|
1374
|
+
typeName: actualTypeName,
|
|
1375
|
+
isPointer
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
extractGoParameters(node) {
|
|
1379
|
+
const parameters = [];
|
|
1380
|
+
let paramLists = [];
|
|
1381
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1382
|
+
const child = node.namedChild(i);
|
|
1383
|
+
if (child?.type === "parameter_list") {
|
|
1384
|
+
paramLists.push(child);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
const targetParamList = node.type === "method_declaration" ? paramLists[1] : paramLists[0];
|
|
1388
|
+
if (!targetParamList) return parameters;
|
|
1389
|
+
for (let i = 0; i < targetParamList.namedChildCount; i++) {
|
|
1390
|
+
const param = targetParamList.namedChild(i);
|
|
1391
|
+
if (param?.type === "parameter_declaration" || param?.type === "variadic_parameter_declaration") {
|
|
1392
|
+
const isVariadic = param.type === "variadic_parameter_declaration";
|
|
1393
|
+
const nameNodes = [];
|
|
1394
|
+
for (let j = 0; j < param.namedChildCount; j++) {
|
|
1395
|
+
const c = param.namedChild(j);
|
|
1396
|
+
if (c && c.type === "identifier") nameNodes.push(c);
|
|
1397
|
+
}
|
|
1398
|
+
const typeNode = param.childForFieldName("type");
|
|
1399
|
+
if (nameNodes.length > 0) {
|
|
1400
|
+
for (const nameNode of nameNodes) {
|
|
1401
|
+
if (!nameNode) continue;
|
|
1402
|
+
parameters.push({
|
|
1403
|
+
name: nameNode.text,
|
|
1404
|
+
type: typeNode?.text,
|
|
1405
|
+
isOptional: false,
|
|
1406
|
+
// Go doesn't have optional parameters
|
|
1407
|
+
...isVariadic ? { isVariadic: true } : {}
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
} else if (typeNode) {
|
|
1411
|
+
parameters.push({
|
|
1412
|
+
name: `arg${parameters.length}`,
|
|
1413
|
+
type: typeNode?.text,
|
|
1414
|
+
isOptional: false,
|
|
1415
|
+
// Go doesn't have optional parameters
|
|
1416
|
+
...isVariadic ? { isVariadic: true } : {}
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return parameters;
|
|
1422
|
+
}
|
|
1423
|
+
extractGoReturnType(node) {
|
|
1424
|
+
const resultNode = node.childForFieldName("result");
|
|
1425
|
+
if (resultNode) {
|
|
1426
|
+
if (resultNode.type === "parameter_list") {
|
|
1427
|
+
const types = [];
|
|
1428
|
+
for (let i = 0; i < resultNode.namedChildCount; i++) {
|
|
1429
|
+
const p = resultNode.namedChild(i);
|
|
1430
|
+
if (!p || p.type !== "parameter_declaration") continue;
|
|
1431
|
+
const t = p.childForFieldName("type");
|
|
1432
|
+
if (!t) continue;
|
|
1433
|
+
let names = 0;
|
|
1434
|
+
for (let j = 0; j < p.namedChildCount; j++) {
|
|
1435
|
+
const c = p.namedChild(j);
|
|
1436
|
+
if (c && c.type === "identifier") names++;
|
|
1437
|
+
}
|
|
1438
|
+
for (let k = 0; k < Math.max(1, names); k++) types.push(t.text);
|
|
1439
|
+
}
|
|
1440
|
+
if (types.length === 1) return types[0];
|
|
1441
|
+
if (types.length > 1) return `(${types.join(", ")})`;
|
|
1442
|
+
return resultNode.text;
|
|
1443
|
+
}
|
|
1444
|
+
return resultNode.text;
|
|
1445
|
+
}
|
|
1446
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1447
|
+
const child = node.namedChild(i);
|
|
1448
|
+
if (child && inArray(
|
|
1449
|
+
[
|
|
1450
|
+
"type_identifier",
|
|
1451
|
+
"qualified_type",
|
|
1452
|
+
"pointer_type",
|
|
1453
|
+
"slice_type",
|
|
1454
|
+
"array_type",
|
|
1455
|
+
"map_type",
|
|
1456
|
+
"channel_type",
|
|
1457
|
+
"function_type"
|
|
1458
|
+
],
|
|
1459
|
+
child.type
|
|
1460
|
+
)) {
|
|
1461
|
+
return child.text;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return void 0;
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* the 1-based line of a syntax node, or `undefined` when the
|
|
1468
|
+
* node is absent. Never 0 — a site that cannot be named is OMITTED from the
|
|
1469
|
+
* edge (the #820 rule), so a reader can tell "no line" from "line 1".
|
|
1470
|
+
*/
|
|
1471
|
+
nodeLine(node) {
|
|
1472
|
+
if (!node || !node.startPosition || typeof node.startPosition.row !== "number") return void 0;
|
|
1473
|
+
return node.startPosition.row + 1;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* the node that NAMES a call. A `selector_expression` starts at
|
|
1477
|
+
* its RECEIVER, and a receiver-continued chain is valid Go — `.` does not
|
|
1478
|
+
* insert a semicolon, so `builder.` + newline + `Reset()` parses — which makes
|
|
1479
|
+
* the selector's own start the wrong site exactly as the c lane measured on a
|
|
1480
|
+
* newline-continued function-pointer call. The `field` child is the leaf the
|
|
1481
|
+
* edge's target is built from, so it is the leaf that carries the line.
|
|
1482
|
+
*/
|
|
1483
|
+
goCalleeNameNode(funcNode) {
|
|
1484
|
+
if (!funcNode) return void 0;
|
|
1485
|
+
if (funcNode.type === "selector_expression") {
|
|
1486
|
+
return funcNode.childForFieldName("field") ?? funcNode;
|
|
1487
|
+
}
|
|
1488
|
+
return funcNode;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* the callee leaf of a STATEMENT-level call — `defer`/`go`
|
|
1492
|
+
* statements. The statement starts at its keyword (and `defer` does not insert
|
|
1493
|
+
* a semicolon, so `defer` + newline + `cleanup()` is valid Go), so the keyword
|
|
1494
|
+
* is not where the target is named.
|
|
1495
|
+
*/
|
|
1496
|
+
goStatementCalleeNode(node) {
|
|
1497
|
+
const callExpr = this.findChildByType(node, "call_expression");
|
|
1498
|
+
return callExpr ? this.goCalleeNameNode(callExpr.childForFieldName("function")) : void 0;
|
|
1499
|
+
}
|
|
1500
|
+
findGoFunctionCalls(functionNode) {
|
|
1501
|
+
const calls = [];
|
|
1502
|
+
const sites = /* @__PURE__ */ new Map();
|
|
1503
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1504
|
+
const bodyNode = functionNode.childForFieldName("body");
|
|
1505
|
+
if (bodyNode) {
|
|
1506
|
+
const stack = [bodyNode];
|
|
1507
|
+
while (stack.length > 0) {
|
|
1508
|
+
const node = stack.pop();
|
|
1509
|
+
if (node.type === "call_expression") {
|
|
1510
|
+
const funcNode = node.childForFieldName("function");
|
|
1511
|
+
if (funcNode) {
|
|
1512
|
+
const callExpression = this.buildGoCallExpression(funcNode);
|
|
1513
|
+
if (callExpression && !this.isGoBuiltin(callExpression) && !seen.has(callExpression)) {
|
|
1514
|
+
seen.add(callExpression);
|
|
1515
|
+
calls.push(callExpression);
|
|
1516
|
+
sites.set(callExpression, this.goCalleeNameNode(funcNode) ?? funcNode);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
1521
|
+
const child = node.namedChild(i);
|
|
1522
|
+
if (child) stack.push(child);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
this.goCallSites.set(functionNode.startIndex, sites);
|
|
1527
|
+
return calls;
|
|
1528
|
+
}
|
|
1529
|
+
buildGoCallExpression(funcNode) {
|
|
1530
|
+
switch (funcNode.type) {
|
|
1531
|
+
case "identifier":
|
|
1532
|
+
return funcNode.text;
|
|
1533
|
+
case "selector_expression":
|
|
1534
|
+
return this.buildGoSelectorExpression(funcNode);
|
|
1535
|
+
default:
|
|
1536
|
+
return void 0;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
buildGoSelectorExpression(node) {
|
|
1540
|
+
const object = node.childForFieldName("operand");
|
|
1541
|
+
const field = node.childForFieldName("field");
|
|
1542
|
+
if (!object || !field) return node.text;
|
|
1543
|
+
const objectText = object.type === "selector_expression" ? this.buildGoSelectorExpression(object) : object.text;
|
|
1544
|
+
return `${objectText}.${field.text}`;
|
|
1545
|
+
}
|
|
1546
|
+
isGoBuiltin(call) {
|
|
1547
|
+
const builtins = [
|
|
1548
|
+
"make",
|
|
1549
|
+
"new",
|
|
1550
|
+
"len",
|
|
1551
|
+
"cap",
|
|
1552
|
+
"append",
|
|
1553
|
+
"copy",
|
|
1554
|
+
"delete",
|
|
1555
|
+
"panic",
|
|
1556
|
+
"recover",
|
|
1557
|
+
"print",
|
|
1558
|
+
"println",
|
|
1559
|
+
"close"
|
|
1560
|
+
];
|
|
1561
|
+
const segs = call.split(".");
|
|
1562
|
+
return inArray(builtins, segs[segs.length - 1]);
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Resolve a goroutine/defer call target through the SAME bare-call symbol
|
|
1566
|
+
* contract as analyzeGoFunctionCalls: package-qualified (has a dot) emits
|
|
1567
|
+
* the leaf unconditionally; bare names emit only if they name an in-file
|
|
1568
|
+
* declared function/method symbol. This drops closure-variable callees
|
|
1569
|
+
* (`go done()`, `defer release()` where done/release are var-assigned
|
|
1570
|
+
* funcs, NOT declared symbols) so goroutine/defer edges stay consistent
|
|
1571
|
+
* with the analyzer's own call resolution and with the differential
|
|
1572
|
+
* oracle — which derives only declared-function + qualified leaves.
|
|
1573
|
+
* GO3: with a dot-import in the file, bare names may be dot-imported
|
|
1574
|
+
* members — resolve leniently (besides builtins, filtered by callers).
|
|
1575
|
+
*/
|
|
1576
|
+
/**
|
|
1577
|
+
* Is this symbol a name a BARE call expression in this file could resolve to?
|
|
1578
|
+
*
|
|
1579
|
+
* A Go `method_spec` is published as `kind: Method` (it is a method
|
|
1580
|
+
* declaration, in an interface body), and both the bare-call resolver and the
|
|
1581
|
+
* package-scope-call predicate accepted it. So a name that exists in the file
|
|
1582
|
+
* ONLY as an interface method — `type I interface{ Handle(x int) error }`
|
|
1583
|
+
* with no `func Handle` anywhere — resolved as a package-scope callable and
|
|
1584
|
+
* the analyzer emitted `calls global -> Handle`.
|
|
1585
|
+
*
|
|
1586
|
+
* That edge is a FABRICATION, and the differential called it one
|
|
1587
|
+
* (`UNEXPLAINED`): the F5 oracle's `declaredFns` is built from
|
|
1588
|
+
* `function_declaration` + `method_declaration` only, because those are the
|
|
1589
|
+
* only two forms a bare callee can name in Go. A `method_spec` is not in
|
|
1590
|
+
* scope as a package identifier at all — `Handle(1)` in that file does not
|
|
1591
|
+
* compile. The two sides disagreed, which is the C5 asymmetry class (the
|
|
1592
|
+
* mirror-image defect the lane hit in round 4), and the corpora could not
|
|
1593
|
+
* see it: a file that declares an interface method and calls that name bare
|
|
1594
|
+
* is not a shape either pinned corpus happens to contain.
|
|
1595
|
+
*
|
|
1596
|
+
* The rule is one predicate, applied at all three call-resolution sites, so
|
|
1597
|
+
* the resolver and the two go/defer handlers cannot drift apart again.
|
|
1598
|
+
*/
|
|
1599
|
+
isGoPackageCallable(s) {
|
|
1600
|
+
if (s.kind !== ASTEngine.SymbolKind.Function && s.kind !== ASTEngine.SymbolKind.Method) return false;
|
|
1601
|
+
if (s.metadata?.isInterfaceMethod) return false;
|
|
1602
|
+
return true;
|
|
1603
|
+
}
|
|
1604
|
+
isGoResolvableCallTarget(target, symbols, dotImported = false) {
|
|
1605
|
+
if (!target) return false;
|
|
1606
|
+
if (target.indexOf(".") !== -1) return true;
|
|
1607
|
+
if (dotImported) return true;
|
|
1608
|
+
return someSymbol(symbols, (s) => this.isGoPackageCallable(s) && s.name === target);
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Resolve the callable LEAF name of a callee node, for goroutine/defer/
|
|
1612
|
+
* channel handlers. Anonymous func-literal callees (`go func(){...}()`,
|
|
1613
|
+
* `defer func(){...}()`) and non-named callees have NO static name and
|
|
1614
|
+
* must be skipped — the pre-fix handlers returned the whole func-literal
|
|
1615
|
+
* text / "anonymous_closure", fabricating garbage calls targets (found
|
|
1616
|
+
* #261-family Go wave 2026-08-28, same class as Rust's `(self.0)` fix).
|
|
1617
|
+
* Selector callees (`s.start`) resolve to the field LEAF (family contract).
|
|
1618
|
+
*/
|
|
1619
|
+
resolveGoCallLeaf(funcNode) {
|
|
1620
|
+
if (!funcNode) return void 0;
|
|
1621
|
+
if (funcNode.type === "identifier") return funcNode.text;
|
|
1622
|
+
if (funcNode.type === "selector_expression") {
|
|
1623
|
+
const field = funcNode.childForFieldName("field");
|
|
1624
|
+
return field?.text;
|
|
1625
|
+
}
|
|
1626
|
+
return void 0;
|
|
1627
|
+
}
|
|
1628
|
+
// Framework detection
|
|
1629
|
+
detectGoFrameworks(imports) {
|
|
1630
|
+
const frameworks = [];
|
|
1631
|
+
const modules = mapSymbols(imports, (imp) => imp.module || "");
|
|
1632
|
+
forEachSymbol(GO_FRAMEWORK_RULES, (rule) => {
|
|
1633
|
+
if (!rule.listed) return;
|
|
1634
|
+
if (!someSymbol(modules, (m) => rule.matches(m))) return;
|
|
1635
|
+
frameworks.push({ name: rule.name, confidence: rule.confidence, patterns: [rule.name] });
|
|
1636
|
+
});
|
|
1637
|
+
return frameworks;
|
|
1638
|
+
}
|
|
1639
|
+
detectGoPatterns(rootNode, symbols) {
|
|
1640
|
+
const patterns = [];
|
|
1641
|
+
const stack = [rootNode];
|
|
1642
|
+
while (stack.length > 0) {
|
|
1643
|
+
const node = stack.pop();
|
|
1644
|
+
switch (node.type) {
|
|
1645
|
+
case "go_statement":
|
|
1646
|
+
if (!inArray(patterns, "goroutines")) patterns.push("goroutines");
|
|
1647
|
+
break;
|
|
1648
|
+
case "defer_statement":
|
|
1649
|
+
if (!inArray(patterns, "defer")) patterns.push("defer");
|
|
1650
|
+
break;
|
|
1651
|
+
case "send_statement":
|
|
1652
|
+
case "receive_statement":
|
|
1653
|
+
case "communication_case":
|
|
1654
|
+
case "channel_type":
|
|
1655
|
+
if (!inArray(patterns, "channels")) patterns.push("channels");
|
|
1656
|
+
break;
|
|
1657
|
+
case "interface_type":
|
|
1658
|
+
if (node.namedChildCount === 0) {
|
|
1659
|
+
if (!inArray(patterns, "empty_interface")) patterns.push("empty_interface");
|
|
1660
|
+
}
|
|
1661
|
+
break;
|
|
1662
|
+
case "if_statement":
|
|
1663
|
+
const condition = node.childForFieldName("condition");
|
|
1664
|
+
if (condition && stringIncludes(condition.text, "err") && stringIncludes(condition.text, "!=")) {
|
|
1665
|
+
if (!inArray(patterns, "error_handling")) patterns.push("error_handling");
|
|
1666
|
+
}
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
1670
|
+
const child = node.namedChild(i);
|
|
1671
|
+
if (child) stack.push(child);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
if (someSymbol(symbols, (s) => s.metadata?.isInterface)) {
|
|
1675
|
+
if (!inArray(patterns, "interfaces")) patterns.push("interfaces");
|
|
1676
|
+
}
|
|
1677
|
+
if (someSymbol(symbols, (s) => s.metadata?.isStruct)) {
|
|
1678
|
+
if (!inArray(patterns, "structs")) patterns.push("structs");
|
|
1679
|
+
}
|
|
1680
|
+
return patterns;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Go Relationship Analysis
|
|
1684
|
+
* Analyzes Go-specific relationships: struct embedding, interface implementation, method receivers, package dependencies
|
|
1685
|
+
*/
|
|
1686
|
+
analyzeGoRelationships(rootNode, relationships, symbols, _ctx, stdlibQualifiers = /* @__PURE__ */ new Map()) {
|
|
1687
|
+
this.analyzeGoStructEmbedding(rootNode, relationships);
|
|
1688
|
+
this.analyzeGoMethodPromotionShadowing(relationships, symbols);
|
|
1689
|
+
this.analyzeGoInterfaceImplementation(rootNode, relationships, symbols);
|
|
1690
|
+
this.analyzeGoFunctionCalls(rootNode, relationships, symbols, stdlibQualifiers);
|
|
1691
|
+
this.analyzeGoPackageScopeCalls(rootNode, relationships, symbols, stdlibQualifiers);
|
|
1692
|
+
this.analyzeGoCompositeLiteralConstruction(rootNode, relationships, symbols, stdlibQualifiers);
|
|
1693
|
+
this.analyzeGoSpecificPatterns(rootNode, relationships, symbols);
|
|
1694
|
+
this.analyzeGoSelectorUses(rootNode, relationships, symbols);
|
|
1695
|
+
this.analyzeGoBareIdentifierUses(rootNode, relationships, symbols);
|
|
1696
|
+
for (const s of symbols) {
|
|
1697
|
+
const owner = s.ownerName;
|
|
1698
|
+
if (owner && owner !== s.name) {
|
|
1699
|
+
relationships.push({
|
|
1700
|
+
source: owner,
|
|
1701
|
+
target: s.name,
|
|
1702
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1703
|
+
confidence: 1,
|
|
1704
|
+
// the member's own declaration line (its name node, so
|
|
1705
|
+
// a wrapped declaration reports where the name is written).
|
|
1706
|
+
...s.range?.startLine ? { line_number: s.range.startLine } : {},
|
|
1707
|
+
metadata: {}
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Go method promotion SHADOWING — the `overrides` relationship.
|
|
1714
|
+
*
|
|
1715
|
+
* WHY THIS IS AN OVERRIDE AND NOT AN INVENTED EDGE. Go has no `override`
|
|
1716
|
+
* keyword, but it has the SEMANTIC: a method declared on the outer type at
|
|
1717
|
+
* depth 0 replaces the promoted method of the same name at depth 1 for every
|
|
1718
|
+
* selector `x.M` (Go spec, selectors: the shallowest depth wins). The family
|
|
1719
|
+
* already models this half — the embedding itself is a `mixes_in` edge
|
|
1720
|
+
* (`go_struct_embedding`: the STRUCT half; the interface half is `extends`).
|
|
1721
|
+
* What was missing is the second half: the analyzer
|
|
1722
|
+
* COMPUTED the shadowing inside `collectGoPromotedMethods` and THREW IT AWAY,
|
|
1723
|
+
* dropping the promoted method from the method set without recording that it
|
|
1724
|
+
* had been displaced. So the relationship was derivable from this file's own
|
|
1725
|
+
* evidence, was never emitted, and the differential could not charge for it —
|
|
1726
|
+
* the exact shape `overrides` was omitted for in bash.
|
|
1727
|
+
*
|
|
1728
|
+
* MEASURED, not ruled — `script/m14/go-ontology-census.ts` probe B2/B3 on
|
|
1729
|
+
* go-stdlib + cobra. See docs/analyzers/go.md for the counts.
|
|
1730
|
+
*
|
|
1731
|
+
* ENDPOINT SPELLING follows the family: a method has no package-scope name of
|
|
1732
|
+
* its own, so the SHADOWING end is `Receiver.Method` (perl, objc, python,
|
|
1733
|
+
* kotlin, dart, swift, php, cpp all publish `overrides|.` as a recorded
|
|
1734
|
+
* separator triple in validation/m60/target-spelling-register.json).
|
|
1735
|
+
*
|
|
1736
|
+
* THE TARGET IS THE MEMBER NAME ALONE (name-surface round, #924). A
|
|
1737
|
+
* relationship target is a NAME the lane publishes, and `Embedder.Method` is a
|
|
1738
|
+
* selector chain — Go has no qualified declaration names. So the promoted
|
|
1739
|
+
* method publishes as its bare name, with `metadata.embedder` and
|
|
1740
|
+
* `metadata.targetQualified` preserving the pair `Embedder.Method` that names
|
|
1741
|
+
* WHICH declaration was displaced. The source keeps its qualified spelling
|
|
1742
|
+
* because a Go method has no package-scope name of its own (the endpoint
|
|
1743
|
+
* ambiguity the fixtures pin); only the target was ever source text on a
|
|
1744
|
+
* name-bearing surface, and this is the rule that built it, fixed at the
|
|
1745
|
+
* source rather than filtered afterwards.
|
|
1746
|
+
*
|
|
1747
|
+
* THE NAME RULE IS GO's, not the family's: the match is on the method NAME
|
|
1748
|
+
* alone. A signature-compatible shadow is the only shape other languages
|
|
1749
|
+
* accept, but Go permits `T.M` and `E.M` to have different signatures, and
|
|
1750
|
+
* `x.M` selects T's either way — so name-only is the fact, and a
|
|
1751
|
+
* signature-matching subset would under-report it. `sameSignature` records
|
|
1752
|
+
* which of the two it is, so a consumer can tell a substitutable shadow from a
|
|
1753
|
+
* hiding one without re-deriving the signature.
|
|
1754
|
+
*
|
|
1755
|
+
* CONFIDENCE 0.95, BELOW 1.0 AND NOT 1.0, with a reason code per the M14
|
|
1756
|
+
* contract : the two declarations are read from this file, but the outer
|
|
1757
|
+
* type's promoted set is resolved against the embedding closure as THIS FILE
|
|
1758
|
+
* declares it. A method promoted from another file of the same package is not
|
|
1759
|
+
* in the closure, so this edge's absence is not evidence of absence — see the
|
|
1760
|
+
* lane's declared limitations.
|
|
1761
|
+
*/
|
|
1762
|
+
analyzeGoMethodPromotionShadowing(relationships, symbols) {
|
|
1763
|
+
const embeddedOf = /* @__PURE__ */ Object.create(null);
|
|
1764
|
+
for (const r of relationships) {
|
|
1765
|
+
{
|
|
1766
|
+
const rt = String(r.type || r.relationshipType || "");
|
|
1767
|
+
const constructId = r.metadata?.relationshipType;
|
|
1768
|
+
if (constructId !== "go_struct_embedding" || rt !== ASTEngine.RelationshipType.MixesIn && rt !== ASTEngine.RelationshipType.Extends) {
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const src = String(r.source ?? r.from ?? "");
|
|
1773
|
+
const tgt = String(r.target ?? r.to ?? r.toSymbol ?? "");
|
|
1774
|
+
if (!src || !tgt) continue;
|
|
1775
|
+
const raw = String(r.metadata?.embeddedStruct ?? tgt);
|
|
1776
|
+
if (raw.includes(".")) continue;
|
|
1777
|
+
if (!embeddedOf[src]) embeddedOf[src] = [];
|
|
1778
|
+
if (!embeddedOf[src].includes(tgt)) embeddedOf[src].push(tgt);
|
|
1779
|
+
}
|
|
1780
|
+
if (Object.keys(embeddedOf).length === 0) return;
|
|
1781
|
+
const methods = filterSymbols(symbols, (s) => s.kind === ASTEngine.SymbolKind.Method);
|
|
1782
|
+
const byReceiver = /* @__PURE__ */ Object.create(null);
|
|
1783
|
+
for (const m of methods) {
|
|
1784
|
+
const r = m.metadata?.receiverType || m.metadata?.receiver?.typeName;
|
|
1785
|
+
if (typeof r !== "string" || !r) continue;
|
|
1786
|
+
if (!byReceiver[r]) byReceiver[r] = [];
|
|
1787
|
+
byReceiver[r].push(m);
|
|
1788
|
+
}
|
|
1789
|
+
if (Object.keys(byReceiver).length === 0) return;
|
|
1790
|
+
const depths = /* @__PURE__ */ new Map();
|
|
1791
|
+
for (const m of methods) {
|
|
1792
|
+
const outer = m.metadata?.receiverType || m.metadata?.receiver?.typeName;
|
|
1793
|
+
if (typeof outer !== "string" || !outer) continue;
|
|
1794
|
+
const ownSig = this.normalizeGoSignature(this.buildGoMethodSignature(m));
|
|
1795
|
+
const promoted = (tname, depth, visiting) => {
|
|
1796
|
+
const out = [];
|
|
1797
|
+
if (depth > 4 || visiting.has(tname)) return out;
|
|
1798
|
+
visiting.add(tname);
|
|
1799
|
+
for (const et of embeddedOf[tname] ?? []) {
|
|
1800
|
+
for (const em of byReceiver[et] ?? []) out.push({ m: em, depth: depth + 1 });
|
|
1801
|
+
for (const em of promoted(et, depth + 1, visiting)) out.push(em);
|
|
1802
|
+
}
|
|
1803
|
+
visiting.delete(tname);
|
|
1804
|
+
return out;
|
|
1805
|
+
};
|
|
1806
|
+
for (const entry of promoted(outer, 0, /* @__PURE__ */ new Set())) {
|
|
1807
|
+
const em = entry.m;
|
|
1808
|
+
if (!em || em.name !== m.name) continue;
|
|
1809
|
+
const embedder = em.metadata?.receiverType || em.metadata?.receiver?.typeName;
|
|
1810
|
+
if (typeof embedder !== "string" || !embedder || embedder === outer) continue;
|
|
1811
|
+
const key = `${outer}.${m.name}|${embedder}.${em.name}`;
|
|
1812
|
+
const prior = depths.get(key);
|
|
1813
|
+
if (prior !== void 0 && prior <= entry.depth) continue;
|
|
1814
|
+
depths.set(key, entry.depth);
|
|
1815
|
+
relationships.push({
|
|
1816
|
+
source: `${outer}.${m.name}`,
|
|
1817
|
+
// THE TARGET IS THE MEMBER NAME, NOT A SELECTOR CHAIN (name-surface
|
|
1818
|
+
// round, #924): a relationship target is a NAME the lane publishes,
|
|
1819
|
+
// and `Embedder.Method` is source text with a separator in it — Go has
|
|
1820
|
+
// no qualified declaration names. The SHADOWING declaration keeps its
|
|
1821
|
+
// qualified spelling on the SOURCE (a Go method has no package-scope
|
|
1822
|
+
// name of its own; that is the endpoint-ambiguity rule the fixtures
|
|
1823
|
+
// pin), and the promoting pair moves to `metadata.targetQualified` /
|
|
1824
|
+
// `metadata.embedder` so no fact is lost with the spelling.
|
|
1825
|
+
target: em.name,
|
|
1826
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
1827
|
+
confidence: 0.95,
|
|
1828
|
+
// the SHADOWING declaration's own line — the site that
|
|
1829
|
+
// makes `x.M` select this method rather than the promoted one.
|
|
1830
|
+
...m.range?.startLine ? { line_number: m.range.startLine } : {},
|
|
1831
|
+
metadata: {
|
|
1832
|
+
relationshipType: "go_method_promotion_shadowing",
|
|
1833
|
+
isOverride: true,
|
|
1834
|
+
// THE PROMOTING TYPE, and the pair spelling it used to publish as the
|
|
1835
|
+
// TARGET before the name-surface round. The target is now the member
|
|
1836
|
+
// name alone, so these two fields carry the identity that distinguishes
|
|
1837
|
+
// two embedders declaring the same method name.
|
|
1838
|
+
embedder,
|
|
1839
|
+
targetQualified: `${embedder}.${em.name}`,
|
|
1840
|
+
// contract : confidence < 1.0 carries a machine-readable reason.
|
|
1841
|
+
reasonCode: "promoted-method-shadowed-by-outer-declaration",
|
|
1842
|
+
// True when the two signatures match, i.e. this is a substitutable
|
|
1843
|
+
// override; false when the outer method merely HIDES the promoted one
|
|
1844
|
+
// (legal in Go, and still the method `x.M` selects).
|
|
1845
|
+
sameSignature: this.normalizeGoSignature(this.buildGoMethodSignature(em)) === ownSig,
|
|
1846
|
+
shadowedAtDepth: 0,
|
|
1847
|
+
promotedAtDepth: entry.depth
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Go Struct Embedding Analysis
|
|
1855
|
+
* Analyzes struct embedding (anonymous fields) which is Go's composition pattern
|
|
1856
|
+
*/
|
|
1857
|
+
analyzeGoStructEmbedding(rootNode, relationships) {
|
|
1858
|
+
const stack = [rootNode];
|
|
1859
|
+
while (stack.length > 0) {
|
|
1860
|
+
const node = stack.pop();
|
|
1861
|
+
if (node.type === "type_declaration") {
|
|
1862
|
+
const typeSpecs = [];
|
|
1863
|
+
for (let ti = 0; ti < node.namedChildCount; ti++) {
|
|
1864
|
+
const ts = node.namedChild(ti);
|
|
1865
|
+
if (ts?.type === "type_spec" || ts?.type === "type_alias") typeSpecs.push(ts);
|
|
1866
|
+
}
|
|
1867
|
+
for (const typeSpec of typeSpecs) {
|
|
1868
|
+
const nameNode = typeSpec.childForFieldName("name");
|
|
1869
|
+
const typeName = nameNode?.text;
|
|
1870
|
+
if (!typeName) continue;
|
|
1871
|
+
const structSpec = this.findChildByType(typeSpec, "struct_type");
|
|
1872
|
+
if (structSpec) {
|
|
1873
|
+
const embeddedTypeNodes = this.extractGoEmbeddedTypeNodes(structSpec);
|
|
1874
|
+
forEachSymbol(embeddedTypeNodes, (embeddedType) => {
|
|
1875
|
+
const rawEmbedded = String(embeddedType.text);
|
|
1876
|
+
const embedLeaf = this.goEmbeddedFieldName(embeddedType);
|
|
1877
|
+
if (!embedLeaf) return;
|
|
1878
|
+
relationships.push({
|
|
1879
|
+
source: typeName,
|
|
1880
|
+
target: embedLeaf,
|
|
1881
|
+
// THE RELABEL: stored type `extends` -> `mixes_in`, with the
|
|
1882
|
+
// confidence (0.8) and therefore the evidenceClass UNCHANGED —
|
|
1883
|
+
// that is the relabel contract, and it is what keeps this a
|
|
1884
|
+
// label move rather than a distribution change.
|
|
1885
|
+
type: ASTEngine.RelationshipType.MixesIn,
|
|
1886
|
+
confidence: 0.8,
|
|
1887
|
+
// the EMBEDDED TYPE's line — the site the
|
|
1888
|
+
// embedded declaration is written on.
|
|
1889
|
+
...this.nodeLine(embeddedType) ? { line_number: this.nodeLine(embeddedType) } : {},
|
|
1890
|
+
metadata: {
|
|
1891
|
+
// THE CONSTRUCT ID IS KEPT: `go_struct_embedding` names the
|
|
1892
|
+
// construct and is what every consumer (including this
|
|
1893
|
+
// analyzer's own two passes) keys on, so the label move does
|
|
1894
|
+
// not need a metadata move as well.
|
|
1895
|
+
relationshipType: "go_struct_embedding",
|
|
1896
|
+
isEmbedding: true,
|
|
1897
|
+
isAnonymous: true,
|
|
1898
|
+
isMixin: true,
|
|
1899
|
+
embedType: "struct_composition_mixes_in",
|
|
1900
|
+
embeddedStruct: rawEmbedded
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
const interfaceSpec = this.findChildByType(typeSpec, "interface_type");
|
|
1906
|
+
if (interfaceSpec) {
|
|
1907
|
+
for (let j = 0; j < interfaceSpec.namedChildCount; j++) {
|
|
1908
|
+
const elem = interfaceSpec.namedChild(j);
|
|
1909
|
+
if (!elem || elem.type !== "constraint_elem") continue;
|
|
1910
|
+
const inner = elem.namedChildCount > 0 ? elem.namedChild(0) : void 0;
|
|
1911
|
+
if (!inner) continue;
|
|
1912
|
+
if (inner.type !== "type_identifier" && inner.type !== "qualified_type") continue;
|
|
1913
|
+
const raw = inner.text;
|
|
1914
|
+
const leaf = raw.lastIndexOf(".") !== -1 ? raw.slice(raw.lastIndexOf(".") + 1) : raw;
|
|
1915
|
+
if (!leaf || !this.isPlainGoIdentifier(leaf)) continue;
|
|
1916
|
+
relationships.push({
|
|
1917
|
+
source: typeName,
|
|
1918
|
+
target: leaf,
|
|
1919
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
1920
|
+
confidence: 0.8,
|
|
1921
|
+
// the embedded interface reference's own line.
|
|
1922
|
+
...this.nodeLine(inner) ? { line_number: this.nodeLine(inner) } : {},
|
|
1923
|
+
metadata: {
|
|
1924
|
+
relationshipType: "go_interface_embedding",
|
|
1925
|
+
isEmbedding: true,
|
|
1926
|
+
isAnonymous: true,
|
|
1927
|
+
embedType: "interface_inheritance_extends",
|
|
1928
|
+
embeddedInterface: raw
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
1936
|
+
const child = node.namedChild(i);
|
|
1937
|
+
if (child) stack.push(child);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* RENDERS — Go's render host is the TEMPLATE EXECUTION, and the rendered
|
|
1943
|
+
* component must be a PUBLISHED SYMBOL.
|
|
1944
|
+
*
|
|
1945
|
+
* WHY THIS IS A RELATION AND NOT A CALL. `pageTemplate.Execute(w, data)` is a
|
|
1946
|
+
* method call and the `calls` pass already publishes it; `renders` asserts the
|
|
1947
|
+
* DIFFERENT fact the call performs: this symbol puts THIS template's output on
|
|
1948
|
+
* the wire. The family's renders contract is "the symbol whose body creates
|
|
1949
|
+
* the rendered component renders it" (jsx/tsx; dart's build method returning a
|
|
1950
|
+
* Widget), and Go's standard-library equivalent is `html/template` execution —
|
|
1951
|
+
* the template IS the rendered component. The call edge stays: it is a real
|
|
1952
|
+
* invocation, and one site can honestly carry both facts.
|
|
1953
|
+
*
|
|
1954
|
+
* THE RENDER HOST FILTER IS THE IMPORT, NOT THE NAME. The file must import
|
|
1955
|
+
* `html/template`, the HTML renderer. `text/template` is the measured IMPOSTOR
|
|
1956
|
+
* class — 151 of the corpus's template-importing files use it and almost all
|
|
1957
|
+
* are CODE GENERATORS, not renderers — so a text/template-only file publishes
|
|
1958
|
+
* no renders row, and a file that aliases text/template as a SECOND import
|
|
1959
|
+
* cannot smuggle one in: the constructor's QUALIFIER is resolved against the
|
|
1960
|
+
* html/template import's own local name, never against the spelling
|
|
1961
|
+
* "template". A dot-imported (`import . "html/template"`) constructor cannot
|
|
1962
|
+
* be qualified, so it publishes no row — declared MUST-NOT, not a guess.
|
|
1963
|
+
*
|
|
1964
|
+
* THE TARGET MUST BE A PUBLISHED SYMBOL. A template value is a component only
|
|
1965
|
+
* when the program NAMES it: a package-level var whose initializer constructs
|
|
1966
|
+
* a template (`var page = template.Must(template.New("page").Parse(...))`).
|
|
1967
|
+
* A local template (`tpl := template.Must(...)`), a parameter, a struct field,
|
|
1968
|
+
* a call result or an expression receiver publishes NO row: Go locals are not
|
|
1969
|
+
* symbols (LocalsAreNotSymbols), and a NAME STRING would let the arbiter bind
|
|
1970
|
+
* an unrelated declaration — the false-edge direction this programme refuses.
|
|
1971
|
+
* Cross-file execution is therefore silent too: a per-file analyzer cannot
|
|
1972
|
+
* prove that a bare identifier from ANOTHER file of the package is a template,
|
|
1973
|
+
* and the limitation is declared rather than fabricated around.
|
|
1974
|
+
*
|
|
1975
|
+
* LINE: the OPERAND's line — the rendered symbol's own name at the call site.
|
|
1976
|
+
* A method selector can wrap (`pageTemplate.\n\tExecute(...)`), so the Execute
|
|
1977
|
+
* token's line is a different, wrong site (a row's line is the
|
|
1978
|
+
* name-bearing node's line).
|
|
1979
|
+
*
|
|
1980
|
+
* CONFIDENCE 0.9 / usage_contextual, the render family's tier: both ends are
|
|
1981
|
+
* read from this file, but the template's identity is recognised from its
|
|
1982
|
+
* CONSTRUCTOR CALL rather than a declared type (tree-sitter-go has no type
|
|
1983
|
+
* resolution), so the row is not `structural`.
|
|
1984
|
+
*/
|
|
1985
|
+
analyzeGoTemplateRenders(rootNode, imports, relationships) {
|
|
1986
|
+
let hostName;
|
|
1987
|
+
let dotImported = false;
|
|
1988
|
+
for (const imp of imports) {
|
|
1989
|
+
if ((imp.module ?? "") !== "html/template") continue;
|
|
1990
|
+
const alias = typeof imp.alias === "string" ? String(imp.alias) : "";
|
|
1991
|
+
if (alias === "_") continue;
|
|
1992
|
+
if (alias === ".") {
|
|
1993
|
+
dotImported = true;
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
hostName = alias.length > 0 ? alias : "template";
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
if (!hostName || dotImported) return;
|
|
2000
|
+
const templateSymbols = /* @__PURE__ */ new Map();
|
|
2001
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
2002
|
+
const decl = rootNode.namedChild(i);
|
|
2003
|
+
if (!decl || decl.type !== "var_declaration") continue;
|
|
2004
|
+
for (let j = 0; j < decl.namedChildCount; j++) {
|
|
2005
|
+
const spec = decl.namedChild(j);
|
|
2006
|
+
if (!spec || spec.type !== "var_spec") continue;
|
|
2007
|
+
const value = spec.childForFieldName?.("value");
|
|
2008
|
+
if (!value) continue;
|
|
2009
|
+
const constructed = this.goTemplateConstructionName(value, hostName);
|
|
2010
|
+
if (constructed === null) continue;
|
|
2011
|
+
for (let k = 0; k < spec.namedChildCount; k++) {
|
|
2012
|
+
const c = spec.namedChild(k);
|
|
2013
|
+
if (!c || c.type !== "identifier") continue;
|
|
2014
|
+
const text = String(c.text ?? "");
|
|
2015
|
+
if (text.length === 0 || text === "_") continue;
|
|
2016
|
+
templateSymbols.set(text, constructed);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
if (templateSymbols.size === 0) return;
|
|
2021
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2022
|
+
const stack = [rootNode];
|
|
2023
|
+
while (stack.length > 0) {
|
|
2024
|
+
const node = stack.pop();
|
|
2025
|
+
if (node.type === "call_expression") {
|
|
2026
|
+
const fn = node.childForFieldName("function");
|
|
2027
|
+
if (fn && fn.type === "selector_expression") {
|
|
2028
|
+
const field = fn.childForFieldName("field");
|
|
2029
|
+
const operand = fn.childForFieldName("operand");
|
|
2030
|
+
const method = field ? String(field.text ?? "") : "";
|
|
2031
|
+
if ((method === "Execute" || method === "ExecuteTemplate") && operand && operand.type === "identifier") {
|
|
2032
|
+
const target = String(operand.text ?? "");
|
|
2033
|
+
const constructedName = templateSymbols.get(target);
|
|
2034
|
+
if (constructedName !== void 0) {
|
|
2035
|
+
const source = this.findContainingGoFunction(node);
|
|
2036
|
+
const key = `${source ?? ""}|${target}`;
|
|
2037
|
+
if (source && !seen.has(key)) {
|
|
2038
|
+
seen.add(key);
|
|
2039
|
+
const templateName = method === "ExecuteTemplate" ? this.goTemplateRenderNameArgument(node, constructedName) : constructedName;
|
|
2040
|
+
relationships.push({
|
|
2041
|
+
source,
|
|
2042
|
+
target,
|
|
2043
|
+
type: ASTEngine.RelationshipType.Renders,
|
|
2044
|
+
confidence: 0.9,
|
|
2045
|
+
...this.nodeLine(operand) ? { line_number: this.nodeLine(operand) } : {},
|
|
2046
|
+
metadata: {
|
|
2047
|
+
relationshipType: "go_template_render",
|
|
2048
|
+
isRenderHost: true,
|
|
2049
|
+
renderHost: "html/template",
|
|
2050
|
+
renderMethod: method,
|
|
2051
|
+
...templateName ? { templateName } : {}
|
|
2052
|
+
}
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
2060
|
+
const child = node.namedChild(i);
|
|
2061
|
+
if (child) stack.push(child);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Does this initializer construct an `html/template` through the import's own
|
|
2067
|
+
* local name? `null` when it does not construct a template at all; the
|
|
2068
|
+
* template's own `template.New("<literal>")` name when it does; `""` when it
|
|
2069
|
+
* constructs one whose name is a runtime value.
|
|
2070
|
+
*
|
|
2071
|
+
* Recognised root constructors are the standard library's own: Must, New,
|
|
2072
|
+
* ParseFiles, ParseGlob, ParseFS. `Parse` is deliberately NOT a root
|
|
2073
|
+
* constructor: it is a METHOD on a template (`tpl.Parse(...)`), so an
|
|
2074
|
+
* initializer whose root call is `Parse` is not constructing one; the
|
|
2075
|
+
* `template.Must(template.New(...).Parse(...))` chain is recognised through
|
|
2076
|
+
* Must and its arguments are searched for the `New` literal.
|
|
2077
|
+
*/
|
|
2078
|
+
goTemplateConstructionName(node, hostName) {
|
|
2079
|
+
const constructors = ["Must", "New", "ParseFiles", "ParseGlob", "ParseFS"];
|
|
2080
|
+
const asHostCall = (n) => {
|
|
2081
|
+
if (n.type !== "call_expression") return null;
|
|
2082
|
+
const fn = n.childForFieldName("function");
|
|
2083
|
+
if (!fn || fn.type !== "selector_expression") return null;
|
|
2084
|
+
const operand = fn.childForFieldName("operand");
|
|
2085
|
+
const field = fn.childForFieldName("field");
|
|
2086
|
+
if (!operand || operand.type !== "identifier") return null;
|
|
2087
|
+
if (String(operand.text ?? "") !== hostName) return null;
|
|
2088
|
+
if (!field || !inArray(constructors, String(field.text ?? ""))) return null;
|
|
2089
|
+
return n;
|
|
2090
|
+
};
|
|
2091
|
+
const nameOfNew = (call) => {
|
|
2092
|
+
const args = call.childForFieldName("arguments");
|
|
2093
|
+
const first = args?.namedChild(0);
|
|
2094
|
+
if (!first || first.type !== "interpreted_string_literal") return "";
|
|
2095
|
+
return this.stripQuotedString(String(first.text ?? ""));
|
|
2096
|
+
};
|
|
2097
|
+
const found = (n, depth) => {
|
|
2098
|
+
if (depth > 6) return null;
|
|
2099
|
+
const root = asHostCall(n);
|
|
2100
|
+
if (root) {
|
|
2101
|
+
const field = root.childForFieldName("function")?.childForFieldName("field");
|
|
2102
|
+
if (String(field?.text ?? "") === "New") return nameOfNew(root);
|
|
2103
|
+
const args = root.childForFieldName("arguments");
|
|
2104
|
+
for (const a of args?.namedChildren ?? []) {
|
|
2105
|
+
if (!a) continue;
|
|
2106
|
+
const r = found(a, depth + 1);
|
|
2107
|
+
if (r !== null && r.length > 0) return r;
|
|
2108
|
+
}
|
|
2109
|
+
return "";
|
|
2110
|
+
}
|
|
2111
|
+
let sawConstruction = false;
|
|
2112
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2113
|
+
const c = n.namedChild(i);
|
|
2114
|
+
if (!c) continue;
|
|
2115
|
+
const r = found(c, depth + 1);
|
|
2116
|
+
if (r !== null) {
|
|
2117
|
+
sawConstruction = true;
|
|
2118
|
+
if (r.length > 0) return r;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
if (!sawConstruction) return null;
|
|
2122
|
+
return "";
|
|
2123
|
+
};
|
|
2124
|
+
return found(node, 0);
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* The rendered sub-template's name, for `ExecuteTemplate(w, "name", data)`:
|
|
2128
|
+
* its SECOND argument when that argument is a string literal; otherwise the
|
|
2129
|
+
* template's own construction name. `undefined` when neither names it — no
|
|
2130
|
+
* NAME is ever invented for metadata.
|
|
2131
|
+
*/
|
|
2132
|
+
goTemplateRenderNameArgument(call, constructedName) {
|
|
2133
|
+
const args = call.childForFieldName("arguments");
|
|
2134
|
+
const second = args?.namedChild(1);
|
|
2135
|
+
if (second && second.type === "interpreted_string_literal") {
|
|
2136
|
+
const name = this.stripQuotedString(String(second.text ?? ""));
|
|
2137
|
+
if (name.length > 0) return name;
|
|
2138
|
+
}
|
|
2139
|
+
return constructedName.length > 0 ? constructedName : void 0;
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Go Interface Implementation Analysis
|
|
2143
|
+
* Analyzes implicit interface implementation (Go's duck typing)
|
|
2144
|
+
*/
|
|
2145
|
+
analyzeGoInterfaceImplementation(rootNode, relationships, symbols) {
|
|
2146
|
+
const interfaces = filterSymbols(symbols, (s) => s.metadata?.isInterface);
|
|
2147
|
+
const structs = filterSymbols(
|
|
2148
|
+
symbols,
|
|
2149
|
+
(s) => (s.kind === ASTEngine.SymbolKind.Struct || s.kind === ASTEngine.SymbolKind.Type) && !s.metadata?.isInterface
|
|
2150
|
+
);
|
|
2151
|
+
const methods = filterSymbols(symbols, (s) => s.kind === ASTEngine.SymbolKind.Method);
|
|
2152
|
+
const declaredTypeNames = /* @__PURE__ */ new Set();
|
|
2153
|
+
for (const s of symbols) {
|
|
2154
|
+
if (s.kind === ASTEngine.SymbolKind.Struct || s.kind === ASTEngine.SymbolKind.Interface || s.kind === ASTEngine.SymbolKind.Type) {
|
|
2155
|
+
declaredTypeNames.add(s.name);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
const candidates = mapSymbols(structs, (s) => ({
|
|
2159
|
+
name: s.name,
|
|
2160
|
+
hasGenerics: !!s.metadata?.hasGenerics
|
|
2161
|
+
}));
|
|
2162
|
+
for (const m of methods) {
|
|
2163
|
+
const receiverType = m.metadata?.receiverType || m.metadata?.receiver?.typeName;
|
|
2164
|
+
if (typeof receiverType !== "string" || !receiverType) continue;
|
|
2165
|
+
if (declaredTypeNames.has(receiverType)) continue;
|
|
2166
|
+
if (someSymbol(candidates, (c) => c.name === receiverType)) continue;
|
|
2167
|
+
candidates.push({ name: receiverType, hasGenerics: false });
|
|
2168
|
+
}
|
|
2169
|
+
const embeddedOf = /* @__PURE__ */ Object.create(null);
|
|
2170
|
+
for (const r of relationships) {
|
|
2171
|
+
{
|
|
2172
|
+
const rt = String(r.type || r.relationshipType || "");
|
|
2173
|
+
const constructId = r.metadata?.relationshipType;
|
|
2174
|
+
if (constructId !== "go_struct_embedding" || rt !== ASTEngine.RelationshipType.MixesIn && rt !== ASTEngine.RelationshipType.Extends) {
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
const src = String(r.source ?? r.from ?? "");
|
|
2178
|
+
const tgt = String(r.target ?? r.to ?? r.toSymbol ?? "");
|
|
2179
|
+
if (src && tgt) {
|
|
2180
|
+
if (!embeddedOf[src]) embeddedOf[src] = [];
|
|
2181
|
+
embeddedOf[src].push(tgt);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
const promotedByCandidate = /* @__PURE__ */ new Map();
|
|
2186
|
+
forEachSymbol(candidates, (candidate) => {
|
|
2187
|
+
promotedByCandidate.set(
|
|
2188
|
+
candidate.name,
|
|
2189
|
+
this.collectGoPromotedMethods(candidate.name, methods, symbols, embeddedOf, /* @__PURE__ */ new Set())
|
|
2190
|
+
);
|
|
2191
|
+
});
|
|
2192
|
+
forEachSymbol(interfaces, (interfaceSymbol) => {
|
|
2193
|
+
if (interfaceSymbol.metadata?.hasGenerics) return;
|
|
2194
|
+
const interfaceMethods = this.getGoInterfaceMethods(interfaceSymbol, symbols);
|
|
2195
|
+
forEachSymbol(candidates, (candidate) => {
|
|
2196
|
+
if (candidate.hasGenerics) return;
|
|
2197
|
+
const structName = candidate.name;
|
|
2198
|
+
if (structName === interfaceSymbol.name) return;
|
|
2199
|
+
const promoted = promotedByCandidate.get(structName) ?? [];
|
|
2200
|
+
const valueMethods = [
|
|
2201
|
+
...filterSymbols(methods, (m) => {
|
|
2202
|
+
const receiverType = m.metadata?.receiverType || m.metadata?.receiver?.typeName;
|
|
2203
|
+
const isPointer = !!(m.metadata?.isPointerReceiver || m.metadata?.receiver?.isPointer);
|
|
2204
|
+
return receiverType === structName && !isPointer;
|
|
2205
|
+
}),
|
|
2206
|
+
...promoted
|
|
2207
|
+
];
|
|
2208
|
+
const pointerMethods = [
|
|
2209
|
+
...filterSymbols(methods, (m) => {
|
|
2210
|
+
const receiverType = m.metadata?.receiverType || m.metadata?.receiver?.typeName;
|
|
2211
|
+
return receiverType === structName;
|
|
2212
|
+
}),
|
|
2213
|
+
...promoted
|
|
2214
|
+
];
|
|
2215
|
+
const valueImpl = this.checkGoInterfaceImplementation(interfaceMethods, valueMethods);
|
|
2216
|
+
const pointerImpl = this.checkGoInterfaceImplementation(interfaceMethods, pointerMethods);
|
|
2217
|
+
if (valueImpl.implements) {
|
|
2218
|
+
relationships.push({
|
|
2219
|
+
source: structName,
|
|
2220
|
+
target: interfaceSymbol.name,
|
|
2221
|
+
type: ASTEngine.RelationshipType.Implements,
|
|
2222
|
+
confidence: valueImpl.confidence,
|
|
2223
|
+
// the interface declaration's own line — the site
|
|
2224
|
+
// that NAMES the target (the struct's line would name the source).
|
|
2225
|
+
...interfaceSymbol.range?.startLine ? { line_number: interfaceSymbol.range.startLine } : {},
|
|
2226
|
+
metadata: {
|
|
2227
|
+
relationshipType: "go_interface_implementation",
|
|
2228
|
+
isImplicit: true,
|
|
2229
|
+
matchedMethods: valueImpl.matchedMethods,
|
|
2230
|
+
implementationType: "duck_typing",
|
|
2231
|
+
receiverSet: "value"
|
|
2232
|
+
}
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
if (!valueImpl.implements && pointerImpl.implements) {
|
|
2236
|
+
relationships.push({
|
|
2237
|
+
source: structName,
|
|
2238
|
+
target: interfaceSymbol.name,
|
|
2239
|
+
type: ASTEngine.RelationshipType.Implements,
|
|
2240
|
+
confidence: pointerImpl.confidence,
|
|
2241
|
+
...interfaceSymbol.range?.startLine ? { line_number: interfaceSymbol.range.startLine } : {},
|
|
2242
|
+
metadata: {
|
|
2243
|
+
relationshipType: "go_interface_implementation",
|
|
2244
|
+
isImplicit: true,
|
|
2245
|
+
matchedMethods: pointerImpl.matchedMethods,
|
|
2246
|
+
implementationType: "duck_typing",
|
|
2247
|
+
receiverSet: "pointer"
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
});
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Go embedded-struct method promotion (Go spec, promoted methods).
|
|
2256
|
+
* Returns the method-symbol set reachable on `structName` through embedded
|
|
2257
|
+
* structs (the go_struct_embedding closure built in
|
|
2258
|
+
* analyzeGoInterfaceImplementation). `worker struct { *shape; name string }`
|
|
2259
|
+
* promotes shape's methods into worker's method set, so worker satisfies
|
|
2260
|
+
* any interface shape satisfies. Transitive and cycle-safe. NOTE: deduped
|
|
2261
|
+
* by method signature (name+params+return); a struct defining its own
|
|
2262
|
+
* method with the same signature as a promoted one shadows the promotion
|
|
2263
|
+
* (Go rule: a promoted method at shallower depth wins / is shadowed by an
|
|
2264
|
+
* explicit method).
|
|
2265
|
+
*/
|
|
2266
|
+
collectGoPromotedMethods(structName, methods, symbols, embeddedOf, visiting) {
|
|
2267
|
+
if (visiting.has(structName)) return [];
|
|
2268
|
+
visiting.add(structName);
|
|
2269
|
+
const acc = [];
|
|
2270
|
+
const embeddedTypes = embeddedOf[structName];
|
|
2271
|
+
if (embeddedTypes) {
|
|
2272
|
+
for (const et of embeddedTypes) {
|
|
2273
|
+
const embeddedMethods = filterSymbols(
|
|
2274
|
+
methods,
|
|
2275
|
+
(m) => (m.metadata?.receiverType || m.metadata?.receiver?.typeName) === et
|
|
2276
|
+
);
|
|
2277
|
+
const ownSig = (m) => this.buildGoMethodSignature(m);
|
|
2278
|
+
forEachSymbol(embeddedMethods, (m) => {
|
|
2279
|
+
const shadowed = someSymbol(
|
|
2280
|
+
[...filterSymbols(methods, (sm) => (sm.metadata?.receiverType || sm.metadata?.receiver?.typeName) === structName), ...acc],
|
|
2281
|
+
(x) => x !== m && this.buildGoMethodSignature(x) === ownSig(m)
|
|
2282
|
+
);
|
|
2283
|
+
if (!shadowed && !someSymbol(acc, (x) => this.buildGoMethodSignature(x) === ownSig(m))) {
|
|
2284
|
+
acc.push(m);
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
const transitive = this.collectGoPromotedMethods(et, methods, symbols, embeddedOf, visiting);
|
|
2288
|
+
forEachSymbol(transitive, (m) => {
|
|
2289
|
+
const shadowed = someSymbol(
|
|
2290
|
+
[...filterSymbols(methods, (sm) => (sm.metadata?.receiverType || sm.metadata?.receiver?.typeName) === structName), ...acc],
|
|
2291
|
+
(x) => x !== m && this.buildGoMethodSignature(x) === this.buildGoMethodSignature(m)
|
|
2292
|
+
);
|
|
2293
|
+
if (!shadowed && !someSymbol(acc, (x) => this.buildGoMethodSignature(x) === this.buildGoMethodSignature(m))) {
|
|
2294
|
+
acc.push(m);
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
visiting.delete(structName);
|
|
2300
|
+
return acc;
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* THE FILE'S STDLIB PACKAGE QUALIFIERS: the spelling a call site may use for an imported stdlib
|
|
2304
|
+
* package -> that package's import path.
|
|
2305
|
+
*
|
|
2306
|
+
* Go lets a file qualify a package member by the import's LOCAL NAME: the alias when written
|
|
2307
|
+
* (`j "encoding/json"`), otherwise the package's own name, which for the standard library is the
|
|
2308
|
+
* last path segment except for the few packages whose name differs from their directory (and
|
|
2309
|
+
* those are aliased by every consumer that needs the distinction). `_` and `.` imports qualify
|
|
2310
|
+
* nothing and are skipped; two distinct modules claiming one qualifier cannot be distinguished at
|
|
2311
|
+
* a call site, so the qualifier is dropped rather than guessed.
|
|
2312
|
+
*/
|
|
2313
|
+
collectGoStdlibQualifiers(imports) {
|
|
2314
|
+
const out = /* @__PURE__ */ new Map();
|
|
2315
|
+
const conflicted = /* @__PURE__ */ new Set();
|
|
2316
|
+
for (const imp of imports) {
|
|
2317
|
+
const module = String(imp.module ?? "");
|
|
2318
|
+
if (module.length === 0 || !isGoStdlibModulePath(module)) continue;
|
|
2319
|
+
const alias = typeof imp.alias === "string" ? imp.alias : "";
|
|
2320
|
+
if (alias === "_" || alias === ".") continue;
|
|
2321
|
+
const packageName = String(imp.metadata?.packageName ?? this.getGoPackageName(module));
|
|
2322
|
+
const qualifier = alias.length > 0 ? alias : packageName;
|
|
2323
|
+
if (qualifier.length === 0) continue;
|
|
2324
|
+
if (out.has(qualifier) && out.get(qualifier) !== module) {
|
|
2325
|
+
conflicted.add(qualifier);
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (!conflicted.has(qualifier)) out.set(qualifier, module);
|
|
2329
|
+
}
|
|
2330
|
+
for (const q of conflicted) out.delete(q);
|
|
2331
|
+
return out;
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* THE ONE CLAIM A LEAF CALL TARGET CAN CARRY — `isStdlib` when the call is a DIRECT member of a
|
|
2335
|
+
* standard-library package this file imports.
|
|
2336
|
+
*
|
|
2337
|
+
* WHY THIS IS THE CHANNEL. The gate reads `metadata.reasonCode` nowhere; the claims it reads are
|
|
2338
|
+
* `EXTERNAL_KEYS` (verified against the key space) and `baseBeyondRunScope` (verified by spelling
|
|
2339
|
+
* + owner consistency). This lane's unresolved calls are BARE LEAVES (`fmt.Println` publishes
|
|
2340
|
+
* `Println`), and `targetOwner("Println")` is null, so a declared cause can never be attached to
|
|
2341
|
+
* one: measured over the 400-file stride, all 24,385 unresolved edges read `causeRefuted` when
|
|
2342
|
+
* their own reason is placed on that key. The external channel is different by construction — a
|
|
2343
|
+
* bare identifier is looked up directly and, because the claim is read only where the target names
|
|
2344
|
+
* nothing, the lookup returns 0 and the claim verifies without ever being able to manufacture a
|
|
2345
|
+
* refutation. What makes it HONEST is the proof in front of it, not the arbiter: the qualifier is
|
|
2346
|
+
* a package the file itself imports, and the import path's first segment is a member of the Go
|
|
2347
|
+
* standard library's closed top-level set. A receiver variable (`r.reader.SearchWhere`), a local
|
|
2348
|
+
* shadow of an import name, a project-local package and a third-party module all fail that proof
|
|
2349
|
+
* and are left silent.
|
|
2350
|
+
*
|
|
2351
|
+
* WHAT IT DOES NOT DO. It never raises `resolved`, it does not change the emitted target, and it
|
|
2352
|
+
* does not claim anything about a leaf this FILE publishes (then the edge is not external at the
|
|
2353
|
+
* per-file scope the analyzer actually knows). Returns the import path when the claim is true.
|
|
2354
|
+
*/
|
|
2355
|
+
goStdlibCallClaim(callExpression, leaf, symbols, qualifiers, ownPackageName) {
|
|
2356
|
+
const firstDot = callExpression.indexOf(".");
|
|
2357
|
+
if (firstDot <= 0) return null;
|
|
2358
|
+
if (callExpression.indexOf(".", firstDot + 1) !== -1) return null;
|
|
2359
|
+
const qualifier = callExpression.slice(0, firstDot);
|
|
2360
|
+
const module = qualifiers.get(qualifier);
|
|
2361
|
+
if (module === void 0) return null;
|
|
2362
|
+
if (qualifier === ownPackageName) return null;
|
|
2363
|
+
if (findSymbol(symbols, (s) => s.name === qualifier)) return null;
|
|
2364
|
+
if (findSymbol(symbols, (s) => this.isGoPackageCallable(s) && s.name === leaf)) return null;
|
|
2365
|
+
return module;
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* THE SAME PROOF FOR A QUALIFIED TYPE SPELLING — the shared shape behind the `references` and
|
|
2369
|
+
* `instantiates` claims: `context.Context`, `http.Server`, `time.Duration`.
|
|
2370
|
+
*
|
|
2371
|
+
* The differences from `goStdlibCallClaim` are deliberate and are the two ways a type use differs
|
|
2372
|
+
* from a call: the leaf is a TYPE, so any declaration of that name in this file is enough to stop
|
|
2373
|
+
* the claim (a call needs a callable); and a qualified spelling is held for the USE even when the
|
|
2374
|
+
* file also declares the leaf, which is exactly the case where the edge may bind to the wrong
|
|
2375
|
+
* declaration — so the guard matters more here, not less. A deeper chain (`pkg.Sub.Type`) is not
|
|
2376
|
+
* a direct package member and is refused, as on the call surface.
|
|
2377
|
+
*/
|
|
2378
|
+
goStdlibTypeClaim(qualified, leaf, symbols, qualifiers, ownPackageName) {
|
|
2379
|
+
const firstDot = qualified.indexOf(".");
|
|
2380
|
+
if (firstDot <= 0) return null;
|
|
2381
|
+
if (qualified.indexOf(".", firstDot + 1) !== -1) return null;
|
|
2382
|
+
const qualifier = qualified.slice(0, firstDot);
|
|
2383
|
+
const module = qualifiers.get(qualifier);
|
|
2384
|
+
if (module === void 0) return null;
|
|
2385
|
+
if (qualifier === ownPackageName) return null;
|
|
2386
|
+
if (findSymbol(symbols, (s) => s.name === qualifier)) return null;
|
|
2387
|
+
if (findSymbol(symbols, (s) => s.name === leaf)) return null;
|
|
2388
|
+
return module;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Go Function Call Analysis
|
|
2392
|
+
* Analyzes Go function calls including package-qualified calls.
|
|
2393
|
+
* Family contract (Rust M12.18): calls emit LEAF targets — a receiver chain
|
|
2394
|
+
* `s.repo.Save(user)` emits `calls Save -> Save` exactly like Rust's
|
|
2395
|
+
* `self.repository.find(id)` emits `calls find -> find`, and stdlib calls
|
|
2396
|
+
* (`strings.ToUpper`) emit `calls transform -> ToUpper` like Rust's
|
|
2397
|
+
* `calls normalize -> println`. Instantiates fires ONLY for PascalCase
|
|
2398
|
+
* top-level FUNCTION symbols (Go constructor convention `NewXxx`) — never
|
|
2399
|
+
* for methods or unresolved stdlib leaves.
|
|
2400
|
+
*/
|
|
2401
|
+
analyzeGoFunctionCalls(rootNode, relationships, symbols, stdlibQualifiers) {
|
|
2402
|
+
const functions = filterSymbols(
|
|
2403
|
+
symbols,
|
|
2404
|
+
(s) => s.kind === ASTEngine.SymbolKind.Function || s.kind === ASTEngine.SymbolKind.Method
|
|
2405
|
+
);
|
|
2406
|
+
const dotImported = this.hasGoDotImport(rootNode);
|
|
2407
|
+
const ownPackageName = this.goOwnPackageName(rootNode);
|
|
2408
|
+
forEachSymbol(functions, (func) => {
|
|
2409
|
+
const qualifiedCalls = func.qualified_calls || [];
|
|
2410
|
+
const callSites = this.goCallSites.get(func.range?.startIndex ?? -1);
|
|
2411
|
+
forEachSymbol(qualifiedCalls, (call) => {
|
|
2412
|
+
const callLine = this.nodeLine(callSites?.get(call));
|
|
2413
|
+
const targetFunction = this.resolveGoFunctionCall(call, symbols, dotImported);
|
|
2414
|
+
if (targetFunction && this.isGoBuiltin(targetFunction)) {
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
if (targetFunction) {
|
|
2418
|
+
const stdlibModule = this.goStdlibCallClaim(call, targetFunction, symbols, stdlibQualifiers, ownPackageName);
|
|
2419
|
+
relationships.push({
|
|
2420
|
+
source: func.name,
|
|
2421
|
+
target: targetFunction,
|
|
2422
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
2423
|
+
confidence: 0.8,
|
|
2424
|
+
...callLine ? { line_number: callLine } : {},
|
|
2425
|
+
metadata: {
|
|
2426
|
+
relationshipType: "go_function_call",
|
|
2427
|
+
callExpression: call,
|
|
2428
|
+
isPackageQualified: stringIncludes(call, "."),
|
|
2429
|
+
// `isStdlib` is one of the gate's recognised EXTERNAL_KEYS, and the PROOF is the
|
|
2430
|
+
// import written in this file plus the closed top-level stdlib set (`stdlibPath` is
|
|
2431
|
+
// the evidence, kept beside the claim for a reviewer; it is not a claim key).
|
|
2432
|
+
...stdlibModule !== null ? { isStdlib: true, stdlibPath: stdlibModule } : {}
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
const ctorSymbol = findSymbol(
|
|
2436
|
+
symbols,
|
|
2437
|
+
(s) => s.name === targetFunction && s.kind === ASTEngine.SymbolKind.Function && !s.ownerName
|
|
2438
|
+
);
|
|
2439
|
+
const calleeLeaf = callSites?.get(call);
|
|
2440
|
+
const calleeNode = calleeLeaf && calleeLeaf.parent?.type === "selector_expression" ? calleeLeaf.parent : calleeLeaf;
|
|
2441
|
+
if (ctorSymbol && this.isGoTopLevelFunctionCallee(calleeNode, ownPackageName) && ctorSymbol.name[0] === ctorSymbol.name[0].toUpperCase() && ctorSymbol.name[0] !== ctorSymbol.name[0].toLowerCase()) {
|
|
2442
|
+
relationships.push({
|
|
2443
|
+
source: func.name,
|
|
2444
|
+
target: targetFunction,
|
|
2445
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
2446
|
+
confidence: 0.6,
|
|
2447
|
+
...callLine ? { line_number: callLine } : {},
|
|
2448
|
+
metadata: { relationshipType: "go_instantiation", callExpression: call }
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Package-scope call analysis. Captures call_expression nodes that sit at
|
|
2457
|
+
* package level (var/const initializers, top-level assignments) — e.g.
|
|
2458
|
+
* `var _ = intVar(123)`, `var r = regexp.MustCompile(...)`, or
|
|
2459
|
+
* `keys.NewString("view_type", "")`. findGoFunctionCalls only walks
|
|
2460
|
+
* function/method bodies, so these real calls were NEVER emitted (Go wave
|
|
2461
|
+
* 2026-08-28: newexpr.go, session.go NewString, semtok.go MustCompile all
|
|
2462
|
+
* leaked as differential MISSING). Emitted with source = package/"global"
|
|
2463
|
+
* (no enclosing function). Mirrors resolveGoFunctionCall: bare callee emits
|
|
2464
|
+
* only if it names an in-file declared function/method; qualified callee
|
|
2465
|
+
* emits the leaf unconditionally; builtins and func-literals skipped.
|
|
2466
|
+
*/
|
|
2467
|
+
analyzeGoPackageScopeCalls(rootNode, relationships, symbols, stdlibQualifiers) {
|
|
2468
|
+
const declared = /* @__PURE__ */ new Set();
|
|
2469
|
+
const dotImported = this.hasGoDotImport(rootNode);
|
|
2470
|
+
const ownPackageName = this.goOwnPackageName(rootNode);
|
|
2471
|
+
const ctorFns = /* @__PURE__ */ new Set();
|
|
2472
|
+
for (const s of symbols) {
|
|
2473
|
+
if (this.isGoPackageCallable(s)) {
|
|
2474
|
+
declared.add(s.name);
|
|
2475
|
+
}
|
|
2476
|
+
if (s.kind === ASTEngine.SymbolKind.Function && !s.ownerName) {
|
|
2477
|
+
ctorFns.add(s.name);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const stack = [rootNode];
|
|
2481
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2482
|
+
while (stack.length > 0) {
|
|
2483
|
+
const node = stack.pop();
|
|
2484
|
+
if (node.type === "function_declaration" || node.type === "method_declaration") continue;
|
|
2485
|
+
if (node.type === "func_literal" && node.parent?.type !== "call_expression") continue;
|
|
2486
|
+
if (node.type === "call_expression") {
|
|
2487
|
+
const fn = node.childForFieldName("function");
|
|
2488
|
+
if (fn) {
|
|
2489
|
+
const leaf = this.resolveGoCallLeaf(fn);
|
|
2490
|
+
if (fn.type === "func_literal") {
|
|
2491
|
+
for (let i = fn.namedChildCount - 1; i >= 0; i--) {
|
|
2492
|
+
const fc = fn.namedChild(i);
|
|
2493
|
+
if (fc) stack.push(fc);
|
|
2494
|
+
}
|
|
2495
|
+
continue;
|
|
2496
|
+
}
|
|
2497
|
+
if (!leaf) {
|
|
2498
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
2499
|
+
const child = node.namedChild(i);
|
|
2500
|
+
if (child) stack.push(child);
|
|
2501
|
+
}
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
const qualified = fn.type === "selector_expression";
|
|
2505
|
+
const isCtor = leaf[0] === leaf[0].toUpperCase() && leaf[0] !== leaf[0].toLowerCase();
|
|
2506
|
+
const emit = !this.isGoBuiltin(leaf) && (qualified || dotImported || declared.has(leaf));
|
|
2507
|
+
const callSrc = this.resolveGoWorkSource(node);
|
|
2508
|
+
const callKey = `call:${callSrc}:${leaf}`;
|
|
2509
|
+
if (emit && !emitted.has(callKey)) {
|
|
2510
|
+
emitted.add(callKey);
|
|
2511
|
+
const stdlibModule = this.goStdlibCallClaim(fn.text, leaf, symbols, stdlibQualifiers, ownPackageName);
|
|
2512
|
+
relationships.push({
|
|
2513
|
+
source: callSrc,
|
|
2514
|
+
target: leaf,
|
|
2515
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
2516
|
+
confidence: 0.8,
|
|
2517
|
+
// the package-scope walk holds the callee node
|
|
2518
|
+
// directly — its LEAF's line, not the statement's.
|
|
2519
|
+
...this.nodeLine(this.goCalleeNameNode(fn)) ? { line_number: this.nodeLine(this.goCalleeNameNode(fn)) } : {},
|
|
2520
|
+
metadata: {
|
|
2521
|
+
relationshipType: "go_package_scope_call",
|
|
2522
|
+
callExpression: fn.text,
|
|
2523
|
+
isPackageQualified: qualified,
|
|
2524
|
+
...stdlibModule !== null ? { isStdlib: true, stdlibPath: stdlibModule } : {}
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
if (isCtor && ctorFns.has(leaf) && this.isGoTopLevelFunctionCallee(fn, ownPackageName) && !emitted.has("inst:" + leaf)) {
|
|
2529
|
+
emitted.add("inst:" + leaf);
|
|
2530
|
+
relationships.push({
|
|
2531
|
+
source: "global",
|
|
2532
|
+
target: leaf,
|
|
2533
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
2534
|
+
confidence: 0.6,
|
|
2535
|
+
...this.nodeLine(this.goCalleeNameNode(fn)) ? { line_number: this.nodeLine(this.goCalleeNameNode(fn)) } : {},
|
|
2536
|
+
metadata: { relationshipType: "go_package_scope_instantiation", callExpression: fn.text }
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
2542
|
+
const child = node.namedChild(i);
|
|
2543
|
+
if (child) stack.push(child);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Go-Specific Pattern Analysis
|
|
2549
|
+
* Analyzes goroutines, channels, defer statements, and Go idioms
|
|
2550
|
+
*/
|
|
2551
|
+
/**
|
|
2552
|
+
* Go composite-literal construction analysis. `Foo{...}`, `&Foo{...}` and
|
|
2553
|
+
* `pkg.Foo{...}` are Go's primary construction syntax — but before M37
|
|
2554
|
+
* #604 instantiates fired ONLY for NewXxx constructor calls, so struct
|
|
2555
|
+
* construction by literal was invisible to the graph (65,843 composite
|
|
2556
|
+
* literals across go-tools+terraform with zero construction edges). Rust
|
|
2557
|
+
* emits instantiates for struct literals (`Foo { ... }`), csharp for
|
|
2558
|
+
* `new Foo()` — this is family parity, not a new edge kind. Target is the
|
|
2559
|
+
* literal's type LEAF (dotted pkg.Foo -> Foo, matching the calls leaf
|
|
2560
|
+
* contract); source is the containing fn/method or "global" at package
|
|
2561
|
+
* scope. Slice/map/array literals carry a wrapper type child (not a named
|
|
2562
|
+
* type) and elided element literals have no type child — both are skipped
|
|
2563
|
+
* naturally by the type-child gate below.
|
|
2564
|
+
*/
|
|
2565
|
+
analyzeGoCompositeLiteralConstruction(rootNode, relationships, symbols = [], stdlibQualifiers = /* @__PURE__ */ new Map()) {
|
|
2566
|
+
const ownPackageName = this.goOwnPackageName(rootNode);
|
|
2567
|
+
const stack = [rootNode];
|
|
2568
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2569
|
+
while (stack.length > 0) {
|
|
2570
|
+
const node = stack.pop();
|
|
2571
|
+
if (node.type === "ERROR" || node.isMissing) continue;
|
|
2572
|
+
if (node.type === "func_literal" && node.parent?.type !== "call_expression" && !this.findContainingGoFunction(node)) {
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
if (node.type === "composite_literal") {
|
|
2576
|
+
const typeNode = node.childForFieldName("type") ?? (node.namedChildCount > 0 ? node.namedChild(0) : void 0);
|
|
2577
|
+
let target;
|
|
2578
|
+
let qualifiedRaw;
|
|
2579
|
+
if (typeNode) {
|
|
2580
|
+
if (typeNode.type === "type_identifier") {
|
|
2581
|
+
target = typeNode.text;
|
|
2582
|
+
} else if (typeNode.type === "qualified_type") {
|
|
2583
|
+
const raw = typeNode.text;
|
|
2584
|
+
qualifiedRaw = raw;
|
|
2585
|
+
const dot = raw.lastIndexOf(".");
|
|
2586
|
+
target = dot !== -1 ? raw.slice(dot + 1) : raw;
|
|
2587
|
+
} else if (typeNode.type === "generic_type") {
|
|
2588
|
+
const base = typeNode.namedChildCount > 0 ? typeNode.namedChild(0) : void 0;
|
|
2589
|
+
if (base && (base.type === "type_identifier" || base.type === "qualified_type")) {
|
|
2590
|
+
const raw = base.text;
|
|
2591
|
+
if (base.type === "qualified_type") qualifiedRaw = raw;
|
|
2592
|
+
const dot = raw.lastIndexOf(".");
|
|
2593
|
+
target = dot !== -1 ? raw.slice(dot + 1) : raw;
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
if (target && !this.isGoBuiltin(target)) {
|
|
2598
|
+
const src = this.resolveGoWorkSource(node);
|
|
2599
|
+
const key = `${src}.${target}`;
|
|
2600
|
+
if (!seen.has(key)) {
|
|
2601
|
+
seen.add(key);
|
|
2602
|
+
const stdlibModule = qualifiedRaw !== void 0 ? this.goStdlibTypeClaim(qualifiedRaw, target, symbols, stdlibQualifiers, ownPackageName) : null;
|
|
2603
|
+
relationships.push({
|
|
2604
|
+
source: src,
|
|
2605
|
+
target,
|
|
2606
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
2607
|
+
confidence: 0.8,
|
|
2608
|
+
// the constructed TYPE's line.
|
|
2609
|
+
...this.nodeLine(typeNode) ? { line_number: this.nodeLine(typeNode) } : {},
|
|
2610
|
+
metadata: {
|
|
2611
|
+
relationshipType: "go_composite_literal_construction",
|
|
2612
|
+
isConstructor: true,
|
|
2613
|
+
...qualifiedRaw !== void 0 ? { qualifiedType: qualifiedRaw } : {},
|
|
2614
|
+
...stdlibModule !== null ? { isStdlib: true, stdlibPath: stdlibModule } : {}
|
|
2615
|
+
}
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
2621
|
+
const child = node.namedChild(i);
|
|
2622
|
+
if (child) stack.push(child);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* Go-Specific Pattern Analysis
|
|
2628
|
+
* Analyzes goroutines, channels, defer statements, and Go idioms
|
|
2629
|
+
*/
|
|
2630
|
+
analyzeGoSpecificPatterns(rootNode, relationships, symbols) {
|
|
2631
|
+
const dotImported = this.hasGoDotImport(rootNode);
|
|
2632
|
+
const emittedCalls = /* @__PURE__ */ new Set();
|
|
2633
|
+
for (const r of relationships) {
|
|
2634
|
+
if ((r.type || r.relationshipType || "") === ASTEngine.RelationshipType.Calls) {
|
|
2635
|
+
emittedCalls.add(`${r.source}->${r.target}`);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
const emittedChannelUses = /* @__PURE__ */ new Set();
|
|
2639
|
+
const stack = [rootNode];
|
|
2640
|
+
while (stack.length > 0) {
|
|
2641
|
+
const node = stack.pop();
|
|
2642
|
+
if (node.type === "go_statement") {
|
|
2643
|
+
const functionCall = this.extractGoRoutineFunction(node);
|
|
2644
|
+
if (functionCall && this.isGoResolvableCallTarget(functionCall, symbols, dotImported)) {
|
|
2645
|
+
if (this.isGoBuiltin(functionCall.split(".").pop() ?? functionCall)) {
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
const containingFunction = this.findContainingGoFunction(node);
|
|
2649
|
+
if (containingFunction && !emittedCalls.has(`${containingFunction}->${functionCall}`)) {
|
|
2650
|
+
emittedCalls.add(`${containingFunction}->${functionCall}`);
|
|
2651
|
+
relationships.push({
|
|
2652
|
+
source: containingFunction,
|
|
2653
|
+
target: functionCall,
|
|
2654
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
2655
|
+
confidence: 0.9,
|
|
2656
|
+
// the callee leaf of the `go` statement.
|
|
2657
|
+
...this.nodeLine(this.goStatementCalleeNode(node)) ? { line_number: this.nodeLine(this.goStatementCalleeNode(node)) } : {},
|
|
2658
|
+
metadata: {
|
|
2659
|
+
relationshipType: "go_goroutine",
|
|
2660
|
+
isAsynchronous: true,
|
|
2661
|
+
pattern: "goroutine_spawn"
|
|
2662
|
+
}
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
if (node.type === "send_statement" || node.type === "receive_statement" || this.isGoReceiveExpression(node)) {
|
|
2668
|
+
const channelVar = this.extractGoChannelVariable(node);
|
|
2669
|
+
const containingFunction = this.findContainingGoFunction(node) ?? "global";
|
|
2670
|
+
if (channelVar && (channelVar.member || this.isGoDeclaredPackageVar(channelVar.name, symbols))) {
|
|
2671
|
+
const isSend = node.type === "send_statement";
|
|
2672
|
+
const key = `${containingFunction}->${channelVar.name}->${isSend ? "send" : "receive"}`;
|
|
2673
|
+
if (!emittedChannelUses.has(key)) {
|
|
2674
|
+
emittedChannelUses.add(key);
|
|
2675
|
+
relationships.push({
|
|
2676
|
+
source: containingFunction,
|
|
2677
|
+
target: channelVar.name,
|
|
2678
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2679
|
+
confidence: 0.8,
|
|
2680
|
+
// the send/receive statement's line.
|
|
2681
|
+
...this.nodeLine(node) ? { line_number: this.nodeLine(node) } : {},
|
|
2682
|
+
metadata: {
|
|
2683
|
+
relationshipType: "go_channel_operation",
|
|
2684
|
+
operationType: isSend ? "send" : "receive",
|
|
2685
|
+
pattern: "channel_communication"
|
|
2686
|
+
}
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
if (node.type === "defer_statement") {
|
|
2692
|
+
const deferredFunction = this.extractDeferredFunction(node);
|
|
2693
|
+
const containingFunction = this.findContainingGoFunction(node);
|
|
2694
|
+
if (deferredFunction && this.isGoResolvableCallTarget(deferredFunction, symbols, dotImported) && !this.isGoBuiltin(deferredFunction.split(".").pop() ?? deferredFunction) && containingFunction && !emittedCalls.has(`${containingFunction}->${deferredFunction}`)) {
|
|
2695
|
+
emittedCalls.add(`${containingFunction}->${deferredFunction}`);
|
|
2696
|
+
relationships.push({
|
|
2697
|
+
source: containingFunction,
|
|
2698
|
+
target: deferredFunction,
|
|
2699
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
2700
|
+
confidence: 0.9,
|
|
2701
|
+
// the callee leaf of the `defer` statement.
|
|
2702
|
+
...this.nodeLine(this.goStatementCalleeNode(node)) ? { line_number: this.nodeLine(this.goStatementCalleeNode(node)) } : {},
|
|
2703
|
+
metadata: {
|
|
2704
|
+
relationshipType: "go_defer",
|
|
2705
|
+
isDeferred: true,
|
|
2706
|
+
pattern: "defer_cleanup"
|
|
2707
|
+
}
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
2712
|
+
const child = node.namedChild(i);
|
|
2713
|
+
if (child) stack.push(child);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* GO1: bare-identifier value reads. Every bare `identifier` that names a
|
|
2719
|
+
* file-visible VALUE (package-scope var/const/func) is a Uses edge, with:
|
|
2720
|
+
* -walked with a lexical scope stack (function/method/func-literal
|
|
2721
|
+
* params+receivers bind at entry; block/short-var/var/range/for/if-init/
|
|
2722
|
+
* select-assign/type-switch-guard bindings bind in walk order, so reads
|
|
2723
|
+
* before any binding still resolve outward — C-family flow discipline);
|
|
2724
|
+
* - declaration positions never read (entity names via first-identifier
|
|
2725
|
+
* rule; composite-literal struct keys skipped; labels/package/import
|
|
2726
|
+
* paths/blank `_` excluded);
|
|
2727
|
+
* - call callees (span-matched; generic/index wrappers climbed),
|
|
2728
|
+
* member leaves (selector path owns them; objects stay readable),
|
|
2729
|
+
* qualified non-leaves, channel operands (channel pass owns them),
|
|
2730
|
+
* assignment/inc/dec LHS writes, go/defer callees all excluded;
|
|
2731
|
+
* - resolvability required: unknown names (std/dot-imported/external)
|
|
2732
|
+
* stay silent — no junk edges.
|
|
2733
|
+
*/
|
|
2734
|
+
analyzeGoBareIdentifierUses(rootNode, relationships, symbols) {
|
|
2735
|
+
const fileValues = /* @__PURE__ */ new Set();
|
|
2736
|
+
for (const s of symbols) {
|
|
2737
|
+
if (!s.ownerName && (s.kind === ASTEngine.SymbolKind.Variable || s.kind === ASTEngine.SymbolKind.Constant || s.kind === ASTEngine.SymbolKind.Function)) {
|
|
2738
|
+
fileValues.add(s.name);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
2742
|
+
const scopeStack = [];
|
|
2743
|
+
const isShadowed = (name) => {
|
|
2744
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
2745
|
+
if (scopeStack[i].names.has(name)) return true;
|
|
2746
|
+
}
|
|
2747
|
+
return false;
|
|
2748
|
+
};
|
|
2749
|
+
const bindNamesUnder = (node, frame) => {
|
|
2750
|
+
const walk = [node];
|
|
2751
|
+
while (walk.length > 0) {
|
|
2752
|
+
const m = walk.pop();
|
|
2753
|
+
if (m !== node && (m.type === "call_expression" || m.type === "composite_literal" || m.type === "func_literal")) {
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
if (m.type === "identifier" && m.text !== "_") frame.names.add(m.text);
|
|
2757
|
+
for (let i = m.namedChildCount - 1; i >= 0; i--) {
|
|
2758
|
+
const c = m.namedChild(i);
|
|
2759
|
+
if (c) walk.push(c);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
};
|
|
2763
|
+
const bindParamsOf = (declNode, frame) => {
|
|
2764
|
+
if (!declNode) return;
|
|
2765
|
+
const seen = [declNode];
|
|
2766
|
+
while (seen.length > 0) {
|
|
2767
|
+
const m = seen.pop();
|
|
2768
|
+
if (m.type === "parameter_declaration" || m.type === "variadic_parameter_declaration") {
|
|
2769
|
+
for (let i = 0; i < m.namedChildCount; i++) {
|
|
2770
|
+
const c = m.namedChild(i);
|
|
2771
|
+
if (!c) continue;
|
|
2772
|
+
if (c.type === "identifier") {
|
|
2773
|
+
if (c.text !== "_") frame.names.add(c.text);
|
|
2774
|
+
} else {
|
|
2775
|
+
break;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
continue;
|
|
2779
|
+
}
|
|
2780
|
+
for (let i = m.namedChildCount - 1; i >= 0; i--) {
|
|
2781
|
+
const c = m.namedChild(i);
|
|
2782
|
+
if (c) seen.push(c);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
const isCompositeKey = (node) => {
|
|
2787
|
+
let cur = node.parent;
|
|
2788
|
+
let keyed = null;
|
|
2789
|
+
while (cur) {
|
|
2790
|
+
if (cur.type === "keyed_element") {
|
|
2791
|
+
const first = cur.namedChildCount > 0 ? cur.namedChild(0) : null;
|
|
2792
|
+
if (first && node.startIndex >= first.startIndex && node.endIndex <= first.endIndex) {
|
|
2793
|
+
keyed = cur;
|
|
2794
|
+
break;
|
|
2795
|
+
}
|
|
2796
|
+
return false;
|
|
2797
|
+
}
|
|
2798
|
+
if (cur.type === "composite_literal" || cur.type === "call_expression" || cur.type === "field_declaration" || cur.type === "function_declaration" || cur.type === "method_declaration" || cur.type === "block") return false;
|
|
2799
|
+
cur = cur.parent;
|
|
2800
|
+
}
|
|
2801
|
+
if (!keyed) return false;
|
|
2802
|
+
cur = keyed.parent;
|
|
2803
|
+
while (cur) {
|
|
2804
|
+
if (cur.type === "composite_literal") {
|
|
2805
|
+
for (let i = 0; i < cur.namedChildCount; i++) {
|
|
2806
|
+
const c = cur.namedChild(i);
|
|
2807
|
+
if (c && c.type === "map_type") return false;
|
|
2808
|
+
}
|
|
2809
|
+
return true;
|
|
2810
|
+
}
|
|
2811
|
+
if (cur.type === "call_expression" || cur.type === "field_declaration" || cur.type === "function_declaration" || cur.type === "method_declaration" || cur.type === "block") return true;
|
|
2812
|
+
cur = cur.parent;
|
|
2813
|
+
}
|
|
2814
|
+
return true;
|
|
2815
|
+
};
|
|
2816
|
+
const isChannelOperand = (node) => {
|
|
2817
|
+
let cur = node.parent;
|
|
2818
|
+
let steps = 0;
|
|
2819
|
+
while (cur && steps < 4) {
|
|
2820
|
+
if (cur.type === "send_statement") {
|
|
2821
|
+
const ch = cur.childForFieldName("channel");
|
|
2822
|
+
return !!ch && node.startIndex >= ch.startIndex && node.endIndex <= ch.endIndex;
|
|
2823
|
+
}
|
|
2824
|
+
if (cur.type === "unary_expression") {
|
|
2825
|
+
const op = cur.child(0);
|
|
2826
|
+
if (op && op.type === "<-") return true;
|
|
2827
|
+
}
|
|
2828
|
+
if (cur.type === "receive_statement" || cur.type === "communication_case") return true;
|
|
2829
|
+
if (cur.type === "call_expression" || cur.type === "field_declaration" || cur.type === "function_declaration" || cur.type === "method_declaration" || cur.type === "block" || cur.type === "expression_list") return false;
|
|
2830
|
+
cur = cur.parent;
|
|
2831
|
+
steps++;
|
|
2832
|
+
}
|
|
2833
|
+
return false;
|
|
2834
|
+
};
|
|
2835
|
+
const isCallCallee = (node) => {
|
|
2836
|
+
let top = node;
|
|
2837
|
+
let p = node.parent;
|
|
2838
|
+
while (p && (p.type === "index_expression" || p.type === "generic_type")) {
|
|
2839
|
+
top = p;
|
|
2840
|
+
p = p.parent;
|
|
2841
|
+
}
|
|
2842
|
+
if (!p || p.type !== "call_expression") return false;
|
|
2843
|
+
const fnField = p.childForFieldName("function");
|
|
2844
|
+
if (fnField) return fnField.startIndex === top.startIndex && fnField.endIndex >= top.endIndex;
|
|
2845
|
+
const first = p.namedChild(0);
|
|
2846
|
+
return !!first && first.startIndex === top.startIndex && first.endIndex >= top.endIndex;
|
|
2847
|
+
};
|
|
2848
|
+
const isAssignLhsWrite = (node) => {
|
|
2849
|
+
let cur = node;
|
|
2850
|
+
let p = node.parent;
|
|
2851
|
+
while (p && p.type === "expression_list") {
|
|
2852
|
+
const gp = p.parent;
|
|
2853
|
+
if (gp && gp.type === "assignment_statement") {
|
|
2854
|
+
const lhs = gp.namedChild(0);
|
|
2855
|
+
return !!lhs && cur.startIndex >= lhs.startIndex && cur.endIndex <= lhs.endIndex;
|
|
2856
|
+
}
|
|
2857
|
+
cur = p;
|
|
2858
|
+
p = p.parent;
|
|
2859
|
+
}
|
|
2860
|
+
return false;
|
|
2861
|
+
};
|
|
2862
|
+
const isEntityName = (node) => {
|
|
2863
|
+
const p = node.parent;
|
|
2864
|
+
if (!p) return false;
|
|
2865
|
+
if (p.type === "function_declaration" || p.type === "method_declaration" || p.type === "type_spec" || p.type === "field_declaration" || p.type === "package_clause" || p.type === "label_name") {
|
|
2866
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
2867
|
+
const c = p.namedChild(i);
|
|
2868
|
+
if (c && (c.type === "identifier" || c.type === "type_identifier" || c.type === "field_identifier")) {
|
|
2869
|
+
return c.startIndex === node.startIndex && c.endIndex === node.endIndex;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
if (p.type === "var_spec" || p.type === "const_spec") {
|
|
2874
|
+
let seenIdent = false;
|
|
2875
|
+
let isFirstIdent = false;
|
|
2876
|
+
let laterNonIdent = false;
|
|
2877
|
+
let found = false;
|
|
2878
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
2879
|
+
const c = p.namedChild(i);
|
|
2880
|
+
if (!c) continue;
|
|
2881
|
+
if (c.type === "identifier") {
|
|
2882
|
+
if (c.startIndex === node.startIndex && c.endIndex === node.endIndex) {
|
|
2883
|
+
if (!seenIdent) isFirstIdent = true;
|
|
2884
|
+
found = true;
|
|
2885
|
+
}
|
|
2886
|
+
seenIdent = true;
|
|
2887
|
+
} else if (found) {
|
|
2888
|
+
laterNonIdent = true;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
return isFirstIdent || laterNonIdent;
|
|
2892
|
+
}
|
|
2893
|
+
if (p.type === "short_var_declaration" || p.type === "expression_list" && p.parent && p.parent.type === "short_var_declaration") {
|
|
2894
|
+
const decl = p.type === "short_var_declaration" ? p : p.parent;
|
|
2895
|
+
const left = decl.childForFieldName("left") ?? decl.namedChild(0);
|
|
2896
|
+
return !!left && node.startIndex >= left.startIndex && node.endIndex <= left.endIndex;
|
|
2897
|
+
}
|
|
2898
|
+
if (p.type === "goto_statement" || p.type === "break_statement" || p.type === "continue_statement" || p.type === "labeled_statement") return true;
|
|
2899
|
+
return false;
|
|
2900
|
+
};
|
|
2901
|
+
const stack = [{ node: rootNode, exit: false, pushed: false }];
|
|
2902
|
+
while (stack.length > 0) {
|
|
2903
|
+
const { node, exit, pushed } = stack.pop();
|
|
2904
|
+
if (exit) {
|
|
2905
|
+
if (pushed) scopeStack.pop();
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
const t = node.type;
|
|
2909
|
+
let pushedScope = false;
|
|
2910
|
+
if (t === "function_declaration" || t === "method_declaration" || t === "func_literal") {
|
|
2911
|
+
const frame = { names: /* @__PURE__ */ new Set() };
|
|
2912
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2913
|
+
const c = node.namedChild(i);
|
|
2914
|
+
if (!c) continue;
|
|
2915
|
+
if (c.type === "parameter_list" || c.type === "type_parameter_list") bindParamsOf(c, frame);
|
|
2916
|
+
}
|
|
2917
|
+
scopeStack.push(frame);
|
|
2918
|
+
pushedScope = true;
|
|
2919
|
+
} else if (t === "block") {
|
|
2920
|
+
scopeStack.push({ names: /* @__PURE__ */ new Set() });
|
|
2921
|
+
pushedScope = true;
|
|
2922
|
+
} else if (t === "var_spec" || t === "const_spec" || t === "short_var_declaration" || t === "range_clause" || t === "type_switch_statement") {
|
|
2923
|
+
const top = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
|
|
2924
|
+
if (top) {
|
|
2925
|
+
if (t === "range_clause") {
|
|
2926
|
+
for (let i = 0; i < node.namedChildCount - 1; i++) {
|
|
2927
|
+
const c = node.namedChild(i);
|
|
2928
|
+
if (c) bindNamesUnder(c, top);
|
|
2929
|
+
}
|
|
2930
|
+
} else if (t === "type_switch_statement") {
|
|
2931
|
+
if (node.namedChildCount > 2) {
|
|
2932
|
+
const first = node.namedChild(0);
|
|
2933
|
+
if (first) bindNamesUnder(first, top);
|
|
2934
|
+
}
|
|
2935
|
+
} else {
|
|
2936
|
+
const walk = [node];
|
|
2937
|
+
while (walk.length > 0) {
|
|
2938
|
+
const m = walk.pop();
|
|
2939
|
+
if (m !== node && (m.type === "call_expression" || m.type === "composite_literal" || m.type === "func_literal")) {
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
if (m.type === "identifier" && m.text !== "_" && isEntityName(m)) top.names.add(m.text);
|
|
2943
|
+
for (let i = m.namedChildCount - 1; i >= 0; i--) {
|
|
2944
|
+
const c = m.namedChild(i);
|
|
2945
|
+
if (c) walk.push(c);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
} else if (t === "receive_statement") {
|
|
2951
|
+
const top = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
|
|
2952
|
+
if (top) {
|
|
2953
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2954
|
+
const c = node.namedChild(i);
|
|
2955
|
+
if (c && c.type === "expression_list") bindNamesUnder(c, top);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
} else if (t === "identifier") {
|
|
2959
|
+
const name = node.text;
|
|
2960
|
+
const p = node.parent;
|
|
2961
|
+
if (name && name !== "_" && !isEntityName(node) && !isCompositeKey(node) && !isChannelOperand(node) && !isCallCallee(node) && // F6 audit: `field_expression` is not a tree-sitter-go type (that is
|
|
2962
|
+
// rust's name for the shape `r.cfg` here — go uses
|
|
2963
|
+
// `selector_expression`), so the old guard could never fire. It is
|
|
2964
|
+
// also not needed: a selector's receiver is a genuine read
|
|
2965
|
+
// (`x := db.cfg` reads db — probe-verified `f->cfg f->db`), and a
|
|
2966
|
+
// selector that is a call callee is already excluded by
|
|
2967
|
+
// isCallCallee above. Removed rather than re-pointed at
|
|
2968
|
+
// selector_expression, which would silently drop the receiver read.
|
|
2969
|
+
!this.hasGoQualifiedAncestor(node) && // GO9 (second review): assignment LHS is a WRITE — but EVERY
|
|
2970
|
+
// side of every assignment wraps in expression_list, so the old
|
|
2971
|
+
// direct-parent check never fired (see isAssignLhsWrite above).
|
|
2972
|
+
!isAssignLhsWrite(node) && !(p && (p.type === "inc_statement" || p.type === "dec_statement")) && !(p && (p.type === "goto_statement" || p.type === "break_statement" || p.type === "continue_statement" || p.type === "labeled_statement")) && !isShadowed(name) && fileValues.has(name)) {
|
|
2973
|
+
const src = this.resolveGoWorkSource(node);
|
|
2974
|
+
const key = `${src}::uses::${name}`;
|
|
2975
|
+
if (!emitted.has(key)) {
|
|
2976
|
+
emitted.add(key);
|
|
2977
|
+
relationships.push({
|
|
2978
|
+
source: src,
|
|
2979
|
+
target: name,
|
|
2980
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
2981
|
+
confidence: 1,
|
|
2982
|
+
// the identifier's own line.
|
|
2983
|
+
...this.nodeLine(node) ? { line_number: this.nodeLine(node) } : {},
|
|
2984
|
+
metadata: { relationshipType: "identifier_usage", nodeType: node.type }
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
stack.push({ node, exit: true, pushed: pushedScope });
|
|
2990
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
2991
|
+
const child = node.namedChild(i);
|
|
2992
|
+
if (child) stack.push({ node: child, exit: false, pushed: false });
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Go Selector Expression Uses Analysis
|
|
2998
|
+
* Tracks field access on structs (e.g., b.name, s.users).
|
|
2999
|
+
* Family contract (Rust M12.18): source = containing function/method,
|
|
3000
|
+
* target = FIELD LEAF (not the receiver variable). Selectors that are the
|
|
3001
|
+
* callee of a call_expression (`s.repo.Save(...)`, `strings.ToUpper(...)`)
|
|
3002
|
+
* are handled by the calls analysis and skipped here — one edge per pair.
|
|
3003
|
+
*/
|
|
3004
|
+
analyzeGoSelectorUses(rootNode, relationships, symbols) {
|
|
3005
|
+
const stack = [rootNode];
|
|
3006
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3007
|
+
while (stack.length > 0) {
|
|
3008
|
+
const node = stack.pop();
|
|
3009
|
+
if (node.type === "selector_expression") {
|
|
3010
|
+
const operand = node.childForFieldName("operand");
|
|
3011
|
+
const field = node.childForFieldName("field");
|
|
3012
|
+
const isWritePos = node.parent?.type === "inc_statement" || node.parent?.type === "dec_statement" || node.parent?.type === "expression_list" && node.parent.parent?.type === "assignment_statement" && node.parent.parent?.namedChild(0)?.id === node.parent.id;
|
|
3013
|
+
const isChannelOperand = node.parent?.type === "send_statement" || node.parent?.type === "communication_case" || node.parent?.type === "unary_expression" && node.parent.child(0)?.type === "<-" || node.parent?.type === "receive_statement" && node.parent.parent?.type === "communication_case";
|
|
3014
|
+
if (operand && field && node.parent?.type !== "call_expression" && !isWritePos && !isChannelOperand) {
|
|
3015
|
+
const src = this.resolveGoWorkSource(node);
|
|
3016
|
+
const tgt = field.text;
|
|
3017
|
+
const key = `${src}.${tgt}`;
|
|
3018
|
+
if (!seen.has(key)) {
|
|
3019
|
+
seen.add(key);
|
|
3020
|
+
relationships.push({
|
|
3021
|
+
source: src,
|
|
3022
|
+
target: tgt,
|
|
3023
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
3024
|
+
confidence: 0.7,
|
|
3025
|
+
// the FIELD identifier's line, not the receiver's —
|
|
3026
|
+
// the c lane's receiver-start lesson applied to `a.b`.
|
|
3027
|
+
...this.nodeLine(field) ? { line_number: this.nodeLine(field) } : {},
|
|
3028
|
+
metadata: { relationshipType: "go_selector_use", selectorExpression: key }
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
3034
|
+
const child = node.namedChild(i);
|
|
3035
|
+
if (child) stack.push(child);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Go Import Processing
|
|
3041
|
+
* Handles Go module paths, vendor imports, and package resolution
|
|
3042
|
+
*/
|
|
3043
|
+
processGoImports(rootNode, imports) {
|
|
3044
|
+
const stack = [rootNode];
|
|
3045
|
+
while (stack.length > 0) {
|
|
3046
|
+
const node = stack.pop();
|
|
3047
|
+
let claimed = false;
|
|
3048
|
+
if (node.type === "import_spec") {
|
|
3049
|
+
this.processGoImportSpec(node, imports);
|
|
3050
|
+
claimed = true;
|
|
3051
|
+
} else if (node.type === "import_declaration") {
|
|
3052
|
+
this.extractGoImportStatement(node, imports);
|
|
3053
|
+
claimed = true;
|
|
3054
|
+
}
|
|
3055
|
+
if (!claimed) {
|
|
3056
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
3057
|
+
const child = node.namedChild(i);
|
|
3058
|
+
if (child) stack.push(child);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Extract Go import statement with proper module path handling
|
|
3065
|
+
*/
|
|
3066
|
+
extractGoImportStatement(node, imports) {
|
|
3067
|
+
const specs = [];
|
|
3068
|
+
const stack = [node];
|
|
3069
|
+
while (stack.length > 0) {
|
|
3070
|
+
const current = stack.pop();
|
|
3071
|
+
if (current.type === "import_spec") {
|
|
3072
|
+
specs.push(current);
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
for (let i = current.namedChildCount - 1; i >= 0; i--) {
|
|
3076
|
+
const child = current.namedChild(i);
|
|
3077
|
+
if (child) stack.push(child);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
for (const spec of specs) {
|
|
3081
|
+
this.processGoImportSpec(spec, imports);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* Process individual Go import specification
|
|
3086
|
+
*/
|
|
3087
|
+
processGoImportSpec(spec, imports) {
|
|
3088
|
+
const pathNode = spec.childForFieldName("path");
|
|
3089
|
+
const aliasNode = spec.childForFieldName("name");
|
|
3090
|
+
if (pathNode) {
|
|
3091
|
+
const importPath = this.stripQuotedString(pathNode.text);
|
|
3092
|
+
const alias = aliasNode?.text;
|
|
3093
|
+
const packageName = this.getGoPackageName(importPath);
|
|
3094
|
+
const framework = this.detectFrameworkFromGoImport(importPath);
|
|
3095
|
+
imports.push({
|
|
3096
|
+
module: importPath,
|
|
3097
|
+
isDefault: false,
|
|
3098
|
+
alias,
|
|
3099
|
+
items: [packageName],
|
|
3100
|
+
range: this.nodeToRange(spec),
|
|
3101
|
+
metadata: {
|
|
3102
|
+
isGoImport: true,
|
|
3103
|
+
packageName,
|
|
3104
|
+
framework,
|
|
3105
|
+
isStandardLibrary: this.isGoStandardLibrary(importPath),
|
|
3106
|
+
isThirdParty: this.isGoThirdPartyPackage(importPath)
|
|
3107
|
+
}
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
/**
|
|
3112
|
+
* Go Framework Pattern Analysis
|
|
3113
|
+
*/
|
|
3114
|
+
getGoFrameworkPatterns(frameworks, symbols) {
|
|
3115
|
+
const patterns = [];
|
|
3116
|
+
for (const framework of frameworks) {
|
|
3117
|
+
switch (framework) {
|
|
3118
|
+
case "gin":
|
|
3119
|
+
patterns.push({
|
|
3120
|
+
framework: "Gin Web Framework",
|
|
3121
|
+
type: "web_framework",
|
|
3122
|
+
patterns: this.detectGinPatterns(symbols),
|
|
3123
|
+
confidence: 0.9,
|
|
3124
|
+
characteristics: {
|
|
3125
|
+
hasRoutes: someSymbol(
|
|
3126
|
+
symbols,
|
|
3127
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "gin.") || stringIncludes(c, "router."))
|
|
3128
|
+
),
|
|
3129
|
+
hasMiddleware: someSymbol(
|
|
3130
|
+
symbols,
|
|
3131
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "Use("))
|
|
3132
|
+
),
|
|
3133
|
+
hasHandlers: someSymbol(symbols, (s) => stringIncludes(s.name.toLowerCase(), "handler"))
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
break;
|
|
3137
|
+
case "echo":
|
|
3138
|
+
patterns.push({
|
|
3139
|
+
framework: "Echo Web Framework",
|
|
3140
|
+
type: "web_framework",
|
|
3141
|
+
patterns: this.detectEchoPatterns(symbols),
|
|
3142
|
+
confidence: 0.9,
|
|
3143
|
+
characteristics: {
|
|
3144
|
+
hasRoutes: someSymbol(
|
|
3145
|
+
symbols,
|
|
3146
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "echo.") || stringIncludes(c, "e."))
|
|
3147
|
+
),
|
|
3148
|
+
hasMiddleware: someSymbol(
|
|
3149
|
+
symbols,
|
|
3150
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "Use("))
|
|
3151
|
+
),
|
|
3152
|
+
hasContext: someSymbol(
|
|
3153
|
+
symbols,
|
|
3154
|
+
(s) => someSymbol(s.parameters || [], (p) => stringIncludes(p.type || "", "Context"))
|
|
3155
|
+
)
|
|
3156
|
+
}
|
|
3157
|
+
});
|
|
3158
|
+
break;
|
|
3159
|
+
case "fiber":
|
|
3160
|
+
patterns.push({
|
|
3161
|
+
framework: "Fiber Web Framework",
|
|
3162
|
+
type: "web_framework",
|
|
3163
|
+
patterns: this.detectFiberPatterns(symbols),
|
|
3164
|
+
confidence: 0.9,
|
|
3165
|
+
characteristics: {
|
|
3166
|
+
hasRoutes: someSymbol(
|
|
3167
|
+
symbols,
|
|
3168
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "fiber.") || stringIncludes(c, "app."))
|
|
3169
|
+
),
|
|
3170
|
+
hasFastHTTP: someSymbol(
|
|
3171
|
+
symbols,
|
|
3172
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "fasthttp"))
|
|
3173
|
+
)
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
break;
|
|
3177
|
+
case "gorilla_mux":
|
|
3178
|
+
patterns.push({
|
|
3179
|
+
framework: "Gorilla Mux Router",
|
|
3180
|
+
type: "router",
|
|
3181
|
+
patterns: this.detectGorillaMuxPatterns(symbols),
|
|
3182
|
+
confidence: 0.8,
|
|
3183
|
+
characteristics: {
|
|
3184
|
+
hasRoutes: someSymbol(
|
|
3185
|
+
symbols,
|
|
3186
|
+
(s) => someSymbol(
|
|
3187
|
+
s.qualified_calls || [],
|
|
3188
|
+
(c) => stringIncludes(c, "mux.") || stringIncludes(c, "HandleFunc")
|
|
3189
|
+
)
|
|
3190
|
+
),
|
|
3191
|
+
hasSubrouters: someSymbol(
|
|
3192
|
+
symbols,
|
|
3193
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "PathPrefix"))
|
|
3194
|
+
)
|
|
3195
|
+
}
|
|
3196
|
+
});
|
|
3197
|
+
break;
|
|
3198
|
+
case "gorm":
|
|
3199
|
+
patterns.push({
|
|
3200
|
+
framework: "GORM ORM",
|
|
3201
|
+
type: "orm",
|
|
3202
|
+
patterns: this.detectGORMPatterns(symbols),
|
|
3203
|
+
confidence: 0.9,
|
|
3204
|
+
characteristics: {
|
|
3205
|
+
hasModels: someSymbol(symbols, (s) => s.kind === ASTEngine.SymbolKind.Struct && s.metadata?.isGoStruct),
|
|
3206
|
+
hasMigrations: someSymbol(
|
|
3207
|
+
symbols,
|
|
3208
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "AutoMigrate"))
|
|
3209
|
+
),
|
|
3210
|
+
hasQueries: someSymbol(
|
|
3211
|
+
symbols,
|
|
3212
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "Find") || stringIncludes(c, "Create"))
|
|
3213
|
+
)
|
|
3214
|
+
}
|
|
3215
|
+
});
|
|
3216
|
+
break;
|
|
3217
|
+
case "grpc":
|
|
3218
|
+
patterns.push({
|
|
3219
|
+
framework: "gRPC",
|
|
3220
|
+
type: "rpc_framework",
|
|
3221
|
+
patterns: this.detectGRPCPatterns(symbols),
|
|
3222
|
+
confidence: 0.9,
|
|
3223
|
+
characteristics: {
|
|
3224
|
+
hasServices: someSymbol(symbols, (s) => stringIncludes(s.name.toLowerCase(), "service")),
|
|
3225
|
+
hasProtobuf: someSymbol(
|
|
3226
|
+
symbols,
|
|
3227
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "proto"))
|
|
3228
|
+
),
|
|
3229
|
+
hasStreamingMethods: someSymbol(
|
|
3230
|
+
symbols,
|
|
3231
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "stream"))
|
|
3232
|
+
)
|
|
3233
|
+
}
|
|
3234
|
+
});
|
|
3235
|
+
break;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
return patterns;
|
|
3239
|
+
}
|
|
3240
|
+
/**
|
|
3241
|
+
* Go Architectural Insights
|
|
3242
|
+
*/
|
|
3243
|
+
generateGoArchitecturalInsights(symbols, relationships) {
|
|
3244
|
+
const insights = [];
|
|
3245
|
+
const complexFunctions = filterSymbols(
|
|
3246
|
+
symbols,
|
|
3247
|
+
(s) => (s.kind === ASTEngine.SymbolKind.Function || s.kind === ASTEngine.SymbolKind.Method) && !!s.qualified_calls && s.qualified_calls.length > 12
|
|
3248
|
+
);
|
|
3249
|
+
if (complexFunctions.length > 0) {
|
|
3250
|
+
insights.push({
|
|
3251
|
+
type: "complexity_analysis",
|
|
3252
|
+
category: "function_complexity",
|
|
3253
|
+
message: `${complexFunctions.length} functions with high complexity detected`,
|
|
3254
|
+
functions: mapSymbols(complexFunctions, (f) => f.name),
|
|
3255
|
+
recommendation: "Consider breaking down complex functions into smaller, focused functions",
|
|
3256
|
+
severity: "warning"
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
const goroutineFunctions = getUniqueStrings(
|
|
3260
|
+
mapSymbols(
|
|
3261
|
+
filterSymbols(relationships, (r) => r.metadata?.relationshipType === "go_goroutine" && !!r.source),
|
|
3262
|
+
(r) => r.source
|
|
3263
|
+
)
|
|
3264
|
+
);
|
|
3265
|
+
if (goroutineFunctions.length > 0) {
|
|
3266
|
+
insights.push({
|
|
3267
|
+
type: "concurrency_pattern",
|
|
3268
|
+
category: "goroutines",
|
|
3269
|
+
message: `Goroutine usage detected in ${goroutineFunctions.length} functions`,
|
|
3270
|
+
functions: goroutineFunctions,
|
|
3271
|
+
recommendation: "Ensure proper goroutine lifecycle management and channel synchronization"
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
const channelFunctions = getUniqueStrings(
|
|
3275
|
+
mapSymbols(
|
|
3276
|
+
filterSymbols(relationships, (r) => r.metadata?.relationshipType === "go_channel_operation" && !!r.source),
|
|
3277
|
+
(r) => r.source
|
|
3278
|
+
)
|
|
3279
|
+
);
|
|
3280
|
+
if (channelFunctions.length > 0) {
|
|
3281
|
+
insights.push({
|
|
3282
|
+
type: "concurrency_pattern",
|
|
3283
|
+
category: "channels",
|
|
3284
|
+
message: `Channel communication pattern detected in ${channelFunctions.length} functions`,
|
|
3285
|
+
functions: channelFunctions,
|
|
3286
|
+
recommendation: "Follow channel ownership patterns and avoid goroutine leaks"
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
const structs = filterSymbols(symbols, (s) => s.kind === ASTEngine.SymbolKind.Struct && s.metadata?.isGoStruct);
|
|
3290
|
+
const methods = filterSymbols(symbols, (s) => s.kind === ASTEngine.SymbolKind.Method);
|
|
3291
|
+
if (structs.length > 0) {
|
|
3292
|
+
const methodToStructRatio = methods.length / structs.length;
|
|
3293
|
+
insights.push({
|
|
3294
|
+
type: "design_pattern",
|
|
3295
|
+
category: "struct_design",
|
|
3296
|
+
message: `${structs.length} structs with ${methods.length} methods (ratio: ${methodToStructRatio.toFixed(2)})`,
|
|
3297
|
+
structs: mapSymbols(structs, (s) => s.name),
|
|
3298
|
+
quality: methodToStructRatio > 3 ? "rich_behavior" : methodToStructRatio > 1 ? "balanced" : "anemic_structs",
|
|
3299
|
+
recommendation: methodToStructRatio < 1 ? "Consider adding methods to structs for better encapsulation" : "Good struct-method balance"
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
const errorHandling = this.analyzeGoErrorHandling(symbols);
|
|
3303
|
+
if (errorHandling.functions > 0) {
|
|
3304
|
+
insights.push({
|
|
3305
|
+
type: "error_handling",
|
|
3306
|
+
category: "go_errors",
|
|
3307
|
+
message: `Error handling detected in ${errorHandling.functions} functions`,
|
|
3308
|
+
coverage: `${(errorHandling.withErrorHandling / errorHandling.functions * 100).toFixed(1)}%`,
|
|
3309
|
+
recommendation: errorHandling.withErrorHandling / errorHandling.functions < 0.7 ? "Improve error handling coverage" : "Good error handling practices"
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
const interfaces = filterSymbols(symbols, (s) => s.kind === ASTEngine.SymbolKind.Interface);
|
|
3313
|
+
if (interfaces.length > 0) {
|
|
3314
|
+
insights.push({
|
|
3315
|
+
type: "design_pattern",
|
|
3316
|
+
category: "interfaces",
|
|
3317
|
+
message: `${interfaces.length} interfaces promoting loose coupling`,
|
|
3318
|
+
interfaces: mapSymbols(interfaces, (i) => i.name),
|
|
3319
|
+
recommendation: "Good use of Go interfaces for abstraction"
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
return insights;
|
|
3323
|
+
}
|
|
3324
|
+
// ======================== GO FRAMEWORK DETECTION METHODS ========================
|
|
3325
|
+
detectGinPatterns(symbols) {
|
|
3326
|
+
const patterns = [];
|
|
3327
|
+
if (someSymbol(
|
|
3328
|
+
symbols,
|
|
3329
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "gin.Default") || stringIncludes(c, "gin.New"))
|
|
3330
|
+
)) {
|
|
3331
|
+
patterns.push("gin_router_setup");
|
|
3332
|
+
}
|
|
3333
|
+
if (someSymbol(
|
|
3334
|
+
symbols,
|
|
3335
|
+
(s) => someSymbol(
|
|
3336
|
+
s.qualified_calls || [],
|
|
3337
|
+
(c) => stringIncludes(c, "GET") || stringIncludes(c, "POST") || stringIncludes(c, "PUT")
|
|
3338
|
+
)
|
|
3339
|
+
)) {
|
|
3340
|
+
patterns.push("rest_endpoints");
|
|
3341
|
+
}
|
|
3342
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "Group(")))) {
|
|
3343
|
+
patterns.push("route_grouping");
|
|
3344
|
+
}
|
|
3345
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "Use(")))) {
|
|
3346
|
+
patterns.push("middleware_usage");
|
|
3347
|
+
}
|
|
3348
|
+
return patterns;
|
|
3349
|
+
}
|
|
3350
|
+
detectEchoPatterns(symbols) {
|
|
3351
|
+
const patterns = [];
|
|
3352
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "echo.New")))) {
|
|
3353
|
+
patterns.push("echo_setup");
|
|
3354
|
+
}
|
|
3355
|
+
if (someSymbol(symbols, (s) => someSymbol(s.parameters || [], (p) => stringIncludes(p.type || "", "echo.Context")))) {
|
|
3356
|
+
patterns.push("echo_context_usage");
|
|
3357
|
+
}
|
|
3358
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "Bind(")))) {
|
|
3359
|
+
patterns.push("request_binding");
|
|
3360
|
+
}
|
|
3361
|
+
return patterns;
|
|
3362
|
+
}
|
|
3363
|
+
detectFiberPatterns(symbols) {
|
|
3364
|
+
const patterns = [];
|
|
3365
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "fiber.New")))) {
|
|
3366
|
+
patterns.push("fiber_app_setup");
|
|
3367
|
+
}
|
|
3368
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "BodyParser(")))) {
|
|
3369
|
+
patterns.push("request_parsing");
|
|
3370
|
+
}
|
|
3371
|
+
return patterns;
|
|
3372
|
+
}
|
|
3373
|
+
detectGorillaMuxPatterns(symbols) {
|
|
3374
|
+
const patterns = [];
|
|
3375
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "mux.NewRouter")))) {
|
|
3376
|
+
patterns.push("gorilla_router_setup");
|
|
3377
|
+
}
|
|
3378
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "HandleFunc(")))) {
|
|
3379
|
+
patterns.push("route_handlers");
|
|
3380
|
+
}
|
|
3381
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "PathPrefix(")))) {
|
|
3382
|
+
patterns.push("path_prefixes");
|
|
3383
|
+
}
|
|
3384
|
+
return patterns;
|
|
3385
|
+
}
|
|
3386
|
+
detectGORMPatterns(symbols) {
|
|
3387
|
+
const patterns = [];
|
|
3388
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "gorm.Open")))) {
|
|
3389
|
+
patterns.push("gorm_connection");
|
|
3390
|
+
}
|
|
3391
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "AutoMigrate(")))) {
|
|
3392
|
+
patterns.push("auto_migration");
|
|
3393
|
+
}
|
|
3394
|
+
if (someSymbol(
|
|
3395
|
+
symbols,
|
|
3396
|
+
(s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "Create(") || stringIncludes(c, "Find("))
|
|
3397
|
+
)) {
|
|
3398
|
+
patterns.push("crud_operations");
|
|
3399
|
+
}
|
|
3400
|
+
return patterns;
|
|
3401
|
+
}
|
|
3402
|
+
detectGRPCPatterns(symbols) {
|
|
3403
|
+
const patterns = [];
|
|
3404
|
+
if (someSymbol(
|
|
3405
|
+
symbols,
|
|
3406
|
+
(s) => stringIncludes(s.name.toLowerCase(), "service") && s.kind === ASTEngine.SymbolKind.Struct
|
|
3407
|
+
)) {
|
|
3408
|
+
patterns.push("grpc_services");
|
|
3409
|
+
}
|
|
3410
|
+
if (someSymbol(symbols, (s) => someSymbol(s.qualified_calls || [], (c) => stringIncludes(c, "grpc.NewServer")))) {
|
|
3411
|
+
patterns.push("grpc_server_setup");
|
|
3412
|
+
}
|
|
3413
|
+
return patterns;
|
|
3414
|
+
}
|
|
3415
|
+
// ======================== GO HELPER METHODS ========================
|
|
3416
|
+
// GO2: plain-identifier shape check (no regex): letter/underscore head,
|
|
3417
|
+
// word-class tail. Recovery-artifact names (qualified, templated,
|
|
3418
|
+
// operator-soup like `~int | ~string`) fail and emit no symbol/edge.
|
|
3419
|
+
isPlainGoIdentifier(s) {
|
|
3420
|
+
if (s.length === 0) return false;
|
|
3421
|
+
const c0 = s.charCodeAt(0);
|
|
3422
|
+
const firstOk = c0 >= 65 && c0 <= 90 || c0 >= 97 && c0 <= 122 || c0 === 95;
|
|
3423
|
+
if (!firstOk) return false;
|
|
3424
|
+
for (let i = 1; i < s.length; i++) {
|
|
3425
|
+
const c = s.charCodeAt(i);
|
|
3426
|
+
const ok = c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 95;
|
|
3427
|
+
if (!ok) return false;
|
|
3428
|
+
}
|
|
3429
|
+
return true;
|
|
3430
|
+
}
|
|
3431
|
+
// ---- Pure-string helpers (M12.22: no regex in this analyzer) ----
|
|
3432
|
+
/** Strip surrounding backticks/quote chars from a raw string literal. */
|
|
3433
|
+
stripRawStringDelimiters(text) {
|
|
3434
|
+
if (text.length >= 2) {
|
|
3435
|
+
const first = text[0];
|
|
3436
|
+
const last = text[text.length - 1];
|
|
3437
|
+
if (first === "`" && last === "`" || first === "'" && last === "'") {
|
|
3438
|
+
return text.slice(1, -1);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
return text;
|
|
3442
|
+
}
|
|
3443
|
+
/** Strip surrounding double quotes from an interpreted string literal. */
|
|
3444
|
+
stripQuotedString(text) {
|
|
3445
|
+
const q = text.length >= 2 ? text[0] : "";
|
|
3446
|
+
if ((q === '"' || q === "`") && text[text.length - 1] === q) {
|
|
3447
|
+
return text.slice(1, -1);
|
|
3448
|
+
}
|
|
3449
|
+
return text;
|
|
3450
|
+
}
|
|
3451
|
+
/** Parse struct tags `json:"name" db:"x"` into key -> value (no regex). */
|
|
3452
|
+
parseStructTags(tagText) {
|
|
3453
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
3454
|
+
let i = 0;
|
|
3455
|
+
const n = tagText.length;
|
|
3456
|
+
while (i < n) {
|
|
3457
|
+
while (i < n && (tagText[i] === " " || tagText[i] === " ")) i++;
|
|
3458
|
+
if (i >= n) break;
|
|
3459
|
+
const keyStart = i;
|
|
3460
|
+
while (i < n && tagText[i] !== ":" && tagText[i] !== " " && tagText[i] !== " ") i++;
|
|
3461
|
+
if (i >= n || tagText[i] !== ":") break;
|
|
3462
|
+
const key = tagText.slice(keyStart, i);
|
|
3463
|
+
i++;
|
|
3464
|
+
if (i < n && tagText[i] === '"') {
|
|
3465
|
+
i++;
|
|
3466
|
+
let value = "";
|
|
3467
|
+
while (i < n && tagText[i] !== '"') {
|
|
3468
|
+
if (tagText[i] === "\\" && i + 1 < n) {
|
|
3469
|
+
value += tagText[i + 1];
|
|
3470
|
+
i += 2;
|
|
3471
|
+
} else {
|
|
3472
|
+
value += tagText[i];
|
|
3473
|
+
i++;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
i++;
|
|
3477
|
+
if (key) result[key] = value;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
return result;
|
|
3481
|
+
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Swaggo @Router annotation: `@Router /path/to/endpoint [method]`.
|
|
3484
|
+
* Case-insensitive marker, whitespace-tolerant pure scan (no regex).
|
|
3485
|
+
*/
|
|
3486
|
+
parseRouterAnnotation(comment) {
|
|
3487
|
+
const marker = "@Router";
|
|
3488
|
+
const idx = this.indexOfIgnoreCase(comment, marker);
|
|
3489
|
+
if (idx < 0) return void 0;
|
|
3490
|
+
const after = comment.slice(idx + marker.length);
|
|
3491
|
+
const path = this.readToken(after);
|
|
3492
|
+
if (!path) return void 0;
|
|
3493
|
+
const open = after.indexOf("[", after.indexOf(path) + path.length);
|
|
3494
|
+
if (open < 0) return void 0;
|
|
3495
|
+
const close = after.indexOf("]", open);
|
|
3496
|
+
if (close < 0) return void 0;
|
|
3497
|
+
const method = after.slice(open + 1, close).trim();
|
|
3498
|
+
if (!method) return void 0;
|
|
3499
|
+
return `${method.toUpperCase()} ${path}`;
|
|
3500
|
+
}
|
|
3501
|
+
indexOfIgnoreCase(text, search) {
|
|
3502
|
+
const lower = text.toLowerCase();
|
|
3503
|
+
const target = search.toLowerCase();
|
|
3504
|
+
return lower.indexOf(target);
|
|
3505
|
+
}
|
|
3506
|
+
/** Read the next whitespace-delimited token. */
|
|
3507
|
+
readToken(text) {
|
|
3508
|
+
const n = text.length;
|
|
3509
|
+
let i = 0;
|
|
3510
|
+
while (i < n && (text[i] === " " || text[i] === " ")) i++;
|
|
3511
|
+
if (i >= n) return void 0;
|
|
3512
|
+
const start = i;
|
|
3513
|
+
while (i < n && text[i] !== " " && text[i] !== " ") i++;
|
|
3514
|
+
return text.slice(start, i);
|
|
3515
|
+
}
|
|
3516
|
+
/** Normalize a Go signature: collapse whitespace, variadic, interface{} (no regex). */
|
|
3517
|
+
normalizeGoSignature(sig) {
|
|
3518
|
+
let collapsed = "";
|
|
3519
|
+
let prevSpace = false;
|
|
3520
|
+
for (let i = 0; i < sig.length; i++) {
|
|
3521
|
+
const ch = sig[i];
|
|
3522
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
3523
|
+
if (!prevSpace && collapsed.length > 0) collapsed += " ";
|
|
3524
|
+
prevSpace = true;
|
|
3525
|
+
} else {
|
|
3526
|
+
collapsed += ch;
|
|
3527
|
+
prevSpace = false;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
let variadic = "";
|
|
3531
|
+
for (let i = 0; i < collapsed.length; i++) {
|
|
3532
|
+
const ch = collapsed[i];
|
|
3533
|
+
if (ch === "." && collapsed[i + 1] === "." && collapsed[i + 2] === ".") {
|
|
3534
|
+
if (variadic.length > 0 && variadic[variadic.length - 1] === " ") {
|
|
3535
|
+
variadic = variadic.slice(0, -1);
|
|
3536
|
+
}
|
|
3537
|
+
variadic += "...";
|
|
3538
|
+
i += 2;
|
|
3539
|
+
} else {
|
|
3540
|
+
variadic += ch;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
let result = "";
|
|
3544
|
+
let k = 0;
|
|
3545
|
+
while (k < variadic.length) {
|
|
3546
|
+
if (variadic[k] === "i" && variadic.slice(k, k + 11) === "interface{}") {
|
|
3547
|
+
result += "any";
|
|
3548
|
+
k += 11;
|
|
3549
|
+
} else {
|
|
3550
|
+
result += variadic[k];
|
|
3551
|
+
k++;
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
let start = 0;
|
|
3555
|
+
let end = result.length;
|
|
3556
|
+
while (start < end && result[start] === " ") start++;
|
|
3557
|
+
while (end > start && result[end - 1] === " ") end--;
|
|
3558
|
+
return result.slice(start, end);
|
|
3559
|
+
}
|
|
3560
|
+
getGoPackageName(importPath) {
|
|
3561
|
+
const parts = importPath.split("/");
|
|
3562
|
+
return parts[parts.length - 1];
|
|
3563
|
+
}
|
|
3564
|
+
detectFrameworkFromGoImport(importPath) {
|
|
3565
|
+
for (const rule of GO_FRAMEWORK_RULES) {
|
|
3566
|
+
if (rule.matches(importPath)) return rule.name;
|
|
3567
|
+
}
|
|
3568
|
+
return void 0;
|
|
3569
|
+
}
|
|
3570
|
+
isGoStandardLibrary(importPath) {
|
|
3571
|
+
const stdLibPrefixes = [
|
|
3572
|
+
"fmt",
|
|
3573
|
+
"net/http",
|
|
3574
|
+
"os",
|
|
3575
|
+
"io",
|
|
3576
|
+
"strings",
|
|
3577
|
+
"strconv",
|
|
3578
|
+
"time",
|
|
3579
|
+
"json",
|
|
3580
|
+
"context",
|
|
3581
|
+
"sync",
|
|
3582
|
+
"log",
|
|
3583
|
+
"bufio",
|
|
3584
|
+
"bytes",
|
|
3585
|
+
"crypto",
|
|
3586
|
+
"database/sql",
|
|
3587
|
+
"encoding",
|
|
3588
|
+
"errors",
|
|
3589
|
+
"flag",
|
|
3590
|
+
"html",
|
|
3591
|
+
"math",
|
|
3592
|
+
"path",
|
|
3593
|
+
"reflect",
|
|
3594
|
+
"regexp",
|
|
3595
|
+
"runtime",
|
|
3596
|
+
"sort",
|
|
3597
|
+
"testing",
|
|
3598
|
+
"unicode",
|
|
3599
|
+
"unsafe"
|
|
3600
|
+
];
|
|
3601
|
+
return someSymbol(stdLibPrefixes, (prefix) => importPath === prefix || importPath.startsWith(prefix + "/")) || !stringIncludes(importPath, ".");
|
|
3602
|
+
}
|
|
3603
|
+
isGoThirdPartyPackage(importPath) {
|
|
3604
|
+
return stringIncludes(importPath, ".") && !this.isGoStandardLibrary(importPath);
|
|
3605
|
+
}
|
|
3606
|
+
analyzeGoErrorHandling(symbols) {
|
|
3607
|
+
const functions = filterSymbols(
|
|
3608
|
+
symbols,
|
|
3609
|
+
(s) => s.kind === ASTEngine.SymbolKind.Function || s.kind === ASTEngine.SymbolKind.Method
|
|
3610
|
+
);
|
|
3611
|
+
let withErrorHandling = 0;
|
|
3612
|
+
for (const func of functions) {
|
|
3613
|
+
if (func.metadata?.hasErrorHandling) {
|
|
3614
|
+
withErrorHandling++;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
return { functions: functions.length, withErrorHandling };
|
|
3618
|
+
}
|
|
3619
|
+
/**
|
|
3620
|
+
* Go Architectural Score Calculation
|
|
3621
|
+
*/
|
|
3622
|
+
calculateArchitecturalScore(symbols, relationships, patterns) {
|
|
3623
|
+
let score = 75;
|
|
3624
|
+
if (inArray(patterns, "error_handling")) score += 8;
|
|
3625
|
+
if (inArray(patterns, "goroutines")) score += 6;
|
|
3626
|
+
if (inArray(patterns, "channels")) score += 6;
|
|
3627
|
+
if (inArray(patterns, "interfaces")) score += 5;
|
|
3628
|
+
if (inArray(patterns, "dependency_injection")) score += 4;
|
|
3629
|
+
const structs = filterSymbols(symbols, (s) => s.kind === ASTEngine.SymbolKind.Struct && s.metadata?.isGoStruct);
|
|
3630
|
+
const methods = filterSymbols(symbols, (s) => s.kind === ASTEngine.SymbolKind.Method);
|
|
3631
|
+
if (structs.length > 0) {
|
|
3632
|
+
const methodToStructRatio = methods.length / structs.length;
|
|
3633
|
+
if (methodToStructRatio >= 1 && methodToStructRatio <= 5) score += 3;
|
|
3634
|
+
}
|
|
3635
|
+
const interfaces = filterSymbols(symbols, (s) => s.kind === ASTEngine.SymbolKind.Interface);
|
|
3636
|
+
if (interfaces.length > 0) {
|
|
3637
|
+
score += Math.min(interfaces.length * 2, 6);
|
|
3638
|
+
}
|
|
3639
|
+
const complexFunctions = filterSymbols(symbols, (s) => !!s.qualified_calls && s.qualified_calls.length > 12);
|
|
3640
|
+
score -= complexFunctions.length * 2;
|
|
3641
|
+
return Math.max(40, Math.min(100, score));
|
|
3642
|
+
}
|
|
3643
|
+
/**
|
|
3644
|
+
* Go Code Quality Assessment
|
|
3645
|
+
*/
|
|
3646
|
+
assessCodeQuality(symbols, relationships, patterns) {
|
|
3647
|
+
const score = this.calculateArchitecturalScore(symbols, relationships, patterns);
|
|
3648
|
+
if (score >= 90) return "exceptional";
|
|
3649
|
+
if (score >= 80) return "excellent";
|
|
3650
|
+
if (score >= 70) return "good";
|
|
3651
|
+
if (score >= 60) return "fair";
|
|
3652
|
+
if (score >= 50) return "needs_improvement";
|
|
3653
|
+
return "poor";
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Go Cyclomatic Complexity Calculation
|
|
3657
|
+
* Calculates complexity for Go code including goroutines, channels, defer, type switches, and Go-specific patterns
|
|
3658
|
+
*/
|
|
3659
|
+
calculateCyclomaticComplexity(node) {
|
|
3660
|
+
let complexity = 1;
|
|
3661
|
+
const traverse = (n) => {
|
|
3662
|
+
switch (n.type) {
|
|
3663
|
+
case "if_statement":
|
|
3664
|
+
case "for_statement":
|
|
3665
|
+
case "expression_case":
|
|
3666
|
+
case "type_case":
|
|
3667
|
+
case "communication_case":
|
|
3668
|
+
case "default_case":
|
|
3669
|
+
complexity++;
|
|
3670
|
+
break;
|
|
3671
|
+
case "binary_expression": {
|
|
3672
|
+
const txt = n.text;
|
|
3673
|
+
if (stringIncludes(txt, "&&") || stringIncludes(txt, "||")) complexity++;
|
|
3674
|
+
break;
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
3678
|
+
const child = n.namedChild(i);
|
|
3679
|
+
if (child) traverse(child);
|
|
3680
|
+
}
|
|
3681
|
+
};
|
|
3682
|
+
traverse(node);
|
|
3683
|
+
return complexity;
|
|
3684
|
+
}
|
|
3685
|
+
// ===== GO HELPER METHODS FOR RELATIONSHIP ANALYSIS =====
|
|
3686
|
+
/**
|
|
3687
|
+
* Find child node by type
|
|
3688
|
+
*/
|
|
3689
|
+
findChildByType(node, type) {
|
|
3690
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3691
|
+
const child = node.namedChild(i);
|
|
3692
|
+
if (child?.type === type) return child;
|
|
3693
|
+
}
|
|
3694
|
+
return null;
|
|
3695
|
+
}
|
|
3696
|
+
findChildrenByType(node, type) {
|
|
3697
|
+
const results = [];
|
|
3698
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3699
|
+
const child = node.namedChild(i);
|
|
3700
|
+
if (child?.type === type) results.push(child);
|
|
3701
|
+
}
|
|
3702
|
+
return results;
|
|
3703
|
+
}
|
|
3704
|
+
/**
|
|
3705
|
+
* The embedded fields of a struct, as NODES. the `extends`
|
|
3706
|
+
* edge these feed carries the embedded TYPE's own line, so the walk has to
|
|
3707
|
+
* return the node the name is written on and not just its text.
|
|
3708
|
+
*
|
|
3709
|
+
* GO26 (round 5): the `extractGoEmbeddedTypes` TEXT form is removed. It was
|
|
3710
|
+
* defined to delegate here and had no call site anywhere in the repository —
|
|
3711
|
+
* a second entry point onto the same walk, and therefore a second thing that
|
|
3712
|
+
* could drift from it. There is now exactly one.
|
|
3713
|
+
*/
|
|
3714
|
+
extractGoEmbeddedTypeNodes(structNode) {
|
|
3715
|
+
const embeddedTypes = [];
|
|
3716
|
+
const fieldList = this.findChildByType(structNode, "field_declaration_list");
|
|
3717
|
+
if (fieldList) {
|
|
3718
|
+
for (let i = 0; i < fieldList.namedChildCount; i++) {
|
|
3719
|
+
const field = fieldList.namedChild(i);
|
|
3720
|
+
if (field?.type === "field_declaration") {
|
|
3721
|
+
const fieldNames = [];
|
|
3722
|
+
for (let j = 0; j < field.namedChildCount; j++) {
|
|
3723
|
+
const c = field.namedChild(j);
|
|
3724
|
+
if (c && (c.type === "field_identifier" || c.type === "identifier")) fieldNames.push(c);
|
|
3725
|
+
}
|
|
3726
|
+
if (fieldNames.length === 0) {
|
|
3727
|
+
const typeNode = field.childForFieldName("type") || this.findChildByType(field, "type_identifier");
|
|
3728
|
+
if (typeNode) {
|
|
3729
|
+
embeddedTypes.push(typeNode);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
return embeddedTypes;
|
|
3736
|
+
}
|
|
3737
|
+
/**
|
|
3738
|
+
* Get interface methods from interface symbol
|
|
3739
|
+
*/
|
|
3740
|
+
getGoInterfaceMethods(interfaceSymbol, symbols, visiting) {
|
|
3741
|
+
const seen = visiting ?? /* @__PURE__ */ new Set();
|
|
3742
|
+
if (seen.has(interfaceSymbol.name)) return [];
|
|
3743
|
+
seen.add(interfaceSymbol.name);
|
|
3744
|
+
const interfaceMethods = [];
|
|
3745
|
+
const methods = filterSymbols(
|
|
3746
|
+
symbols,
|
|
3747
|
+
(s) => s.kind === ASTEngine.SymbolKind.Method && s.ownerName === interfaceSymbol.name
|
|
3748
|
+
);
|
|
3749
|
+
forEachSymbol(methods, (method) => {
|
|
3750
|
+
const signature = this.buildGoMethodSignature(method);
|
|
3751
|
+
interfaceMethods.push({
|
|
3752
|
+
name: method.name,
|
|
3753
|
+
signature
|
|
3754
|
+
});
|
|
3755
|
+
});
|
|
3756
|
+
const embedded = filterSymbols(
|
|
3757
|
+
symbols,
|
|
3758
|
+
(s) => s.ownerName === interfaceSymbol.name && s.ownerKind === ASTEngine.SymbolKind.Interface && s.metadata?.isEmbeddedInterface
|
|
3759
|
+
);
|
|
3760
|
+
forEachSymbol(embedded, (entry) => {
|
|
3761
|
+
const target = findSymbol(
|
|
3762
|
+
symbols,
|
|
3763
|
+
(s) => s.kind === ASTEngine.SymbolKind.Interface && s.name === entry.name && !s.metadata?.isEmbeddedInterface
|
|
3764
|
+
);
|
|
3765
|
+
if (!target) return;
|
|
3766
|
+
const inherited = this.getGoInterfaceMethods(target, symbols, seen);
|
|
3767
|
+
forEachSymbol(inherited, (m) => {
|
|
3768
|
+
if (!someSymbol(interfaceMethods, (x) => x.name === m.name && x.signature === m.signature)) {
|
|
3769
|
+
interfaceMethods.push(m);
|
|
3770
|
+
}
|
|
3771
|
+
});
|
|
3772
|
+
});
|
|
3773
|
+
seen.delete(interfaceSymbol.name);
|
|
3774
|
+
return interfaceMethods;
|
|
3775
|
+
}
|
|
3776
|
+
/**
|
|
3777
|
+
* Check if struct implements interface (Go's implicit interface implementation)
|
|
3778
|
+
*/
|
|
3779
|
+
checkGoInterfaceImplementation(interfaceMethods, structMethods) {
|
|
3780
|
+
const matchedMethods = [];
|
|
3781
|
+
const totalMethods = interfaceMethods.length;
|
|
3782
|
+
if (totalMethods === 0) {
|
|
3783
|
+
return { implements: false, confidence: 0, matchedMethods: [] };
|
|
3784
|
+
}
|
|
3785
|
+
const available = /* @__PURE__ */ new Set();
|
|
3786
|
+
forEachSymbol(structMethods, (m) => {
|
|
3787
|
+
available.add(`${m.name}\0${this.normalizeGoSignature(this.buildGoMethodSignature(m))}`);
|
|
3788
|
+
});
|
|
3789
|
+
forEachSymbol(interfaceMethods, (interfaceMethod) => {
|
|
3790
|
+
if (available.has(`${interfaceMethod.name}\0${this.normalizeGoSignature(interfaceMethod.signature)}`)) {
|
|
3791
|
+
matchedMethods.push(interfaceMethod.name);
|
|
3792
|
+
}
|
|
3793
|
+
});
|
|
3794
|
+
const doesImplement = matchedMethods.length === totalMethods;
|
|
3795
|
+
const confidence = doesImplement ? 1 : matchedMethods.length / totalMethods;
|
|
3796
|
+
return { implements: doesImplement, confidence, matchedMethods };
|
|
3797
|
+
}
|
|
3798
|
+
/**
|
|
3799
|
+
* Build Go method signature for comparison
|
|
3800
|
+
*/
|
|
3801
|
+
buildGoMethodSignature(method) {
|
|
3802
|
+
const key = method.range?.startIndex;
|
|
3803
|
+
if (typeof key === "number") {
|
|
3804
|
+
const hit = this.goSignatures.get(key);
|
|
3805
|
+
if (hit !== void 0) return hit;
|
|
3806
|
+
}
|
|
3807
|
+
const params = mapSymbols(method.parameters || [], (p) => (p.isVariadic ? "..." : "") + (p.type || "interface{}")).join(", ") || "";
|
|
3808
|
+
const returnType = method.returnType || "void";
|
|
3809
|
+
const sig = `${method.name}(${params}) ${returnType}`;
|
|
3810
|
+
if (typeof key === "number") this.goSignatures.set(key, sig);
|
|
3811
|
+
return sig;
|
|
3812
|
+
}
|
|
3813
|
+
/**
|
|
3814
|
+
* The name of THIS file's own package, read from the grammar.
|
|
3815
|
+
*
|
|
3816
|
+
* `package_clause` has NO fields in tree-sitter-go 0.25.0's node-types.json:
|
|
3817
|
+
* the identifier is a plain named child. Reading `childForFieldName("name")`
|
|
3818
|
+
* here returns "" and the caller would then reject EVERY selector callee,
|
|
3819
|
+
* silently deleting the self-qualified constructions the edge is supposed to
|
|
3820
|
+
* carry — the same class of silent total loss the oracle hit on its first run.
|
|
3821
|
+
*/
|
|
3822
|
+
goOwnPackageName(rootNode) {
|
|
3823
|
+
const pkgClause = this.findChildByType(rootNode, "package_clause");
|
|
3824
|
+
if (!pkgClause) return "";
|
|
3825
|
+
for (let i = 0; i < pkgClause.namedChildCount; i++) {
|
|
3826
|
+
const c = pkgClause.namedChild(i);
|
|
3827
|
+
if (c && c.type === "package_identifier") return String(c.text ?? "");
|
|
3828
|
+
}
|
|
3829
|
+
return "";
|
|
3830
|
+
}
|
|
3831
|
+
/**
|
|
3832
|
+
* Can this call's callee be the top-level function THIS FILE declares?
|
|
3833
|
+
*
|
|
3834
|
+
* The `instantiates` edge's target is always a symbol from this file's own
|
|
3835
|
+
* symbol table, so the question is not "is this a package-qualified call" —
|
|
3836
|
+
* it is "does this call select something this file could be responsible for".
|
|
3837
|
+
* Two shapes qualify:
|
|
3838
|
+
*
|
|
3839
|
+
* `NewReader(1)` a bare identifier call. In Go a bare call at
|
|
3840
|
+
* package scope can only reach a name in this
|
|
3841
|
+
* package (or a dot-imported one, which the
|
|
3842
|
+
* leniency parameter already handles).
|
|
3843
|
+
* `calleeshape.NewReader(3)` the file's OWN package name as the qualifier.
|
|
3844
|
+
* Legal Go — `src/html/template/escape.go:977` is
|
|
3845
|
+
* `func HTMLEscape(...) { template.HTMLEscape(w, b) }`
|
|
3846
|
+
* — and it selects this file's top-level function.
|
|
3847
|
+
*
|
|
3848
|
+
* Any OTHER qualifier does not qualify, and the reason is the bug this method
|
|
3849
|
+
* exists for. `bufio.NewReader(r)` is a top-level function of `bufio`, but the
|
|
3850
|
+
* `instantiates` target had to come from THIS file's symbol table, so a file
|
|
3851
|
+
* that happened to declare its own `NewReader` published
|
|
3852
|
+
* `instantiates ReceiverCollision -> NewReader` for a construction it did not
|
|
3853
|
+
* perform. The callee's shape was right; the callee's PACKAGE was not the one
|
|
3854
|
+
* the target symbol lives in. Counting import aliases as qualifiers does not
|
|
3855
|
+
* fix this — an alias can collide with a same-named local function just as
|
|
3856
|
+
* easily as a package name can.
|
|
3857
|
+
*
|
|
3858
|
+
* Measured over a 500-file stride sample of go-stdlib: 155 `instantiates`
|
|
3859
|
+
* edges at confidence 0.6, 8 of them (6 files) naming a symbol in the wrong
|
|
3860
|
+
* package. See validation/m14/go/go-mislabel-probe.ts and the fixture
|
|
3861
|
+
* `instantiates-callee-shape`.
|
|
3862
|
+
*/
|
|
3863
|
+
isGoTopLevelFunctionCallee(calleeNode, ownPackageName) {
|
|
3864
|
+
if (!calleeNode) return false;
|
|
3865
|
+
if (calleeNode.type === "identifier") return true;
|
|
3866
|
+
if (calleeNode.type !== "selector_expression") return false;
|
|
3867
|
+
const operand = calleeNode.childForFieldName("operand");
|
|
3868
|
+
if (!operand || operand.type !== "identifier") return false;
|
|
3869
|
+
if (ownPackageName.length === 0) return false;
|
|
3870
|
+
return String(operand.text ?? "") === ownPackageName;
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Resolve Go function call to find target
|
|
3874
|
+
*/
|
|
3875
|
+
resolveGoFunctionCall(callExpression, symbols, dotImported = false) {
|
|
3876
|
+
if (stringIncludes(callExpression, ".")) {
|
|
3877
|
+
const parts = callExpression.split(".");
|
|
3878
|
+
const functionName = parts[parts.length - 1];
|
|
3879
|
+
if (!functionName) return void 0;
|
|
3880
|
+
const fallback = findSymbol(symbols, (s) => this.isGoPackageCallable(s) && s.name === functionName);
|
|
3881
|
+
return fallback?.name ?? functionName;
|
|
3882
|
+
}
|
|
3883
|
+
if (dotImported) return callExpression || void 0;
|
|
3884
|
+
const targetFunction = findSymbol(symbols, (s) => this.isGoPackageCallable(s) && s.name === callExpression);
|
|
3885
|
+
return targetFunction?.name;
|
|
3886
|
+
}
|
|
3887
|
+
/**
|
|
3888
|
+
* Extract Go package name from content
|
|
3889
|
+
*/
|
|
3890
|
+
findGoPackageClause(rootNode) {
|
|
3891
|
+
return this.findChildByType(rootNode, "package_clause") ?? void 0;
|
|
3892
|
+
}
|
|
3893
|
+
/**
|
|
3894
|
+
* The package name a `package_clause` declares. M14.0.F1 (#880):
|
|
3895
|
+
* `package_clause` declares NO fields in this grammar, so the old
|
|
3896
|
+
* `childForFieldName("package_identifier")` could never match (that is a
|
|
3897
|
+
* node TYPE, not a field) and the named-child fallback carried the value all
|
|
3898
|
+
* along. Read the node by TYPE, so the audit's field table and this lookup
|
|
3899
|
+
* agree.
|
|
3900
|
+
*/
|
|
3901
|
+
goPackageClauseName(clause) {
|
|
3902
|
+
const pkgId = this.findChildByType(clause, "package_identifier") ?? clause.namedChild(0);
|
|
3903
|
+
return pkgId ? pkgId.text : void 0;
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* Extract function called in goroutine
|
|
3907
|
+
*/
|
|
3908
|
+
extractGoRoutineFunction(node) {
|
|
3909
|
+
const callExpr = this.findChildByType(node, "call_expression");
|
|
3910
|
+
if (callExpr) {
|
|
3911
|
+
const functionNode = callExpr.childForFieldName("function");
|
|
3912
|
+
return this.resolveGoCallLeaf(functionNode);
|
|
3913
|
+
}
|
|
3914
|
+
return void 0;
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* Find containing Go function for a node
|
|
3918
|
+
*/
|
|
3919
|
+
findContainingGoFunction(node) {
|
|
3920
|
+
let current = node.parent;
|
|
3921
|
+
while (current) {
|
|
3922
|
+
if (current.type === "function_declaration" || current.type === "method_declaration") {
|
|
3923
|
+
const nameNode = current.childForFieldName("name");
|
|
3924
|
+
return nameNode?.text;
|
|
3925
|
+
}
|
|
3926
|
+
current = current.parent;
|
|
3927
|
+
}
|
|
3928
|
+
return void 0;
|
|
3929
|
+
}
|
|
3930
|
+
/**
|
|
3931
|
+
* GOD12 (round 7): the SOURCE for work done inside a PACKAGE-SCOPE initializer.
|
|
3932
|
+
*
|
|
3933
|
+
* `findContainingGoFunction` walks up for a `function_declaration` or
|
|
3934
|
+
* `method_declaration` and returns undefined when there is none, and every
|
|
3935
|
+
* caller turned that into the `global` pseudo-source. That is correct for a
|
|
3936
|
+
* statement at file scope and wrong for the very common
|
|
3937
|
+
*
|
|
3938
|
+
* var re = regexp.MustCompile(`...`)
|
|
3939
|
+
* var api = http.Client{Timeout: time.Second}
|
|
3940
|
+
* var host = os.Getenv("HOST") + ".example.com"
|
|
3941
|
+
*
|
|
3942
|
+
* where the call, the construction and the selector read are all work the
|
|
3943
|
+
* DECLARATION is doing, for a binding the analyzer has already published as a
|
|
3944
|
+
* symbol. At full extent that was 6,723 edges whose source named nothing:
|
|
3945
|
+
* 3,396 `uses`, 1,391 `calls`, 985 `instantiates`, 947 `references`.
|
|
3946
|
+
*
|
|
3947
|
+
* The rule is the one the declared-type pass already follows: a declaration
|
|
3948
|
+
* attributes to what it declares. `global` remains the answer for work that is
|
|
3949
|
+
* genuinely at file scope, and a FUNCTION-LOCAL `var` deliberately does not
|
|
3950
|
+
* use its name — LocalsAreNotSymbols makes a local var not a symbol, so
|
|
3951
|
+
* naming it would trade one unresolvable source for another. A `func_literal`
|
|
3952
|
+
* is skipped rather than treated as a stop, which is the pre-existing
|
|
3953
|
+
* behaviour and is also what makes `var y = func() { f() }()` attribute to `y`.
|
|
3954
|
+
*/
|
|
3955
|
+
resolveGoWorkSource(node) {
|
|
3956
|
+
const declaredNameOf = (spec) => {
|
|
3957
|
+
for (let q = spec.parent; q; q = q.parent) {
|
|
3958
|
+
if (q.type === "function_declaration" || q.type === "method_declaration" || q.type === "func_literal") return void 0;
|
|
3959
|
+
}
|
|
3960
|
+
for (let i = 0; i < spec.namedChildCount; i++) {
|
|
3961
|
+
const c = spec.namedChild(i);
|
|
3962
|
+
if (c && c.type === "identifier" && String(c.text ?? "") !== "_") return String(c.text ?? "");
|
|
3963
|
+
}
|
|
3964
|
+
return void 0;
|
|
3965
|
+
};
|
|
3966
|
+
for (let q = node; q; q = q.parent) {
|
|
3967
|
+
if (q.type === "function_declaration" || q.type === "method_declaration") {
|
|
3968
|
+
for (let i = 0; i < q.namedChildCount; i++) {
|
|
3969
|
+
const c = q.namedChild(i);
|
|
3970
|
+
if (!c) continue;
|
|
3971
|
+
if (q.type === "method_declaration" && c.type === "field_identifier" || q.type === "function_declaration" && c.type === "identifier") return String(c.text ?? "");
|
|
3972
|
+
}
|
|
3973
|
+
return "global";
|
|
3974
|
+
}
|
|
3975
|
+
if (q.type === "var_spec" || q.type === "const_spec") {
|
|
3976
|
+
const nm = declaredNameOf(q);
|
|
3977
|
+
if (nm) return nm;
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
return "global";
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Extract channel variable from channel operation
|
|
3984
|
+
*/
|
|
3985
|
+
// GOD8 (M37 #604 wave-3): channel-op target resolution. Returns the
|
|
3986
|
+
// channel's declared-name LEAF for (a) a bare identifier naming a
|
|
3987
|
+
// PACKAGE-scope var/const (caller gates via isGoDeclaredPackageVar) and
|
|
3988
|
+
// (b) a member selector (leaf = field name — the channel-op pass owns
|
|
3989
|
+
// member channel edges with send/receive direction, so the selector pass
|
|
3990
|
+
// skips channel operands). Call results (`<-ctx.Done()`) and bare
|
|
3991
|
+
// LOCAL/param channels (`c <- v`) have no declared channel symbol and
|
|
3992
|
+
// return undefined (LocalsAreNotSymbols — same resolvability gate the
|
|
3993
|
+
// goroutine/defer branches apply via isGoResolvableCallTarget).
|
|
3994
|
+
extractGoChannelVariable(node) {
|
|
3995
|
+
const operandNode = (n) => {
|
|
3996
|
+
if (n.type === "send_statement") {
|
|
3997
|
+
const c = n.childForFieldName("channel");
|
|
3998
|
+
return c?.type === "identifier" || c?.type === "selector_expression" ? c : void 0;
|
|
3999
|
+
}
|
|
4000
|
+
const direct = n.childForFieldName("operand") || (n.namedChildCount > 0 ? n.namedChild(0) : void 0);
|
|
4001
|
+
const unwrapped = direct && (direct.type === "unary_expression" || direct.type === "receive_statement") ? direct.namedChildCount > 0 ? direct.namedChild(0) : void 0 : direct;
|
|
4002
|
+
return unwrapped?.type === "identifier" || unwrapped?.type === "selector_expression" ? unwrapped : void 0;
|
|
4003
|
+
};
|
|
4004
|
+
if (node.type === "send_statement" || node.type === "receive_statement" || this.isGoReceiveExpression(node)) {
|
|
4005
|
+
const op = operandNode(node);
|
|
4006
|
+
if (!op) return void 0;
|
|
4007
|
+
if (op.type === "identifier") return { name: op.text, member: false };
|
|
4008
|
+
if (op.type === "selector_expression") {
|
|
4009
|
+
const f = op.childForFieldName("field");
|
|
4010
|
+
return f ? { name: f.text, member: true } : void 0;
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
return void 0;
|
|
4014
|
+
}
|
|
4015
|
+
isGoReceiveExpression(node) {
|
|
4016
|
+
return node.type === "unary_expression" && node.child(0)?.type === "<-";
|
|
4017
|
+
}
|
|
4018
|
+
// GOD8: a bare identifier is a valid channel-op target only when it names a
|
|
4019
|
+
// declared PACKAGE-scope var/const (LocalsAreNotSymbols — locals/params
|
|
4020
|
+
// never qualify; the same resolvability gate the goroutine/defer branches
|
|
4021
|
+
// apply via isGoResolvableCallTarget). Member selectors (member=true) are
|
|
4022
|
+
// always valid — the field is a declared member.
|
|
4023
|
+
isGoDeclaredPackageVar(name, symbols) {
|
|
4024
|
+
return someSymbol(
|
|
4025
|
+
symbols,
|
|
4026
|
+
(s) => s.name === name && (s.kind === ASTEngine.SymbolKind.Variable || s.kind === ASTEngine.SymbolKind.Constant) && !s.ownerName
|
|
4027
|
+
);
|
|
4028
|
+
}
|
|
4029
|
+
/**
|
|
4030
|
+
* Extract deferred function from defer statement
|
|
4031
|
+
*/
|
|
4032
|
+
extractDeferredFunction(node) {
|
|
4033
|
+
const callExpr = this.findChildByType(node, "call_expression");
|
|
4034
|
+
if (callExpr) {
|
|
4035
|
+
const functionNode = callExpr.childForFieldName("function");
|
|
4036
|
+
return this.resolveGoCallLeaf(functionNode);
|
|
4037
|
+
}
|
|
4038
|
+
return void 0;
|
|
4039
|
+
}
|
|
4040
|
+
extractGoMethodSpecParameters(node) {
|
|
4041
|
+
const parameters = [];
|
|
4042
|
+
const paramList = this.findChildByType(node, "parameter_list");
|
|
4043
|
+
if (!paramList) return parameters;
|
|
4044
|
+
for (let i = 0; i < paramList.namedChildCount; i++) {
|
|
4045
|
+
const param = paramList.namedChild(i);
|
|
4046
|
+
const isVariadic = param?.type === "variadic_parameter_declaration";
|
|
4047
|
+
if (param?.type !== "parameter_declaration" && !isVariadic) continue;
|
|
4048
|
+
const typeNode = param.childForFieldName("type");
|
|
4049
|
+
const nameNodes = [];
|
|
4050
|
+
for (let j = 0; j < param.namedChildCount; j++) {
|
|
4051
|
+
const c = param.namedChild(j);
|
|
4052
|
+
if (c && c.type === "identifier") nameNodes.push(c);
|
|
4053
|
+
}
|
|
4054
|
+
if (nameNodes.length > 0) {
|
|
4055
|
+
for (const nameNode of nameNodes) {
|
|
4056
|
+
if (!nameNode) continue;
|
|
4057
|
+
parameters.push({
|
|
4058
|
+
name: nameNode.text,
|
|
4059
|
+
type: typeNode?.text,
|
|
4060
|
+
isOptional: false,
|
|
4061
|
+
...isVariadic ? { isVariadic: true } : {}
|
|
4062
|
+
});
|
|
4063
|
+
}
|
|
4064
|
+
} else if (typeNode) {
|
|
4065
|
+
parameters.push({
|
|
4066
|
+
name: `arg${parameters.length}`,
|
|
4067
|
+
type: typeNode.text,
|
|
4068
|
+
isOptional: false,
|
|
4069
|
+
...isVariadic ? { isVariadic: true } : {}
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
return parameters;
|
|
4074
|
+
}
|
|
4075
|
+
extractGoMethodSpecReturnType(node) {
|
|
4076
|
+
const resultNode = node.childForFieldName("result");
|
|
4077
|
+
if (resultNode) {
|
|
4078
|
+
if (resultNode.type === "parameter_list") {
|
|
4079
|
+
const types = [];
|
|
4080
|
+
for (let i = 0; i < resultNode.namedChildCount; i++) {
|
|
4081
|
+
const p = resultNode.namedChild(i);
|
|
4082
|
+
if (!p || p.type !== "parameter_declaration") continue;
|
|
4083
|
+
const t = p.childForFieldName("type");
|
|
4084
|
+
if (!t) continue;
|
|
4085
|
+
let names = 0;
|
|
4086
|
+
for (let j = 0; j < p.namedChildCount; j++) {
|
|
4087
|
+
const c = p.namedChild(j);
|
|
4088
|
+
if (c && c.type === "identifier") names++;
|
|
4089
|
+
}
|
|
4090
|
+
for (let k = 0; k < Math.max(1, names); k++) types.push(t.text);
|
|
4091
|
+
}
|
|
4092
|
+
if (types.length === 1) return types[0];
|
|
4093
|
+
if (types.length > 1) return `(${types.join(", ")})`;
|
|
4094
|
+
return resultNode.text;
|
|
4095
|
+
}
|
|
4096
|
+
return resultNode.text;
|
|
4097
|
+
}
|
|
4098
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4099
|
+
const child = node.namedChild(i);
|
|
4100
|
+
if (child && inArray(
|
|
4101
|
+
[
|
|
4102
|
+
"type_identifier",
|
|
4103
|
+
"qualified_type",
|
|
4104
|
+
"pointer_type",
|
|
4105
|
+
"slice_type",
|
|
4106
|
+
"array_type",
|
|
4107
|
+
"map_type",
|
|
4108
|
+
"channel_type",
|
|
4109
|
+
"function_type"
|
|
4110
|
+
],
|
|
4111
|
+
child.type
|
|
4112
|
+
)) {
|
|
4113
|
+
return child.text;
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
return void 0;
|
|
4117
|
+
}
|
|
4118
|
+
hasGoErrorHandling(text) {
|
|
4119
|
+
if (!text) return false;
|
|
4120
|
+
return stringIncludes(text, "err != nil") || stringIncludes(text, "fmt.Errorf(") || stringIncludes(text, "errors.New(") || stringIncludes(text, "panic(");
|
|
4121
|
+
}
|
|
4122
|
+
};
|
|
4123
|
+
export {
|
|
4124
|
+
GO_STDLIB_TOP_LEVEL,
|
|
4125
|
+
RealGoAnalyzer
|
|
4126
|
+
};
|