feat: implement all P1/P2/P3/P4 improvements from issue backlog
Some checks failed
CI / Check / Test (push) Failing after 1m33s
Architecture Docs / Generate diagrams (push) Successful in 3m21s

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:
2026-06-17 09:50:50 +02:00
parent 27197062eb
commit fdd85011a4
42 changed files with 2767 additions and 92 deletions

View File

@@ -74,10 +74,15 @@ fn collect_classes(
let name = &source[name_node.byte_range()];
let line = child.start_position().row + 1;
let methods = child
.child_by_field_name("body")
.map(|body| collect_methods(&body, source))
.unwrap_or_default();
match CodeElement::new(name, CodeElementKind::Class, file_path.clone(), line) {
Ok(element) => {
type_names.insert(name.to_string());
elements.push(element);
elements.push(element.with_methods(methods));
}
Err(e) => {
if let Ok(w) = AnalysisWarning::new(file_path.clone(), line, &e.to_string()) {
@@ -267,6 +272,73 @@ fn collect_imports(
}
}
fn collect_methods(body: &Node, source: &str) -> Vec<String> {
let mut methods = Vec::new();
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() != "function_definition" {
continue;
}
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let fn_name = &source[name_node.byte_range()];
if fn_name.starts_with('_') {
continue;
}
let params = child
.child_by_field_name("parameters")
.map(|p| extract_python_params(&p, source))
.unwrap_or_default();
let ret = child
.child_by_field_name("return_type")
.map(|n| source[n.byte_range()].trim().to_string())
.unwrap_or_default();
let sig = if ret.is_empty() {
format!("+{fn_name}({params})")
} else {
format!("+{fn_name}({params}) -> {ret}")
};
methods.push(sig);
}
methods
}
fn extract_python_params(params_node: &Node, source: &str) -> String {
let mut parts = Vec::new();
let mut cursor = params_node.walk();
for param in params_node.children(&mut cursor) {
match param.kind() {
"typed_parameter" => {
if let Some(type_node) = param.child_by_field_name("type") {
// name is the first identifier child (not a named field)
let mut inner = param.walk();
let name = param
.children(&mut inner)
.find(|c| c.kind() == "identifier")
.map(|c| &source[c.byte_range()])
.unwrap_or_default();
if name != "self" && name != "cls" && !name.is_empty() {
let ty = source[type_node.byte_range()].trim();
parts.push(format!("{name}: {ty}"));
}
}
}
"identifier" => {
let name = &source[param.byte_range()];
if name != "self" && name != "cls" {
parts.push(name.to_string());
}
}
_ => {}
}
}
parts.join(", ")
}
fn collect_constructor_params(
body: &Node,
source: &str,