use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
#[derive(Parser)]
#[command(
name = "slopguard",
about = "Catch AI-generated code patterns and common issues"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Clone, ValueEnum)]
pub enum Format {
Text,
Json,
Sarif,
}
#[derive(Clone, ValueEnum)]
pub enum SeverityThreshold {
Error,
Warning,
}
#[derive(Clone, ValueEnum)]
pub enum CategoryFilter {
Slop,
Security,
Correctness,
}
#[derive(Clone, ValueEnum)]
pub enum LanguageFilter {
Rust,
Typescript,
}
#[derive(Subcommand)]
pub enum Command {
Scan {
#[arg(default_value = ".")]
paths: Vec<PathBuf>,
#[arg(long)]
format: Option<Format>,
#[arg(long, default_value = "warning")]
severity_threshold: SeverityThreshold,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
no_colors: bool,
#[arg(long = "disable", value_name = "RULE_ID")]
cli_disable: Vec<String>,
#[arg(long = "enable", value_name = "RULE_ID")]
cli_enable: Vec<String>,
#[arg(long = "rule", value_name = "RULE_ID")]
rule_filter: Option<String>,
#[arg(long)]
no_cache: bool,
#[arg(long, value_name = "PATH")]
cache_dir: Option<PathBuf>,
#[arg(long)]
no_ai: bool,
#[arg(long, conflicts_with = "baseline_path")]
no_baseline: bool,
#[arg(long = "baseline", value_name = "PATH")]
baseline_path: Option<PathBuf>,
},
Baseline {
#[arg(default_value = ".")]
paths: Vec<PathBuf>,
#[arg(long, short = 'o', value_name = "PATH")]
output: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long = "disable", value_name = "RULE_ID")]
cli_disable: Vec<String>,
#[arg(long = "enable", value_name = "RULE_ID")]
cli_enable: Vec<String>,
#[arg(long)]
no_cache: bool,
#[arg(long, value_name = "PATH")]
cache_dir: Option<PathBuf>,
#[arg(long)]
no_ai: bool,
},
Init {
#[arg(long)]
force: bool,
},
Test {
#[arg(long)]
config: Option<PathBuf>,
},
Explain {
rule_id: String,
#[arg(long)]
format: Option<Format>,
#[arg(long)]
config: Option<PathBuf>,
},
List {
#[arg(long)]
all: bool,
#[arg(long)]
category: Option<CategoryFilter>,
#[arg(long)]
language: Option<LanguageFilter>,
#[arg(long)]
config: Option<PathBuf>,
},
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_parses_scan_subcommand() {
Cli::command()
.try_get_matches_from(["slopguard", "scan", "."])
.expect("scan subcommand should parse");
}
#[test]
fn cli_parses_scan_subcommand_default_path() {
Cli::command()
.try_get_matches_from(["slopguard", "scan"])
.expect("scan subcommand should parse without explicit path");
}
#[test]
fn cli_parses_scan_with_format() {
Cli::command()
.try_get_matches_from(["slopguard", "scan", "--format", "json", "."])
.expect("scan with --format json should parse");
}
#[test]
fn cli_parses_scan_with_severity_threshold() {
Cli::command()
.try_get_matches_from(["slopguard", "scan", "--severity-threshold", "error", "."])
.expect("scan with --severity-threshold should parse");
}
#[test]
fn cli_parses_scan_with_config() {
Cli::command()
.try_get_matches_from(["slopguard", "scan", "--config", "my.toml", "."])
.expect("scan with --config should parse");
}
#[test]
fn cli_parses_scan_with_no_colors() {
Cli::command()
.try_get_matches_from(["slopguard", "scan", "--no-colors", "."])
.expect("scan with --no-colors should parse");
}
#[test]
fn cli_parses_init_subcommand() {
Cli::command()
.try_get_matches_from(["slopguard", "init"])
.expect("init subcommand should parse");
}
#[test]
fn cli_parses_test_subcommand() {
Cli::command()
.try_get_matches_from(["slopguard", "test"])
.expect("test subcommand should parse");
}
#[test]
fn cli_parses_baseline_subcommand() {
Cli::command()
.try_get_matches_from(["slopguard", "baseline", "."])
.expect("baseline subcommand should parse");
}
#[test]
fn cli_parses_baseline_with_output() {
Cli::command()
.try_get_matches_from(["slopguard", "baseline", "-o", "custom.json", "."])
.expect("baseline with --output should parse");
}
#[test]
fn cli_parses_scan_with_no_baseline() {
Cli::command()
.try_get_matches_from(["slopguard", "scan", "--no-baseline", "."])
.expect("scan with --no-baseline should parse");
}
#[test]
fn cli_parses_scan_with_baseline_path() {
Cli::command()
.try_get_matches_from(["slopguard", "scan", "--baseline", "custom.json", "."])
.expect("scan with --baseline should parse");
}
#[test]
fn cli_rejects_no_baseline_with_baseline_path() {
let result = Cli::command().try_get_matches_from([
"slopguard",
"scan",
"--no-baseline",
"--baseline",
"custom.json",
".",
]);
assert!(
result.is_err(),
"--no-baseline and --baseline should conflict"
);
}
#[test]
fn cli_parses_list_subcommand() {
Cli::command()
.try_get_matches_from(["slopguard", "list"])
.expect("list subcommand should parse");
}
}