refactor: five architectural deepening improvements
Some checks failed
CI / Check / Test (push) Failing after 43s
Architecture Docs / Generate diagrams (push) Successful in 3m20s

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:
2026-06-17 11:24:18 +02:00
parent b159cafc9d
commit fc8ad0ebc0
18 changed files with 614 additions and 511 deletions

View File

@@ -1,12 +1,11 @@
use std::collections::HashSet;
use tree_sitter::{Node, Parser};
use archlens_domain::{
AnalysisResult, AnalysisWarning, CodeElement, CodeElementKind, DomainError, FilePath,
Relationship, RelationshipKind,
AnalysisResult, CodeElement, CodeElementKind, DomainError, FilePath, Relationship,
RelationshipKind,
};
use crate::extraction_context::ExtractionContext;
use crate::language_extractor::LanguageExtractor;
pub struct PythonExtractor;
@@ -27,40 +26,16 @@ pub fn analyze(source: &str, file_path: &FilePath) -> Result<AnalysisResult, Dom
.parse(source, None)
.ok_or_else(|| DomainError::AnalysisError("failed to parse".to_string()))?;
let mut elements = Vec::new();
let mut relationships = Vec::new();
let mut warnings = Vec::new();
let mut type_names: HashSet<String> = HashSet::new();
let mut ctx = ExtractionContext::new(file_path.clone());
let root = tree.root_node();
collect_classes(
&root,
source,
file_path,
&mut elements,
&mut type_names,
&mut relationships,
&mut warnings,
);
collect_imports(&root, source, file_path, &mut relationships);
let relationships = relationships
.into_iter()
.map(|r| r.with_source_file(file_path.clone()))
.collect();
collect_classes(&root, source, &mut ctx);
collect_imports(&root, source, &mut ctx);
Ok(AnalysisResult::new(elements, relationships, warnings))
ctx.into_result()
}
fn collect_classes(
node: &Node,
source: &str,
file_path: &FilePath,
elements: &mut Vec<CodeElement>,
type_names: &mut HashSet<String>,
relationships: &mut Vec<Relationship>,
warnings: &mut Vec<AnalysisWarning>,
) {
fn collect_classes(node: &Node, source: &str, ctx: &mut ExtractionContext) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() != "class_definition" {
@@ -79,26 +54,21 @@ fn collect_classes(
.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.with_methods(methods));
}
match CodeElement::new(name, CodeElementKind::Class, ctx.file_path().clone(), line) {
Ok(element) => ctx.add_element(element.with_methods(methods)),
Err(e) => {
if let Ok(w) = AnalysisWarning::new(file_path.clone(), line, &e.to_string()) {
warnings.push(w);
}
ctx.add_warning(ctx.file_path().clone(), line, &e.to_string());
continue;
}
}
if let Some(superclasses) = child.child_by_field_name("superclasses") {
collect_inheritance(&superclasses, source, name, type_names, relationships);
collect_inheritance(&superclasses, source, name, ctx);
}
if let Some(body) = child.child_by_field_name("body") {
collect_typed_fields(&body, source, name, type_names, relationships);
collect_constructor_params(&body, source, name, type_names, relationships);
collect_typed_fields(&body, source, name, ctx);
collect_constructor_params(&body, source, name, ctx);
}
}
}
@@ -107,8 +77,7 @@ fn collect_inheritance(
superclasses: &Node,
source: &str,
class_name: &str,
_type_names: &HashSet<String>,
relationships: &mut Vec<Relationship>,
ctx: &mut ExtractionContext,
) {
let mut cursor = superclasses.walk();
for child in superclasses.children(&mut cursor) {
@@ -118,7 +87,7 @@ fn collect_inheritance(
&& let Ok(rel) =
Relationship::new(class_name, base_name, RelationshipKind::Inheritance)
{
relationships.push(rel);
ctx.add_relationship(rel);
}
}
}
@@ -228,16 +197,12 @@ fn is_external_import(module: &str) -> bool {
false
}
fn collect_imports(
node: &Node,
source: &str,
file_path: &FilePath,
relationships: &mut Vec<Relationship>,
) {
let file_name = std::path::Path::new(file_path.as_str())
fn collect_imports(node: &Node, source: &str, ctx: &mut ExtractionContext) {
let file_name = std::path::Path::new(ctx.file_path().as_str())
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
.unwrap_or("unknown")
.to_string();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
@@ -249,9 +214,9 @@ fn collect_imports(
let module = &source[name_child.byte_range()];
if !is_external_import(module)
&& let Ok(rel) =
Relationship::new(file_name, module, RelationshipKind::Import)
Relationship::new(&file_name, module, RelationshipKind::Import)
{
relationships.push(rel);
ctx.add_relationship(rel);
}
}
}
@@ -261,9 +226,9 @@ fn collect_imports(
let module = &source[module_node.byte_range()];
if !is_external_import(module)
&& let Ok(rel) =
Relationship::new(file_name, module, RelationshipKind::Import)
Relationship::new(&file_name, module, RelationshipKind::Import)
{
relationships.push(rel);
ctx.add_relationship(rel);
}
}
}
@@ -314,7 +279,6 @@ fn extract_python_params(params_node: &Node, source: &str) -> String {
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)
@@ -339,13 +303,7 @@ fn extract_python_params(params_node: &Node, source: &str) -> String {
parts.join(", ")
}
fn collect_constructor_params(
body: &Node,
source: &str,
class_name: &str,
_type_names: &HashSet<String>,
relationships: &mut Vec<Relationship>,
) {
fn collect_constructor_params(body: &Node, source: &str, class_name: &str, ctx: &mut ExtractionContext) {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() != "function_definition" {
@@ -373,30 +331,18 @@ fn collect_constructor_params(
&& let Ok(rel) =
Relationship::new(class_name, base_type, RelationshipKind::Composition)
{
relationships.push(rel);
ctx.add_relationship(rel);
}
}
}
}
}
fn collect_typed_fields(
body: &Node,
source: &str,
class_name: &str,
type_names: &HashSet<String>,
relationships: &mut Vec<Relationship>,
) {
collect_typed_fields_recursive(body, source, class_name, type_names, relationships);
fn collect_typed_fields(body: &Node, source: &str, class_name: &str, ctx: &mut ExtractionContext) {
collect_typed_fields_recursive(body, source, class_name, ctx);
}
fn collect_typed_fields_recursive(
node: &Node,
source: &str,
class_name: &str,
_type_names: &HashSet<String>,
relationships: &mut Vec<Relationship>,
) {
fn collect_typed_fields_recursive(node: &Node, source: &str, class_name: &str, ctx: &mut ExtractionContext) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if (child.kind() == "assignment" || child.kind() == "typed_assignment")
@@ -410,10 +356,10 @@ fn collect_typed_fields_recursive(
&& let Ok(rel) =
Relationship::new(class_name, base_type, RelationshipKind::Composition)
{
relationships.push(rel);
ctx.add_relationship(rel);
}
}
collect_typed_fields_recursive(&child, source, class_name, _type_names, relationships);
collect_typed_fields_recursive(&child, source, class_name, ctx);
}
}