1pub mod acknowledge;
11pub mod auth;
12pub mod check;
13pub mod doctor;
14pub mod init;
15pub mod lint_docs;
16pub mod render;
17
18use anyhow::Result;
19use clap::builder::{PossibleValuesParser, TypedValueParser};
20use clap::{Parser, Subcommand, ValueEnum};
21
22use crate::analysis::findings::Severity;
23
24use crate::Exit;
25use acknowledge::AcknowledgeArgs;
26use auth::AuthArgs;
27use check::CheckArgs;
28use doctor::DoctorArgs;
29use init::InitArgs;
30use lint_docs::LintDocsArgs;
31
32#[derive(Debug, Parser)]
33#[command(
34 name = "drep",
35 version,
36 about = "Run the linters your repo configures, and have an LLM review what changed",
37 long_about = None,
38)]
39pub struct Cli {
40 #[command(subcommand)]
41 pub command: Command,
42}
43
44#[derive(Debug, Subcommand)]
45pub enum Command {
46 Check(CheckArgs),
48 LintDocs(LintDocsArgs),
50 Doctor(DoctorArgs),
52 Init(InitArgs),
54 Auth(AuthArgs),
56 Acknowledge(AcknowledgeArgs),
58}
59
60pub(crate) fn severity_parser() -> impl TypedValueParser<Value = Severity> {
67 PossibleValuesParser::new(Severity::ALL.map(Severity::as_str))
68 .map(|name| name.parse::<Severity>().expect("possible values parse"))
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
73pub enum OutputFormat {
74 Text,
76 Json,
79}
80
81pub async fn run(cli: Cli) -> Result<Exit> {
83 match cli.command {
84 Command::Check(args) => check::run(&args, std::path::Path::new(".")).await,
85 Command::LintDocs(args) => lint_docs::run(&args, std::path::Path::new(".")).await,
86 Command::Doctor(args) => doctor::run(&args),
87 Command::Init(args) => init::run(&args).await,
88 Command::Auth(args) => auth::run(&args),
89 Command::Acknowledge(args) => acknowledge::run(&args, std::path::Path::new(".")),
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::analysis::findings::Severity;
97 use crate::config;
98 use clap::CommandFactory;
99
100 fn check_args<const N: usize>(argv: [&str; N]) -> CheckArgs {
102 let cli = Cli::try_parse_from(argv).expect("should parse");
103 match cli.command {
104 Command::Check(args) => args,
105 other => panic!("expected check, got {other:?}"),
106 }
107 }
108
109 fn lint_docs_args<const N: usize>(argv: [&str; N]) -> LintDocsArgs {
111 let cli = Cli::try_parse_from(argv).expect("should parse");
112 match cli.command {
113 Command::LintDocs(args) => args,
114 other => panic!("expected lint-docs, got {other:?}"),
115 }
116 }
117
118 #[test]
119 fn lint_docs_gating_is_off_by_default() {
120 let args = lint_docs_args(["drep", "lint-docs"]);
121 assert!(!args.strict);
122 assert_eq!(args.fail_on, None);
123 assert_eq!(args.threshold(), None);
124 }
125
126 #[test]
127 fn lint_docs_fail_on_accepts_the_whole_severity_vocabulary() {
128 for expected in Severity::ALL {
129 let args = lint_docs_args(["drep", "lint-docs", "--fail-on", expected.as_str()]);
130 assert_eq!(args.fail_on, Some(expected));
131 assert_eq!(args.threshold(), Some(expected));
132 }
133 assert!(Cli::try_parse_from(["drep", "lint-docs", "--fail-on", "critical"]).is_err());
134 }
135
136 #[test]
138 fn lint_docs_strict_is_fail_on_info() {
139 assert_eq!(
140 lint_docs_args(["drep", "lint-docs", "--strict"]).threshold(),
141 Some(Severity::Info)
142 );
143 }
144
145 #[test]
148 fn lint_docs_strict_and_fail_on_are_mutually_exclusive() {
149 assert!(
150 Cli::try_parse_from(["drep", "lint-docs", "--strict", "--fail-on", "error"]).is_err()
151 );
152 }
153
154 #[test]
155 fn cli_definition_is_valid() {
156 Cli::command().debug_assert();
157 }
158
159 #[test]
160 fn input_modes_are_mutually_exclusive() {
161 assert!(Cli::try_parse_from(["drep", "check", "--staged", "a.rs"]).is_err());
162 assert!(Cli::try_parse_from(["drep", "check", "--staged", "--diff", "main"]).is_err());
163 assert!(Cli::try_parse_from(["drep", "check", "--diff", "main", "a.rs"]).is_err());
164 }
165
166 #[test]
167 fn each_input_mode_parses_alone() {
168 assert_eq!(check_args(["drep", "check", "a.rs", "src/"]).paths.len(), 2);
169 assert!(check_args(["drep", "check", "--staged"]).staged);
170 assert_eq!(
171 check_args(["drep", "check", "--diff", "origin/main"])
172 .diff
173 .as_deref(),
174 Some("origin/main")
175 );
176 assert!(check_args(["drep", "check"]).paths.is_empty());
177 }
178
179 #[test]
180 fn format_defaults_to_text_and_fail_on_defaults_to_off() {
181 let args = check_args(["drep", "check"]);
182 assert_eq!(args.format, OutputFormat::Text);
183 assert_eq!(args.fail_on, None);
184 assert!(!args.cache_only);
185 assert!(!args.push_gate);
186 }
187
188 #[test]
189 fn cache_only_and_push_gate_parse_individually_but_not_together() {
190 assert!(check_args(["drep", "check", "--cache-only"]).cache_only);
191 assert!(check_args(["drep", "check", "--push-gate"]).push_gate);
192 assert!(Cli::try_parse_from(["drep", "check", "--cache-only", "--push-gate"]).is_err());
193 }
194
195 #[test]
196 fn default_repository_config_path_is_relative_to_the_requested_root() {
197 assert!(config::default_config_path().is_relative());
198 }
199
200 #[test]
201 fn fail_on_accepts_the_whole_severity_vocabulary() {
202 for expected in Severity::ALL {
205 let args = check_args(["drep", "check", "--fail-on", expected.as_str()]);
206 assert_eq!(args.fail_on, Some(expected));
207 }
208 assert!(Cli::try_parse_from(["drep", "check", "--fail-on", "critical"]).is_err());
209 }
210
211 #[test]
212 fn every_command_dispatches_to_an_implementation() {
213 let names: Vec<String> = Cli::command()
221 .get_subcommands()
222 .map(|c| c.get_name().to_owned())
223 .collect();
224 assert_eq!(
225 names,
226 vec![
227 "check",
228 "lint-docs",
229 "doctor",
230 "init",
231 "auth",
232 "acknowledge"
233 ]
234 );
235 }
236
237 #[test]
238 fn lint_docs_takes_paths_and_strict() {
239 let cli = Cli::try_parse_from(["drep", "lint-docs", "--strict", "a.md"]).unwrap();
240 match cli.command {
241 Command::LintDocs(args) => {
242 assert!(args.strict);
243 assert_eq!(args.paths.len(), 1);
244 }
245 other => panic!("expected lint-docs, got {other:?}"),
246 }
247 let cli = Cli::try_parse_from(["drep", "lint-docs"]).unwrap();
248 match cli.command {
249 Command::LintDocs(args) => {
250 assert!(!args.strict, "report-only is the default");
251 assert!(args.paths.is_empty());
252 }
253 other => panic!("expected lint-docs, got {other:?}"),
254 }
255 }
256}