mod presets;
use anyhow::{bail, Context, Result};
use presets::{LintLevel, Preset, PRESETS};
use sap::{Argument, Parser};
use serde::Deserialize;
use std::process::{Command, ExitCode};
#[derive(Deserialize)]
struct Config {
preset: String,
}
struct CliArgs {
dry_run: bool,
list: bool,
extra_cargo_args: Vec<String>,
}
fn parse_args() -> Result<CliArgs> {
let mut parser = Parser::from_env().context("failed to read program arguments")?;
let mut dry_run = false;
let mut list = false;
let mut extra_cargo_args = Vec::new();
while let Some(arg) = parser.forward().context("failed to parse argument")? {
match arg {
Argument::Long("dry-run") => dry_run = true,
Argument::Long("list") => list = true,
Argument::Long(flag) => bail!("unknown flag: --{flag}"),
Argument::Short(c) => bail!("unknown flag: -{c}"),
Argument::Value(v) => extra_cargo_args.push(v.into_owned()),
Argument::Stdio => bail!("unexpected '-'"),
}
}
Ok(CliArgs { dry_run, list, extra_cargo_args })
}
fn read_config() -> Result<Config> {
let contents = std::fs::read_to_string("leash.toml")
.context("leash.toml not found in current directory")?;
toml::from_str(&contents).context("failed to parse leash.toml")
}
fn find_preset(name: &str) -> Result<Preset> {
PRESETS.iter().copied().find(|p| p.name == name).copied().ok_or_else(|| {
let available = PRESETS.iter().map(|p| p.name).collect::<Vec<_>>().join(", ");
anyhow::anyhow!("unknown preset '{name}'. Available presets: {available}")
})
}
fn build_flags(preset: &Preset) -> Vec<String> {
let mut flags = Vec::new();
for rule in preset.rules {
flags.push(
match rule.level {
LintLevel::Forbid => "-F",
LintLevel::Deny => "-D",
}
.to_owned(),
);
flags.push(rule.name.to_owned());
}
flags
}
fn run_clippy(extra_cargo_args: &[String], lint_flags: &[String]) -> Result<ExitCode> {
let mut args = vec!["clippy".to_owned()];
args.extend_from_slice(extra_cargo_args);
args.push("--".to_owned());
args.extend_from_slice(lint_flags);
let status = Command::new("cargo")
.args(&args)
.status()
.context("failed to spawn cargo")?;
let code = u8::try_from(status.code().unwrap_or(1)).unwrap_or(1);
Ok(ExitCode::from(code))
}
fn run() -> Result<ExitCode> {
let args = parse_args()?;
if args.list {
println!("Available presets:");
for preset in PRESETS {
println!(" {:<10} - {}", preset.name, preset.description);
}
return Ok(ExitCode::SUCCESS);
}
let config = read_config()?;
let preset = find_preset(&config.preset)?;
let flags = build_flags(&preset);
if args.dry_run {
let extra = args.extra_cargo_args.join(" ");
let lint_flags = flags.join(" ");
if extra.is_empty() {
println!("cargo clippy -- {lint_flags}");
} else {
println!("cargo clippy {extra} -- {lint_flags}");
}
return Ok(ExitCode::SUCCESS);
}
run_clippy(&args.extra_cargo_args, &flags)
}
fn main() -> ExitCode {
match run() {
Ok(code) => code,
Err(e) => {
eprintln!("leash: {e:#}");
ExitCode::FAILURE
}
}
}