lua.rs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  1. //! Lua + Luau extraction — a faithful Rust port of the lua/luau paths of
  2. //! `TreeSitterExtractor` (src/extraction/tree-sitter.ts) plus
  3. //! languages/lua.ts and languages/luau.ts (36 lines extending lua).
  4. //!
  5. //! One walker, two dialects (ccpp precedent): the differences are exactly
  6. //! four — luau's typeAliasTypes=['type_definition'], the `export `-slice
  7. //! isExported hook, the return-type signature suffix, and the grammar handle.
  8. //! The authoritative quirk list is docs/design/lua-luau-kernel-port-checklist.md.
  9. //! Load-bearing oddities preserved on purpose: the require/visitNode-hook
  10. //! ASYMMETRIES (top-level requires — including inside top-level if/for/while
  11. //! blocks — mint import nodes, while the identical statement in a function
  12. //! body emits `calls "require"`; a top-level `local x = foo()` initializer
  13. //! emits NO calls ref while a top-level global `x = foo()` does), the BFS
  14. //! string-win inside require args (`require(script:WaitForChild("Kid"))` →
  15. //! import "Kid"; `require("a".."b")` → import "a"), raw-text callees verbatim
  16. //! (colon forms `M:render` with `self` never stripped, brackets `t2[k2]`,
  17. //! newline-glued chains byte-verbatim, the `(handler)` paren-conversion),
  18. //! receiver-QN methods (`M.sub.deep::chained`, stack-QN nested globals like
  19. //! `render::leakedGlobal`), variable nodes positioned at the IDENTIFIER with
  20. //! positional value pairing, LuaDoc `---` keeping a leading `- ` and
  21. //! `--!strict` joining docstring chains, the lua↔luau isExported wire
  22. //! divergence (lua functions: flag ABSENT; luau functions: present-false;
  23. //! methods: absent in both; variables: present-false in both), and duplicate
  24. //! same-(kind,name,line) ids emitted twice. Positions in UTF-16 code units.
  25. //! Files with parse errors defer to wasm (lua ~0%; luau 1.4–7.1% both-arm).
  26. use crate::buffers::{
  27. build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
  28. RefRow, Tables, FLAG_IS_EXPORTED, FUNCTION_REF_CODE, NONE, NONE_STR, StrRef,
  29. };
  30. use crate::docstring::preceding_docstring;
  31. use crate::ids;
  32. use crate::textutil as util;
  33. use std::collections::{HashSet, VecDeque};
  34. use tree_sitter::{Node, Parser};
  35. /// NAME_STOPLIST (function-ref.ts).
  36. fn is_stoplisted(name: &str) -> bool {
  37. matches!(
  38. name,
  39. "this" | "self" | "super" | "null" | "nil" | "true" | "false" | "undefined" | "new"
  40. | "NULL" | "nullptr" | "None"
  41. )
  42. }
  43. struct Scope {
  44. row: u32,
  45. kind: &'static str,
  46. name: String,
  47. }
  48. struct Cand {
  49. from: u32,
  50. name: String,
  51. line: u32,
  52. column_byte: usize,
  53. row: usize,
  54. }
  55. #[derive(Default)]
  56. struct Extra<'a> {
  57. docstring: Option<String>,
  58. signature: Option<String>,
  59. /// Some(_) sets the present bit (luau functions/type_aliases, variables
  60. /// in both dialects); None leaves the pair absent (lua functions,
  61. /// methods, imports).
  62. is_exported: Option<bool>,
  63. qualified_name_override: Option<String>,
  64. _marker: std::marker::PhantomData<&'a ()>,
  65. }
  66. pub struct Walker<'t> {
  67. src: &'t str,
  68. file_path: &'t str,
  69. is_luau: bool,
  70. line_starts: Vec<usize>,
  71. arena: Arena,
  72. tables: Tables,
  73. stack: Vec<Scope>,
  74. node_ids: Vec<String>,
  75. defined_fn_names: HashSet<String>,
  76. imported_names: HashSet<String>,
  77. fn_ref_cands: Vec<Cand>,
  78. }
  79. pub fn extract(file_path: &str, source: &str, language: &str) -> Result<EmitOut, String> {
  80. let grammar = crate::langs::grammar_for(language).ok_or("no lua/luau grammar")?;
  81. let t0 = std::time::Instant::now();
  82. let mut parser = Parser::new();
  83. parser
  84. .set_language(&grammar)
  85. .map_err(|e| format!("set_language({language}) failed: {e}"))?;
  86. let tree = parser
  87. .parse(source, None)
  88. .ok_or_else(|| "parser returned null tree".to_string())?;
  89. if tree.root_node().has_error() {
  90. return Err("defer: parse tree contains errors — wasm recovery is canonical".to_string());
  91. }
  92. let mut w = Walker {
  93. src: source,
  94. file_path,
  95. is_luau: language == "luau",
  96. line_starts: util::line_starts(source),
  97. arena: Arena::default(),
  98. tables: Tables::default(),
  99. stack: Vec::new(),
  100. node_ids: Vec::new(),
  101. defined_fn_names: HashSet::new(),
  102. imported_names: HashSet::new(),
  103. fn_ref_cands: Vec::new(),
  104. };
  105. // File node (tree-sitter.ts:508-521).
  106. let line_count = source.bytes().filter(|b| *b == b'\n').count() as u32 + 1;
  107. let base_name = file_path.rsplit(['/', '\\']).next().unwrap_or(file_path);
  108. let mut flags = BoolFlags::default();
  109. flags.set(FLAG_IS_EXPORTED, false);
  110. let file_id = w.arena.put(&ids::file_node_id(file_path));
  111. let name_ref = w.arena.put(base_name);
  112. let qn_ref = w.arena.put(file_path);
  113. w.tables.push_node(&NodeRow {
  114. kind: node_kind_index("file").unwrap(),
  115. visibility: 0,
  116. flags,
  117. start_line: 1,
  118. end_line: line_count,
  119. start_column: 0,
  120. end_column: 0,
  121. name: name_ref,
  122. qualified_name: qn_ref,
  123. id: file_id,
  124. docstring: NONE_STR,
  125. signature: NONE_STR,
  126. decorators: NONE_STR,
  127. type_parameters: NONE_STR,
  128. return_type: NONE_STR,
  129. extra_json: NONE_STR,
  130. });
  131. w.node_ids.push(ids::file_node_id(file_path));
  132. w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() });
  133. // No packageTypes → no namespace node. Value-refs are language-gated off.
  134. w.visit(tree.root_node());
  135. w.flush_fn_ref_candidates();
  136. w.stack.pop();
  137. let duration_ms = t0.elapsed().as_secs_f64() * 1000.0;
  138. let meta = build_meta(&w.tables, w.arena.len(), NONE_STR, duration_ms);
  139. Ok(EmitOut {
  140. meta,
  141. nodes: w.tables.nodes,
  142. edges: w.tables.edges,
  143. refs: w.tables.refs,
  144. arena: w.arena.into_vec(),
  145. })
  146. }
  147. impl<'t> Walker<'t> {
  148. fn text(&self, node: Node) -> &'t str {
  149. &self.src[node.byte_range()]
  150. }
  151. fn line_of(&self, node: Node) -> u32 {
  152. node.start_position().row as u32 + 1
  153. }
  154. fn col_of(&self, node: Node) -> u32 {
  155. util::col16(self.src, &self.line_starts, node.start_position().row, node.start_byte())
  156. }
  157. fn end_col_of(&self, node: Node) -> u32 {
  158. util::col16(self.src, &self.line_starts, node.end_position().row, node.end_byte())
  159. }
  160. fn top_row(&self) -> u32 {
  161. self.stack.last().map(|s| s.row).unwrap_or(0)
  162. }
  163. fn push_ref_at(&mut self, from_row: u32, name: &str, kind: &str, node: Node) {
  164. let name_ref = self.arena.put(name);
  165. self.tables.push_ref(&RefRow {
  166. from_idx: from_row,
  167. kind: edge_kind_index(kind).unwrap(),
  168. line: self.line_of(node),
  169. column: self.col_of(node),
  170. reference_name: name_ref,
  171. candidates: NONE_STR,
  172. from_id_str: NONE_STR,
  173. });
  174. // flushFnRefCandidates' importedNames gate (tree-sitter.ts:661-675):
  175. // dotted lua module paths contribute their LAST segment; simple names
  176. // (Roblox leaves) pass whole.
  177. if kind == "imports" {
  178. if util::simple_name().is_match(name) {
  179. self.imported_names.insert(name.to_string());
  180. } else if let Some(c) = util::qualified_import().captures(name) {
  181. self.imported_names.insert(c[1].to_string());
  182. }
  183. }
  184. }
  185. // --- createNode (tree-sitter.ts:1308) ---------------------------------
  186. fn create_node(&mut self, kind: &'static str, name: &str, node: Node<'t>, extra: Extra) -> Option<u32> {
  187. if name.is_empty() {
  188. return None;
  189. }
  190. let start_line = self.line_of(node);
  191. let id = ids::node_id(self.file_path, kind, name, start_line);
  192. // buildQualifiedName (1447-1460) — non-file stack NAMES joined `::`;
  193. // the receiver override (extractMethod:1790-1792) replaces it whole.
  194. let qualified = match &extra.qualified_name_override {
  195. Some(qn) => qn.clone(),
  196. None => {
  197. let mut parts: Vec<&str> = Vec::new();
  198. for s in &self.stack {
  199. if s.kind != "file" {
  200. parts.push(&s.name);
  201. }
  202. }
  203. let mut qn = parts.join("::");
  204. if !qn.is_empty() {
  205. qn.push_str("::");
  206. }
  207. qn.push_str(name);
  208. qn
  209. }
  210. };
  211. let name_ref = self.arena.put(name);
  212. let qn_ref = self.arena.put(&qualified);
  213. let id_ref = self.arena.put(&id);
  214. let doc_ref = opt_str(&mut self.arena, extra.docstring.as_deref());
  215. let sig_ref = opt_str(&mut self.arena, extra.signature.as_deref());
  216. let mut flags = BoolFlags::default();
  217. if let Some(v) = extra.is_exported {
  218. flags.set(FLAG_IS_EXPORTED, v);
  219. }
  220. let row = self.tables.push_node(&NodeRow {
  221. kind: node_kind_index(kind).unwrap(),
  222. visibility: 0,
  223. flags,
  224. start_line,
  225. end_line: node.end_position().row as u32 + 1, // no resolveBody
  226. start_column: self.col_of(node),
  227. end_column: self.end_col_of(node),
  228. name: name_ref,
  229. qualified_name: qn_ref,
  230. id: id_ref,
  231. docstring: doc_ref,
  232. signature: sig_ref,
  233. decorators: NONE_STR,
  234. type_parameters: NONE_STR,
  235. return_type: NONE_STR,
  236. extra_json: NONE_STR,
  237. });
  238. self.node_ids.push(id);
  239. if kind == "function" || kind == "method" {
  240. self.defined_fn_names.insert(name.to_string());
  241. }
  242. let parent_row = self.top_row();
  243. self.tables.push_edge(&EdgeRow {
  244. source_idx: parent_row,
  245. target_idx: row,
  246. kind: edge_kind_index("contains").unwrap(),
  247. provenance: 0,
  248. line: NONE,
  249. column: NONE,
  250. metadata_json: NONE_STR,
  251. source_id_str: NONE_STR,
  252. target_id_str: NONE_STR,
  253. });
  254. Some(row)
  255. }
  256. // --- lua.ts helper transcriptions -------------------------------------
  257. /// findDescendant (lua.ts:9-17) — breadth-first over namedChildren.
  258. fn find_descendant(&self, node: Node<'t>, kind: &str) -> Option<Node<'t>> {
  259. let mut queue: VecDeque<Node<'t>> = VecDeque::new();
  260. let mut cursor = node.walk();
  261. for c in node.named_children(&mut cursor) {
  262. queue.push_back(c);
  263. }
  264. while let Some(n) = queue.pop_front() {
  265. if n.kind() == kind {
  266. return Some(n);
  267. }
  268. let mut cur = n.walk();
  269. for c in n.named_children(&mut cur) {
  270. queue.push_back(c);
  271. }
  272. }
  273. None
  274. }
  275. /// requireModule (lua.ts:28-60).
  276. fn require_module(&self, call: Node<'t>) -> Option<String> {
  277. let name = call.child_by_field_name("name")?;
  278. if name.kind() != "identifier" || self.text(name) != "require" {
  279. return None;
  280. }
  281. let args = call.child_by_field_name("arguments")?;
  282. // String win: first string_content descendant, BFS order.
  283. if let Some(content) = self.find_descendant(args, "string_content") {
  284. let t = self.text(content).trim();
  285. return if t.is_empty() { None } else { Some(t.to_string()) };
  286. }
  287. // Fallback: a string node with no content child — strip [[ ]] / quotes.
  288. if let Some(s) = self.find_descendant(args, "string") {
  289. let mut t = self.text(s).trim();
  290. t = t.strip_prefix("[[").unwrap_or(t);
  291. t = t.strip_suffix("]]").unwrap_or(t);
  292. t = t.strip_prefix(['"', '\'']).unwrap_or(t);
  293. t = t.strip_suffix(['"', '\'']).unwrap_or(t);
  294. if !t.is_empty() {
  295. return Some(t.to_string());
  296. }
  297. }
  298. // Roblox instance path: trailing field/method segment.
  299. let idx = self
  300. .find_descendant(args, "dot_index_expression")
  301. .or_else(|| self.find_descendant(args, "method_index_expression"));
  302. if let Some(idx) = idx {
  303. if let Some(field) = idx
  304. .child_by_field_name("field")
  305. .or_else(|| idx.child_by_field_name("method"))
  306. {
  307. let t = self.text(field).trim();
  308. return if t.is_empty() { None } else { Some(t.to_string()) };
  309. }
  310. }
  311. None
  312. }
  313. /// The hook's `emit` (lua.ts:108-126): import node at the CALL node +
  314. /// imports ref from the stack top.
  315. fn emit_require(&mut self, call: Node<'t>, module: &str) {
  316. let (sig, _) = util::slice_utf16(self.text(call).trim(), 100);
  317. let imp = self.create_node(
  318. "import",
  319. module,
  320. call,
  321. Extra { signature: Some(sig), ..Default::default() },
  322. );
  323. if imp.is_some() && !self.stack.is_empty() {
  324. let parent_row = self.top_row();
  325. self.push_ref_at(parent_row, module, "imports", call);
  326. }
  327. }
  328. /// getReceiverType (lua.ts:92-99).
  329. fn receiver_type(&self, node: Node<'t>) -> Option<&'t str> {
  330. let name = node.child_by_field_name("name")?;
  331. if name.kind() == "dot_index_expression" || name.kind() == "method_index_expression" {
  332. return name.child_by_field_name("table").map(|t| self.text(t));
  333. }
  334. None
  335. }
  336. /// extractName (tree-sitter.ts:98-192) — the lua-reachable branches.
  337. fn extract_name(&self, node: Node<'t>) -> String {
  338. if let Some(name_node) = node.child_by_field_name("name") {
  339. // Lua: dot/method index → the trailing field/method segment.
  340. if name_node.kind() == "dot_index_expression" {
  341. if let Some(f) = name_node.child_by_field_name("field") {
  342. return self.text(f).to_string();
  343. }
  344. }
  345. if name_node.kind() == "method_index_expression" {
  346. if let Some(m) = name_node.child_by_field_name("method") {
  347. return self.text(m).to_string();
  348. }
  349. }
  350. return self.text(name_node).to_string();
  351. }
  352. // Fallback: first identifier-ish named child.
  353. let mut cursor = node.walk();
  354. for c in node.named_children(&mut cursor) {
  355. if matches!(c.kind(), "identifier" | "type_identifier" | "simple_identifier" | "constant") {
  356. return self.text(c).to_string();
  357. }
  358. }
  359. "<anonymous>".to_string()
  360. }
  361. /// getSignature — lua (lua.ts:83-86) / luau (luau.ts:26-35).
  362. fn signature_of(&self, node: Node<'t>) -> Option<String> {
  363. let params = node.child_by_field_name("parameters")?;
  364. let mut sig = self.text(params).to_string();
  365. if self.is_luau {
  366. // Return type = the named child AFTER `parameters` (found by
  367. // startIndex match), unless it's the block.
  368. let mut cursor = node.walk();
  369. let kids: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
  370. if let Some(idx) = kids.iter().position(|k| k.start_byte() == params.start_byte()) {
  371. if let Some(ret) = kids.get(idx + 1) {
  372. if ret.kind() != "block" {
  373. sig.push_str(": ");
  374. sig.push_str(self.text(*ret));
  375. }
  376. }
  377. }
  378. }
  379. Some(sig)
  380. }
  381. /// isExported (luau.ts:23) — the raw 7-unit slice is an ASCII prefix test.
  382. fn is_exported_of(&self, node: Node<'t>) -> Option<bool> {
  383. if self.is_luau {
  384. Some(self.text(node).starts_with("export "))
  385. } else {
  386. None
  387. }
  388. }
  389. // --- the main walk (visitNode, tree-sitter.ts:936-1303) ---------------
  390. fn visit(&mut self, node: Node<'t>) {
  391. let kind = node.kind();
  392. // The visitNode hook (lua.ts:105-151) runs FIRST.
  393. if kind == "function_call" {
  394. if let Some(module) = self.require_module(node) {
  395. self.emit_require(node, &module);
  396. // Consumed → scanFnRefSubtree (tree-sitter.ts:951).
  397. self.scan_fn_ref_subtree(node, 0);
  398. return;
  399. }
  400. // falls through — extractCall claims it below
  401. } else if kind == "variable_declaration" {
  402. // `local x = require(...)` — dig requires out of the initializer
  403. // the variable branch will skip. Always falls through.
  404. let mut cursor = node.walk();
  405. let assign = node.named_children(&mut cursor).find(|c| c.kind() == "assignment_statement");
  406. if let Some(assign) = assign {
  407. let mut ac = assign.walk();
  408. let expr_list = assign.named_children(&mut ac).find(|c| c.kind() == "expression_list");
  409. if let Some(expr_list) = expr_list {
  410. let mut ec = expr_list.walk();
  411. let vals: Vec<Node<'t>> = expr_list.named_children(&mut ec).collect();
  412. for val in vals {
  413. if val.kind() == "function_call" {
  414. if let Some(module) = self.require_module(val) {
  415. self.emit_require(val, &module);
  416. }
  417. }
  418. }
  419. }
  420. }
  421. }
  422. // maybeCaptureFnRefs (tree-sitter.ts:990).
  423. self.maybe_capture_fn_refs(node);
  424. // The dispatch ladder — lua/luau rows only.
  425. if kind == "function_declaration" {
  426. // isInsideClassLikeNode is always false (no class-like kinds).
  427. self.extract_function(node);
  428. return; // skipChildren — the body walk handles children
  429. }
  430. if self.is_luau && kind == "type_definition" {
  431. let skip = self.extract_type_alias(node);
  432. if skip {
  433. return;
  434. }
  435. // plain path returns false → children re-visited (the
  436. // typeof(require(...)) alias+import pair rides this).
  437. } else if kind == "variable_declaration" {
  438. self.extract_variable(node);
  439. // Initializer subtrees are never walked — candidates only.
  440. self.scan_fn_ref_subtree(node, 0);
  441. return; // skipChildren
  442. } else if kind == "function_call" {
  443. self.extract_call(node);
  444. // no skipChildren — nested/inner calls each get their own ref
  445. }
  446. let mut cursor = node.walk();
  447. let children: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
  448. for child in children {
  449. self.visit(child);
  450. }
  451. }
  452. // --- extractFunction / extractMethod (1517 / 1737) --------------------
  453. fn extract_function(&mut self, node: Node<'t>) {
  454. // :1522 receiver short-circuit IS the method routing.
  455. if let Some(receiver) = self.receiver_type(node) {
  456. let receiver = receiver.to_string();
  457. self.extract_method(node, receiver);
  458. return;
  459. }
  460. let name = self.extract_name(node);
  461. if name == "<anonymous>" {
  462. // Unreachable for function_declaration (grammar requires a name)
  463. // but preserved: body walked with nothing pushed.
  464. if let Some(body) = node.child_by_field_name("body") {
  465. self.visit_body(body);
  466. }
  467. return;
  468. }
  469. let docstring = preceding_docstring(node, self.src);
  470. let signature = self.signature_of(node);
  471. let is_exported = self.is_exported_of(node); // lua None / luau Some(false)
  472. let fn_row = self.create_node(
  473. "function",
  474. &name,
  475. node,
  476. Extra { docstring, signature, is_exported, ..Default::default() },
  477. );
  478. let Some(row) = fn_row else { return };
  479. // extractTypeAnnotations / extractDecoratorsFor: structurally zero
  480. // output for lua/luau (gates + no decorator kinds in scan positions).
  481. self.stack.push(Scope { row, kind: "function", name });
  482. if let Some(body) = node.child_by_field_name("body") {
  483. self.visit_body(body);
  484. }
  485. self.stack.pop();
  486. }
  487. fn extract_method(&mut self, node: Node<'t>, receiver: String) {
  488. let name = self.extract_name(node);
  489. let docstring = preceding_docstring(node, self.src);
  490. let signature = self.signature_of(node);
  491. // extractMethod passes NO isExported — absent for BOTH dialects.
  492. // QN override (:1790-1792): `receiver::name` verbatim (namespacePrefix
  493. // is empty outside C++).
  494. let qn = format!("{receiver}::{name}");
  495. let method_row = self.create_node(
  496. "method",
  497. &name,
  498. node,
  499. Extra {
  500. docstring,
  501. signature,
  502. qualified_name_override: Some(qn),
  503. ..Default::default()
  504. },
  505. );
  506. let Some(row) = method_row else { return };
  507. // Owner-contains (:1799-1813) never fires: lua mints no
  508. // struct/class/enum/trait nodes for a receiver name to match.
  509. self.stack.push(Scope { row, kind: "method", name });
  510. if let Some(body) = node.child_by_field_name("body") {
  511. self.visit_body(body);
  512. }
  513. self.stack.pop();
  514. }
  515. // --- extractVariable — the lua/luau branch (2538-2549, 2789-2805) -----
  516. fn extract_variable(&mut self, node: Node<'t>) {
  517. // isConst absent → kind is ALWAYS `variable`; docstring from the
  518. // DECLARATION node; isExported = hook ?? false → false for BOTH
  519. // dialects (luau's slice sees `local …`).
  520. let docstring = preceding_docstring(node, self.src);
  521. let is_exported = self.is_exported_of(node).unwrap_or(false);
  522. let mut cursor = node.walk();
  523. let assign = node
  524. .named_children(&mut cursor)
  525. .find(|c| c.kind() == "assignment_statement")
  526. .unwrap_or(node);
  527. let mut ac = assign.walk();
  528. let var_list = assign.named_children(&mut ac).find(|c| c.kind() == "variable_list");
  529. let mut ec = assign.walk();
  530. let expr_list = assign.named_children(&mut ec).find(|c| c.kind() == "expression_list");
  531. let values: Vec<Node<'t>> = match expr_list {
  532. Some(el) => {
  533. let mut c = el.walk();
  534. el.named_children(&mut c).collect()
  535. }
  536. None => Vec::new(),
  537. };
  538. let names: Vec<Node<'t>> = match var_list {
  539. Some(vl) => {
  540. let mut c = vl.walk();
  541. vl.named_children(&mut c).filter(|n| n.kind() == "identifier").collect()
  542. }
  543. None => Vec::new(),
  544. };
  545. for (i, name_node) in names.iter().enumerate() {
  546. let name = self.text(*name_node);
  547. if name.is_empty() {
  548. continue;
  549. }
  550. // Positional value pairing; a missing value → NO signature key.
  551. let signature = values.get(i).map(|v| util::init_signature(self.text(*v)));
  552. let name = name.to_string();
  553. self.create_node(
  554. "variable",
  555. &name,
  556. *name_node, // positioned at the IDENTIFIER
  557. Extra {
  558. docstring: docstring.clone(),
  559. signature,
  560. is_exported: Some(is_exported),
  561. ..Default::default()
  562. },
  563. );
  564. }
  565. }
  566. // --- extractTypeAlias (2890; plain path 2967-2991) — luau only --------
  567. /// Returns skipChildren (always false on the plain path).
  568. fn extract_type_alias(&mut self, node: Node<'t>) -> bool {
  569. let name = self.extract_name(node); // generic_type name → verbatim text
  570. if name == "<anonymous>" {
  571. return false;
  572. }
  573. let docstring = preceding_docstring(node, self.src);
  574. let is_exported = self.is_exported_of(node); // Some(true) for `export type`
  575. self.create_node(
  576. "type_alias",
  577. &name,
  578. node,
  579. Extra { docstring, is_exported, ..Default::default() },
  580. );
  581. // TYPE_ANNOTATION_LANGUAGES excludes luau → no alias-value refs.
  582. false // children re-visited by the ladder
  583. }
  584. // --- extractCall (3684; generic tail 4313, 4518-4532, 4572-4580) ------
  585. fn extract_call(&mut self, node: Node<'t>) {
  586. if self.stack.is_empty() {
  587. return;
  588. }
  589. let caller_row = self.top_row();
  590. // The `function` field is NULL in this grammar → namedChild(0) (the
  591. // `name:` child). Member branch never fires (dot/method_index aren't
  592. // in its type list) → raw source text, then the paren-conversion.
  593. let func = node
  594. .child_by_field_name("function")
  595. .or_else(|| node.named_child(0));
  596. let Some(func) = func else { return };
  597. let mut callee: &str = self.text(func);
  598. if let Some(caps) = util::paren_conversion().captures(callee) {
  599. if let Some(inner) = caps.get(1) {
  600. callee = &callee[inner.range()];
  601. }
  602. }
  603. if callee.is_empty() {
  604. return;
  605. }
  606. let callee = callee.to_string();
  607. self.push_ref_at(caller_row, &callee, "calls", node);
  608. }
  609. // --- visitFunctionBody (5129-5286) — the hook-free body walk ----------
  610. fn visit_body(&mut self, node: Node<'t>) {
  611. // maybeCaptureFnRefs (5137) fires in the body walker too.
  612. self.maybe_capture_fn_refs(node);
  613. let kind = node.kind();
  614. if kind == "function_call" {
  615. // The hook NEVER runs here — a body-level require emits
  616. // `calls "require"` (the neovim lazy-loading idiom).
  617. self.extract_call(node);
  618. // falls through to recursion — chains emit every link
  619. } else if kind == "function_declaration" {
  620. // Nested NAMED functions (5245-5250): extractFunction walks the
  621. // nested body itself, so return. extractName is never
  622. // `<anonymous>` for function_declaration.
  623. self.extract_function(node);
  624. return;
  625. }
  626. // variable_declaration / type_definition have NO branch here → plain
  627. // recursion: body-local initializers ARE walked (calls emit), no
  628. // variable/type_alias nodes minted.
  629. let mut cursor = node.walk();
  630. let children: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
  631. for child in children {
  632. self.visit_body(child);
  633. }
  634. }
  635. // --- function-as-value capture (#756) — LUA_SPEC ----------------------
  636. fn maybe_capture_fn_refs(&mut self, node: Node<'t>) {
  637. // LUA_SPEC dispatch: arguments → args; assignment_statement → rhs
  638. // (no field — last named child; param-storage skip via namedChild(0));
  639. // field → value (field 'value', last-named-child fallback).
  640. let mode: &str = match node.kind() {
  641. "arguments" => "args",
  642. "assignment_statement" => "rhs",
  643. "field" => "value",
  644. _ => return,
  645. };
  646. if self.stack.is_empty() {
  647. return;
  648. }
  649. let from = self.top_row();
  650. let mut values: Vec<Node<'t>> = Vec::new();
  651. match mode {
  652. "args" => {
  653. let mut cursor = node.walk();
  654. for c in node.named_children(&mut cursor) {
  655. values.push(c);
  656. }
  657. }
  658. "rhs" => {
  659. // No `field` in the rule → RHS = LAST named child (the
  660. // expression_list). Param-storage skip: lhs =
  661. // left/lhs/target field ?? namedChild(0) when ≥2 children;
  662. // its trailing identifier vs the whole RHS text.
  663. let count = node.named_child_count();
  664. let rhs = if count > 0 { node.named_child(count - 1) } else { None };
  665. if let Some(rhs) = rhs {
  666. let lhs = node
  667. .child_by_field_name("left")
  668. .or_else(|| node.child_by_field_name("lhs"))
  669. .or_else(|| node.child_by_field_name("target"))
  670. .or_else(|| if count >= 2 { node.named_child(0) } else { None });
  671. let lhs_text = lhs.map(|l| self.text(l)).unwrap_or("");
  672. let lhs_last = util::lhs_last_name()
  673. .captures(lhs_text)
  674. .and_then(|c| c.get(1))
  675. .map(|m| m.as_str());
  676. if !(lhs_last.is_some() && lhs_last == Some(self.text(rhs).trim())) {
  677. values.push(rhs);
  678. }
  679. }
  680. }
  681. _ => {
  682. // value — the `value` field (keyed AND positional table
  683. // fields carry it), falling back to the last named child.
  684. let v = node.child_by_field_name("value").or_else(|| {
  685. let count = node.named_child_count();
  686. if count > 0 { node.named_child(count - 1) } else { None }
  687. });
  688. if let Some(v) = v {
  689. values.push(v);
  690. }
  691. }
  692. }
  693. for v in values {
  694. self.normalize_fn_ref_value(v, from, 0);
  695. }
  696. }
  697. /// normalizeValue with LUA_SPEC's one transparent layer (expression_list
  698. /// fans out to named children).
  699. fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
  700. if depth > 4 {
  701. return;
  702. }
  703. match v.kind() {
  704. "identifier" => {
  705. let name = self.text(v).to_string();
  706. if name.is_empty() || is_stoplisted(&name) {
  707. return;
  708. }
  709. let p = v.start_position();
  710. self.fn_ref_cands.push(Cand {
  711. from,
  712. name,
  713. line: p.row as u32 + 1,
  714. column_byte: v.start_byte(),
  715. row: p.row,
  716. });
  717. }
  718. "expression_list" => {
  719. let mut cursor = v.walk();
  720. let kids: Vec<Node<'t>> = v.named_children(&mut cursor).collect();
  721. for c in kids {
  722. self.normalize_fn_ref_value(c, from, depth + 1);
  723. }
  724. }
  725. _ => {}
  726. }
  727. }
  728. fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
  729. if depth > 12 {
  730. return;
  731. }
  732. // Halt at nested function definitions (their bodies are walked — and
  733. // attributed — by extractFunction). function_definition (anonymous)
  734. // is deliberately NOT in the halt list — the scan descends into
  735. // anonymous initializer bodies, attributing candidates to the file.
  736. if depth > 0
  737. && matches!(
  738. node.kind(),
  739. "function_declaration" | "arrow_function" | "function_expression"
  740. | "lambda_literal" | "lambda_expression"
  741. )
  742. {
  743. return;
  744. }
  745. self.maybe_capture_fn_refs(node);
  746. let mut cursor = node.walk();
  747. let children: Vec<Node<'t>> = node.named_children(&mut cursor).collect();
  748. for c in children {
  749. self.scan_fn_ref_subtree(c, depth + 1);
  750. }
  751. }
  752. fn flush_fn_ref_candidates(&mut self) {
  753. let cands = std::mem::take(&mut self.fn_ref_cands);
  754. if cands.is_empty() || util::is_generated_file(self.file_path) {
  755. return;
  756. }
  757. let mut seen: HashSet<(String, String)> = HashSet::new();
  758. for c in cands {
  759. // Gate: same-file function/method names ∪ imported names (lua
  760. // candidates are always bare identifiers — no `this.`/`::`).
  761. if !c.name.starts_with("this.")
  762. && !c.name.contains("::")
  763. && !self.defined_fn_names.contains(&c.name)
  764. && !self.imported_names.contains(&c.name)
  765. {
  766. continue;
  767. }
  768. if !seen.insert((self.node_ids[c.from as usize].clone(), c.name.clone())) {
  769. continue;
  770. }
  771. let column = util::col16(self.src, &self.line_starts, c.row, c.column_byte);
  772. let name_ref = self.arena.put(&c.name);
  773. self.tables.push_ref(&RefRow {
  774. from_idx: c.from,
  775. kind: FUNCTION_REF_CODE,
  776. line: c.line,
  777. column,
  778. reference_name: name_ref,
  779. candidates: NONE_STR,
  780. from_id_str: NONE_STR,
  781. });
  782. }
  783. }
  784. }
  785. fn opt_str(arena: &mut Arena, s: Option<&str>) -> StrRef {
  786. match s {
  787. Some(s) => arena.put(s),
  788. None => NONE_STR,
  789. }
  790. }