use std::cell::RefCell;
use std::path::Path;
use tree_sitter::{Node, Parser};
use tree_sitter_language::LanguageFn;
use domain::error::CodeGraphError;
use domain::model::{Edge, EdgeKind, Language, Location, SymbolKind, SymbolNode, Visibility};
use crate::{Export, ImportName, LanguageParser, ParseResult, RawImport};
thread_local! {
static RUST_PARSER: RefCell<Parser> = RefCell::new(Parser::new());
}
pub struct RustParser {
lang: LanguageFn,
}
impl RustParser {
pub fn new() -> Self {
Self {
lang: tree_sitter_rust::LANGUAGE,
}
}
}
impl Default for RustParser {
fn default() -> Self {
Self::new()
}
}
impl LanguageParser for RustParser {
fn language(&self) -> Language {
Language::Rust
}
fn file_extensions(&self) -> &[&str] {
&["rs"]
}
fn parse(&self, source: &[u8], path: &Path) -> domain::error::Result<ParseResult> {
let lang: tree_sitter::Language = self.lang.into();
RUST_PARSER.with(|parser_cell| {
let mut parser = parser_cell.borrow_mut();
parser
.set_language(&lang)
.map_err(|e| CodeGraphError::Parse {
file: path.to_path_buf(),
message: format!("failed to set language: {e}"),
})?;
let tree = parser
.parse(source, None)
.ok_or_else(|| CodeGraphError::Parse {
file: path.to_path_buf(),
message: "tree-sitter parse returned None".into(),
})?;
extract_all(source, path, &tree)
})
}
}
fn extract_all(
source: &[u8],
path: &Path,
tree: &tree_sitter::Tree,
) -> domain::error::Result<ParseResult> {
let mut symbols = Vec::new();
let mut edges = Vec::new();
let mut imports = Vec::new();
let mut exports = Vec::new();
let file_path = path.to_string_lossy().to_string();
let root = tree.root_node();
let mut cursor = root.walk();
let mut pending_attrs: Vec<String> = Vec::new();
for child in root.children(&mut cursor) {
if !child.is_named() {
continue;
}
match child.kind() {
"attribute_item" => {
if let Ok(text) = child.utf8_text(source) {
pending_attrs.push(text.to_string());
}
continue; }
"function_item" => {
if let Some(sym) = extract_function(source, &file_path, child, None, &pending_attrs)
{
edges.push(contains_edge(&file_path, &sym.qualified_name));
symbols.push(sym);
}
}
"struct_item" => {
if let Some(sym) = extract_struct(source, &file_path, child) {
edges.push(contains_edge(&file_path, &sym.qualified_name));
symbols.push(sym);
}
}
"enum_item" => {
if let Some(sym) = extract_enum(source, &file_path, child) {
edges.push(contains_edge(&file_path, &sym.qualified_name));
symbols.push(sym);
}
}
"trait_item" => {
if let Some(sym) = extract_trait(source, &file_path, child) {
edges.push(contains_edge(&file_path, &sym.qualified_name));
symbols.push(sym);
}
}
"type_item" => {
if let Some(sym) = extract_type_alias(source, &file_path, child) {
edges.push(contains_edge(&file_path, &sym.qualified_name));
symbols.push(sym);
}
}
"const_item" => {
if let Some(sym) = extract_const(source, &file_path, child) {
edges.push(contains_edge(&file_path, &sym.qualified_name));
symbols.push(sym);
}
}
"static_item" => {
if let Some(sym) = extract_static(source, &file_path, child) {
edges.push(contains_edge(&file_path, &sym.qualified_name));
symbols.push(sym);
}
}
"macro_definition" => {
if let Some(sym) = extract_macro(source, &file_path, child) {
edges.push(contains_edge(&file_path, &sym.qualified_name));
symbols.push(sym);
}
}
"impl_item" => {
extract_impl(source, &file_path, child, &mut symbols, &mut edges);
}
"use_declaration" => {
extract_use_declaration(source, child, &mut imports, &mut exports);
}
"mod_item" => {
if child.child_by_field_name("body").is_none() {
if let Some(name) = node_name(source, child) {
let line = child.start_position().row + 1;
imports.push(RawImport {
specifier: format!("mod::{name}"),
names: Vec::new(),
is_type_only: false,
is_side_effect: false,
is_namespace: false,
line,
});
}
}
}
_ => {}
}
pending_attrs.clear();
}
Ok(ParseResult {
symbols,
edges,
imports,
exports,
})
}
fn extract_function(
source: &[u8],
file_path: &str,
node: Node,
owner_name: Option<&str>,
preceding_attrs: &[String],
) -> Option<SymbolNode> {
let name = node_name(source, node)?;
let qualified_name = match owner_name {
Some(owner) => format!("{file_path}::{owner}.{name}"),
None => format!("{file_path}::{name}"),
};
let visibility = extract_visibility(source, node);
let is_exported = visibility == Visibility::Public;
let is_async = is_async_fn(source, node);
let is_test = attrs_contain_test(preceding_attrs);
let signature = build_rust_signature(source, node);
let kind = if owner_name.is_some() {
SymbolKind::Method
} else {
SymbolKind::Function
};
Some(SymbolNode {
name,
qualified_name,
kind,
location: node_location(file_path, node),
visibility,
is_exported,
is_async,
is_test,
decorators: Vec::new(),
signature,
})
}
fn extract_struct(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
let name = node_name(source, node)?;
let qualified_name = format!("{file_path}::{name}");
let visibility = extract_visibility(source, node);
let is_exported = visibility == Visibility::Public;
Some(SymbolNode {
name,
qualified_name,
kind: SymbolKind::Struct,
location: node_location(file_path, node),
visibility,
is_exported,
is_async: false,
is_test: false,
decorators: Vec::new(),
signature: None,
})
}
fn extract_enum(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
let name = node_name(source, node)?;
let qualified_name = format!("{file_path}::{name}");
let visibility = extract_visibility(source, node);
let is_exported = visibility == Visibility::Public;
Some(SymbolNode {
name,
qualified_name,
kind: SymbolKind::Enum,
location: node_location(file_path, node),
visibility,
is_exported,
is_async: false,
is_test: false,
decorators: Vec::new(),
signature: None,
})
}
fn extract_trait(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
let name = node_name(source, node)?;
let qualified_name = format!("{file_path}::{name}");
let visibility = extract_visibility(source, node);
let is_exported = visibility == Visibility::Public;
Some(SymbolNode {
name,
qualified_name,
kind: SymbolKind::Trait,
location: node_location(file_path, node),
visibility,
is_exported,
is_async: false,
is_test: false,
decorators: Vec::new(),
signature: None,
})
}
fn extract_type_alias(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
let name = node_name(source, node)?;
let qualified_name = format!("{file_path}::{name}");
let visibility = extract_visibility(source, node);
let is_exported = visibility == Visibility::Public;
Some(SymbolNode {
name,
qualified_name,
kind: SymbolKind::TypeAlias,
location: node_location(file_path, node),
visibility,
is_exported,
is_async: false,
is_test: false,
decorators: Vec::new(),
signature: None,
})
}
fn extract_const(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
let name = node_name(source, node)?;
let qualified_name = format!("{file_path}::{name}");
let visibility = extract_visibility(source, node);
let is_exported = visibility == Visibility::Public;
Some(SymbolNode {
name,
qualified_name,
kind: SymbolKind::Const,
location: node_location(file_path, node),
visibility,
is_exported,
is_async: false,
is_test: false,
decorators: Vec::new(),
signature: None,
})
}
fn extract_static(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
let name = node_name(source, node)?;
let qualified_name = format!("{file_path}::{name}");
let visibility = extract_visibility(source, node);
let is_exported = visibility == Visibility::Public;
Some(SymbolNode {
name,
qualified_name,
kind: SymbolKind::Variable,
location: node_location(file_path, node),
visibility,
is_exported,
is_async: false,
is_test: false,
decorators: Vec::new(),
signature: None,
})
}
fn extract_macro(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
let name = node_name(source, node)?;
let qualified_name = format!("{file_path}::{name}");
Some(SymbolNode {
name,
qualified_name,
kind: SymbolKind::Macro,
location: node_location(file_path, node),
visibility: Visibility::Public, is_exported: false,
is_async: false,
is_test: false,
decorators: Vec::new(),
signature: None,
})
}
fn extract_impl(
source: &[u8],
file_path: &str,
node: Node,
symbols: &mut Vec<SymbolNode>,
edges: &mut Vec<Edge>,
) {
let type_name = match node.child_by_field_name("type") {
Some(t) => match t.utf8_text(source) {
Ok(s) => s.to_string(),
Err(_) => return,
},
None => return,
};
let trait_name = node
.child_by_field_name("trait")
.and_then(|t| t.utf8_text(source).ok())
.map(|s| s.to_string());
if let Some(ref tname) = trait_name {
let trait_qn = format!("{file_path}::{tname}");
let type_qn = format!("{file_path}::{type_name}");
edges.push(Edge {
kind: EdgeKind::Implements,
source: type_qn,
target: trait_qn,
metadata: None,
});
}
let body = match node.child_by_field_name("body") {
Some(b) => b,
None => return,
};
let mut body_cursor = body.walk();
let mut pending_attrs: Vec<String> = Vec::new();
for member in body.children(&mut body_cursor) {
if !member.is_named() {
continue;
}
if member.kind() == "attribute_item" {
if let Ok(text) = member.utf8_text(source) {
pending_attrs.push(text.to_string());
}
continue;
}
if member.kind() == "function_item" {
if let Some(sym) =
extract_function(source, file_path, member, Some(&type_name), &pending_attrs)
{
edges.push(contains_edge(file_path, &sym.qualified_name));
let type_qn = format!("{file_path}::{type_name}");
edges.push(Edge {
kind: EdgeKind::ChildOf,
source: sym.qualified_name.clone(),
target: type_qn,
metadata: None,
});
symbols.push(sym);
}
}
pending_attrs.clear();
}
}
fn extract_use_declaration(
source: &[u8],
node: Node,
imports: &mut Vec<RawImport>,
exports: &mut Vec<Export>,
) {
let line = node.start_position().row + 1;
let is_pub_use = {
let mut cur = node.walk();
let result = node
.children(&mut cur)
.any(|c| c.kind() == "visibility_modifier");
result
};
let argument = match node.child_by_field_name("argument") {
Some(a) => a,
None => return,
};
process_use_argument(source, argument, &[], line, is_pub_use, imports, exports);
}
fn process_use_argument(
source: &[u8],
node: Node,
prefix_parts: &[String],
line: usize,
is_pub_use: bool,
imports: &mut Vec<RawImport>,
exports: &mut Vec<Export>,
) {
match node.kind() {
"scoped_identifier" => {
let parts = flatten_scoped_identifier(source, node);
let specifier = parts.join("::");
let name = parts.last().cloned().unwrap_or_default();
emit_import_and_maybe_export(
specifier,
vec![ImportName {
name,
alias: None,
is_type: false,
}],
false,
line,
is_pub_use,
imports,
exports,
);
}
"use_wildcard" => {
let embedded_parts: Vec<String> = {
let mut cur = node.walk();
node.children(&mut cur)
.filter(|c| {
matches!(
c.kind(),
"scoped_identifier" | "identifier" | "crate" | "self" | "super"
)
})
.flat_map(|c| flatten_path_node(source, c))
.collect()
};
let specifier = if !embedded_parts.is_empty() {
embedded_parts.join("::")
} else {
prefix_parts.join("::")
};
emit_import_and_maybe_export(
specifier,
Vec::new(),
true, line,
is_pub_use,
imports,
exports,
);
}
"scoped_use_list" => {
let path_parts: Vec<String> = match node.child_by_field_name("path") {
Some(p) => flatten_path_node(source, p),
None => prefix_parts.to_vec(),
};
let list = match node.child_by_field_name("list") {
Some(l) => l,
None => return,
};
let mut list_cursor = list.walk();
for item in list.children(&mut list_cursor) {
if !item.is_named() {
continue;
}
process_use_argument(
source,
item,
&path_parts,
line,
is_pub_use,
imports,
exports,
);
}
}
"use_as_clause" => {
let path_node = node.child_by_field_name("path");
let alias_node = node.child_by_field_name("alias");
let (specifier, name) = match path_node {
Some(p) => {
let parts = flatten_path_node(source, p);
let name = parts.last().cloned().unwrap_or_default();
(parts.join("::"), name)
}
None => (prefix_parts.join("::"), String::new()),
};
let alias = alias_node
.and_then(|a| a.utf8_text(source).ok())
.map(|s| s.to_string());
emit_import_and_maybe_export(
specifier,
vec![ImportName {
name,
alias,
is_type: false,
}],
false,
line,
is_pub_use,
imports,
exports,
);
}
"identifier" | "crate" | "self" | "super" => {
if let Ok(text) = node.utf8_text(source) {
let mut parts = prefix_parts.to_vec();
let name = text.to_string();
parts.push(name.clone());
let specifier = parts.join("::");
emit_import_and_maybe_export(
specifier,
vec![ImportName {
name,
alias: None,
is_type: false,
}],
false,
line,
is_pub_use,
imports,
exports,
);
}
}
"use_list" => {
let mut list_cursor = node.walk();
for item in node.children(&mut list_cursor) {
if !item.is_named() {
continue;
}
process_use_argument(
source,
item,
prefix_parts,
line,
is_pub_use,
imports,
exports,
);
}
}
_ => {}
}
}
fn emit_import_and_maybe_export(
specifier: String,
names: Vec<ImportName>,
is_namespace: bool,
line: usize,
is_pub_use: bool,
imports: &mut Vec<RawImport>,
exports: &mut Vec<Export>,
) {
if is_pub_use {
for n in &names {
exports.push(Export {
name: n.alias.clone().unwrap_or_else(|| n.name.clone()),
local_name: Some(n.name.clone()),
is_default: false,
is_type_only: false,
is_reexport: true,
source_specifier: Some(specifier.clone()),
});
}
if is_namespace {
exports.push(Export {
name: String::new(),
local_name: None,
is_default: false,
is_type_only: false,
is_reexport: true,
source_specifier: Some(specifier.clone()),
});
}
}
imports.push(RawImport {
specifier,
names,
is_type_only: false,
is_side_effect: false,
is_namespace,
line,
});
}
fn flatten_scoped_identifier(source: &[u8], node: Node) -> Vec<String> {
let mut parts = Vec::new();
flatten_scoped_identifier_into(source, node, &mut parts);
parts
}
fn flatten_scoped_identifier_into(source: &[u8], node: Node, parts: &mut Vec<String>) {
if let Some(path) = node.child_by_field_name("path") {
match path.kind() {
"scoped_identifier" => flatten_scoped_identifier_into(source, path, parts),
_ => {
if let Ok(text) = path.utf8_text(source) {
parts.push(text.to_string());
}
}
}
}
if let Some(name) = node.child_by_field_name("name") {
if let Ok(text) = name.utf8_text(source) {
parts.push(text.to_string());
}
}
}
fn flatten_path_node(source: &[u8], node: Node) -> Vec<String> {
match node.kind() {
"scoped_identifier" => flatten_scoped_identifier(source, node),
"identifier" | "crate" | "self" | "super" => node
.utf8_text(source)
.ok()
.map(|s| vec![s.to_string()])
.unwrap_or_default(),
_ => Vec::new(),
}
}
fn node_name(source: &[u8], node: Node) -> Option<String> {
node.child_by_field_name("name")
.and_then(|n| n.utf8_text(source).ok())
.map(|s| s.to_string())
}
fn node_location(file_path: &str, node: Node) -> Location {
let start = node.start_position();
let end = node.end_position();
Location {
file: file_path.into(),
line_start: start.row + 1,
line_end: end.row + 1,
col_start: start.column,
col_end: end.column,
}
}
fn contains_edge(file_path: &str, qualified_name: &str) -> Edge {
Edge {
kind: EdgeKind::Contains,
source: file_path.to_string(),
target: qualified_name.to_string(),
metadata: None,
}
}
fn extract_visibility(source: &[u8], node: Node) -> Visibility {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "visibility_modifier" {
let text = child.utf8_text(source).unwrap_or("");
return if text.contains("crate") {
Visibility::Crate
} else {
Visibility::Public
};
}
}
Visibility::Private
}
fn is_async_fn(source: &[u8], node: Node) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "function_modifiers" {
let text = child.utf8_text(source).unwrap_or("");
return text.split_whitespace().any(|w| w == "async");
}
}
false
}
fn attrs_contain_test(attrs: &[String]) -> bool {
attrs.iter().any(|a| a.contains("test"))
}
fn build_rust_signature(source: &[u8], node: Node) -> Option<String> {
let params = node
.child_by_field_name("parameters")
.and_then(|n| n.utf8_text(source).ok())?;
let return_type = node
.child_by_field_name("return_type")
.and_then(|n| n.utf8_text(source).ok());
Some(match return_type {
Some(ret) => format!("{params} {ret}"),
None => params.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_rust(source: &str) -> ParseResult {
let parser = RustParser::new();
parser
.parse(source.as_bytes(), Path::new("test.rs"))
.expect("parse failed")
}
#[test]
fn ac6_function_item_extracts_function_symbol() {
let result = parse_rust("fn foo() {}");
assert_eq!(result.symbols.len(), 1);
let sym = &result.symbols[0];
assert_eq!(sym.name, "foo");
assert_eq!(sym.kind, SymbolKind::Function);
}
#[test]
fn function_qualified_name_uses_file_path() {
let result = parse_rust("fn foo() {}");
assert_eq!(result.symbols[0].qualified_name, "test.rs::foo");
}
#[test]
fn function_contains_edge_emitted() {
let result = parse_rust("fn foo() {}");
let contains: Vec<_> = result
.edges
.iter()
.filter(|e| e.kind == EdgeKind::Contains)
.collect();
assert_eq!(contains.len(), 1);
assert_eq!(contains[0].source, "test.rs");
assert_eq!(contains[0].target, "test.rs::foo");
}
#[test]
fn ac7_struct_item_extracts_struct_symbol() {
let result = parse_rust("struct Bar {}");
assert_eq!(result.symbols.len(), 1);
let sym = &result.symbols[0];
assert_eq!(sym.name, "Bar");
assert_eq!(sym.kind, SymbolKind::Struct);
}
#[test]
fn ac8_impl_item_extracts_method_symbol() {
let result = parse_rust("struct Foo; impl Foo { fn bar(&self) {} }");
let method = result
.symbols
.iter()
.find(|s| s.name == "bar")
.expect("method 'bar' not found");
assert_eq!(method.kind, SymbolKind::Method);
assert_eq!(method.qualified_name, "test.rs::Foo.bar");
}
#[test]
fn ac8_impl_method_has_child_of_edge() {
let result = parse_rust("struct Foo; impl Foo { fn bar(&self) {} }");
let child_of = result
.edges
.iter()
.find(|e| e.kind == EdgeKind::ChildOf)
.expect("ChildOf edge not found");
assert_eq!(child_of.source, "test.rs::Foo.bar");
assert_eq!(child_of.target, "test.rs::Foo");
}
#[test]
fn impl_method_also_has_contains_edge() {
let result = parse_rust("struct Foo; impl Foo { fn bar(&self) {} }");
let contains: Vec<_> = result
.edges
.iter()
.filter(|e| e.kind == EdgeKind::Contains && e.target == "test.rs::Foo.bar")
.collect();
assert_eq!(contains.len(), 1);
}
#[test]
fn ac9_trait_item_extracts_trait_symbol() {
let result = parse_rust("trait Baz {}");
assert_eq!(result.symbols.len(), 1);
let sym = &result.symbols[0];
assert_eq!(sym.name, "Baz");
assert_eq!(sym.kind, SymbolKind::Trait);
}
#[test]
fn ac10_enum_item_extracts_enum_symbol() {
let result = parse_rust("enum Color { Red, Green }");
assert_eq!(result.symbols.len(), 1);
let sym = &result.symbols[0];
assert_eq!(sym.name, "Color");
assert_eq!(sym.kind, SymbolKind::Enum);
}
#[test]
fn ac14_pub_fn_is_public() {
let result = parse_rust("pub fn visible() {}");
let sym = &result.symbols[0];
assert_eq!(sym.visibility, Visibility::Public);
assert!(sym.is_exported);
}
#[test]
fn ac14_pub_crate_fn_is_crate() {
let result = parse_rust("pub(crate) fn crate_fn() {}");
let sym = &result.symbols[0];
assert_eq!(sym.visibility, Visibility::Crate);
assert!(!sym.is_exported);
}
#[test]
fn ac14_private_fn_is_private() {
let result = parse_rust("fn private_fn() {}");
let sym = &result.symbols[0];
assert_eq!(sym.visibility, Visibility::Private);
assert!(!sym.is_exported);
}
#[test]
fn pub_struct_is_exported() {
let result = parse_rust("pub struct MyStruct {}");
let sym = &result.symbols[0];
assert_eq!(sym.visibility, Visibility::Public);
assert!(sym.is_exported);
}
#[test]
fn ac49_empty_source_does_not_panic() {
let parser = RustParser::new();
let result = parser.parse(b"", Path::new("empty.rs"));
assert!(result.is_ok());
let r = result.unwrap();
assert!(r.symbols.is_empty());
assert!(r.edges.is_empty());
}
#[test]
fn ac50_partial_extraction_from_broken_source() {
let source = r#"
fn valid() {}
fn broken( {{{
fn also_valid() {}
"#;
let parser = RustParser::new();
let result = parser
.parse(source.as_bytes(), Path::new("broken.rs"))
.expect("should not error on syntax errors");
assert!(
result.symbols.iter().any(|s| s.name == "valid"),
"should find 'valid' function in broken source"
);
}
#[test]
fn trait_impl_emits_implements_edge() {
let source = "trait Display {} struct Foo; impl Display for Foo {}";
let result = parse_rust(source);
let implements = result
.edges
.iter()
.find(|e| e.kind == EdgeKind::Implements)
.expect("Implements edge not found");
assert_eq!(implements.source, "test.rs::Foo");
assert_eq!(implements.target, "test.rs::Display");
}
#[test]
fn type_alias_is_extracted() {
let result = parse_rust("type MyAlias = u32;");
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "MyAlias");
assert_eq!(result.symbols[0].kind, SymbolKind::TypeAlias);
}
#[test]
fn const_item_is_extracted() {
let result = parse_rust("const MAX: u32 = 100;");
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "MAX");
assert_eq!(result.symbols[0].kind, SymbolKind::Const);
}
#[test]
fn static_item_is_extracted_as_variable() {
let result = parse_rust(r#"static GREETING: &str = "hello";"#);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "GREETING");
assert_eq!(result.symbols[0].kind, SymbolKind::Variable);
}
#[test]
fn macro_definition_is_extracted() {
let result = parse_rust("macro_rules! my_macro { () => {} }");
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "my_macro");
assert_eq!(result.symbols[0].kind, SymbolKind::Macro);
}
#[test]
fn async_fn_is_flagged() {
let result = parse_rust("async fn fetch() {}");
assert!(result.symbols[0].is_async);
}
#[test]
fn sync_fn_is_not_async() {
let result = parse_rust("fn sync_fn() {}");
assert!(!result.symbols[0].is_async);
}
#[test]
fn test_attribute_sets_is_test() {
let result = parse_rust("#[test]\nfn my_test() {}");
assert!(result.symbols[0].is_test);
}
#[test]
fn no_test_attribute_is_not_test() {
let result = parse_rust("fn regular_fn() {}");
assert!(!result.symbols[0].is_test);
}
#[test]
fn function_signature_includes_params_and_return_type() {
let result = parse_rust("fn add(a: i32, b: i32) -> i32 { a + b }");
let sig = result.symbols[0].signature.as_ref().expect("no signature");
assert!(sig.contains("a: i32"));
assert!(sig.contains("b: i32"));
assert!(sig.contains("i32")); }
#[test]
fn function_signature_without_return_type() {
let result = parse_rust("fn greet(name: &str) {}");
let sig = result.symbols[0].signature.as_ref().expect("no signature");
assert!(sig.contains("name: &str"));
}
#[test]
fn location_is_one_based_line_numbers() {
let result = parse_rust("fn foo() {}");
let loc = &result.symbols[0].location;
assert_eq!(loc.file.to_string_lossy(), "test.rs");
assert_eq!(loc.line_start, 1);
assert!(loc.line_end >= 1);
}
#[test]
fn multiple_top_level_items_all_extracted() {
let source = r#"
fn foo() {}
struct Bar {}
enum Baz { A }
trait Qux {}
"#;
let result = parse_rust(source);
assert_eq!(result.symbols.len(), 4);
assert!(result
.symbols
.iter()
.any(|s| s.name == "foo" && s.kind == SymbolKind::Function));
assert!(result
.symbols
.iter()
.any(|s| s.name == "Bar" && s.kind == SymbolKind::Struct));
assert!(result
.symbols
.iter()
.any(|s| s.name == "Baz" && s.kind == SymbolKind::Enum));
assert!(result
.symbols
.iter()
.any(|s| s.name == "Qux" && s.kind == SymbolKind::Trait));
let contains_count = result
.edges
.iter()
.filter(|e| e.kind == EdgeKind::Contains)
.count();
assert_eq!(contains_count, 4);
}
#[test]
fn impl_with_multiple_methods() {
let source = r#"
struct Counter;
impl Counter {
fn new() -> Self { Counter }
fn increment(&mut self) {}
fn value(&self) -> u32 { 0 }
}
"#;
let result = parse_rust(source);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 3);
let child_of_count = result
.edges
.iter()
.filter(|e| e.kind == EdgeKind::ChildOf)
.count();
assert_eq!(child_of_count, 3);
}
#[test]
fn ac11_use_scoped_identifier_extracts_raw_import() {
let result = parse_rust("use crate::auth::validate;");
assert_eq!(result.imports.len(), 1);
let imp = &result.imports[0];
assert_eq!(imp.specifier, "crate::auth::validate");
assert_eq!(imp.names.len(), 1);
assert_eq!(imp.names[0].name, "validate");
assert!(!imp.is_namespace);
}
#[test]
fn use_simple_identifier_extracts_raw_import() {
let result = parse_rust("use std::fmt;");
assert_eq!(result.imports.len(), 1);
let imp = &result.imports[0];
assert_eq!(imp.specifier, "std::fmt");
assert_eq!(imp.names[0].name, "fmt");
}
#[test]
fn ac12_pub_use_creates_reexport_export_entry() {
let result = parse_rust("pub use self::greetings::hello;");
assert_eq!(result.imports.len(), 1);
let imp = &result.imports[0];
assert_eq!(imp.specifier, "self::greetings::hello");
assert_eq!(result.exports.len(), 1);
let exp = &result.exports[0];
assert!(exp.is_reexport);
assert_eq!(
exp.source_specifier.as_deref(),
Some("self::greetings::hello")
);
assert_eq!(exp.name, "hello");
}
#[test]
fn use_wildcard_sets_is_namespace() {
let result = parse_rust("use foo::bar::*;");
assert_eq!(result.imports.len(), 1);
let imp = &result.imports[0];
assert!(imp.is_namespace);
assert_eq!(imp.specifier, "foo::bar");
}
#[test]
fn use_scoped_list_extracts_multiple_names() {
let result = parse_rust("use foo::{A, B};");
assert_eq!(result.imports.len(), 2);
let specifiers: Vec<_> = result
.imports
.iter()
.map(|i| i.specifier.as_str())
.collect();
assert!(
specifiers.contains(&"foo::A"),
"expected foo::A, got {specifiers:?}"
);
assert!(
specifiers.contains(&"foo::B"),
"expected foo::B, got {specifiers:?}"
);
}
#[test]
fn use_as_clause_extracts_alias() {
let result = parse_rust("use foo as bar;");
assert_eq!(result.imports.len(), 1);
let imp = &result.imports[0];
assert_eq!(imp.specifier, "foo");
assert_eq!(imp.names.len(), 1);
assert_eq!(imp.names[0].name, "foo");
assert_eq!(imp.names[0].alias.as_deref(), Some("bar"));
}
#[test]
fn ac13_mod_declaration_captured_with_mod_prefix() {
let result = parse_rust("mod submodule;");
assert_eq!(result.imports.len(), 1);
let imp = &result.imports[0];
assert_eq!(imp.specifier, "mod::submodule");
assert!(imp.names.is_empty());
}
#[test]
fn inline_mod_not_captured_as_import() {
let result = parse_rust("mod inline { fn inner() {} }");
assert!(
result.imports.is_empty(),
"inline mod should not produce an import, got: {:?}",
result.imports
);
}
#[test]
fn integration_realistic_module() {
let source = r#"
use std::fmt;
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
pub fn distance(&self, other: &Point) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
pub trait Shape {
fn area(&self) -> f64;
}
pub enum Color {
Red,
Green,
Blue,
}
pub const MAX_POINTS: usize = 1000;
pub async fn fetch_data() -> Vec<Point> {
vec![]
}
#[test]
fn test_distance() {
let a = Point { x: 0.0, y: 0.0 };
let b = Point { x: 3.0, y: 4.0 };
assert_eq!(a.distance(&b), 5.0);
}
"#;
let result = parse_rust(source);
assert!(result
.symbols
.iter()
.any(|s| s.name == "Point" && s.kind == SymbolKind::Struct));
assert!(result
.symbols
.iter()
.any(|s| s.name == "Shape" && s.kind == SymbolKind::Trait));
assert!(result
.symbols
.iter()
.any(|s| s.name == "Color" && s.kind == SymbolKind::Enum));
assert!(result
.symbols
.iter()
.any(|s| s.name == "MAX_POINTS" && s.kind == SymbolKind::Const));
assert!(result
.symbols
.iter()
.any(|s| s.name == "fetch_data" && s.is_async));
assert!(result
.symbols
.iter()
.any(|s| s.name == "test_distance" && s.is_test));
assert!(result.symbols.iter().any(|s| s.name == "new"
&& s.kind == SymbolKind::Method
&& s.qualified_name == "test.rs::Point.new"));
assert!(result
.symbols
.iter()
.any(|s| s.name == "distance" && s.kind == SymbolKind::Method));
assert!(result
.symbols
.iter()
.any(|s| s.name == "fmt" && s.kind == SymbolKind::Method));
let point_sym = result.symbols.iter().find(|s| s.name == "Point").unwrap();
assert_eq!(point_sym.visibility, Visibility::Public);
assert!(point_sym.is_exported);
let implements = result
.edges
.iter()
.filter(|e| e.kind == EdgeKind::Implements)
.collect::<Vec<_>>();
assert!(
!implements.is_empty(),
"should have at least one Implements edge"
);
let child_of_count = result
.edges
.iter()
.filter(|e| e.kind == EdgeKind::ChildOf)
.count();
assert!(
child_of_count >= 3,
"expected at least 3 ChildOf edges, got {child_of_count}"
);
}
}