init: archlens — architecture diagram generator
Some checks failed
CI / Check / Test (push) Failing after 1m24s

Hex arch + DDD, tree-sitter parsing, Mermaid/ASCII output.
Supports Rust + Python. 92 tests. CI, diff, --check for staleness detection.
This commit is contained in:
2026-06-16 16:13:04 +02:00
commit 35f27d00b0
106 changed files with 6744 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
use archlens_domain::{
CodeElementKind, FilePath, Language, RelationshipKind, SourceFile, ports::SourceAnalyzer,
};
use archlens_tree_sitter::TreeSitterAnalyzer;
fn analyze_python(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::Python,
);
analyzer.analyze_file(&source_file).unwrap()
}
#[test]
fn extracts_python_class() {
let result = analyze_python("class Order:\n pass\n", "order.py");
assert_eq!(result.elements().len(), 1);
assert_eq!(result.elements()[0].name(), "Order");
assert_eq!(result.elements()[0].kind(), CodeElementKind::Class);
}
#[test]
fn extracts_python_inheritance() {
let source = "class Animal:\n pass\n\nclass Dog(Animal):\n pass\n";
let result = analyze_python(source, "animals.py");
assert_eq!(result.elements().len(), 2);
let inheritance: Vec<_> = result
.relationships()
.iter()
.filter(|r| r.kind() == RelationshipKind::Inheritance)
.collect();
assert_eq!(inheritance.len(), 1);
assert_eq!(inheritance[0].source(), "Dog");
assert_eq!(inheritance[0].target(), "Animal");
}
#[test]
fn extracts_composition_from_type_annotated_fields() {
let source = "class Address:\n pass\n\nclass User:\n def __init__(self):\n self.address: Address = Address()\n";
let result = analyze_python(source, "user.py");
let composition: Vec<_> = result
.relationships()
.iter()
.filter(|r| r.kind() == RelationshipKind::Composition)
.collect();
assert_eq!(composition.len(), 1);
assert_eq!(composition[0].source(), "User");
assert_eq!(composition[0].target(), "Address");
}
#[test]
fn extracts_import_from_import_statement() {
let source = "import os\nfrom commons.src.schema import BaseModel\n";
let result = analyze_python(source, "service.py");
let imports: Vec<_> = result
.relationships()
.iter()
.filter(|r| r.kind() == RelationshipKind::Import)
.collect();
assert!(imports.iter().any(|r| r.target() == "commons.src.schema"));
assert!(
!imports.iter().any(|r| r.target() == "os"),
"stdlib should be filtered"
);
}
#[test]
fn extracts_relative_imports_from_init() {
let source = "from .schema import BaseModel\nfrom .client import ApiClient\n";
let result = analyze_python(source, "__init__.py");
let imports: Vec<_> = result
.relationships()
.iter()
.filter(|r| r.kind() == RelationshipKind::Import)
.collect();
assert_eq!(imports.len(), 2);
}
#[test]
fn extracts_import_from_plain_import() {
let source = "import commons.utils\n";
let result = analyze_python(source, "service.py");
let imports: Vec<_> = result
.relationships()
.iter()
.filter(|r| r.kind() == RelationshipKind::Import)
.collect();
assert!(imports.iter().any(|r| r.target() == "commons.utils"));
}
#[test]
fn extracts_composition_from_constructor_params() {
let source = "class Config:\n pass\n\nclass Service:\n def __init__(self, config: Config):\n pass\n";
let result = analyze_python(source, "service.py");
let composition: Vec<_> = result
.relationships()
.iter()
.filter(|r| r.kind() == RelationshipKind::Composition)
.collect();
assert_eq!(composition.len(), 1);
assert_eq!(composition[0].source(), "Service");
assert_eq!(composition[0].target(), "Config");
}
#[test]
fn extracts_composition_from_class_level_annotations() {
let source = "class Gad:\n pass\n\nclass Definition:\n gad: Gad\n name: str\n";
let result = analyze_python(source, "models.py");
let composition: Vec<_> = result
.relationships()
.iter()
.filter(|r| r.kind() == RelationshipKind::Composition)
.collect();
assert_eq!(composition.len(), 1);
assert_eq!(composition[0].source(), "Definition");
assert_eq!(composition[0].target(), "Gad");
}

View File

@@ -0,0 +1,130 @@
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"));
}