use std::path::Path;
use anyhow::{Context, Result};
use tree_sitter::{Language, Parser, Query, QueryCursor, StreamingIterator};
use boundary_core::analyzer::{LanguageAnalyzer, ParsedFile};
use boundary_core::types::*;
pub struct RustAnalyzer {
language: Language,
trait_query: Query,
struct_query: Query,
impl_query: Query,
use_query: Query,
}
impl RustAnalyzer {
pub fn new() -> Result<Self> {
let language: Language = tree_sitter_rust::LANGUAGE.into();
let trait_query = Query::new(
&language,
r#"
(trait_item
name: (type_identifier) @name
body: (declaration_list
(function_signature_item
name: (identifier) @method)*))
"#,
)
.context("failed to compile trait query")?;
let struct_query = Query::new(
&language,
r#"
(struct_item
name: (type_identifier) @name
body: (field_declaration_list
(field_declaration
name: (field_identifier) @field
type: (_) @field_type)*)?)
"#,
)
.context("failed to compile struct query")?;
let impl_query = Query::new(
&language,
r#"
(impl_item
trait: (type_identifier)? @trait_name
type: (type_identifier) @type_name)
"#,
)
.context("failed to compile impl query")?;
let use_query = Query::new(
&language,
r#"
(use_declaration
argument: (_) @path)
"#,
)
.context("failed to compile use query")?;
Ok(Self {
language,
trait_query,
struct_query,
impl_query,
use_query,
})
}
}
impl LanguageAnalyzer for RustAnalyzer {
fn language(&self) -> &'static str {
"rust"
}
fn file_extensions(&self) -> &[&str] {
&["rs"]
}
fn parse_file(&self, path: &Path, content: &str) -> Result<ParsedFile> {
let mut parser = Parser::new();
parser
.set_language(&self.language)
.context("failed to set Rust language")?;
let tree = parser
.parse(content, None)
.context("failed to parse Rust file")?;
Ok(ParsedFile {
path: path.to_path_buf(),
tree,
content: content.to_string(),
})
}
fn extract_components(&self, parsed: &ParsedFile) -> Vec<Component> {
let mut components = Vec::new();
let module_path = derive_module_path(&parsed.path);
extract_traits(&self.trait_query, parsed, &module_path, &mut components);
extract_structs(&self.struct_query, parsed, &module_path, &mut components);
enrich_with_impls(&self.impl_query, parsed, &module_path, &mut components);
components
}
fn extract_dependencies(&self, parsed: &ParsedFile) -> Vec<Dependency> {
let mut deps = Vec::new();
let module_path = derive_module_path(&parsed.path);
let from_id = ComponentId::new(&module_path, "<file>");
let mut cursor = QueryCursor::new();
let path_idx = self
.use_query
.capture_names()
.iter()
.position(|n| *n == "path")
.unwrap_or(0);
let mut matches = cursor.matches(
&self.use_query,
parsed.tree.root_node(),
parsed.content.as_bytes(),
);
while let Some(m) = matches.next() {
for capture in m.captures {
if capture.index as usize == path_idx {
let node = capture.node;
let use_path = node_text(node, &parsed.content);
if use_path.starts_with("std::") || use_path.starts_with("core::") {
continue;
}
let to_id = ComponentId::new(&use_path, "<module>");
deps.push(Dependency {
from: from_id.clone(),
to: to_id,
kind: DependencyKind::Import,
location: SourceLocation {
file: parsed.path.clone(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
},
import_path: Some(use_path),
});
}
}
}
deps
}
}
fn extract_traits(
query: &Query,
parsed: &ParsedFile,
module_path: &str,
components: &mut Vec<Component>,
) {
let mut cursor = QueryCursor::new();
let name_idx = query
.capture_names()
.iter()
.position(|n| *n == "name")
.unwrap_or(0);
let method_idx = query.capture_names().iter().position(|n| *n == "method");
let mut matches = cursor.matches(query, parsed.tree.root_node(), parsed.content.as_bytes());
while let Some(m) = matches.next() {
let mut name = String::new();
let mut methods = Vec::new();
let mut start_row = 0;
let mut start_col = 0;
for capture in m.captures {
if capture.index as usize == name_idx {
name = node_text(capture.node, &parsed.content);
start_row = capture.node.start_position().row;
start_col = capture.node.start_position().column;
} else if Some(capture.index as usize) == method_idx {
methods.push(MethodInfo {
name: node_text(capture.node, &parsed.content),
parameters: String::new(),
return_type: String::new(),
});
}
}
if name.is_empty() {
continue;
}
components.push(Component {
id: ComponentId::new(module_path, &name),
name: name.clone(),
kind: ComponentKind::Port(PortInfo { name, methods }),
layer: None,
location: SourceLocation {
file: parsed.path.clone(),
line: start_row + 1,
column: start_col + 1,
},
is_cross_cutting: false,
architecture_mode: ArchitectureMode::default(),
});
}
}
fn extract_structs(
query: &Query,
parsed: &ParsedFile,
module_path: &str,
components: &mut Vec<Component>,
) {
let mut cursor = QueryCursor::new();
let name_idx = query
.capture_names()
.iter()
.position(|n| *n == "name")
.unwrap_or(0);
let field_idx = query.capture_names().iter().position(|n| *n == "field");
let field_type_idx = query
.capture_names()
.iter()
.position(|n| *n == "field_type");
let mut matches = cursor.matches(query, parsed.tree.root_node(), parsed.content.as_bytes());
while let Some(m) = matches.next() {
let mut name = String::new();
let mut fields = Vec::new();
let mut start_row = 0;
let mut start_col = 0;
let mut current_field_name = String::new();
for capture in m.captures {
if capture.index as usize == name_idx {
name = node_text(capture.node, &parsed.content);
start_row = capture.node.start_position().row;
start_col = capture.node.start_position().column;
} else if Some(capture.index as usize) == field_idx {
current_field_name = node_text(capture.node, &parsed.content);
} else if Some(capture.index as usize) == field_type_idx {
let type_name = node_text(capture.node, &parsed.content);
if !current_field_name.is_empty() {
fields.push(FieldInfo {
name: current_field_name.clone(),
type_name,
});
current_field_name = String::new();
}
}
}
if name.is_empty() {
continue;
}
let kind = classify_struct_kind(&name, &fields);
components.push(Component {
id: ComponentId::new(module_path, &name),
name: name.clone(),
kind,
layer: None,
location: SourceLocation {
file: parsed.path.clone(),
line: start_row + 1,
column: start_col + 1,
},
is_cross_cutting: false,
architecture_mode: ArchitectureMode::default(),
});
}
}
fn enrich_with_impls(
query: &Query,
parsed: &ParsedFile,
module_path: &str,
components: &mut [Component],
) {
let mut cursor = QueryCursor::new();
let trait_name_idx = query
.capture_names()
.iter()
.position(|n| *n == "trait_name");
let type_name_idx = query
.capture_names()
.iter()
.position(|n| *n == "type_name")
.unwrap_or(0);
let mut matches = cursor.matches(query, parsed.tree.root_node(), parsed.content.as_bytes());
while let Some(m) = matches.next() {
let mut trait_name: Option<String> = None;
let mut type_name = String::new();
for capture in m.captures {
if Some(capture.index as usize) == trait_name_idx {
trait_name = Some(node_text(capture.node, &parsed.content));
}
if capture.index as usize == type_name_idx {
type_name = node_text(capture.node, &parsed.content);
}
}
if type_name.is_empty() {
continue;
}
if let Some(ref trait_name) = trait_name {
let id = ComponentId::new(module_path, &type_name);
if let Some(comp) = components.iter_mut().find(|c| c.id == id) {
match &mut comp.kind {
ComponentKind::Adapter(info) => {
if !info.implements.contains(trait_name) {
info.implements.push(trait_name.clone());
}
}
_ => {
comp.kind = ComponentKind::Adapter(AdapterInfo {
name: type_name.clone(),
implements: vec![trait_name.clone()],
});
}
}
}
}
}
}
fn classify_struct_kind(name: &str, fields: &[FieldInfo]) -> ComponentKind {
let lower = name.to_lowercase();
if lower.ends_with("repository") || lower.ends_with("repo") {
ComponentKind::Repository
} else if lower.ends_with("service") || lower.ends_with("svc") {
ComponentKind::Service
} else if lower.ends_with("handler") || lower.ends_with("controller") {
ComponentKind::Adapter(AdapterInfo {
name: name.to_string(),
implements: Vec::new(),
})
} else if lower.ends_with("usecase") || lower.ends_with("interactor") {
ComponentKind::UseCase
} else if lower.ends_with("event") {
ComponentKind::DomainEvent(EventInfo {
name: name.to_string(),
fields: fields.to_vec(),
})
} else if !fields.is_empty()
&& !fields.iter().any(|f| {
let fl = f.name.to_lowercase();
fl == "id" || fl == "uuid"
})
{
ComponentKind::ValueObject
} else {
ComponentKind::Entity(EntityInfo {
name: name.to_string(),
fields: fields.to_vec(),
methods: Vec::new(),
is_active_record: false,
})
}
}
fn node_text(node: tree_sitter::Node, source: &str) -> String {
source[node.byte_range()].to_string()
}
fn derive_module_path(path: &Path) -> String {
let path_str = path.to_string_lossy().replace('\\', "/");
if let Some(parent) = path.parent() {
parent.to_string_lossy().replace('\\', "/")
} else {
path_str
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_parse_simple_rust_file() {
let analyzer = RustAnalyzer::new().unwrap();
let content = r#"
pub trait UserRepository {
fn save(&self, user: &User) -> Result<(), Error>;
fn find_by_id(&self, id: &str) -> Result<User, Error>;
}
pub struct User {
pub id: String,
pub name: String,
}
"#;
let path = PathBuf::from("src/domain/user/mod.rs");
let parsed = analyzer.parse_file(&path, content).unwrap();
let components = analyzer.extract_components(&parsed);
assert!(
components.len() >= 2,
"expected at least 2 components, got {}",
components.len()
);
let trait_comp = components.iter().find(|c| c.name == "UserRepository");
assert!(trait_comp.is_some(), "should find UserRepository trait");
assert!(matches!(trait_comp.unwrap().kind, ComponentKind::Port(_)));
if let ComponentKind::Port(ref info) = trait_comp.unwrap().kind {
assert!(info.methods.iter().any(|m| m.name == "save"));
assert!(info.methods.iter().any(|m| m.name == "find_by_id"));
}
let entity = components.iter().find(|c| c.name == "User");
assert!(entity.is_some(), "should find User struct");
}
#[test]
fn test_extract_use_statements() {
let analyzer = RustAnalyzer::new().unwrap();
let content = r#"
use std::collections::HashMap;
use crate::domain::user::User;
use crate::infrastructure::postgres::PostgresRepo;
"#;
let path = PathBuf::from("src/application/user_service.rs");
let parsed = analyzer.parse_file(&path, content).unwrap();
let deps = analyzer.extract_dependencies(&parsed);
let paths: Vec<&str> = deps
.iter()
.filter_map(|d| d.import_path.as_deref())
.collect();
assert!(!paths.iter().any(|p| p.starts_with("std::")));
assert!(paths.iter().any(|p| p.contains("domain::user::User")));
assert!(paths
.iter()
.any(|p| p.contains("infrastructure::postgres::PostgresRepo")));
}
#[test]
fn test_struct_classification() {
let analyzer = RustAnalyzer::new().unwrap();
let content = r#"
pub struct PostgresUserRepository {
pool: Pool,
}
pub struct UserService {
repo: Box<dyn UserRepository>,
}
pub struct HttpHandler {
service: UserService,
}
pub struct CreateUserUseCase {
repo: Box<dyn UserRepository>,
}
"#;
let path = PathBuf::from("src/lib.rs");
let parsed = analyzer.parse_file(&path, content).unwrap();
let components = analyzer.extract_components(&parsed);
let repo = components
.iter()
.find(|c| c.name == "PostgresUserRepository");
assert!(matches!(repo.unwrap().kind, ComponentKind::Repository));
let svc = components.iter().find(|c| c.name == "UserService");
assert!(matches!(svc.unwrap().kind, ComponentKind::Service));
let handler = components.iter().find(|c| c.name == "HttpHandler");
assert!(matches!(handler.unwrap().kind, ComponentKind::Adapter(_)));
let uc = components.iter().find(|c| c.name == "CreateUserUseCase");
assert!(matches!(uc.unwrap().kind, ComponentKind::UseCase));
}
#[test]
fn test_impl_trait_enrichment() {
let analyzer = RustAnalyzer::new().unwrap();
let content = r#"
pub trait UserRepository {
fn save(&self, user: &User);
}
pub struct PostgresRepo {
pool: Pool,
}
impl UserRepository for PostgresRepo {
fn save(&self, user: &User) {}
}
"#;
let path = PathBuf::from("src/infrastructure/postgres.rs");
let parsed = analyzer.parse_file(&path, content).unwrap();
let components = analyzer.extract_components(&parsed);
let repo = components.iter().find(|c| c.name == "PostgresRepo");
assert!(repo.is_some(), "should find PostgresRepo");
match &repo.unwrap().kind {
ComponentKind::Adapter(info) => {
assert!(
info.implements.contains(&"UserRepository".to_string()),
"should track implemented trait"
);
}
other => panic!("expected Adapter, got {:?}", other),
}
}
}