rlang.rs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  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. if self.hook(node) {
  277. return;
  278. }
  279. if node.kind() == "call" {
  280. self.extract_call(node);
  281. }
  282. let mut cursor = node.walk();
  283. let children: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
  284. for child in children {
  285. self.visit(child);
  286. }
  287. }
  288. /// The visitNode hook (r.ts:180-309). Returns true when consumed.
  289. fn hook(&mut self, node: Node<'t>) -> bool {
  290. match node.kind() {
  291. "call" => self.hook_call(node),
  292. "binary_operator" => self.hook_binary_operator(node),
  293. _ => false,
  294. }
  295. }
  296. fn hook_call(&mut self, node: Node<'t>) -> bool {
  297. let fname = match self.callee_name(node) {
  298. Some(f) => f,
  299. None => return false,
  300. };
  301. // library(dplyr) / require(stats) / requireNamespace("jsonlite") /
  302. // source("helpers.R") (r.ts:189-208). A dynamic/missing/empty first
  303. // arg is consumed SILENTLY — nothing recorded, subtree never visited.
  304. if is_import_fn(fname) || fname == "source" {
  305. let module = match self.literal_or_identifier(self.first_arg_value(node)) {
  306. Some(m) if !m.is_empty() => m,
  307. _ => return true,
  308. };
  309. // signature: whole call text .trim().slice(0, 100) — UTF-16 slice.
  310. // (A call node's text starts at the callee and ends at `)`, so
  311. // trim() never has anything to strip on reachable inputs.)
  312. let (sig, _) = util::slice_utf16(self.text(node).trim(), 100);
  313. let module = module.to_string();
  314. let imp = self.create_node("import", &module, node, Some(&sig));
  315. if imp.is_some() && !self.stack.is_empty() {
  316. let parent_row = self.top_row();
  317. self.push_ref_at(parent_row, &module, "imports", node);
  318. }
  319. return true;
  320. }
  321. // setClass("Patient", …) / setRefClass / R6Class / ggproto
  322. // (r.ts:211-221). A falsy name FALLS THROUGH to the generic call —
  323. // `ggproto(NULL, Geom, …)` emits `calls ggproto` + file-scope body
  324. // leak (asymmetric with imports, preserved).
  325. if is_class_fn(fname) {
  326. let name = match self.literal_or_identifier(self.first_arg_value(node)) {
  327. Some(n) if !n.is_empty() => n.to_string(),
  328. _ => return false,
  329. };
  330. if let Some(cls_row) = self.create_node("class", &name, node, None) {
  331. self.stack.push(Scope { row: cls_row, kind: "class", name });
  332. self.extract_class_members(node, cls_row);
  333. self.stack.pop();
  334. }
  335. return true;
  336. }
  337. // setGeneric("describe", …) / setMethod("describe", "Patient", fn)
  338. // (r.ts:224-249): function node named by the first arg; signature and
  339. // body from the FIRST argument (any position) whose value is a
  340. // function_definition.
  341. if is_generic_fn(fname) {
  342. let name = match self.literal_or_identifier(self.first_arg_value(node)) {
  343. Some(n) if !n.is_empty() => n.to_string(),
  344. _ => return false,
  345. };
  346. let mut impl_node: Option<Node<'t>> = None;
  347. if let Some(args) = node.child_by_field_name("arguments") {
  348. let mut cursor = args.walk();
  349. for a in args.named_children(&mut cursor) {
  350. if a.kind() != "argument" {
  351. continue;
  352. }
  353. if let Some(v) = a.child_by_field_name("value") {
  354. if v.kind() == "function_definition" {
  355. impl_node = Some(v);
  356. break;
  357. }
  358. }
  359. }
  360. }
  361. let params_text = impl_node
  362. .and_then(|i| i.child_by_field_name("parameters"))
  363. .map(|p| self.text(p));
  364. let fn_row = self.create_node("function", &name, node, params_text);
  365. let body = impl_node.and_then(|i| i.child_by_field_name("body"));
  366. if let (Some(row), Some(body)) = (fn_row, body) {
  367. self.stack.push(Scope { row, kind: "function", name });
  368. self.visit(body);
  369. self.stack.pop();
  370. }
  371. return true;
  372. }
  373. false // ordinary call — generic extraction records the edge
  374. }
  375. fn hook_binary_operator(&mut self, node: Node<'t>) -> bool {
  376. let op = match node.child_by_field_name("operator") {
  377. Some(o) => self.text(o),
  378. None => return false,
  379. };
  380. let lhs = node.child_by_field_name("lhs");
  381. let rhs = node.child_by_field_name("rhs");
  382. // name <- function(…) — ANY scope (r.ts:267-279). Body walked through
  383. // the hook-aware visit (visitFunctionBody never runs for R).
  384. if is_assign_left(op) {
  385. if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
  386. if lhs.kind() == "identifier" && rhs.kind() == "function_definition" {
  387. let params_text = rhs.child_by_field_name("parameters").map(|p| self.text(p));
  388. let name = self.text(lhs).to_string();
  389. let fn_row = self.create_node("function", &name, node, params_text);
  390. let body = rhs.child_by_field_name("body");
  391. if let (Some(row), Some(body)) = (fn_row, body) {
  392. self.stack.push(Scope { row, kind: "function", name });
  393. self.visit(body);
  394. self.stack.pop();
  395. }
  396. return true;
  397. }
  398. }
  399. }
  400. let top_level = node.parent().map(|p| p.kind() == "program").unwrap_or(false);
  401. // Top-level value assignments → variable/constant (r.ts:284-296);
  402. // the class-definition idiom suppresses the twin variable node but the
  403. // rhs is ALWAYS visited.
  404. if top_level && is_assign_left(op) {
  405. if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
  406. if lhs.kind() == "identifier" {
  407. let rhs_callee = if rhs.kind() == "call" { self.callee_name(rhs) } else { None };
  408. let suppressed = rhs_callee
  409. .map(|c| is_class_fn(c) || is_generic_fn(c))
  410. .unwrap_or(false);
  411. if !suppressed {
  412. let name = self.text(lhs);
  413. let kind = if constant_name_re().is_match(name) { "constant" } else { "variable" };
  414. self.create_node(kind, name, node, None);
  415. }
  416. self.visit(rhs);
  417. return true;
  418. }
  419. }
  420. }
  421. // value -> name / value ->> name (r.ts:298-303).
  422. if top_level && is_assign_right(op) {
  423. if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
  424. if rhs.kind() == "identifier" {
  425. let name = self.text(rhs);
  426. let kind = if constant_name_re().is_match(name) { "constant" } else { "variable" };
  427. self.create_node(kind, name, node, None);
  428. self.visit(lhs);
  429. return true;
  430. }
  431. }
  432. }
  433. false
  434. }
  435. /// extractClassMembers (r.ts:110-163): arguments in source order with a
  436. /// positional counter — ggproto's 2nd positional identifier and
  437. /// `inherit`/`contains` named args become `extends` refs (from the CLASS
  438. /// row, positioned at the VALUE node); named function_definition args and
  439. /// `list(…)` entries become methods. Non-method argument subtrees are
  440. /// NEVER visited (`representation(…)`, `signature(…)` invisible).
  441. fn extract_class_members(&mut self, class_call: Node<'t>, class_row: u32) {
  442. let args = match class_call.child_by_field_name("arguments") {
  443. Some(a) => a,
  444. None => return,
  445. };
  446. let mut positional = 0u32;
  447. let mut cursor = args.walk();
  448. let arg_nodes: Vec<Node<'t>> = args.named_children(&mut cursor).collect();
  449. for arg in arg_nodes {
  450. if arg.kind() != "argument" {
  451. continue;
  452. }
  453. let arg_name = arg.child_by_field_name("name");
  454. let value = arg.child_by_field_name("value");
  455. let arg_name = match arg_name {
  456. None => {
  457. positional += 1;
  458. if positional == 2 {
  459. if let Some(v) = value {
  460. if v.kind() == "identifier" {
  461. let parent = self.text(v).to_string();
  462. self.push_ref_at(class_row, &parent, "extends", v);
  463. }
  464. }
  465. }
  466. continue;
  467. }
  468. Some(n) => n,
  469. };
  470. let arg_name_text = self.text(arg_name);
  471. // R6 `inherit = Parent` / S4 `contains = "Parent"` — a falsy
  472. // resolution (`inherit = pkg::Parent`, empty string) emits nothing.
  473. if (arg_name_text == "inherit" || arg_name_text == "contains") && value.is_some() {
  474. if let Some(parent) = self.literal_or_identifier(value) {
  475. if !parent.is_empty() {
  476. let parent = parent.to_string();
  477. self.push_ref_at(class_row, &parent, "extends", value.unwrap());
  478. }
  479. }
  480. continue;
  481. }
  482. // Direct named function argument (ggproto methods).
  483. if let Some(v) = value {
  484. if v.kind() == "function_definition" {
  485. self.emit_method_arg(arg);
  486. continue;
  487. }
  488. // list(…) of named function arguments (R5/R6 methods).
  489. if v.kind() == "call" && self.callee_name(v) == Some("list") {
  490. if let Some(list_args) = v.child_by_field_name("arguments") {
  491. let mut lc = list_args.walk();
  492. let entries: Vec<Node<'t>> = list_args.named_children(&mut lc).collect();
  493. for entry in entries {
  494. if entry.kind() == "argument" {
  495. self.emit_method_arg(entry);
  496. }
  497. }
  498. }
  499. }
  500. }
  501. }
  502. }
  503. /// emitMethodArg (r.ts:84-98): `name = function(…)` argument entry → a
  504. /// `method` node positioned at the ARGUMENT node, signature from the raw
  505. /// parameters text, body walked hook-aware inside the method scope.
  506. fn emit_method_arg(&mut self, entry: Node<'t>) {
  507. let entry_name = match entry.child_by_field_name("name") {
  508. Some(n) => n,
  509. None => return,
  510. };
  511. let entry_value = match entry.child_by_field_name("value") {
  512. Some(v) if v.kind() == "function_definition" => v,
  513. _ => return,
  514. };
  515. let params_text = entry_value.child_by_field_name("parameters").map(|p| self.text(p));
  516. let name = self.text(entry_name).to_string();
  517. let method_row = self.create_node("method", &name, entry, params_text);
  518. let body = entry_value.child_by_field_name("body");
  519. if let (Some(row), Some(body)) = (method_row, body) {
  520. self.stack.push(Scope { row, kind: "method", name });
  521. self.visit(body);
  522. self.stack.pop();
  523. }
  524. }
  525. // --- extractCall (tree-sitter.ts:3684, 4313, 4518-4532, 4572-4580) ----
  526. // R call nodes reach the generic tail: callee = RAW `function`-field text
  527. // verbatim (member branch unreachable, cpp recoveries language-gated),
  528. // then the parenthesized-conversion regex. No skipChildren — inner calls
  529. // are visited by the ladder's recursion afterward.
  530. fn extract_call(&mut self, node: Node<'t>) {
  531. if self.stack.is_empty() {
  532. return;
  533. }
  534. let caller_row = self.top_row();
  535. let func = node
  536. .child_by_field_name("function")
  537. .or_else(|| node.named_child(0));
  538. let func = match func {
  539. Some(f) => f,
  540. None => return,
  541. };
  542. let mut callee: &str = self.text(func);
  543. if let Some(caps) = util::paren_conversion().captures(callee) {
  544. if let Some(inner) = caps.get(1) {
  545. callee = &callee[inner.range()];
  546. }
  547. }
  548. if callee.is_empty() {
  549. return;
  550. }
  551. let callee = callee.to_string();
  552. self.push_ref_at(caller_row, &callee, "calls", node);
  553. }
  554. }