1pub mod acknowledge;
9pub mod auth;
10pub mod check;
11pub mod doctor;
12pub mod init;
13pub mod lint_docs;
14pub mod render;
15
16use anyhow::Result;
17use clap::builder::{PossibleValuesParser, TypedValueParser};
18use clap::{Parser, Subcommand, ValueEnum};
19
20use crate::analysis::findings::Severity;
21
22use crate::Exit;
23use acknowledge::AcknowledgeArgs;
24use auth::AuthArgs;
25use check::CheckArgs;
26use doctor::DoctorArgs;
27use init::InitArgs;
28use lint_docs::LintDocsArgs;
29
30#[derive(Debug, Parser)]
31#[command(
32 name = "drep",
33 version,
34 about = "Run the linters your repo configures, and have an LLM review what changed",
35 long_about = None,
36)]
37pub struct Cli {
38 #[command(subcommand)]
39 pub command: Command,
40}
41
42#[derive(Debug, Subcommand)]
43pub enum Command {
44 Check(CheckArgs),
46 LintDocs(LintDocsArgs),
48 Doctor(DoctorArgs),
50 Init(InitArgs),
52 Auth(AuthArgs),
54 Acknowledge(AcknowledgeArgs),
56}
57
58pub(crate) fn severity_parser() -> impl TypedValueParser<Value = Severity> {
65 PossibleValuesParser::new(Severity::ALL.map(Severity::as_str))
66 .map(|name| name.parse::<Severity>().expect("possible values parse"))
67}
68
69pub struct MachineFiles<'a> {
80 pub auth: &'a std::path::Path,
82 pub policy: &'a std::path::Path,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
88pub enum OutputFormat {
89 Text,
91 Json,
94}
95
96pub async fn run(cli: Cli) -> Result<Exit> {
98 match cli.command {
99 Command::Check(args) => check::run(&args, std::path::Path::new(".")).await,
100 Command::LintDocs(args) => lint_docs::run(&args, std::path::Path::new(".")).await,
101 Command::Doctor(args) => doctor::run(&args).await,
102 Command::Init(args) => init::run(&args).await,
103 Command::Auth(args) => auth::run(&args),
104 Command::Acknowledge(args) => acknowledge::run(&args, std::path::Path::new(".")),
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::analysis::findings::Severity;
112 use crate::config;
113 use clap::CommandFactory;
114
115 fn check_args<const N: usize>(argv: [&str; N]) -> CheckArgs {
117 let cli = Cli::try_parse_from(argv).expect("should parse");
118 match cli.command {
119 Command::Check(args) => args,
120 other => panic!("expected check, got {other:?}"),
121 }
122 }
123
124 fn lint_docs_args<const N: usize>(argv: [&str; N]) -> LintDocsArgs {
126 let cli = Cli::try_parse_from(argv).expect("should parse");
127 match cli.command {
128 Command::LintDocs(args) => args,
129 other => panic!("expected lint-docs, got {other:?}"),
130 }
131 }
132
133 #[test]
134 fn lint_docs_gating_is_off_by_default() {
135 let args = lint_docs_args(["drep", "lint-docs"]);
136 assert!(!args.strict);
137 assert_eq!(args.fail_on, None);
138 assert_eq!(args.threshold(), None);
139 }
140
141 #[test]
142 fn lint_docs_fail_on_accepts_the_whole_severity_vocabulary() {
143 for expected in Severity::ALL {
144 let args = lint_docs_args(["drep", "lint-docs", "--fail-on", expected.as_str()]);
145 assert_eq!(args.fail_on, Some(expected));
146 assert_eq!(args.threshold(), Some(expected));
147 }
148 assert!(Cli::try_parse_from(["drep", "lint-docs", "--fail-on", "critical"]).is_err());
149 }
150
151 #[test]
153 fn lint_docs_strict_is_fail_on_info() {
154 assert_eq!(
155 lint_docs_args(["drep", "lint-docs", "--strict"]).threshold(),
156 Some(Severity::Info)
157 );
158 }
159
160 #[test]
163 fn lint_docs_strict_and_fail_on_are_mutually_exclusive() {
164 assert!(
165 Cli::try_parse_from(["drep", "lint-docs", "--strict", "--fail-on", "error"]).is_err()
166 );
167 }
168
169 #[test]
170 fn cli_definition_is_valid() {
171 Cli::command().debug_assert();
172 }
173
174 #[test]
175 fn input_modes_are_mutually_exclusive() {
176 assert!(Cli::try_parse_from(["drep", "check", "--staged", "a.rs"]).is_err());
177 assert!(Cli::try_parse_from(["drep", "check", "--staged", "--diff", "main"]).is_err());
178 assert!(Cli::try_parse_from(["drep", "check", "--diff", "main", "a.rs"]).is_err());
179 }
180
181 #[test]
182 fn each_input_mode_parses_alone() {
183 assert_eq!(check_args(["drep", "check", "a.rs", "src/"]).paths.len(), 2);
184 assert!(check_args(["drep", "check", "--staged"]).staged);
185 assert_eq!(
186 check_args(["drep", "check", "--diff", "origin/main"])
187 .diff
188 .as_deref(),
189 Some("origin/main")
190 );
191 assert!(check_args(["drep", "check"]).paths.is_empty());
192 assert!(check_args(["drep", "check", "--pre-commit-push"]).pre_commit_push);
193 }
194
195 #[test]
196 fn pre_commit_push_is_an_input_mode_not_a_modifier() {
197 assert!(Cli::try_parse_from(["drep", "check", "--pre-commit-push", "a.rs"]).is_err());
198 assert!(Cli::try_parse_from(["drep", "check", "--pre-commit-push", "--staged"]).is_err());
199 assert!(
200 Cli::try_parse_from(["drep", "check", "--pre-commit-push", "--diff", "main",]).is_err()
201 );
202 }
203
204 #[test]
205 fn format_defaults_to_text_and_fail_on_defaults_to_off() {
206 let args = check_args(["drep", "check"]);
207 assert_eq!(args.format, OutputFormat::Text);
208 assert_eq!(args.fail_on, None);
209 assert!(!args.cache_only);
210 assert!(!args.push_gate);
211 assert_eq!(args.max_review_rounds, None);
212 assert!(!args.unlimited_reviews);
213 }
214
215 #[test]
216 fn review_limit_override_and_unlimited_mode_are_mutually_exclusive() {
217 assert_eq!(
218 check_args(["drep", "check", "--max-review-rounds", "7"]).max_review_rounds,
219 Some(7)
220 );
221 assert!(check_args(["drep", "check", "--unlimited-reviews"]).unlimited_reviews);
222 assert!(
223 Cli::try_parse_from([
224 "drep",
225 "check",
226 "--max-review-rounds",
227 "7",
228 "--unlimited-reviews",
229 ])
230 .is_err()
231 );
232 assert!(Cli::try_parse_from(["drep", "check", "--max-review-rounds", "0"]).is_err());
233 }
234
235 #[test]
236 fn cache_only_and_push_gate_parse_individually_but_not_together() {
237 assert!(check_args(["drep", "check", "--cache-only"]).cache_only);
238 assert!(check_args(["drep", "check", "--push-gate"]).push_gate);
239 assert!(Cli::try_parse_from(["drep", "check", "--cache-only", "--push-gate"]).is_err());
240 }
241
242 #[test]
243 fn default_repository_config_path_is_relative_to_the_requested_root() {
244 assert!(config::default_config_path().is_relative());
245 }
246
247 #[test]
248 fn fail_on_accepts_the_whole_severity_vocabulary() {
249 for expected in Severity::ALL {
252 let args = check_args(["drep", "check", "--fail-on", expected.as_str()]);
253 assert_eq!(args.fail_on, Some(expected));
254 }
255 assert!(Cli::try_parse_from(["drep", "check", "--fail-on", "critical"]).is_err());
256 }
257
258 #[test]
259 fn every_command_dispatches_to_an_implementation() {
260 let names: Vec<String> = Cli::command()
266 .get_subcommands()
267 .map(|c| c.get_name().to_owned())
268 .collect();
269 assert_eq!(
270 names,
271 vec![
272 "check",
273 "lint-docs",
274 "doctor",
275 "init",
276 "auth",
277 "acknowledge"
278 ]
279 );
280 }
281
282 #[test]
283 fn lint_docs_takes_paths_and_strict() {
284 let cli = Cli::try_parse_from(["drep", "lint-docs", "--strict", "a.md"]).unwrap();
285 match cli.command {
286 Command::LintDocs(args) => {
287 assert!(args.strict);
288 assert_eq!(args.paths.len(), 1);
289 }
290 other => panic!("expected lint-docs, got {other:?}"),
291 }
292 let cli = Cli::try_parse_from(["drep", "lint-docs"]).unwrap();
293 match cli.command {
294 Command::LintDocs(args) => {
295 assert!(!args.strict, "report-only is the default");
296 assert!(args.paths.is_empty());
297 }
298 other => panic!("expected lint-docs, got {other:?}"),
299 }
300 }
301}