use crate::extraction::tree_sitter_helpers::{get_child_by_field, get_node_text};
use crate::extraction::tree_sitter_types::{
ExtractorContext, LanguageExtractor, NodeExtra, UnresolvedReferenceInput, VariableInfo,
};
use crate::types::{NodeKind, ReferenceKind};
use crate::web_tree_sitter::SyntaxNode;
pub struct LuaExtractor;
pub const LUA_EXTRACTOR: LuaExtractor = LuaExtractor;
pub fn lua_extractor() -> &'static LuaExtractor {
&LUA_EXTRACTOR
}
fn find_descendant(node: &SyntaxNode, node_type: &str) -> Option<SyntaxNode> {
let mut queue = node.named_children();
while let Some(current) = queue.first().cloned() {
queue.remove(0);
if current.type_name() == node_type {
return Some(current);
}
queue.extend(current.named_children());
}
None
}
fn require_module(call_node: &SyntaxNode, source: &str) -> Option<String> {
let name = get_child_by_field(call_node, "name")?;
if name.type_name() != "identifier" || get_node_text(name, source) != "require" {
return None;
}
let args = get_child_by_field(call_node, "arguments")?;
if let Some(content) = find_descendant(args, "string_content") {
let module_name = get_node_text(&content, source).trim().to_string();
return (!module_name.is_empty()).then_some(module_name);
}
if let Some(str_node) = find_descendant(args, "string") {
let module_name = get_node_text(&str_node, source)
.trim()
.trim_start_matches("[[")
.trim_end_matches("]]")
.trim_matches(['"', '\''])
.to_string();
if !module_name.is_empty() {
return Some(module_name);
}
}
let idx = find_descendant(args, "dot_index_expression")
.or_else(|| find_descendant(args, "method_index_expression"))?;
let field = get_child_by_field(&idx, "field").or_else(|| get_child_by_field(&idx, "method"))?;
let module_name = get_node_text(field, source).trim().to_string();
(!module_name.is_empty()).then_some(module_name)
}
fn emit_require_import(call_node: &SyntaxNode, ctx: &mut dyn ExtractorContext) {
let Some(module_name) = require_module(call_node, ctx.source()) else {
return;
};
let extra = NodeExtra {
signature: Some(
get_node_text(call_node, ctx.source())
.trim()
.chars()
.take(100)
.collect(),
),
..Default::default()
};
let import = ctx.create_node(NodeKind::Import, &module_name, call_node, extra);
if import.is_some()
&& let Some(parent_id) = ctx.node_stack().last()
{
ctx.add_unresolved_reference(UnresolvedReferenceInput {
from_node_id: parent_id.clone(),
reference_name: module_name,
reference_kind: ReferenceKind::Imports,
file_path: None,
line: Some(call_node.start_position().row + 1),
column: Some(call_node.start_position().column),
});
}
}
fn variable_list_names(variable_list: &SyntaxNode, source: &str) -> Vec<(String, SyntaxNode)> {
variable_list
.named_children()
.into_iter()
.filter(|child| matches!(child.type_name().as_str(), "variable" | "identifier"))
.filter_map(|variable| {
let name = get_node_text(&variable, source).trim().to_string();
(!name.is_empty()).then_some((name, variable))
})
.collect()
}
fn expression_values(expression_list: Option<SyntaxNode>) -> Vec<SyntaxNode> {
expression_list
.map(|list| list.named_children())
.unwrap_or_default()
}
fn assignment_parts(node: &SyntaxNode) -> (Option<SyntaxNode>, Option<SyntaxNode>) {
let assignment = if node.type_name() == "assignment_statement" {
Some(node.clone())
} else {
node.named_children()
.into_iter()
.find(|child| child.type_name() == "assignment_statement")
};
let Some(assignment) = assignment else {
let variables = node
.named_children()
.into_iter()
.find(|child| child.type_name() == "variable_list");
return (variables, None);
};
let variables = assignment
.named_children()
.into_iter()
.find(|child| child.type_name() == "variable_list");
let values = assignment
.named_children()
.into_iter()
.find(|child| child.type_name() == "expression_list");
(variables, values)
}
impl LanguageExtractor for LuaExtractor {
fn function_types(&self) -> &'static [&'static str] {
&["function_declaration"]
}
fn class_types(&self) -> &'static [&'static str] {
&[]
}
fn method_types(&self) -> &'static [&'static str] {
&[]
}
fn interface_types(&self) -> &'static [&'static str] {
&[]
}
fn struct_types(&self) -> &'static [&'static str] {
&[]
}
fn enum_types(&self) -> &'static [&'static str] {
&[]
}
fn type_alias_types(&self) -> &'static [&'static str] {
&[]
}
fn import_types(&self) -> &'static [&'static str] {
&[]
}
fn call_types(&self) -> &'static [&'static str] {
&["function_call"]
}
fn variable_types(&self) -> &'static [&'static str] {
&["variable_declaration"]
}
fn name_field(&self) -> &'static str {
"name"
}
fn body_field(&self) -> &'static str {
"body"
}
fn params_field(&self) -> &'static str {
"parameters"
}
fn get_signature(&self, node: &SyntaxNode, source: &str) -> Option<String> {
get_child_by_field(node, "parameters").map(|params| get_node_text(params, source))
}
fn get_receiver_type(&self, node: &SyntaxNode, source: &str) -> Option<String> {
let name = get_child_by_field(node, "name")?;
if name.type_name() == "dot_index_expression"
|| name.type_name() == "method_index_expression"
{
return get_child_by_field(name, "table").map(|table| get_node_text(table, source));
}
None
}
fn visit_node(&self, node: &SyntaxNode, ctx: &mut dyn ExtractorContext) -> bool {
if node.type_name() == "function_call" {
if require_module(node, ctx.source()).is_some() {
emit_require_import(node, ctx);
return true;
}
return false;
}
false
}
fn extract_variables(&self, node: &SyntaxNode, source: &str) -> Vec<VariableInfo> {
let (variables, values) = assignment_parts(node);
let Some(variables) = variables else {
return Vec::new();
};
let values = expression_values(values);
variable_list_names(&variables, source)
.into_iter()
.enumerate()
.map(|(idx, (name, position_node))| VariableInfo {
name,
kind: NodeKind::Variable,
signature: None,
is_exported: Some(false),
delegate_to_function: None,
position_node: Some(position_node),
visit_value: values.get(idx).cloned(),
object_literal_functions: None,
})
.collect()
}
}