feat: implement all P1/P2/P3/P4 improvements from issue backlog
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>)
This commit is contained in:
@@ -7,6 +7,7 @@ use archlens_domain::{
|
||||
|
||||
pub struct MermaidRenderer {
|
||||
level: DiagramLevel,
|
||||
show_weights: bool,
|
||||
}
|
||||
|
||||
impl Default for MermaidRenderer {
|
||||
@@ -19,11 +20,24 @@ impl MermaidRenderer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
level: DiagramLevel::Type,
|
||||
show_weights: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_level(level: DiagramLevel) -> Self {
|
||||
Self { level }
|
||||
Self {
|
||||
level,
|
||||
show_weights: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_weights(mut self, show: bool) -> Self {
|
||||
self.show_weights = show;
|
||||
self
|
||||
}
|
||||
|
||||
fn display_name(qualified: &str) -> &str {
|
||||
qualified.split("::").last().unwrap_or(qualified)
|
||||
}
|
||||
|
||||
fn format_element_name(element: &CodeElement) -> String {
|
||||
@@ -89,7 +103,9 @@ impl MermaidRenderer {
|
||||
RelationshipKind::Composition => "-->",
|
||||
RelationshipKind::Import => "..>",
|
||||
};
|
||||
let key = format!("{} {} {}", rel.source(), arrow, rel.target());
|
||||
let src = Self::display_name(rel.source());
|
||||
let tgt = Self::display_name(rel.target());
|
||||
let key = format!("{} {} {}", src, arrow, tgt);
|
||||
if rel_seen.insert(key.clone()) {
|
||||
lines.push(format!(" {key}"));
|
||||
}
|
||||
@@ -136,10 +152,15 @@ impl MermaidRenderer {
|
||||
|
||||
for element in graph.elements() {
|
||||
if let Some(module) = element.module() {
|
||||
// Index both bare name and qualified name for lookup
|
||||
name_to_modules
|
||||
.entry(element.name())
|
||||
.or_default()
|
||||
.insert(module.as_str());
|
||||
name_to_modules
|
||||
.entry(element.qualified_name())
|
||||
.or_default()
|
||||
.insert(module.as_str());
|
||||
modules.insert(module.as_str().to_string());
|
||||
|
||||
let file_stem = std::path::Path::new(element.file_path().as_str())
|
||||
@@ -156,7 +177,7 @@ impl MermaidRenderer {
|
||||
lines.push(format!(" {module}[{module}]"));
|
||||
}
|
||||
|
||||
let mut module_edges: HashSet<(String, String)> = HashSet::new();
|
||||
let mut module_edges: HashMap<(String, String), usize> = HashMap::new();
|
||||
for rel in graph.relationships() {
|
||||
match rel.kind() {
|
||||
RelationshipKind::Import => {
|
||||
@@ -168,7 +189,7 @@ impl MermaidRenderer {
|
||||
&& modules.contains(&target_mod)
|
||||
&& *src != target_mod
|
||||
{
|
||||
module_edges.insert((src.clone(), target_mod));
|
||||
*module_edges.entry((src.clone(), target_mod)).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -176,7 +197,9 @@ impl MermaidRenderer {
|
||||
&& modules.contains(rel.target())
|
||||
&& rel.source() != rel.target()
|
||||
{
|
||||
module_edges.insert((rel.source().to_string(), rel.target().to_string()));
|
||||
*module_edges
|
||||
.entry((rel.source().to_string(), rel.target().to_string()))
|
||||
.or_insert(0) += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -190,7 +213,9 @@ impl MermaidRenderer {
|
||||
}
|
||||
for tgt_mod in tgt_set {
|
||||
if src_mod != tgt_mod {
|
||||
module_edges.insert((src_mod.to_string(), tgt_mod.to_string()));
|
||||
*module_edges
|
||||
.entry((src_mod.to_string(), tgt_mod.to_string()))
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,8 +224,18 @@ impl MermaidRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
for (source, target) in &module_edges {
|
||||
lines.push(format!(" {source} --> {target}"));
|
||||
for ((source, target), count) in &module_edges {
|
||||
let arrow = if self.show_weights {
|
||||
let label = if *count == 1 {
|
||||
r#"|"1 dep"|"#.to_string()
|
||||
} else {
|
||||
format!(r#"|"{count} deps"|"#)
|
||||
};
|
||||
format!("--{label}")
|
||||
} else {
|
||||
"-->".to_string()
|
||||
};
|
||||
lines.push(format!(" {source} {arrow} {target}"));
|
||||
}
|
||||
|
||||
lines.join("\n")
|
||||
@@ -249,4 +284,43 @@ impl DiagramRenderer for MermaidRenderer {
|
||||
let file = RenderedFile::new("diagram.mmd", &content)?;
|
||||
Ok(RenderOutput::single(file))
|
||||
}
|
||||
|
||||
fn append_cross_module_deps(
|
||||
&self,
|
||||
content: &str,
|
||||
module: &ModuleName,
|
||||
deps: &[(ModuleName, usize)],
|
||||
) -> String {
|
||||
if deps.is_empty() {
|
||||
return content.to_string();
|
||||
}
|
||||
|
||||
let src_id = format!(
|
||||
"{}_module",
|
||||
module.as_str().to_lowercase().replace('-', "_")
|
||||
);
|
||||
let mut extra = format!(
|
||||
" class {src_id}[\"{}\"] {{\n <<module>>\n }}\n",
|
||||
module.as_str()
|
||||
);
|
||||
|
||||
for (dep_mod, count) in deps {
|
||||
let dep_id = format!(
|
||||
"{}_module",
|
||||
dep_mod.as_str().to_lowercase().replace('-', "_")
|
||||
);
|
||||
extra.push_str(&format!(
|
||||
" class {dep_id}[\"{}\"] {{\n <<module>>\n }}\n",
|
||||
dep_mod.as_str()
|
||||
));
|
||||
let label = if *count == 1 {
|
||||
"1 dep".to_string()
|
||||
} else {
|
||||
format!("{count} deps")
|
||||
};
|
||||
extra.push_str(&format!(" {src_id} --> {dep_id} : {label}\n"));
|
||||
}
|
||||
|
||||
format!("{content}\n{extra}")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user