refactor: five architectural deepening improvements
Candidate 1 (NormalizedGraph): qualify→resolve→filter is now a single
named operation returning a distinct type; raw CodeGraph cannot call
module_edges/subgraph_by_module — pipeline order enforced at compile time.
Candidate 2 (Use cases): GenerateDiagram, CheckFreshness, DiffDiagram
extracted to application/src/use_cases/; presentation is now a thin CLI
dispatcher (~100 lines less, three fewer local functions).
Candidate 3 (ExtractionContext): shared accumulator for both Rust and
Python extractors replaces parallel Vec<> + 4-arg passing chains.
Candidate 4 (ModuleAssignment): ModuleName::assign() returns
ModuleAssignment { Explicit | Inferred | Unresolved } instead of Option,
callers can distinguish resolution strategies.
Candidate 5 (SplitRenderer): append_cross_module_deps removed from
DiagramRenderer port; replaced by render_for_module() default impl —
port interface now reflects what all renderers actually share.
This commit is contained in:
@@ -1 +1,2 @@
|
||||
pub mod queries;
|
||||
pub mod use_cases;
|
||||
|
||||
@@ -4,7 +4,8 @@ use std::path::Path;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use archlens_domain::{
|
||||
AnalysisConfig, AnalysisWarning, CodeElement, CodeGraph, DomainError, ModuleName, Relationship,
|
||||
AnalysisConfig, AnalysisWarning, CodeElement, CodeGraph, DomainError, ModuleName,
|
||||
NormalizedGraph, Relationship,
|
||||
ports::{FileDiscovery, SourceAnalyzer},
|
||||
};
|
||||
|
||||
@@ -40,26 +41,22 @@ where
|
||||
.par_iter()
|
||||
.map(|file| match self.source_analyzer.analyze_file(file) {
|
||||
Ok(result) => {
|
||||
let module =
|
||||
ModuleName::from_path(file.path().as_str(), root, config.module_mappings());
|
||||
let assignment =
|
||||
ModuleName::assign(file.path().as_str(), root, config.module_mappings());
|
||||
let elements: Vec<CodeElement> = result
|
||||
.elements()
|
||||
.iter()
|
||||
.map(|el| {
|
||||
let mut el = el.clone();
|
||||
if el.module().is_none()
|
||||
&& let Some(ref m) = module
|
||||
&& let Some(m) = assignment.module_name()
|
||||
{
|
||||
el = el.with_module(m.clone());
|
||||
}
|
||||
el
|
||||
})
|
||||
.collect();
|
||||
(
|
||||
elements,
|
||||
result.relationships().to_vec(),
|
||||
result.warnings().to_vec(),
|
||||
)
|
||||
(elements, result.relationships().to_vec(), result.warnings().to_vec())
|
||||
}
|
||||
Err(err) => {
|
||||
let mut warnings = Vec::new();
|
||||
@@ -73,14 +70,14 @@ where
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut graph = CodeGraph::new();
|
||||
let mut raw = CodeGraph::new();
|
||||
let mut warnings = Vec::new();
|
||||
for (elements, relationships, warns) in file_results {
|
||||
for el in elements {
|
||||
graph.add_element(el);
|
||||
raw.add_element(el);
|
||||
}
|
||||
for rel in relationships {
|
||||
graph.add_relationship(rel);
|
||||
raw.add_relationship(rel);
|
||||
}
|
||||
warnings.extend(warns);
|
||||
}
|
||||
@@ -94,22 +91,19 @@ where
|
||||
.map(|s| s.to_lowercase())
|
||||
.collect();
|
||||
|
||||
let graph = graph
|
||||
.qualify()
|
||||
.resolve_relationships()
|
||||
.filter_external_imports(&known_dirs);
|
||||
let graph = NormalizedGraph::from_analyzed(raw, &known_dirs)?;
|
||||
|
||||
Ok(AnalyzeCodebaseResult { graph, warnings })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnalyzeCodebaseResult {
|
||||
graph: CodeGraph,
|
||||
graph: NormalizedGraph,
|
||||
warnings: Vec<AnalysisWarning>,
|
||||
}
|
||||
|
||||
impl AnalyzeCodebaseResult {
|
||||
pub fn graph(&self) -> &CodeGraph {
|
||||
pub fn graph(&self) -> &NormalizedGraph {
|
||||
&self.graph
|
||||
}
|
||||
|
||||
|
||||
18
crates/application/src/use_cases/check_freshness.rs
Normal file
18
crates/application/src/use_cases/check_freshness.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use archlens_domain::{DomainError, NormalizedGraph, ports::DiagramRenderer};
|
||||
|
||||
/// Compares the current rendered output against an on-disk file.
|
||||
/// Returns `Ok(true)` if up to date, `Ok(false)` if stale.
|
||||
pub struct CheckFreshness<'a> {
|
||||
pub graph: &'a NormalizedGraph,
|
||||
pub renderer: &'a dyn DiagramRenderer,
|
||||
pub existing_path: &'a std::path::Path,
|
||||
}
|
||||
|
||||
impl<'a> CheckFreshness<'a> {
|
||||
pub fn execute(&self) -> Result<bool, DomainError> {
|
||||
let rendered = self.renderer.render(self.graph.as_graph())?;
|
||||
let current = rendered.files().first().map(|f| f.content()).unwrap_or("");
|
||||
let existing = std::fs::read_to_string(self.existing_path).unwrap_or_default();
|
||||
Ok(current == existing)
|
||||
}
|
||||
}
|
||||
46
crates/application/src/use_cases/diff_diagram.rs
Normal file
46
crates/application/src/use_cases/diff_diagram.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use archlens_domain::{DomainError, NormalizedGraph, ports::DiagramRenderer};
|
||||
|
||||
/// The semantic result of comparing two diagram snapshots.
|
||||
pub struct DiffResult {
|
||||
pub added: Vec<String>,
|
||||
pub removed: Vec<String>,
|
||||
}
|
||||
|
||||
impl DiffResult {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.added.is_empty() && self.removed.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares the rendered current graph against an existing diagram file and
|
||||
/// returns which lines were added or removed.
|
||||
pub struct DiffDiagram<'a> {
|
||||
pub graph: &'a NormalizedGraph,
|
||||
pub renderer: &'a dyn DiagramRenderer,
|
||||
pub existing_path: &'a std::path::Path,
|
||||
}
|
||||
|
||||
impl<'a> DiffDiagram<'a> {
|
||||
pub fn execute(&self) -> Result<DiffResult, DomainError> {
|
||||
let rendered = self.renderer.render(self.graph.as_graph())?;
|
||||
let current = rendered.files().first().map(|f| f.content()).unwrap_or("");
|
||||
let existing = std::fs::read_to_string(self.existing_path).unwrap_or_default();
|
||||
|
||||
let current_lines: std::collections::HashSet<&str> = current.lines().collect();
|
||||
let existing_lines: std::collections::HashSet<&str> = existing.lines().collect();
|
||||
|
||||
let added: Vec<String> = current_lines
|
||||
.difference(&existing_lines)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.map(|l| format!("+ {l}"))
|
||||
.collect();
|
||||
|
||||
let removed: Vec<String> = existing_lines
|
||||
.difference(¤t_lines)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.map(|l| format!("- {l}"))
|
||||
.collect();
|
||||
|
||||
Ok(DiffResult { added, removed })
|
||||
}
|
||||
}
|
||||
125
crates/application/src/use_cases/generate_diagram.rs
Normal file
125
crates/application/src/use_cases/generate_diagram.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use archlens_domain::{
|
||||
BoundaryRule, DomainError, NormalizedGraph, RenderedFile, RenderOutput,
|
||||
check_boundary_rules,
|
||||
ports::DiagramRenderer,
|
||||
};
|
||||
|
||||
/// Result of running the generate use case — exposed violations and any output
|
||||
/// that should be written to disk.
|
||||
pub struct GenerateDiagramResult {
|
||||
pub violations: Vec<String>,
|
||||
pub output: RenderOutput,
|
||||
}
|
||||
|
||||
/// Orchestrates diagram generation: renders the graph (split or single),
|
||||
/// checks boundary rules, and returns the output for the caller to write.
|
||||
pub struct GenerateDiagram {
|
||||
pub graph: NormalizedGraph,
|
||||
pub renderer: Box<dyn DiagramRenderer>,
|
||||
pub allow_rules: Vec<BoundaryRule>,
|
||||
pub deny_rules: Vec<BoundaryRule>,
|
||||
pub split_by_module: bool,
|
||||
pub format_ext: String,
|
||||
pub output_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl GenerateDiagram {
|
||||
pub fn execute(self) -> Result<(), DomainError> {
|
||||
// Boundary rule checking
|
||||
let violations = if !self.allow_rules.is_empty() || !self.deny_rules.is_empty() {
|
||||
check_boundary_rules(self.graph.as_graph(), &self.allow_rules, &self.deny_rules)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Render and write
|
||||
if self.split_by_module {
|
||||
write_split(
|
||||
&self.graph,
|
||||
&*self.renderer,
|
||||
&self.output_dir,
|
||||
&self.format_ext,
|
||||
)?;
|
||||
} else {
|
||||
let rendered = self.renderer.render(self.graph.as_graph())?;
|
||||
write_to_output(rendered, &self.output_dir)?;
|
||||
}
|
||||
|
||||
// Report violations (after writing so the diagram is still produced)
|
||||
for v in &violations {
|
||||
eprintln!("RULE VIOLATION: {}", v.message());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_violations_only(&self) -> Vec<String> {
|
||||
if self.allow_rules.is_empty() && self.deny_rules.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
check_boundary_rules(self.graph.as_graph(), &self.allow_rules, &self.deny_rules)
|
||||
.into_iter()
|
||||
.map(|v| v.message())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_split(
|
||||
graph: &NormalizedGraph,
|
||||
renderer: &dyn DiagramRenderer,
|
||||
output_dir: &Option<PathBuf>,
|
||||
ext: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let dir = output_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
|
||||
let overview = renderer.render(graph.as_graph())?;
|
||||
let overview_file = RenderedFile::new(
|
||||
&format!("overview.{ext}"),
|
||||
overview.files().first().map(|f| f.content()).unwrap_or(""),
|
||||
)?;
|
||||
write_file_to_dir(&dir, overview_file)?;
|
||||
|
||||
for module in graph.modules() {
|
||||
let subgraph = graph.subgraph_by_module(&module);
|
||||
let cross_deps = graph.cross_module_deps_for(&module);
|
||||
let module_output = renderer.render_for_module(&subgraph, &module, &cross_deps)?;
|
||||
let module_file = RenderedFile::new(
|
||||
&format!("{}.{ext}", module.as_str().to_lowercase()),
|
||||
module_output.files().first().map(|f| f.content()).unwrap_or(""),
|
||||
)?;
|
||||
write_file_to_dir(&dir, module_file)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_file_to_dir(dir: &PathBuf, file: RenderedFile) -> Result<(), DomainError> {
|
||||
let path = dir.join(file.name());
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| DomainError::IoError(e.to_string()))?;
|
||||
std::fs::write(&path, file.content())
|
||||
.map_err(|e| DomainError::IoError(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_to_output(rendered: RenderOutput, output: &Option<PathBuf>) -> Result<(), DomainError> {
|
||||
let content = rendered.files().first().map(|f| f.content()).unwrap_or("");
|
||||
match output {
|
||||
Some(path) => {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| DomainError::IoError(e.to_string()))?;
|
||||
}
|
||||
std::fs::write(path, content)
|
||||
.map_err(|e| DomainError::IoError(e.to_string()))
|
||||
}
|
||||
None => {
|
||||
print!("{content}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
3
crates/application/src/use_cases/mod.rs
Normal file
3
crates/application/src/use_cases/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod check_freshness;
|
||||
pub mod diff_diagram;
|
||||
pub mod generate_diagram;
|
||||
Reference in New Issue
Block a user