mod cli;
mod format;
mod logger;
use clap::Parser;
use cli::{
CheckArgs, CheckOutputFormat, CrawkArgs, CrawkCommands, CyclesMode, DepsArgs, DepsOutputFormat,
ListArgs, ListOutputFormat, UseArgs, UseOutputFormat, WhyArgs, WhyOutputFormat,
};
use crawk::{
AnalysisOptions, Analyzer, AnnotatedEdges, CheckOptions, DependencyGraph,
DependencyGraphOptions, version,
};
use logger::configure_tracing;
use std::path::Path;
use tracing::info;
fn main() -> anyhow::Result<()> {
let command = CrawkArgs::parse();
let is_check = matches!(command.command, CrawkCommands::Check(_));
match run(&command) {
Ok(0) => Ok(()),
Ok(code) => exit_with(code),
Err(e) if is_check => {
eprintln!("Error: {e:#}");
exit_with(2)
}
Err(e) => Err(e),
}
}
fn exit_with(code: i32) -> ! {
#[allow(clippy::exit)]
std::process::exit(code)
}
fn run(command: &CrawkArgs) -> anyhow::Result<i32> {
configure_tracing(command)?;
let crate_root = command.crate_root()?;
info!("Running {} v{}", version::NAME, version::VERSION);
info!("Crate root: {}", crate_root.display());
let crate_root = crate_root.canonicalize()?;
match command.command {
CrawkCommands::Use(ref args) => handle_use_command(&crate_root, args)?,
CrawkCommands::List(ref args) => handle_list_command(&crate_root, args)?,
CrawkCommands::Deps(ref args) => handle_deps_command(&crate_root, args)?,
CrawkCommands::Why(ref args) => handle_why_command(&crate_root, args)?,
CrawkCommands::Check(ref args) => return handle_check_command(&crate_root, args),
}
Ok(0)
}
fn handle_check_command(crate_root: &Path, args: &CheckArgs) -> anyhow::Result<i32> {
let mut analyzer = Analyzer::new(crate_root)?;
let opts = CheckOptions {
config: args.config.clone(),
include_tests: args.include_tests,
show_apis: args.show_apis,
};
if args.init {
let outcome = analyzer.init_check_config(crate_root, &opts)?;
let rerun = args.config.as_ref().map_or_else(
|| "crawk check".to_owned(),
|cfg| format!("crawk check -c {}", cfg.display()),
);
eprintln!("Scaffolded {}.", outcome.path.display());
eprintln!();
eprintln!(" Reorder the modules: highest-level layer first, lowest last.");
eprintln!(" A lower layer must never depend on a higher one.");
if outcome.frozen_cycles > 0 {
let (noun, tail) = if outcome.frozen_cycles == 1 {
("loop", "untangle it and delete the entry")
} else {
("loops", "untangle them and delete the entries")
};
eprintln!(
" Froze {} existing dependency {noun} as `allow-cycle`; {tail}.",
outcome.frozen_cycles
);
}
eprintln!(" Then run `{rerun}`.");
return Ok(0);
}
let report = analyzer.check(crate_root, &opts)?;
if report.is_clean() {
info!("All architectural rules satisfied.");
return Ok(0);
}
let output = match args.format {
CheckOutputFormat::Plain => format::check_cmd::render_plain(&report),
};
print!("{output}");
Ok(report.exit_code())
}
fn handle_deps_command(crate_root: &Path, args: &DepsArgs) -> anyhow::Result<()> {
let mut analyzer = Analyzer::new(crate_root)?;
let mut graph_opts = DependencyGraphOptions::default();
graph_opts.include_tests = args.include_tests;
graph_opts.depth = if args.path.is_some() {
None
} else {
args.depth
};
graph_opts.show_apis = args.show_apis;
let graph = analyzer.dependency_graph(&graph_opts)?;
let output = if let Some(ref pair) = args.path {
render_path_output(&graph, &pair[0], &pair[1], args)?
} else if args.orphans {
render_orphans_output(&graph)
} else if let Some(ref cycles_mode) = args.cycles {
render_cycles_output(&graph, cycles_mode, args)
} else {
render_deps_output(graph.edges(), &args.format)
};
if output.is_empty() {
if args.orphans {
eprintln!("No orphan modules found.");
} else if args.cycles.is_some() {
eprintln!("No dependency cycles found.");
} else {
info!("No inter-module dependencies found.");
}
} else {
print!("{output}");
}
Ok(())
}
fn render_path_output(
graph: &DependencyGraph,
src: &str,
tgt: &str,
args: &DepsArgs,
) -> anyhow::Result<String> {
let sp = graph.shortest_paths(src, tgt)?;
if sp.is_empty() {
eprintln!("No path from {src} to {tgt}.");
return Ok(String::new());
}
info!(
"Found {} shortest path(s) of length {}.",
sp.paths.len(),
sp.length().unwrap_or(0)
);
let resolved = sp.truncated(args.depth);
Ok(match args.format {
DepsOutputFormat::Plain => format::paths::render_paths_plain(&resolved),
DepsOutputFormat::Grouped => format::paths::render_paths_grouped(&resolved),
DepsOutputFormat::Dot => {
format::paths::render_paths_dot(&graph.truncated_edges(args.depth), &resolved)
}
})
}
fn render_orphans_output(graph: &DependencyGraph) -> String {
let orphans = graph.orphans();
if orphans.is_empty() {
return String::new();
}
info!("Found {} orphan module(s).", orphans.len());
format::orphans::render_orphans(&orphans)
}
fn render_cycles_output(
graph: &DependencyGraph,
cycles_mode: &CyclesMode,
args: &DepsArgs,
) -> String {
let cycles = graph.cycles();
if cycles.is_empty() {
return String::new();
}
info!("Found {} dependency cycle(s).", cycles.len());
if *cycles_mode == CyclesMode::Highlight && args.format != DepsOutputFormat::Dot {
eprintln!(
"warning: --cycles highlight has no effect with {} format, showing cycles only",
args.format
);
}
match (&args.format, cycles_mode) {
(DepsOutputFormat::Plain, _) => format::cycles::render_cycles_plain(&cycles),
(DepsOutputFormat::Grouped, _) => format::cycles::render_cycles_grouped(&cycles),
(DepsOutputFormat::Dot, CyclesMode::Detect) => format::cycles::render_cycles_dot(&cycles),
(DepsOutputFormat::Dot, CyclesMode::Highlight) => {
format::cycles::render_cycles_dot_highlight(&cycles, graph.edges())
}
}
}
fn render_deps_output(edges: &AnnotatedEdges, format: &DepsOutputFormat) -> String {
match format {
DepsOutputFormat::Plain => format::deps_cmd::render_plain(edges),
DepsOutputFormat::Grouped => format::deps_cmd::render_grouped(edges),
DepsOutputFormat::Dot => format::deps_cmd::render_dot(edges),
}
}
fn handle_list_command(crate_root: &Path, args: &ListArgs) -> anyhow::Result<()> {
let mut analyzer = Analyzer::new(crate_root)?;
let (mut modules, is_all_targets) = if let Some(ref module_path) = args.module_path {
info!("Listing modules from: {module_path}");
let mods = analyzer.list_modules(module_path, args.include_tests)?;
(mods, false)
} else {
info!("Listing all targets");
let mods = analyzer.list_all_modules(args.include_tests)?;
(mods, true)
};
let multi_target = args.display.show_targets || {
is_all_targets && {
let distinct_targets = modules
.iter()
.map(crawk::ModuleInfo::target)
.collect::<std::collections::HashSet<_>>()
.len();
distinct_targets > 1
}
};
if let Some(depth) = args.depth {
modules.retain(|m| m.path().matches("::").count() < depth);
}
if let Some(ref filter) = args.filter {
modules.retain(|m| m.path().contains(filter.as_str()));
}
if modules.is_empty() {
info!("No modules found.");
} else {
let display_opts = format::list_cmd::ListDisplayOptions {
show_source: args.display.show_source,
show_visibility: args.display.show_visibility,
multi_target,
};
let output = match args.format {
ListOutputFormat::Plain => {
format::list_cmd::render_list_plain(&modules, &display_opts, crate_root)
}
ListOutputFormat::Table => {
format::list_cmd::render_list_table(&modules, &display_opts, crate_root)
}
};
print!("{output}");
}
Ok(())
}
fn handle_why_command(crate_root: &Path, args: &WhyArgs) -> anyhow::Result<()> {
let mut analyzer = Analyzer::new(crate_root)?;
let options = AnalysisOptions {
recursive: args.recursive,
include_tests: args.include_tests,
expand_groups: true,
resolve_globs: false,
};
let refs = analyzer.explain_dependency(&args.source, &args.target, &options)?;
if refs.is_empty() {
info!("No references from '{}' to '{}'.", args.source, args.target);
} else {
let output = match args.format {
WhyOutputFormat::Plain => format::why_cmd::render_plain(&refs),
WhyOutputFormat::Grouped => format::why_cmd::render_grouped(&refs),
};
print!("{output}");
}
Ok(())
}
fn handle_use_command(crate_root: &Path, args: &UseArgs) -> anyhow::Result<()> {
let mut analyzer = Analyzer::new(crate_root)?;
let options = AnalysisOptions {
recursive: args.recursive,
include_tests: args.include_tests,
expand_groups: args.expand,
resolve_globs: args.resolve_globs,
};
let result = analyzer.analyze_module(&args.module_path, &options)?;
info!("Module file: {}", result.source_file().display());
if result.is_empty() {
info!("No internal crate use statements found.");
} else {
let output = match args.format {
UseOutputFormat::Plain => format::use_cmd::render_flat(&result, args.depth),
UseOutputFormat::Grouped => format::use_cmd::render_grouped(&result, args.depth),
};
print!("{output}");
}
Ok(())
}