refactor: deepen modules, consolidate inference, delete dead code

- Extract build_graph/load_config/create_renderer in presentation (393→~250 lines)
- Move module inference into ModuleName::from_path(), delete 3 scattered copies
- Move resolve_relationships/filter_external_imports into CodeGraph
- Add LanguageExtractor trait in tree-sitter adapter
- Add CodeGraph::elements_by_module(), replace 6 identical grouping loops
- Delete dead RenderDiagrams query
This commit is contained in:
2026-06-16 16:34:41 +02:00
parent dc8ecd983a
commit d28b00c697
15 changed files with 322 additions and 460 deletions

View File

@@ -8,7 +8,7 @@ use archlens_application::queries::AnalyzeCodebase;
use archlens_ascii::AsciiRenderer;
use archlens_cargo_workspace::CargoWorkspaceAnalyzer;
use archlens_domain::{
CodeGraph, DiagramLevel,
CodeGraph, DiagramLevel, ModuleName,
ports::{ConfigLoader, OutputWriter, ProjectAnalyzer},
};
use archlens_file_writer::FileOutputWriter;
@@ -30,21 +30,43 @@ pub fn run(args: Cli) -> Result<()> {
}
init_tracing(args.verbose);
let config_loader = match &args.config {
Some(path) => TomlConfigLoader::from_path(std::path::Path::new(path))?,
let level = parse_level(&args.level);
let graph = build_graph(&args, level)?;
let renderer = create_renderer(&args.format, level)?;
let ext = format_extension(&args.format);
if args.check {
return check_freshness(&args.output, &graph, &*renderer);
}
if args.split_by_module {
write_split(&graph, &*renderer, &args.output, ext)?;
} else {
write_single(&graph, &*renderer, &args.output)?;
}
Ok(())
}
fn load_config(args: &Cli) -> Result<TomlConfigLoader> {
match &args.config {
Some(path) => Ok(TomlConfigLoader::from_path(std::path::Path::new(path))?),
None => {
let default_path = args.path.join("archlens.toml");
if default_path.exists() {
TomlConfigLoader::from_path(&default_path)?
Ok(TomlConfigLoader::from_path(&default_path)?)
} else {
TomlConfigLoader::default()
Ok(TomlConfigLoader::default())
}
}
};
}
}
fn build_graph(args: &Cli, level: DiagramLevel) -> Result<CodeGraph> {
let config_loader = load_config(args)?;
let mut analysis_config = config_loader.load_analysis_config()?;
let level = parse_level(&args.level);
analysis_config = analysis_config.with_level(level);
if let Some(ref scope) = args.scope {
analysis_config = analysis_config.with_scope(scope.clone());
}
@@ -54,79 +76,80 @@ pub fn run(args: Cli) -> Result<()> {
analysis_config = analysis_config.with_excludes(excludes);
}
let graph = if level == DiagramLevel::Project {
let project_analyzer = CargoWorkspaceAnalyzer::new();
project_analyzer.analyze(&args.path)?
} else {
let discovery = WalkdirDiscovery::new();
let analyzer = TreeSitterAnalyzer::new();
let analyze = AnalyzeCodebase::new(discovery, analyzer);
let result = analyze.execute(&args.path, &analysis_config)?;
if level == DiagramLevel::Project {
return Ok(CargoWorkspaceAnalyzer::new().analyze(&args.path)?);
}
if !result.warnings().is_empty() {
for warning in result.warnings() {
eprintln!(
"WARNING: {}:{} {}",
warning.file_path().as_str(),
warning.line(),
warning.message()
);
}
if args.strict {
bail!(
"analysis produced {} warning(s) in strict mode",
result.warnings().len()
);
}
let discovery = WalkdirDiscovery::new();
let analyzer = TreeSitterAnalyzer::new();
let analyze = AnalyzeCodebase::new(discovery, analyzer);
let result = analyze.execute(&args.path, &analysis_config)?;
if !result.warnings().is_empty() {
for warning in result.warnings() {
eprintln!(
"WARNING: {}:{} {}",
warning.file_path().as_str(),
warning.line(),
warning.message()
);
}
let mut graph = result.graph().clone();
if level == DiagramLevel::Module {
let workspace_toml = args.path.join("Cargo.toml");
if workspace_toml.exists()
&& let Ok(project_graph) = CargoWorkspaceAnalyzer::new().analyze(&args.path)
{
merge_project_deps_as_module_edges(&mut graph, &project_graph);
}
if args.strict {
bail!(
"analysis produced {} warning(s) in strict mode",
result.warnings().len()
);
}
}
graph
};
let mut graph = result.graph().clone();
let renderer: Box<dyn archlens_domain::ports::DiagramRenderer> = match &args.format[..] {
"mermaid" => Box::new(MermaidRenderer::with_level(level)),
"ascii" => Box::new(AsciiRenderer::new()),
if level == DiagramLevel::Module {
let workspace_toml = args.path.join("Cargo.toml");
if workspace_toml.exists()
&& let Ok(project_graph) = CargoWorkspaceAnalyzer::new().analyze(&args.path)
{
merge_project_deps_as_module_edges(&mut graph, &project_graph);
}
}
Ok(graph)
}
fn create_renderer(
format: &str,
level: DiagramLevel,
) -> Result<Box<dyn archlens_domain::ports::DiagramRenderer>> {
match format {
"mermaid" => Ok(Box::new(MermaidRenderer::with_level(level))),
"ascii" => Ok(Box::new(AsciiRenderer::new())),
fmt => bail!("unknown format: {fmt}"),
};
}
}
let ext = match &args.format[..] {
fn format_extension(format: &str) -> &str {
match format {
"mermaid" => "mmd",
_ => "txt",
}
}
fn check_freshness(
output: &Option<String>,
graph: &CodeGraph,
renderer: &dyn archlens_domain::ports::DiagramRenderer,
) -> Result<()> {
let Some(path) = output else {
bail!("--check requires --output to specify the file to check against");
};
if args.check {
if let Some(ref path) = args.output {
let output = renderer.render(&graph)?;
let current = output.files().first().map(|f| f.content()).unwrap_or("");
let existing = std::fs::read_to_string(path).unwrap_or_default();
if current != existing {
eprintln!("Architecture diagram is outdated: {path}");
std::process::exit(1);
}
println!("Architecture diagram is up to date.");
return Ok(());
} else {
bail!("--check requires --output to specify the file to check against");
}
let rendered = renderer.render(graph)?;
let current = rendered.files().first().map(|f| f.content()).unwrap_or("");
let existing = std::fs::read_to_string(path).unwrap_or_default();
if current != existing {
eprintln!("Architecture diagram is outdated: {path}");
std::process::exit(1);
}
if args.split_by_module {
write_split(&graph, &*renderer, &args.output, ext)?;
} else {
write_single(&graph, &*renderer, &args.output)?;
}
println!("Architecture diagram is up to date.");
Ok(())
}
@@ -213,8 +236,8 @@ fn merge_project_deps_as_module_edges(
let tgt_module = crate_to_module.get(rel.target());
if let (Some(src), Some(tgt)) = (src_module, tgt_module) {
let src_cap = capitalize(src);
let tgt_cap = capitalize(tgt);
let src_cap = ModuleName::capitalize(src);
let tgt_cap = ModuleName::capitalize(tgt);
if src_cap != tgt_cap
&& graph_modules.contains(&src_cap)
@@ -231,62 +254,12 @@ fn merge_project_deps_as_module_edges(
}
}
fn capitalize(s: &str) -> String {
s.split('-')
.map(|seg| {
if seg.is_empty() {
String::new()
} else {
format!("{}{}", seg[..1].to_uppercase(), &seg[1..])
}
})
.collect::<Vec<_>>()
.join("-")
}
fn run_diff(args: &Cli, existing_path: &std::path::Path) -> Result<()> {
init_tracing(args.verbose);
let config_loader = match &args.config {
Some(path) => TomlConfigLoader::from_path(std::path::Path::new(path))?,
None => {
let default_path = args.path.join("archlens.toml");
if default_path.exists() {
TomlConfigLoader::from_path(&default_path)?
} else {
TomlConfigLoader::default()
}
}
};
let mut analysis_config = config_loader.load_analysis_config()?;
let level = parse_level(&args.level);
analysis_config = analysis_config.with_level(level);
let graph = if level == DiagramLevel::Project {
CargoWorkspaceAnalyzer::new().analyze(&args.path)?
} else {
let discovery = WalkdirDiscovery::new();
let analyzer = TreeSitterAnalyzer::new();
let analyze = AnalyzeCodebase::new(discovery, analyzer);
let result = analyze.execute(&args.path, &analysis_config)?;
let mut graph = result.graph().clone();
if level == DiagramLevel::Module {
let workspace_toml = args.path.join("Cargo.toml");
if workspace_toml.exists()
&& let Ok(project_graph) = CargoWorkspaceAnalyzer::new().analyze(&args.path)
{
merge_project_deps_as_module_edges(&mut graph, &project_graph);
}
}
graph
};
let renderer: Box<dyn archlens_domain::ports::DiagramRenderer> = match &args.format[..] {
"mermaid" => Box::new(MermaidRenderer::with_level(level)),
"ascii" => Box::new(AsciiRenderer::new()),
fmt => bail!("unknown format: {fmt}"),
};
let graph = build_graph(args, level)?;
let renderer = create_renderer(&args.format, level)?;
let output = renderer.render(&graph)?;
let current = output.files().first().map(|f| f.content()).unwrap_or("");