@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,2596 @@
|
|
|
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-r-analyzer.ts
|
|
12
|
+
var LEFT_BINDING_OPERATORS = /* @__PURE__ */ new Set(["<-", "<<-", "="]);
|
|
13
|
+
var RIGHT_BINDING_OPERATORS = /* @__PURE__ */ new Set(["->", "->>"]);
|
|
14
|
+
var ASSIGNMENT_OPERATORS = /* @__PURE__ */ new Set([...LEFT_BINDING_OPERATORS, ...RIGHT_BINDING_OPERATORS]);
|
|
15
|
+
var NAMESPACE_OPERATORS = /* @__PURE__ */ new Set(["::", ":::"]);
|
|
16
|
+
var APPLY_FAMILY = /* @__PURE__ */ new Set([
|
|
17
|
+
"apply",
|
|
18
|
+
"sapply",
|
|
19
|
+
"lapply",
|
|
20
|
+
"vapply",
|
|
21
|
+
"mapply",
|
|
22
|
+
"rapply",
|
|
23
|
+
"tapply",
|
|
24
|
+
"Map",
|
|
25
|
+
"mapply",
|
|
26
|
+
"Reduce",
|
|
27
|
+
"Filter",
|
|
28
|
+
"Negate"
|
|
29
|
+
]);
|
|
30
|
+
var TIDYVERSE_NSE = /* @__PURE__ */ new Set([
|
|
31
|
+
"mutate",
|
|
32
|
+
"select",
|
|
33
|
+
"filter",
|
|
34
|
+
"summarise",
|
|
35
|
+
"summarize",
|
|
36
|
+
"group_by",
|
|
37
|
+
"arrange",
|
|
38
|
+
"aes",
|
|
39
|
+
"vars",
|
|
40
|
+
"transmute",
|
|
41
|
+
"distinct",
|
|
42
|
+
"slice",
|
|
43
|
+
"pull",
|
|
44
|
+
"rename",
|
|
45
|
+
"relocate"
|
|
46
|
+
]);
|
|
47
|
+
var REACTIVE_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
48
|
+
"reactive",
|
|
49
|
+
"observe",
|
|
50
|
+
"observeEvent",
|
|
51
|
+
"eventReactive",
|
|
52
|
+
"reactiveVal",
|
|
53
|
+
"reactiveValues",
|
|
54
|
+
"reactiveExpr",
|
|
55
|
+
"reactivePoll",
|
|
56
|
+
"reactiveFileReader",
|
|
57
|
+
"bindCache",
|
|
58
|
+
"freezeReactiveValue"
|
|
59
|
+
]);
|
|
60
|
+
var STATISTICAL_MODELS = /* @__PURE__ */ new Set(["lm", "glm", "summary", "glm.nb", "MASS::rlm"]);
|
|
61
|
+
var DATA_FRAME_CONSTRUCTORS = /* @__PURE__ */ new Set(["data.frame", "tibble", "as.data.frame", "data.table"]);
|
|
62
|
+
var ERROR_HANDLERS = /* @__PURE__ */ new Set(["tryCatch", "withCallingHandlers", "try", "tryCatch"]);
|
|
63
|
+
var CLEANUP_HANDLERS = /* @__PURE__ */ new Set(["on.exit", "onLoad", "onUnload"]);
|
|
64
|
+
var NSE_CAPTURERS = /* @__PURE__ */ new Set([
|
|
65
|
+
"substitute",
|
|
66
|
+
"quote",
|
|
67
|
+
"bquote",
|
|
68
|
+
"match.call",
|
|
69
|
+
"match.arg",
|
|
70
|
+
"enquo",
|
|
71
|
+
"enquos",
|
|
72
|
+
"expr",
|
|
73
|
+
"enexpr",
|
|
74
|
+
"eval",
|
|
75
|
+
"rlang::enquo",
|
|
76
|
+
"ensym",
|
|
77
|
+
"enname",
|
|
78
|
+
"quo",
|
|
79
|
+
"quos"
|
|
80
|
+
]);
|
|
81
|
+
var CLASS_MEMBER_LISTS = /* @__PURE__ */ new Set(["public", "private", "active"]);
|
|
82
|
+
var R5_MEMBER_LISTS = /* @__PURE__ */ new Set(["fields", "methods"]);
|
|
83
|
+
var DATA_MEMBER_LISTS = /* @__PURE__ */ new Set(["fields"]);
|
|
84
|
+
var PACKAGE_NAMESPACE_SUBTYPES = /* @__PURE__ */ new Set([
|
|
85
|
+
"library_import",
|
|
86
|
+
"library_dependency",
|
|
87
|
+
"namespace_access"
|
|
88
|
+
]);
|
|
89
|
+
function isPackageNameShape(name) {
|
|
90
|
+
if (name.length === 0) return false;
|
|
91
|
+
const first = name.charCodeAt(0);
|
|
92
|
+
const firstIsLetter = first >= 65 && first <= 90 || first >= 97 && first <= 122;
|
|
93
|
+
if (!firstIsLetter) return false;
|
|
94
|
+
if (name.length === 1) return true;
|
|
95
|
+
const last = name.charCodeAt(name.length - 1);
|
|
96
|
+
const lastIsAlnum = last >= 65 && last <= 90 || last >= 97 && last <= 122 || last >= 48 && last <= 57;
|
|
97
|
+
if (!lastIsAlnum) return false;
|
|
98
|
+
for (let i = 1; i < name.length - 1; i++) {
|
|
99
|
+
const c = name.charCodeAt(i);
|
|
100
|
+
const ok = c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 46;
|
|
101
|
+
if (!ok) return false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
var PACKAGE_NAMESPACE_LABELS = /* @__PURE__ */ new Set([
|
|
106
|
+
ASTEngine.RelationshipType.Imports,
|
|
107
|
+
ASTEngine.RelationshipType.References
|
|
108
|
+
]);
|
|
109
|
+
var RealRAnalyzer = class {
|
|
110
|
+
/**
|
|
111
|
+
* UTF-16 code-unit index -> byte offset, set for the duration of an `analyze` call and
|
|
112
|
+
* cleared on return, so that every `getRange` call populates `byteStart`/`byteEnd`
|
|
113
|
+
* without each of them having to be handed the source text.
|
|
114
|
+
*/
|
|
115
|
+
byteOffsetOf = null;
|
|
116
|
+
analyze(rootNode, content, filePath) {
|
|
117
|
+
const symbols = [];
|
|
118
|
+
const imports = [];
|
|
119
|
+
const exports = [];
|
|
120
|
+
const relationships = [];
|
|
121
|
+
const declaredTypes = /* @__PURE__ */ new Set();
|
|
122
|
+
const metadata = {
|
|
123
|
+
detectedFrameworks: [],
|
|
124
|
+
patterns: [],
|
|
125
|
+
architecturalInsights: [],
|
|
126
|
+
cyclomaticComplexity: 1,
|
|
127
|
+
linesOfCode: 0,
|
|
128
|
+
dataManipulationIntensity: 0,
|
|
129
|
+
statisticalFocus: 0,
|
|
130
|
+
codeQuality: "excellent",
|
|
131
|
+
architecturalScore: 100
|
|
132
|
+
};
|
|
133
|
+
if (!rootNode) return { symbols, imports, errors: [] };
|
|
134
|
+
const nonAscii = [];
|
|
135
|
+
const surplus = [];
|
|
136
|
+
for (let i = 0, extra = 0; i < content.length; i++) {
|
|
137
|
+
const c = content.charCodeAt(i);
|
|
138
|
+
if (c < 128) continue;
|
|
139
|
+
const cp = c >= 55296 && c <= 56319 ? content.codePointAt(i) : c;
|
|
140
|
+
const units = cp > 65535 ? 2 : 1;
|
|
141
|
+
const bytes = cp <= 127 ? 1 : cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
|
|
142
|
+
nonAscii.push(i);
|
|
143
|
+
surplus.push(extra);
|
|
144
|
+
extra += bytes - units;
|
|
145
|
+
if (units === 2) i++;
|
|
146
|
+
}
|
|
147
|
+
const byteOffset = (utf16Index) => {
|
|
148
|
+
if (nonAscii.length === 0) return utf16Index;
|
|
149
|
+
let lo = 0;
|
|
150
|
+
let hi = nonAscii.length - 1;
|
|
151
|
+
let lastBefore = -1;
|
|
152
|
+
while (lo <= hi) {
|
|
153
|
+
const mid = lo + hi >> 1;
|
|
154
|
+
if (nonAscii[mid] <= utf16Index) {
|
|
155
|
+
lastBefore = mid;
|
|
156
|
+
lo = mid + 1;
|
|
157
|
+
} else {
|
|
158
|
+
hi = mid - 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return lastBefore < 0 ? utf16Index : utf16Index + surplus[lastBefore];
|
|
162
|
+
};
|
|
163
|
+
this.byteOffsetOf = byteOffset;
|
|
164
|
+
const symbolLine = /* @__PURE__ */ new Map();
|
|
165
|
+
const memberLine = /* @__PURE__ */ new Map();
|
|
166
|
+
const classDecls = /* @__PURE__ */ new Map();
|
|
167
|
+
const ambiguity = {
|
|
168
|
+
callsUnresolvedScope: 0,
|
|
169
|
+
callsAttributedThroughClosure: 0,
|
|
170
|
+
heuristicS3Generic: 0,
|
|
171
|
+
// A call whose callee is not a name at all -- `(if (uplo == "U") triu else
|
|
172
|
+
// tril)(x)`. Counted, because the alternative is that these vanish from the totals
|
|
173
|
+
// and a reader cannot tell "no such calls" from "not measured".
|
|
174
|
+
callsToUnnameableCallee: 0,
|
|
175
|
+
// `uses` accounting. The three numbers the `uses` surface cannot hide behind:
|
|
176
|
+
// a member read already carried by `calls`, a member WRITE, and a read with no
|
|
177
|
+
// enclosing named symbol to attribute to. Each is counted rather than dropped,
|
|
178
|
+
// for the reason the counter above states.
|
|
179
|
+
memberReadsSuppressedByCall: 0,
|
|
180
|
+
memberWrites: 0,
|
|
181
|
+
memberReadsUnresolvedScope: 0,
|
|
182
|
+
memberReadsWithComputedIndex: 0,
|
|
183
|
+
// `overrides` accounting, for the same reason: the pass declines to assert an edge
|
|
184
|
+
// when no EARLIER declaration of the base name exists in the file, and the count of
|
|
185
|
+
// those declines is otherwise indistinguishable from "there was nothing to inherit".
|
|
186
|
+
overridesEdgesEmitted: 0,
|
|
187
|
+
overridesBasesNotDeclaredEarlierInFile: 0
|
|
188
|
+
};
|
|
189
|
+
if (rootNode.hasError) {
|
|
190
|
+
const errStack = [rootNode];
|
|
191
|
+
while (errStack.length > 0) {
|
|
192
|
+
const n = errStack.pop();
|
|
193
|
+
if (n.type === "ERROR" || n.isMissing) {
|
|
194
|
+
if (!metadata.errors) metadata.errors = [];
|
|
195
|
+
metadata.errors.push({
|
|
196
|
+
message: n.type === "ERROR" ? `Unrecoverable syntax region: tree-sitter-r could not parse this subtree. Extraction continued on a best-effort basis, so symbols and relationships inside this range may be missing.` : `Missing ${n.type} token: the source is syntactically incomplete at this point. Extraction continued on a best-effort basis.`,
|
|
197
|
+
range: this.getRange(n),
|
|
198
|
+
severity: "warning"
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
for (let i = n.namedChildCount - 1; i >= 0; i--) {
|
|
202
|
+
const child = n.namedChild(i);
|
|
203
|
+
if (child) errStack.push(child);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
let loc = 1;
|
|
208
|
+
for (let i = 0; i < content.length; i++) {
|
|
209
|
+
if (content[i] === "\n") loc++;
|
|
210
|
+
}
|
|
211
|
+
metadata.linesOfCode = loc;
|
|
212
|
+
const EXIT_MARKER = /* @__PURE__ */ Symbol("r-exit");
|
|
213
|
+
const stack = [rootNode];
|
|
214
|
+
const fnStack = [];
|
|
215
|
+
while (stack.length > 0) {
|
|
216
|
+
const item = stack.pop();
|
|
217
|
+
if (item === EXIT_MARKER) {
|
|
218
|
+
fnStack.pop();
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!item) continue;
|
|
222
|
+
const current = item;
|
|
223
|
+
const type = current.type;
|
|
224
|
+
if (type === "binary_operator") {
|
|
225
|
+
this.handleBinaryOperator(current, symbols, exports, relationships, metadata, declaredTypes, fnStack.length > 0);
|
|
226
|
+
} else if (type === "call") {
|
|
227
|
+
this.handleCall(current, symbols, imports, exports, relationships, metadata, declaredTypes, ambiguity, fnStack);
|
|
228
|
+
} else if (type === "comment") {
|
|
229
|
+
this.handleComment(current, metadata);
|
|
230
|
+
} else if (type === "function_definition") {
|
|
231
|
+
const fnParent = current.parent;
|
|
232
|
+
const isNestedWrapper = fnParent && fnParent.type === "function_definition";
|
|
233
|
+
const isAssignmentTarget = fnParent && fnParent.type === "binary_operator" && this.assignmentTarget(fnParent) === current;
|
|
234
|
+
if (!isNestedWrapper && !isAssignmentTarget) {
|
|
235
|
+
this.pushIfUnique(metadata.patterns, "Anonymous Functions");
|
|
236
|
+
}
|
|
237
|
+
fnStack.push({ node: current, attribution: this.resolveFunctionOwner(current) });
|
|
238
|
+
stack.push(EXIT_MARKER);
|
|
239
|
+
} else if (type === "subset" || type === "subset2") {
|
|
240
|
+
this.pushIfUnique(metadata.patterns, "Vectorized Subsetting");
|
|
241
|
+
if (type === "subset2" && this.stringSubsetIndex(current)) {
|
|
242
|
+
this.handleMemberRead(current, fnStack, relationships, ambiguity);
|
|
243
|
+
}
|
|
244
|
+
} else if (type === "extract_operator") {
|
|
245
|
+
this.handleMemberRead(current, fnStack, relationships, ambiguity);
|
|
246
|
+
} else if (type === "unary_operator" && current.childForFieldName("operator")?.text === "~") {
|
|
247
|
+
this.pushIfUnique(metadata.patterns, "Formula Interface");
|
|
248
|
+
if (metadata.statisticalFocus !== void 0) metadata.statisticalFocus += 10;
|
|
249
|
+
}
|
|
250
|
+
if (type === "if_statement" || type === "for_statement" || type === "while_statement" || type === "repeat_statement") {
|
|
251
|
+
if (metadata.cyclomaticComplexity !== void 0) {
|
|
252
|
+
metadata.cyclomaticComplexity++;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (type === "if_statement" && this.isStaticallyFalseCondition(current)) {
|
|
256
|
+
const elseBranch = current.childForFieldName("alternative");
|
|
257
|
+
if (elseBranch) {
|
|
258
|
+
for (let i = elseBranch.childCount - 1; i >= 0; i--) {
|
|
259
|
+
const c = elseBranch.child(i);
|
|
260
|
+
if (c) stack.push(c);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
for (let i = current.childCount - 1; i >= 0; i--) {
|
|
266
|
+
const child = current.child(i);
|
|
267
|
+
if (child) {
|
|
268
|
+
stack.push(child);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const s of symbols) {
|
|
273
|
+
if (!symbolLine.has(s.name)) symbolLine.set(s.name, this.nameLineOf(s, content));
|
|
274
|
+
if (s.kind !== ASTEngine.SymbolKind.Class) continue;
|
|
275
|
+
const line = s.range.startLine;
|
|
276
|
+
const bases = s.metadata?.baseClasses ?? [];
|
|
277
|
+
const parent = s.metadata?.parent;
|
|
278
|
+
const all = Array.isArray(parent) ? parent : bases;
|
|
279
|
+
const decls = classDecls.get(s.name) ?? [];
|
|
280
|
+
decls.push({ line, endLine: s.range.endLine, members: /* @__PURE__ */ new Map(), bases: all });
|
|
281
|
+
classDecls.set(s.name, decls);
|
|
282
|
+
}
|
|
283
|
+
for (const decls of classDecls.values()) decls.sort((a, b) => a.line - b.line);
|
|
284
|
+
for (const s of symbols) {
|
|
285
|
+
if (!s.ownerName) continue;
|
|
286
|
+
if (s.kind !== ASTEngine.SymbolKind.Method && s.kind !== ASTEngine.SymbolKind.Property) continue;
|
|
287
|
+
const line = this.nameLineOf(s, content);
|
|
288
|
+
const decls = classDecls.get(s.ownerName);
|
|
289
|
+
if (!decls?.length) continue;
|
|
290
|
+
let owner = decls.find((d) => line >= d.line && line <= d.endLine);
|
|
291
|
+
if (!owner) {
|
|
292
|
+
const prior = decls.filter((d) => d.line < line);
|
|
293
|
+
owner = prior.length ? prior[prior.length - 1] : decls[0];
|
|
294
|
+
}
|
|
295
|
+
if (!owner.members.has(s.name)) owner.members.set(s.name, line);
|
|
296
|
+
}
|
|
297
|
+
for (const e of exports) {
|
|
298
|
+
this.pushRel(
|
|
299
|
+
relationships,
|
|
300
|
+
"global",
|
|
301
|
+
e.name,
|
|
302
|
+
ASTEngine.RelationshipType.Exports,
|
|
303
|
+
1,
|
|
304
|
+
{ rSpecific: true, relationshipSubType: "roxygen_export", declaredKind: e.kind },
|
|
305
|
+
symbolLine.get(e.name) ?? null
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
let overridesEdges = 0;
|
|
309
|
+
let overridesBasesUnresolved = 0;
|
|
310
|
+
for (const [className, decls] of classDecls) {
|
|
311
|
+
for (const decl of decls) {
|
|
312
|
+
if (!decl.members.size || !decl.bases.length) continue;
|
|
313
|
+
for (const base of decl.bases) {
|
|
314
|
+
const prior = (classDecls.get(base) ?? []).filter((d) => d.line < decl.line);
|
|
315
|
+
const baseDecl = prior.length ? prior[prior.length - 1] : null;
|
|
316
|
+
if (!baseDecl) {
|
|
317
|
+
overridesBasesUnresolved++;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
for (const member of baseDecl.members.keys()) {
|
|
321
|
+
if (!decl.members.has(member)) continue;
|
|
322
|
+
overridesEdges++;
|
|
323
|
+
this.pushRel(relationships, className, member, ASTEngine.RelationshipType.Overrides, 0.95, {
|
|
324
|
+
rSpecific: true,
|
|
325
|
+
relationshipSubType: "object_system_method_override",
|
|
326
|
+
overridingMember: `${className}$${member}`,
|
|
327
|
+
overriddenMember: `${base}$${member}`,
|
|
328
|
+
overriddenClass: base,
|
|
329
|
+
reasonCode: "base_class_declared_in_same_file",
|
|
330
|
+
reason: `\`${className}$${member}\` shadows \`${base}$${member}\`, whose class is declared earlier in this file, so the shadowing member is provably an override`
|
|
331
|
+
// The OVERRIDING declaration's line, not the overridden one's. Taking the
|
|
332
|
+
// line out of `baseDecl` instead of `decl` put `BC -overrides-> x` on AC's
|
|
333
|
+
// `x` at R6/tests/testthat/test-portable-inheritance.R:194, four lines
|
|
334
|
+
// above the `BC` that declares it.
|
|
335
|
+
}, decl.members.get(member));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
ambiguity.overridesBasesNotDeclaredEarlierInFile = overridesBasesUnresolved;
|
|
341
|
+
ambiguity.overridesEdgesEmitted = overridesEdges;
|
|
342
|
+
this.detectFrameworksFromImports(imports, metadata);
|
|
343
|
+
const containsKeys = /* @__PURE__ */ new Set();
|
|
344
|
+
for (const rel of relationships) {
|
|
345
|
+
if (rel.type === ASTEngine.RelationshipType.Contains && rel.source && rel.target) {
|
|
346
|
+
containsKeys.add(`${rel.source}${rel.target}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const s of symbols) {
|
|
350
|
+
const _owner = s.ownerName || s.scope || "global";
|
|
351
|
+
if (_owner) {
|
|
352
|
+
const key = `${_owner}${s.name}`;
|
|
353
|
+
if (containsKeys.has(key)) continue;
|
|
354
|
+
containsKeys.add(key);
|
|
355
|
+
this.pushRel(
|
|
356
|
+
relationships,
|
|
357
|
+
_owner,
|
|
358
|
+
s.name,
|
|
359
|
+
ASTEngine.RelationshipType.Contains,
|
|
360
|
+
1,
|
|
361
|
+
{},
|
|
362
|
+
symbolLine.get(s.name) ?? s.range
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const uniqueRelationships = [];
|
|
367
|
+
const relationshipKeys = /* @__PURE__ */ new Set();
|
|
368
|
+
for (const rel of relationships) {
|
|
369
|
+
const key = `${rel.type || rel.relationshipType || ""}${rel.source || rel.from || ""}${rel.target || rel.to || ""}`;
|
|
370
|
+
if (relationshipKeys.has(key)) continue;
|
|
371
|
+
relationshipKeys.add(key);
|
|
372
|
+
uniqueRelationships.push(rel);
|
|
373
|
+
}
|
|
374
|
+
this.certifyPackageNamespaces(uniqueRelationships, symbols);
|
|
375
|
+
const overridesInResult = uniqueRelationships.filter(
|
|
376
|
+
(r) => (r.type || r.relationshipType) === ASTEngine.RelationshipType.Overrides
|
|
377
|
+
).length;
|
|
378
|
+
const uniqueImports = [];
|
|
379
|
+
const importKeys = /* @__PURE__ */ new Set();
|
|
380
|
+
for (const imp of imports) {
|
|
381
|
+
const r = imp.range;
|
|
382
|
+
const key = `${imp.module ?? ""}${r ? `${r.startLine}:${r.startColumn}-${r.endLine}:${r.endColumn}` : ""}`;
|
|
383
|
+
if (importKeys.has(key)) continue;
|
|
384
|
+
importKeys.add(key);
|
|
385
|
+
uniqueImports.push(imp);
|
|
386
|
+
}
|
|
387
|
+
metadata.architecturalInsights = [
|
|
388
|
+
`r.ambiguity.callsUnresolvedScope=${ambiguity.callsUnresolvedScope}`,
|
|
389
|
+
`r.ambiguity.callsAttributedThroughClosure=${ambiguity.callsAttributedThroughClosure}`,
|
|
390
|
+
`r.ambiguity.heuristicS3Generic=${ambiguity.heuristicS3Generic}`
|
|
391
|
+
];
|
|
392
|
+
metadata.callAttribution = {
|
|
393
|
+
unresolvedScope: ambiguity.callsUnresolvedScope,
|
|
394
|
+
throughClosure: ambiguity.callsAttributedThroughClosure,
|
|
395
|
+
heuristicS3Generic: ambiguity.heuristicS3Generic,
|
|
396
|
+
unnameableCallee: ambiguity.callsToUnnameableCallee ?? 0,
|
|
397
|
+
memberReadsSuppressedByCall: ambiguity.memberReadsSuppressedByCall,
|
|
398
|
+
memberWrites: ambiguity.memberWrites,
|
|
399
|
+
memberReadsUnresolvedScope: ambiguity.memberReadsUnresolvedScope,
|
|
400
|
+
memberReadsWithComputedIndex: ambiguity.memberReadsWithComputedIndex,
|
|
401
|
+
// Counts what the RESULT carries, after the (type, source, target) collapse — not the
|
|
402
|
+
// emissions the pass made. See `overridesInResult` above; the two disagree by exactly the
|
|
403
|
+
// number of times two bases of one class declare the same member name.
|
|
404
|
+
overridesEdgesEmitted: overridesInResult,
|
|
405
|
+
/** The pass's raw emission count, before the collapse. */
|
|
406
|
+
overridesEmissionsBeforeDedup: ambiguity.overridesEdgesEmitted,
|
|
407
|
+
/** Emissions collapsed into an edge already present: 23 over the lane's corpus, not lost. */
|
|
408
|
+
overridesEmissionsCollapsedByDedup: ambiguity.overridesEdgesEmitted - overridesInResult,
|
|
409
|
+
// Bases named by `inherit=`/`contains=`/`parent=` that no EARLIER declaration of
|
|
410
|
+
// that name in the same file satisfies. Counted rather than dropped so a reader can
|
|
411
|
+
// see how much of the inheritance graph this pass declines to assert.
|
|
412
|
+
overridesBasesNotDeclaredEarlierInFile: ambiguity.overridesBasesNotDeclaredEarlierInFile
|
|
413
|
+
};
|
|
414
|
+
this.byteOffsetOf = null;
|
|
415
|
+
return {
|
|
416
|
+
symbols,
|
|
417
|
+
imports: uniqueImports,
|
|
418
|
+
relationships: uniqueRelationships,
|
|
419
|
+
exports,
|
|
420
|
+
metadata,
|
|
421
|
+
errors: metadata.errors || []
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
detectFrameworksFromImports(imports, metadata) {
|
|
425
|
+
let hasTidyverse = false;
|
|
426
|
+
let hasGgplot2 = false;
|
|
427
|
+
let hasShiny = false;
|
|
428
|
+
let hasPlumber = false;
|
|
429
|
+
let hasDataTable = false;
|
|
430
|
+
let hasRcpp = false;
|
|
431
|
+
for (let i = 0; i < imports.length; i++) {
|
|
432
|
+
const module = imports[i].module;
|
|
433
|
+
if (module === "dplyr" || module === "ggplot2" || module === "tidyr" || module === "tidyverse")
|
|
434
|
+
hasTidyverse = true;
|
|
435
|
+
if (module === "ggplot2") hasGgplot2 = true;
|
|
436
|
+
if (module === "shiny") hasShiny = true;
|
|
437
|
+
if (module === "plumber") hasPlumber = true;
|
|
438
|
+
if (module === "data.table") hasDataTable = true;
|
|
439
|
+
if (module === "Rcpp") hasRcpp = true;
|
|
440
|
+
}
|
|
441
|
+
if (hasTidyverse) this.pushIfUnique(metadata.detectedFrameworks, "Tidyverse");
|
|
442
|
+
if (hasGgplot2) this.pushIfUnique(metadata.detectedFrameworks, "ggplot2");
|
|
443
|
+
if (hasShiny) {
|
|
444
|
+
this.pushIfUnique(metadata.detectedFrameworks, "Shiny");
|
|
445
|
+
if (!metadata.frameworkPatterns) metadata.frameworkPatterns = [];
|
|
446
|
+
metadata.frameworkPatterns.push({ framework: "Shiny", pattern: "ui/server" });
|
|
447
|
+
}
|
|
448
|
+
if (hasPlumber) this.pushIfUnique(metadata.detectedFrameworks, "Plumber");
|
|
449
|
+
if (hasDataTable) this.pushIfUnique(metadata.detectedFrameworks, "data.table");
|
|
450
|
+
if (hasRcpp) this.pushIfUnique(metadata.detectedFrameworks, "Rcpp");
|
|
451
|
+
}
|
|
452
|
+
pushIfUnique(arr, value) {
|
|
453
|
+
if (!arr) return;
|
|
454
|
+
let exists = false;
|
|
455
|
+
for (let i = 0; i < arr.length; i++) {
|
|
456
|
+
if (arr[i] === value) {
|
|
457
|
+
exists = true;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!exists) arr.push(value);
|
|
462
|
+
}
|
|
463
|
+
// ---------------------------------------------------------------- assignments
|
|
464
|
+
/**
|
|
465
|
+
* The two SIDES of an assignment, without deciding which is a declaration.
|
|
466
|
+
*
|
|
467
|
+
* `<-`/`<<-`/`=` bind their LEFT operand. `->`/`->>` bind their RIGHT operand and
|
|
468
|
+
* nothing else — the left side is always a VALUE.
|
|
469
|
+
*
|
|
470
|
+
* This is not a stylistic choice, it is what R does, and it was measured
|
|
471
|
+
* against R 4.4.3's own runtime rather than assumed:
|
|
472
|
+
*
|
|
473
|
+
* 2 -> b OK, binds b
|
|
474
|
+
* 2 ->> cc OK, binds cc
|
|
475
|
+
* identity(1) -> gg OK, binds gg
|
|
476
|
+
* bb -> 2 Error: invalid (do_set) left-hand side to assignment
|
|
477
|
+
* cc ->> 2 Error: invalid (do_set) left-hand side to assignment
|
|
478
|
+
*
|
|
479
|
+
* R desugars both rightward forms to `<-`(value, name) at parse time, so a
|
|
480
|
+
* name on the LEFT of `->` is not a declaration — it is a runtime error, and
|
|
481
|
+
* publishing a symbol for it would invent a binding R refuses to make.
|
|
482
|
+
* tree-sitter-r does NOT desugar (it keeps `->` as an operator), which is
|
|
483
|
+
* exactly why the two sides have to be distinguished here.
|
|
484
|
+
*
|
|
485
|
+
* An earlier revision of this analyzer read whichever side happened to be an
|
|
486
|
+
* identifier. That published `variable e` for `e -> 6` — a construct R
|
|
487
|
+
* rejects. Caught by the R oracle, not by the fixtures, because the fixture
|
|
488
|
+
* had been written to match the same wrong belief.
|
|
489
|
+
*/
|
|
490
|
+
assignmentSides(node) {
|
|
491
|
+
const operator = node.childForFieldName("operator")?.text;
|
|
492
|
+
if (operator === void 0) return { target: null, value: null };
|
|
493
|
+
const lhs = node.childForFieldName("lhs");
|
|
494
|
+
const rhs = node.childForFieldName("rhs");
|
|
495
|
+
if (LEFT_BINDING_OPERATORS.has(operator)) return { target: lhs ?? null, value: rhs ?? null };
|
|
496
|
+
if (RIGHT_BINDING_OPERATORS.has(operator)) return { target: rhs ?? null, value: lhs ?? null };
|
|
497
|
+
return { target: null, value: null };
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* The IDENTIFIER node an assignment binds, or null when it binds something
|
|
501
|
+
* other than a simple name. `->>` is handled here too; it was absent from the
|
|
502
|
+
* operator set entirely, so rightward superassignment produced no symbol.
|
|
503
|
+
*/
|
|
504
|
+
assignmentTarget(node) {
|
|
505
|
+
const { target } = this.assignmentSides(node);
|
|
506
|
+
return target && target.type === "identifier" ? target : null;
|
|
507
|
+
}
|
|
508
|
+
handleBinaryOperator(node, symbols, exports, relationships, metadata, declaredTypes, insideFunction) {
|
|
509
|
+
const operator = node.childForFieldName("operator")?.text;
|
|
510
|
+
if (operator === void 0) return;
|
|
511
|
+
if (operator === "%>%" || operator === "|>") {
|
|
512
|
+
this.pushIfUnique(metadata.patterns, "Pipe Operations");
|
|
513
|
+
if (metadata.dataManipulationIntensity !== void 0) metadata.dataManipulationIntensity += 20;
|
|
514
|
+
}
|
|
515
|
+
if (!ASSIGNMENT_OPERATORS.has(operator)) return;
|
|
516
|
+
const { target: targetNode, value } = this.assignmentSides(node);
|
|
517
|
+
if (!targetNode || !value) return;
|
|
518
|
+
if (targetNode.type === "call") {
|
|
519
|
+
this.handleS7Assignment(node, targetNode, value, symbols, relationships, metadata, insideFunction);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (targetNode.type === "string") {
|
|
523
|
+
const strName = this.unquoteString(targetNode.text);
|
|
524
|
+
if (!strName) return;
|
|
525
|
+
if (insideFunction) return;
|
|
526
|
+
if (this.hasLocalBoundary(node)) return;
|
|
527
|
+
symbols.push({
|
|
528
|
+
name: strName,
|
|
529
|
+
kind: value.type === "function_definition" ? ASTEngine.SymbolKind.Function : ASTEngine.SymbolKind.Variable,
|
|
530
|
+
range: this.getRange(node),
|
|
531
|
+
docstring: this.extractDocstring(node),
|
|
532
|
+
metadata: { rSpecific: true, quotedAssignmentTarget: true, declarationOperator: node.childForFieldName("operator")?.text }
|
|
533
|
+
});
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (targetNode.type !== "identifier") return;
|
|
537
|
+
const name = this.unquoteIdentifier(targetNode.text);
|
|
538
|
+
if (!name) return;
|
|
539
|
+
const doc = this.extractDocstring(node);
|
|
540
|
+
if (insideFunction) return;
|
|
541
|
+
if (this.hasLocalBoundary(node)) return;
|
|
542
|
+
if (value.type === "function_definition") {
|
|
543
|
+
const isS3Method = name.indexOf(".") > 0;
|
|
544
|
+
symbols.push({
|
|
545
|
+
name,
|
|
546
|
+
kind: isS3Method ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function,
|
|
547
|
+
range: this.getRange(node),
|
|
548
|
+
signature: this.extractSignature(value),
|
|
549
|
+
docstring: doc,
|
|
550
|
+
parameters: this.extractParameters(value),
|
|
551
|
+
metadata: { rSpecific: true, isS3Method, declarationOperator: operator }
|
|
552
|
+
});
|
|
553
|
+
if (isS3Method) {
|
|
554
|
+
this.pushIfUnique(metadata.patterns, "S3 Object System");
|
|
555
|
+
const toName = name.substring(0, name.indexOf("."));
|
|
556
|
+
this.pushRel(
|
|
557
|
+
relationships,
|
|
558
|
+
name,
|
|
559
|
+
toName,
|
|
560
|
+
ASTEngine.RelationshipType.Implements,
|
|
561
|
+
0.8,
|
|
562
|
+
{
|
|
563
|
+
relationshipSubType: "s3_method",
|
|
564
|
+
reasonCode: "s3_dotted_name_convention",
|
|
565
|
+
reason: "R registers S3 methods by the `generic.class` naming convention; the generic itself is not declared in this file, so the target is inferred from the name."
|
|
566
|
+
},
|
|
567
|
+
node
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (doc && this.roxygenHasTag(doc, "@export"))
|
|
571
|
+
exports.push({ name, kind: isS3Method ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (value.type === "call") {
|
|
575
|
+
const handled = this.handleClassSystemAssignment(
|
|
576
|
+
node,
|
|
577
|
+
name,
|
|
578
|
+
value,
|
|
579
|
+
symbols,
|
|
580
|
+
exports,
|
|
581
|
+
relationships,
|
|
582
|
+
metadata,
|
|
583
|
+
declaredTypes,
|
|
584
|
+
doc,
|
|
585
|
+
insideFunction
|
|
586
|
+
);
|
|
587
|
+
if (handled) return;
|
|
588
|
+
}
|
|
589
|
+
symbols.push({
|
|
590
|
+
name,
|
|
591
|
+
kind: this.isAllCapsName(name) ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Variable,
|
|
592
|
+
range: this.getRange(node),
|
|
593
|
+
docstring: doc,
|
|
594
|
+
metadata: { rSpecific: true, declarationOperator: operator }
|
|
595
|
+
});
|
|
596
|
+
if (doc && this.roxygenHasTag(doc, "@export"))
|
|
597
|
+
exports.push({ name, kind: this.isAllCapsName(name) ? ASTEngine.SymbolKind.Constant : ASTEngine.SymbolKind.Variable });
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Declare an R5 reference class from its constructor call, for BOTH spellings.
|
|
601
|
+
*
|
|
602
|
+
* R5's `setRefClass("Name", fields=, methods=, contains=)` registers a generator under
|
|
603
|
+
* the DECLARED name whether or not the call is bound to a variable — reference classes
|
|
604
|
+
* are registered in the package's namespace at evaluation time. The corpus says so
|
|
605
|
+
* plainly: `r-Rcpp/R/00_classes.R` declares `C++Field`, `C++OverloadedMethods` and
|
|
606
|
+
* `C++Constructor` as BARE statements, and the analyzer published none of them, because
|
|
607
|
+
* the only path that recognised the constructor was the assignment-RHS one. The loss is
|
|
608
|
+
* not only three symbols: a statement-form class's `contains=` clause produced no
|
|
609
|
+
* `extends`/`mixes_in` edge either.
|
|
610
|
+
*
|
|
611
|
+
* `rangeNode` is the node the symbol's range is taken from: the assignment when the call
|
|
612
|
+
* is bound to a name (the declaration begins at the binding), the CALL itself when it is
|
|
613
|
+
* a statement.
|
|
614
|
+
*
|
|
615
|
+
* The class name must be a literal string in the first positional argument. An R5 class
|
|
616
|
+
* whose name is computed at runtime (`setRefClass(Class, ...)` in a factory) has no
|
|
617
|
+
* publishable identity, and naming it after a variable would fabricate a declaration —
|
|
618
|
+
* so those publish nothing, deliberately.
|
|
619
|
+
*/
|
|
620
|
+
declareR5Class(rangeNode, name, callNode, symbols, exports, relationships, declaredTypes, doc, patterns) {
|
|
621
|
+
const baseClasses = this.extractNamedArguments(callNode, "contains");
|
|
622
|
+
const composesList = this.compositionClauseIsList(callNode, "contains");
|
|
623
|
+
symbols.push({
|
|
624
|
+
name,
|
|
625
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
626
|
+
range: this.getRange(rangeNode),
|
|
627
|
+
docstring: doc,
|
|
628
|
+
metadata: { rSpecific: true, type: "R5", isR5Class: true, baseClasses }
|
|
629
|
+
});
|
|
630
|
+
declaredTypes.add(name);
|
|
631
|
+
if (patterns) this.pushIfUnique(patterns, "R5 Reference Classes");
|
|
632
|
+
for (const bc of baseClasses) {
|
|
633
|
+
const type = composesList ? ASTEngine.RelationshipType.MixesIn : ASTEngine.RelationshipType.Extends;
|
|
634
|
+
const subType = composesList ? "r5_composes" : "r5_inherits";
|
|
635
|
+
this.pushRel(relationships, name, bc, type, 1, { relationshipSubType: subType }, this.namedArgumentNode(callNode, "contains", bc));
|
|
636
|
+
}
|
|
637
|
+
this.extractR5Members(callNode, name, symbols, relationships, doc);
|
|
638
|
+
if (doc && this.roxygenHasTag(doc, "@export")) exports.push({ name, kind: ASTEngine.SymbolKind.Class });
|
|
639
|
+
}
|
|
640
|
+
/** Returns true when the call was recognized as a class-system constructor. */
|
|
641
|
+
handleClassSystemAssignment(node, name, callNode, symbols, exports, relationships, metadata, declaredTypes, doc, insideFunction) {
|
|
642
|
+
const fnNode = callNode.childForFieldName("function");
|
|
643
|
+
const fnName = this.calleeName(fnNode);
|
|
644
|
+
if (fnName === "R6Class") {
|
|
645
|
+
const baseClasses = this.extractNamedArguments(callNode, "inherit");
|
|
646
|
+
symbols.push({
|
|
647
|
+
name,
|
|
648
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
649
|
+
range: this.getRange(node),
|
|
650
|
+
docstring: doc,
|
|
651
|
+
metadata: { rSpecific: true, type: "R6", isR6Class: true, baseClasses }
|
|
652
|
+
});
|
|
653
|
+
declaredTypes.add(name);
|
|
654
|
+
for (const bc of baseClasses) {
|
|
655
|
+
this.pushRel(relationships, name, bc, ASTEngine.RelationshipType.Extends, 1, { relationshipSubType: "r6_inherits" }, this.namedArgumentNode(callNode, "inherit", bc));
|
|
656
|
+
}
|
|
657
|
+
this.extractR6Members(callNode, name, symbols, relationships, doc);
|
|
658
|
+
if (doc && this.roxygenHasTag(doc, "@export")) exports.push({ name, kind: ASTEngine.SymbolKind.Class });
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
if (fnName === "setRefClass") {
|
|
662
|
+
this.declareR5Class(node, name, callNode, symbols, exports, relationships, declaredTypes, doc, metadata.patterns);
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
if (fnName === "moduleServer") {
|
|
666
|
+
symbols.push({
|
|
667
|
+
name,
|
|
668
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
669
|
+
// Mapped as functional component
|
|
670
|
+
range: this.getRange(node),
|
|
671
|
+
docstring: doc,
|
|
672
|
+
metadata: { rSpecific: true, isShinyModuleServer: true, framework: "Shiny" }
|
|
673
|
+
});
|
|
674
|
+
this.pushIfUnique(metadata.patterns, "Shiny Modules");
|
|
675
|
+
if (doc && this.roxygenHasTag(doc, "@export")) exports.push({ name, kind: ASTEngine.SymbolKind.Function });
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
if (fnName === "cppFunction" || fnName === "cxxfunction") {
|
|
679
|
+
symbols.push({
|
|
680
|
+
name,
|
|
681
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
682
|
+
range: this.getRange(node),
|
|
683
|
+
docstring: doc,
|
|
684
|
+
metadata: { rSpecific: true, isInlineCpp: true, crossLanguage: "cpp" }
|
|
685
|
+
});
|
|
686
|
+
this.pushIfUnique(metadata.patterns, "Rcpp / Inline C++");
|
|
687
|
+
if (doc && this.roxygenHasTag(doc, "@export")) exports.push({ name, kind: ASTEngine.SymbolKind.Function });
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
if (REACTIVE_CONSTRUCTORS.has(fnName)) {
|
|
691
|
+
symbols.push({
|
|
692
|
+
name,
|
|
693
|
+
kind: ASTEngine.SymbolKind.Function,
|
|
694
|
+
range: this.getRange(node),
|
|
695
|
+
docstring: doc,
|
|
696
|
+
metadata: { rSpecific: true, isReactive: true, reactiveType: fnName }
|
|
697
|
+
});
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
if (fnName === "new_class" || fnName === "new_base_class") {
|
|
701
|
+
const s7Args = this.getArgumentsNode(callNode);
|
|
702
|
+
const declaredNode = this.stringArgumentNodeAt(s7Args, 0);
|
|
703
|
+
const declared = declaredNode ? this.stringNodeText(declaredNode) : void 0;
|
|
704
|
+
const className = declared || name;
|
|
705
|
+
const classNameNode = declaredNode ?? node;
|
|
706
|
+
symbols.push({
|
|
707
|
+
name: className,
|
|
708
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
709
|
+
range: this.getRange(node),
|
|
710
|
+
docstring: doc,
|
|
711
|
+
metadata: { rSpecific: true, type: "S7", isS7Class: true, parent: this.extractNamedArguments(callNode, "parent") }
|
|
712
|
+
});
|
|
713
|
+
declaredTypes.add(className);
|
|
714
|
+
this.pushIfUnique(metadata.patterns, "S7 Object System");
|
|
715
|
+
if (declared && declared !== name) {
|
|
716
|
+
symbols.push({
|
|
717
|
+
name,
|
|
718
|
+
kind: ASTEngine.SymbolKind.Variable,
|
|
719
|
+
range: this.getRange(node),
|
|
720
|
+
docstring: doc,
|
|
721
|
+
metadata: { rSpecific: true, isS7ClassBinding: true, boundClass: className }
|
|
722
|
+
});
|
|
723
|
+
this.pushRel(relationships, name, className, ASTEngine.RelationshipType.References, 1, { rSpecific: true, relationshipSubType: "s7_class_binding" }, classNameNode);
|
|
724
|
+
}
|
|
725
|
+
for (const parent of this.extractNamedArguments(callNode, "parent")) {
|
|
726
|
+
this.pushRel(relationships, className, parent, ASTEngine.RelationshipType.Extends, 1, { rSpecific: true, relationshipSubType: "s7_parent" }, this.namedArgumentNode(callNode, "parent", parent));
|
|
727
|
+
}
|
|
728
|
+
const propertyHolder = this.namedArgumentValue(callNode, "properties");
|
|
729
|
+
if (propertyHolder && propertyHolder.type === "call" && this.calleeName(propertyHolder.childForFieldName("function")) === "list") {
|
|
730
|
+
const entries = this.getArgumentsNode(propertyHolder);
|
|
731
|
+
if (entries) {
|
|
732
|
+
for (let i = 0; i < entries.namedChildCount; i++) {
|
|
733
|
+
const entry = entries.namedChild(i);
|
|
734
|
+
if (entry?.type !== "argument") continue;
|
|
735
|
+
const propName = entry.childForFieldName("name");
|
|
736
|
+
if (!propName) continue;
|
|
737
|
+
const prop = this.unquoteIdentifier(propName.text);
|
|
738
|
+
symbols.push({
|
|
739
|
+
name: prop,
|
|
740
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
741
|
+
range: this.getRange(propName),
|
|
742
|
+
ownerName: className,
|
|
743
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
744
|
+
docstring: "",
|
|
745
|
+
metadata: { rSpecific: true, isS7Property: true, objectSystem: "S7" }
|
|
746
|
+
});
|
|
747
|
+
this.pushRel(relationships, className, prop, ASTEngine.RelationshipType.Contains, 1, { rSpecific: true, relationshipSubType: "s7_property" }, propName);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (doc && this.roxygenHasTag(doc, "@export")) {
|
|
752
|
+
exports.push({ name, kind: declared && declared !== name ? ASTEngine.SymbolKind.Variable : ASTEngine.SymbolKind.Class });
|
|
753
|
+
}
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
if (fnName === "setGeneric" || fnName === "setMethod" || fnName === "setClass" || fnName === "setValidity") {
|
|
757
|
+
symbols.push({
|
|
758
|
+
name,
|
|
759
|
+
kind: ASTEngine.SymbolKind.Variable,
|
|
760
|
+
range: this.getRange(node),
|
|
761
|
+
docstring: doc,
|
|
762
|
+
metadata: {
|
|
763
|
+
rSpecific: true,
|
|
764
|
+
isS4Binding: true,
|
|
765
|
+
bindingFor: fnName,
|
|
766
|
+
note: "R binds the S4 generator/definition to this name; the declaration itself is emitted under its declared class/generic name."
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
this.pushRel(relationships, name, name, ASTEngine.RelationshipType.References, 1, { rSpecific: true, relationshipSubType: "s4_binding" }, node);
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
sameNode(a, b) {
|
|
775
|
+
if (!a || !b) return false;
|
|
776
|
+
return a.startIndex === b.startIndex && a.endIndex === b.endIndex;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Push one relationship, stamping `line_number` with the 1-based line of the
|
|
780
|
+
* node that NAMES the edge.
|
|
781
|
+
*
|
|
782
|
+
* M59.A7 (#825) established this convention across the analyzer family (33 of
|
|
783
|
+
* 44 analyzers carry it): the line is the node the edge's TARGET is written as
|
|
784
|
+
* — a call's callee leaf, a member's own entry, a base class's argument — not
|
|
785
|
+
* the construct that merely contains it. A missing line is honest, so a call
|
|
786
|
+
* with no node to ask omits the field rather than writing a fabricated 0.
|
|
787
|
+
* The R analyzer previously carried no line on any edge, so every R
|
|
788
|
+
* relationship was unlocatable in the graph and the LSP.
|
|
789
|
+
*/
|
|
790
|
+
pushRel(out, source, target, type, confidence, metadata, namingNode) {
|
|
791
|
+
if (!source || !target) return;
|
|
792
|
+
const rel = { source, target, type, confidence, metadata };
|
|
793
|
+
let line;
|
|
794
|
+
if (typeof namingNode === "number") line = namingNode;
|
|
795
|
+
else if (namingNode) {
|
|
796
|
+
const row = namingNode.startPosition?.row;
|
|
797
|
+
line = typeof row === "number" ? row + 1 : namingNode.startLine;
|
|
798
|
+
}
|
|
799
|
+
if (typeof line === "number" && Number.isFinite(line) && line > 0) rel.line_number = line;
|
|
800
|
+
out.push(rel);
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* CERTIFY THE EDGES WHOSE TARGET IS A PACKAGE NAMESPACE — R's base and dependency packages are
|
|
804
|
+
* not repositories in this corpus, so no key space it can build will ever answer them.
|
|
805
|
+
*
|
|
806
|
+
* THE DEFECT THIS CLOSES. `library(cli)` and `cli::cli_abort()` reach the graph as a dependency
|
|
807
|
+
* on `cli`, and the arbiter reads `imports`/`references` target `cli` and finds no declaration
|
|
808
|
+
* of it anywhere in the project — which is TRUE, and which is what `unresolved` means. The lane
|
|
809
|
+
* measures this class at **660 of its 8,785-edge residual** on its own 400-file draw
|
|
810
|
+
* (`validation/m14/verification/R-FIX.md` §3: `imports:bare-name 330 · references:bare-name
|
|
811
|
+
* 330`, top targets `cli 61 · withr 38 · lifecycle 19 · utils 18 · testthat 14 · Rcpp 13`), while
|
|
812
|
+
* publishing NOTHING the arbiter reads. This is the fleet's finding (a) in its claim form: the
|
|
813
|
+
* analyzer already HOLDS the namespace — it is the edge's target — and never declares it.
|
|
814
|
+
*
|
|
815
|
+
* WHY THIS IS NOT A BLANKET EXTERNAL MARKER, and the distinction is the one that matters. The
|
|
816
|
+
* claim does not say "this did not resolve". It says **the target's ROOT is a language
|
|
817
|
+
* namespace, and here is the closed list of packages this FILE names** — decidable from R's own
|
|
818
|
+
* semantics, because `library(x)` LOADS a package and `x::f` is only legal when `x` IS one. It is
|
|
819
|
+
* the shape five other lanes already carry: hcl's `BUILTIN_ROOTS`, python's
|
|
820
|
+
* `PY_BUILTIN_NAMESPACE` (whose class is this class in its own words — *"Python's runtime
|
|
821
|
+
* namespace is not a project, so `dict`, `tuple`, `object` … can never be answered by any key
|
|
822
|
+
* space this corpus builds"*), zig's `["std"]`, powershell's `["System"]`, graphql's built-in
|
|
823
|
+
* scalars. The evidence list is the file's own dependency set, so widening it is a visible act on
|
|
824
|
+
* the edge rather than an opaque boolean (property 2 of `NAMESPACE_CLAIM_KEY`).
|
|
825
|
+
*
|
|
826
|
+
* FIVE GUARDS, each evidential, mirroring the python lane's `certifyLanguageNamespaceRoutes`:
|
|
827
|
+
* 1. the edge's SUBTYPE is one whose target is a package by construction
|
|
828
|
+
* (`PACKAGE_NAMESPACE_SUBTYPES`) — never inferred from the spelling alone;
|
|
829
|
+
* 2. the target IS in the file's declared package list — the claim's evidence;
|
|
830
|
+
* 3. the target is package-name-shaped (`isPackageNameShape`) as well as listed;
|
|
831
|
+
* 4. the file does not DECLARE the name itself — `cli <- function(...)` makes `cli` a
|
|
832
|
+
* declaration in this file and the claim would then be false;
|
|
833
|
+
* 5. the edge carries no claim already (`languageNamespace` is undefined), so an edge that
|
|
834
|
+
* another mechanism already explains keeps that mechanism.
|
|
835
|
+
*
|
|
836
|
+
* THE TARGET IS NEVER REWRITTEN AND NO PRE-EXISTING FIELD CHANGES: one additive metadata key.
|
|
837
|
+
* Every other byte of every edge this lane emitted is identical, which is what makes the
|
|
838
|
+
* before/after table a measurement of this claim and of nothing else.
|
|
839
|
+
*
|
|
840
|
+
* WHAT IT CANNOT DO, stated here rather than discovered later. The analyzer sees ONE FILE, so
|
|
841
|
+
* guard 4 covers only the file's own declarations: where the PROJECT declares a symbol that
|
|
842
|
+
* shares a package's name — `methods()` in S7, the `Matrix` CLASS in the Matrix package — the
|
|
843
|
+
* claim still wins and the edge reads `externalVerified` instead of `resolved`. That is 40 edges
|
|
844
|
+
* on the 400-file draw, each one a name-coincidence in ANOTHER file (gate intent:
|
|
845
|
+
* `arbitraryTwin`, the class this programme's brief calls the cross-file hazard), and they are
|
|
846
|
+
* enumerated in `namespace-claim-price.ts` and in the report rather than absorbed.
|
|
847
|
+
*/
|
|
848
|
+
certifyPackageNamespaces(relationships, symbols) {
|
|
849
|
+
const packages = /* @__PURE__ */ new Set();
|
|
850
|
+
for (const rel of relationships) {
|
|
851
|
+
const label = String(rel.type || rel.relationshipType || "");
|
|
852
|
+
if (!PACKAGE_NAMESPACE_LABELS.has(label)) continue;
|
|
853
|
+
const md = rel.metadata ?? {};
|
|
854
|
+
if (!PACKAGE_NAMESPACE_SUBTYPES.has(String(md.relationshipSubType ?? ""))) continue;
|
|
855
|
+
const tgt = String(rel.target ?? "");
|
|
856
|
+
if (isPackageNameShape(tgt)) packages.add(tgt);
|
|
857
|
+
}
|
|
858
|
+
if (packages.size === 0) return 0;
|
|
859
|
+
const roots = [...packages].sort();
|
|
860
|
+
const declaredHere = /* @__PURE__ */ new Set();
|
|
861
|
+
for (const s of symbols) {
|
|
862
|
+
const name = String(s?.name ?? "");
|
|
863
|
+
if (name.length > 0) declaredHere.add(name);
|
|
864
|
+
}
|
|
865
|
+
let certified = 0;
|
|
866
|
+
for (const rel of relationships) {
|
|
867
|
+
const md = rel.metadata;
|
|
868
|
+
if (!md || typeof md !== "object") continue;
|
|
869
|
+
if (md.languageNamespace !== void 0) continue;
|
|
870
|
+
const label = String(rel.type || rel.relationshipType || "");
|
|
871
|
+
if (!PACKAGE_NAMESPACE_LABELS.has(label)) continue;
|
|
872
|
+
if (!PACKAGE_NAMESPACE_SUBTYPES.has(String(md.relationshipSubType ?? ""))) continue;
|
|
873
|
+
const tgt = String(rel.target ?? "");
|
|
874
|
+
if (!packages.has(tgt)) continue;
|
|
875
|
+
if (declaredHere.has(tgt)) continue;
|
|
876
|
+
md.languageNamespace = [...roots];
|
|
877
|
+
certified++;
|
|
878
|
+
}
|
|
879
|
+
return certified;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The bare name a call/namespace refers to, read from grammar FIELDS.
|
|
883
|
+
*
|
|
884
|
+
* identifier -> its own text
|
|
885
|
+
* namespace_operator -> the `rhs` field (`R6::R6Class` -> `R6Class`,
|
|
886
|
+
* `R6:::R6Class` -> `R6Class`, `base::sqrt` -> `sqrt`)
|
|
887
|
+
* extract_operator -> the `rhs` field (`obj$method` -> `method`)
|
|
888
|
+
*
|
|
889
|
+
* The previous `fnNode.text.split("::")` was both a source-text scan and
|
|
890
|
+
* wrong: it turned `R6:::R6Class` into `:R6Class`, and it silently missed every
|
|
891
|
+
* namespaced call whose `::` form differed from the bare name.
|
|
892
|
+
*/
|
|
893
|
+
calleeName(fnNode) {
|
|
894
|
+
if (!fnNode) return "";
|
|
895
|
+
if (fnNode.type === "namespace_operator" || fnNode.type === "extract_operator") {
|
|
896
|
+
const rhs = fnNode.childForFieldName("rhs");
|
|
897
|
+
if (rhs) {
|
|
898
|
+
if (rhs.type === "identifier" || rhs.type === "dots") return this.unquoteIdentifier(rhs.text);
|
|
899
|
+
return this.unquoteIdentifier(rhs.text);
|
|
900
|
+
}
|
|
901
|
+
return "";
|
|
902
|
+
}
|
|
903
|
+
if (fnNode.type === "identifier") return this.unquoteIdentifier(fnNode.text);
|
|
904
|
+
if (this.isRName(fnNode.text)) return this.unquoteIdentifier(fnNode.text);
|
|
905
|
+
return "";
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Is this text an R name? R's rule: a letter or a `.` not followed by a digit, then
|
|
909
|
+
* letters, digits, `.` and `_`; or a backtick-quoted name, inside which almost
|
|
910
|
+
* anything is legal. Letters beyond ASCII are accepted, since R source is frequently
|
|
911
|
+
* UTF-8 and a name is a name in any alphabet.
|
|
912
|
+
*
|
|
913
|
+
* Character codes rather than a regexp, to keep the analyzer free of pattern matching.
|
|
914
|
+
* This is the only place the analyzer inspects a node's TEXT to decide what it is -- the
|
|
915
|
+
* grammar's node types do not carry a "this is a name" flag -- and it is listed in the
|
|
916
|
+
* documentation's inventory of text operations.
|
|
917
|
+
*/
|
|
918
|
+
isRName(text) {
|
|
919
|
+
if (text.length === 0) return false;
|
|
920
|
+
if (text.charCodeAt(0) === 96) {
|
|
921
|
+
return text.length >= 2 && text.charCodeAt(text.length - 1) === 96;
|
|
922
|
+
}
|
|
923
|
+
const isLetter = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 192 && c <= 591 || c >= 880 && c <= 8191;
|
|
924
|
+
const isDigit = (c) => c >= 48 && c <= 57;
|
|
925
|
+
const first = text.charCodeAt(0);
|
|
926
|
+
let i = 0;
|
|
927
|
+
if (first === 46) {
|
|
928
|
+
i = 1;
|
|
929
|
+
if (i < text.length && isDigit(text.charCodeAt(i))) return false;
|
|
930
|
+
} else if (!isLetter(first)) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
for (; i < text.length; i++) {
|
|
934
|
+
const c = text.charCodeAt(i);
|
|
935
|
+
if (isLetter(c) || isDigit(c) || c === 46 || c === 95) continue;
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* R identifiers may be backtick-quoted; the backticks are syntax, not part of
|
|
942
|
+
* the name. tree-sitter-r reports a quoted identifier as a plain `identifier`
|
|
943
|
+
* whose text still carries the backticks, so exactly one leading/trailing
|
|
944
|
+
* backtick pair is removed. Nothing else is inspected.
|
|
945
|
+
*/
|
|
946
|
+
unquoteIdentifier(text) {
|
|
947
|
+
if (text.length >= 2 && text.charCodeAt(0) === 96 && text.charCodeAt(text.length - 1) === 96) {
|
|
948
|
+
return text.substring(1, text.length - 1);
|
|
949
|
+
}
|
|
950
|
+
return text;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* S7 (rlang's object system) declarations, whose assigned name is a CALL:
|
|
954
|
+
*
|
|
955
|
+
* `Foo <- S7::new_class("Foo", parent = Bar, properties = list(...))`
|
|
956
|
+
* `S7::method(print, class_foo) <- function(self, ...) ...`
|
|
957
|
+
*
|
|
958
|
+
* Neither has an identifier on the left, so both were previously invisible:
|
|
959
|
+
* no symbol, and calls inside the body attributed to a source string that was
|
|
960
|
+
* never a symbol.
|
|
961
|
+
*/
|
|
962
|
+
handleS7Assignment(node, targetCall, value, symbols, relationships, metadata, insideFunction) {
|
|
963
|
+
const base = this.calleeName(targetCall.childForFieldName("function"));
|
|
964
|
+
if (insideFunction) return;
|
|
965
|
+
if (base === "method" || base === "S7_method") {
|
|
966
|
+
if (value.type !== "function_definition") return;
|
|
967
|
+
const tArgs = this.getArgumentsNode(targetCall);
|
|
968
|
+
const genericNode = this.firstPositionalValue(tArgs);
|
|
969
|
+
const classNode = this.secondPositionalValue(tArgs);
|
|
970
|
+
if (!genericNode) return;
|
|
971
|
+
const generic = this.nodeAsName(genericNode);
|
|
972
|
+
let owner;
|
|
973
|
+
if (classNode) {
|
|
974
|
+
owner = this.nodeAsName(classNode) || void 0;
|
|
975
|
+
if (!owner && classNode.type === "call" && this.calleeName(classNode.childForFieldName("function")) === "list") {
|
|
976
|
+
const listArgs = this.getArgumentsNode(classNode);
|
|
977
|
+
for (let i = 0; listArgs && i < listArgs.namedChildCount; i++) {
|
|
978
|
+
const entry = listArgs.namedChild(i);
|
|
979
|
+
if (entry?.type !== "argument") continue;
|
|
980
|
+
if (entry.childForFieldName("name")) continue;
|
|
981
|
+
const v = this.nodeAsName(entry.childForFieldName("value"));
|
|
982
|
+
if (v) {
|
|
983
|
+
owner = v;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (!generic) return;
|
|
990
|
+
this.pushIfUnique(metadata.patterns, "S7 Object System");
|
|
991
|
+
symbols.push({
|
|
992
|
+
name: generic,
|
|
993
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
994
|
+
range: this.getRange(node),
|
|
995
|
+
ownerName: owner,
|
|
996
|
+
ownerKind: owner ? ASTEngine.SymbolKind.Class : void 0,
|
|
997
|
+
signature: this.extractSignature(value),
|
|
998
|
+
parameters: this.extractParameters(value),
|
|
999
|
+
docstring: this.extractDocstring(node),
|
|
1000
|
+
metadata: {
|
|
1001
|
+
rSpecific: true,
|
|
1002
|
+
isS7Method: true,
|
|
1003
|
+
s7Generic: generic,
|
|
1004
|
+
s7Class: owner,
|
|
1005
|
+
objectSystem: "S7"
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
if (owner) {
|
|
1009
|
+
this.pushRel(relationships, owner, generic, ASTEngine.RelationshipType.Contains, 1, { rSpecific: true, relationshipSubType: "s7_method" }, genericNode);
|
|
1010
|
+
}
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (base === "new_class" || base === "new_base_class") {
|
|
1014
|
+
const cArgs = this.getArgumentsNode(targetCall);
|
|
1015
|
+
const declared = cArgs ? this.stringArgumentAt(cArgs, 0) : void 0;
|
|
1016
|
+
const name = declared || (value.type === "identifier" ? this.unquoteIdentifier(value.text) : "");
|
|
1017
|
+
if (!name) return;
|
|
1018
|
+
this.pushIfUnique(metadata.patterns, "S7 Object System");
|
|
1019
|
+
symbols.push({
|
|
1020
|
+
name,
|
|
1021
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1022
|
+
range: this.getRange(node),
|
|
1023
|
+
docstring: this.extractDocstring(node),
|
|
1024
|
+
metadata: { rSpecific: true, type: "S7", isS7Class: true }
|
|
1025
|
+
});
|
|
1026
|
+
for (const parent of this.extractNamedArguments(targetCall, "parent")) {
|
|
1027
|
+
this.pushRel(relationships, name, parent, ASTEngine.RelationshipType.Extends, 1, { rSpecific: true, relationshipSubType: "s7_parent" }, this.namedArgumentNode(targetCall, "parent", parent));
|
|
1028
|
+
}
|
|
1029
|
+
const propertyHolder = this.namedArgumentValue(targetCall, "properties");
|
|
1030
|
+
if (propertyHolder && propertyHolder.type === "call" && this.calleeName(propertyHolder.childForFieldName("function")) === "list") {
|
|
1031
|
+
const entries = this.getArgumentsNode(propertyHolder);
|
|
1032
|
+
if (entries) {
|
|
1033
|
+
for (let i = 0; i < entries.namedChildCount; i++) {
|
|
1034
|
+
const entry = entries.namedChild(i);
|
|
1035
|
+
if (entry?.type !== "argument") continue;
|
|
1036
|
+
const propName = entry.childForFieldName("name");
|
|
1037
|
+
if (!propName) continue;
|
|
1038
|
+
symbols.push({
|
|
1039
|
+
name: this.unquoteIdentifier(propName.text),
|
|
1040
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1041
|
+
range: this.getRange(propName),
|
|
1042
|
+
ownerName: name,
|
|
1043
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
1044
|
+
docstring: "",
|
|
1045
|
+
metadata: { rSpecific: true, isS7Property: true, objectSystem: "S7" }
|
|
1046
|
+
});
|
|
1047
|
+
this.pushRel(relationships, name, this.unquoteIdentifier(propName.text), ASTEngine.RelationshipType.Contains, 1, { rSpecific: true, relationshipSubType: "s7_property" }, propName);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/** An identifier or string node read as a name. */
|
|
1054
|
+
nodeAsName(node) {
|
|
1055
|
+
if (!node) return "";
|
|
1056
|
+
if (node.type === "string") return this.stringNodeText(node);
|
|
1057
|
+
if (node.type === "identifier" || node.type === "dots") return this.unquoteIdentifier(node.text);
|
|
1058
|
+
return "";
|
|
1059
|
+
}
|
|
1060
|
+
/** The value of a named argument of a call, or null. */
|
|
1061
|
+
/**
|
|
1062
|
+
* True when the node sits inside a `local({ ... })` body.
|
|
1063
|
+
*
|
|
1064
|
+
* `local()` is the one construct in ordinary R that evaluates an expression in a
|
|
1065
|
+
* NEW environment without introducing a `function_definition` node, so the
|
|
1066
|
+
* function-ancestor test cannot see it. Assignments inside it are NOT file-level
|
|
1067
|
+
* bindings: R's `local()` creates a child environment and nothing escapes.
|
|
1068
|
+
*
|
|
1069
|
+
* Found by the R oracle, not by reading: dplyr's test-across.R binds
|
|
1070
|
+
* `_local_var` inside `local({ ... })` and the analyzer was publishing it as a
|
|
1071
|
+
* global variable. A plain `{ ... }` block inside a call is deliberately NOT
|
|
1072
|
+
* treated as a boundary, because R evaluates that in the CALLING frame and the
|
|
1073
|
+
* binding really is global.
|
|
1074
|
+
*/
|
|
1075
|
+
hasLocalBoundary(node) {
|
|
1076
|
+
let current = node;
|
|
1077
|
+
while (current) {
|
|
1078
|
+
const parent = current.parent;
|
|
1079
|
+
if (!parent) return false;
|
|
1080
|
+
if (parent.type === "braced_expression" || parent.type === "parenthesized_expression") {
|
|
1081
|
+
let owner = parent;
|
|
1082
|
+
for (let hop = 0; hop < 3 && owner; hop++) {
|
|
1083
|
+
const up = owner.parent;
|
|
1084
|
+
if (!up) break;
|
|
1085
|
+
if (up.type === "call") {
|
|
1086
|
+
const fn = up.childForFieldName("function");
|
|
1087
|
+
if (fn && this.calleeName(fn) === "local") return true;
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
owner = up;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
current = parent;
|
|
1094
|
+
}
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* The line a symbol's NAME is written on.
|
|
1099
|
+
*
|
|
1100
|
+
* A declaration's range starts where the declaration starts, which for a
|
|
1101
|
+
* multi-line `Cls <- R6Class(` is the line ABOVE the name, and for an S4 slot
|
|
1102
|
+
* is the whole `setClass` call. M59.A7's convention is the line that NAMES the
|
|
1103
|
+
* edge, so a `contains` edge is stamped with the name's own line, found by
|
|
1104
|
+
* searching the symbol's own range for its name text. Falls back to the range's
|
|
1105
|
+
* start when the name is synthesized (an S4 `validity` method has no literal
|
|
1106
|
+
* token in the source).
|
|
1107
|
+
*/
|
|
1108
|
+
/**
|
|
1109
|
+
* Whether `at` is the start of the IDENTIFIER `name` in `segment`, rather than a place
|
|
1110
|
+
* where those characters merely occur.
|
|
1111
|
+
*
|
|
1112
|
+
* R identifiers continue with letters, digits, `.` and `_`, so `theme` occurs inside
|
|
1113
|
+
* `class_theme` and `sizes` inside `resize`. A substring search cannot tell those apart
|
|
1114
|
+
* from the real thing, and the consequence is a relationship pointing at a line that
|
|
1115
|
+
* does not name its target: `class_theme <- S7::new_class(` on line 219 matched
|
|
1116
|
+
* `theme`, so `global exports theme` and `global contains theme` were attributed to 219
|
|
1117
|
+
* while the class name is written on 220. Eight such edges in ggplot2's all-classes.R.
|
|
1118
|
+
*
|
|
1119
|
+
* A backtick-quoted name is a special case: inside backticks almost anything is legal,
|
|
1120
|
+
* so only the closing backtick is excluded, and the backticks themselves are not part of
|
|
1121
|
+
* the name.
|
|
1122
|
+
*/
|
|
1123
|
+
isIdentifierOccurrence(segment, at, name) {
|
|
1124
|
+
const isPart = (ch) => {
|
|
1125
|
+
if (ch === void 0) return false;
|
|
1126
|
+
const c = ch.charCodeAt(0);
|
|
1127
|
+
return c >= 65 && c <= 90 || c >= 97 && c <= 122 || c >= 48 && c <= 57 || c === 46 || c === 95;
|
|
1128
|
+
};
|
|
1129
|
+
const before = at > 0 ? segment[at - 1] : void 0;
|
|
1130
|
+
if (before === "`") {
|
|
1131
|
+
return segment[at + name.length] === "`";
|
|
1132
|
+
}
|
|
1133
|
+
if (isPart(before)) return false;
|
|
1134
|
+
const after = segment[at + name.length];
|
|
1135
|
+
return !isPart(after);
|
|
1136
|
+
}
|
|
1137
|
+
nameLineOf(symbol, content) {
|
|
1138
|
+
const start = symbol.range.startIndex;
|
|
1139
|
+
const end = symbol.range.endIndex;
|
|
1140
|
+
if (typeof start !== "number" || typeof end !== "number" || end > content.length || end <= start) {
|
|
1141
|
+
return symbol.range.startLine;
|
|
1142
|
+
}
|
|
1143
|
+
const segment = content.substring(start, end);
|
|
1144
|
+
let at = -1;
|
|
1145
|
+
for (let from = 0; from <= segment.length - symbol.name.length; from++) {
|
|
1146
|
+
const i = segment.indexOf(symbol.name, from);
|
|
1147
|
+
if (i === -1) break;
|
|
1148
|
+
if (this.isIdentifierOccurrence(segment, i, symbol.name)) {
|
|
1149
|
+
at = i;
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
from = i;
|
|
1153
|
+
}
|
|
1154
|
+
if (at === -1) return symbol.range.startLine;
|
|
1155
|
+
let line = symbol.range.startLine;
|
|
1156
|
+
for (let i = 0; i < at; i++) if (segment.charCodeAt(i) === 10) line++;
|
|
1157
|
+
return line;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* The node that NAMES `wanted` inside a named argument's value, used to stamp a
|
|
1161
|
+
* relationship's line. Looks in `c(...)`/`list(...)` element lists first, then
|
|
1162
|
+
* in the argument's own value, so `inherit = c("A","B")`, `inherit = "A"` and
|
|
1163
|
+
* `parent = Bar` all resolve to the node that actually spells the name.
|
|
1164
|
+
*/
|
|
1165
|
+
namedArgumentNode(node, argName, wanted) {
|
|
1166
|
+
const args = this.getArgumentsNode(node);
|
|
1167
|
+
if (!args) return null;
|
|
1168
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1169
|
+
const arg = args.namedChild(i);
|
|
1170
|
+
if (arg?.type !== "argument") continue;
|
|
1171
|
+
if (arg.childForFieldName("name")?.text !== argName) continue;
|
|
1172
|
+
const value = arg.childForFieldName("value");
|
|
1173
|
+
if (!value) continue;
|
|
1174
|
+
if (value.type === "call") {
|
|
1175
|
+
const inner = this.getArgumentsNode(value);
|
|
1176
|
+
if (inner) {
|
|
1177
|
+
for (let j = 0; j < inner.namedChildCount; j++) {
|
|
1178
|
+
const entry = inner.namedChild(j);
|
|
1179
|
+
if (!entry) continue;
|
|
1180
|
+
if (entry.type === "argument") {
|
|
1181
|
+
const nm = entry.childForFieldName("name");
|
|
1182
|
+
const v = entry.childForFieldName("value");
|
|
1183
|
+
const text = nm?.text ?? (v ? this.baseClassNodeName(v) : "");
|
|
1184
|
+
if (text === wanted) return nm ?? v ?? entry;
|
|
1185
|
+
} else {
|
|
1186
|
+
if (this.baseClassNodeName(entry) === wanted) return entry;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (this.baseClassNodeName(value) === wanted) return value;
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
if (this.baseClassNodeName(value) === wanted) return value;
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
namedArgumentValue(node, argName) {
|
|
1198
|
+
const args = this.getArgumentsNode(node);
|
|
1199
|
+
if (!args) return null;
|
|
1200
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1201
|
+
const arg = args.namedChild(i);
|
|
1202
|
+
if (arg?.type !== "argument") continue;
|
|
1203
|
+
if (arg.childForFieldName("name")?.text === argName) return arg.childForFieldName("value");
|
|
1204
|
+
}
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
/** The second positional argument value of a call. */
|
|
1208
|
+
secondPositionalValue(args) {
|
|
1209
|
+
if (!args) return null;
|
|
1210
|
+
let seen = 0;
|
|
1211
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1212
|
+
const arg = args.namedChild(i);
|
|
1213
|
+
if (!arg || arg.type === "comma") continue;
|
|
1214
|
+
if (arg.type === "argument") {
|
|
1215
|
+
if (arg.childForFieldName("name")) continue;
|
|
1216
|
+
const v = arg.childForFieldName("value");
|
|
1217
|
+
if (seen === 1) return v;
|
|
1218
|
+
seen++;
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
if (seen === 1) return arg;
|
|
1222
|
+
seen++;
|
|
1223
|
+
}
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
isAllCapsName(name) {
|
|
1227
|
+
if (!name || name.length < 2) return false;
|
|
1228
|
+
let hasLetter = false;
|
|
1229
|
+
for (let i = 0; i < name.length; i++) {
|
|
1230
|
+
const c = name[i];
|
|
1231
|
+
if (c >= "a" && c <= "z" || c >= "A" && c <= "Z") {
|
|
1232
|
+
hasLetter = true;
|
|
1233
|
+
if (c >= "a" && c <= "z") return false;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return hasLetter;
|
|
1237
|
+
}
|
|
1238
|
+
// ------------------------------------------------------------ R6 / R5 members
|
|
1239
|
+
/**
|
|
1240
|
+
* R6 members. Inside `public=`/`private=`/`active=` a named entry is either a
|
|
1241
|
+
* METHOD (value is a `function_definition`) or a DATA MEMBER (any other value).
|
|
1242
|
+
* Data members are declarations too: they are the class's public surface and
|
|
1243
|
+
* are read as `self$name`, so omitting them hides the class API entirely.
|
|
1244
|
+
*
|
|
1245
|
+
* Only entries with a real `name` FIELD are members. The earlier
|
|
1246
|
+
* `childForFieldName("name") || child(0)` fallback turned an unnamed list
|
|
1247
|
+
* element holding a function into a Method whose "name" was the function's own
|
|
1248
|
+
* source text — a phantom declaration invented from syntax.
|
|
1249
|
+
*/
|
|
1250
|
+
extractR6Members(callNode, className, symbols, relationships, _doc) {
|
|
1251
|
+
this.extractMemberLists(callNode, className, symbols, relationships, CLASS_MEMBER_LISTS, {
|
|
1252
|
+
methodMeta: { isR6Method: true },
|
|
1253
|
+
methodSubType: "r6_method",
|
|
1254
|
+
system: "R6"
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
extractR5Members(callNode, className, symbols, relationships, _doc) {
|
|
1258
|
+
this.extractMemberLists(callNode, className, symbols, relationships, R5_MEMBER_LISTS, {
|
|
1259
|
+
methodMeta: { isR5Method: true },
|
|
1260
|
+
methodSubType: "r5_method",
|
|
1261
|
+
system: "R5"
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
extractMemberLists(callNode, className, symbols, relationships, listNames, opts) {
|
|
1265
|
+
const argsNode = this.getArgumentsNode(callNode);
|
|
1266
|
+
if (!argsNode) return;
|
|
1267
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
1268
|
+
const arg = argsNode.child(i);
|
|
1269
|
+
if (!arg || arg.type !== "argument") continue;
|
|
1270
|
+
const argNameNode = arg.childForFieldName("name");
|
|
1271
|
+
if (!argNameNode) continue;
|
|
1272
|
+
const listName = argNameNode.text;
|
|
1273
|
+
if (!listNames.has(listName)) continue;
|
|
1274
|
+
const listNode = arg.childForFieldName("value");
|
|
1275
|
+
if (!listNode || listNode.type !== "call") continue;
|
|
1276
|
+
if (this.calleeName(listNode.childForFieldName("function")) !== "list") continue;
|
|
1277
|
+
const entries = this.getArgumentsNode(listNode);
|
|
1278
|
+
if (!entries) continue;
|
|
1279
|
+
for (let j = 0; j < entries.childCount; j++) {
|
|
1280
|
+
const entry = entries.child(j);
|
|
1281
|
+
if (!entry || entry.type !== "argument") continue;
|
|
1282
|
+
const memberNameNode = entry.childForFieldName("name");
|
|
1283
|
+
const memberValue = entry.childForFieldName("value");
|
|
1284
|
+
if (!memberNameNode || !memberValue) continue;
|
|
1285
|
+
const memberName = this.unquoteIdentifier(memberNameNode.text);
|
|
1286
|
+
if (!memberName) continue;
|
|
1287
|
+
const isMethod = memberValue.type === "function_definition" && !DATA_MEMBER_LISTS.has(listName);
|
|
1288
|
+
if (isMethod) {
|
|
1289
|
+
symbols.push({
|
|
1290
|
+
name: memberName,
|
|
1291
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1292
|
+
range: this.getDeclarationRange(memberNameNode, memberValue),
|
|
1293
|
+
signature: this.extractSignature(memberValue),
|
|
1294
|
+
parameters: this.extractParameters(memberValue),
|
|
1295
|
+
docstring: "",
|
|
1296
|
+
ownerName: className,
|
|
1297
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
1298
|
+
metadata: {
|
|
1299
|
+
rSpecific: true,
|
|
1300
|
+
...opts.methodMeta,
|
|
1301
|
+
memberList: listName,
|
|
1302
|
+
ownerClass: className,
|
|
1303
|
+
objectSystem: opts.system
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
} else {
|
|
1307
|
+
symbols.push({
|
|
1308
|
+
name: memberName,
|
|
1309
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1310
|
+
range: this.getDeclarationRange(memberNameNode, memberValue),
|
|
1311
|
+
ownerName: className,
|
|
1312
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
1313
|
+
docstring: "",
|
|
1314
|
+
metadata: {
|
|
1315
|
+
rSpecific: true,
|
|
1316
|
+
isObjectSystemField: true,
|
|
1317
|
+
memberList: listName,
|
|
1318
|
+
ownerClass: className,
|
|
1319
|
+
objectSystem: opts.system
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
this.pushRel(
|
|
1324
|
+
relationships,
|
|
1325
|
+
className,
|
|
1326
|
+
memberName,
|
|
1327
|
+
ASTEngine.RelationshipType.Contains,
|
|
1328
|
+
1,
|
|
1329
|
+
{
|
|
1330
|
+
relationshipSubType: isMethod ? opts.methodSubType : `${opts.system.toLowerCase()}_field`,
|
|
1331
|
+
visibility: listName,
|
|
1332
|
+
memberKind: isMethod ? "method" : "field"
|
|
1333
|
+
},
|
|
1334
|
+
memberNameNode
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
// ----------------------------------------------------------------- parameters
|
|
1340
|
+
getParametersNode(node) {
|
|
1341
|
+
const byField = node.childForFieldName("parameters");
|
|
1342
|
+
if (byField) return byField;
|
|
1343
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1344
|
+
const c = node.child(i);
|
|
1345
|
+
if (c && c.type === "parameters") return c;
|
|
1346
|
+
}
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* A parameter's default lives in the grammar's `default` FIELD. The analyzer
|
|
1351
|
+
* asked for `default_value`, which this grammar does not define, so the lookup
|
|
1352
|
+
* was always null and the analyzer fell back to scanning the parameter's raw
|
|
1353
|
+
* text for an `=`. Using the field removes the dead branch, removes the text
|
|
1354
|
+
* scan, and additionally recovers the default EXPRESSION, which was being
|
|
1355
|
+
* computed and then discarded.
|
|
1356
|
+
*
|
|
1357
|
+
* `...` arrives as a `parameter` whose `name` field is a `dots` node, so rest
|
|
1358
|
+
* parameters are recognized by node type rather than by their text.
|
|
1359
|
+
*/
|
|
1360
|
+
extractParameters(node) {
|
|
1361
|
+
const params = [];
|
|
1362
|
+
const parametersNode = this.getParametersNode(node);
|
|
1363
|
+
if (parametersNode) {
|
|
1364
|
+
for (let i = 0; i < parametersNode.childCount; i++) {
|
|
1365
|
+
const p = parametersNode.child(i);
|
|
1366
|
+
if (p && p.type === "parameter") {
|
|
1367
|
+
const nameNode = p.childForFieldName("name");
|
|
1368
|
+
if (!nameNode) continue;
|
|
1369
|
+
const defaultValueNode = p.childForFieldName("default");
|
|
1370
|
+
const isRest = nameNode.type === "dots";
|
|
1371
|
+
const param = {
|
|
1372
|
+
name: this.unquoteIdentifier(nameNode.text),
|
|
1373
|
+
isOptional: !!defaultValueNode
|
|
1374
|
+
};
|
|
1375
|
+
if (defaultValueNode) param.defaultValue = defaultValueNode.text;
|
|
1376
|
+
if (isRest) param.isRest = true;
|
|
1377
|
+
params.push(param);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return params;
|
|
1382
|
+
}
|
|
1383
|
+
handleComment(node, metadata) {
|
|
1384
|
+
if (node.text.startsWith("#'")) {
|
|
1385
|
+
this.pushIfUnique(metadata.patterns, "Roxygen Documentation");
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Roxygen tags live inside a single `comment` token — the grammar exposes no
|
|
1390
|
+
* sub-structure for them — so a tag is recognised only at a tag position
|
|
1391
|
+
* (start of line, after `#'` and optional spaces). A loose substring test also
|
|
1392
|
+
* fired on prose such as "not @exported".
|
|
1393
|
+
*/
|
|
1394
|
+
roxygenHasTag(doc, tag) {
|
|
1395
|
+
let lineStart = 0;
|
|
1396
|
+
while (lineStart <= doc.length) {
|
|
1397
|
+
let lineEnd = doc.indexOf("\n", lineStart);
|
|
1398
|
+
if (lineEnd === -1) lineEnd = doc.length;
|
|
1399
|
+
const line = doc.substring(lineStart, lineEnd);
|
|
1400
|
+
if (line.startsWith("#'")) {
|
|
1401
|
+
let i = 2;
|
|
1402
|
+
while (i < line.length && (line[i] === " " || line[i] === " ")) i++;
|
|
1403
|
+
if (line.startsWith(tag, i)) {
|
|
1404
|
+
const after = line.charAt(i + tag.length);
|
|
1405
|
+
if (after === "" || after === " " || after === " ") return true;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
lineStart = lineEnd + 1;
|
|
1409
|
+
}
|
|
1410
|
+
return false;
|
|
1411
|
+
}
|
|
1412
|
+
getArgumentsNode(node) {
|
|
1413
|
+
const byField = node.childForFieldName("arguments");
|
|
1414
|
+
if (byField) return byField;
|
|
1415
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1416
|
+
const c = node.child(i);
|
|
1417
|
+
if (c && c.type === "arguments") return c;
|
|
1418
|
+
}
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
/** Positional arguments of a call, in source order, `argument` nodes unwrapped. */
|
|
1422
|
+
positionalArguments(args) {
|
|
1423
|
+
const out = [];
|
|
1424
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1425
|
+
const arg = args.namedChild(i);
|
|
1426
|
+
if (!arg || arg.type === "comma") continue;
|
|
1427
|
+
if (arg.type !== "argument") {
|
|
1428
|
+
out.push(arg);
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
out.push(arg);
|
|
1432
|
+
}
|
|
1433
|
+
return out;
|
|
1434
|
+
}
|
|
1435
|
+
/** The first POSITIONAL (unnamed) argument value of a call, or null. */
|
|
1436
|
+
firstPositionalValue(args) {
|
|
1437
|
+
if (!args) return null;
|
|
1438
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1439
|
+
const arg = args.namedChild(i);
|
|
1440
|
+
if (!arg || arg.type === "comma") continue;
|
|
1441
|
+
if (arg.type === "argument") {
|
|
1442
|
+
if (arg.childForFieldName("name")) continue;
|
|
1443
|
+
return arg.childForFieldName("value");
|
|
1444
|
+
}
|
|
1445
|
+
return arg;
|
|
1446
|
+
}
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
// ------------------------------------------------------------------ member reads
|
|
1450
|
+
/**
|
|
1451
|
+
* The `arguments` of a `subset2`, i.e. `x[[ ... ]]` -- not the same node as the
|
|
1452
|
+
* `arguments` of a `call`. The grammar gives the field the same name in both, so
|
|
1453
|
+
* reading it off the wrong node kind returns null and the count is 0 rather than
|
|
1454
|
+
* wrong, which is the failure mode worth avoiding here.
|
|
1455
|
+
*/
|
|
1456
|
+
subset2Arguments(node) {
|
|
1457
|
+
if (node.type !== "subset2") return null;
|
|
1458
|
+
return node.childForFieldName("arguments");
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* The NAME a `subset2` indexes by, when it indexes by a string literal:
|
|
1462
|
+
* `df[["col"]]` -> `col`. Returns null for `df[[i]]` and `df[[get_col()]]`,
|
|
1463
|
+
* because a computed index does not name a member and naming one anyway would be
|
|
1464
|
+
* the fabrication this lane's ontology workstream warns about.
|
|
1465
|
+
*
|
|
1466
|
+
* Only `[[` is read, never `[`. In R the two are not interchangeable: `df[["col"]]`
|
|
1467
|
+
* is a member lookup, while `df["col"]` on a plain list is a CHARACTER SUBSET that
|
|
1468
|
+
* evaluates to NULL, and on a data.frame it is a column selection by a rule R
|
|
1469
|
+
* documents separately (exact, then partial match). Publishing the string index of
|
|
1470
|
+
* a `[` as a member name would put a `uses` edge on an expression R does not treat
|
|
1471
|
+
* as a member read at all.
|
|
1472
|
+
*/
|
|
1473
|
+
stringSubsetIndex(node) {
|
|
1474
|
+
const args = this.subset2Arguments(node);
|
|
1475
|
+
if (!args) return null;
|
|
1476
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1477
|
+
const arg = args.namedChild(i);
|
|
1478
|
+
if (!arg || arg.type !== "argument") continue;
|
|
1479
|
+
const value = arg.childForFieldName("value");
|
|
1480
|
+
if (value && value.type === "string") {
|
|
1481
|
+
const name = this.stringNodeText(value);
|
|
1482
|
+
if (name) return name;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* The MEMBER NAME an `extract_operator` reads, or null when the right side does
|
|
1489
|
+
* not name one. `obj$member` and `obj@slot` name a member by construction; a
|
|
1490
|
+
* computed right side (`obj[[which()]]` is a different node, but `obj$f()`-shaped
|
|
1491
|
+
* and interpolated forms exist) does not, so no edge is emitted and the case is
|
|
1492
|
+
* counted.
|
|
1493
|
+
*/
|
|
1494
|
+
extractOperatorMember(node) {
|
|
1495
|
+
if (node.type !== "extract_operator") return null;
|
|
1496
|
+
const operator = node.childForFieldName("operator")?.text;
|
|
1497
|
+
if (operator !== "$" && operator !== "@") return null;
|
|
1498
|
+
const rhs = node.childForFieldName("rhs");
|
|
1499
|
+
if (!rhs) return null;
|
|
1500
|
+
if (rhs.type === "identifier") return this.unquoteIdentifier(rhs.text) || null;
|
|
1501
|
+
if (rhs.type === "string") return this.stringNodeText(rhs) || null;
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Is this node the `function` field of an enclosing `call`?
|
|
1506
|
+
*
|
|
1507
|
+
* Compared by RANGE and not by object identity: web-tree-sitter allocates a fresh
|
|
1508
|
+
* JS wrapper on every accessor call, so `parent.childForFieldName("function") === n`
|
|
1509
|
+
* is false for the very node it just returned. The walk that built this gate first
|
|
1510
|
+
* compared identities and reported zero callee-position nodes out of 11,216 --
|
|
1511
|
+
* a number that reads as a fact about R and is a bug in the gate.
|
|
1512
|
+
*
|
|
1513
|
+
* `pkg::obj$fn(...)` is the wrapped form the second clause exists for: the
|
|
1514
|
+
* `namespace_operator` occupies the call's function field and the `extract_operator`
|
|
1515
|
+
* is its `rhs`.
|
|
1516
|
+
*/
|
|
1517
|
+
isCalleePosition(node) {
|
|
1518
|
+
const parent = node.parent;
|
|
1519
|
+
if (!parent) return false;
|
|
1520
|
+
if (parent.type === "call") return this.sameNode(parent.childForFieldName("function"), node);
|
|
1521
|
+
if (parent.type === "extract_operator" || parent.type === "namespace_operator") {
|
|
1522
|
+
if (this.sameNode(parent.childForFieldName("rhs"), node)) return this.isCalleePosition(parent);
|
|
1523
|
+
}
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Is this member access the TARGET of an assignment, i.e. a write?
|
|
1528
|
+
* `obj$field <- v` and `obj$field <<- v` store into the member; they do not read
|
|
1529
|
+
* it. `x <- obj$field` reads. The two sides are read off the grammar's operator
|
|
1530
|
+
* field through `assignmentSides`, so the rightward forms are handled by the same
|
|
1531
|
+
* rule that already decides which side an assignment binds -- not by a second,
|
|
1532
|
+
* possibly disagreeing, test.
|
|
1533
|
+
*/
|
|
1534
|
+
isMemberWrite(node) {
|
|
1535
|
+
const parent = node.parent;
|
|
1536
|
+
if (!parent || parent.type !== "binary_operator") return false;
|
|
1537
|
+
return this.sameNode(this.assignmentSides(parent).target, node);
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* A `uses` edge for one member read of a value.
|
|
1541
|
+
*
|
|
1542
|
+
* THE RULE, and why it is this rule: in R, `obj$member`, `obj@slot` and
|
|
1543
|
+
* `df[["col"]]` all READ a named member of something. Reading a member is not
|
|
1544
|
+
* calling anything, so it is not a `calls` edge, and it is not a package
|
|
1545
|
+
* dependency, so it is not an `imports` edge. It is the same construct the Python
|
|
1546
|
+
* and Julia lanes publish as `uses` (`python:1425` `isMemberRead`,
|
|
1547
|
+
* `script/m14/oracles/julia-refs.ts` on `a.b`), and it is by a wide margin the
|
|
1548
|
+
* largest thing this analyzer was not reading: measured over 1,890 real R files
|
|
1549
|
+
* (`validation/m14/fixtures/r/probe-surfaces.ts`), the corpus holds 9,108
|
|
1550
|
+
* `extract_operator` nodes of which **1,012 are the callee of a call** and
|
|
1551
|
+
* **8,096 are not**. Before this method all 8,096 produced nothing.
|
|
1552
|
+
*
|
|
1553
|
+
* FOUR GATES, each of which exists because the alternative is a false edge:
|
|
1554
|
+
*
|
|
1555
|
+
* 1. not the callee of a call -- `obj$method()` is already a `calls` edge whose
|
|
1556
|
+
* target is `obj$method`. Publishing a second edge to `method` would be the
|
|
1557
|
+
* same fact under two labels, which is the `uses`/`references`/`calls`
|
|
1558
|
+
* confusion the workstream names as question 4.
|
|
1559
|
+
* 2. not the target of an assignment -- `obj$field <- v` writes.
|
|
1560
|
+
* 3. the member must be NAMEABLE -- a computed right side yields no edge, and is
|
|
1561
|
+
* counted rather than dropped.
|
|
1562
|
+
* 4. an enclosing named symbol must exist -- the same attribution rule `calls`
|
|
1563
|
+
* uses, including the 0.7 closure case, so the two surfaces cannot disagree
|
|
1564
|
+
* about who owns a body. 55.6% of R's call sites are at file scope, so this
|
|
1565
|
+
* gate suppresses a large number and that number is reported in
|
|
1566
|
+
* `metadata.callAttribution.memberReadsUnresolvedScope` rather than lost.
|
|
1567
|
+
*
|
|
1568
|
+
* CONFIDENCE is the ATTRIBUTION's, not a constant, and that is the whole point of
|
|
1569
|
+
* reading it from `enclosingAttribution` rather than writing 1.0: the member name
|
|
1570
|
+
* is a certain parse fact, but the OWNER of a body reached through an anonymous
|
|
1571
|
+
* function is inferred, so the edge is 0.7 there exactly as a `calls` edge in the
|
|
1572
|
+
* same body is. A `uses` edge that reported 1.0 while the `calls` edge beside it
|
|
1573
|
+
* reported 0.7 would be a confidence that measures the label rather than the
|
|
1574
|
+
* evidence, and the honesty contract §4 ratio would then understate this lane's
|
|
1575
|
+
* guesswork by exactly the closure cases. Every edge below 1.0 carries a
|
|
1576
|
+
* `reasonCode` and a `reason`.
|
|
1577
|
+
*
|
|
1578
|
+
* The RECEIVER is recorded in metadata, not in the edge target, so the edge binds
|
|
1579
|
+
* to a symbol with the member's own name -- which is what makes it joinable with
|
|
1580
|
+
* the `property` symbols R6/R5/S4/S7 publish, and is the convention the Python and
|
|
1581
|
+
* Julia lanes follow.
|
|
1582
|
+
*/
|
|
1583
|
+
handleMemberRead(node, fnStack, relationships, ambiguity) {
|
|
1584
|
+
const member = node.type === "extract_operator" ? this.extractOperatorMember(node) : this.stringSubsetIndex(node);
|
|
1585
|
+
if (!member) {
|
|
1586
|
+
if (node.type === "extract_operator") ambiguity.memberReadsWithComputedIndex++;
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (this.isCalleePosition(node)) {
|
|
1590
|
+
ambiguity.memberReadsSuppressedByCall++;
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (this.isMemberWrite(node)) {
|
|
1594
|
+
ambiguity.memberWrites++;
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const attribution = this.enclosingAttribution(fnStack);
|
|
1598
|
+
if (!attribution) {
|
|
1599
|
+
ambiguity.memberReadsUnresolvedScope++;
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const receiver = this.memberReadReceiver(node);
|
|
1603
|
+
const inferred = attribution.subType !== "function";
|
|
1604
|
+
this.pushRel(
|
|
1605
|
+
relationships,
|
|
1606
|
+
attribution.name,
|
|
1607
|
+
member,
|
|
1608
|
+
ASTEngine.RelationshipType.Uses,
|
|
1609
|
+
attribution.confidence,
|
|
1610
|
+
{
|
|
1611
|
+
rSpecific: true,
|
|
1612
|
+
relationshipSubType: "member_read",
|
|
1613
|
+
memberAccessOperator: node.type === "extract_operator" ? node.childForFieldName("operator")?.text : "[[",
|
|
1614
|
+
...receiver ? { receiver } : {},
|
|
1615
|
+
...inferred ? { attributionSubType: attribution.subType, reasonCode: attribution.reason, reason: attribution.reason } : {}
|
|
1616
|
+
},
|
|
1617
|
+
this.memberNameNode(node)
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* The receiver of a member read, when it is itself a plain NAME, for metadata
|
|
1622
|
+
* only. A receiver that is an expression (`pkg::obj$fn`, `get_obj()$field`) is
|
|
1623
|
+
* omitted rather than interpolated: the edge target never carries source text, and
|
|
1624
|
+
* a metadata field that quotes the input back is the same smell in a place where
|
|
1625
|
+
* nobody is looking at it.
|
|
1626
|
+
*/
|
|
1627
|
+
memberReadReceiver(node) {
|
|
1628
|
+
const lhs = node.type === "extract_operator" ? node.childForFieldName("lhs") : node.childForFieldName("function");
|
|
1629
|
+
if (!lhs) return null;
|
|
1630
|
+
if (lhs.type === "identifier") return this.unquoteIdentifier(lhs.text) || null;
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
/** The node that WRITES the member name, which is where the edge's line comes from. */
|
|
1634
|
+
memberNameNode(node) {
|
|
1635
|
+
if (node.type === "extract_operator") {
|
|
1636
|
+
const rhs = node.childForFieldName("rhs");
|
|
1637
|
+
if (rhs && (rhs.type === "identifier" || rhs.type === "string")) return rhs;
|
|
1638
|
+
}
|
|
1639
|
+
const args = this.subset2Arguments(node);
|
|
1640
|
+
if (args) {
|
|
1641
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1642
|
+
const arg = args.namedChild(i);
|
|
1643
|
+
if (!arg || arg.type !== "argument") continue;
|
|
1644
|
+
const value = arg.childForFieldName("value");
|
|
1645
|
+
if (value && value.type === "string") return value;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return node;
|
|
1649
|
+
}
|
|
1650
|
+
// ----------------------------------------------------------------------- call
|
|
1651
|
+
handleCall(node, symbols, imports, exports, relationships, metadata, declaredTypes, ambiguity, fnStack) {
|
|
1652
|
+
const fnNode = node.childForFieldName("function");
|
|
1653
|
+
if (!fnNode) return;
|
|
1654
|
+
const fnName = this.calleeName(fnNode);
|
|
1655
|
+
const args = this.getArgumentsNode(node);
|
|
1656
|
+
if (fnNode.type === "namespace_operator") {
|
|
1657
|
+
const ns = fnNode.childForFieldName("lhs");
|
|
1658
|
+
const op = fnNode.childForFieldName("operator")?.text;
|
|
1659
|
+
if (ns && op && NAMESPACE_OPERATORS.has(op)) {
|
|
1660
|
+
imports.push({
|
|
1661
|
+
module: this.unquoteIdentifier(ns.text),
|
|
1662
|
+
names: [],
|
|
1663
|
+
range: this.getRange(fnNode),
|
|
1664
|
+
isWildcard: true,
|
|
1665
|
+
metadata: { rSpecific: true, accessOperator: op }
|
|
1666
|
+
});
|
|
1667
|
+
this.pushRel(relationships, "global", this.unquoteIdentifier(ns.text), ASTEngine.RelationshipType.Imports, 1, {
|
|
1668
|
+
rSpecific: true,
|
|
1669
|
+
relationshipSubType: "namespace_access",
|
|
1670
|
+
accessOperator: op
|
|
1671
|
+
}, ns);
|
|
1672
|
+
this.pushRel(relationships, "global", this.unquoteIdentifier(ns.text), ASTEngine.RelationshipType.References, 1, {
|
|
1673
|
+
rSpecific: true,
|
|
1674
|
+
relationshipSubType: "namespace_access",
|
|
1675
|
+
accessOperator: op
|
|
1676
|
+
}, ns);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (fnName === "UseMethod") {
|
|
1680
|
+
this.pushIfUnique(metadata.patterns, "S3 Object System (Generics)");
|
|
1681
|
+
}
|
|
1682
|
+
if (APPLY_FAMILY.has(fnName)) this.pushIfUnique(metadata.patterns, "Apply Family");
|
|
1683
|
+
if (DATA_FRAME_CONSTRUCTORS.has(fnName)) this.pushIfUnique(metadata.patterns, "Data Frames");
|
|
1684
|
+
if (STATISTICAL_MODELS.has(fnName) || STATISTICAL_MODELS.has(fnNode.text)) {
|
|
1685
|
+
if (metadata.statisticalFocus !== void 0) metadata.statisticalFocus += 30;
|
|
1686
|
+
}
|
|
1687
|
+
if (TIDYVERSE_NSE.has(fnName)) {
|
|
1688
|
+
this.pushIfUnique(metadata.patterns, "Tidyverse Data Masking (NSE)");
|
|
1689
|
+
if (metadata.dataManipulationIntensity !== void 0) metadata.dataManipulationIntensity += 10;
|
|
1690
|
+
}
|
|
1691
|
+
if (fnName === "NS") this.pushIfUnique(metadata.patterns, "Shiny Module Namespace (NS)");
|
|
1692
|
+
if (ERROR_HANDLERS.has(fnName)) this.pushIfUnique(metadata.patterns, "Error Handling");
|
|
1693
|
+
if (CLEANUP_HANDLERS.has(fnName)) this.pushIfUnique(metadata.patterns, "Cleanup Handlers");
|
|
1694
|
+
if (NSE_CAPTURERS.has(fnName)) this.pushIfUnique(metadata.patterns, "Non-Standard Evaluation (NSE)");
|
|
1695
|
+
const attribution = this.enclosingAttribution(fnStack);
|
|
1696
|
+
if (attribution) {
|
|
1697
|
+
const relMeta = { rSpecific: true };
|
|
1698
|
+
if (attribution.subType !== "function") {
|
|
1699
|
+
relMeta.relationshipSubType = attribution.subType;
|
|
1700
|
+
if (attribution.reason) {
|
|
1701
|
+
relMeta.reasonCode = attribution.reason;
|
|
1702
|
+
relMeta.reason = attribution.reason;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
const callTarget = this.callTargetName(fnNode, fnName);
|
|
1706
|
+
if (!callTarget) ambiguity.callsToUnnameableCallee++;
|
|
1707
|
+
else this.pushRel(relationships, attribution.name, callTarget, ASTEngine.RelationshipType.Calls, attribution.confidence, relMeta, this.callTargetNode(fnNode, callTarget));
|
|
1708
|
+
if (attribution.confidence < 1) ambiguity.callsAttributedThroughClosure++;
|
|
1709
|
+
const constructorName = this.constructorTarget(fnNode);
|
|
1710
|
+
if (constructorName && declaredTypes.has(constructorName)) {
|
|
1711
|
+
this.pushRel(relationships, attribution.name, constructorName, ASTEngine.RelationshipType.Instantiates, 0.9, { isRConstructor: true, reasonCode: "declared_class_constructor" }, fnNode);
|
|
1712
|
+
}
|
|
1713
|
+
if (fnName === "new") {
|
|
1714
|
+
const cls = this.firstStringArgumentOf(node, 0);
|
|
1715
|
+
if (cls && declaredTypes.has(cls)) {
|
|
1716
|
+
this.pushRel(relationships, attribution.name, cls, ASTEngine.RelationshipType.Instantiates, 0.9, { isS4Constructor: true, reasonCode: "declared_class_constructor" }, fnNode);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
} else {
|
|
1720
|
+
ambiguity.callsUnresolvedScope++;
|
|
1721
|
+
}
|
|
1722
|
+
if (fnName === "library" || fnName === "require") {
|
|
1723
|
+
this.handleLibraryCall(node, args, imports, relationships);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
if (fnName === "rm" && fnStack.length === 0) {
|
|
1727
|
+
const removed = this.removeTopLevelBindings(node, args, symbols, relationships, metadata);
|
|
1728
|
+
if (removed) {
|
|
1729
|
+
if (metadata.ambiguity === void 0) metadata.ambiguity = {};
|
|
1730
|
+
metadata.ambiguity.removedByTopLevelRm = (metadata.ambiguity.removedByTopLevelRm ?? 0) + removed;
|
|
1731
|
+
}
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
if (fnName === "sourceCpp") {
|
|
1735
|
+
this.pushIfUnique(metadata.patterns, "Rcpp / C++ Integration");
|
|
1736
|
+
this.handleSourceCall(node, args, imports, relationships, {
|
|
1737
|
+
relationshipSubType: "rcpp_source",
|
|
1738
|
+
language: "cpp",
|
|
1739
|
+
crossLanguage: true
|
|
1740
|
+
});
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (fnName === "source") {
|
|
1744
|
+
this.handleSourceCall(node, args, imports, relationships, { relationshipSubType: "r_source" });
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (fnName === "setClass") {
|
|
1748
|
+
this.pushIfUnique(metadata.patterns, "S4 Object System");
|
|
1749
|
+
this.handleSetClass(node, args, symbols, exports, relationships, declaredTypes);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (fnName === "setRefClass") {
|
|
1753
|
+
const parent = node.parent;
|
|
1754
|
+
const isAssignmentValue = parent?.type === "binary_operator" && this.sameNode(this.assignmentSides(parent).value, node);
|
|
1755
|
+
if (!isAssignmentValue && fnStack.length === 0 && !this.hasLocalBoundary(node)) {
|
|
1756
|
+
const declared = this.firstStringArgumentOf(node, 0);
|
|
1757
|
+
if (declared) {
|
|
1758
|
+
this.declareR5Class(
|
|
1759
|
+
node,
|
|
1760
|
+
declared,
|
|
1761
|
+
node,
|
|
1762
|
+
symbols,
|
|
1763
|
+
exports,
|
|
1764
|
+
relationships,
|
|
1765
|
+
declaredTypes,
|
|
1766
|
+
this.extractDocstring(node),
|
|
1767
|
+
metadata.patterns
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
if (fnName === "setGeneric") {
|
|
1774
|
+
this.handleSetGenericOrMethod(node, args, symbols, exports, relationships, ambiguity);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (fnName === "setMethod") {
|
|
1778
|
+
this.handleSetGenericOrMethod(node, args, symbols, exports, relationships, ambiguity);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
if (fnName === "setValidity") {
|
|
1782
|
+
this.handleSetValidity(node, args, symbols, relationships);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* `library(pkg)` / `require(pkg)` declare ONE dependency: the first POSITIONAL
|
|
1788
|
+
* argument. Every other argument is an option — `warn.conflicts`, `quietly`,
|
|
1789
|
+
* `character.only`, `pos`, `lib.loc`, `mask.ok`, `exclude`, `include.only`,
|
|
1790
|
+
* `attach.required` — and the previous implementation turned each of those
|
|
1791
|
+
* option NAMES into both an import and a `references` edge, publishing phantom
|
|
1792
|
+
* packages. `library(dplyr, warn.conflicts = FALSE)` is the polite form and is
|
|
1793
|
+
* everywhere in CRAN code.
|
|
1794
|
+
*
|
|
1795
|
+
* Confidence is 1.0 for a string literal (`library("dplyr")` names the package
|
|
1796
|
+
* outright) and 0.8 for a bare identifier, which with `character.only = TRUE`
|
|
1797
|
+
* may hold a computed package name.
|
|
1798
|
+
*/
|
|
1799
|
+
handleLibraryCall(node, args, imports, relationships) {
|
|
1800
|
+
if (!args) return;
|
|
1801
|
+
let value = null;
|
|
1802
|
+
for (let i = 0; i < args.namedChildCount && !value; i++) {
|
|
1803
|
+
const arg = args.namedChild(i);
|
|
1804
|
+
if (!arg || arg.type === "comma") continue;
|
|
1805
|
+
if (arg.type === "argument") {
|
|
1806
|
+
const nameNode = arg.childForFieldName("name");
|
|
1807
|
+
if (nameNode) {
|
|
1808
|
+
if (nameNode.text === "package") value = arg.childForFieldName("value");
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
value = arg.childForFieldName("value");
|
|
1812
|
+
} else {
|
|
1813
|
+
value = arg;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (!value) return;
|
|
1817
|
+
let moduleName = "";
|
|
1818
|
+
let confidence = 0.8;
|
|
1819
|
+
if (value.type === "string") {
|
|
1820
|
+
moduleName = this.stringNodeText(value);
|
|
1821
|
+
confidence = 1;
|
|
1822
|
+
} else if (value.type === "identifier") {
|
|
1823
|
+
moduleName = this.unquoteIdentifier(value.text);
|
|
1824
|
+
}
|
|
1825
|
+
if (!moduleName) return;
|
|
1826
|
+
imports.push({
|
|
1827
|
+
module: moduleName,
|
|
1828
|
+
names: [],
|
|
1829
|
+
range: this.getRange(node),
|
|
1830
|
+
isWildcard: true,
|
|
1831
|
+
metadata: { rSpecific: true, declaredBy: "library" }
|
|
1832
|
+
});
|
|
1833
|
+
this.pushRel(
|
|
1834
|
+
relationships,
|
|
1835
|
+
"global",
|
|
1836
|
+
moduleName,
|
|
1837
|
+
ASTEngine.RelationshipType.References,
|
|
1838
|
+
confidence,
|
|
1839
|
+
{
|
|
1840
|
+
rSpecific: true,
|
|
1841
|
+
relationshipSubType: "library_dependency",
|
|
1842
|
+
reasonCode: confidence < 1 ? "library_bare_identifier" : "library_string_literal"
|
|
1843
|
+
},
|
|
1844
|
+
value
|
|
1845
|
+
);
|
|
1846
|
+
this.pushRel(relationships, "global", moduleName, ASTEngine.RelationshipType.Imports, confidence, {
|
|
1847
|
+
rSpecific: true,
|
|
1848
|
+
relationshipSubType: "library_import",
|
|
1849
|
+
reasonCode: confidence < 1 ? "library_bare_identifier" : "library_string_literal"
|
|
1850
|
+
}, value);
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* `source("helpers.R")` / `sourceCpp("code.cpp")` declare a file dependency.
|
|
1854
|
+
* The first argument is read as the first `argument` child of the `arguments`
|
|
1855
|
+
* node; indexing `child(0)` instead returned the `(` token, so the guard never
|
|
1856
|
+
* matched and these calls produced no dependency at all while still advertising
|
|
1857
|
+
* the Rcpp pattern in metadata.
|
|
1858
|
+
*/
|
|
1859
|
+
handleSourceCall(node, args, imports, relationships, meta) {
|
|
1860
|
+
const value = this.firstPositionalValue(args);
|
|
1861
|
+
if (!value || value.type !== "string") return;
|
|
1862
|
+
const path = this.stringNodeText(value);
|
|
1863
|
+
if (!path) return;
|
|
1864
|
+
imports.push({
|
|
1865
|
+
module: path,
|
|
1866
|
+
names: [],
|
|
1867
|
+
range: this.getRange(node),
|
|
1868
|
+
isWildcard: true,
|
|
1869
|
+
metadata: { rSpecific: true, ...meta }
|
|
1870
|
+
});
|
|
1871
|
+
this.pushRel(relationships, "global", path, ASTEngine.RelationshipType.References, 1, { ...meta, relationshipSubType: meta.relationshipSubType }, value);
|
|
1872
|
+
this.pushRel(relationships, "global", path, ASTEngine.RelationshipType.Imports, 1, { ...meta, relationshipSubType: meta.relationshipSubType }, value);
|
|
1873
|
+
}
|
|
1874
|
+
handleSetClass(node, args, symbols, exports, relationships, declaredTypes) {
|
|
1875
|
+
if (!args) return;
|
|
1876
|
+
const className = this.firstStringArgumentOf(node, 0);
|
|
1877
|
+
if (!className) return;
|
|
1878
|
+
const baseClasses = this.extractNamedArguments(node, "contains");
|
|
1879
|
+
const composesList = this.compositionClauseIsList(node, "contains");
|
|
1880
|
+
const doc = this.extractDocstring(node);
|
|
1881
|
+
symbols.push({
|
|
1882
|
+
name: className,
|
|
1883
|
+
kind: ASTEngine.SymbolKind.Class,
|
|
1884
|
+
range: this.getRange(node),
|
|
1885
|
+
docstring: doc,
|
|
1886
|
+
metadata: { rSpecific: true, type: "S4", isS4Class: true, baseClasses }
|
|
1887
|
+
});
|
|
1888
|
+
declaredTypes.add(className);
|
|
1889
|
+
const slotNames = /* @__PURE__ */ new Map();
|
|
1890
|
+
for (const slot of this.extractSlotKeys(node)) {
|
|
1891
|
+
if (!slotNames.has(slot.name)) slotNames.set(slot.name, slot);
|
|
1892
|
+
}
|
|
1893
|
+
for (const slotName of slotNames.values()) {
|
|
1894
|
+
symbols.push({
|
|
1895
|
+
name: slotName.name,
|
|
1896
|
+
kind: ASTEngine.SymbolKind.Property,
|
|
1897
|
+
range: slotName.range ?? this.getRange(node),
|
|
1898
|
+
ownerName: className,
|
|
1899
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
1900
|
+
docstring: "",
|
|
1901
|
+
metadata: { rSpecific: true, isS4Slot: true, declaredVia: slotName.via }
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
for (const bc of baseClasses) {
|
|
1905
|
+
const type = composesList ? ASTEngine.RelationshipType.MixesIn : ASTEngine.RelationshipType.Extends;
|
|
1906
|
+
const subType = composesList ? "s4_composes" : "s4_inherits";
|
|
1907
|
+
this.pushRel(relationships, className, bc, type, 1, { relationshipSubType: subType }, this.namedArgumentNode(node, "contains", bc));
|
|
1908
|
+
}
|
|
1909
|
+
if (doc && this.roxygenHasTag(doc, "@export")) exports.push({ name: className, kind: ASTEngine.SymbolKind.Class });
|
|
1910
|
+
}
|
|
1911
|
+
handleSetGenericOrMethod(node, args, symbols, exports, relationships, ambiguity) {
|
|
1912
|
+
if (!args) return;
|
|
1913
|
+
const fnName = this.calleeName(node.childForFieldName("function"));
|
|
1914
|
+
const methodName = this.firstStringArgumentOf(node, 0);
|
|
1915
|
+
if (!methodName) return;
|
|
1916
|
+
let ownerName;
|
|
1917
|
+
if (fnName === "setMethod") {
|
|
1918
|
+
ownerName = this.setMethodOwner(args);
|
|
1919
|
+
}
|
|
1920
|
+
const body = this.lastPositionalValue(args);
|
|
1921
|
+
let parameters;
|
|
1922
|
+
let signature;
|
|
1923
|
+
if (body && body.type === "function_definition") {
|
|
1924
|
+
parameters = this.extractParameters(body);
|
|
1925
|
+
signature = this.extractSignature(body);
|
|
1926
|
+
}
|
|
1927
|
+
const doc = this.extractDocstring(node);
|
|
1928
|
+
symbols.push({
|
|
1929
|
+
name: methodName,
|
|
1930
|
+
kind: fnName === "setMethod" ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function,
|
|
1931
|
+
range: this.getRange(node),
|
|
1932
|
+
ownerName,
|
|
1933
|
+
ownerKind: ownerName ? ASTEngine.SymbolKind.Class : void 0,
|
|
1934
|
+
signature,
|
|
1935
|
+
parameters,
|
|
1936
|
+
docstring: doc,
|
|
1937
|
+
metadata: {
|
|
1938
|
+
rSpecific: true,
|
|
1939
|
+
type: fnName === "setGeneric" ? "S4_Generic" : "S4_Method",
|
|
1940
|
+
isS4Generic: fnName === "setGeneric",
|
|
1941
|
+
isS4Method: fnName === "setMethod"
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
if (doc && this.roxygenHasTag(doc, "@export"))
|
|
1945
|
+
exports.push({
|
|
1946
|
+
name: methodName,
|
|
1947
|
+
kind: fnName === "setMethod" ? ASTEngine.SymbolKind.Method : ASTEngine.SymbolKind.Function
|
|
1948
|
+
});
|
|
1949
|
+
if (fnName === "setMethod" && ownerName) {
|
|
1950
|
+
this.pushRel(relationships, ownerName, methodName, ASTEngine.RelationshipType.Contains, 1, { rSpecific: true, relationshipSubType: "s4_method" }, node);
|
|
1951
|
+
}
|
|
1952
|
+
void ambiguity;
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* `setValidity("Class", function(object) ...)` declares a class-level
|
|
1956
|
+
* validator. It was not handled at all, so a validation rule attached to an
|
|
1957
|
+
* S4 class was invisible.
|
|
1958
|
+
*/
|
|
1959
|
+
handleSetValidity(node, args, symbols, relationships) {
|
|
1960
|
+
if (!args) return;
|
|
1961
|
+
const className = this.firstStringArgumentOf(node, 0);
|
|
1962
|
+
if (!className) return;
|
|
1963
|
+
const body = this.lastPositionalValue(args);
|
|
1964
|
+
let parameters;
|
|
1965
|
+
let signature;
|
|
1966
|
+
if (body && body.type === "function_definition") {
|
|
1967
|
+
parameters = this.extractParameters(body);
|
|
1968
|
+
signature = this.extractSignature(body);
|
|
1969
|
+
}
|
|
1970
|
+
symbols.push({
|
|
1971
|
+
name: "validity",
|
|
1972
|
+
kind: ASTEngine.SymbolKind.Method,
|
|
1973
|
+
range: this.getRange(node),
|
|
1974
|
+
ownerName: className,
|
|
1975
|
+
ownerKind: ASTEngine.SymbolKind.Class,
|
|
1976
|
+
signature,
|
|
1977
|
+
parameters,
|
|
1978
|
+
docstring: this.extractDocstring(node),
|
|
1979
|
+
metadata: { rSpecific: true, isS4Validity: true, ownerClass: className, objectSystem: "S4" }
|
|
1980
|
+
});
|
|
1981
|
+
this.pushRel(relationships, className, "validity", ASTEngine.RelationshipType.Contains, 1, { rSpecific: true, relationshipSubType: "s4_validity" }, node);
|
|
1982
|
+
}
|
|
1983
|
+
/** The class named by a `setMethod` second argument, string or `signature(...)`. */
|
|
1984
|
+
setMethodOwner(args) {
|
|
1985
|
+
let seen = 0;
|
|
1986
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1987
|
+
const arg = args.namedChild(i);
|
|
1988
|
+
if (!arg || arg.type === "comma") continue;
|
|
1989
|
+
if (arg.type === "argument") {
|
|
1990
|
+
if (arg.childForFieldName("name")) continue;
|
|
1991
|
+
const value = arg.childForFieldName("value");
|
|
1992
|
+
if (!value) continue;
|
|
1993
|
+
if (seen === 1) {
|
|
1994
|
+
if (value.type === "string") return this.stringNodeText(value);
|
|
1995
|
+
if (value.type === "call" && this.calleeName(value.childForFieldName("function")) === "signature") {
|
|
1996
|
+
const inner = this.getArgumentsNode(value);
|
|
1997
|
+
const first = this.firstPositionalValue(inner);
|
|
1998
|
+
if (first && first.type === "string") return this.stringNodeText(first);
|
|
1999
|
+
}
|
|
2000
|
+
return void 0;
|
|
2001
|
+
}
|
|
2002
|
+
seen++;
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
if (seen === 1) {
|
|
2006
|
+
if (arg.type === "string") return this.stringNodeText(arg);
|
|
2007
|
+
return void 0;
|
|
2008
|
+
}
|
|
2009
|
+
seen++;
|
|
2010
|
+
}
|
|
2011
|
+
return void 0;
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Attribute a call to the nearest enclosing symbol that the SYNTAX actually
|
|
2015
|
+
* names, and say how sure we are.
|
|
2016
|
+
*
|
|
2017
|
+
* Resolution order for each enclosing `function_definition`, innermost first:
|
|
2018
|
+
* 1. assigned to a name -> confidence 1.0
|
|
2019
|
+
* 2. a named R6/R5 list member -> confidence 1.0
|
|
2020
|
+
* 3. the body of a `setGeneric`/`setMethod` -> confidence 1.0
|
|
2021
|
+
* 4. any other closure -> attributed to the nearest
|
|
2022
|
+
* enclosing NAMED symbol, confidence 0.7, with a reason code; if there is
|
|
2023
|
+
* no enclosing symbol the call is counted unresolved rather than given an
|
|
2024
|
+
* invented source.
|
|
2025
|
+
*
|
|
2026
|
+
* Case 4 is the common case in idiomatic R: `lapply(x, function(v) f(v))`,
|
|
2027
|
+
* `tryCatch(..., error = function(e) ...)`, `shiny::moduleServer(id, function(id)
|
|
2028
|
+
* ...)`. The earlier implementation returned null for every one of them, so
|
|
2029
|
+
* idiomatic functional R contributed no call edges at all.
|
|
2030
|
+
*/
|
|
2031
|
+
enclosingAttribution(fnStack) {
|
|
2032
|
+
for (let i = fnStack.length - 1; i >= 0; i--) {
|
|
2033
|
+
const resolved = fnStack[i].attribution;
|
|
2034
|
+
if (!resolved) continue;
|
|
2035
|
+
if (i === fnStack.length - 1) return resolved;
|
|
2036
|
+
return {
|
|
2037
|
+
name: resolved.name,
|
|
2038
|
+
confidence: 0.7,
|
|
2039
|
+
subType: "closure_call",
|
|
2040
|
+
reason: "call appears inside an anonymous function expression; attributed to the nearest enclosing named symbol"
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
return null;
|
|
2044
|
+
}
|
|
2045
|
+
/** The symbol a `function_definition` node is the body of, if the syntax names one. */
|
|
2046
|
+
resolveFunctionOwner(fnDef) {
|
|
2047
|
+
const parent = fnDef.parent;
|
|
2048
|
+
if (!parent) return null;
|
|
2049
|
+
if (parent.type === "binary_operator") {
|
|
2050
|
+
const { target, value } = this.assignmentSides(parent);
|
|
2051
|
+
if (target?.type === "identifier" && value && this.sameNode(value, fnDef)) {
|
|
2052
|
+
return {
|
|
2053
|
+
name: this.unquoteIdentifier(target.text),
|
|
2054
|
+
confidence: 1,
|
|
2055
|
+
subType: "function",
|
|
2056
|
+
reason: ""
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
if (value && this.sameNode(value, fnDef)) {
|
|
2060
|
+
const s7 = this.findOwningS7Method(fnDef);
|
|
2061
|
+
if (s7) return { name: s7, confidence: 1, subType: "s7_method", reason: "" };
|
|
2062
|
+
}
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
if (parent.type === "argument") {
|
|
2066
|
+
const memberOwner = this.findOwningObjectSystem(fnDef);
|
|
2067
|
+
if (memberOwner) {
|
|
2068
|
+
return {
|
|
2069
|
+
name: memberOwner.methodName,
|
|
2070
|
+
confidence: 1,
|
|
2071
|
+
subType: memberOwner.subType,
|
|
2072
|
+
reason: ""
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
const s4 = this.findOwningS4Declaration(fnDef);
|
|
2076
|
+
if (s4) {
|
|
2077
|
+
return { name: s4, confidence: 1, subType: "s4_method", reason: "" };
|
|
2078
|
+
}
|
|
2079
|
+
return null;
|
|
2080
|
+
}
|
|
2081
|
+
return null;
|
|
2082
|
+
}
|
|
2083
|
+
/** If this function is the body of an `S7::method(...) <-` definition, name the generic. */
|
|
2084
|
+
findOwningS7Method(fnDef) {
|
|
2085
|
+
const parent = fnDef.parent;
|
|
2086
|
+
if (!parent || parent.type !== "binary_operator") return null;
|
|
2087
|
+
const { target, value } = this.assignmentSides(parent);
|
|
2088
|
+
if (!value || !this.sameNode(value, fnDef)) return null;
|
|
2089
|
+
if (!target || target.type !== "call") return null;
|
|
2090
|
+
const base = this.calleeName(target.childForFieldName("function"));
|
|
2091
|
+
if (base !== "method" && base !== "S7_method") return null;
|
|
2092
|
+
const generic = this.firstPositionalValue(this.getArgumentsNode(target));
|
|
2093
|
+
return generic ? this.nodeAsName(generic) || null : null;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* If this function is a member of an R6Class/setRefClass list, name its class.
|
|
2097
|
+
*
|
|
2098
|
+
* The chain is: function_definition -> argument (the `go = function() ...`
|
|
2099
|
+
* entry) -> arguments (of the `list(...)`) -> call (`list`) -> argument
|
|
2100
|
+
* (`public = list(...)`) -> arguments (of `R6Class`) -> call (`R6Class`).
|
|
2101
|
+
* The member-list NAME is on the argument wrapping the `list` call, not on the
|
|
2102
|
+
* method's own argument.
|
|
2103
|
+
*/
|
|
2104
|
+
findOwningObjectSystem(fnDef) {
|
|
2105
|
+
const methodArg = fnDef.parent;
|
|
2106
|
+
if (!methodArg || methodArg.type !== "argument") return null;
|
|
2107
|
+
const methodName = methodArg.childForFieldName("name")?.text ?? "";
|
|
2108
|
+
if (!methodName) return null;
|
|
2109
|
+
const listArgs = methodArg.parent;
|
|
2110
|
+
if (!listArgs || listArgs.type !== "arguments") return null;
|
|
2111
|
+
const listCall = listArgs.parent;
|
|
2112
|
+
if (!listCall || listCall.type !== "call") return null;
|
|
2113
|
+
if (this.calleeName(listCall.childForFieldName("function")) !== "list") return null;
|
|
2114
|
+
const memberArg = listCall.parent;
|
|
2115
|
+
if (!memberArg || memberArg.type !== "argument") return null;
|
|
2116
|
+
const listName = memberArg.childForFieldName("name")?.text ?? "";
|
|
2117
|
+
if (!CLASS_MEMBER_LISTS.has(listName) && !R5_MEMBER_LISTS.has(listName)) return null;
|
|
2118
|
+
const classArgs = memberArg.parent;
|
|
2119
|
+
if (!classArgs || classArgs.type !== "arguments") return null;
|
|
2120
|
+
const classCall = classArgs.parent;
|
|
2121
|
+
if (!classCall || classCall.type !== "call") return null;
|
|
2122
|
+
const base = this.calleeName(classCall.childForFieldName("function"));
|
|
2123
|
+
if (base !== "R6Class" && base !== "setRefClass") return null;
|
|
2124
|
+
return {
|
|
2125
|
+
className: this.assignmentTargetForCall(classCall) ?? "",
|
|
2126
|
+
methodName,
|
|
2127
|
+
subType: base === "R6Class" ? "r6_method" : "r5_method"
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
/** If this function is the body of a setGeneric/setMethod, return the declared name. */
|
|
2131
|
+
findOwningS4Declaration(fnDef) {
|
|
2132
|
+
const parent = fnDef.parent;
|
|
2133
|
+
if (!parent) return null;
|
|
2134
|
+
let p = parent;
|
|
2135
|
+
let depth = 0;
|
|
2136
|
+
while (p && depth < 4) {
|
|
2137
|
+
if (p.type === "call") {
|
|
2138
|
+
const base = this.calleeName(p.childForFieldName("function"));
|
|
2139
|
+
if (base === "setGeneric" || base === "setMethod") {
|
|
2140
|
+
const args = this.getArgumentsNode(p);
|
|
2141
|
+
const first = this.firstPositionalValue(args);
|
|
2142
|
+
if (first && first.type === "string") return this.stringNodeText(first);
|
|
2143
|
+
}
|
|
2144
|
+
return null;
|
|
2145
|
+
}
|
|
2146
|
+
if (p.type === "function_definition") return null;
|
|
2147
|
+
p = p.parent;
|
|
2148
|
+
depth++;
|
|
2149
|
+
}
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
/** The name a class-constructor call is assigned to, used to label R6 members. */
|
|
2153
|
+
assignmentTargetForCall(callNode) {
|
|
2154
|
+
const parent = callNode.parent;
|
|
2155
|
+
if (parent && parent.type === "binary_operator") {
|
|
2156
|
+
const target = this.assignmentTarget(parent);
|
|
2157
|
+
if (target) return this.unquoteIdentifier(target.text);
|
|
2158
|
+
}
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* The receiver of a constructor call, read from grammar FIELDS.
|
|
2163
|
+
* `Cls$new()` -> extract_operator(lhs=Cls, operator=$, rhs=new)
|
|
2164
|
+
* `pkg::Cls$new()` -> namespace_operator(rhs=extract_operator(...))
|
|
2165
|
+
* The previous `text.indexOf("$new")` was a source-text scan that also fired on
|
|
2166
|
+
* arguments and strings containing that sequence.
|
|
2167
|
+
*/
|
|
2168
|
+
constructorTarget(fnNode) {
|
|
2169
|
+
const candidate = fnNode.type === "namespace_operator" ? fnNode.childForFieldName("rhs") : fnNode;
|
|
2170
|
+
if (!candidate) return null;
|
|
2171
|
+
if (candidate.type !== "extract_operator") return null;
|
|
2172
|
+
const op = candidate.childForFieldName("operator")?.text;
|
|
2173
|
+
if (op !== "$" && op !== "@") return null;
|
|
2174
|
+
const rhs = candidate.childForFieldName("rhs");
|
|
2175
|
+
if (!rhs || rhs.type !== "identifier" || rhs.text !== "new") return null;
|
|
2176
|
+
const lhs = candidate.childForFieldName("lhs");
|
|
2177
|
+
if (!lhs || lhs.type !== "identifier") return null;
|
|
2178
|
+
return this.unquoteIdentifier(lhs.text);
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* The name a `calls` edge should carry: the callee as written, so a reader can
|
|
2182
|
+
* see `dplyr::filter`, `obj$method` and `Cls$new` rather than three different
|
|
2183
|
+
* functions that all end in the same word. Falls back to the bare name when the
|
|
2184
|
+
* function slot is not a simple callable form.
|
|
2185
|
+
*/
|
|
2186
|
+
callTargetName(fnNode, bare) {
|
|
2187
|
+
if (!bare) return "";
|
|
2188
|
+
if (fnNode.type === "identifier") return bare;
|
|
2189
|
+
if (fnNode.type === "namespace_operator" || fnNode.type === "extract_operator") {
|
|
2190
|
+
const ns = fnNode.childForFieldName("lhs");
|
|
2191
|
+
const op = fnNode.childForFieldName("operator")?.text;
|
|
2192
|
+
const rhs = fnNode.childForFieldName("rhs");
|
|
2193
|
+
if (ns && op && rhs) {
|
|
2194
|
+
const receiver = this.calleeName(ns);
|
|
2195
|
+
if (!receiver) return bare;
|
|
2196
|
+
const rhsName = rhs.type === "identifier" ? this.unquoteIdentifier(rhs.text) : rhs.text;
|
|
2197
|
+
return `${receiver}${op}${rhsName}`;
|
|
2198
|
+
}
|
|
2199
|
+
return bare;
|
|
2200
|
+
}
|
|
2201
|
+
return bare;
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* The node that WRITES the edge's target, which is the node the edge's `line_number`
|
|
2205
|
+
* must come from.
|
|
2206
|
+
*
|
|
2207
|
+
* The rule is: **the line where the target's LAST name component is written.** For
|
|
2208
|
+
* `pkg::fn` and `obj$method` on one line that is the same line either way, so the
|
|
2209
|
+
* common case is unaffected. It matters when R splits a chain, which the promises
|
|
2210
|
+
* package does routinely:
|
|
2211
|
+
*
|
|
2212
|
+
* p$value <- p$value$ <- line 743: `value` is here
|
|
2213
|
+
* then(function(value) { <- line 744: `then` is HERE
|
|
2214
|
+
*
|
|
2215
|
+
* The target is `value$then`, and the method name is what identifies the call and what
|
|
2216
|
+
* a reader searches for. Preferring the rhs puts the edge on 744. Preferring the
|
|
2217
|
+
* expression start put it on 743, a line that does not contain the string `then` at
|
|
2218
|
+
* all -- and "the line where the expression happens to begin" is not a stable property
|
|
2219
|
+
* of the call, it is an artifact of where the author pressed return.
|
|
2220
|
+
*/
|
|
2221
|
+
callTargetNode(fnNode, _target) {
|
|
2222
|
+
if (fnNode.type !== "namespace_operator" && fnNode.type !== "extract_operator") return fnNode;
|
|
2223
|
+
const rhs = fnNode.childForFieldName("rhs");
|
|
2224
|
+
if (rhs && rhs.type === "identifier") return rhs;
|
|
2225
|
+
return fnNode;
|
|
2226
|
+
}
|
|
2227
|
+
stringArgumentAt(args, index) {
|
|
2228
|
+
const n = this.stringArgumentNodeAt(args, index);
|
|
2229
|
+
return n ? this.stringNodeText(n) : void 0;
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* The Nth positional STRING argument of a call, as the NODE rather than the text.
|
|
2233
|
+
*
|
|
2234
|
+
* The node matters because an edge's `line_number` is the line of the node that NAMES
|
|
2235
|
+
* the target, and the name of an S7 class lives in a string that need not be on the
|
|
2236
|
+
* same line as the call:
|
|
2237
|
+
*
|
|
2238
|
+
* class_theme <- S7::new_class( <- line 219
|
|
2239
|
+
* "theme", class_S3_gg, <- line 220: this is where `theme` is named
|
|
2240
|
+
* ...
|
|
2241
|
+
*
|
|
2242
|
+
* Attributing the edge to the call put `class_theme references theme` on line 219, a
|
|
2243
|
+
* line that does not contain the word `theme` anywhere. The R-positions oracle found 6
|
|
2244
|
+
* such edges in ggplot2 alone. Passing the string node puts the edge on the line that
|
|
2245
|
+
* actually names the class, which is the line a reader needs.
|
|
2246
|
+
*/
|
|
2247
|
+
stringArgumentNodeAt(args, index) {
|
|
2248
|
+
if (!args) return null;
|
|
2249
|
+
let seen = 0;
|
|
2250
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2251
|
+
const arg = args.namedChild(i);
|
|
2252
|
+
if (!arg || arg.type === "comma") continue;
|
|
2253
|
+
if (arg.type === "argument") {
|
|
2254
|
+
if (arg.childForFieldName("name")) continue;
|
|
2255
|
+
const value = arg.childForFieldName("value");
|
|
2256
|
+
if (!value) continue;
|
|
2257
|
+
if (seen === index) return value.type === "string" ? value : null;
|
|
2258
|
+
seen++;
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
if (seen === index) return arg.type === "string" ? arg : null;
|
|
2262
|
+
seen++;
|
|
2263
|
+
}
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
/** The Nth positional STRING argument of a call, unquoted. */
|
|
2267
|
+
firstStringArgumentOf(node, index) {
|
|
2268
|
+
return this.stringArgumentAt(this.getArgumentsNode(node), index);
|
|
2269
|
+
}
|
|
2270
|
+
/** The last positional argument value of a call (the function body, for setGeneric/setMethod). */
|
|
2271
|
+
lastPositionalValue(args) {
|
|
2272
|
+
if (!args) return null;
|
|
2273
|
+
let last = null;
|
|
2274
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2275
|
+
const arg = args.namedChild(i);
|
|
2276
|
+
if (!arg || arg.type === "comma") continue;
|
|
2277
|
+
if (arg.type === "argument") {
|
|
2278
|
+
if (arg.childForFieldName("name")) continue;
|
|
2279
|
+
last = arg.childForFieldName("value");
|
|
2280
|
+
} else {
|
|
2281
|
+
last = arg;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return last;
|
|
2285
|
+
}
|
|
2286
|
+
/** The text of a `string` node: the grammar's `content` field, else the raw token. */
|
|
2287
|
+
stringNodeText(value) {
|
|
2288
|
+
const content = value.childForFieldName("content");
|
|
2289
|
+
if (content) return content.text;
|
|
2290
|
+
return this.unquoteString(value.text);
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Slot names of an S4 class, each with the RANGE OF ITS OWN NAME NODE so a
|
|
2294
|
+
* slot's symbol points at the slot rather than at the whole `setClass` call.
|
|
2295
|
+
*
|
|
2296
|
+
* Two shapes declare slots and both are in real use:
|
|
2297
|
+
* slots = c(a = "numeric") the current idiom
|
|
2298
|
+
* representation(a = "numeric") the original S4 idiom, still written by
|
|
2299
|
+
* R Core itself in Matrix
|
|
2300
|
+
* Reading only the first left every `representation()` class with no members.
|
|
2301
|
+
*/
|
|
2302
|
+
extractSlotKeys(node) {
|
|
2303
|
+
const results = [];
|
|
2304
|
+
const args = this.getArgumentsNode(node);
|
|
2305
|
+
if (!args) return results;
|
|
2306
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2307
|
+
const arg = args.namedChild(i);
|
|
2308
|
+
if (arg?.type !== "argument") continue;
|
|
2309
|
+
const nameNode = arg.childForFieldName("name");
|
|
2310
|
+
const value = arg.childForFieldName("value");
|
|
2311
|
+
if (!value) continue;
|
|
2312
|
+
const argName = nameNode?.text;
|
|
2313
|
+
const callee = value.type === "call" ? this.calleeName(value.childForFieldName("function")) : "";
|
|
2314
|
+
if (argName === "slots" && callee === "c") {
|
|
2315
|
+
for (const slot of this.namedArgumentNameKeys(value, "slots")) results.push(slot);
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
if ((argName === "representation" || callee === "representation") && value.type === "call") {
|
|
2319
|
+
for (const slot of this.namedArgumentNameKeys(value, "representation")) results.push(slot);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return results;
|
|
2323
|
+
}
|
|
2324
|
+
/** The names (with ranges) of the named arguments of a call. */
|
|
2325
|
+
namedArgumentNameKeys(callNode, via) {
|
|
2326
|
+
const results = [];
|
|
2327
|
+
const args = this.getArgumentsNode(callNode);
|
|
2328
|
+
if (!args) return results;
|
|
2329
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2330
|
+
const entry = args.namedChild(i);
|
|
2331
|
+
if (entry?.type !== "argument") continue;
|
|
2332
|
+
const entryName = entry.childForFieldName("name");
|
|
2333
|
+
if (entryName?.text) {
|
|
2334
|
+
const name = this.unquoteString(this.unquoteIdentifier(entryName.text));
|
|
2335
|
+
results.push({ name, range: this.getRange(entryName), via });
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return results;
|
|
2339
|
+
}
|
|
2340
|
+
unquoteString(text) {
|
|
2341
|
+
const trimmed = text.trim();
|
|
2342
|
+
if (trimmed.length >= 2) {
|
|
2343
|
+
const first = trimmed[0];
|
|
2344
|
+
const last = trimmed[trimmed.length - 1];
|
|
2345
|
+
if ((first === '"' || first === "'") && first === last) return trimmed.substring(1, trimmed.length - 1);
|
|
2346
|
+
}
|
|
2347
|
+
return trimmed;
|
|
2348
|
+
}
|
|
2349
|
+
extractSignature(node) {
|
|
2350
|
+
const parameters = this.getParametersNode(node);
|
|
2351
|
+
return parameters ? parameters.text : void 0;
|
|
2352
|
+
}
|
|
2353
|
+
extractDocstring(node) {
|
|
2354
|
+
let prev = node.previousSibling;
|
|
2355
|
+
const docsStack = [];
|
|
2356
|
+
while (prev) {
|
|
2357
|
+
if (prev.type === "comment") {
|
|
2358
|
+
docsStack.push(prev.text);
|
|
2359
|
+
prev = prev.previousSibling;
|
|
2360
|
+
} else if (prev.type === "return") {
|
|
2361
|
+
prev = prev.previousSibling;
|
|
2362
|
+
} else {
|
|
2363
|
+
break;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (docsStack.length === 0) return void 0;
|
|
2367
|
+
let combined = "";
|
|
2368
|
+
for (let i = docsStack.length - 1; i >= 0; i--) {
|
|
2369
|
+
combined += docsStack[i];
|
|
2370
|
+
if (i > 0) combined += "\n";
|
|
2371
|
+
}
|
|
2372
|
+
return combined;
|
|
2373
|
+
}
|
|
2374
|
+
extractNamedArguments(node, argName) {
|
|
2375
|
+
const results = [];
|
|
2376
|
+
const args = this.getArgumentsNode(node);
|
|
2377
|
+
if (!args) return results;
|
|
2378
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2379
|
+
const arg = args.namedChild(i);
|
|
2380
|
+
if (!arg || arg.type !== "argument") continue;
|
|
2381
|
+
const nameNode = arg.childForFieldName("name");
|
|
2382
|
+
if (!nameNode || nameNode.text !== argName) continue;
|
|
2383
|
+
const valNode = arg.childForFieldName("value");
|
|
2384
|
+
if (!valNode) continue;
|
|
2385
|
+
if (valNode.type === "call" && this.calleeName(valNode.childForFieldName("function")) === "c") {
|
|
2386
|
+
const cArgs = this.getArgumentsNode(valNode);
|
|
2387
|
+
if (cArgs) {
|
|
2388
|
+
for (let j = 0; j < cArgs.namedChildCount; j++) {
|
|
2389
|
+
const ca = cArgs.namedChild(j);
|
|
2390
|
+
if (!ca || ca.type === "comma") continue;
|
|
2391
|
+
if (ca.type === "argument") {
|
|
2392
|
+
const v = ca.childForFieldName("value");
|
|
2393
|
+
const text = v ? this.baseClassNodeName(v) : "";
|
|
2394
|
+
if (text) results.push(text);
|
|
2395
|
+
} else {
|
|
2396
|
+
const text = this.baseClassNodeName(ca);
|
|
2397
|
+
if (text) results.push(text);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
} else {
|
|
2402
|
+
const text = this.baseClassNodeName(valNode);
|
|
2403
|
+
if (text) results.push(text);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
return results;
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Whether a class-construction argument is a LIST-shaped composition clause.
|
|
2410
|
+
*
|
|
2411
|
+
* `setClass(contains=)` and `setRefClass(contains=)` are R's composition clauses, and they
|
|
2412
|
+
* come in two shapes that are not the same construct:
|
|
2413
|
+
*
|
|
2414
|
+
* contains = "Base" ONE class — a single supertype; R's own `is()` and
|
|
2415
|
+
* `extends()` answer TRUE for it, so the relation is a
|
|
2416
|
+
* conformance edge (`extends`).
|
|
2417
|
+
* contains = c("A", "B") SEVERAL classes — a member-set clause with no single
|
|
2418
|
+
* base. This is the shape the fleet's `mixes_in` names
|
|
2419
|
+
* ("no single base and the `with` list is not an
|
|
2420
|
+
* inheritance clause", `src/ast/types.ts:156-160`), the
|
|
2421
|
+
* same reading the Scala `extends A with B, C` clause
|
|
2422
|
+
* takes for its later positions.
|
|
2423
|
+
*
|
|
2424
|
+
* The count is taken from the VALUE node, not from the names that resolve: `c("A", pkg)` has
|
|
2425
|
+
* two entries even though only one is nameable, and the clause is a list either way. `comma`
|
|
2426
|
+
* is a named node type in this grammar, so the entries alternate argument/comma; commas are
|
|
2427
|
+
* skipped or every two-element list would read as four.
|
|
2428
|
+
*
|
|
2429
|
+
* A single-base `contains=` is deliberately NOT relabelled: R says it is conformance, and
|
|
2430
|
+
* moving it to `mixes_in` would publish a false edge. The conformance fact is carried as a
|
|
2431
|
+
* stated caveat on the list-shaped branch rather than dropped — R's `contains=` also makes
|
|
2432
|
+
* `is()` TRUE for the composed classes, and the fleet's label is the composition-clause
|
|
2433
|
+
* reading, not a claim that no conformance exists.
|
|
2434
|
+
*/
|
|
2435
|
+
compositionClauseIsList(node, argName) {
|
|
2436
|
+
const args = this.getArgumentsNode(node);
|
|
2437
|
+
if (!args) return false;
|
|
2438
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2439
|
+
const arg = args.namedChild(i);
|
|
2440
|
+
if (!arg || arg.type !== "argument") continue;
|
|
2441
|
+
if (arg.childForFieldName("name")?.text !== argName) continue;
|
|
2442
|
+
const value = arg.childForFieldName("value");
|
|
2443
|
+
if (!value || value.type !== "call") return false;
|
|
2444
|
+
const callee = this.calleeName(value.childForFieldName("function"));
|
|
2445
|
+
if (callee !== "c" && callee !== "list") return false;
|
|
2446
|
+
const entries = this.getArgumentsNode(value);
|
|
2447
|
+
if (!entries) return false;
|
|
2448
|
+
let count = 0;
|
|
2449
|
+
for (let j = 0; j < entries.namedChildCount; j++) {
|
|
2450
|
+
const entry = entries.namedChild(j);
|
|
2451
|
+
if (!entry || entry.type === "comma") continue;
|
|
2452
|
+
if (entry.type === "argument" && !entry.childForFieldName("value")) continue;
|
|
2453
|
+
count++;
|
|
2454
|
+
}
|
|
2455
|
+
return count > 1;
|
|
2456
|
+
}
|
|
2457
|
+
return false;
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* The class name a base-class ARGUMENT node carries, or "".
|
|
2461
|
+
*
|
|
2462
|
+
* R writes a base class three ways and this reads all three:
|
|
2463
|
+
*
|
|
2464
|
+
* inherit = Parent identifier -> Parent
|
|
2465
|
+
* contains = "Parent" string -> Parent
|
|
2466
|
+
* inherit = pkg::Parent namespace_operator
|
|
2467
|
+
*
|
|
2468
|
+
* The third was dropped. R6 documents it — `inherit` may name a class in another package,
|
|
2469
|
+
* and R6's own test suite uses it: `R6/tests/manual/test-inheritance.R:126` writes
|
|
2470
|
+
* `BC <- R6Class(inherit = R6testA::AC, ...)`, where `AC` is declared 9 lines above in the
|
|
2471
|
+
* same file inside the `R6testA = {...}` argument. A census of every class-constructor call
|
|
2472
|
+
* in the corpus (738 of them across 2,342 R files: `new_class` 283, `R6Class` 229,
|
|
2473
|
+
* `setClass` 201, `new_base_class` 12, `setRefClass` 9) found exactly ONE qualified base
|
|
2474
|
+
* class, so this is a 1-edge gap and is recorded as one rather than dressed up. It is a
|
|
2475
|
+
* deterministic parse fact, so the edge it produces is a fact and not a heuristic.
|
|
2476
|
+
*
|
|
2477
|
+
* The qualifier is dropped and the BARE name kept: `pkg::Parent` names the class `Parent`,
|
|
2478
|
+
* which is what `class(x)` returns and what dispatch keys on, and a symbol named
|
|
2479
|
+
* `pkg::Parent` binds to nothing in any file.
|
|
2480
|
+
*/
|
|
2481
|
+
baseClassNodeName(node) {
|
|
2482
|
+
if (!node) return "";
|
|
2483
|
+
if (node.type === "string") return this.stringNodeText(node);
|
|
2484
|
+
if (node.type === "identifier") return this.unquoteIdentifier(node.text);
|
|
2485
|
+
if (node.type === "namespace_operator") {
|
|
2486
|
+
const rhs = node.childForFieldName("rhs");
|
|
2487
|
+
return rhs ? this.unquoteIdentifier(rhs.text) : "";
|
|
2488
|
+
}
|
|
2489
|
+
return "";
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* The range of a member DECLARATION: from the declared name to the end of its value.
|
|
2493
|
+
*
|
|
2494
|
+
* A member is written `name = value`, and a range taken from `value` alone starts at
|
|
2495
|
+
* the value. For an R6 or R5 method that means the range begins at the `function`
|
|
2496
|
+
* keyword and so EXCLUDES the name the symbol is published under -- a range that does
|
|
2497
|
+
* not contain the thing it names. The R-positions oracle found 833 of these across the
|
|
2498
|
+
* pinned corpora (dplyr, shiny, testthat), every one a method, all of them off by the
|
|
2499
|
+
* length of `name = `.
|
|
2500
|
+
*
|
|
2501
|
+
* Both nodes are required, so a member with no name is not representable here and the
|
|
2502
|
+
* caller is forced to decide what it means rather than being handed a range that
|
|
2503
|
+
* silently omits the name.
|
|
2504
|
+
*/
|
|
2505
|
+
getDeclarationRange(nameNode, valueNode) {
|
|
2506
|
+
const r = {
|
|
2507
|
+
startLine: nameNode.startPosition.row + 1,
|
|
2508
|
+
startColumn: nameNode.startPosition.column + 1,
|
|
2509
|
+
endLine: valueNode.endPosition.row + 1,
|
|
2510
|
+
endColumn: valueNode.endPosition.column + 1,
|
|
2511
|
+
startIndex: nameNode.startIndex,
|
|
2512
|
+
endIndex: valueNode.endIndex
|
|
2513
|
+
};
|
|
2514
|
+
const toBytes = this.byteOffsetOf;
|
|
2515
|
+
if (toBytes) {
|
|
2516
|
+
r.byteStart = toBytes(nameNode.startIndex);
|
|
2517
|
+
r.byteEnd = toBytes(valueNode.endIndex);
|
|
2518
|
+
}
|
|
2519
|
+
return r;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* The range of a node.
|
|
2523
|
+
*
|
|
2524
|
+
* `startIndex`/`endIndex` are whatever the parser reported -- UTF-16 code units, for
|
|
2525
|
+
* web-tree-sitter -- and are passed through unchanged so nothing that already reads them
|
|
2526
|
+
* moves. `byteStart`/`byteEnd` are the same positions in BYTES, added alongside, which is
|
|
2527
|
+
* what a consumer slicing a Buffer needs. `byteOffset` is supplied by `analyze`, which is
|
|
2528
|
+
* the only place that has the source text; a node range built outside an analyze call
|
|
2529
|
+
* simply omits the byte fields rather than reporting a wrong one.
|
|
2530
|
+
*/
|
|
2531
|
+
/**
|
|
2532
|
+
* Is this `if` condition the literal FALSE, so the `then` branch never executes?
|
|
2533
|
+
*
|
|
2534
|
+
* Only the literals. A variable holding FALSE is not statically knowable, and R does not
|
|
2535
|
+
* constant-fold, so guessing there would drop real declarations.
|
|
2536
|
+
*/
|
|
2537
|
+
/**
|
|
2538
|
+
* Remove the symbols a top-level `rm(...)` unbinds, and any relationship that named one
|
|
2539
|
+
* as its source. Returns how many were removed, so the removal is countable rather than
|
|
2540
|
+
* silent.
|
|
2541
|
+
*/
|
|
2542
|
+
removeTopLevelBindings(node, args, symbols, relationships, metadata) {
|
|
2543
|
+
if (!args) return 0;
|
|
2544
|
+
const doomed = /* @__PURE__ */ new Set();
|
|
2545
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2546
|
+
const arg = args.namedChild(i);
|
|
2547
|
+
if (!arg) continue;
|
|
2548
|
+
if (arg.childForFieldName("name")) continue;
|
|
2549
|
+
const v = arg.childForFieldName("value") ?? (arg.type === "identifier" ? arg : null);
|
|
2550
|
+
if (!v) continue;
|
|
2551
|
+
const nm = v.type === "identifier" ? this.unquoteIdentifier(v.text) : v.type === "string" ? this.unquoteString(v.text) : null;
|
|
2552
|
+
if (nm) doomed.add(nm);
|
|
2553
|
+
}
|
|
2554
|
+
if (doomed.size === 0) return 0;
|
|
2555
|
+
const before = symbols.length;
|
|
2556
|
+
for (let i = symbols.length - 1; i >= 0; i--) {
|
|
2557
|
+
const s = symbols[i];
|
|
2558
|
+
if (s.ownerName || !doomed.has(s.name)) continue;
|
|
2559
|
+
symbols.splice(i, 1);
|
|
2560
|
+
}
|
|
2561
|
+
for (let i = relationships.length - 1; i >= 0; i--) {
|
|
2562
|
+
const r = relationships[i];
|
|
2563
|
+
if (doomed.has(String(r.source))) relationships.splice(i, 1);
|
|
2564
|
+
}
|
|
2565
|
+
void node;
|
|
2566
|
+
void metadata;
|
|
2567
|
+
return before - symbols.length;
|
|
2568
|
+
}
|
|
2569
|
+
isStaticallyFalseCondition(ifNode) {
|
|
2570
|
+
const cond = ifNode.childForFieldName("condition");
|
|
2571
|
+
if (!cond) return false;
|
|
2572
|
+
if (cond.type === "false") return true;
|
|
2573
|
+
if (cond.type !== "identifier") return false;
|
|
2574
|
+
const t = this.unquoteIdentifier(cond.text);
|
|
2575
|
+
return t === "F" || t === "FALSE" || t === "false";
|
|
2576
|
+
}
|
|
2577
|
+
getRange(node, byteOffset) {
|
|
2578
|
+
const toBytes = byteOffset ?? this.byteOffsetOf;
|
|
2579
|
+
const r = {
|
|
2580
|
+
startLine: node.startPosition.row + 1,
|
|
2581
|
+
startColumn: node.startPosition.column + 1,
|
|
2582
|
+
endLine: node.endPosition.row + 1,
|
|
2583
|
+
endColumn: node.endPosition.column + 1,
|
|
2584
|
+
startIndex: node.startIndex,
|
|
2585
|
+
endIndex: node.endIndex
|
|
2586
|
+
};
|
|
2587
|
+
if (toBytes) {
|
|
2588
|
+
r.byteStart = toBytes(node.startIndex);
|
|
2589
|
+
r.byteEnd = toBytes(node.endIndex);
|
|
2590
|
+
}
|
|
2591
|
+
return r;
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
export {
|
|
2595
|
+
RealRAnalyzer
|
|
2596
|
+
};
|