use crate::analyzers::{DeterministicAnalyzer, ExternalAnalyzer};
use crate::config::Config;
use crate::core::CheckRegistry;
use crate::discovery::discover_python_files;
use crate::integrations::builtin_checkers;
use crate::reporters::{render, ReportKind};
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand, ValueEnum};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
pub const EXIT_ISSUES: u8 = 1;
pub const EXIT_ERROR: u8 = 2;
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum ReportFormat {
#[default]
Human,
Json,
#[cfg(feature = "sarif")]
Sarif,
}
impl From<ReportFormat> for ReportKind {
fn from(format: ReportFormat) -> Self {
match format {
ReportFormat::Human => ReportKind::Human,
ReportFormat::Json => ReportKind::Json,
#[cfg(feature = "sarif")]
ReportFormat::Sarif => ReportKind::Sarif,
}
}
}
#[derive(Parser, Debug)]
#[command(
name = "zerum",
version,
about = "Deterministic code governance for Python"
)]
pub struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Check {
path: PathBuf,
#[arg(long, value_enum, default_value_t = ReportFormat::Human)]
format: ReportFormat,
#[arg(long)]
profile: Option<String>,
#[arg(long, value_delimiter = ',')]
with_external: Vec<String>,
},
Explain { id: String },
Init {
#[arg(long)]
strict: bool,
},
ListChecks,
ListCheckers,
}
impl Cli {
pub fn run(self) -> Result<ExitCode> {
match self.command {
Commands::Check {
path,
format,
profile,
with_external,
} => run_check(&path, format, profile.as_deref(), with_external),
Commands::Explain { id } => run_explain(&id),
Commands::Init { strict } => run_init(strict),
Commands::ListChecks => run_list_checks(),
Commands::ListCheckers => run_list_checkers(),
}
}
}
fn apply_profile_override(mut config: Config, profile: Option<&str>) -> Config {
if let Some(name) = profile {
config.profile.name = name.to_string();
}
config
}
fn run_check(
path: &Path,
format: ReportFormat,
profile: Option<&str>,
with_external: Vec<String>,
) -> Result<ExitCode> {
let mut config = Config::discover(path)?;
config = apply_profile_override(config, profile);
let files = discover_python_files(path)?;
if files.is_empty() {
bail!("no Python files found under {}", path.display());
}
let analyzer = DeterministicAnalyzer::new();
let mut result = analyzer.analyze_paths(&files, &config);
let mut external_ids = config.external_checkers.clone();
external_ids.extend(with_external);
if !external_ids.is_empty() {
let ext = ExternalAnalyzer::with_checkers(external_ids);
let mut external_issues = ext.run(path)?;
result.issues.append(&mut external_issues);
result.issues.sort_by(|a, b| {
(&a.file, a.line, a.column, &a.id, &a.message)
.cmp(&(&b.file, b.line, b.column, &b.id, &b.message))
});
}
println!("{}", render(format.into(), &result.issues)?);
if !result.file_errors.is_empty() && result.issues.is_empty() {
return Ok(ExitCode::from(EXIT_ERROR));
}
if result.issues.is_empty() {
return Ok(ExitCode::SUCCESS);
}
Ok(ExitCode::from(EXIT_ISSUES))
}
fn run_explain(id: &str) -> Result<ExitCode> {
let registry = CheckRegistry::new();
let check = registry
.find(id)
.with_context(|| format!("unknown check id: {id}"))?;
println!("{} — {}", check.id(), check.name());
println!("category: {}", check.category());
println!("severity: {}", check.severity());
println!();
println!("{}", check.explanation());
println!();
println!("False positives: {}", check.false_positives());
println!("Tradeoffs: {}", check.tradeoffs());
println!("Examples: {}", check.metadata().examples);
println!();
println!("Remediation: {}", check.remediation());
Ok(ExitCode::SUCCESS)
}
fn run_init(strict: bool) -> Result<ExitCode> {
let dest = PathBuf::from("zerum.toml");
if dest.exists() {
bail!("{} already exists", dest.display());
}
let template = if strict {
include_str!("../zerum.toml.strict.example")
} else {
include_str!("../zerum.toml.example")
};
std::fs::write(&dest, template)?;
println!("Wrote {}", dest.display());
Ok(ExitCode::SUCCESS)
}
fn run_list_checks() -> Result<ExitCode> {
let registry = CheckRegistry::new();
for check in registry.iter() {
println!(
"{} {} [{}] {}",
check.id(),
check.name(),
check.category(),
check.severity()
);
}
Ok(ExitCode::SUCCESS)
}
fn run_list_checkers() -> Result<ExitCode> {
for checker in builtin_checkers() {
let status = if checker.is_available() {
"available"
} else {
"not installed"
};
println!("{} {} [{status}]", checker.id(), checker.name());
}
Ok(ExitCode::SUCCESS)
}