rlang.rs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. //! R extraction — a faithful Rust port of the R paths of `TreeSitterExtractor`
  2. //! (src/extraction/tree-sitter.ts) plus languages/r.ts.
  3. //!
  4. //! Same porting contract as the other walkers: behavior parity, bug-for-bug.
  5. //! The authoritative quirk list is docs/design/r-kernel-port-checklist.md.
  6. //! R is the lightest shared-surface port and the heaviest hook port: r.ts has
  7. //! every type list empty except `callTypes: ['call']`, so the whole shared
  8. //! extraction machine (extractFunction/Class/Import/Variable, docstrings,
  9. //! decorators, inheritance, visitFunctionBody, value-refs, fn-refs,
  10. //! static-member reads, type annotations) never runs — the walker is a file
  11. //! node + the visitNode hook + the generic extractCall + pre-order recursion.
  12. //! Load-bearing oddities preserved on purpose: `calls "return"` on every
  13. //! `return(x)` (return/next/break are named nodes in v1.2.0), silent
  14. //! consumption of imports with dynamic/missing/empty first args (subtree
  15. //! never visited) vs class/generic calls FALLING THROUGH on the same shapes
  16. //! (generic call + file-scope body leak), `library(help = pkg)` importing the
  17. //! named arg's value, class-idiom variable suppression checking only the
  18. //! callee NAME, chained `a <- b <- 5` minting only `a`, `env$fn <- function`
  19. //! minting nothing while its body calls leak to file scope, raw-text callees
  20. //! verbatim (`pkg::fn`, `obj$meth`, `"strfn"` quotes kept), and duplicate
  21. //! same-(kind,name,line) ids emitted twice. Positions in UTF-16 code units.
  22. //! Files with parse errors defer to wasm (~0% incidence on real repos).
  23. use crate::buffers::{
  24. build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
  25. RefRow, Tables, FLAG_IS_EXPORTED, NONE, NONE_STR,
  26. };
  27. use crate::ids;
  28. use crate::textutil as util;
  29. use regex::Regex;
  30. use std::sync::OnceLock;
  31. use tree_sitter::{Node, Parser};
  32. /// CONSTANT_NAME (r.ts:43) — ALL_CAPS or DOTTED.CAPS top-level assignment.
  33. fn constant_name_re() -> &'static Regex {
  34. static RE: OnceLock<Regex> = OnceLock::new();
  35. RE.get_or_init(|| Regex::new(r"^[A-Z][A-Z0-9._]*$").unwrap())
  36. }
  37. /// ASSIGN_LEFT / ASSIGN_RIGHT (r.ts:37-38).
  38. fn is_assign_left(op: &str) -> bool {
  39. matches!(op, "<-" | "<<-" | "=")
  40. }
  41. fn is_assign_right(op: &str) -> bool {
  42. matches!(op, "->" | "->>")
  43. }
  44. /// IMPORT_FNS (r.ts:39) — `source` is checked alongside (r.ts:189).
  45. fn is_import_fn(name: &str) -> bool {
  46. matches!(name, "library" | "require" | "requireNamespace" | "loadNamespace")
  47. }
  48. /// CLASS_FNS (r.ts:40).
  49. fn is_class_fn(name: &str) -> bool {
  50. matches!(name, "setClass" | "setRefClass" | "R6Class" | "ggproto")
  51. }
  52. /// GENERIC_FNS (r.ts:41).
  53. fn is_generic_fn(name: &str) -> bool {
  54. matches!(name, "setGeneric" | "setMethod")
  55. }
  56. struct Scope {
  57. row: u32,
  58. kind: &'static str,
  59. name: String,
  60. }
  61. pub struct Walker<'t> {
  62. src: &'t str,
  63. file_path: &'t str,
  64. line_starts: Vec<usize>,
  65. arena: Arena,
  66. tables: Tables,
  67. stack: Vec<Scope>,
  68. }
  69. pub fn extract(file_path: &str, source: &str) -> Result<EmitOut, String> {
  70. let grammar = crate::langs::grammar_for("r").ok_or("no r grammar")?;
  71. let t0 = std::time::Instant::now();
  72. let mut parser = Parser::new();
  73. parser
  74. .set_language(&grammar)
  75. .map_err(|e| format!("set_language(r) failed: {e}"))?;
  76. let tree = parser
  77. .parse(source, None)
  78. .ok_or_else(|| "parser returned null tree".to_string())?;
  79. if tree.root_node().has_error() {
  80. return Err("defer: parse tree contains errors — wasm recovery is canonical".to_string());
  81. }
  82. let mut w = Walker {
  83. src: source,
  84. file_path,
  85. line_starts: util::line_starts(source),
  86. arena: Arena::default(),
  87. tables: Tables::default(),
  88. stack: Vec::new(),
  89. };
  90. // File node (tree-sitter.ts:508-521) — the only node with isExported set.
  91. let line_count = source.bytes().filter(|b| *b == b'\n').count() as u32 + 1;
  92. let base_name = file_path.rsplit(['/', '\\']).next().unwrap_or(file_path);
  93. let mut flags = BoolFlags::default();
  94. flags.set(FLAG_IS_EXPORTED, false);
  95. let file_id = w.arena.put(&ids::file_node_id(file_path));
  96. let name_ref = w.arena.put(base_name);
  97. let qn_ref = w.arena.put(file_path);
  98. w.tables.push_node(&NodeRow {
  99. kind: node_kind_index("file").unwrap(),
  100. visibility: 0,
  101. flags,
  102. start_line: 1,
  103. end_line: line_count,
  104. start_column: 0,
  105. end_column: 0,
  106. name: name_ref,
  107. qualified_name: qn_ref,
  108. id: file_id,
  109. docstring: NONE_STR,
  110. signature: NONE_STR,
  111. decorators: NONE_STR,
  112. type_parameters: NONE_STR,
  113. return_type: NONE_STR,
  114. extra_json: NONE_STR,
  115. });
  116. w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() });
  117. // extractFilePackage returns null (no packageTypes); both end-of-file
  118. // flushes are no-ops for R (no fnRefSpec, VALUE_REF_LANGS gate).
  119. w.visit(tree.root_node());
  120. w.stack.pop();
  121. let duration_ms = t0.elapsed().as_secs_f64() * 1000.0;
  122. let meta = build_meta(&w.tables, w.arena.len(), NONE_STR, duration_ms);
  123. Ok(EmitOut {
  124. meta,
  125. nodes: w.tables.nodes,
  126. edges: w.tables.edges,
  127. refs: w.tables.refs,
  128. arena: w.arena.into_vec(),
  129. })
  130. }
  131. impl<'t> Walker<'t> {
  132. fn text(&self, node: Node) -> &'t str {
  133. &self.src[node.byte_range()]
  134. }
  135. fn line_of(&self, node: Node) -> u32 {
  136. node.start_position().row as u32 + 1
  137. }
  138. fn col_of(&self, node: Node) -> u32 {
  139. util::col16(self.src, &self.line_starts, node.start_position().row, node.start_byte())
  140. }
  141. fn end_col_of(&self, node: Node) -> u32 {
  142. util::col16(self.src, &self.line_starts, node.end_position().row, node.end_byte())
  143. }
  144. fn top_row(&self) -> u32 {
  145. self.stack.last().map(|s| s.row).unwrap_or(0)
  146. }
  147. fn push_ref_at(&mut self, from_row: u32, name: &str, kind: &str, node: Node) {
  148. let name_ref = self.arena.put(name);
  149. self.tables.push_ref(&RefRow {
  150. from_idx: from_row,
  151. kind: edge_kind_index(kind).unwrap(),
  152. line: self.line_of(node),
  153. column: self.col_of(node),
  154. reference_name: name_ref,
  155. candidates: NONE_STR,
  156. from_id_str: NONE_STR,
  157. });
  158. }
  159. // --- createNode (tree-sitter.ts:1308) ---------------------------------
  160. // R extras carry only `signature` (or nothing): no docstring, visibility,
  161. // isStatic/isAsync/isExported, returnType, decorators — ever. The endLine
  162. // body-extension is dead (no resolveBody hook).
  163. fn create_node(&mut self, kind: &'static str, name: &str, node: Node<'t>, signature: Option<&str>) -> Option<u32> {
  164. if name.is_empty() {
  165. return None;
  166. }
  167. let start_line = self.line_of(node);
  168. let id = ids::node_id(self.file_path, kind, name, start_line);
  169. // buildQualifiedName (tree-sitter.ts:1447-1460): non-file stack names
  170. // joined `::` (namespacePrefix is always empty for R).
  171. let qualified = {
  172. let mut parts: Vec<&str> = Vec::new();
  173. for s in &self.stack {
  174. if s.kind != "file" {
  175. parts.push(&s.name);
  176. }
  177. }
  178. let mut qn = parts.join("::");
  179. if !qn.is_empty() {
  180. qn.push_str("::");
  181. }
  182. qn.push_str(name);
  183. qn
  184. };
  185. let name_ref = self.arena.put(name);
  186. let qn_ref = self.arena.put(&qualified);
  187. let id_ref = self.arena.put(&id);
  188. let sig_ref = match signature {
  189. Some(s) => self.arena.put(s),
  190. None => NONE_STR,
  191. };
  192. let row = self.tables.push_node(&NodeRow {
  193. kind: node_kind_index(kind).unwrap(),
  194. visibility: 0,
  195. flags: BoolFlags::default(),
  196. start_line,
  197. end_line: node.end_position().row as u32 + 1,
  198. start_column: self.col_of(node),
  199. end_column: self.end_col_of(node),
  200. name: name_ref,
  201. qualified_name: qn_ref,
  202. id: id_ref,
  203. docstring: NONE_STR,
  204. signature: sig_ref,
  205. decorators: NONE_STR,
  206. type_parameters: NONE_STR,
  207. return_type: NONE_STR,
  208. extra_json: NONE_STR,
  209. });
  210. // Containment edge from the stack top (always non-empty — file node).
  211. let parent_row = self.top_row();
  212. self.tables.push_edge(&EdgeRow {
  213. source_idx: parent_row,
  214. target_idx: row,
  215. kind: edge_kind_index("contains").unwrap(),
  216. provenance: 0,
  217. line: NONE,
  218. column: NONE,
  219. metadata_json: NONE_STR,
  220. source_id_str: NONE_STR,
  221. target_id_str: NONE_STR,
  222. });
  223. Some(row)
  224. }
  225. // --- r.ts helper transcriptions ---------------------------------------
  226. /// calleeName (r.ts:46-55): bare identifier text, or a
  227. /// namespace_operator's `rhs` field text (`pkg::fn` → `fn`) — so
  228. /// `methods::setClass` etc. trigger the special branches. Everything else
  229. /// (extract_operator, subset2, string, call, return…) → None.
  230. fn callee_name(&self, call: Node<'t>) -> Option<&'t str> {
  231. let f = call.child_by_field_name("function")?;
  232. match f.kind() {
  233. "identifier" => Some(self.text(f)),
  234. "namespace_operator" => f.child_by_field_name("rhs").map(|rhs| self.text(rhs)),
  235. _ => None,
  236. }
  237. }
  238. /// firstArgValue (r.ts:58-67): the FIRST `argument`-typed named child of
  239. /// the `arguments` field → its `value` field (named arguments are NOT
  240. /// skipped — `library(help = docpkg)` imports "docpkg", preserved bug).
  241. fn first_arg_value(&self, call: Node<'t>) -> Option<Node<'t>> {
  242. let args = call.child_by_field_name("arguments")?;
  243. let mut cursor = args.walk();
  244. for arg in args.named_children(&mut cursor) {
  245. if arg.kind() != "argument" {
  246. continue;
  247. }
  248. return arg.child_by_field_name("value");
  249. }
  250. None
  251. }
  252. /// literalOrIdentifier (r.ts:70-81): identifier text (backticks kept), a
  253. /// string's first string_content text, `Some("")` for an empty string
  254. /// literal (falsy downstream, like null), None otherwise.
  255. fn literal_or_identifier(&self, node: Option<Node<'t>>) -> Option<&'t str> {
  256. let node = node?;
  257. match node.kind() {
  258. "identifier" => Some(self.text(node)),
  259. "string" => {
  260. let mut cursor = node.walk();
  261. for c in node.named_children(&mut cursor) {
  262. if c.kind() == "string_content" {
  263. return Some(self.text(c));
  264. }
  265. }
  266. Some("")
  267. }
  268. _ => None,
  269. }
  270. }
  271. // --- the visitNode walk (tree-sitter.ts:936-953, 1248, 1295-1301) -----
  272. // Hook first (consumed → return; scanFnRefSubtree is a no-op for R), then
  273. // the ladder — only callTypes can match, with children still recursed —
  274. // else plain recursion over namedChildren in order.
  275. fn visit(&mut self, node: Node<'t>) {
  276. stack_guard!();
  277. if self.hook(node) {
  278. return;
  279. }
  280. if node.kind() == "call" {
  281. self.extract_call(node);
  282. }
  283. let mut cursor = node.walk();
  284. let children: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
  285. for child in children {
  286. self.visit(child);
  287. }
  288. }
  289. /// The visitNode hook (r.ts:180-309). Returns true when consumed.
  290. fn hook(&mut self, node: Node<'t>) -> bool {
  291. stack_guard!();
  292. match node.kind() {
  293. "call" => self.hook_call(node),
  294. "binary_operator" => self.hook_binary_operator(node),
  295. _ => false,
  296. }
  297. }
  298. fn hook_call(&mut self, node: Node<'t>) -> bool {
  299. stack_guard!();
  300. let fname = match self.callee_name(node) {
  301. Some(f) => f,
  302. None => return false,
  303. };
  304. // library(dplyr) / require(stats) / requireNamespace("jsonlite") /
  305. // source("helpers.R") (r.ts:189-208). A dynamic/missing/empty first
  306. // arg is consumed SILENTLY — nothing recorded, subtree never visited.
  307. if is_import_fn(fname) || fname == "source" {
  308. let module = match self.literal_or_identifier(self.first_arg_value(node)) {
  309. Some(m) if !m.is_empty() => m,
  310. _ => return true,
  311. };
  312. // signature: whole call text .trim().slice(0, 100) — UTF-16 slice.
  313. // (A call node's text starts at the callee and ends at `)`, so
  314. // trim() never has anything to strip on reachable inputs.)
  315. let (sig, _) = util::slice_utf16(self.text(node).trim(), 100);
  316. let module = module.to_string();
  317. let imp = self.create_node("import", &module, node, Some(&sig));
  318. if imp.is_some() && !self.stack.is_empty() {
  319. let parent_row = self.top_row();
  320. self.push_ref_at(parent_row, &module, "imports", node);
  321. }
  322. return true;
  323. }
  324. // setClass("Patient", …) / setRefClass / R6Class / ggproto
  325. // (r.ts:211-221). A falsy name FALLS THROUGH to the generic call —
  326. // `ggproto(NULL, Geom, …)` emits `calls ggproto` + file-scope body
  327. // leak (asymmetric with imports, preserved).
  328. if is_class_fn(fname) {
  329. let name = match self.literal_or_identifier(self.first_arg_value(node)) {
  330. Some(n) if !n.is_empty() => n.to_string(),
  331. _ => return false,
  332. };
  333. if let Some(cls_row) = self.create_node("class", &name, node, None) {
  334. self.stack.push(Scope { row: cls_row, kind: "class", name });
  335. self.extract_class_members(node, cls_row);
  336. self.stack.pop();
  337. }
  338. return true;
  339. }
  340. // setGeneric("describe", …) / setMethod("describe", "Patient", fn)
  341. // (r.ts:224-249): function node named by the first arg; signature and
  342. // body from the FIRST argument (any position) whose value is a
  343. // function_definition.
  344. if is_generic_fn(fname) {
  345. let name = match self.literal_or_identifier(self.first_arg_value(node)) {
  346. Some(n) if !n.is_empty() => n.to_string(),
  347. _ => return false,
  348. };
  349. let mut impl_node: Option<Node<'t>> = None;
  350. if let Some(args) = node.child_by_field_name("arguments") {
  351. let mut cursor = args.walk();
  352. for a in args.named_children(&mut cursor) {
  353. if a.kind() != "argument" {
  354. continue;
  355. }
  356. if let Some(v) = a.child_by_field_name("value") {
  357. if v.kind() == "function_definition" {
  358. impl_node = Some(v);
  359. break;
  360. }
  361. }
  362. }
  363. }
  364. let params_text = impl_node
  365. .and_then(|i| i.child_by_field_name("parameters"))
  366. .map(|p| self.text(p));
  367. let fn_row = self.create_node("function", &name, node, params_text);
  368. let body = impl_node.and_then(|i| i.child_by_field_name("body"));
  369. if let (Some(row), Some(body)) = (fn_row, body) {
  370. self.stack.push(Scope { row, kind: "function", name });
  371. self.visit(body);
  372. self.stack.pop();
  373. }
  374. return true;
  375. }
  376. false // ordinary call — generic extraction records the edge
  377. }
  378. fn hook_binary_operator(&mut self, node: Node<'t>) -> bool {
  379. stack_guard!();
  380. let op = match node.child_by_field_name("operator") {
  381. Some(o) => self.text(o),
  382. None => return false,
  383. };
  384. let lhs = node.child_by_field_name("lhs");
  385. let rhs = node.child_by_field_name("rhs");
  386. // name <- function(…) — ANY scope (r.ts:267-279). Body walked through
  387. // the hook-aware visit (visitFunctionBody never runs for R).
  388. if is_assign_left(op) {
  389. if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
  390. if lhs.kind() == "identifier" && rhs.kind() == "function_definition" {
  391. let params_text = rhs.child_by_field_name("parameters").map(|p| self.text(p));
  392. let name = self.text(lhs).to_string();
  393. let fn_row = self.create_node("function", &name, node, params_text);
  394. let body = rhs.child_by_field_name("body");
  395. if let (Some(row), Some(body)) = (fn_row, body) {
  396. self.stack.push(Scope { row, kind: "function", name });
  397. self.visit(body);
  398. self.stack.pop();
  399. }
  400. return true;
  401. }
  402. }
  403. }
  404. let top_level = node.parent().map(|p| p.kind() == "program").unwrap_or(false);
  405. // Top-level value assignments → variable/constant (r.ts:284-296);
  406. // the class-definition idiom suppresses the twin variable node but the
  407. // rhs is ALWAYS visited.
  408. if top_level && is_assign_left(op) {
  409. if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
  410. if lhs.kind() == "identifier" {
  411. let rhs_callee = if rhs.kind() == "call" { self.callee_name(rhs) } else { None };
  412. let suppressed = rhs_callee
  413. .map(|c| is_class_fn(c) || is_generic_fn(c))
  414. .unwrap_or(false);
  415. if !suppressed {
  416. let name = self.text(lhs);
  417. let kind = if constant_name_re().is_match(name) { "constant" } else { "variable" };
  418. self.create_node(kind, name, node, None);
  419. }
  420. self.visit(rhs);
  421. return true;
  422. }
  423. }
  424. }
  425. // value -> name / value ->> name (r.ts:298-303).
  426. if top_level && is_assign_right(op) {
  427. if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
  428. if rhs.kind() == "identifier" {
  429. let name = self.text(rhs);
  430. let kind = if constant_name_re().is_match(name) { "constant" } else { "variable" };
  431. self.create_node(kind, name, node, None);
  432. self.visit(lhs);
  433. return true;
  434. }
  435. }
  436. }
  437. false
  438. }
  439. /// extractClassMembers (r.ts:110-163): arguments in source order with a
  440. /// positional counter — ggproto's 2nd positional identifier and
  441. /// `inherit`/`contains` named args become `extends` refs (from the CLASS
  442. /// row, positioned at the VALUE node); named function_definition args and
  443. /// `list(…)` entries become methods. Non-method argument subtrees are
  444. /// NEVER visited (`representation(…)`, `signature(…)` invisible).
  445. fn extract_class_members(&mut self, class_call: Node<'t>, class_row: u32) {
  446. stack_guard!();
  447. let args = match class_call.child_by_field_name("arguments") {
  448. Some(a) => a,
  449. None => return,
  450. };
  451. let mut positional = 0u32;
  452. let mut cursor = args.walk();
  453. let arg_nodes: Vec<Node<'t>> = args.named_children(&mut cursor).collect();
  454. for arg in arg_nodes {
  455. if arg.kind() != "argument" {
  456. continue;
  457. }
  458. let arg_name = arg.child_by_field_name("name");
  459. let value = arg.child_by_field_name("value");
  460. let arg_name = match arg_name {
  461. None => {
  462. positional += 1;
  463. if positional == 2 {
  464. if let Some(v) = value {
  465. if v.kind() == "identifier" {
  466. let parent = self.text(v).to_string();
  467. self.push_ref_at(class_row, &parent, "extends", v);
  468. }
  469. }
  470. }
  471. continue;
  472. }
  473. Some(n) => n,
  474. };
  475. let arg_name_text = self.text(arg_name);
  476. // R6 `inherit = Parent` / S4 `contains = "Parent"` — a falsy
  477. // resolution (`inherit = pkg::Parent`, empty string) emits nothing.
  478. if (arg_name_text == "inherit" || arg_name_text == "contains") && value.is_some() {
  479. if let Some(parent) = self.literal_or_identifier(value) {
  480. if !parent.is_empty() {
  481. let parent = parent.to_string();
  482. self.push_ref_at(class_row, &parent, "extends", value.unwrap());
  483. }
  484. }
  485. continue;
  486. }
  487. // Direct named function argument (ggproto methods).
  488. if let Some(v) = value {
  489. if v.kind() == "function_definition" {
  490. self.emit_method_arg(arg);
  491. continue;
  492. }
  493. // list(…) of named function arguments (R5/R6 methods).
  494. if v.kind() == "call" && self.callee_name(v) == Some("list") {
  495. if let Some(list_args) = v.child_by_field_name("arguments") {
  496. let mut lc = list_args.walk();
  497. let entries: Vec<Node<'t>> = list_args.named_children(&mut lc).collect();
  498. for entry in entries {
  499. if entry.kind() == "argument" {
  500. self.emit_method_arg(entry);
  501. }
  502. }
  503. }
  504. }
  505. }
  506. }
  507. }
  508. /// emitMethodArg (r.ts:84-98): `name = function(…)` argument entry → a
  509. /// `method` node positioned at the ARGUMENT node, signature from the raw
  510. /// parameters text, body walked hook-aware inside the method scope.
  511. fn emit_method_arg(&mut self, entry: Node<'t>) {
  512. stack_guard!();
  513. let entry_name = match entry.child_by_field_name("name") {
  514. Some(n) => n,
  515. None => return,
  516. };
  517. let entry_value = match entry.child_by_field_name("value") {
  518. Some(v) if v.kind() == "function_definition" => v,
  519. _ => return,
  520. };
  521. let params_text = entry_value.child_by_field_name("parameters").map(|p| self.text(p));
  522. let name = self.text(entry_name).to_string();
  523. let method_row = self.create_node("method", &name, entry, params_text);
  524. let body = entry_value.child_by_field_name("body");
  525. if let (Some(row), Some(body)) = (method_row, body) {
  526. self.stack.push(Scope { row, kind: "method", name });
  527. self.visit(body);
  528. self.stack.pop();
  529. }
  530. }
  531. // --- extractCall (tree-sitter.ts:3684, 4313, 4518-4532, 4572-4580) ----
  532. // R call nodes reach the generic tail: callee = RAW `function`-field text
  533. // verbatim (member branch unreachable, cpp recoveries language-gated),
  534. // then the parenthesized-conversion regex. No skipChildren — inner calls
  535. // are visited by the ladder's recursion afterward.
  536. fn extract_call(&mut self, node: Node<'t>) {
  537. if self.stack.is_empty() {
  538. return;
  539. }
  540. let caller_row = self.top_row();
  541. let func = node
  542. .child_by_field_name("function")
  543. .or_else(|| node.named_child(0));
  544. let func = match func {
  545. Some(f) => f,
  546. None => return,
  547. };
  548. let mut callee: &str = self.text(func);
  549. if let Some(caps) = util::paren_conversion().captures(callee) {
  550. if let Some(inner) = caps.get(1) {
  551. callee = &callee[inner.range()];
  552. }
  553. }
  554. if callee.is_empty() {
  555. return;
  556. }
  557. let callee = callee.to_string();
  558. self.push_ref_at(caller_row, &callee, "calls", node);
  559. }
  560. }