@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,2429 @@
|
|
|
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-ruby-analyzer.ts
|
|
12
|
+
function isAsciiWhitespace(code) {
|
|
13
|
+
return code === 32 || code === 9 || code === 10 || code === 13 || code === 12 || code === 11;
|
|
14
|
+
}
|
|
15
|
+
var FILE_SCOPE_OWNER = "Global";
|
|
16
|
+
function rubyReopenIdentity(rawName, enclosingOwner) {
|
|
17
|
+
const sepIdx = rawName.lastIndexOf("::");
|
|
18
|
+
if (sepIdx < 0) return { name: rawName, owner: enclosingOwner };
|
|
19
|
+
const name = rawName.substring(sepIdx + 2);
|
|
20
|
+
const scopeOwner = rawName.substring(0, sepIdx);
|
|
21
|
+
if (scopeOwner.length === 0) return { name, owner: enclosingOwner };
|
|
22
|
+
return { name, owner: enclosingOwner ? `${enclosingOwner}::${scopeOwner}` : scopeOwner };
|
|
23
|
+
}
|
|
24
|
+
function rubyFileUnitName(filePath) {
|
|
25
|
+
if (!filePath) return FILE_SCOPE_OWNER;
|
|
26
|
+
let end = filePath.length;
|
|
27
|
+
while (end > 0) {
|
|
28
|
+
const c = filePath.charCodeAt(end - 1);
|
|
29
|
+
if (c === 47 || c === 92) break;
|
|
30
|
+
end--;
|
|
31
|
+
}
|
|
32
|
+
const tail = filePath.substring(end);
|
|
33
|
+
if (tail.length === 0) return FILE_SCOPE_OWNER;
|
|
34
|
+
const dot = tail.lastIndexOf(".");
|
|
35
|
+
let stem = dot > 0 ? tail.substring(0, dot) : tail;
|
|
36
|
+
if (stem.indexOf(".") >= 0) {
|
|
37
|
+
let out = "";
|
|
38
|
+
for (let i = 0; i < stem.length; i++) out += stem[i] === "." ? "_" : stem[i];
|
|
39
|
+
stem = out;
|
|
40
|
+
}
|
|
41
|
+
return stem.length > 0 ? stem : FILE_SCOPE_OWNER;
|
|
42
|
+
}
|
|
43
|
+
var RAILS_FRAMEWORK = "Ruby on Rails";
|
|
44
|
+
var VARIABLE_RECEIVER_TYPES = /* @__PURE__ */ new Set(["instance_variable", "class_variable", "global_variable"]);
|
|
45
|
+
function isRubyVisibilityDirectiveName(name) {
|
|
46
|
+
return name === "private" || name === "protected" || name === "public";
|
|
47
|
+
}
|
|
48
|
+
var BARE_CALL_SKIP_PARENTS = /* @__PURE__ */ new Set([
|
|
49
|
+
// `call` — the METHOD NAME of a call node is part of the call, not a bare
|
|
50
|
+
// read: `Mutex.new` must not also read as a bare `new`. The pinned grammar
|
|
51
|
+
// folds every call shape — paren'd (`foo(1)`), paren-less command (`foo 1`),
|
|
52
|
+
// and command-with-block (`foo 1 do ... end`) — into `call` nodes, and
|
|
53
|
+
// `command` / `command_call` are not node types it can produce. This set USED
|
|
54
|
+
// to carry those two as "grammar-version drift" insurance, which cost two
|
|
55
|
+
// dead branches (a skip entry that can never match, and a `type ===` arm
|
|
56
|
+
// that can never fire) and made the file cite a grammar audit as proof of a
|
|
57
|
+
// claim the audit could not see.
|
|
58
|
+
//
|
|
59
|
+
// Removing the two dead entries is a one-token edit with a real failure mode
|
|
60
|
+
// and the rack-wave fixture caught it immediately: the same edit dropped
|
|
61
|
+
// `call` itself, and every constructor call in a default argument
|
|
62
|
+
// (`def initialize(app, mutex = Mutex.new)`) began reading its `new` as a
|
|
63
|
+
// bare call as well — `calls initialize->new`, a fabricated edge the RB-A
|
|
64
|
+
// fixture has forbidden since the rack wave. A cleanup of two provably dead
|
|
65
|
+
// strings was never a safe place to be casual about the live one beside them.
|
|
66
|
+
"call",
|
|
67
|
+
// NOTE: `method`/`singleton_method` are deliberately ABSENT — an endless
|
|
68
|
+
// `def label = name` body is a DIRECT identifier child of the def node,
|
|
69
|
+
// and only the FIRST named child is the def name (second review: the set
|
|
70
|
+
// form suppressed endless-body reads on both sides). The def-name check
|
|
71
|
+
// lives inline at the identifier branch (positional).
|
|
72
|
+
// `def size=(v)` parses the NAME as a `setter` node wrapping the plain
|
|
73
|
+
// identifier — without this the nested `size` leaked as a bare call
|
|
74
|
+
// (master-review 2026-09; oracle already carried `setter`).
|
|
75
|
+
"setter",
|
|
76
|
+
// Keyword `alias new_name old_name` (any scope): its identifiers are
|
|
77
|
+
// method-name DECLARATIONS, never expression calls. Class-level alias
|
|
78
|
+
// gets the RB-E alias_method contract in the main loop; alias inside a
|
|
79
|
+
// method body is a runtime-definition (SyntaxError in practice) and must
|
|
80
|
+
// emit nothing, not two junk calls (master-review 2026-09).
|
|
81
|
+
"alias",
|
|
82
|
+
// NOT present: `pair` — a hash/argument pair's VALUE is a real expression
|
|
83
|
+
// (`{ x: helper }`, `foo(k: helper)`); only colon-form KEYS are
|
|
84
|
+
// non-expression, and those are hash_key_symbol/simple_symbol nodes, never
|
|
85
|
+
// identifiers. Rocket-form keys (`key => v`) also evaluate. pair was
|
|
86
|
+
// suppressing genuine value-position method calls on both sides — a
|
|
87
|
+
// shared blind spot F5 cannot see (master-review 2026-09, RUBD12).
|
|
88
|
+
"method_parameters",
|
|
89
|
+
"parameters",
|
|
90
|
+
"block_parameters",
|
|
91
|
+
"lambda_parameters",
|
|
92
|
+
"block_parameters",
|
|
93
|
+
"class",
|
|
94
|
+
"module",
|
|
95
|
+
"singleton_class"
|
|
96
|
+
]);
|
|
97
|
+
var RUBY_BUILTINS = /* @__PURE__ */ new Set([
|
|
98
|
+
// Kernel / top-level
|
|
99
|
+
"puts",
|
|
100
|
+
"print",
|
|
101
|
+
"p",
|
|
102
|
+
"pp",
|
|
103
|
+
"p$",
|
|
104
|
+
"print_flush",
|
|
105
|
+
"putc",
|
|
106
|
+
"raise",
|
|
107
|
+
"fail",
|
|
108
|
+
"exit",
|
|
109
|
+
"exit!",
|
|
110
|
+
"abort",
|
|
111
|
+
"warn",
|
|
112
|
+
"gets",
|
|
113
|
+
"readline",
|
|
114
|
+
"readlines",
|
|
115
|
+
"rand",
|
|
116
|
+
"srand",
|
|
117
|
+
"sleep",
|
|
118
|
+
"loop",
|
|
119
|
+
"require",
|
|
120
|
+
"require_relative",
|
|
121
|
+
"load",
|
|
122
|
+
"autoload",
|
|
123
|
+
"eval",
|
|
124
|
+
"binding",
|
|
125
|
+
"local_variables",
|
|
126
|
+
"global_variables",
|
|
127
|
+
"global_variable_get",
|
|
128
|
+
"global_variable_set",
|
|
129
|
+
"caller",
|
|
130
|
+
"caller_locations",
|
|
131
|
+
"trace_var",
|
|
132
|
+
"untrace_var",
|
|
133
|
+
"method",
|
|
134
|
+
"public_method",
|
|
135
|
+
"private_method",
|
|
136
|
+
"protected_method",
|
|
137
|
+
"instance_method",
|
|
138
|
+
"public_instance_method",
|
|
139
|
+
"private_instance_method",
|
|
140
|
+
"respond_to",
|
|
141
|
+
"respond_to?",
|
|
142
|
+
"class",
|
|
143
|
+
"superclass",
|
|
144
|
+
"Object",
|
|
145
|
+
"Kernel",
|
|
146
|
+
"BasicObject",
|
|
147
|
+
"NilClass",
|
|
148
|
+
"TrueClass",
|
|
149
|
+
"FalseClass",
|
|
150
|
+
"Integer",
|
|
151
|
+
"Float",
|
|
152
|
+
"String",
|
|
153
|
+
"Array",
|
|
154
|
+
"Hash",
|
|
155
|
+
"Symbol",
|
|
156
|
+
"Regexp",
|
|
157
|
+
"Range",
|
|
158
|
+
"IO",
|
|
159
|
+
"File",
|
|
160
|
+
"Dir",
|
|
161
|
+
"Process",
|
|
162
|
+
"Signal",
|
|
163
|
+
"GC",
|
|
164
|
+
"ObjectSpace",
|
|
165
|
+
"Marshal",
|
|
166
|
+
"Thread",
|
|
167
|
+
"Fiber",
|
|
168
|
+
"Random",
|
|
169
|
+
"SecureRandom",
|
|
170
|
+
"Time",
|
|
171
|
+
"Date",
|
|
172
|
+
"DateTime",
|
|
173
|
+
"Math",
|
|
174
|
+
"Comparable",
|
|
175
|
+
"Enumerable",
|
|
176
|
+
"Errno",
|
|
177
|
+
// Ruby dunders / special methods
|
|
178
|
+
"super",
|
|
179
|
+
"method_missing",
|
|
180
|
+
"respond_to_missing?",
|
|
181
|
+
"initialize_copy",
|
|
182
|
+
"initialize_dup",
|
|
183
|
+
"initialize_clone",
|
|
184
|
+
"send",
|
|
185
|
+
"__send__",
|
|
186
|
+
"freeze",
|
|
187
|
+
"frozen?",
|
|
188
|
+
"dup",
|
|
189
|
+
"clone",
|
|
190
|
+
"taint",
|
|
191
|
+
"untaint",
|
|
192
|
+
"trust",
|
|
193
|
+
"untrust",
|
|
194
|
+
"hash",
|
|
195
|
+
"eql?",
|
|
196
|
+
"equal?",
|
|
197
|
+
"object_id",
|
|
198
|
+
"instance_of?",
|
|
199
|
+
"kind_of?",
|
|
200
|
+
"is_a?",
|
|
201
|
+
"nil?",
|
|
202
|
+
"instance_variable_defined?",
|
|
203
|
+
"instance_variables",
|
|
204
|
+
"instance_variable_get",
|
|
205
|
+
"instance_variable_set",
|
|
206
|
+
"class_variables",
|
|
207
|
+
"class_variable_defined?",
|
|
208
|
+
"class_variable_get",
|
|
209
|
+
"class_variable_set",
|
|
210
|
+
"const_defined?",
|
|
211
|
+
"const_get",
|
|
212
|
+
"const_set",
|
|
213
|
+
"const_defined?",
|
|
214
|
+
"method_defined?",
|
|
215
|
+
"private_method_defined?",
|
|
216
|
+
"protected_method_defined?",
|
|
217
|
+
"singleton_method",
|
|
218
|
+
"define_singleton_method",
|
|
219
|
+
"to_s",
|
|
220
|
+
"to_i",
|
|
221
|
+
"to_f",
|
|
222
|
+
"to_a",
|
|
223
|
+
"to_h",
|
|
224
|
+
"to_proc",
|
|
225
|
+
"inspect",
|
|
226
|
+
"display",
|
|
227
|
+
"extend",
|
|
228
|
+
"include",
|
|
229
|
+
"prepend",
|
|
230
|
+
// Rails macros (emit properties, not call edges)
|
|
231
|
+
"has_many",
|
|
232
|
+
"belongs_to",
|
|
233
|
+
"has_one",
|
|
234
|
+
"has_and_belongs_to_many",
|
|
235
|
+
"scope",
|
|
236
|
+
"default_scope",
|
|
237
|
+
"validates",
|
|
238
|
+
"validate",
|
|
239
|
+
"validates_with",
|
|
240
|
+
"validates_each",
|
|
241
|
+
"before_action",
|
|
242
|
+
"after_action",
|
|
243
|
+
"before_filter",
|
|
244
|
+
"after_filter",
|
|
245
|
+
"before_validation",
|
|
246
|
+
"after_validation",
|
|
247
|
+
"before_save",
|
|
248
|
+
"after_save",
|
|
249
|
+
"before_create",
|
|
250
|
+
"after_create",
|
|
251
|
+
"before_update",
|
|
252
|
+
"after_update",
|
|
253
|
+
"before_destroy",
|
|
254
|
+
"after_destroy",
|
|
255
|
+
"before_commit",
|
|
256
|
+
"after_commit",
|
|
257
|
+
"after_rollback",
|
|
258
|
+
"attr_reader",
|
|
259
|
+
"attr_writer",
|
|
260
|
+
"attr_accessor",
|
|
261
|
+
"alias_method",
|
|
262
|
+
"remove_method",
|
|
263
|
+
"undef_method",
|
|
264
|
+
"delegate",
|
|
265
|
+
"delegate_missing_to",
|
|
266
|
+
"event",
|
|
267
|
+
"transition",
|
|
268
|
+
"state_machine",
|
|
269
|
+
"module_function",
|
|
270
|
+
"using",
|
|
271
|
+
// Module-level visibility MODIFIER METHODS. `private_constant :X` is not a
|
|
272
|
+
// keyword in this grammar — it is an ordinary `call` on Module — and it was
|
|
273
|
+
// missing from this list, so it published `calls <owner> -> private_constant`:
|
|
274
|
+
// an edge whose target is a stdlib method no project declares, which is the same
|
|
275
|
+
// shape as every other entry above and the reason they are here. Found by the
|
|
276
|
+
// oracle-agreement suite, whose negative case asserts the modifier is not a
|
|
277
|
+
// call: the oracle suppressed it (it lists both names among its visibility
|
|
278
|
+
// modifiers) and the analyzer did not, and the analyzer is the side whose
|
|
279
|
+
// suppression list IS the contract — `RUBY_BUILTINS` is a list of things the
|
|
280
|
+
// payload does not carry as call targets, not a judgement about Ruby.
|
|
281
|
+
//
|
|
282
|
+
// The DECLARATION side is not lost: `private_constant`'s effect on the export
|
|
283
|
+
// surface is read by `collectPrivateConstants`. So the pair is the same shape as
|
|
284
|
+
// `attr_reader` (declares a member, no call edge) and `module_function`
|
|
285
|
+
// (declares, no call edge). Measured on the corpus: 142 `private_constant` and
|
|
286
|
+
// 2 `public_constant` sites.
|
|
287
|
+
"private_constant",
|
|
288
|
+
"public_constant",
|
|
289
|
+
"ruby2_keywords",
|
|
290
|
+
"no_touching"
|
|
291
|
+
]);
|
|
292
|
+
function isRubyInstanceVariableWrite(node) {
|
|
293
|
+
const parent = node.parent;
|
|
294
|
+
if (!parent) return false;
|
|
295
|
+
if (parent.type === "assignment" || parent.type === "operator_assignment") {
|
|
296
|
+
const left = parent.childForFieldName("left");
|
|
297
|
+
if (!left || left.id !== node.id) return false;
|
|
298
|
+
return left.type !== "call" && left.type !== "element_reference";
|
|
299
|
+
}
|
|
300
|
+
if (parent.type === "left_assignment_list") {
|
|
301
|
+
const grand = parent.parent;
|
|
302
|
+
if (!grand || grand.type !== "assignment" && grand.type !== "operator_assignment") return false;
|
|
303
|
+
const left = grand.childForFieldName("left");
|
|
304
|
+
if (!left || left.id !== parent.id) return false;
|
|
305
|
+
const _nc1 = parent.namedChildCount;
|
|
306
|
+
for (let i = 0; i < _nc1; i++) {
|
|
307
|
+
const c = parent.namedChild(i);
|
|
308
|
+
if (c && c.id === node.id) return c.type !== "call" && c.type !== "element_reference";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
function isRubyStaticSuperclassName(text) {
|
|
314
|
+
if (!text) return false;
|
|
315
|
+
const t = text.trim();
|
|
316
|
+
const isConstChar = (c) => {
|
|
317
|
+
const code = c.charCodeAt(0);
|
|
318
|
+
return code >= 65 && code <= 90;
|
|
319
|
+
};
|
|
320
|
+
const isNameChar = (c) => {
|
|
321
|
+
const code = c.charCodeAt(0);
|
|
322
|
+
return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || c === "_";
|
|
323
|
+
};
|
|
324
|
+
let i = 0;
|
|
325
|
+
const readConst = () => {
|
|
326
|
+
if (i >= t.length || !isConstChar(t[i])) return false;
|
|
327
|
+
i++;
|
|
328
|
+
while (i < t.length && isNameChar(t[i])) i++;
|
|
329
|
+
return true;
|
|
330
|
+
};
|
|
331
|
+
if (t.startsWith("::")) i = 2;
|
|
332
|
+
if (!readConst()) return false;
|
|
333
|
+
while (i < t.length) {
|
|
334
|
+
while (i < t.length && (t[i] === " " || t[i] === " ")) i++;
|
|
335
|
+
if (i >= t.length) break;
|
|
336
|
+
if (t[i] !== ":") return false;
|
|
337
|
+
if (t[i + 1] !== ":") return false;
|
|
338
|
+
i += 2;
|
|
339
|
+
while (i < t.length && (t[i] === " " || t[i] === " ")) i++;
|
|
340
|
+
if (!readConst()) return false;
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
function isRubyIdentifierShaped(name) {
|
|
345
|
+
if (!name) return false;
|
|
346
|
+
const isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 36;
|
|
347
|
+
const isDigit = (c) => c >= 48 && c <= 57;
|
|
348
|
+
if (!isAlpha(name.charCodeAt(0))) return false;
|
|
349
|
+
for (let i = 1; i < name.length; i++) {
|
|
350
|
+
const c = name.charCodeAt(i);
|
|
351
|
+
if (!isAlpha(c) && !isDigit(c)) return false;
|
|
352
|
+
}
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
var RAILS_MACRO_METHODS = /* @__PURE__ */ new Set([
|
|
356
|
+
"has_many",
|
|
357
|
+
"belongs_to",
|
|
358
|
+
"has_one",
|
|
359
|
+
"has_and_belongs_to_many",
|
|
360
|
+
"scope",
|
|
361
|
+
"default_scope",
|
|
362
|
+
"validates",
|
|
363
|
+
"validate",
|
|
364
|
+
"validates_with",
|
|
365
|
+
"validates_each",
|
|
366
|
+
"attr_reader",
|
|
367
|
+
"attr_writer",
|
|
368
|
+
"attr_accessor",
|
|
369
|
+
"alias_method",
|
|
370
|
+
"remove_method",
|
|
371
|
+
"undef_method",
|
|
372
|
+
"delegate",
|
|
373
|
+
"delegate_missing_to",
|
|
374
|
+
"event",
|
|
375
|
+
"transition",
|
|
376
|
+
"before_action",
|
|
377
|
+
"after_action",
|
|
378
|
+
"before_validation",
|
|
379
|
+
"after_validation",
|
|
380
|
+
"before_save",
|
|
381
|
+
"after_save",
|
|
382
|
+
"before_create",
|
|
383
|
+
"after_create",
|
|
384
|
+
"before_update",
|
|
385
|
+
"after_update",
|
|
386
|
+
"before_destroy",
|
|
387
|
+
"after_destroy",
|
|
388
|
+
"before_commit",
|
|
389
|
+
"after_commit",
|
|
390
|
+
"after_rollback",
|
|
391
|
+
"define_method",
|
|
392
|
+
"module_function"
|
|
393
|
+
]);
|
|
394
|
+
var RealRubyAnalyzer = class {
|
|
395
|
+
analyze(rootNode, content, filePath, context) {
|
|
396
|
+
const symbols = [];
|
|
397
|
+
const imports = [];
|
|
398
|
+
const exports = [];
|
|
399
|
+
const relationships = [];
|
|
400
|
+
const metadata = {
|
|
401
|
+
detectedFrameworks: [],
|
|
402
|
+
frameworkPatterns: [],
|
|
403
|
+
cyclomaticComplexity: 1,
|
|
404
|
+
linesOfCode: 0
|
|
405
|
+
};
|
|
406
|
+
if (!rootNode) return { symbols, imports, errors: [] };
|
|
407
|
+
if (rootNode.hasError) {
|
|
408
|
+
const errStack = [rootNode];
|
|
409
|
+
while (errStack.length > 0) {
|
|
410
|
+
const n = errStack.pop();
|
|
411
|
+
if (n.type === "ERROR" || n.isMissing) {
|
|
412
|
+
if (!metadata.errors) metadata.errors = [];
|
|
413
|
+
metadata.errors.push({
|
|
414
|
+
message: `Syntax ${n.type} detected.`,
|
|
415
|
+
range: this.nodeToRange(n),
|
|
416
|
+
severity: "warning"
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
420
|
+
const child = n.namedChild(i);
|
|
421
|
+
if (child) errStack.push(child);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
let loc = 1;
|
|
426
|
+
for (let i = 0; i < content.length; i++) {
|
|
427
|
+
if (content[i] === "\n") loc++;
|
|
428
|
+
}
|
|
429
|
+
metadata.linesOfCode = loc;
|
|
430
|
+
const visibilityOverrides = /* @__PURE__ */ new Map();
|
|
431
|
+
const stack = [{
|
|
432
|
+
node: rootNode,
|
|
433
|
+
owner: void 0,
|
|
434
|
+
ownerKind: void 0,
|
|
435
|
+
visibility: "public",
|
|
436
|
+
isSingleton: false,
|
|
437
|
+
inSingletonClass: false
|
|
438
|
+
}];
|
|
439
|
+
while (stack.length > 0) {
|
|
440
|
+
const frame = stack.pop();
|
|
441
|
+
const { node, owner, ownerKind, visibility, isSingleton, anonBinding, inSingletonClass } = frame;
|
|
442
|
+
const type = node.type;
|
|
443
|
+
if (type === "class") {
|
|
444
|
+
const nameNode = node.childForFieldName("name");
|
|
445
|
+
if (!nameNode) continue;
|
|
446
|
+
const rawName = nameNode.text;
|
|
447
|
+
const { name, owner: effOwner } = rubyReopenIdentity(rawName, owner);
|
|
448
|
+
const sepIdx = rawName.lastIndexOf("::");
|
|
449
|
+
const superClass = this.extractSuperclass(node);
|
|
450
|
+
const isRailsModel = this.isRailsModelName(superClass);
|
|
451
|
+
const isRailsController = this.isRailsControllerName(superClass);
|
|
452
|
+
if (isRailsModel || isRailsController) {
|
|
453
|
+
this.recordFramework(metadata, RAILS_FRAMEWORK);
|
|
454
|
+
}
|
|
455
|
+
const sym = {
|
|
456
|
+
name,
|
|
457
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
458
|
+
range: this.nodeToRange(node),
|
|
459
|
+
ownerName: effOwner,
|
|
460
|
+
metadata: {
|
|
461
|
+
superClass,
|
|
462
|
+
isRailsModel,
|
|
463
|
+
isRailsController,
|
|
464
|
+
// A class declared inside a `class << obj` body lands on that object's
|
|
465
|
+
// SINGLETON, so no consumer can reach it by name. The module branch
|
|
466
|
+
// below carries the same marker for the same reason, and the export
|
|
467
|
+
// pass reads it for both.
|
|
468
|
+
...inSingletonClass ? { declaredInSingletonClass: true } : {},
|
|
469
|
+
// Full declaration path for reopen spellings (`Rake::NameSpace`)
|
|
470
|
+
// so consumers that want the lexical path can recover it even
|
|
471
|
+
// though the identity is leaf+owner (php-family precedent).
|
|
472
|
+
...sepIdx >= 0 ? { fqn: rawName } : {}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
symbols.push(sym);
|
|
476
|
+
if (superClass && this.isStaticSuperclassName(superClass)) {
|
|
477
|
+
const leaf = this.leafName(superClass);
|
|
478
|
+
relationships.push({
|
|
479
|
+
source: name,
|
|
480
|
+
target: leaf,
|
|
481
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
482
|
+
confidence: 1,
|
|
483
|
+
// #825: the base type's own node, NOT the `superclass` node — that
|
|
484
|
+
// one starts at `<`, so a wrapped `class X <\n Base` would stamp the
|
|
485
|
+
// edge one line above the type it names (measured, not assumed).
|
|
486
|
+
line_number: this.nodeLine(this.superclassTargetNode(node)),
|
|
487
|
+
metadata: {}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const body = this.findChild(node, "body_statement");
|
|
491
|
+
if (body) {
|
|
492
|
+
const bodyFrames = [];
|
|
493
|
+
let bodyVisibility = "public";
|
|
494
|
+
const _nc2 = body.namedChildCount;
|
|
495
|
+
for (let i = 0; i < _nc2; i++) {
|
|
496
|
+
const child = body.namedChild(i);
|
|
497
|
+
if (!child) continue;
|
|
498
|
+
bodyFrames.push({
|
|
499
|
+
node: child,
|
|
500
|
+
owner: name,
|
|
501
|
+
ownerKind: "class",
|
|
502
|
+
visibility: bodyVisibility,
|
|
503
|
+
isSingleton: false,
|
|
504
|
+
// A named class declared inside an anonymous block is a REAL
|
|
505
|
+
// class with a real name, so the anonymous binding does not
|
|
506
|
+
// carry into it.
|
|
507
|
+
anonBinding: void 0,
|
|
508
|
+
// Nor does a singleton-class scope: `class << obj; class K; end;
|
|
509
|
+
// end` declares K in the file, not on obj's singleton.
|
|
510
|
+
inSingletonClass: false
|
|
511
|
+
});
|
|
512
|
+
if (this.isNoArgumentVisibilityMarker(child)) {
|
|
513
|
+
bodyVisibility = child.text;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (let i = bodyFrames.length - 1; i >= 0; i--) stack.push(bodyFrames[i]);
|
|
517
|
+
}
|
|
518
|
+
const superNode = this.findChild(node, "superclass");
|
|
519
|
+
if (superNode) {
|
|
520
|
+
const _nc3 = superNode.namedChildCount;
|
|
521
|
+
for (let i = 0; i < _nc3; i++) {
|
|
522
|
+
const sc = superNode.namedChild(i);
|
|
523
|
+
if (sc && sc.type === "call") {
|
|
524
|
+
stack.push({ node: sc, owner, ownerKind, visibility, isSingleton: false, inSingletonClass });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (type === "module") {
|
|
531
|
+
const nameNode = node.childForFieldName("name");
|
|
532
|
+
if (!nameNode) continue;
|
|
533
|
+
const rawName = nameNode.text;
|
|
534
|
+
const { name, owner: effOwner } = rubyReopenIdentity(rawName, owner);
|
|
535
|
+
const sepIdx = rawName.lastIndexOf("::");
|
|
536
|
+
const sym = {
|
|
537
|
+
name,
|
|
538
|
+
kind: ASTEngine.SymbolKind.Module,
|
|
539
|
+
range: this.nodeToRange(node),
|
|
540
|
+
ownerName: effOwner,
|
|
541
|
+
metadata: {
|
|
542
|
+
...sepIdx >= 0 ? { fqn: rawName } : {},
|
|
543
|
+
// A module declared inside a `class << obj` body lands on that object's
|
|
544
|
+
// singleton; the class branch above carries the same marker for the
|
|
545
|
+
// same reason.
|
|
546
|
+
...inSingletonClass ? { declaredInSingletonClass: true } : {}
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
symbols.push(sym);
|
|
550
|
+
const body = this.findChild(node, "body_statement");
|
|
551
|
+
if (body) {
|
|
552
|
+
const bodyFrames = [];
|
|
553
|
+
let bodyVisibility = "public";
|
|
554
|
+
const _nc4 = body.namedChildCount;
|
|
555
|
+
for (let i = 0; i < _nc4; i++) {
|
|
556
|
+
const child = body.namedChild(i);
|
|
557
|
+
if (!child) continue;
|
|
558
|
+
bodyFrames.push({
|
|
559
|
+
node: child,
|
|
560
|
+
owner: name,
|
|
561
|
+
ownerKind: "module",
|
|
562
|
+
visibility: bodyVisibility,
|
|
563
|
+
isSingleton: false,
|
|
564
|
+
anonBinding: void 0,
|
|
565
|
+
inSingletonClass: false
|
|
566
|
+
});
|
|
567
|
+
if (this.isNoArgumentVisibilityMarker(child)) {
|
|
568
|
+
bodyVisibility = child.text;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (let i = bodyFrames.length - 1; i >= 0; i--) stack.push(bodyFrames[i]);
|
|
572
|
+
}
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (type === "method") {
|
|
576
|
+
const nameNode = node.childForFieldName("name");
|
|
577
|
+
if (!nameNode) continue;
|
|
578
|
+
const name = nameNode.text;
|
|
579
|
+
const params = this.extractParameters(node);
|
|
580
|
+
const isTopLevel = !owner;
|
|
581
|
+
const sym = {
|
|
582
|
+
name,
|
|
583
|
+
kind: isTopLevel ? ASTEngine.SymbolKind.Function : ASTEngine.SymbolKind.Method,
|
|
584
|
+
range: this.nodeToRange(node),
|
|
585
|
+
ownerName: owner,
|
|
586
|
+
parameters: params,
|
|
587
|
+
visibility,
|
|
588
|
+
isStatic: isSingleton || void 0,
|
|
589
|
+
// Per-symbol cyclomatic complexity (managed-OO family contract —
|
|
590
|
+
// csharp #264 wave precedent). Mirrors the file-level counter's
|
|
591
|
+
// node set (if/unless/while/until/case/rescue/elsif), scoped to
|
|
592
|
+
// this method's subtree; base 1.
|
|
593
|
+
complexity: this.calculateNodeComplexity(node),
|
|
594
|
+
metadata: {
|
|
595
|
+
...isSingleton ? { isSingleton: true } : {},
|
|
596
|
+
// Declares that `ownerName` is the LEXICAL owner, not the class
|
|
597
|
+
// this method actually belongs to: the method is defined in the
|
|
598
|
+
// block of an anonymous dynamic class bound to `anonBinding`.
|
|
599
|
+
...anonBinding ? { anonymousScopeBinding: anonBinding } : {},
|
|
600
|
+
// ...and that the method belongs to ONE object's singleton, which is
|
|
601
|
+
// not the same statement: `ownerName` can be right while the method
|
|
602
|
+
// is still unreachable by any name from outside.
|
|
603
|
+
...inSingletonClass ? { declaredInSingletonClass: true } : {}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
symbols.push(sym);
|
|
607
|
+
const enclosingMethod = name;
|
|
608
|
+
const locals = new Set((params ?? []).map((p) => p.name));
|
|
609
|
+
for (const bound of this.extractParamDefaultBindings(node)) locals.add(bound);
|
|
610
|
+
this.walkMethodBody(
|
|
611
|
+
node,
|
|
612
|
+
this.findChild(node, "body_statement"),
|
|
613
|
+
symbols,
|
|
614
|
+
relationships,
|
|
615
|
+
imports,
|
|
616
|
+
metadata,
|
|
617
|
+
owner,
|
|
618
|
+
ownerKind,
|
|
619
|
+
enclosingMethod,
|
|
620
|
+
visibilityOverrides,
|
|
621
|
+
new Set(locals)
|
|
622
|
+
);
|
|
623
|
+
this.walkParamDefaults(
|
|
624
|
+
node,
|
|
625
|
+
symbols,
|
|
626
|
+
relationships,
|
|
627
|
+
imports,
|
|
628
|
+
metadata,
|
|
629
|
+
owner,
|
|
630
|
+
ownerKind,
|
|
631
|
+
enclosingMethod,
|
|
632
|
+
visibilityOverrides,
|
|
633
|
+
new Set(locals)
|
|
634
|
+
);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (type === "singleton_class") {
|
|
638
|
+
const body = this.findChild(node, "body_statement");
|
|
639
|
+
if (body) {
|
|
640
|
+
const bodyFrames = [];
|
|
641
|
+
let bodyVisibility = "public";
|
|
642
|
+
const _nc5 = body.namedChildCount;
|
|
643
|
+
for (let i = 0; i < _nc5; i++) {
|
|
644
|
+
const child = body.namedChild(i);
|
|
645
|
+
if (!child) continue;
|
|
646
|
+
bodyFrames.push({
|
|
647
|
+
node: child,
|
|
648
|
+
owner,
|
|
649
|
+
ownerKind: "class",
|
|
650
|
+
visibility: bodyVisibility,
|
|
651
|
+
isSingleton: true,
|
|
652
|
+
anonBinding: void 0,
|
|
653
|
+
inSingletonClass: true
|
|
654
|
+
});
|
|
655
|
+
if (this.isNoArgumentVisibilityMarker(child)) bodyVisibility = child.text;
|
|
656
|
+
}
|
|
657
|
+
for (let i = bodyFrames.length - 1; i >= 0; i--) stack.push(bodyFrames[i]);
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (type === "singleton_method") {
|
|
662
|
+
const nameNode = node.childForFieldName("name");
|
|
663
|
+
if (!nameNode) continue;
|
|
664
|
+
const name = nameNode.text;
|
|
665
|
+
const params = this.extractParameters(node);
|
|
666
|
+
const isTopLevel = !owner;
|
|
667
|
+
const sym = {
|
|
668
|
+
name,
|
|
669
|
+
kind: isTopLevel ? ASTEngine.SymbolKind.Function : ASTEngine.SymbolKind.Method,
|
|
670
|
+
range: this.nodeToRange(node),
|
|
671
|
+
ownerName: owner,
|
|
672
|
+
parameters: params,
|
|
673
|
+
visibility,
|
|
674
|
+
isStatic: true,
|
|
675
|
+
// Per-symbol cyclomatic complexity (managed-OO family contract —
|
|
676
|
+
// csharp #264 wave precedent). Same node set as the file-level
|
|
677
|
+
// counter, scoped to this method's subtree; base 1.
|
|
678
|
+
complexity: this.calculateNodeComplexity(node),
|
|
679
|
+
metadata: {
|
|
680
|
+
isSingleton: true,
|
|
681
|
+
...anonBinding ? { anonymousScopeBinding: anonBinding } : {},
|
|
682
|
+
...inSingletonClass ? { declaredInSingletonClass: true } : {}
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
symbols.push(sym);
|
|
686
|
+
const locals = new Set((params ?? []).map((p) => p.name));
|
|
687
|
+
for (const bound of this.extractParamDefaultBindings(node)) locals.add(bound);
|
|
688
|
+
this.walkMethodBody(
|
|
689
|
+
node,
|
|
690
|
+
this.findChild(node, "body_statement"),
|
|
691
|
+
symbols,
|
|
692
|
+
relationships,
|
|
693
|
+
imports,
|
|
694
|
+
metadata,
|
|
695
|
+
owner,
|
|
696
|
+
ownerKind,
|
|
697
|
+
name,
|
|
698
|
+
visibilityOverrides,
|
|
699
|
+
new Set(locals)
|
|
700
|
+
);
|
|
701
|
+
const _nc6 = node.namedChildCount;
|
|
702
|
+
for (let i = 0; i < _nc6; i++) {
|
|
703
|
+
const c = node.namedChild(i);
|
|
704
|
+
if (!c || c.type !== "instance_variable") continue;
|
|
705
|
+
const nm = node.childForFieldName("name");
|
|
706
|
+
if (nm && c.id === nm.id) continue;
|
|
707
|
+
const p0 = node.namedChild(0);
|
|
708
|
+
if (p0 && c.id === p0.id && node.childForFieldName("name") == null) continue;
|
|
709
|
+
relationships.push({
|
|
710
|
+
source: name,
|
|
711
|
+
target: c.text,
|
|
712
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
713
|
+
confidence: 0.9,
|
|
714
|
+
// #825: the member-read node names the edge.
|
|
715
|
+
line_number: this.nodeLine(c),
|
|
716
|
+
metadata: { memberRead: true }
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
this.walkParamDefaults(
|
|
720
|
+
node,
|
|
721
|
+
symbols,
|
|
722
|
+
relationships,
|
|
723
|
+
imports,
|
|
724
|
+
metadata,
|
|
725
|
+
owner,
|
|
726
|
+
ownerKind,
|
|
727
|
+
name,
|
|
728
|
+
visibilityOverrides,
|
|
729
|
+
new Set(locals)
|
|
730
|
+
);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (type === "alias") {
|
|
734
|
+
const names = [];
|
|
735
|
+
const nameNodes = [];
|
|
736
|
+
let oldName;
|
|
737
|
+
for (let i = 0; i < node.namedChildCount && names.length < 2; i++) {
|
|
738
|
+
const c = node.namedChild(i);
|
|
739
|
+
if (!c) continue;
|
|
740
|
+
if (c.type === "identifier") {
|
|
741
|
+
names.push(c.text);
|
|
742
|
+
nameNodes.push(c);
|
|
743
|
+
} else if (c.type === "simple_symbol") {
|
|
744
|
+
const n = this.rubyNameFromLiteral(c);
|
|
745
|
+
if (n !== void 0 && n.length > 0) {
|
|
746
|
+
names.push(n);
|
|
747
|
+
nameNodes.push(c);
|
|
748
|
+
}
|
|
749
|
+
} else if (names.length === 1 && oldName === void 0) {
|
|
750
|
+
oldName = c.text;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (names.length >= 1) {
|
|
754
|
+
relationships.push({
|
|
755
|
+
source: owner || FILE_SCOPE_OWNER,
|
|
756
|
+
target: names[0],
|
|
757
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
758
|
+
confidence: 0.9,
|
|
759
|
+
line_number: this.nodeLine(nameNodes[0]),
|
|
760
|
+
metadata: { aliasedTo: names[1] ?? oldName ?? "", keywordAlias: true }
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (type === "identifier" && isRubyVisibilityDirectiveName(node.text)) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (type === "call") {
|
|
769
|
+
this.processCall(
|
|
770
|
+
node,
|
|
771
|
+
symbols,
|
|
772
|
+
relationships,
|
|
773
|
+
imports,
|
|
774
|
+
metadata,
|
|
775
|
+
owner,
|
|
776
|
+
void 0,
|
|
777
|
+
visibilityOverrides,
|
|
778
|
+
anonBinding
|
|
779
|
+
);
|
|
780
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
781
|
+
const child = node.namedChild(i);
|
|
782
|
+
if (child) stack.push({ node: child, owner, ownerKind, visibility, isSingleton: false, anonBinding, inSingletonClass });
|
|
783
|
+
}
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (type === "program") {
|
|
787
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
788
|
+
const child = node.namedChild(i);
|
|
789
|
+
if (child) stack.push({ node: child, owner, ownerKind, visibility, isSingleton: false, inSingletonClass });
|
|
790
|
+
}
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (type === "assignment" && (owner || this.isDynamicClassFactoryRhs(node))) {
|
|
794
|
+
const left = node.childForFieldName("left");
|
|
795
|
+
let factoryOwner;
|
|
796
|
+
let anonBindingForFrames = anonBinding;
|
|
797
|
+
if (left) {
|
|
798
|
+
const leftType = left.type;
|
|
799
|
+
if (leftType === "constant") {
|
|
800
|
+
const right = node.childForFieldName("right");
|
|
801
|
+
const factoryKind = right ? this.dynamicClassFactoryKind(right) : null;
|
|
802
|
+
if (factoryKind) {
|
|
803
|
+
symbols.push({
|
|
804
|
+
name: left.text,
|
|
805
|
+
kind: factoryKind === "Module" ? ASTEngine.SymbolKind.Module : ASTEngine.SymbolKind.Class,
|
|
806
|
+
range: this.nodeToRange(node),
|
|
807
|
+
ownerName: owner,
|
|
808
|
+
visibility,
|
|
809
|
+
metadata: { definedBy: factoryKind === "Module" ? "Module.new" : "classFactory" }
|
|
810
|
+
});
|
|
811
|
+
factoryOwner = left.text;
|
|
812
|
+
const factoryBaseNode = this.factorySuperclassNode(right);
|
|
813
|
+
const factoryBase = factoryBaseNode ? factoryBaseNode.text.trim() : void 0;
|
|
814
|
+
if (factoryBase && this.isStaticSuperclassName(factoryBase)) {
|
|
815
|
+
relationships.push({
|
|
816
|
+
source: left.text,
|
|
817
|
+
target: this.leafName(factoryBase),
|
|
818
|
+
type: ASTEngine.RelationshipType.Extends,
|
|
819
|
+
confidence: 1,
|
|
820
|
+
// #825: the base type's own node names the edge, which is the
|
|
821
|
+
// first argument of the factory call.
|
|
822
|
+
line_number: this.nodeLine(factoryBaseNode),
|
|
823
|
+
metadata: {
|
|
824
|
+
// Both ends are WRITTEN — the constant on the left and the
|
|
825
|
+
// constant in the call — so this is structural evidence with
|
|
826
|
+
// no inferred name, unlike `instantiates`'
|
|
827
|
+
// `resolvedTarget: "enclosing-class"`. What the edge ASSERTS is
|
|
828
|
+
// `Class.new`'s documented meaning, which is Ruby rather than
|
|
829
|
+
// this file, and there is no `superclass` clause on the line
|
|
830
|
+
// for a reader to check; the mechanism is recorded so that is
|
|
831
|
+
// visible rather than implied.
|
|
832
|
+
definedBy: "Class.new"
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
} else if (owner) {
|
|
837
|
+
symbols.push({
|
|
838
|
+
name: left.text,
|
|
839
|
+
kind: ASTEngine.SymbolKind.Constant,
|
|
840
|
+
range: this.nodeToRange(node),
|
|
841
|
+
ownerName: owner,
|
|
842
|
+
visibility,
|
|
843
|
+
metadata: { variableType: "constant" }
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
} else if (leftType === "identifier") {
|
|
847
|
+
const right = node.childForFieldName("right");
|
|
848
|
+
if (right && this.dynamicClassFactoryKind(right)) {
|
|
849
|
+
anonBindingForFrames = left.text;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
854
|
+
const child = node.namedChild(i);
|
|
855
|
+
if (child) stack.push({ node: child, owner: factoryOwner ?? owner, ownerKind, visibility, isSingleton: false, anonBinding: anonBindingForFrames, inSingletonClass });
|
|
856
|
+
}
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (type === "instance_variable") {
|
|
860
|
+
if (!isRubyInstanceVariableWrite(node)) {
|
|
861
|
+
relationships.push({
|
|
862
|
+
source: owner || FILE_SCOPE_OWNER,
|
|
863
|
+
target: node.text,
|
|
864
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
865
|
+
confidence: 0.9,
|
|
866
|
+
// #825: the read node names the edge (top-level/DSL member read).
|
|
867
|
+
line_number: this.nodeLine(node),
|
|
868
|
+
metadata: { memberRead: true }
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
{
|
|
873
|
+
const childFrames = [];
|
|
874
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
875
|
+
const child = node.namedChild(i);
|
|
876
|
+
if (child) childFrames.push({ node: child, owner, ownerKind, visibility, isSingleton: false, anonBinding, inSingletonClass });
|
|
877
|
+
}
|
|
878
|
+
for (const cf of childFrames) stack.push(cf);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
for (const symbol of symbols) {
|
|
882
|
+
if (!symbol.ownerName) continue;
|
|
883
|
+
const override = visibilityOverrides.get(`${symbol.ownerName}\0${symbol.name}`);
|
|
884
|
+
if (override) symbol.visibility = override;
|
|
885
|
+
}
|
|
886
|
+
const privateConstants = content.indexOf("private_constant") >= 0 ? this.collectPrivateConstants(rootNode) : /* @__PURE__ */ new Map();
|
|
887
|
+
const fileUnit = rubyFileUnitName(filePath);
|
|
888
|
+
const exportedNames = /* @__PURE__ */ new Set();
|
|
889
|
+
let exportDuplicate = 0;
|
|
890
|
+
let exportPrivateConstant = 0;
|
|
891
|
+
let exportUnnameable = 0;
|
|
892
|
+
let exportMainSingleton = 0;
|
|
893
|
+
let exportSingletonMember = 0;
|
|
894
|
+
for (const s of symbols) {
|
|
895
|
+
if (s.ownerName) continue;
|
|
896
|
+
if (s.kind !== ASTEngine.SymbolKind.Class && s.kind !== ASTEngine.SymbolKind.Module && s.kind !== ASTEngine.SymbolKind.Constant && s.kind !== ASTEngine.SymbolKind.Function) continue;
|
|
897
|
+
const name = String(s.name ?? "");
|
|
898
|
+
if (name.length === 0 || name.trim().length === 0 || name.indexOf("#{") >= 0) {
|
|
899
|
+
exportUnnameable++;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (s.isStatic) {
|
|
903
|
+
exportMainSingleton++;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (s.metadata?.declaredInSingletonClass === true) {
|
|
907
|
+
exportSingletonMember++;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const hidden = privateConstants.get(FILE_SCOPE_OWNER);
|
|
911
|
+
if (hidden?.has(name)) {
|
|
912
|
+
exportPrivateConstant++;
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
if (exportedNames.has(name)) {
|
|
916
|
+
exportDuplicate++;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
exportedNames.add(name);
|
|
920
|
+
exports.push({
|
|
921
|
+
name,
|
|
922
|
+
type: "export_surface",
|
|
923
|
+
kind: s.kind,
|
|
924
|
+
range: s.range,
|
|
925
|
+
metadata: { unit: fileUnit, unitKind: "file", definedBy: s.metadata?.definedBy }
|
|
926
|
+
});
|
|
927
|
+
relationships.push({
|
|
928
|
+
source: fileUnit,
|
|
929
|
+
target: name,
|
|
930
|
+
type: ASTEngine.RelationshipType.Exports,
|
|
931
|
+
confidence: 1,
|
|
932
|
+
line_number: s.range?.startLine,
|
|
933
|
+
metadata: {
|
|
934
|
+
// The unit name comes from the PATH, not from a declaration in the
|
|
935
|
+
// file. Declared, so a consumer can tell an inferred unit from a
|
|
936
|
+
// written one — the same convention `instantiates` uses.
|
|
937
|
+
resolvedTarget: "file-unit",
|
|
938
|
+
exportKind: s.kind
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
metadata.exportsExcluded = {
|
|
943
|
+
duplicateReopen: exportDuplicate,
|
|
944
|
+
privateConstant: exportPrivateConstant,
|
|
945
|
+
unnameable: exportUnnameable,
|
|
946
|
+
mainSingleton: exportMainSingleton,
|
|
947
|
+
singletonMember: exportSingletonMember,
|
|
948
|
+
// Owned symbols are refused by RULE (the exports-family ruling), not by a
|
|
949
|
+
// filter that could drift, so this is a constant rather than a count. It is
|
|
950
|
+
// stated here so a reader does not have to infer it from the absence of a
|
|
951
|
+
// number.
|
|
952
|
+
memberByContains: "owned symbols are containment, not importable units"
|
|
953
|
+
};
|
|
954
|
+
const baseByClass = /* @__PURE__ */ new Map();
|
|
955
|
+
for (const s of symbols) {
|
|
956
|
+
if (s.kind === ASTEngine.SymbolKind.Class && s.metadata?.superClass) {
|
|
957
|
+
const superText = String(s.metadata.superClass);
|
|
958
|
+
if (this.isStaticSuperclassName(superText)) {
|
|
959
|
+
baseByClass.set(s.name, this.leafName(superText));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const methodsByOwner = /* @__PURE__ */ new Map();
|
|
964
|
+
for (const s of symbols) {
|
|
965
|
+
if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName) continue;
|
|
966
|
+
let names = methodsByOwner.get(s.ownerName);
|
|
967
|
+
if (!names) {
|
|
968
|
+
names = /* @__PURE__ */ new Set();
|
|
969
|
+
methodsByOwner.set(s.ownerName, names);
|
|
970
|
+
}
|
|
971
|
+
names.add(s.name);
|
|
972
|
+
}
|
|
973
|
+
for (const s of symbols) {
|
|
974
|
+
if (s.kind !== ASTEngine.SymbolKind.Method || !s.ownerName) continue;
|
|
975
|
+
const base = baseByClass.get(s.ownerName);
|
|
976
|
+
if (!base) continue;
|
|
977
|
+
const inFileMethods = base === s.ownerName ? void 0 : methodsByOwner.get(base);
|
|
978
|
+
const baseMethods = inFileMethods ?? context?.overrideCompanion?.get(base);
|
|
979
|
+
if (baseMethods?.has(s.name)) {
|
|
980
|
+
relationships.push({
|
|
981
|
+
source: `${s.ownerName}.${s.name}`,
|
|
982
|
+
target: `${base}.${s.name}`,
|
|
983
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
984
|
+
confidence: 0.95,
|
|
985
|
+
// #825: a symbol-named edge carries the SYMBOL's own declaration line,
|
|
986
|
+
// so the edge moves with its symbol.
|
|
987
|
+
line_number: s.range?.startLine,
|
|
988
|
+
metadata: { isOverride: true }
|
|
989
|
+
});
|
|
990
|
+
} else if (!inFileMethods && !context?.overrideCompanion?.get(base)) {
|
|
991
|
+
relationships.push({
|
|
992
|
+
source: `${s.ownerName}.${s.name}`,
|
|
993
|
+
target: `${base}.${s.name}`,
|
|
994
|
+
type: ASTEngine.RelationshipType.Overrides,
|
|
995
|
+
confidence: 0.5,
|
|
996
|
+
line_number: s.range?.startLine,
|
|
997
|
+
metadata: {
|
|
998
|
+
isOverride: true,
|
|
999
|
+
reasonCode: "first-parent-guess",
|
|
1000
|
+
// THE REASON, ROUTED WHERE THE ARBITER READS IT.
|
|
1001
|
+
//
|
|
1002
|
+
// The lane has stated why since M37.10 — `reasonCode: "first-parent-guess"` — and the
|
|
1003
|
+
// incoming gate never saw it: its declared-cause channel reads `baseBeyondRunScope`
|
|
1004
|
+
// (`script/m14/incoming-rule.ts:274`), so `causeOK` is 0 fleet-wide and the residual
|
|
1005
|
+
// reads as an unstated hole. The claim is the TRUE half of what this branch already
|
|
1006
|
+
// knows: the base is not declared in this file AND the companion index gave no opinion
|
|
1007
|
+
// (it is never populated in production, M37.10), so the run could not look. It is a
|
|
1008
|
+
// claim about the RUN, not about the project, which is why the arbiter reads it only
|
|
1009
|
+
// where the target names nothing and why it can never raise `resolved`.
|
|
1010
|
+
//
|
|
1011
|
+
// Three conditions, each of them the arbiter's own (`classifyEdge`):
|
|
1012
|
+
// · the value must be IDENTIFIER-SHAPED and must BE the target's owner, or the
|
|
1013
|
+
// claim is REFUTED (`causeFAIL`, counted against the lane, clause 4);
|
|
1014
|
+
// · `base === s.ownerName` is the leaf-collapse self-collision guard's case
|
|
1015
|
+
// (`class Rake::TestCase < Test::Unit::TestCase`): the run DID reach that
|
|
1016
|
+
// spelling — it is the class's own leaf — so "beyond this run's reach" would be
|
|
1017
|
+
// the wrong statement and that edge stays silent;
|
|
1018
|
+
// · it is MUTUALLY EXCLUSIVE with every external marker, by the arbiter's rule.
|
|
1019
|
+
...base !== s.ownerName && isRubyIdentifierShaped(base) ? { baseBeyondRunScope: base } : {}
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
for (const s of symbols) {
|
|
1025
|
+
const _owner = s.ownerName;
|
|
1026
|
+
if (_owner) {
|
|
1027
|
+
relationships.push({
|
|
1028
|
+
source: _owner,
|
|
1029
|
+
target: s.name,
|
|
1030
|
+
type: ASTEngine.RelationshipType.Contains,
|
|
1031
|
+
confidence: 1,
|
|
1032
|
+
line_number: s.range?.startLine,
|
|
1033
|
+
metadata: {}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const containsPairs = /* @__PURE__ */ new Set();
|
|
1038
|
+
for (const r of relationships) {
|
|
1039
|
+
if (r.type === ASTEngine.RelationshipType.Contains) containsPairs.add(`${r.source}\0${r.target}`);
|
|
1040
|
+
}
|
|
1041
|
+
let exportDuplicateOfContains = 0;
|
|
1042
|
+
for (const r of relationships) {
|
|
1043
|
+
if (r.type === ASTEngine.RelationshipType.Exports && containsPairs.has(`${r.source}\0${r.target}`)) {
|
|
1044
|
+
exportDuplicateOfContains++;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
metadata.exportsOverlappingContains = exportDuplicateOfContains;
|
|
1048
|
+
const indexFacts = this.applyProjectIndexFacts(relationships, context);
|
|
1049
|
+
if (indexFacts !== void 0) metadata.projectIndex = indexFacts;
|
|
1050
|
+
this.classifyEvidenceTier(relationships);
|
|
1051
|
+
return {
|
|
1052
|
+
symbols,
|
|
1053
|
+
imports,
|
|
1054
|
+
relationships,
|
|
1055
|
+
exports,
|
|
1056
|
+
metadata,
|
|
1057
|
+
errors: metadata.errors || []
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Walk a method's body: the body_statement when present, else (endless
|
|
1062
|
+
* `def f = expr`) every direct child except the name and parameter nodes.
|
|
1063
|
+
* Endless bodies are real executable code in the method's scope with its
|
|
1064
|
+
* params bound — skipping them dropped all calls/reads inside.
|
|
1065
|
+
*/
|
|
1066
|
+
walkMethodBody(node, body, symbols, relationships, imports, metadata, owner, ownerKind, enclosingMethod, visibilityOverrides, locals) {
|
|
1067
|
+
if (body) {
|
|
1068
|
+
this.walkBody(
|
|
1069
|
+
body,
|
|
1070
|
+
symbols,
|
|
1071
|
+
relationships,
|
|
1072
|
+
imports,
|
|
1073
|
+
metadata,
|
|
1074
|
+
owner,
|
|
1075
|
+
ownerKind,
|
|
1076
|
+
enclosingMethod,
|
|
1077
|
+
visibilityOverrides,
|
|
1078
|
+
locals
|
|
1079
|
+
);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const _nc7 = node.namedChildCount;
|
|
1083
|
+
for (let i = 0; i < _nc7; i++) {
|
|
1084
|
+
const c = node.namedChild(i);
|
|
1085
|
+
if (!c) continue;
|
|
1086
|
+
if (c.type === "method_parameters" || c.type === "block_parameters") continue;
|
|
1087
|
+
const fld = node.childForFieldName?.("name");
|
|
1088
|
+
if (fld && c.id === fld.id) continue;
|
|
1089
|
+
if (c.type === "identifier" && i === 0) {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
this.walkBody(
|
|
1093
|
+
c,
|
|
1094
|
+
symbols,
|
|
1095
|
+
relationships,
|
|
1096
|
+
imports,
|
|
1097
|
+
metadata,
|
|
1098
|
+
owner,
|
|
1099
|
+
ownerKind,
|
|
1100
|
+
enclosingMethod,
|
|
1101
|
+
visibilityOverrides,
|
|
1102
|
+
new Set(locals)
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
// ========================================================================
|
|
1107
|
+
// BODY WALKER — processes statements inside a method/function body
|
|
1108
|
+
// Handles: calls, require/require_relative, include/extend/prepend,
|
|
1109
|
+
// attr_*, define_method, alias_method, etc.
|
|
1110
|
+
// ========================================================================
|
|
1111
|
+
walkBody(node, symbols, relationships, imports, metadata, owner, ownerKind, enclosingMethod, visibilityOverrides, locals = /* @__PURE__ */ new Set()) {
|
|
1112
|
+
const localNames = new Set(locals);
|
|
1113
|
+
const scanStack = [node];
|
|
1114
|
+
while (scanStack.length > 0) {
|
|
1115
|
+
const sn = scanStack.pop();
|
|
1116
|
+
if (sn.type === "unary") {
|
|
1117
|
+
const op = sn.childCount > 0 ? sn.child(0) : null;
|
|
1118
|
+
if (op && !op.isNamed && op.type === "defined?") continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (sn.type === "assignment" || sn.type === "operator_assignment") {
|
|
1121
|
+
const left = sn.childForFieldName("left") ?? sn.namedChild(0);
|
|
1122
|
+
if (left) {
|
|
1123
|
+
if (left.type === "identifier") {
|
|
1124
|
+
localNames.add(left.text);
|
|
1125
|
+
}
|
|
1126
|
+
const _nc8 = left.namedChildCount;
|
|
1127
|
+
for (let i = 0; i < _nc8; i++) {
|
|
1128
|
+
const c = left.namedChild(i);
|
|
1129
|
+
if (c?.type === "identifier") localNames.add(c.text);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (sn.type === "method_parameters") {
|
|
1134
|
+
const inner = [sn];
|
|
1135
|
+
while (inner.length > 0) {
|
|
1136
|
+
const q = inner.pop();
|
|
1137
|
+
if (q.type === "identifier") localNames.add(q.text);
|
|
1138
|
+
for (let j = 0; j < q.namedChildCount; j++) {
|
|
1139
|
+
const d = q.namedChild(j);
|
|
1140
|
+
if (d) inner.push(d);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (sn.type === "block_parameters" || sn.type === "lambda_parameters") {
|
|
1145
|
+
const _nc9 = sn.namedChildCount;
|
|
1146
|
+
for (let i = 0; i < _nc9; i++) {
|
|
1147
|
+
const c = sn.namedChild(i);
|
|
1148
|
+
if (!c) continue;
|
|
1149
|
+
if (c.type === "identifier") {
|
|
1150
|
+
localNames.add(c.text);
|
|
1151
|
+
} else if (c.type === "destructured_parameter") {
|
|
1152
|
+
const inner = [c];
|
|
1153
|
+
while (inner.length > 0) {
|
|
1154
|
+
const q = inner.pop();
|
|
1155
|
+
if (q.type === "identifier") localNames.add(q.text);
|
|
1156
|
+
for (let j = 0; j < q.namedChildCount; j++) {
|
|
1157
|
+
const d = q.namedChild(j);
|
|
1158
|
+
if (d) inner.push(d);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
} else {
|
|
1162
|
+
const inner = [c];
|
|
1163
|
+
while (inner.length > 0) {
|
|
1164
|
+
const q = inner.pop();
|
|
1165
|
+
if (q.type === "identifier") localNames.add(q.text);
|
|
1166
|
+
for (let j = 0; j < q.namedChildCount; j++) {
|
|
1167
|
+
const d = q.namedChild(j);
|
|
1168
|
+
if (d) inner.push(d);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (sn.type === "exception_variable") {
|
|
1175
|
+
const _nc10 = sn.namedChildCount;
|
|
1176
|
+
for (let i = 0; i < _nc10; i++) {
|
|
1177
|
+
const c = sn.namedChild(i);
|
|
1178
|
+
if (c?.type === "identifier") localNames.add(c.text);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (sn.type === "array_pattern" || sn.type === "find_pattern" || sn.type === "as_pattern") {
|
|
1182
|
+
const _nc11 = sn.namedChildCount;
|
|
1183
|
+
for (let i = 0; i < _nc11; i++) {
|
|
1184
|
+
const c = sn.namedChild(i);
|
|
1185
|
+
if (!c) continue;
|
|
1186
|
+
if (c.type === "identifier") {
|
|
1187
|
+
localNames.add(c.text);
|
|
1188
|
+
} else if (c.type === "splat_parameter") {
|
|
1189
|
+
const nm = c.childForFieldName("name") ?? c.namedChild(0);
|
|
1190
|
+
if (nm?.type === "identifier") localNames.add(nm.text);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
} else if (sn.type === "keyword_pattern") {
|
|
1194
|
+
const _nc12 = sn.namedChildCount;
|
|
1195
|
+
for (let i = 0; i < _nc12; i++) {
|
|
1196
|
+
const c = sn.namedChild(i);
|
|
1197
|
+
if (!c) continue;
|
|
1198
|
+
if (c.type === "identifier") localNames.add(c.text);
|
|
1199
|
+
else if (c.type === "hash_key_symbol") localNames.add(this.stripSymbolColon(c.text));
|
|
1200
|
+
}
|
|
1201
|
+
} else if (sn.type === "in_clause") {
|
|
1202
|
+
const first = sn.namedChild(0);
|
|
1203
|
+
if (first?.type === "identifier") localNames.add(first.text);
|
|
1204
|
+
}
|
|
1205
|
+
for (let i = sn.namedChildCount - 1; i >= 0; i--) {
|
|
1206
|
+
const child = sn.namedChild(i);
|
|
1207
|
+
if (child) scanStack.push(child);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
const anonRanges = [];
|
|
1211
|
+
{
|
|
1212
|
+
const scan = [node];
|
|
1213
|
+
while (scan.length > 0) {
|
|
1214
|
+
const c = scan.pop();
|
|
1215
|
+
if (c.type === "assignment") {
|
|
1216
|
+
const left = c.childForFieldName("left");
|
|
1217
|
+
const right = c.childForFieldName("right");
|
|
1218
|
+
if (left && left.type === "identifier" && right && this.dynamicClassFactoryKind(right)) {
|
|
1219
|
+
anonRanges.push({ lo: right.startIndex, hi: right.endIndex, binding: left.text });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
1223
|
+
const d = c.namedChild(j);
|
|
1224
|
+
if (d) scan.push(d);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
anonRanges.sort((a, b) => a.hi - a.lo - (b.hi - b.lo));
|
|
1229
|
+
const anonBindingAt = (startIndex) => {
|
|
1230
|
+
for (const r of anonRanges) {
|
|
1231
|
+
if (startIndex >= r.lo && startIndex <= r.hi) return r.binding;
|
|
1232
|
+
}
|
|
1233
|
+
return void 0;
|
|
1234
|
+
};
|
|
1235
|
+
const stack = [node];
|
|
1236
|
+
while (stack.length > 0) {
|
|
1237
|
+
const n = stack.pop();
|
|
1238
|
+
const type = n.type;
|
|
1239
|
+
if (type === "unary") {
|
|
1240
|
+
const op = n.childCount > 0 ? n.child(0) : null;
|
|
1241
|
+
if (op && !op.isNamed && op.type === "defined?") continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (type === "call") {
|
|
1244
|
+
this.processCall(
|
|
1245
|
+
n,
|
|
1246
|
+
symbols,
|
|
1247
|
+
relationships,
|
|
1248
|
+
imports,
|
|
1249
|
+
metadata,
|
|
1250
|
+
owner,
|
|
1251
|
+
enclosingMethod,
|
|
1252
|
+
visibilityOverrides,
|
|
1253
|
+
anonBindingAt(n.startIndex)
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
if (type === "identifier") {
|
|
1257
|
+
const parentType = n.parent?.type;
|
|
1258
|
+
const nameField = parentType === "method" || parentType === "singleton_method" ? n.parent?.childForFieldName?.("name") ?? null : null;
|
|
1259
|
+
const isDefName = nameField ? nameField.id === n.id : (parentType === "method" || parentType === "singleton_method") && n.parent.namedChild(0)?.id === n.id;
|
|
1260
|
+
if (isDefName) continue;
|
|
1261
|
+
if (!BARE_CALL_SKIP_PARENTS.has(parentType) && !localNames.has(n.text)) {
|
|
1262
|
+
this.pushBareCall(n.text, n, owner, enclosingMethod, relationships);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (type === "instance_variable") {
|
|
1266
|
+
if (!isRubyInstanceVariableWrite(n)) {
|
|
1267
|
+
const src = enclosingMethod || owner || FILE_SCOPE_OWNER;
|
|
1268
|
+
relationships.push({
|
|
1269
|
+
source: src,
|
|
1270
|
+
target: n.text,
|
|
1271
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1272
|
+
confidence: 0.9,
|
|
1273
|
+
line_number: this.nodeLine(n),
|
|
1274
|
+
metadata: { memberRead: true }
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (type === "if" || type === "unless" || type === "while" || type === "until" || type === "case" || type === "rescue" || type === "elsif" || type === "if_modifier" || type === "unless_modifier" || type === "while_modifier" || type === "until_modifier" || type === "rescue_modifier") {
|
|
1279
|
+
if (metadata.cyclomaticComplexity !== void 0) {
|
|
1280
|
+
metadata.cyclomaticComplexity++;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (type === "class_variable" || type === "global_variable") {
|
|
1284
|
+
metadata.classAndGlobalVarsSkipped = (metadata.classAndGlobalVarsSkipped ?? 0) + 1;
|
|
1285
|
+
}
|
|
1286
|
+
if (type === "undef") continue;
|
|
1287
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
1288
|
+
const child = n.namedChild(i);
|
|
1289
|
+
if (child) stack.push(child);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Names a method's PARAMETER DEFAULTS assign, which are locals of the method
|
|
1295
|
+
* scope because Ruby evaluates a default expression in that scope.
|
|
1296
|
+
*
|
|
1297
|
+
* `def all(all_queries: (seen = true))` binds `seen`; a later
|
|
1298
|
+
* `return nil if seen` reads a local, not a method. walkBody's pre-scan only
|
|
1299
|
+
* ever saw the BODY subtree, so these names were unbound and every read of
|
|
1300
|
+
* one became a fabricated bare call — measured on rails in
|
|
1301
|
+
* activerecord/lib/active_record/scoping/named.rb:23, where the analyzer
|
|
1302
|
+
* published `calls all -> all_queries_not_set` for a name the signature on
|
|
1303
|
+
* line 22 itself binds.
|
|
1304
|
+
*
|
|
1305
|
+
* Only ASSIGNMENT targets are collected, and only from the VALUE position of
|
|
1306
|
+
* an `optional_parameter` / `keyword_parameter` — a bare call in a default
|
|
1307
|
+
* (`def f(lock = Mutex.new)`) is a real call and must stay one, so the value
|
|
1308
|
+
* is scanned for what it BINDS and the call walk is left to walkParamDefaults.
|
|
1309
|
+
* The left-hand side forms mirror the body pre-scan's: a plain identifier, and
|
|
1310
|
+
* the identifiers under a `left_assignment_list` (tree-sitter-ruby has no
|
|
1311
|
+
* `multiple_assignment` node, so `a, b = 1, 2` is one assignment whose left
|
|
1312
|
+
* is a list).
|
|
1313
|
+
*/
|
|
1314
|
+
extractParamDefaultBindings(node) {
|
|
1315
|
+
const names = /* @__PURE__ */ new Set();
|
|
1316
|
+
const paramsNode = this.findChild(node, "method_parameters") || this.findChild(node, "parameters");
|
|
1317
|
+
if (!paramsNode) return names;
|
|
1318
|
+
const _nc13 = paramsNode.namedChildCount;
|
|
1319
|
+
for (let i = 0; i < _nc13; i++) {
|
|
1320
|
+
const pc = paramsNode.namedChild(i);
|
|
1321
|
+
if (!pc) continue;
|
|
1322
|
+
if (pc.type !== "optional_parameter" && pc.type !== "keyword_parameter") continue;
|
|
1323
|
+
if (pc.namedChildCount < 2) continue;
|
|
1324
|
+
const stack = [];
|
|
1325
|
+
for (let j = 1; j < pc.namedChildCount; j++) {
|
|
1326
|
+
const c = pc.namedChild(j);
|
|
1327
|
+
if (c) stack.push(c);
|
|
1328
|
+
}
|
|
1329
|
+
while (stack.length > 0) {
|
|
1330
|
+
const n = stack.pop();
|
|
1331
|
+
if (n.type === "assignment" || n.type === "operator_assignment") {
|
|
1332
|
+
const left = n.childForFieldName("left") ?? n.namedChild(0);
|
|
1333
|
+
if (left) {
|
|
1334
|
+
if (left.type === "identifier") {
|
|
1335
|
+
names.add(left.text);
|
|
1336
|
+
} else {
|
|
1337
|
+
for (let j = 0; j < left.namedChildCount; j++) {
|
|
1338
|
+
const s = left.namedChild(j);
|
|
1339
|
+
if (s && s.type === "identifier") names.add(s.text);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (n.type === "do_block" || n.type === "block") continue;
|
|
1345
|
+
for (let j = 0; j < n.namedChildCount; j++) {
|
|
1346
|
+
const c = n.namedChild(j);
|
|
1347
|
+
if (c) stack.push(c);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return names;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Walk the default-argument VALUE expressions of a method's parameters
|
|
1355
|
+
* (`optional_parameter` `x = v`, `keyword_parameter` `k: v`) in the method's
|
|
1356
|
+
* own scope. Ruby evaluates these at call time, so their calls, member
|
|
1357
|
+
* reads and instantiations belong to the method — only the body was walked
|
|
1358
|
+
* before, silently dropping `def f(app, lock = Lock.new)`-style call sites
|
|
1359
|
+
* (RB-B, rack wave: mutex = Mutex.new, params = Rack::Utils.default_query_parser,
|
|
1360
|
+
* app = @app). Mirrors the oracle truth, which walks parameter values.
|
|
1361
|
+
*/
|
|
1362
|
+
walkParamDefaults(node, symbols, relationships, imports, metadata, owner, ownerKind, enclosingMethod, visibilityOverrides, locals) {
|
|
1363
|
+
const paramsNode = this.findChild(node, "method_parameters") || this.findChild(node, "parameters");
|
|
1364
|
+
if (!paramsNode) return;
|
|
1365
|
+
const _nc14 = paramsNode.namedChildCount;
|
|
1366
|
+
for (let i = 0; i < _nc14; i++) {
|
|
1367
|
+
const pc = paramsNode.namedChild(i);
|
|
1368
|
+
if (!pc) continue;
|
|
1369
|
+
if (pc.type === "optional_parameter" || pc.type === "keyword_parameter") {
|
|
1370
|
+
if (pc.namedChildCount >= 2) {
|
|
1371
|
+
const value = pc.namedChild(pc.namedChildCount - 1);
|
|
1372
|
+
if (value) {
|
|
1373
|
+
this.walkBody(
|
|
1374
|
+
value,
|
|
1375
|
+
symbols,
|
|
1376
|
+
relationships,
|
|
1377
|
+
imports,
|
|
1378
|
+
metadata,
|
|
1379
|
+
owner,
|
|
1380
|
+
ownerKind,
|
|
1381
|
+
enclosingMethod,
|
|
1382
|
+
visibilityOverrides,
|
|
1383
|
+
locals
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
// ========================================================================
|
|
1391
|
+
// LOAD FACTS — one `require`-family site, THREE surfaces
|
|
1392
|
+
// ========================================================================
|
|
1393
|
+
/**
|
|
1394
|
+
* Everything a receiverless `require` / `require_relative` / `load` /
|
|
1395
|
+
* `autoload` of a literal path states. One function because the four call
|
|
1396
|
+
* sites this replaces were four copies of the same three pushes, and the
|
|
1397
|
+
* `references`/`imports` PAIR is exactly the kind of thing that must not be
|
|
1398
|
+
* half-applied: an earlier round of this file already lost a fix to a branch
|
|
1399
|
+
* that claimed a rule "already existed" above the code that implemented it.
|
|
1400
|
+
*
|
|
1401
|
+
* 1. `ImportInfo` — the brain_import row. Reaches the file-dependency engine.
|
|
1402
|
+
* 2. `references` — "this file mentions this path". Source is the declared
|
|
1403
|
+
* file-scope owner. Pinned by three committed fixtures; unchanged.
|
|
1404
|
+
* 3. `imports` — "this file DEPENDS ON this path". The same target, a second
|
|
1405
|
+
* label, and the reason is not cosmetic:
|
|
1406
|
+
*
|
|
1407
|
+
* - it is the label the dependency surfaces and `bb_graph`/`bb_impact`/
|
|
1408
|
+
* `bb_migrate` read. A graph query that narrows to `imports` sees
|
|
1409
|
+
* nothing for ruby today, so "ruby has no file dependencies" is
|
|
1410
|
+
* answerable from the graph and it is false.
|
|
1411
|
+
* - `metadata.fileLevel` is what makes the SOURCE bind to the importing
|
|
1412
|
+
* FILE: `parallel-indexer.ts` binds `source_external` to the
|
|
1413
|
+
* project-relative path when the source names no symbol and the edge
|
|
1414
|
+
* declares `fileLevel`. The `references` edge does not declare it, so
|
|
1415
|
+
* its source stays the scope sentinel and no surface can answer "what
|
|
1416
|
+
* does this file require".
|
|
1417
|
+
*
|
|
1418
|
+
* bash emits the same pair for `source` (`isSourceImport` +
|
|
1419
|
+
* `isSourceDependency` on the same normalized path), so this is the family
|
|
1420
|
+
* precedent and not a ruby invention.
|
|
1421
|
+
*
|
|
1422
|
+
* The TARGET is the bare require path, not the `<path>:<item>` form the
|
|
1423
|
+
* js/python/dart lanes use. That is a property of the language, not a choice:
|
|
1424
|
+
* `require "json/parser"` names a FILE and Ruby has no syntax for requiring one
|
|
1425
|
+
* member of it, so there is no item to write after a colon. The consequence is
|
|
1426
|
+
* measured and declared: `import-resolution.ts` only considers `imports` rows
|
|
1427
|
+
* whose target contains a `:`, so these rows reach the graph and the dependency
|
|
1428
|
+
* engine but NOT that resolver. Inventing a `:*` suffix would buy admission to
|
|
1429
|
+
* a pass that then refuses `*` by design ("namespace-star-no-single-target") —
|
|
1430
|
+
* bytes and a target string that is not a path, for nothing.
|
|
1431
|
+
*/
|
|
1432
|
+
pushLoadFacts(node, arg, cleanText, methodName, relationships, imports) {
|
|
1433
|
+
if (cleanText.length === 0) return;
|
|
1434
|
+
imports.push({
|
|
1435
|
+
module: cleanText,
|
|
1436
|
+
names: [],
|
|
1437
|
+
range: this.nodeToRange(node),
|
|
1438
|
+
isWildcard: true,
|
|
1439
|
+
metadata: { requireMethod: methodName }
|
|
1440
|
+
});
|
|
1441
|
+
relationships.push({
|
|
1442
|
+
// GAD2 twin (groovy wave, standing rule): the require edge's
|
|
1443
|
+
// source used a hardcoded "root" while calls/uses/instan-
|
|
1444
|
+
// tiates at the same top level use "Global" — two sentinels
|
|
1445
|
+
// for one role in one file. Unified on "Global" (the pinned
|
|
1446
|
+
// convention; no fixture ever pinned "root") — and now unified
|
|
1447
|
+
// through FILE_SCOPE_OWNER, which is what made the SECOND
|
|
1448
|
+
// divergent spelling ("unknown", four sites: alias,
|
|
1449
|
+
// alias_method, mixin, association) visible at all.
|
|
1450
|
+
source: FILE_SCOPE_OWNER,
|
|
1451
|
+
target: cleanText,
|
|
1452
|
+
type: ASTEngine.RelationshipType.References,
|
|
1453
|
+
confidence: 0.9,
|
|
1454
|
+
// #825: the required path's string node names the edge.
|
|
1455
|
+
line_number: this.nodeLine(arg),
|
|
1456
|
+
metadata: { requireMethod: methodName }
|
|
1457
|
+
});
|
|
1458
|
+
relationships.push({
|
|
1459
|
+
source: FILE_SCOPE_OWNER,
|
|
1460
|
+
target: cleanText,
|
|
1461
|
+
type: ASTEngine.RelationshipType.Imports,
|
|
1462
|
+
confidence: 1,
|
|
1463
|
+
line_number: this.nodeLine(arg),
|
|
1464
|
+
metadata: {
|
|
1465
|
+
requireMethod: methodName,
|
|
1466
|
+
// The importer is the FILE, not a scope — see the note above.
|
|
1467
|
+
fileLevel: true,
|
|
1468
|
+
// HOW the path is resolved, which is decidable per method and is the
|
|
1469
|
+
// only classification that is a fact rather than a guess:
|
|
1470
|
+
// `require_relative` is relative to the requiring file by definition, so
|
|
1471
|
+
// its target is a path in the project; a bare `require` is resolved
|
|
1472
|
+
// against `$LOAD_PATH`, which can name a gem OR a project file, so its
|
|
1473
|
+
// target's location is unknown. `is_external` is therefore NOT set:
|
|
1474
|
+
// parallel-indexer reads it to keep external packages out of internal
|
|
1475
|
+
// resolution, and asserting `false` for a load-path require would be a
|
|
1476
|
+
// claim this file cannot make.
|
|
1477
|
+
resolution: methodName === "require_relative" ? "file-relative" : "load-path"
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
// ========================================================================
|
|
1482
|
+
// CALL PROCESSOR — resolves a single `call` node
|
|
1483
|
+
// ========================================================================
|
|
1484
|
+
processCall(node, symbols, relationships, imports, metadata, owner, enclosingMethod, visibilityOverrides, anonBinding) {
|
|
1485
|
+
const methodNode = node.childForFieldName("method");
|
|
1486
|
+
if (!methodNode) return;
|
|
1487
|
+
const methodName = methodNode.text;
|
|
1488
|
+
if (!methodName) return;
|
|
1489
|
+
let receiver;
|
|
1490
|
+
let receiverLeaf;
|
|
1491
|
+
let receiverNode = null;
|
|
1492
|
+
let hasExpressionReceiver = false;
|
|
1493
|
+
const _nc15 = node.namedChildCount;
|
|
1494
|
+
for (let i = 0; i < _nc15; i++) {
|
|
1495
|
+
const child = node.namedChild(i);
|
|
1496
|
+
if (child && child.id === methodNode.id) break;
|
|
1497
|
+
if (!child) continue;
|
|
1498
|
+
if (child.type === "identifier" || child.type === "constant" || child.type === "scope_resolution") {
|
|
1499
|
+
receiver = child.text;
|
|
1500
|
+
receiverLeaf = this.leafName(child.text);
|
|
1501
|
+
receiverNode = child;
|
|
1502
|
+
} else if (VARIABLE_RECEIVER_TYPES.has(child.type)) {
|
|
1503
|
+
receiver = child.text;
|
|
1504
|
+
receiverNode = child;
|
|
1505
|
+
} else if (child.type === "self") {
|
|
1506
|
+
} else {
|
|
1507
|
+
hasExpressionReceiver = true;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (methodName === "private" || methodName === "protected" || methodName === "public") {
|
|
1511
|
+
const args = this.findChild(node, "argument_list");
|
|
1512
|
+
if (args && owner && args.namedChildCount > 0) {
|
|
1513
|
+
const _nc16 = args.namedChildCount;
|
|
1514
|
+
for (let i = 0; i < _nc16; i++) {
|
|
1515
|
+
const arg = args.namedChild(i);
|
|
1516
|
+
if (!arg || arg.type !== "simple_symbol") continue;
|
|
1517
|
+
const targetName = this.stripSymbolColon(arg.text);
|
|
1518
|
+
visibilityOverrides.set(`${owner}\0${targetName}`, methodName);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if ((methodName === "require" || methodName === "require_relative" || methodName === "load") && !receiver && receiverNode === null && !hasExpressionReceiver) {
|
|
1524
|
+
const args = this.findChild(node, "argument_list");
|
|
1525
|
+
if (args) {
|
|
1526
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
1527
|
+
const arg = args.child(i);
|
|
1528
|
+
if (arg && arg.type === "string") {
|
|
1529
|
+
const cleanText = this.stripQuotes(arg.text);
|
|
1530
|
+
if (cleanText.indexOf("#{") >= 0) continue;
|
|
1531
|
+
this.pushLoadFacts(node, arg, cleanText, methodName, relationships, imports);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
if (methodName === "autoload" && !receiver && receiverNode === null && !hasExpressionReceiver) {
|
|
1538
|
+
const args = this.findChild(node, "argument_list");
|
|
1539
|
+
if (args) {
|
|
1540
|
+
for (let i = 0; i < args.childCount; i++) {
|
|
1541
|
+
const arg = args.child(i);
|
|
1542
|
+
if (arg && arg.type === "string") {
|
|
1543
|
+
const cleanText = this.stripQuotes(arg.text);
|
|
1544
|
+
if (cleanText.indexOf("#{") >= 0) continue;
|
|
1545
|
+
this.pushLoadFacts(node, arg, cleanText, methodName, relationships, imports);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if (methodName === "include" || methodName === "extend" || methodName === "prepend") {
|
|
1552
|
+
const relType = ASTEngine.RelationshipType.MixesIn;
|
|
1553
|
+
const args = this.findChild(node, "argument_list");
|
|
1554
|
+
const selfKind = this.mixinSelfKind(node);
|
|
1555
|
+
const hasExplicitReceiver = receiver !== void 0 || hasExpressionReceiver;
|
|
1556
|
+
if (selfKind === "instance" || selfKind === "top" || hasExplicitReceiver) {
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (!owner) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (args) {
|
|
1563
|
+
const _nc17 = args.namedChildCount;
|
|
1564
|
+
for (let i = 0; i < _nc17; i++) {
|
|
1565
|
+
const arg = args.namedChild(i);
|
|
1566
|
+
if (arg && (arg.type === "constant" || arg.type === "scope_resolution")) {
|
|
1567
|
+
const target = this.leafName(arg.text);
|
|
1568
|
+
relationships.push({
|
|
1569
|
+
source: owner || FILE_SCOPE_OWNER,
|
|
1570
|
+
target,
|
|
1571
|
+
type: relType,
|
|
1572
|
+
confidence: 1,
|
|
1573
|
+
// #825: the mixin CONSTANT's node names the edge.
|
|
1574
|
+
line_number: this.nodeLine(arg),
|
|
1575
|
+
metadata: { mixinType: methodName }
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (methodName === "attr_reader" || methodName === "attr_writer" || methodName === "attr_accessor") {
|
|
1583
|
+
const args = this.findChild(node, "argument_list");
|
|
1584
|
+
if (args) {
|
|
1585
|
+
const _nc18 = args.namedChildCount;
|
|
1586
|
+
for (let i = 0; i < _nc18; i++) {
|
|
1587
|
+
const arg = args.namedChild(i);
|
|
1588
|
+
if (arg && (arg.type === "simple_symbol" || arg.type === "string")) {
|
|
1589
|
+
const propName = this.rubyNameFromLiteral(arg);
|
|
1590
|
+
if (propName === void 0 || propName.length === 0) continue;
|
|
1591
|
+
symbols.push({
|
|
1592
|
+
name: propName,
|
|
1593
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1594
|
+
range: this.nodeToRange(arg),
|
|
1595
|
+
ownerName: owner,
|
|
1596
|
+
visibility: "public",
|
|
1597
|
+
metadata: { accessorType: methodName }
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (methodName === "define_method") {
|
|
1605
|
+
const args = this.findChild(node, "argument_list");
|
|
1606
|
+
const nameArg = args && args.namedChildCount > 0 ? args.namedChild(0) : null;
|
|
1607
|
+
if (nameArg && (nameArg.type === "simple_symbol" || nameArg.type === "string")) {
|
|
1608
|
+
const name = this.rubyNameFromLiteral(nameArg);
|
|
1609
|
+
if (name !== void 0 && name.length > 0 && name.indexOf("#{") < 0) {
|
|
1610
|
+
symbols.push({
|
|
1611
|
+
name,
|
|
1612
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1613
|
+
range: this.nodeToRange(nameArg),
|
|
1614
|
+
ownerName: owner,
|
|
1615
|
+
visibility: "public",
|
|
1616
|
+
metadata: { isDynamicMethod: true }
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (methodName === "alias_method") {
|
|
1623
|
+
const args = this.findChild(node, "argument_list");
|
|
1624
|
+
if (args) {
|
|
1625
|
+
const names = [];
|
|
1626
|
+
const argNodes = [];
|
|
1627
|
+
for (let i = 0; i < args.namedChildCount && names.length < 2; i++) {
|
|
1628
|
+
const arg = args.namedChild(i);
|
|
1629
|
+
if (arg && arg.type === "simple_symbol") {
|
|
1630
|
+
const n = this.rubyNameFromLiteral(arg);
|
|
1631
|
+
if (n === void 0 || n.length === 0) continue;
|
|
1632
|
+
names.push(n);
|
|
1633
|
+
argNodes.push(arg);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if (names.length === 2) {
|
|
1637
|
+
relationships.push({
|
|
1638
|
+
source: owner || FILE_SCOPE_OWNER,
|
|
1639
|
+
target: names[0],
|
|
1640
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1641
|
+
confidence: 0.9,
|
|
1642
|
+
// #825: the `:new_name` argument's node names the edge.
|
|
1643
|
+
line_number: this.nodeLine(argNodes[0]),
|
|
1644
|
+
metadata: { aliasedTo: names[1] }
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
if (methodName === "redirect_to" || methodName === "render" || methodName === "before_action" || methodName === "after_action") {
|
|
1651
|
+
this.recordFramework(metadata, RAILS_FRAMEWORK);
|
|
1652
|
+
}
|
|
1653
|
+
if (methodName === "before_action" || methodName === "after_action") {
|
|
1654
|
+
this.recordFramework(metadata, RAILS_FRAMEWORK, "Callbacks");
|
|
1655
|
+
}
|
|
1656
|
+
if (RAILS_MACRO_METHODS.has(methodName)) {
|
|
1657
|
+
if (["has_many", "belongs_to", "has_one", "has_and_belongs_to_many"].includes(methodName)) {
|
|
1658
|
+
const args = this.findChild(node, "argument_list");
|
|
1659
|
+
if (args) {
|
|
1660
|
+
const _nc20 = args.namedChildCount;
|
|
1661
|
+
for (let i = 0; i < _nc20; i++) {
|
|
1662
|
+
const arg = args.namedChild(i);
|
|
1663
|
+
if (arg && (arg.type === "simple_symbol" || arg.type === "string")) {
|
|
1664
|
+
const name = this.rubyNameFromLiteral(arg);
|
|
1665
|
+
if (name === void 0 || name.length === 0) continue;
|
|
1666
|
+
const anonMd = anonBinding ? { anonymousScopeBinding: anonBinding } : {};
|
|
1667
|
+
symbols.push({
|
|
1668
|
+
name,
|
|
1669
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1670
|
+
range: this.nodeToRange(arg),
|
|
1671
|
+
ownerName: owner,
|
|
1672
|
+
visibility: "public",
|
|
1673
|
+
metadata: { associationType: methodName, ...anonMd }
|
|
1674
|
+
});
|
|
1675
|
+
relationships.push({
|
|
1676
|
+
source: owner || FILE_SCOPE_OWNER,
|
|
1677
|
+
target: name,
|
|
1678
|
+
type: ASTEngine.RelationshipType.Uses,
|
|
1679
|
+
confidence: 0.8,
|
|
1680
|
+
// #825: the association's own argument node names the edge.
|
|
1681
|
+
line_number: this.nodeLine(arg),
|
|
1682
|
+
metadata: { activerecordMacro: methodName, ...anonMd }
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
} else if (methodName === "scope" || methodName === "default_scope") {
|
|
1688
|
+
const args = this.findChild(node, "argument_list");
|
|
1689
|
+
if (args && args.namedChildCount > 0) {
|
|
1690
|
+
const firstArg = args.namedChild(0);
|
|
1691
|
+
if (firstArg && firstArg.type === "simple_symbol") {
|
|
1692
|
+
const scopeName = this.rubyNameFromLiteral(firstArg);
|
|
1693
|
+
if (scopeName === void 0 || scopeName.length === 0) return;
|
|
1694
|
+
symbols.push({
|
|
1695
|
+
name: scopeName,
|
|
1696
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1697
|
+
range: this.nodeToRange(firstArg),
|
|
1698
|
+
ownerName: owner,
|
|
1699
|
+
visibility: "public",
|
|
1700
|
+
metadata: { scopeType: methodName }
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (RUBY_BUILTINS.has(methodName)) return;
|
|
1708
|
+
if (methodName === "class" && receiver === "self") return;
|
|
1709
|
+
const source = enclosingMethod || owner || FILE_SCOPE_OWNER;
|
|
1710
|
+
if (methodName === "new") {
|
|
1711
|
+
if (hasExpressionReceiver) return;
|
|
1712
|
+
const target = receiverLeaf || (!receiver ? owner : void 0);
|
|
1713
|
+
const firstChar = target?.charCodeAt(0) ?? 0;
|
|
1714
|
+
if (target && (!receiverLeaf || firstChar >= 65 && firstChar <= 90)) {
|
|
1715
|
+
relationships.push({
|
|
1716
|
+
source,
|
|
1717
|
+
target,
|
|
1718
|
+
fromSymbol: source,
|
|
1719
|
+
toSymbol: target,
|
|
1720
|
+
type: ASTEngine.RelationshipType.Instantiates,
|
|
1721
|
+
// #825: the constructed TYPE names the edge — its receiver node when
|
|
1722
|
+
// there is one, else the call node itself (bare `new`).
|
|
1723
|
+
line_number: this.nodeLine(receiverNode ?? node),
|
|
1724
|
+
confidence: 0.9,
|
|
1725
|
+
// When no receiver supplied the name, the target is the ENCLOSING
|
|
1726
|
+
// class: Ruby's `self.new`, `self.class.new` and a receiverless `new`
|
|
1727
|
+
// all construct the class they appear in. The line is the call site
|
|
1728
|
+
// (correct), but the class name is IMPLIED rather than written there,
|
|
1729
|
+
// so the inference is declared instead of left to look like a written
|
|
1730
|
+
// name. The M59 oracle reads this marker to classify the claim as
|
|
1731
|
+
// structurally uncheckable-by-token rather than as a failed claim.
|
|
1732
|
+
metadata: receiverLeaf ? {} : { resolvedTarget: "enclosing-class" }
|
|
1733
|
+
});
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
if (!receiver && !owner) return;
|
|
1737
|
+
}
|
|
1738
|
+
const qualified = receiver ? `${receiver}.${methodName}` : methodName;
|
|
1739
|
+
relationships.push({
|
|
1740
|
+
source,
|
|
1741
|
+
target: methodName,
|
|
1742
|
+
fromSymbol: source,
|
|
1743
|
+
toSymbol: qualified,
|
|
1744
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1745
|
+
confidence: 0.9,
|
|
1746
|
+
// #825: the METHOD field's node, never the call node — a call node starts
|
|
1747
|
+
// at its receiver, which is the line ABOVE the callee in `a.b.c` chains.
|
|
1748
|
+
line_number: this.nodeLine(methodNode),
|
|
1749
|
+
metadata: {}
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Emit a calls relationship for a bare identifier used as a method call
|
|
1754
|
+
* (tree-sitter-ruby parses `foo` with no receiver/args as an identifier).
|
|
1755
|
+
* Mirrors processCall's regular-call tail: builtins are skipped and the
|
|
1756
|
+
* same confidence/metadata shape is used so the graph stays consistent.
|
|
1757
|
+
*/
|
|
1758
|
+
pushBareCall(name, nameNode, owner, enclosingMethod, relationships) {
|
|
1759
|
+
if (RUBY_BUILTINS.has(name)) return;
|
|
1760
|
+
if (name === "self" || name === "nil" || name === "true" || name === "false") return;
|
|
1761
|
+
if (isRubyVisibilityDirectiveName(name)) return;
|
|
1762
|
+
const firstChar = name.charCodeAt(0);
|
|
1763
|
+
if (firstChar >= 65 && firstChar <= 90) return;
|
|
1764
|
+
if (name.startsWith("__") && name.endsWith("__")) return;
|
|
1765
|
+
const source = enclosingMethod || owner || FILE_SCOPE_OWNER;
|
|
1766
|
+
relationships.push({
|
|
1767
|
+
source,
|
|
1768
|
+
target: name,
|
|
1769
|
+
fromSymbol: source,
|
|
1770
|
+
toSymbol: name,
|
|
1771
|
+
type: ASTEngine.RelationshipType.Calls,
|
|
1772
|
+
confidence: 0.9,
|
|
1773
|
+
// #825: the identifier's own node names the bare call.
|
|
1774
|
+
line_number: this.nodeLine(nameNode),
|
|
1775
|
+
metadata: { bareCall: true }
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
// ========================================================================
|
|
1779
|
+
// HELPERS
|
|
1780
|
+
// ========================================================================
|
|
1781
|
+
/**
|
|
1782
|
+
* True when an `assignment` node's RHS is a dynamic class/module factory
|
|
1783
|
+
* call (`Struct.new`, `Data.define`, `Class.new`, `Module.new`, possibly
|
|
1784
|
+
* with blocks/args). Used to admit top-level factory assignments that
|
|
1785
|
+
* the owner-gated constant rule would otherwise skip.
|
|
1786
|
+
*/
|
|
1787
|
+
isDynamicClassFactoryRhs(node) {
|
|
1788
|
+
const _nc21 = node.namedChildCount;
|
|
1789
|
+
for (let i = 0; i < _nc21; i++) {
|
|
1790
|
+
const c = node.namedChild(i);
|
|
1791
|
+
if (!c) continue;
|
|
1792
|
+
if (c.type === "call") {
|
|
1793
|
+
if (this.dynamicClassFactoryKind(c)) return true;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* The NAME a `simple_symbol` or `string` literal declares, or `undefined` when
|
|
1800
|
+
* the analyzer cannot state it.
|
|
1801
|
+
*
|
|
1802
|
+
* ## Why this exists
|
|
1803
|
+
*
|
|
1804
|
+
* `stripQuotes` removes the OUTER quotes and stops there, so a Ruby escape inside
|
|
1805
|
+
* the literal survives into the published name. Ruby's
|
|
1806
|
+
* `define_method("test_safe_load_marshal String \"hello\" ivar")` defines a method
|
|
1807
|
+
* whose name is `test_safe_load_marshal String "hello" ivar` — with a plain quote,
|
|
1808
|
+
* no backslash — and the analyzer published the name with the backslash still in
|
|
1809
|
+
* it. That name matches no method in the program, and it propagated: the symbol
|
|
1810
|
+
* carried it, `contains` carried it, and `overrides` carried it again as
|
|
1811
|
+
* `TestCase.<that name>`. Measured by the full-corpus R7 sweep: 19 distinct
|
|
1812
|
+
* escaped names, 14 `contains` edges and 14 `overrides` edges with a quote in a
|
|
1813
|
+
* target, every one in test/rubygems/test_gem_safe_marshal.rb — generated
|
|
1814
|
+
* round-trip test methods, so the names are real but the escapes were not
|
|
1815
|
+
* resolved.
|
|
1816
|
+
*
|
|
1817
|
+
* ## The escape set, and why it is closed
|
|
1818
|
+
*
|
|
1819
|
+
* The corpus says which escapes occur, and the inventory is four pairs:
|
|
1820
|
+
*
|
|
1821
|
+
* \" 56 \\ 21 \x 21 \u 1
|
|
1822
|
+
*
|
|
1823
|
+
* `\"`, `\\` and the single-character control escapes resolve to text a name can
|
|
1824
|
+
* carry, so they are resolved here and the name is published correct. `\x` and
|
|
1825
|
+
* `\u` are NOT resolved, and that is a decision rather than an omission: `\x61`
|
|
1826
|
+
* is the BYTE 0x61 and `\u{3042}` is a non-ASCII character, so resolving them
|
|
1827
|
+
* would publish a name built from raw bytes or a non-Latin script — a string that
|
|
1828
|
+
* no consumer can bind and that the parallel-indexer's own name filters would
|
|
1829
|
+
* then treat as garbage. A name this analyzer cannot state is not published,
|
|
1830
|
+
* which is the same doctrine the `#{…}` template refusal above applies and the
|
|
1831
|
+
* reason that refusal exists.
|
|
1832
|
+
*
|
|
1833
|
+
* So the rule is: resolve the escapes that yield a name, refuse the name when it
|
|
1834
|
+
* carries an escape that does not. `\q` — an escape Ruby does not define at all
|
|
1835
|
+
* — takes the same path, and returns `undefined` rather than passing the backslash
|
|
1836
|
+
* through.
|
|
1837
|
+
*
|
|
1838
|
+
* ## Character scan, no pattern
|
|
1839
|
+
*
|
|
1840
|
+
* The escapes are matched by character code against a switch, and the scan walks
|
|
1841
|
+
* the string once. A table keyed on a substring would be a lookup dressed as a
|
|
1842
|
+
* parse; this reads the backslash, then the character it escapes, and decides on
|
|
1843
|
+
* those two codes.
|
|
1844
|
+
*/
|
|
1845
|
+
rubyNameFromLiteral(node) {
|
|
1846
|
+
const raw = node.text;
|
|
1847
|
+
let s = raw;
|
|
1848
|
+
if (node.type === "string") {
|
|
1849
|
+
s = this.stripQuotes(raw);
|
|
1850
|
+
} else {
|
|
1851
|
+
s = this.stripSymbolColon(raw);
|
|
1852
|
+
}
|
|
1853
|
+
if (s.indexOf("\\") < 0) return s;
|
|
1854
|
+
let out = "";
|
|
1855
|
+
for (let i = 0; i < s.length; i++) {
|
|
1856
|
+
const c = s.charCodeAt(i);
|
|
1857
|
+
if (c !== 92) {
|
|
1858
|
+
out += s[i];
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (i + 1 >= s.length) return void 0;
|
|
1862
|
+
const e = s[i + 1];
|
|
1863
|
+
if (e === "\\" || e === '"' || e === "'" || e === "#") {
|
|
1864
|
+
out += e;
|
|
1865
|
+
i++;
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
if (e === "a") {
|
|
1869
|
+
out += "\x07";
|
|
1870
|
+
i++;
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
if (e === "b") {
|
|
1874
|
+
out += "\b";
|
|
1875
|
+
i++;
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
if (e === "e") {
|
|
1879
|
+
out += "\x1B";
|
|
1880
|
+
i++;
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
if (e === "f") {
|
|
1884
|
+
out += "\f";
|
|
1885
|
+
i++;
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
if (e === "n") {
|
|
1889
|
+
out += "\n";
|
|
1890
|
+
i++;
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
if (e === "r") {
|
|
1894
|
+
out += "\r";
|
|
1895
|
+
i++;
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
if (e === "s") {
|
|
1899
|
+
out += " ";
|
|
1900
|
+
i++;
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
if (e === "t") {
|
|
1904
|
+
out += " ";
|
|
1905
|
+
i++;
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
if (e === "v") {
|
|
1909
|
+
out += "\v";
|
|
1910
|
+
i++;
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (e === "0") {
|
|
1914
|
+
out += "\0";
|
|
1915
|
+
i++;
|
|
1916
|
+
continue;
|
|
1917
|
+
}
|
|
1918
|
+
return void 0;
|
|
1919
|
+
}
|
|
1920
|
+
return out;
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* The node that NAMES the base of a `K = Class.new(Base)` factory, or null.
|
|
1924
|
+
*
|
|
1925
|
+
* Only `Class.new` counts, and only when the base is the FIRST argument and is
|
|
1926
|
+
* written as a constant or a scope path. `Struct.new(:a, :b)` and
|
|
1927
|
+
* `Data.define(:a, :b)` take member names there, not a superclass — neither
|
|
1928
|
+
* class has a superclass parameter — so recognising them as a base would
|
|
1929
|
+
* fabricate an `extends` edge against a symbol name. `Module.new` takes no
|
|
1930
|
+
* argument at all, which the method check below also rules out.
|
|
1931
|
+
*
|
|
1932
|
+
* The RHS may wrap the call (`Class.new(Base) { … }.new`, which
|
|
1933
|
+
* `dynamicClassFactoryKind` already recurses through), so the search for the
|
|
1934
|
+
* `Class.new` head repeats the same bounded walk rather than assuming the call
|
|
1935
|
+
* sits at the top. The recursion is bounded by the grammar: the inner node it
|
|
1936
|
+
* recurses into must itself be a `.new` call whose own receiver is a constant.
|
|
1937
|
+
*/
|
|
1938
|
+
factorySuperclassNode(rhs) {
|
|
1939
|
+
if (!rhs) return null;
|
|
1940
|
+
let callNode = rhs.type === "call" ? rhs : null;
|
|
1941
|
+
if (!callNode) {
|
|
1942
|
+
for (let i = 0; i < rhs.namedChildCount; i++) {
|
|
1943
|
+
const c = rhs.namedChild(i);
|
|
1944
|
+
if (c && c.type === "call") {
|
|
1945
|
+
callNode = c;
|
|
1946
|
+
break;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (!callNode) return null;
|
|
1951
|
+
const methodNode = callNode.childForFieldName("method");
|
|
1952
|
+
if (!methodNode || methodNode.text !== "new") return null;
|
|
1953
|
+
let receiver;
|
|
1954
|
+
for (let i = 0; i < callNode.namedChildCount; i++) {
|
|
1955
|
+
const c = callNode.namedChild(i);
|
|
1956
|
+
if (!c) continue;
|
|
1957
|
+
if (c.id === methodNode.id) break;
|
|
1958
|
+
if (c.type === "constant" || c.type === "scope_resolution") {
|
|
1959
|
+
receiver = c.text;
|
|
1960
|
+
continue;
|
|
1961
|
+
}
|
|
1962
|
+
if (c.type === "call") return this.factorySuperclassNode(c);
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
if (receiver !== "Class") return null;
|
|
1966
|
+
const args = this.findChild(callNode, "argument_list");
|
|
1967
|
+
if (!args || args.namedChildCount === 0) return null;
|
|
1968
|
+
const first = args.namedChild(0);
|
|
1969
|
+
if (!first) return null;
|
|
1970
|
+
if (first.type !== "constant" && first.type !== "scope_resolution") return null;
|
|
1971
|
+
return first;
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* If `node` is a `X.new`/`X.define` call on a class-factory receiver,
|
|
1975
|
+
* return "Class" (Struct/Data/Class) or "Module" (Module.new); else null.
|
|
1976
|
+
* Receiver must be the bare constant (RHS of the assignment may wrap in
|
|
1977
|
+
* blocks — only the call head matters).
|
|
1978
|
+
*/
|
|
1979
|
+
dynamicClassFactoryKind(node) {
|
|
1980
|
+
let callNode = node.type === "call" ? node : null;
|
|
1981
|
+
if (!callNode) {
|
|
1982
|
+
const _nc22 = node.namedChildCount;
|
|
1983
|
+
for (let i = 0; i < _nc22; i++) {
|
|
1984
|
+
const c = node.namedChild(i);
|
|
1985
|
+
if (c && c.type === "call") {
|
|
1986
|
+
callNode = c;
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
if (!callNode) return null;
|
|
1992
|
+
const methodNode = callNode.childForFieldName("method");
|
|
1993
|
+
if (!methodNode) return null;
|
|
1994
|
+
let methodName = methodNode.text;
|
|
1995
|
+
let receiver;
|
|
1996
|
+
const _nc23 = callNode.namedChildCount;
|
|
1997
|
+
for (let i = 0; i < _nc23; i++) {
|
|
1998
|
+
const child = callNode.namedChild(i);
|
|
1999
|
+
if (child && child.id === methodNode.id) break;
|
|
2000
|
+
if (!child) continue;
|
|
2001
|
+
if (child.type === "constant" || child.type === "scope_resolution") {
|
|
2002
|
+
receiver = child.text;
|
|
2003
|
+
} else if (!receiver && child.type === "call" && (methodName === "new" || methodName === "define")) {
|
|
2004
|
+
const inner = this.dynamicClassFactoryKind(child);
|
|
2005
|
+
if (inner) return inner;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
if (!receiver) return null;
|
|
2009
|
+
if (methodName !== "new" && methodName !== "define") return null;
|
|
2010
|
+
const leaf = receiver.split("::").filter(Boolean).pop() ?? receiver;
|
|
2011
|
+
if (leaf === "Module" && methodName === "new") return "Module";
|
|
2012
|
+
if ((leaf === "Struct" || leaf === "Data" || leaf === "Class") && (methodName === "new" || methodName === "define")) return "Class";
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* The constants a scope removes from its public surface with
|
|
2017
|
+
* `private_constant :A, :B`, keyed by the OWNER NAME THE SYMBOL TABLE USES.
|
|
2018
|
+
*
|
|
2019
|
+
* Ruby's constant visibility has its own keyword and the method family does not,
|
|
2020
|
+
* so the lexical `visibility` this file tracks for methods says nothing about a
|
|
2021
|
+
* constant — a `private_constant :VERSION` member would otherwise be published
|
|
2022
|
+
* as part of the export surface, which is precisely the members the language
|
|
2023
|
+
* declares unexported. Measured on the corpus: 142 sites in 73 files.
|
|
2024
|
+
*
|
|
2025
|
+
* It is collected in its own walk rather than in `processCall` because it needs
|
|
2026
|
+
* the ENCLOSING SCOPE, and threading a map through `walkBody` /
|
|
2027
|
+
* `walkMethodBody` / `walkParamDefaults` / `processCall` — eight signatures and
|
|
2028
|
+
* eight call sites — to carry one set is a large diff in a hot file for a small
|
|
2029
|
+
* fact. The scope rule it needs is the same one the main walk uses, so it calls
|
|
2030
|
+
* `rubyReopenIdentity` rather than restating it: a second copy of the reopen
|
|
2031
|
+
* rule is exactly how the two existing copies came to disagree.
|
|
2032
|
+
*
|
|
2033
|
+
* Keyed by the same owner string the export pass reads off `ownerName`, and
|
|
2034
|
+
* missing a scope costs a wrong edge, so the walk descends into every node type
|
|
2035
|
+
* rather than only the ones a declaration can sit under.
|
|
2036
|
+
*/
|
|
2037
|
+
collectPrivateConstants(root) {
|
|
2038
|
+
const out = /* @__PURE__ */ new Map();
|
|
2039
|
+
const stack = [{ node: root, owner: void 0 }];
|
|
2040
|
+
while (stack.length > 0) {
|
|
2041
|
+
const { node, owner } = stack.pop();
|
|
2042
|
+
const type = node.type;
|
|
2043
|
+
let nextOwner = owner;
|
|
2044
|
+
if (type === "class" || type === "module" || type === "singleton_class") {
|
|
2045
|
+
const nameNode = node.childForFieldName("name");
|
|
2046
|
+
if (nameNode) nextOwner = rubyReopenIdentity(nameNode.text, owner).name;
|
|
2047
|
+
if (type === "singleton_class") nextOwner = owner;
|
|
2048
|
+
} else if (type === "call") {
|
|
2049
|
+
const methodNode = node.childForFieldName("method");
|
|
2050
|
+
if (methodNode?.text === "private_constant") {
|
|
2051
|
+
const args = this.findChild(node, "argument_list");
|
|
2052
|
+
if (args) {
|
|
2053
|
+
const key = owner ?? FILE_SCOPE_OWNER;
|
|
2054
|
+
let set = out.get(key);
|
|
2055
|
+
if (!set) {
|
|
2056
|
+
set = /* @__PURE__ */ new Set();
|
|
2057
|
+
out.set(key, set);
|
|
2058
|
+
}
|
|
2059
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2060
|
+
const arg = args.namedChild(i);
|
|
2061
|
+
if (!arg) continue;
|
|
2062
|
+
if (arg.type === "simple_symbol" || arg.type === "string") {
|
|
2063
|
+
const n = this.rubyNameFromLiteral(arg);
|
|
2064
|
+
if (n !== void 0 && n.length > 0) set.add(n);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
2071
|
+
const child = node.namedChild(i);
|
|
2072
|
+
if (child) stack.push({ node: child, owner: nextOwner });
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return out;
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Extract superclass from class node — pure char scan, no regex
|
|
2079
|
+
*/
|
|
2080
|
+
/**
|
|
2081
|
+
* The node that NAMES the base type of a `class X < Base` declaration: the
|
|
2082
|
+
* first named child of the `superclass` node, i.e. `Base` as written. The
|
|
2083
|
+
* `superclass` node itself begins at the `<` token, which sits on the previous
|
|
2084
|
+
* line whenever the base is wrapped — so it is the wrong anchor for a line.
|
|
2085
|
+
*/
|
|
2086
|
+
superclassTargetNode(node) {
|
|
2087
|
+
const superNode = this.findChild(node, "superclass");
|
|
2088
|
+
if (!superNode || superNode.namedChildCount === 0) return null;
|
|
2089
|
+
return superNode.namedChild(0);
|
|
2090
|
+
}
|
|
2091
|
+
extractSuperclass(node) {
|
|
2092
|
+
const superNode = this.findChild(node, "superclass");
|
|
2093
|
+
if (!superNode) return void 0;
|
|
2094
|
+
const target = this.superclassTargetNode(node);
|
|
2095
|
+
if (target?.text) return target.text.trim();
|
|
2096
|
+
const rawText = superNode.text;
|
|
2097
|
+
let i = rawText.charCodeAt(0) === 60 ? 1 : 0;
|
|
2098
|
+
while (i < rawText.length && isAsciiWhitespace(rawText.charCodeAt(i))) i++;
|
|
2099
|
+
const text = rawText.substring(i).trim();
|
|
2100
|
+
return text.length > 0 ? text : void 0;
|
|
2101
|
+
}
|
|
2102
|
+
/** Extract parameters from method node */
|
|
2103
|
+
extractParameters(node) {
|
|
2104
|
+
const params = this.findChild(node, "method_parameters") || this.findChild(node, "parameters");
|
|
2105
|
+
if (!params) return void 0;
|
|
2106
|
+
const result = [];
|
|
2107
|
+
const _nc24 = params.namedChildCount;
|
|
2108
|
+
for (let i = 0; i < _nc24; i++) {
|
|
2109
|
+
const child = params.namedChild(i);
|
|
2110
|
+
if (!child) continue;
|
|
2111
|
+
const childType = child.type;
|
|
2112
|
+
if (childType === "identifier") {
|
|
2113
|
+
result.push({ name: child.text });
|
|
2114
|
+
} else if (childType === "optional_parameter") {
|
|
2115
|
+
const name = child.childForFieldName("name");
|
|
2116
|
+
if (name) result.push({ name: name.text });
|
|
2117
|
+
} else if (childType === "keyword_parameter" || childType === "keyword_rest") {
|
|
2118
|
+
const name = child.childForFieldName("name");
|
|
2119
|
+
if (name) result.push({ name: name.text });
|
|
2120
|
+
} else if (childType === "splat_parameter" || childType === "hash_splat_parameter") {
|
|
2121
|
+
const name = child.childForFieldName("name");
|
|
2122
|
+
if (name) result.push({ name: name.text });
|
|
2123
|
+
} else if (childType === "block_parameter") {
|
|
2124
|
+
const name = child.childForFieldName("name") ?? child.namedChild(0);
|
|
2125
|
+
if (name && name.type === "identifier") result.push({ name: name.text });
|
|
2126
|
+
} else if (childType === "destructured_parameter") {
|
|
2127
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
2128
|
+
const sub = child.namedChild(j);
|
|
2129
|
+
if (sub && sub.type === "identifier") {
|
|
2130
|
+
result.push({ name: sub.text });
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
return result.length > 0 ? result : void 0;
|
|
2136
|
+
}
|
|
2137
|
+
/** Return true for a bare visibility directive that affects following methods. */
|
|
2138
|
+
isNoArgumentVisibilityMarker(node) {
|
|
2139
|
+
if (node.type === "identifier") {
|
|
2140
|
+
return node.text === "private" || node.text === "protected" || node.text === "public";
|
|
2141
|
+
}
|
|
2142
|
+
if (node.type !== "call") return false;
|
|
2143
|
+
const method = node.childForFieldName("method");
|
|
2144
|
+
if (!method || method.text !== "private" && method.text !== "protected" && method.text !== "public") return false;
|
|
2145
|
+
const args = this.findChild(node, "argument_list");
|
|
2146
|
+
return !args || args.namedChildCount === 0;
|
|
2147
|
+
}
|
|
2148
|
+
/** Per-symbol cyclomatic complexity (managed-OO family contract —
|
|
2149
|
+
* csharp #264 wave precedent). Mirrors the file-level counter's node set
|
|
2150
|
+
* (if/unless/while/until/case/rescue/elsif PLUS the postfix *modifier
|
|
2151
|
+
* forms), scoped to one method/function subtree; base 1.
|
|
2152
|
+
*
|
|
2153
|
+
* Postfix conditionals MUST count: `return false if amount <= 0` is a
|
|
2154
|
+
* genuine branch — semantically identical to the block form, and the most
|
|
2155
|
+
* idiomatic Ruby. tree-sitter-ruby emits a DISTINCT node type
|
|
2156
|
+
* (if_modifier/unless_modifier/while_modifier/until_modifier/
|
|
2157
|
+
* rescue_modifier) for postfix forms; before this fix the analyzer counted
|
|
2158
|
+
* only the block forms, under-reporting every modifier-heavy method
|
|
2159
|
+
* (verified: fixture authorize has 2 if_modifier + 1 while = complexity 4,
|
|
2160
|
+
* previously pinned at the wrong value 2). */
|
|
2161
|
+
calculateNodeComplexity(node) {
|
|
2162
|
+
if (!node) return 1;
|
|
2163
|
+
let complexity = 1;
|
|
2164
|
+
const stack = [node];
|
|
2165
|
+
let guard = 0;
|
|
2166
|
+
while (stack.length > 0 && guard++ < 2e5) {
|
|
2167
|
+
const n = stack.pop();
|
|
2168
|
+
if (n.type === "if" || n.type === "unless" || n.type === "while" || n.type === "until" || n.type === "case" || n.type === "rescue" || n.type === "elsif" || n.type === "if_modifier" || n.type === "unless_modifier" || n.type === "while_modifier" || n.type === "until_modifier" || n.type === "rescue_modifier") {
|
|
2169
|
+
complexity++;
|
|
2170
|
+
}
|
|
2171
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
2172
|
+
const child = n.namedChild(i);
|
|
2173
|
+
if (child) stack.push(child);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
return complexity;
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Evidence-tier classification — same rules as the swift/dart/csharp/objc
|
|
2180
|
+
* family (see swift wave #266 for the full rationale).
|
|
2181
|
+
*/
|
|
2182
|
+
/**
|
|
2183
|
+
* PROJECT INDEX CONSUMPTION (task-22 phase 2) — facts, never fabrications.
|
|
2184
|
+
*
|
|
2185
|
+
* Behind `BB_RUBY_INDEX_CONSUME=1` ONLY, and FAIL-CLOSED: no context, no index, or `index.complete`
|
|
2186
|
+
* not strictly true ⇒ touches nothing and the file's output is byte-identical to the pre-index run.
|
|
2187
|
+
* What it does, in the pre-committed shape of the phase-1 sizing (4,611-row bound):
|
|
2188
|
+
* · overrides — VERIFY the first-parent guess: base type declared AND the base declares the member
|
|
2189
|
+
* (`typeKinds` + `typeMembers`, NOT counts) ⇒ `metadata.projectIndexVerified`/`ownerEvidence`,
|
|
2190
|
+
* confidence raised to structural; otherwise DECLINED with a reason (the 102 none-base rows stay
|
|
2191
|
+
* declared causes, they are not overwritten).
|
|
2192
|
+
* · instantiates/extends/mixes_in — a static constant path that the index declares gets
|
|
2193
|
+
* `metadata.projectIndexDeclared` (module existence is all mixes_in needs; typeBases is not read).
|
|
2194
|
+
* · calls — NOT TOUCHED. Name-only binding would bind decoys (the ruby D4 lesson); ruby has no
|
|
2195
|
+
* static receiver typing, so the index may not manufacture a call binding.
|
|
2196
|
+
* Declined rows are COUNTED BY REASON in `metadata.projectIndex.declined`, never merged into a result.
|
|
2197
|
+
*/
|
|
2198
|
+
applyProjectIndexFacts(relationships, context) {
|
|
2199
|
+
if (process.env.BB_RUBY_INDEX_CONSUME !== "1") return void 0;
|
|
2200
|
+
const index = context?.projectIndex;
|
|
2201
|
+
if (!index || index.complete !== true) return void 0;
|
|
2202
|
+
const leafOf = (name) => {
|
|
2203
|
+
const at = name.lastIndexOf("::");
|
|
2204
|
+
return at >= 0 ? name.slice(at + 2) : name;
|
|
2205
|
+
};
|
|
2206
|
+
const hasType = (name) => index.typeKinds.has(name) || index.typeKinds.has(leafOf(name));
|
|
2207
|
+
const membersOf = (name) => index.typeMembers.get(name) ?? index.typeMembers.get(leafOf(name));
|
|
2208
|
+
let verifiedOverrides = 0;
|
|
2209
|
+
let declaredTargets = 0;
|
|
2210
|
+
const declined = {};
|
|
2211
|
+
const decline = (reason) => {
|
|
2212
|
+
declined[reason] = (declined[reason] ?? 0) + 1;
|
|
2213
|
+
};
|
|
2214
|
+
for (const r of relationships) {
|
|
2215
|
+
const type = String(r.type ?? "");
|
|
2216
|
+
const md = r.metadata ??= {};
|
|
2217
|
+
if (type === "overrides") {
|
|
2218
|
+
const target = String(r.target ?? "");
|
|
2219
|
+
const dot = target.lastIndexOf(".");
|
|
2220
|
+
if (dot <= 0) {
|
|
2221
|
+
decline("overrideTargetUnnameable");
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
const base = target.slice(0, dot);
|
|
2225
|
+
const method = target.slice(dot + 1);
|
|
2226
|
+
if (!hasType(base)) {
|
|
2227
|
+
decline("baseNotDeclared");
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
const members = membersOf(base);
|
|
2231
|
+
if (!members || !members.has(method)) {
|
|
2232
|
+
decline("memberNotOnBase");
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
md.projectIndexVerified = true;
|
|
2236
|
+
md.ownerEvidence = "projectIndex";
|
|
2237
|
+
r.confidence = Math.max(r.confidence ?? 0, 0.95);
|
|
2238
|
+
verifiedOverrides++;
|
|
2239
|
+
} else if (type === "instantiates" || type === "extends" || type === "mixes_in") {
|
|
2240
|
+
if (!hasType(String(r.target ?? ""))) {
|
|
2241
|
+
decline(`${type}:typeNotDeclared`);
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
md.projectIndexDeclared = true;
|
|
2245
|
+
declaredTargets++;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return {
|
|
2249
|
+
complete: true,
|
|
2250
|
+
verifiedOverrides,
|
|
2251
|
+
declaredTargets,
|
|
2252
|
+
callsUntouched: true,
|
|
2253
|
+
declined
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
classifyEvidenceTier(relationships) {
|
|
2257
|
+
for (const r of relationships) {
|
|
2258
|
+
const type = r.type ?? r.relationshipType ?? "";
|
|
2259
|
+
const md = r.metadata ?? {};
|
|
2260
|
+
if (type === "contains") {
|
|
2261
|
+
md.evidenceClass = "structural";
|
|
2262
|
+
continue;
|
|
2263
|
+
}
|
|
2264
|
+
if (type === "imports" || type === "references") {
|
|
2265
|
+
md.evidenceClass = "import_based";
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
if (type === "exports") {
|
|
2269
|
+
md.evidenceClass = "structural";
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
if (type === "calls" || type === "uses") {
|
|
2273
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.85 ? "usage_contextual" : "inferred";
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
if (type === "instantiates" || type === "extends" || type === "implements" || type === "mixes_in") {
|
|
2277
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.9 ? "structural" : "inferred";
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
if (type === "overrides") {
|
|
2281
|
+
md.evidenceClass = (r.confidence ?? 0) >= 0.9 ? "structural" : "inferred";
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
md.evidenceClass = "usage_contextual";
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* THE STATIC SELF-TYPE AT A CALL SITE — ruby's analogue of dart's "declared return type".
|
|
2289
|
+
*
|
|
2290
|
+
* `include`, `extend` and `prepend` are Module's methods, so a call with one of those names
|
|
2291
|
+
* composes a mixin into the ENCLOSING CLASS only if `self` IS that class at that point. The
|
|
2292
|
+
* static self-type is a NODE-TYPE question, never a name question:
|
|
2293
|
+
*
|
|
2294
|
+
* · a class / module / `class << self` body → "class" (`self` IS the class)
|
|
2295
|
+
* · a `Class.new { }` / `Module.new { }` block → "class" (the block is class_eval'd, so
|
|
2296
|
+
* `self` is the NEW class whatever encloses it lexically — and the enclosing call is the
|
|
2297
|
+
* block's PARENT, because the block node's own first child is the first statement of its body)
|
|
2298
|
+
* · a `singleton_method` node (`def self.x`, `def x.y`) → "class" (`self` IS the class)
|
|
2299
|
+
* · a plain `method` node (`def x`) → "instance" (`self` IS an INSTANCE)
|
|
2300
|
+
* · nothing enclosing it → "top" (`self` IS `main`)
|
|
2301
|
+
*
|
|
2302
|
+
* ⇒ MEASURED over the whole admitted population (989 files): of the 543 published `mixes_in` rows,
|
|
2303
|
+
* 416 sit where `self` is the class, 29 in a singleton method, 18 in a plain instance method, and
|
|
2304
|
+
* 80 at the file's top level. **⇒ THE LAST TWO CLASSES ARE IMPOSTORS: 127 rows, 23.4 %.** The 80
|
|
2305
|
+
* top-level rows are RSpec's example-group `include` — the same NAME, a different construct.
|
|
2306
|
+
*/
|
|
2307
|
+
mixinSelfKind(node) {
|
|
2308
|
+
let cur = node.parent ?? null;
|
|
2309
|
+
while (cur) {
|
|
2310
|
+
if (cur.type === "method") return "instance";
|
|
2311
|
+
if (cur.type === "singleton_method") return "singleton";
|
|
2312
|
+
if (cur.type === "class" || cur.type === "module" || cur.type === "singleton_class") return "class";
|
|
2313
|
+
if (cur.type === "do_block" || cur.type === "block") {
|
|
2314
|
+
const p = cur.parent;
|
|
2315
|
+
if (p && p.type === "call") {
|
|
2316
|
+
const recv = p.childForFieldName?.("receiver");
|
|
2317
|
+
const meth = p.childForFieldName?.("method");
|
|
2318
|
+
const rt = recv ? recv.text : "";
|
|
2319
|
+
const mt = meth ? meth.text : "";
|
|
2320
|
+
if ((rt === "Class" || rt === "Module") && mt === "new") return "class";
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
cur = cur.parent ?? null;
|
|
2324
|
+
}
|
|
2325
|
+
return "top";
|
|
2326
|
+
}
|
|
2327
|
+
findChild(node, type) {
|
|
2328
|
+
const _nc25 = node.namedChildCount;
|
|
2329
|
+
for (let i = 0; i < _nc25; i++) {
|
|
2330
|
+
const child = node.namedChild(i);
|
|
2331
|
+
if (child && child.type === type) return child;
|
|
2332
|
+
}
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
/** Strip leading ': ' from symbol text — pure char scan */
|
|
2336
|
+
/**
|
|
2337
|
+
* Record a detected framework EXACTLY ONCE, and publish the primary pick (#749).
|
|
2338
|
+
*
|
|
2339
|
+
* Both accumulators are arrays published into the graph, and every caller runs once
|
|
2340
|
+
* per class and once per call, so a bare `.push` published one entry per trigger hit:
|
|
2341
|
+
* measured, `["Ruby on Rails", "Ruby on Rails", …]` five times on a five-model file
|
|
2342
|
+
* and three times on `rack/test.rb`. `metadata.framework` — the contract's
|
|
2343
|
+
* deterministic primary pick — was published for nothing at all, so every file that
|
|
2344
|
+
* DID detect Rails read as framework-less downstream.
|
|
2345
|
+
*/
|
|
2346
|
+
recordFramework(metadata, name, pattern) {
|
|
2347
|
+
const detected = metadata.detectedFrameworks ??= [];
|
|
2348
|
+
if (!detected.includes(name)) detected.push(name);
|
|
2349
|
+
metadata.framework ??= name;
|
|
2350
|
+
if (!pattern) return;
|
|
2351
|
+
const patterns = metadata.frameworkPatterns ??= [];
|
|
2352
|
+
if (!patterns.some((p) => p.framework === name)) patterns.push({ framework: name, patterns: [pattern] });
|
|
2353
|
+
}
|
|
2354
|
+
stripSymbolColon(text) {
|
|
2355
|
+
let start = 0;
|
|
2356
|
+
while (start < text.length && text[start] === ":") start++;
|
|
2357
|
+
return start < text.length ? text.substring(start) : text;
|
|
2358
|
+
}
|
|
2359
|
+
/** Strip surrounding quotes — pure char scan */
|
|
2360
|
+
stripQuotes(text) {
|
|
2361
|
+
if (text.length >= 2) {
|
|
2362
|
+
const first = text.charCodeAt(0);
|
|
2363
|
+
const last = text.charCodeAt(text.length - 1);
|
|
2364
|
+
if (first === 34 && last === 34 || first === 39 && last === 39) {
|
|
2365
|
+
return text.substring(1, text.length - 1);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return text;
|
|
2369
|
+
}
|
|
2370
|
+
/** Extract the leaf name from a possibly-qualified name like A::B */
|
|
2371
|
+
leafName(name) {
|
|
2372
|
+
const idx = name.lastIndexOf("::");
|
|
2373
|
+
return idx >= 0 ? name.substring(idx + 2) : name;
|
|
2374
|
+
}
|
|
2375
|
+
isStaticSuperclassName(text) {
|
|
2376
|
+
return isRubyStaticSuperclassName(text);
|
|
2377
|
+
}
|
|
2378
|
+
isRailsModelName(superClass) {
|
|
2379
|
+
if (!superClass) return false;
|
|
2380
|
+
const leaf = this.leafName(superClass);
|
|
2381
|
+
return leaf === "Base" && superClass.includes("ActiveRecord") || leaf === "ApplicationRecord";
|
|
2382
|
+
}
|
|
2383
|
+
isRailsControllerName(superClass) {
|
|
2384
|
+
if (!superClass) return false;
|
|
2385
|
+
const leaf = this.leafName(superClass);
|
|
2386
|
+
return leaf === "Base" && superClass.includes("ActionController") || leaf === "ApplicationController";
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* M59.A7 (#825): the 1-based line of the node that NAMES an edge's target.
|
|
2390
|
+
*
|
|
2391
|
+
* The contract is the family's, not this file's: a relationship's `line_number`
|
|
2392
|
+
* is the line where the TARGET is written — the callee for a call, the read for
|
|
2393
|
+
* a member use, the constructed type for an instantiation, the symbol's own
|
|
2394
|
+
* declaration for contains/overrides. It is checked independently by
|
|
2395
|
+
* script/m59/oracle-callsite.ts, which re-derives every edge from the tree and
|
|
2396
|
+
* requires the target token to appear on the claimed line.
|
|
2397
|
+
*
|
|
2398
|
+
* Two shapes are traps and both were measured on the python lane first:
|
|
2399
|
+
* * a call's node starts at its RECEIVER, so `items.strip.upper` stamped the
|
|
2400
|
+
* whole call on the `items` line — hence the method field's node below,
|
|
2401
|
+
* which in this grammar IS the leaf that names the callee;
|
|
2402
|
+
* * `undefined` is a first-class answer. A node that does not exist (a dynamic
|
|
2403
|
+
* mixin, a computed callee) contributes NO line rather than a fabricated 0,
|
|
2404
|
+
* which is the rule `0 means unknown and unknown must be absent` (#820/#821)
|
|
2405
|
+
* applied to this field.
|
|
2406
|
+
*/
|
|
2407
|
+
nodeLine(node) {
|
|
2408
|
+
if (!node) return void 0;
|
|
2409
|
+
return node.startPosition.row + 1;
|
|
2410
|
+
}
|
|
2411
|
+
nodeToRange(node) {
|
|
2412
|
+
return {
|
|
2413
|
+
startLine: node.startPosition.row + 1,
|
|
2414
|
+
startColumn: node.startPosition.column + 1,
|
|
2415
|
+
endLine: node.endPosition.row + 1,
|
|
2416
|
+
endColumn: node.endPosition.column + 1,
|
|
2417
|
+
startIndex: node.startIndex,
|
|
2418
|
+
endIndex: node.endIndex
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
export {
|
|
2423
|
+
RAILS_MACRO_METHODS,
|
|
2424
|
+
RUBY_BUILTINS,
|
|
2425
|
+
RealRubyAnalyzer,
|
|
2426
|
+
isRubyIdentifierShaped,
|
|
2427
|
+
isRubyInstanceVariableWrite,
|
|
2428
|
+
isRubyStaticSuperclassName
|
|
2429
|
+
};
|