refactor: move scattered business logic into domain
- CodeGraph::merge_project_edges() replaces presentation-layer function - Language::is_test_file() centralises test file detection (was in walkdir) - AnalysisConfig::is_standard_excluded() centralises default dir exclusions (was in walkdir) - normalize_cargo_package() / normalize_python_package() in domain replace duplicated normalisers in each adapter - walkdir, cargo-workspace, python-project updated to call domain methods
This commit is contained in:
@@ -279,6 +279,43 @@ impl CodeGraph {
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge project-level crate dependencies into module-level edges.
|
||||
///
|
||||
/// Maps each crate in `project_graph` to a module name (using the crate's
|
||||
/// explicit module if set, otherwise capitalizing its directory name), then
|
||||
/// adds `Composition` edges between modules whose crates have a dependency —
|
||||
/// but only when both modules already exist in this graph.
|
||||
pub fn merge_project_edges(&mut self, project_graph: &CodeGraph) {
|
||||
let mut crate_to_module: HashMap<String, String> = HashMap::new();
|
||||
for element in project_graph.elements() {
|
||||
let module = if let Some(m) = element.module() {
|
||||
m.as_str().to_string()
|
||||
} else {
|
||||
let path = element.file_path().as_str();
|
||||
let dir = path.split('/').rev().nth(1).unwrap_or(element.name());
|
||||
ModuleName::capitalize(dir)
|
||||
};
|
||||
crate_to_module.insert(element.name().to_string(), module);
|
||||
}
|
||||
|
||||
let graph_modules: HashSet<String> =
|
||||
self.modules().iter().map(|m| m.as_str().to_string()).collect();
|
||||
|
||||
for rel in project_graph.relationships() {
|
||||
let src_module = crate_to_module.get(rel.source());
|
||||
let tgt_module = crate_to_module.get(rel.target());
|
||||
if let (Some(src), Some(tgt)) = (src_module, tgt_module)
|
||||
&& src != tgt
|
||||
&& graph_modules.contains(src)
|
||||
&& graph_modules.contains(tgt)
|
||||
&& let Ok(edge) =
|
||||
Relationship::new(src, tgt, RelationshipKind::Composition)
|
||||
{
|
||||
self.add_relationship(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute module-to-module edges with relationship counts.
|
||||
///
|
||||
/// Handles three cases:
|
||||
|
||||
@@ -12,4 +12,6 @@ pub use value_objects::analysis::{AnalysisConfig, AnalysisResult, AnalysisWarnin
|
||||
pub use value_objects::graph::{CodeElementKind, RelationshipKind, Visibility};
|
||||
pub use value_objects::output::{DiagramLevel, OutputConfig, RenderOutput, RenderedFile};
|
||||
pub use value_objects::rules::{BoundaryRule, RuleKind, RuleViolation, check_boundary_rules};
|
||||
pub use value_objects::source::{FilePath, Language, ModuleName, SourceFile};
|
||||
pub use value_objects::source::{
|
||||
FilePath, Language, ModuleName, SourceFile, normalize_cargo_package, normalize_python_package,
|
||||
};
|
||||
|
||||
@@ -68,6 +68,26 @@ impl AnalysisConfig {
|
||||
}
|
||||
}
|
||||
|
||||
const STANDARD_EXCLUDED_DIRS: &[&str] = &[
|
||||
".venv",
|
||||
"venv",
|
||||
"node_modules",
|
||||
"__pycache__",
|
||||
".git",
|
||||
"target",
|
||||
"bin",
|
||||
"obj",
|
||||
"dist",
|
||||
".tox",
|
||||
".eggs",
|
||||
];
|
||||
|
||||
impl AnalysisConfig {
|
||||
pub fn is_standard_excluded(name: &str) -> bool {
|
||||
STANDARD_EXCLUDED_DIRS.contains(&name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AnalysisConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Language {
|
||||
Rust,
|
||||
@@ -13,4 +15,26 @@ impl Language {
|
||||
Self::Python => "Python",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_test_file(&self, path: &Path) -> bool {
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
let in_tests_dir = path
|
||||
.parent()
|
||||
.map(|p| p.components().any(|c| c.as_os_str() == "tests"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if in_tests_dir {
|
||||
return true;
|
||||
}
|
||||
|
||||
match self {
|
||||
Self::Rust => stem.ends_with("_test") || stem.ends_with("_tests"),
|
||||
Self::Python => stem.starts_with("test_") || stem.ends_with("_test"),
|
||||
Self::CSharp => stem.ends_with("Tests") || stem.ends_with("Test"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,3 +7,11 @@ pub use file_path::FilePath;
|
||||
pub use language::Language;
|
||||
pub use module_name::ModuleName;
|
||||
pub use source_file::SourceFile;
|
||||
|
||||
pub fn normalize_cargo_package(name: &str) -> String {
|
||||
name.replace('_', "-")
|
||||
}
|
||||
|
||||
pub fn normalize_python_package(name: &str) -> String {
|
||||
name.to_lowercase().replace(['-', '.'], "_")
|
||||
}
|
||||
|
||||
162
crates/domain/tests/policy_tests.rs
Normal file
162
crates/domain/tests/policy_tests.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use std::path::Path;
|
||||
|
||||
use archlens_domain::{
|
||||
CodeElement, CodeElementKind, CodeGraph, FilePath, Language, ModuleName, Relationship,
|
||||
RelationshipKind, normalize_cargo_package, normalize_python_package,
|
||||
};
|
||||
|
||||
fn make_element(name: &str, module: Option<&str>) -> CodeElement {
|
||||
let mut el = CodeElement::new(
|
||||
name,
|
||||
CodeElementKind::Class,
|
||||
FilePath::new(&format!("src/{name}.rs")).unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(m) = module {
|
||||
el = el.with_module(ModuleName::new(m).unwrap());
|
||||
}
|
||||
el
|
||||
}
|
||||
|
||||
// ── merge_project_edges ───────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn merge_project_edges_adds_module_level_edges_from_project_deps() {
|
||||
let mut graph = CodeGraph::new();
|
||||
graph.add_element(make_element("Service", Some("Application")));
|
||||
graph.add_element(make_element("Order", Some("Domain")));
|
||||
|
||||
let mut project_graph = CodeGraph::new();
|
||||
project_graph.add_element(
|
||||
CodeElement::new(
|
||||
"myapp-application",
|
||||
CodeElementKind::Project,
|
||||
FilePath::new("crates/application/Cargo.toml").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
project_graph.add_element(
|
||||
CodeElement::new(
|
||||
"myapp-domain",
|
||||
CodeElementKind::Project,
|
||||
FilePath::new("crates/domain/Cargo.toml").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
project_graph.add_relationship(
|
||||
Relationship::new(
|
||||
"myapp-application",
|
||||
"myapp-domain",
|
||||
RelationshipKind::Composition,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
graph.merge_project_edges(&project_graph);
|
||||
|
||||
let module_edges = graph.module_edges();
|
||||
assert!(
|
||||
module_edges.contains_key(&("Application".to_string(), "Domain".to_string())),
|
||||
"expected Application -> Domain edge, got: {module_edges:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_project_edges_ignores_crates_with_no_matching_module() {
|
||||
let mut graph = CodeGraph::new();
|
||||
graph.add_element(make_element("Order", Some("Domain")));
|
||||
|
||||
let mut project_graph = CodeGraph::new();
|
||||
project_graph.add_element(
|
||||
CodeElement::new(
|
||||
"external-lib",
|
||||
CodeElementKind::Project,
|
||||
FilePath::new("external/Cargo.toml").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
project_graph.add_element(
|
||||
CodeElement::new(
|
||||
"myapp-domain",
|
||||
CodeElementKind::Project,
|
||||
FilePath::new("crates/domain/Cargo.toml").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
project_graph.add_relationship(
|
||||
Relationship::new("external-lib", "myapp-domain", RelationshipKind::Composition).unwrap(),
|
||||
);
|
||||
|
||||
graph.merge_project_edges(&project_graph);
|
||||
|
||||
// "External" (from external-lib) isn't a known module in graph → no edge
|
||||
assert!(graph.module_edges().is_empty());
|
||||
}
|
||||
|
||||
// ── Language::is_test_file ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn language_detects_python_test_prefix_files() {
|
||||
assert!(Language::Python.is_test_file(Path::new("src/test_orders.py")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_detects_python_test_suffix_files() {
|
||||
assert!(Language::Python.is_test_file(Path::new("src/orders_test.py")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_detects_rust_test_files() {
|
||||
assert!(Language::Rust.is_test_file(Path::new("src/orders_tests.rs")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_detects_files_in_tests_dir() {
|
||||
assert!(Language::Rust.is_test_file(Path::new("tests/integration.rs")));
|
||||
assert!(Language::Python.is_test_file(Path::new("tests/helpers.py")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_does_not_flag_regular_files_as_tests() {
|
||||
assert!(!Language::Rust.is_test_file(Path::new("src/orders.rs")));
|
||||
assert!(!Language::Python.is_test_file(Path::new("src/orders.py")));
|
||||
}
|
||||
|
||||
// ── AnalysisConfig::is_standard_excluded ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn analysis_config_identifies_standard_excluded_dirs() {
|
||||
use archlens_domain::AnalysisConfig;
|
||||
assert!(AnalysisConfig::is_standard_excluded(".venv"));
|
||||
assert!(AnalysisConfig::is_standard_excluded("node_modules"));
|
||||
assert!(AnalysisConfig::is_standard_excluded("target"));
|
||||
assert!(AnalysisConfig::is_standard_excluded("__pycache__"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analysis_config_does_not_exclude_source_dirs() {
|
||||
use archlens_domain::AnalysisConfig;
|
||||
assert!(!AnalysisConfig::is_standard_excluded("src"));
|
||||
assert!(!AnalysisConfig::is_standard_excluded("domain"));
|
||||
assert!(!AnalysisConfig::is_standard_excluded("application"));
|
||||
}
|
||||
|
||||
// ── Name normalization ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn normalize_cargo_package_converts_underscores_to_hyphens() {
|
||||
assert_eq!(normalize_cargo_package("myapp_domain"), "myapp-domain");
|
||||
assert_eq!(normalize_cargo_package("myapp-domain"), "myapp-domain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_python_package_lowercases_and_unifies_separators() {
|
||||
assert_eq!(normalize_python_package("My-Package"), "my_package");
|
||||
assert_eq!(normalize_python_package("my.package"), "my_package");
|
||||
assert_eq!(normalize_python_package("my_package"), "my_package");
|
||||
}
|
||||
Reference in New Issue
Block a user