leash 0.1.0

Shareable Clippy lint presets for teams and orgs
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
        }
    }
}