omnicoach::architecture_component!(Bin);
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
use omnicoach::code_lint::graph::{
GraphOptions, OutputFormat as GraphOutputFormat, ProjectDepth, generate_graph,
};
use omnicoach::code_lint::runner::{LintOptions, run_code_lint};
use omnicoach::command_lint::runner::run_command_lint;
use omnicoach::config::Config;
use omnicoach::diagnostic::{OutputFormat, print_diagnostics};
use omnicoach::rule_catalog::{DiscoveryArgs, code_report_footer, explain_footer};
use omnicoach::rule_selection::{ConfigOverrides, load_config};
fn main() {
if let Err(error) = run() {
eprintln!("Error: {error}");
std::process::exit(2);
}
}
fn run() -> anyhow::Result<()> {
let cli = Cli::parse();
if let Some(output) = cli.discovery.render(&cli.config_overrides) {
print!("{}", output?);
return Ok(());
}
let config = load_config(&cli.config_overrides)
.map_err(|error| anyhow::anyhow!("Failed to load configuration: {error}"))?;
match cli.subcommand {
Some(DomainCommand::Code(args)) => {
let options = LintOptions {
paths: args.paths,
diff: args.diff || cli.diff,
diff_rev: args.diff_rev.or(cli.diff_rev),
};
run_code(&options, &config, cli.format)
}
None => {
let options = LintOptions {
paths: vec![PathBuf::from(".")],
diff: cli.diff,
diff_rev: cli.diff_rev,
};
run_code(&options, &config, cli.format)
}
Some(DomainCommand::Command(args)) => {
let Some(command) = args.cmd else {
unreachable!("clap requires --cmd unless a discovery flag is given");
};
run_command(&command, &config, cli.format)
}
Some(DomainCommand::Graph(args)) => {
let options = GraphOptions {
paths: args.paths,
depth: match args.depth {
GraphDepthArg::Module => ProjectDepth::ModuleOnly,
GraphDepthArg::Public => ProjectDepth::PublicSurface,
GraphDepthArg::Full => ProjectDepth::Full,
},
format: if args.no_signatures {
GraphOutputFormat::CompactBi
} else {
GraphOutputFormat::AnnotatedStubs
},
};
let output = generate_graph(&options, &config)?;
if let Err(error) =
std::io::Write::write_all(&mut std::io::stdout().lock(), output.as_bytes())
&& error.kind() != std::io::ErrorKind::BrokenPipe
{
return Err(error.into());
}
Ok(())
}
}
}
fn run_code(options: &LintOptions, config: &Config, format: OutputFormat) -> anyhow::Result<()> {
let report = run_code_lint(options, config)?;
if !report.diagnostics.is_empty() {
print_diagnostics(&report.diagnostics, format)?;
if format == OutputFormat::Plain {
println!(
"{}",
code_report_footer(env!("CARGO_BIN_NAME"), &report.diagnostics)
);
}
}
for unreadable in &report.unreadable_files {
eprintln!(
"Error: Failed to read file '{}': {}",
unreadable.path.display(),
unreadable.error
);
}
if !report.unreadable_files.is_empty() {
std::process::exit(2);
}
if !report.diagnostics.is_empty() {
std::process::exit(1);
}
if format == OutputFormat::Plain {
println!("All checks passed!");
}
Ok(())
}
fn run_command(command: &str, config: &Config, format: OutputFormat) -> anyhow::Result<()> {
let diagnostics = run_command_lint(command, config);
if !diagnostics.is_empty() {
print_diagnostics(&diagnostics, format)?;
if format == OutputFormat::Plain {
println!("{}", explain_footer(env!("CARGO_BIN_NAME")));
}
std::process::exit(1);
}
Ok(())
}
#[derive(Parser, Debug)]
#[command(
name = "omnicoach",
version,
about = "Static code analysis and command workflow coach"
)]
struct Cli {
#[command(subcommand)]
subcommand: Option<DomainCommand>,
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Plain)]
format: OutputFormat,
#[arg(long)]
diff: bool,
#[arg(long)]
diff_rev: Option<String>,
#[command(flatten)]
config_overrides: ConfigOverrides,
#[command(flatten)]
discovery: DiscoveryArgs,
}
#[derive(Subcommand, Debug)]
enum DomainCommand {
Code(CodeArgs),
Command(CommandArgs),
Graph(GraphArgs),
}
#[derive(Args, Debug)]
struct CodeArgs {
#[arg(default_value = ".")]
paths: Vec<PathBuf>,
#[arg(long)]
diff: bool,
#[arg(long)]
diff_rev: Option<String>,
}
#[derive(Args, Debug)]
struct CommandArgs {
#[arg(long, required_unless_present_any = ["list_rules", "list_tags", "explain"])]
cmd: Option<String>,
}
#[derive(Args, Debug)]
struct GraphArgs {
#[arg(default_value = ".")]
paths: Vec<PathBuf>,
#[arg(long, value_enum, default_value_t = GraphDepthArg::Full)]
depth: GraphDepthArg,
#[arg(long)]
no_signatures: bool,
}
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
enum GraphDepthArg {
Module,
Public,
#[default]
Full,
}