use super::languages::SupportedLanguage;
use crate::db::SymbolEdge;
use chrono::Utc;
use tree_sitter::Tree;
use uuid::Uuid;
pub fn extract_imports(
tree: &Tree,
content: &str,
language: SupportedLanguage,
source_file: &str,
) -> Vec<SymbolEdge> {
match language {
SupportedLanguage::Rust => extract_rust_imports(tree, content, source_file),
SupportedLanguage::Python => extract_python_imports(tree, content, source_file),
_ => Vec::new(),
}
}
fn extract_rust_imports(tree: &Tree, content: &str, source_file: &str) -> Vec<SymbolEdge> {
let mut edges = Vec::new();
let root = tree.root_node();
let timestamp = Utc::now().to_rfc3339();
for i in 0..root.child_count() {
let node = match root.child(i) {
Some(n) => n,
None => continue,
};
if node.kind() != "use_declaration" {
continue;
}
let mut targets: Vec<String> = Vec::new();
collect_rust_use_targets(&node, content, &mut Vec::new(), &mut targets);
for target in targets {
edges.push(SymbolEdge {
id: Uuid::new_v4().to_string(),
source_file: source_file.to_string(),
target_symbol: target,
edge_type: "imports".to_string(),
target_file: None,
indexed_at: timestamp.clone(),
});
}
}
edges
}
fn collect_rust_use_targets(
node: &tree_sitter::Node,
content: &str,
prefix_parts: &mut Vec<String>,
targets: &mut Vec<String>,
) {
let kind = node.kind();
match kind {
"use_declaration" => {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
let ck = child.kind();
if ck != "use"
&& ck != ";"
&& ck != "pub"
&& ck != "visibility_modifier"
&& ck != "line_comment"
&& ck != "block_comment"
{
collect_rust_use_targets(&child, content, prefix_parts, targets);
}
}
}
}
"scoped_identifier" => {
let mut parts: Vec<String> = Vec::new();
flatten_scoped_identifier(node, content, &mut parts);
let combined = if prefix_parts.is_empty() {
parts.join("::")
} else {
format!("{}::{}", prefix_parts.join("::"), parts.join("::"))
};
if !combined.is_empty() {
targets.push(combined);
}
}
"scoped_use_list" => {
let mut new_prefix = prefix_parts.clone();
let child_count = node.child_count();
let mut i = 0;
while i < child_count {
if let Some(child) = node.child(i) {
let ck = child.kind();
if ck != "::" && ck != "{" && ck != "}" {
if ck == "use_list" {
collect_rust_use_targets(&child, content, &mut new_prefix, targets);
} else {
let mut parts = Vec::new();
flatten_scoped_identifier(&child, content, &mut parts);
if !parts.is_empty() {
let seg = parts.join("::");
if !new_prefix.is_empty() {
new_prefix.push(seg);
} else {
new_prefix = parts;
}
}
}
}
}
i += 1;
}
}
"use_list" => {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
let ck = child.kind();
if ck != "{" && ck != "}" && ck != "," {
collect_rust_use_targets(&child, content, prefix_parts, targets);
}
}
}
}
"use_as_clause" => {
if let Some(orig) = node.child(0) {
collect_rust_use_targets(&orig, content, prefix_parts, targets);
}
}
"use_wildcard" => {
let raw = node.utf8_text(content.as_bytes()).unwrap_or("").to_string();
let path = if raw.contains("::") || prefix_parts.is_empty() {
raw
} else {
format!("{}::*", prefix_parts.join("::"))
};
if !path.is_empty() {
targets.push(path);
}
}
"identifier" | "self" | "super" | "crate" => {
let text = node.utf8_text(content.as_bytes()).unwrap_or("").to_string();
if !text.is_empty() {
let path = if text == "self" && !prefix_parts.is_empty() {
prefix_parts.join("::")
} else if prefix_parts.is_empty() {
text
} else {
format!("{}::{}", prefix_parts.join("::"), text)
};
targets.push(path);
}
}
_ => {
}
}
}
fn flatten_scoped_identifier(node: &tree_sitter::Node, content: &str, parts: &mut Vec<String>) {
match node.kind() {
"scoped_identifier" => {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() != "::" {
flatten_scoped_identifier(&child, content, parts);
}
}
}
}
"identifier" | "self" | "super" | "crate" => {
let text = node.utf8_text(content.as_bytes()).unwrap_or("").to_string();
if !text.is_empty() {
parts.push(text);
}
}
_ => {
let text = node.utf8_text(content.as_bytes()).unwrap_or("").to_string();
if !text.is_empty() {
parts.push(text);
}
}
}
}
fn extract_python_imports(tree: &Tree, content: &str, source_file: &str) -> Vec<SymbolEdge> {
let mut edges = Vec::new();
let root = tree.root_node();
let timestamp = Utc::now().to_rfc3339();
let content_bytes = content.as_bytes();
for i in 0..root.child_count() {
let node = match root.child(i) {
Some(n) => n,
None => continue,
};
match node.kind() {
"import_statement" => {
for j in 0..node.child_count() {
if let Some(child) = node.child(j) {
let ck = child.kind();
if ck == "dotted_name" {
let text = child.utf8_text(content_bytes).unwrap_or("").to_string();
if !text.is_empty() {
edges.push(make_edge(source_file, &text, ×tamp));
}
} else if ck == "aliased_import" {
if let Some(orig) = child.child(0) {
let text = orig.utf8_text(content_bytes).unwrap_or("").to_string();
if !text.is_empty() {
edges.push(make_edge(source_file, &text, ×tamp));
}
}
}
}
}
}
"import_from_statement" => {
let module_prefix = extract_python_from_module(&node, content_bytes);
let mut past_import_kw = false;
for j in 0..node.child_count() {
if let Some(child) = node.child(j) {
let ck = child.kind();
if ck == "import" {
past_import_kw = true;
continue;
}
if !past_import_kw {
continue;
}
collect_python_import_names(
&child,
content_bytes,
&module_prefix,
source_file,
×tamp,
&mut edges,
);
}
}
}
_ => {}
}
}
edges
}
fn collect_python_import_names(
node: &tree_sitter::Node,
content_bytes: &[u8],
module_prefix: &str,
source_file: &str,
timestamp: &str,
edges: &mut Vec<SymbolEdge>,
) {
let ck = node.kind();
if ck == "wildcard_import" {
let target = join_python_module(module_prefix, "*");
edges.push(make_edge(source_file, &target, timestamp));
} else if ck == "dotted_name" || ck == "identifier" {
let name = node.utf8_text(content_bytes).unwrap_or("").to_string();
if !name.is_empty() {
let target = join_python_module(module_prefix, &name);
edges.push(make_edge(source_file, &target, timestamp));
}
} else if ck == "aliased_import" {
if let Some(orig) = node.child(0) {
let name = orig.utf8_text(content_bytes).unwrap_or("").to_string();
if !name.is_empty() {
let target = join_python_module(module_prefix, &name);
edges.push(make_edge(source_file, &target, timestamp));
}
}
} else if ck != "," && ck != "(" && ck != ")" {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
collect_python_import_names(
&child,
content_bytes,
module_prefix,
source_file,
timestamp,
edges,
);
}
}
}
}
fn extract_python_from_module(node: &tree_sitter::Node, content_bytes: &[u8]) -> String {
let mut dots = String::new();
let mut module_name = String::new();
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
match child.kind() {
"import" => break,
"from" => {}
"relative_import" => {
for j in 0..child.child_count() {
if let Some(rc) = child.child(j) {
match rc.kind() {
"import_prefix" => {
dots = rc.utf8_text(content_bytes).unwrap_or("").to_string();
}
"dotted_name" => {
module_name =
rc.utf8_text(content_bytes).unwrap_or("").to_string();
}
_ => {}
}
}
}
}
"dotted_name" => {
module_name = child.utf8_text(content_bytes).unwrap_or("").to_string();
}
_ => {}
}
}
}
if dots.is_empty() {
module_name
} else if module_name.is_empty() {
dots
} else {
format!("{}{}", dots, module_name)
}
}
fn join_python_module(prefix: &str, name: &str) -> String {
if prefix.is_empty() {
name.to_string()
} else if prefix.ends_with('.') {
format!("{}{}", prefix, name)
} else {
format!("{}.{}", prefix, name)
}
}
fn make_edge(source_file: &str, target_symbol: &str, timestamp: &str) -> SymbolEdge {
SymbolEdge {
id: Uuid::new_v4().to_string(),
source_file: source_file.to_string(),
target_symbol: target_symbol.to_string(),
edge_type: "imports".to_string(),
target_file: None,
indexed_at: timestamp.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Parser;
fn parse_rust(src: &str) -> Tree {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_rust::language())
.expect("failed to set Rust language");
parser.parse(src, None).expect("failed to parse Rust")
}
fn parse_python(src: &str) -> Tree {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_python::language())
.expect("failed to set Python language");
parser.parse(src, None).expect("failed to parse Python")
}
fn symbols(edges: &[SymbolEdge]) -> Vec<&str> {
let mut v: Vec<&str> = edges.iter().map(|e| e.target_symbol.as_str()).collect();
v.sort_unstable();
v
}
#[test]
fn test_rust_simple_use() {
let src = "use std::fs::File;\nfn main() {}";
let tree = parse_rust(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Rust, "src/main.rs");
assert_eq!(symbols(&edges), vec!["std::fs::File"]);
assert!(edges.iter().all(|e| e.edge_type == "imports"));
assert!(edges.iter().all(|e| e.target_file.is_none()));
assert!(edges.iter().all(|e| e.source_file == "src/main.rs"));
}
#[test]
fn test_rust_grouped_use() {
let src = "use crate::db::{CodeChunk, Database};\nfn main() {}";
let tree = parse_rust(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Rust, "src/lib.rs");
assert_eq!(
symbols(&edges),
vec!["crate::db::CodeChunk", "crate::db::Database"]
);
}
#[test]
fn test_rust_self_in_grouped_use() {
let src = "use crate::db::{self, Database};\nfn main() {}";
let tree = parse_rust(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Rust, "src/lib.rs");
assert_eq!(symbols(&edges), vec!["crate::db", "crate::db::Database"]);
}
#[test]
fn test_rust_glob_use() {
let src = "use std::io::*;\nfn main() {}";
let tree = parse_rust(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Rust, "src/main.rs");
assert_eq!(symbols(&edges), vec!["std::io::*"]);
}
#[test]
fn test_rust_use_as_alias() {
let src = "use std::collections::HashMap as Map;\nfn main() {}";
let tree = parse_rust(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Rust, "src/main.rs");
assert_eq!(symbols(&edges), vec!["std::collections::HashMap"]);
}
#[test]
fn test_rust_multiple_uses() {
let src = r#"
use anyhow::Result;
use std::path::Path;
use super::languages::SupportedLanguage;
fn main() {}
"#;
let tree = parse_rust(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Rust, "src/indexer/mod.rs");
let syms = symbols(&edges);
assert!(syms.contains(&"anyhow::Result"), "{:?}", syms);
assert!(syms.contains(&"std::path::Path"), "{:?}", syms);
assert!(
syms.contains(&"super::languages::SupportedLanguage"),
"{:?}",
syms
);
}
#[test]
fn test_rust_empty_file() {
let src = "";
let tree = parse_rust(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Rust, "src/empty.rs");
assert!(edges.is_empty());
}
#[test]
fn test_rust_no_imports() {
let src = "fn main() { println!(\"hello\"); }";
let tree = parse_rust(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Rust, "src/main.rs");
assert!(edges.is_empty());
}
#[test]
fn test_python_simple_import() {
let src = "import os\n";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/main.py");
assert_eq!(symbols(&edges), vec!["os"]);
}
#[test]
fn test_python_dotted_import() {
let src = "import os.path\n";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/main.py");
assert_eq!(symbols(&edges), vec!["os.path"]);
}
#[test]
fn test_python_from_import_single() {
let src = "from os.path import join\n";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/main.py");
assert_eq!(symbols(&edges), vec!["os.path.join"]);
}
#[test]
fn test_python_from_import_multiple() {
let src = "from os.path import join, exists\n";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/main.py");
assert_eq!(symbols(&edges), vec!["os.path.exists", "os.path.join"]);
}
#[test]
fn test_python_from_import_parenthesized() {
let src = "from os.path import (join, exists)\n";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/main.py");
assert_eq!(symbols(&edges), vec!["os.path.exists", "os.path.join"]);
}
#[test]
fn test_python_from_import_aliased() {
let src = "from os.path import join as j\n";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/main.py");
assert_eq!(symbols(&edges), vec!["os.path.join"]);
}
#[test]
fn test_python_relative_import_single_dot() {
let src = "from . import utils\n";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/sub/mod.py");
assert_eq!(symbols(&edges), vec![".utils"]);
}
#[test]
fn test_python_relative_import_double_dot() {
let src = "from ..models import Foo\n";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/sub/mod.py");
assert_eq!(symbols(&edges), vec!["..models.Foo"]);
}
#[test]
fn test_python_empty_file() {
let src = "";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/empty.py");
assert!(edges.is_empty());
}
#[test]
fn test_python_no_imports() {
let src = "def foo():\n pass\n";
let tree = parse_python(src);
let edges = extract_imports(&tree, src, SupportedLanguage::Python, "app/main.py");
assert!(edges.is_empty());
}
#[test]
fn test_unsupported_language_returns_empty() {
let src = "package main\nimport \"fmt\"\n";
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_rust::language())
.expect("set language");
let tree = parser.parse(src, None).expect("parse");
let edges = extract_imports(&tree, src, SupportedLanguage::Go, "main.go");
assert!(edges.is_empty(), "Go should return empty edges");
}
#[test]
fn test_unsupported_languages_return_empty() {
let src = "fn main() {}";
let tree = parse_rust(src);
for lang in [
SupportedLanguage::JavaScript,
SupportedLanguage::TypeScript,
SupportedLanguage::Go,
SupportedLanguage::C,
SupportedLanguage::Cpp,
SupportedLanguage::Java,
SupportedLanguage::CSharp,
] {
let edges = extract_imports(&tree, src, lang, "file");
assert!(
edges.is_empty(),
"{} should return empty edges",
lang.name()
);
}
}
}