refactor: move scattered business logic into domain
Some checks failed
CI / Check / Test (push) Failing after 44s
Architecture Docs / Generate diagrams (push) Successful in 3m21s

- 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:
2026-06-17 10:58:42 +02:00
parent 428faa957c
commit e26151b4a1
10 changed files with 262 additions and 87 deletions

View File

@@ -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:

View File

@@ -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,
};

View File

@@ -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 {

View File

@@ -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"),
}
}
}

View File

@@ -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(['-', '.'], "_")
}