1use bonsai_common::FileId;
3use bonsai_lang_api::{
4 decl_index_with_handler, extract_imports_via,
5 kit::{
6 collect_kinds, collect_receiver_field_writes, first_named_child_of_kind, language_from_pack,
7 node_text, parse_with, span_of,
8 },
9 AdapterContext, AdapterError, AssignValueKind, CallTargetExtraction, DeclIndex, FlowEvent,
10 GrammarHandler, ImportIndex, ImportScope, ImportSpec, LanguageAdapter, LanguageCapabilities, LanguageId,
11 Ref, RefKind,
12};
13use tree_sitter::{Language, Node, Tree};
14
15fn lua_foreach_binding(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
16 if !matches!(node.kind(), "for_statement" | "for_in_statement") {
17 return None;
18 }
19 let clause = node.child_by_field_name("clause").or_else(|| {
20 let mut cursor = node.walk();
21 let clause = node
22 .named_children(&mut cursor)
23 .find(|child| child.kind() == "for_generic_clause");
24 clause
25 })?;
26 Some((clause.named_child(0)?, clause.named_child(1)?))
27}
28
29fn lua_call_target<'tree>(node: Node<'tree>, src: &[u8]) -> Option<CallTargetExtraction<'tree>> {
35 if !matches!(node.kind(), "function_call" | "method_call") {
36 return None;
37 }
38 let target = node.child_by_field_name("name").or_else(|| node.named_child(0))?;
39 if !matches!(
40 target.kind(),
41 "identifier" | "dot_index_expression" | "bracket_index_expression" | "method_index_expression"
42 ) {
43 return None;
44 }
45 let full_text = node_text(&target, src)
46 .chars()
47 .filter(|character| !character.is_whitespace())
48 .collect::<String>();
49 (!full_text.is_empty()).then_some(CallTargetExtraction {
50 node: target,
51 full_text,
52 })
53}
54
55pub const LANG_ID: LanguageId = LanguageId::new("lua");
56const PACK_NAME: &str = "lua";
57
58fn lua_static_key(node: Node<'_>, src: &[u8]) -> Option<String> {
59 let raw = node_text(&node, src).trim();
60 if node.kind() == "identifier" {
61 return (!raw.is_empty()).then(|| raw.to_string());
62 }
63 if node.kind() != "string" {
64 return None;
65 }
66 let quote = raw.as_bytes().first().copied()?;
67 if !matches!(quote, b'\'' | b'"') || raw.as_bytes().last().copied() != Some(quote) {
68 return None;
69 }
70 let value = raw.get(1..raw.len().checked_sub(1)?)?;
71 (!value.is_empty() && !value.contains('\\')).then(|| value.to_string())
72}
73
74const HANDLER: GrammarHandler = GrammarHandler {
82 expression_value_kind_extractor: None,
83 literal_value_kinds: &["nil", "number", "true", "false"],
84 string_literal_kinds: &["string"],
85 comment_kinds: &["comment", "hash_bang_line"],
86 doc_comment_prefixes: &["---"],
87 decorator_kinds: &[],
88 parameter_container_kinds: &["parameters"],
89 parameter_kinds: &["identifier", "vararg_expression"],
90 parameter_annotation_name_extractor: None,
91 variadic_parameter_kinds: &["vararg_expression"],
92 binding_identifier_kinds: &["identifier"],
93 identifier_kinds: &["identifier"],
94 aggregate_pattern_kinds: &["variable_list"],
95 named_aggregate_kinds: &["table_constructor"],
96 positional_aggregate_kinds: &["table_constructor"],
97 aggregate_pair_kinds: &["field"],
98 aggregate_key_field_names: &["name"],
99 aggregate_value_field_names: &["value"],
100 static_field_name_kinds: &["identifier"],
101 static_subscript_key_extractor: Some(lua_static_key),
102 lambda_value_container_kinds: &["table_constructor", "field"],
103 transparent_call_wrapper_kinds: &["dot_index_expression", "bracket_index_expression"],
104 single_expression_group_kinds: &["expression_list", "variable_list"],
108 assignment_target_wrapper_kinds: &["variable_declaration"],
109 binding_declaration_keyword_spellings: &["local"],
110 nested_type_ownership: true,
111 fn_kinds: &["function_declaration", "function_definition", "local_function"],
112 class_kinds: &[],
113 class_decl_kinds: &[],
114 method_kinds: &[],
115 method_context_kinds: &[],
116 method_owner_barrier_kinds: &[],
117 constructor_method_kinds: &[],
118 constructor_names: bonsai_lang_api::NO_CONSTRUCTOR_METHOD_NAMES,
119 if_kinds: &["if_statement"],
120 branch_then_field_names: &["consequence", "body"],
121 branch_else_field_names: &["alternative"],
122 branch_condition_field_names: &["condition"],
123 loop_body_field_names: &["body"],
124 loop_body_kinds: &["block"],
125 branch_arm_kinds: &["block", "elseif_statement", "else_statement"],
126 additional_alternative_kinds: &["elseif_statement", "else_statement"],
127 for_kinds: &["for_statement"],
128 foreach_kinds: &["for_in_statement"],
129 foreach_binding_extractor: Some(lua_foreach_binding),
130 while_kinds: &["while_statement"],
131 do_kinds: &["repeat_statement"],
132 loop_kinds: &[],
133 call_kinds: &["function_call", "method_call"],
134 call_callee_field_names: &["name"],
135 call_receiver_field_names: &["table", "prefix"],
136 call_member_field_names: &["method", "field"],
137 call_argument_field_names: &["arguments"],
138 call_argument_container_kinds: &["arguments"],
139 call_target_extractor: Some(lua_call_target),
140 lambda_body_field_names: &["body"],
141 argument_passing_mode_extractor: None,
142 call_ref_kinds: &["function_call", "method_call"],
143 member_expression_kinds: &["dot_index_expression"],
144 subscript_expression_kinds: &["bracket_index_expression"],
145 member_base_field_names: &["table", "prefix"],
146 member_name_field_names: &["field"],
147 subscript_base_field_names: &["table", "prefix"],
148 subscript_index_field_names: &["field", "index"],
152 assignment_kinds: &["assignment_statement", "variable_declaration"],
153 return_kinds: &["return_statement"],
154 throw_kinds: &[],
155 lambda_kinds: &["function_definition"],
156 try_kinds: &[],
157 catch_kinds: &[],
158 finally_kinds: &[],
159 break_kinds: &["break_statement"],
160 control_label_field_names: &[],
161 continue_kinds: &[],
165 yield_kinds: &[],
166 await_kinds: &[],
167 defer_kinds: &[],
168 using_kinds: &[],
169 special_forms: &[],
170 method_receiver_param_index: None,
171 implicit_receiver_names: &[],
172 implicit_receiver_prefixes: &[],
173 tail_expression_returns: false,
174 void_return_type_names: &[],
175 ..bonsai_lang_api::EMPTY_HANDLER
176};
177
178#[derive(Debug, Default, Copy, Clone)]
179pub struct LuaAdapter;
180
181impl LuaAdapter {
182 #[must_use]
183 pub fn new() -> Self {
184 Self
185 }
186}
187
188impl LanguageAdapter for LuaAdapter {
189 fn language_id(&self) -> LanguageId {
190 LANG_ID
191 }
192 fn display_name(&self) -> &'static str {
193 "Lua"
194 }
195 fn file_extensions(&self) -> &'static [&'static str] {
196 &["lua"]
197 }
198 fn tree_sitter_language(&self) -> Result<Language, AdapterError> {
199 language_from_pack(PACK_NAME)
200 }
201 fn capabilities(&self) -> LanguageCapabilities {
202 LanguageCapabilities {
203 module_default_export_names: &[],
204 universal_type_names: &[],
205 module_path_syntax: bonsai_lang_api::ModulePathSyntax::none(),
206 constructor_method_names: bonsai_lang_api::NO_CONSTRUCTOR_METHOD_NAMES,
207 super_receiver_tokens: &[],
208 implicit_receiver_tokens: &["self"],
211 ..LanguageCapabilities::partial_baseline()
212 }
213 }
214 fn extract_declarations(&self, file: FileId, ctx: &AdapterContext<'_>) -> DeclIndex {
215 let mut idx = decl_index_with_handler(PACK_NAME, file, ctx, &HANDLER);
216 let (local_fn_spans, table_member_names) =
217 if let Some((snapshot, tree)) = bonsai_lang_api::kit::parse_with(PACK_NAME, file, ctx) {
218 let source = snapshot.text.as_bytes();
219 idx.refs
220 .extend(synthesize_lua_global_arg_refs(&tree, source, file));
221 let mut spans: Vec<bonsai_common::Span> = collect_kinds(&tree, &["local_function"])
231 .into_iter()
232 .map(|local_fn_node| span_of(file, &local_fn_node))
233 .collect();
234 let root = tree.root_node();
235 let mut chunk_cursor = root.walk();
236 if chunk_cursor.goto_first_child() {
240 loop {
241 if chunk_cursor.field_name() == Some("local_declaration")
242 && chunk_cursor.node().kind() == "function_declaration"
243 {
244 spans.push(span_of(file, &chunk_cursor.node()));
245 }
246 if !chunk_cursor.goto_next_sibling() {
247 break;
248 }
249 }
250 }
251 (spans, collect_lua_table_member_names(&tree, source, file))
252 } else {
253 (Vec::new(), Vec::new())
254 };
255 bonsai_lang_api::apply_file_stem_semantic_identity(&mut idx, ctx);
259 apply_lua_table_member_semantic_identity(&mut idx, &table_member_names);
260 for decl in &mut idx.defs {
264 if local_fn_spans.contains(&decl.span) {
265 decl.visibility = bonsai_lang_api::Visibility::Private;
266 }
267 }
268 if let Some((snapshot, tree)) = bonsai_lang_api::kit::parse_with(PACK_NAME, file, ctx) {
275 let src = snapshot.text.as_bytes();
276 if let Some(table_name) = collect_lua_module_export_table(&tree, src) {
277 let table_dotted_prefix = format!("{table_name}.");
278 let table_member_decls: std::collections::HashSet<bonsai_common::Span> =
279 collect_lua_table_member_decl_spans(&tree, src, &table_name, file);
280 for decl in &mut idx.defs {
281 if !matches!(decl.kind, bonsai_lang_api::DeclKind::Function) {
282 continue;
283 }
284 if decl.parent.is_some() {
285 continue;
286 }
287 if matches!(decl.visibility, bonsai_lang_api::Visibility::Private) {
288 continue;
289 }
290 let attached_to_table = table_member_decls.contains(&decl.span)
291 || decl.name.starts_with(&table_dotted_prefix);
292 if !attached_to_table {
293 decl.visibility = bonsai_lang_api::Visibility::Module;
294 }
295 }
296 }
297 }
298 let table_field_assigns = parse_with(PACK_NAME, file, ctx)
299 .map(|(snapshot, tree)| {
300 collect_lua_table_literal_field_assigns(&tree, snapshot.text.as_bytes(), file)
301 })
302 .unwrap_or_default();
303 for decl in &mut idx.defs {
304 insert_lua_table_field_assigns_in_events(&mut decl.flow_events, &table_field_assigns);
305 normalize_lua_dot_calls(&mut decl.flow_events);
306 enrich_lua_factory_receiver_field_writes(decl);
307 bonsai_lang_api::normalize_call_result_assignment_sources(&mut decl.flow_events);
308 }
309 bonsai_lang_api::apply_constructor_result_type_aliases(&mut idx);
317 bonsai_lang_api::apply_class_field_type_aliases(&mut idx);
318 idx
319 }
320 fn extract_imports(&self, file: FileId, ctx: &AdapterContext<'_>) -> ImportIndex {
321 let mut idx = extract_imports_via(PACK_NAME, file, ctx, parse_imports);
322 if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
323 if let (Some(table_name), Some(module)) = (
324 collect_lua_module_export_table(&tree, snapshot.text.as_bytes()),
325 lua_file_module_name(file, ctx),
326 ) {
327 idx.imports.push(ImportSpec {
328 span: span_of(file, &tree.root_node()),
329 module,
330 alias: Some(table_name),
331 is_wildcard: false,
332 original_name: None,
333 scope: ImportScope::Local,
338 });
339 }
340 }
341 idx
342 }
343}
344
345fn collect_lua_table_member_names(
350 tree: &Tree,
351 src: &[u8],
352 file: FileId,
353) -> Vec<(bonsai_common::Span, String)> {
354 let mut out = Vec::new();
355 for declaration in collect_kinds(tree, &["function_declaration"]) {
356 let Some(name_node) = declaration.child_by_field_name("name") else {
357 continue;
358 };
359 let rendered = node_text(&name_node, src).trim();
360 if !rendered.contains(['.', ':']) {
361 continue;
362 }
363 let canonical = rendered
364 .chars()
365 .filter(|character| !character.is_whitespace())
366 .map(|character| if character == ':' { '.' } else { character })
367 .collect::<String>();
368 if canonical.split('.').any(str::is_empty) {
369 continue;
370 }
371 out.push((span_of(file, &declaration), canonical));
372 }
373 out
374}
375
376fn apply_lua_table_member_semantic_identity(
377 index: &mut DeclIndex,
378 table_members: &[(bonsai_common::Span, String)],
379) {
380 for declaration in &mut index.defs {
381 let Some((_, qualified_name)) = table_members.iter().find(|(span, _)| *span == declaration.span)
382 else {
383 continue;
384 };
385 declaration.qualified_name = Some(qualified_name.clone());
386 }
387}
388
389#[derive(Clone, Debug)]
390struct LuaTableFieldAssigns {
391 assign_span: bonsai_common::Span,
392 target: String,
393 fields: Vec<FlowEvent>,
394}
395
396fn collect_lua_table_literal_field_assigns(
397 tree: &Tree,
398 src: &[u8],
399 file: FileId,
400) -> Vec<LuaTableFieldAssigns> {
401 let mut out = Vec::new();
402 for assignment in collect_kinds(tree, &["assignment_statement"]) {
403 let Some(variable_list) = first_named_child_of_kind(&assignment, "variable_list") else {
404 continue;
405 };
406 let Some(expression_list) = first_named_child_of_kind(&assignment, "expression_list") else {
407 continue;
408 };
409 let Some(target_node) = variable_list
410 .child_by_field_name("name")
411 .or_else(|| first_named_child_of_kind(&variable_list, "identifier"))
412 else {
413 continue;
414 };
415 let target = node_text(&target_node, src).trim();
416 if target.is_empty() || !target.chars().all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) {
417 continue;
418 }
419 let Some(table) = expression_list
420 .child_by_field_name("value")
421 .filter(|node| node.kind() == "table_constructor")
422 .or_else(|| first_named_child_of_kind(&expression_list, "table_constructor"))
423 else {
424 continue;
425 };
426 let mut fields = Vec::new();
427 let mut cursor = table.walk();
428 for field in table
429 .named_children(&mut cursor)
430 .filter(|node| node.kind() == "field")
431 {
432 let Some(name_node) = field.child_by_field_name("name") else {
433 continue;
434 };
435 let Some(value_node) = field.child_by_field_name("value") else {
436 continue;
437 };
438 let key = node_text(&name_node, src).trim();
439 if key.is_empty() || !key.chars().all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) {
440 continue;
441 }
442 let sources = lua_value_source_names(value_node, src);
443 fields.push(FlowEvent::Assign {
444 span: span_of(file, &value_node),
445 target: format!("{target}.{key}"),
446 source_name: (sources.len() == 1).then(|| sources[0].clone()),
447 source_call: None,
448 source_call_args: Vec::new(),
449 source_names: sources.clone(),
450 declares_new_binding: false,
451 value_kind: Some(if sources.is_empty() {
452 AssignValueKind::Literal
453 } else {
454 AssignValueKind::Compound
455 }),
456 });
457 }
458 if !fields.is_empty() {
459 out.push(LuaTableFieldAssigns {
460 assign_span: span_of(file, &assignment),
461 target: target.to_string(),
462 fields,
463 });
464 }
465 }
466 out
467}
468
469fn lua_value_source_names(node: Node<'_>, src: &[u8]) -> Vec<String> {
470 fn collect(node: Node<'_>, src: &[u8], out: &mut Vec<String>) {
471 match node.kind() {
472 "identifier" => {
473 let name = node_text(&node, src).trim();
474 if !name.is_empty() {
475 out.push(name.to_string());
476 }
477 return;
478 }
479 "dot_index_expression" | "bracket_index_expression" => {
480 let name = node_text(&node, src)
481 .replace([' ', '\t', '\n', '\r'], "")
482 .replace('[', ".")
483 .replace(']', "")
484 .replace(['\"', '\''], "");
485 if !name.is_empty() {
486 out.push(name);
487 }
488 return;
489 }
490 "string" | "number" | "nil" | "true" | "false" => return,
491 _ => {}
492 }
493 let mut cursor = node.walk();
494 for child in node.named_children(&mut cursor) {
495 collect(child, src, out);
496 }
497 }
498
499 let mut out = Vec::new();
500 collect(node, src, &mut out);
501 out.sort();
502 out.dedup();
503 out
504}
505
506fn insert_lua_table_field_assigns_in_events(
507 events: &mut Vec<FlowEvent>,
508 field_assigns: &[LuaTableFieldAssigns],
509) {
510 let mut index = 0usize;
511 while index < events.len() {
512 match &mut events[index] {
513 FlowEvent::Branch {
514 then_events,
515 else_events,
516 ..
517 } => {
518 insert_lua_table_field_assigns_in_events(then_events, field_assigns);
519 insert_lua_table_field_assigns_in_events(else_events, field_assigns);
520 }
521 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
522 insert_lua_table_field_assigns_in_events(body, field_assigns);
523 }
524 FlowEvent::Try {
525 body,
526 catch_events,
527 finally_events,
528 ..
529 } => {
530 insert_lua_table_field_assigns_in_events(body, field_assigns);
531 insert_lua_table_field_assigns_in_events(catch_events, field_assigns);
532 insert_lua_table_field_assigns_in_events(finally_events, field_assigns);
533 }
534 _ => {}
535 }
536
537 let inserts = match &events[index] {
538 FlowEvent::Assign { span, target, .. } => field_assigns
539 .iter()
540 .filter(|item| {
541 item.target == *target
542 && span.file == item.assign_span.file
543 && span.start <= item.assign_span.end
544 && item.assign_span.start <= span.end
545 })
546 .flat_map(|item| item.fields.clone())
547 .collect::<Vec<_>>(),
548 _ => Vec::new(),
549 };
550 if inserts.is_empty() {
551 index += 1;
552 continue;
553 }
554 let inserted = inserts.len();
555 events.splice((index + 1)..=index, inserts);
556 index += inserted + 1;
557 }
558}
559
560fn normalize_lua_dot_calls(events: &mut [FlowEvent]) {
561 for event in events {
562 match event {
563 FlowEvent::Call {
564 name,
565 receiver,
566 call_kind,
567 ..
568 } if name.contains(':') => {
569 let canonical = name.replace(':', ".");
574 *receiver = canonical.rsplit_once('.').map(|(owner, _)| owner.to_string());
575 *name = canonical;
576 *call_kind = bonsai_lang_api::CallKind::Method;
577 }
578 FlowEvent::Call {
579 name,
580 receiver,
581 receiver_types,
582 call_kind,
583 ..
584 } if name.contains('.') => {
585 *call_kind = bonsai_lang_api::CallKind::Function;
593 *receiver = None;
594 receiver_types.clear();
595 }
596 FlowEvent::Branch {
597 then_events,
598 else_events,
599 ..
600 } => {
601 normalize_lua_dot_calls(then_events);
602 normalize_lua_dot_calls(else_events);
603 }
604 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
605 normalize_lua_dot_calls(body);
606 }
607 FlowEvent::Try {
608 body,
609 catch_events,
610 finally_events,
611 ..
612 } => {
613 normalize_lua_dot_calls(body);
614 normalize_lua_dot_calls(catch_events);
615 normalize_lua_dot_calls(finally_events);
616 }
617 _ => {}
618 }
619 }
620}
621
622fn enrich_lua_factory_receiver_field_writes(decl: &mut bonsai_lang_api::Decl) {
623 let has_self_param = decl.params.iter().any(|param| param == "self");
629 if !has_self_param && !lua_returns_name(&decl.flow_events, "self") {
630 return;
631 }
632 let writes = collect_receiver_field_writes(&decl.flow_events, &decl.params, None, &["self"], &[]);
633 if writes.is_empty() {
634 return;
635 }
636 decl.receiver_field_writes.extend(writes);
637 if !decl.implicit_receiver_names.iter().any(|name| name == "self") {
638 decl.implicit_receiver_names.push("self".to_string());
639 }
640 decl.receiver_field_writes
641 .sort_by_key(|write| (write.span.start, write.target.clone()));
642 decl.receiver_field_writes.dedup_by(|a, b| {
643 a.span == b.span && a.target == b.target && a.source_param_indices == b.source_param_indices
644 });
645}
646
647fn lua_returns_name(events: &[FlowEvent], name: &str) -> bool {
648 events.iter().any(|event| match event {
649 FlowEvent::Return {
650 value_name,
651 value_flow,
652 ..
653 } => value_name.as_deref() == Some(name) || value_flow.place.as_deref() == Some(name),
654 FlowEvent::Branch {
655 then_events,
656 else_events,
657 ..
658 } => lua_returns_name(then_events, name) || lua_returns_name(else_events, name),
659 FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
660 lua_returns_name(body, name)
661 }
662 FlowEvent::Try {
663 body,
664 catch_events,
665 finally_events,
666 ..
667 } => {
668 lua_returns_name(body, name)
669 || lua_returns_name(catch_events, name)
670 || lua_returns_name(finally_events, name)
671 }
672 _ => false,
673 })
674}
675
676fn synthesize_lua_global_arg_refs(tree: &Tree, src: &[u8], file: FileId) -> Vec<Ref> {
680 collect_kinds(tree, &["identifier"])
681 .into_iter()
682 .filter(|node| node_text(node, src) == "arg")
683 .map(|node| Ref {
684 span: span_of(file, &node),
685 name: "arg".to_string(),
686 kind: RefKind::Read,
687 scope: None,
688 resolved: None,
689 })
690 .collect()
691}
692
693fn parse_imports(tree: &Tree, src: &[u8], file: FileId) -> Vec<ImportSpec> {
697 let mut imports = Vec::new();
698 for call_node in collect_kinds(tree, &["function_call"]) {
701 let Some(name_node) = call_node.child_by_field_name("name") else {
702 continue;
703 };
704 if node_text(&name_node, src) != "require" {
705 continue;
706 }
707 let Some(arg_list) = call_node.child_by_field_name("arguments") else {
708 continue;
709 };
710 let module = first_named_child_of_kind(&arg_list, "string")
711 .and_then(|string_node| first_named_child_of_kind(&string_node, "string_content"))
712 .map(|content_node| node_text(&content_node, src).to_string())
713 .unwrap_or_default();
714 if module.is_empty() {
715 continue;
716 }
717 let alias = call_node
718 .parent()
719 .filter(|parent| parent.kind() == "expression_list")
720 .and_then(|expr_list| expr_list.parent())
721 .filter(|parent| parent.kind() == "assignment_statement")
722 .and_then(|assignment| first_named_child_of_kind(&assignment, "variable_list"))
723 .and_then(|var_list| first_named_child_of_kind(&var_list, "identifier"))
724 .map(|ident| node_text(&ident, src).to_string());
725 let member = call_node
726 .parent()
727 .filter(|parent| parent.kind() == "dot_index_expression")
728 .and_then(|dot| dot.child_by_field_name("field"))
729 .map(|field| node_text(&field, src).to_string())
730 .filter(|field| !field.trim().is_empty());
731 imports.push(ImportSpec {
732 span: span_of(file, &call_node),
733 module: module.clone(),
734 alias,
735 is_wildcard: false,
736 original_name: None,
737 scope: ImportScope::Module,
738 });
739 if let Some(member) = member {
740 if let Some(local) = local_lua_assignment_target_for_call(call_node, src) {
741 imports.push(ImportSpec {
742 span: span_of(file, &call_node),
743 module,
744 alias: Some(local),
745 is_wildcard: false,
746 original_name: Some(member),
747 scope: ImportScope::Local,
748 });
749 }
750 }
751 }
752 imports
753}
754
755fn local_lua_assignment_target_for_call(call_node: tree_sitter::Node<'_>, src: &[u8]) -> Option<String> {
756 let expr_node = call_node
757 .parent()
758 .filter(|parent| parent.kind() == "dot_index_expression")
759 .unwrap_or(call_node);
760 let assignment = expr_node
761 .parent()
762 .filter(|parent| parent.kind() == "expression_list")
763 .and_then(|expr_list| expr_list.parent())
764 .filter(|parent| parent.kind() == "assignment_statement")?;
765 first_named_child_of_kind(&assignment, "variable_list")
766 .and_then(|var_list| first_named_child_of_kind(&var_list, "identifier"))
767 .map(|ident| node_text(&ident, src).to_string())
768 .filter(|text| !text.trim().is_empty())
769}
770
771fn lua_file_module_name(file: FileId, ctx: &AdapterContext<'_>) -> Option<String> {
772 let path = ctx
773 .workspace_relative_path(file)
774 .or_else(|| ctx.vfs.path(file).ok().map(|p| (*p).clone()))?;
775 path.file_stem()
776 .map(|stem| stem.to_string_lossy().into_owned())
777 .filter(|stem| !stem.is_empty())
778}
779
780fn collect_lua_module_export_table(tree: &Tree, src: &[u8]) -> Option<String> {
786 let root = tree.root_node();
787 let mut last_return: Option<tree_sitter::Node<'_>> = None;
788 let mut cursor = root.walk();
789 for child in root.named_children(&mut cursor) {
792 if child.kind() == "return_statement" {
793 last_return = Some(child);
794 }
795 }
796 let return_stmt = last_return?;
797 let exprs = match return_stmt.child_by_field_name("expression_list") {
798 Some(node) => node,
799 None => {
800 let mut return_cursor = return_stmt.walk();
805 let found = return_stmt
806 .named_children(&mut return_cursor)
807 .find(|child| child.kind() == "expression_list");
808 found?
809 }
810 };
811 let mut expr_cursor = exprs.walk();
812 let mut returned_exprs: Vec<tree_sitter::Node<'_>> = exprs.named_children(&mut expr_cursor).collect();
813 if returned_exprs.len() != 1 {
815 return None;
816 }
817 let only_expr = returned_exprs.pop()?;
818 if only_expr.kind() != "identifier" {
821 return None;
822 }
823 Some(node_text(&only_expr, src).to_string())
824}
825
826fn collect_lua_table_member_decl_spans(
831 tree: &Tree,
832 src: &[u8],
833 table_name: &str,
834 file: FileId,
835) -> std::collections::HashSet<bonsai_common::Span> {
836 let mut member_spans = std::collections::HashSet::new();
837 for fn_node in collect_kinds(tree, &["function_declaration"]) {
838 let Some(name_node) = fn_node.child_by_field_name("name") else {
839 continue;
840 };
841 if name_node.kind() != "dot_index_expression" {
844 continue;
845 }
846 let Some(table_node) = name_node.child_by_field_name("table") else {
847 continue;
848 };
849 if node_text(&table_node, src) != table_name {
850 continue;
851 }
852 member_spans.insert(span_of(file, &fn_node));
853 }
854 member_spans
855}
856
857#[cfg(test)]
858#[path = "tests.rs"]
859mod tests;