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:
@@ -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,
|
||||
|
||||
@@ -270,7 +270,14 @@ fn extract_methods(root: &Node, source: &str, type_name: &str) -> Vec<String> {
|
||||
} else {
|
||||
"-"
|
||||
};
|
||||
methods.push(format!("{vis}{fn_name}()"));
|
||||
let params = extract_fn_params(&item, source);
|
||||
let ret = extract_fn_return(&item, source);
|
||||
let sig = if ret.is_empty() {
|
||||
format!("{vis}{fn_name}({params})")
|
||||
} else {
|
||||
format!("{vis}{fn_name}({params}) -> {ret}")
|
||||
};
|
||||
methods.push(sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,6 +285,38 @@ fn extract_methods(root: &Node, source: &str, type_name: &str) -> Vec<String> {
|
||||
methods
|
||||
}
|
||||
|
||||
fn extract_fn_params(fn_item: &Node, source: &str) -> String {
|
||||
let Some(params_node) = fn_item.child_by_field_name("parameters") else {
|
||||
return String::new();
|
||||
};
|
||||
let mut parts = Vec::new();
|
||||
let mut cursor = params_node.walk();
|
||||
for param in params_node.children(&mut cursor) {
|
||||
match param.kind() {
|
||||
"parameter" => {
|
||||
if let (Some(pat), Some(ty)) = (
|
||||
param.child_by_field_name("pattern"),
|
||||
param.child_by_field_name("type"),
|
||||
) {
|
||||
let name = &source[pat.byte_range()];
|
||||
let ty_text = source[ty.byte_range()].trim();
|
||||
parts.push(format!("{name}: {ty_text}"));
|
||||
}
|
||||
}
|
||||
"self_parameter" | "&self" | "self" => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
parts.join(", ")
|
||||
}
|
||||
|
||||
fn extract_fn_return(fn_item: &Node, source: &str) -> String {
|
||||
fn_item
|
||||
.child_by_field_name("return_type")
|
||||
.map(|n| source[n.byte_range()].trim().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn collect_mod_declarations(
|
||||
node: &Node,
|
||||
source: &str,
|
||||
|
||||
Reference in New Issue
Block a user