use crate::types::*;
use super::{node_text, extract_doc_comment};
pub fn extract(tree: &tree_sitter::Tree, source: &[u8]) -> (Vec<Symbol>, Vec<Import>) {
let root = tree.root_node();
let mut symbols = Vec::new();
let mut imports = Vec::new();
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
extract_node(&child, source, &mut symbols, &mut imports, None);
}
(symbols, imports)
}
fn extract_node(
node: &tree_sitter::Node,
source: &[u8],
symbols: &mut Vec<Symbol>,
imports: &mut Vec<Import>,
current_module: Option<&str>,
) {
match node.kind() {
"call" => {
extract_call(node, source, symbols, imports, current_module);
}
"def" | "defp" | "defmacro" | "defmacrop" => {
if let Some(sym) = extract_def(node, source, current_module) {
symbols.push(sym);
}
}
"defmodule" => {
extract_defmodule(node, source, symbols, imports);
}
_ => {
let mut inner = node.walk();
for child in node.children(&mut inner) {
extract_node(&child, source, symbols, imports, current_module);
}
}
}
}
fn extract_call(
node: &tree_sitter::Node,
source: &[u8],
symbols: &mut Vec<Symbol>,
imports: &mut Vec<Import>,
current_module: Option<&str>,
) {
let text = node_text(node, source);
let call_name = get_call_name(node, source);
match call_name.as_deref() {
Some("defmodule") => {
extract_defmodule(node, source, symbols, imports);
}
Some("def") | Some("defp") => {
if let Some(sym) = extract_def(node, source, current_module) {
symbols.push(sym);
}
}
Some("defmacro") | Some("defmacrop") => {
if let Some(sym) = extract_macro_def(node, source, current_module) {
symbols.push(sym);
}
}
Some("defstruct") => {
if let Some(mod_name) = current_module {
symbols.push(Symbol {
name: mod_name.to_string(),
kind: SymbolKind::Struct,
span: Span::from_node(node),
signature: first_line(text).to_string(),
doc_comment: extract_doc_comment(node, source),
parent: current_module.map(|s| s.to_string()),
children: Vec::new(),
});
}
}
Some("use") | Some("import") | Some("alias") | Some("require") => {
let path = extract_module_ref(node, source, text);
imports.push(Import {
path: path.unwrap_or_else(|| text.trim().to_string()),
alias: None,
span: Span::from_node(node),
});
}
_ => {
let mut inner = node.walk();
for child in node.children(&mut inner) {
if child.kind() == "call" || child.kind() == "do_block" || child.kind() == "arguments" {
extract_node(&child, source, symbols, imports, current_module);
}
}
}
}
}
fn extract_defmodule(
node: &tree_sitter::Node,
source: &[u8],
symbols: &mut Vec<Symbol>,
imports: &mut Vec<Import>,
) {
let text = node_text(node, source);
let module_name = extract_module_name(node, source, text);
let name = module_name.clone().unwrap_or_else(|| "?".to_string());
let mut children = Vec::new();
let mut module_imports = Vec::new();
walk_do_block(node, source, &mut children, &mut module_imports, Some(&name));
imports.extend(module_imports);
symbols.push(Symbol {
name,
kind: SymbolKind::Module,
span: Span::from_node(node),
signature: first_line(text).to_string(),
doc_comment: extract_doc_comment(node, source),
parent: None,
children,
});
}
fn walk_do_block(
node: &tree_sitter::Node,
source: &[u8],
symbols: &mut Vec<Symbol>,
imports: &mut Vec<Import>,
current_module: Option<&str>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"do_block" | "do" | "body" | "arguments" | "stab_clause" => {
walk_do_block(&child, source, symbols, imports, current_module);
}
"call" => {
extract_call(&child, source, symbols, imports, current_module);
}
"def" | "defp" | "defmacro" | "defmacrop" => {
if let Some(sym) = extract_def(&child, source, current_module) {
symbols.push(sym);
}
}
_ => {
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if grandchild.kind() == "call"
|| grandchild.kind() == "do_block"
|| grandchild.kind() == "body"
{
walk_do_block(&grandchild, source, symbols, imports, current_module);
}
}
}
}
}
}
fn extract_def(
node: &tree_sitter::Node,
source: &[u8],
parent: Option<&str>,
) -> Option<Symbol> {
let text = node_text(node, source);
let name = extract_def_name(node, source, text)?;
Some(Symbol {
name,
kind: SymbolKind::Function,
span: Span::from_node(node),
signature: first_line(text).to_string(),
doc_comment: extract_doc_comment(node, source),
parent: parent.map(|s| s.to_string()),
children: Vec::new(),
})
}
fn extract_macro_def(
node: &tree_sitter::Node,
source: &[u8],
parent: Option<&str>,
) -> Option<Symbol> {
let text = node_text(node, source);
let name = extract_def_name(node, source, text)?;
Some(Symbol {
name,
kind: SymbolKind::Function,
span: Span::from_node(node),
signature: first_line(text).to_string(),
doc_comment: extract_doc_comment(node, source),
parent: parent.map(|s| s.to_string()),
children: Vec::new(),
})
}
fn get_call_name(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
if let Some(target) = node.child_by_field_name("target") {
let t = node_text(&target, source).trim().to_string();
if !t.is_empty() {
return Some(t);
}
}
if let Some(first) = node.child(0) {
let kind = first.kind();
if kind == "identifier" || kind == "atom" || kind == "special_identifier" {
let t = node_text(&first, source).trim().to_string();
if !t.is_empty() {
return Some(t);
}
}
}
let text = node_text(node, source).trim().to_string();
let first_word = text.split_whitespace().next()?;
Some(first_word.to_string())
}
fn extract_def_name(
node: &tree_sitter::Node,
source: &[u8],
text: &str,
) -> Option<String> {
if let Some(args) = node.child_by_field_name("arguments") {
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
match child.kind() {
"call" => {
if let Some(target) = child.child_by_field_name("target") {
let t = node_text(&target, source).trim().to_string();
if !t.is_empty() {
return Some(t);
}
}
if let Some(first) = child.child(0) {
let t = node_text(&first, source).trim().to_string();
if !t.is_empty() && t != "(" {
return Some(t);
}
}
}
"identifier" => {
let t = node_text(&child, source).trim().to_string();
if !t.is_empty() {
return Some(t);
}
}
_ => {}
}
}
}
let mut cursor = node.walk();
let mut found_keyword = false;
for child in node.children(&mut cursor) {
let t = node_text(&child, source).trim().to_string();
if t == "def" || t == "defp" || t == "defmacro" || t == "defmacrop" {
found_keyword = true;
continue;
}
if found_keyword {
if child.kind() == "call" || child.kind() == "identifier" {
if let Some(target) = child.child_by_field_name("target") {
return Some(node_text(&target, source).trim().to_string());
}
let name_text = node_text(&child, source);
let name = name_text.split('(').next().unwrap_or(name_text).trim();
if !name.is_empty() {
return Some(name.to_string());
}
}
}
}
extract_def_name_from_text(text)
}
fn extract_def_name_from_text(text: &str) -> Option<String> {
let text = text.trim();
let rest = text.strip_prefix("defmacrop ")
.or_else(|| text.strip_prefix("defmacro "))
.or_else(|| text.strip_prefix("defp "))
.or_else(|| text.strip_prefix("def "))?;
let end = rest.find(|c: char| c == '(' || c == ' ' || c == ',' || c == '\n')?;
let name = rest[..end].trim();
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
fn extract_module_name(
node: &tree_sitter::Node,
source: &[u8],
text: &str,
) -> Option<String> {
if let Some(args) = node.child_by_field_name("arguments") {
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
let kind = child.kind();
if kind == "alias" || kind == "identifier" || kind == "dot" {
let t = node_text(&child, source).trim().to_string();
if !t.is_empty() && t != "do" {
return Some(t);
}
}
}
}
let mut cursor = node.walk();
let mut found_keyword = false;
for child in node.children(&mut cursor) {
let t = node_text(&child, source).trim().to_string();
if t == "defmodule" {
found_keyword = true;
continue;
}
if found_keyword && child.kind() != "do_block" && !t.is_empty() && t != "do" {
let name = t.split_whitespace().next().unwrap_or(&t);
return Some(name.to_string());
}
}
let rest = text.strip_prefix("defmodule ")?;
let end = rest.find(|c: char| c == ' ' || c == '\n')?;
Some(rest[..end].trim().to_string())
}
fn extract_module_ref(
node: &tree_sitter::Node,
source: &[u8],
text: &str,
) -> Option<String> {
if let Some(args) = node.child_by_field_name("arguments") {
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
let kind = child.kind();
if kind == "alias" || kind == "identifier" || kind == "dot" || kind == "atom" {
let t = node_text(&child, source).trim().to_string();
if !t.is_empty() {
return Some(t);
}
}
}
}
let text = text.trim();
let rest = text.strip_prefix("use ")
.or_else(|| text.strip_prefix("import "))
.or_else(|| text.strip_prefix("alias "))
.or_else(|| text.strip_prefix("require "))?;
let end = rest.find(|c: char| c == ',' || c == '\n').unwrap_or(rest.len());
Some(rest[..end].trim().to_string())
}
fn first_line(text: &str) -> &str {
text.lines().next().unwrap_or(text)
}
#[cfg(test)]
mod tests {
use crate::{parse, Language, SymbolKind};
#[test]
fn test_elixir_extract() {
let source = r#"defmodule MyApp.Calculator do
use GenServer
import Enum
alias MyApp.Helper
def add(a, b) do
a + b
end
defp validate(x) do
x > 0
end
defmacro debug(expr) do
quote do
IO.inspect(unquote(expr))
end
end
end
defmodule MyApp.Runner do
def run do
:ok
end
end
"#;
let parsed = parse(source, Language::Elixir);
if let Some(parsed) = parsed {
let top_names: Vec<&str> = parsed.symbols.iter().map(|s| s.name.as_str()).collect();
let top_kinds: Vec<(&str, SymbolKind)> = parsed
.symbols
.iter()
.map(|s| (s.name.as_str(), s.kind))
.collect();
let import_paths: Vec<&str> = parsed.imports.iter().map(|i| i.path.as_str()).collect();
assert!(
top_kinds.iter().any(|(n, k)| n.contains("Calculator") && *k == SymbolKind::Module),
"Should find Calculator module, got: {:?}",
top_kinds
);
assert!(
top_kinds.iter().any(|(n, k)| n.contains("Runner") && *k == SymbolKind::Module),
"Should find Runner module, got: {:?}",
top_kinds
);
assert!(
import_paths.iter().any(|p| p.contains("GenServer")),
"Should find GenServer use, got: {:?}",
import_paths
);
assert!(
import_paths.iter().any(|p| p.contains("Enum")),
"Should find Enum import, got: {:?}",
import_paths
);
let calc = parsed.symbols.iter().find(|s| s.name.contains("Calculator"));
if let Some(calc) = calc {
let child_names: Vec<&str> = calc.children.iter().map(|s| s.name.as_str()).collect();
let child_kinds: Vec<(&str, SymbolKind)> = calc
.children
.iter()
.map(|s| (s.name.as_str(), s.kind))
.collect();
assert!(
child_kinds.iter().any(|(n, k)| *n == "add" && *k == SymbolKind::Function),
"Calculator should have 'add' function, got children: {:?}",
child_kinds
);
assert!(
child_kinds.iter().any(|(n, k)| *n == "validate" && *k == SymbolKind::Function),
"Calculator should have 'validate' function, got children: {:?}",
child_kinds
);
assert!(
child_kinds.iter().any(|(n, k)| *n == "debug" && *k == SymbolKind::Function),
"Calculator should have 'debug' macro, got children: {:?}",
child_kinds
);
eprintln!("Calculator children: {:?}", child_names);
}
eprintln!("Elixir top-level symbols: {:?}", top_names);
eprintln!("Elixir imports: {:?}", import_paths);
} else {
eprintln!("Elixir parser not available or parse failed — skipping assertions");
}
}
}