P1 correctness: - filter test files by default (--include-tests to opt in) - per-module diagrams show cross-module dependency arrows - qualified type names (Module::TypeName) fix false edges from duplicate names P2 output richness: - method parameter types and return types in class diagrams (Rust + Python) - Python pyproject.toml project analyzer (--level project for monorepos) P3 unique value: - boundary rules in archlens.toml ([rules] allow/deny, --strict enforcement) P4 nice to have: - dependency weight labels on module arrows (--no-weights to disable) - --watch mode with 500ms debounce - D2 renderer adapter (--format d2) - interactive self-contained HTML viewer (--format html) - git-aware incremental analysis (--since <ref>)
273 lines
7.4 KiB
Rust
273 lines
7.4 KiB
Rust
use archlens_domain::{
|
|
CodeElementKind, FilePath, Language, RelationshipKind, SourceFile, ports::SourceAnalyzer,
|
|
};
|
|
use archlens_tree_sitter::TreeSitterAnalyzer;
|
|
|
|
fn analyze_rust(source: &str, filename: &str) -> archlens_domain::AnalysisResult {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let file_path = dir.path().join(filename);
|
|
std::fs::write(&file_path, source).unwrap();
|
|
|
|
let analyzer = TreeSitterAnalyzer::new();
|
|
let source_file = SourceFile::new(
|
|
FilePath::new(file_path.to_str().unwrap()).unwrap(),
|
|
Language::Rust,
|
|
);
|
|
analyzer.analyze_file(&source_file).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_rust_struct() {
|
|
let result = analyze_rust("pub struct Order {\n id: u64,\n}", "order.rs");
|
|
|
|
assert_eq!(result.elements().len(), 1);
|
|
assert_eq!(result.elements()[0].name(), "Order");
|
|
assert_eq!(result.elements()[0].kind(), CodeElementKind::Struct);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_rust_enum() {
|
|
let result = analyze_rust(
|
|
"pub enum Status {\n Active,\n Inactive,\n}",
|
|
"status.rs",
|
|
);
|
|
|
|
assert_eq!(result.elements().len(), 1);
|
|
assert_eq!(result.elements()[0].name(), "Status");
|
|
assert_eq!(result.elements()[0].kind(), CodeElementKind::Enum);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_rust_trait() {
|
|
let result = analyze_rust("pub trait Repository {\n fn find(&self);\n}", "repo.rs");
|
|
|
|
assert_eq!(result.elements().len(), 1);
|
|
assert_eq!(result.elements()[0].name(), "Repository");
|
|
assert_eq!(result.elements()[0].kind(), CodeElementKind::Trait);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_composition_from_struct_fields() {
|
|
let source =
|
|
"pub struct Order {\n id: u64,\n}\npub struct OrderService {\n order: Order,\n}";
|
|
let result = analyze_rust(source, "service.rs");
|
|
|
|
assert_eq!(result.elements().len(), 2);
|
|
assert_eq!(result.relationships().len(), 1);
|
|
assert_eq!(result.relationships()[0].source(), "OrderService");
|
|
assert_eq!(result.relationships()[0].target(), "Order");
|
|
assert_eq!(
|
|
result.relationships()[0].kind(),
|
|
RelationshipKind::Composition
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_inheritance_from_trait_impl() {
|
|
let source = "pub trait Printable {}\npub struct Order {}\nimpl Printable for Order {}";
|
|
let result = analyze_rust(source, "order.rs");
|
|
|
|
let inheritance: Vec<_> = result
|
|
.relationships()
|
|
.iter()
|
|
.filter(|r| r.kind() == RelationshipKind::Inheritance)
|
|
.collect();
|
|
|
|
assert_eq!(inheritance.len(), 1);
|
|
assert_eq!(inheritance[0].source(), "Order");
|
|
assert_eq!(inheritance[0].target(), "Printable");
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_use_imports() {
|
|
let source =
|
|
"use crate::domain::Order;\nuse crate::ports::Repository;\n\npub struct Service {}";
|
|
let result = analyze_rust(source, "service.rs");
|
|
|
|
let imports: Vec<_> = result
|
|
.relationships()
|
|
.iter()
|
|
.filter(|r| r.kind() == RelationshipKind::Import)
|
|
.collect();
|
|
|
|
assert!(imports.iter().any(|r| r.target() == "crate::domain::Order"));
|
|
assert!(
|
|
imports
|
|
.iter()
|
|
.any(|r| r.target() == "crate::ports::Repository")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn filters_std_and_external_crate_imports() {
|
|
let source =
|
|
"use std::collections::HashMap;\nuse serde::Serialize;\nuse crate::models::Order;\n";
|
|
let result = analyze_rust(source, "lib.rs");
|
|
|
|
let imports: Vec<_> = result
|
|
.relationships()
|
|
.iter()
|
|
.filter(|r| r.kind() == RelationshipKind::Import)
|
|
.collect();
|
|
|
|
assert_eq!(imports.len(), 1);
|
|
assert_eq!(imports[0].target(), "crate::models::Order");
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_mod_declarations() {
|
|
let source = "mod models;\nmod services;\npub struct App {}";
|
|
let result = analyze_rust(source, "lib.rs");
|
|
|
|
let imports: Vec<_> = result
|
|
.relationships()
|
|
.iter()
|
|
.filter(|r| r.kind() == RelationshipKind::Import)
|
|
.collect();
|
|
|
|
assert!(imports.iter().any(|r| r.target() == "crate::models"));
|
|
assert!(imports.iter().any(|r| r.target() == "crate::services"));
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_rust_method_with_typed_params() {
|
|
let source = r#"
|
|
pub struct OrderService;
|
|
impl OrderService {
|
|
pub fn process(&self, order: Order, count: u64) {}
|
|
}
|
|
"#;
|
|
let result = analyze_rust(source, "service.rs");
|
|
|
|
let element = result
|
|
.elements()
|
|
.iter()
|
|
.find(|e| e.name() == "OrderService")
|
|
.unwrap();
|
|
|
|
assert!(
|
|
element
|
|
.methods()
|
|
.iter()
|
|
.any(|m| m.contains("order: Order") && m.contains("count: u64")),
|
|
"expected typed params in method, got: {:?}",
|
|
element.methods()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_rust_method_return_type() {
|
|
let source = r#"
|
|
pub struct OrderService;
|
|
impl OrderService {
|
|
pub fn get(&self) -> Order {}
|
|
}
|
|
"#;
|
|
let result = analyze_rust(source, "service.rs");
|
|
|
|
let element = result
|
|
.elements()
|
|
.iter()
|
|
.find(|e| e.name() == "OrderService")
|
|
.unwrap();
|
|
|
|
assert!(
|
|
element.methods().iter().any(|m| m.contains("-> Order")),
|
|
"expected return type in method, got: {:?}",
|
|
element.methods()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_rust_method_params_and_return() {
|
|
let source = r#"
|
|
pub struct OrderService;
|
|
impl OrderService {
|
|
pub fn process(&self, order: Order) -> Result<(), Error> {}
|
|
}
|
|
"#;
|
|
let result = analyze_rust(source, "service.rs");
|
|
|
|
let element = result
|
|
.elements()
|
|
.iter()
|
|
.find(|e| e.name() == "OrderService")
|
|
.unwrap();
|
|
|
|
let method = element
|
|
.methods()
|
|
.iter()
|
|
.find(|m| m.contains("process"))
|
|
.unwrap();
|
|
assert!(method.contains("order: Order"), "missing param: {method}");
|
|
assert!(method.contains("->"), "missing return arrow: {method}");
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_rust_static_method_params() {
|
|
let source = r#"
|
|
pub struct Finder;
|
|
impl Finder {
|
|
pub fn detect(path: &str, count: usize) -> bool { false }
|
|
}
|
|
"#;
|
|
let result = analyze_rust(source, "finder.rs");
|
|
let element = result
|
|
.elements()
|
|
.iter()
|
|
.find(|e| e.name() == "Finder")
|
|
.unwrap();
|
|
let method = element
|
|
.methods()
|
|
.iter()
|
|
.find(|m| m.contains("detect"))
|
|
.unwrap();
|
|
assert!(method.contains("path"), "missing path param: {method}");
|
|
assert!(method.contains("count"), "missing count param: {method}");
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_rust_private_method_params() {
|
|
let source = r#"
|
|
pub struct WalkdirDiscovery;
|
|
impl WalkdirDiscovery {
|
|
fn detect_language(path: &std::path::Path) -> Option<String> { None }
|
|
}
|
|
"#;
|
|
let result = analyze_rust(source, "discovery.rs");
|
|
let element = result
|
|
.elements()
|
|
.iter()
|
|
.find(|e| e.name() == "WalkdirDiscovery")
|
|
.unwrap();
|
|
let method = element
|
|
.methods()
|
|
.iter()
|
|
.find(|m| m.contains("detect_language"))
|
|
.unwrap();
|
|
assert!(method.contains("path"), "missing path param: {method}");
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_rust_method_reference_param() {
|
|
let source = r#"
|
|
use std::path::Path;
|
|
pub struct WalkdirDiscovery;
|
|
impl WalkdirDiscovery {
|
|
fn detect_language(path: &Path) -> Option<String> { None }
|
|
}
|
|
"#;
|
|
let result = analyze_rust(source, "discovery.rs");
|
|
let element = result
|
|
.elements()
|
|
.iter()
|
|
.find(|e| e.name() == "WalkdirDiscovery")
|
|
.unwrap();
|
|
let method = element
|
|
.methods()
|
|
.iter()
|
|
.find(|m| m.contains("detect_language"))
|
|
.unwrap();
|
|
assert!(method.contains("path"), "missing path param: {method}");
|
|
}
|