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
69#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
71pub enum OutputFormat {
72 Text,
74 Json,
77}
78
79pub async fn run(cli: Cli) -> Result<Exit> {
81 match cli.command {
82 Command::Check(args) => check::run(&args, std::path::Path::new(".")).await,
83 Command::LintDocs(args) => lint_docs::run(&args, std::path::Path::new(".")).await,
84 Command::Doctor(args) => doctor::run(&args),
85 Command::Init(args) => init::run(&args).await,
86 Command::Auth(args) => auth::run(&args),
87 Command::Acknowledge(args) => acknowledge::run(&args, std::path::Path::new(".")),
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::analysis::findings::Severity;
95 use crate::config;
96 use clap::CommandFactory;
97
98 fn check_args<const N: usize>(argv: [&str; N]) -> CheckArgs {
100 let cli = Cli::try_parse_from(argv).expect("should parse");
101 match cli.command {
102 Command::Check(args) => args,
103 other => panic!("expected check, got {other:?}"),
104 }
105 }
106
107 fn lint_docs_args<const N: usize>(argv: [&str; N]) -> LintDocsArgs {
109 let cli = Cli::try_parse_from(argv).expect("should parse");
110 match cli.command {
111 Command::LintDocs(args) => args,
112 other => panic!("expected lint-docs, got {other:?}"),
113 }
114 }
115
116 #[test]
117 fn lint_docs_gating_is_off_by_default() {
118 let args = lint_docs_args(["drep", "lint-docs"]);
119 assert!(!args.strict);
120 assert_eq!(args.fail_on, None);
121 assert_eq!(args.threshold(), None);
122 }
123
124 #[test]
125 fn lint_docs_fail_on_accepts_the_whole_severity_vocabulary() {
126 for expected in Severity::ALL {
127 let args = lint_docs_args(["drep", "lint-docs", "--fail-on", expected.as_str()]);
128 assert_eq!(args.fail_on, Some(expected));
129 assert_eq!(args.threshold(), Some(expected));
130 }
131 assert!(Cli::try_parse_from(["drep", "lint-docs", "--fail-on", "critical"]).is_err());
132 }
133
134 #[test]
136 fn lint_docs_strict_is_fail_on_info() {
137 assert_eq!(
138 lint_docs_args(["drep", "lint-docs", "--strict"]).threshold(),
139 Some(Severity::Info)
140 );
141 }
142
143 #[test]
146 fn lint_docs_strict_and_fail_on_are_mutually_exclusive() {
147 assert!(
148 Cli::try_parse_from(["drep", "lint-docs", "--strict", "--fail-on", "error"]).is_err()
149 );
150 }
151
152 #[test]
153 fn cli_definition_is_valid() {
154 Cli::command().debug_assert();
155 }
156
157 #[test]
158 fn input_modes_are_mutually_exclusive() {
159 assert!(Cli::try_parse_from(["drep", "check", "--staged", "a.rs"]).is_err());
160 assert!(Cli::try_parse_from(["drep", "check", "--staged", "--diff", "main"]).is_err());
161 assert!(Cli::try_parse_from(["drep", "check", "--diff", "main", "a.rs"]).is_err());
162 }
163
164 #[test]
165 fn each_input_mode_parses_alone() {
166 assert_eq!(check_args(["drep", "check", "a.rs", "src/"]).paths.len(), 2);
167 assert!(check_args(["drep", "check", "--staged"]).staged);
168 assert_eq!(
169 check_args(["drep", "check", "--diff", "origin/main"])
170 .diff
171 .as_deref(),
172 Some("origin/main")
173 );
174 assert!(check_args(["drep", "check"]).paths.is_empty());
175 assert!(check_args(["drep", "check", "--pre-commit-push"]).pre_commit_push);
176 }
177
178 #[test]
179 fn pre_commit_push_is_an_input_mode_not_a_modifier() {
180 assert!(Cli::try_parse_from(["drep", "check", "--pre-commit-push", "a.rs"]).is_err());
181 assert!(Cli::try_parse_from(["drep", "check", "--pre-commit-push", "--staged"]).is_err());
182 assert!(
183 Cli::try_parse_from(["drep", "check", "--pre-commit-push", "--diff", "main",]).is_err()
184 );
185 }
186
187 #[test]
188 fn format_defaults_to_text_and_fail_on_defaults_to_off() {
189 let args = check_args(["drep", "check"]);
190 assert_eq!(args.format, OutputFormat::Text);
191 assert_eq!(args.fail_on, None);
192 assert!(!args.cache_only);
193 assert!(!args.push_gate);
194 assert_eq!(args.max_review_rounds, None);
195 assert!(!args.unlimited_reviews);
196 }
197
198 #[test]
199 fn review_limit_override_and_unlimited_mode_are_mutually_exclusive() {
200 assert_eq!(
201 check_args(["drep", "check", "--max-review-rounds", "7"]).max_review_rounds,
202 Some(7)
203 );
204 assert!(check_args(["drep", "check", "--unlimited-reviews"]).unlimited_reviews);
205 assert!(
206 Cli::try_parse_from([
207 "drep",
208 "check",
209 "--max-review-rounds",
210 "7",
211 "--unlimited-reviews",
212 ])
213 .is_err()
214 );
215 assert!(Cli::try_parse_from(["drep", "check", "--max-review-rounds", "0"]).is_err());
216 }
217
218 #[test]
219 fn cache_only_and_push_gate_parse_individually_but_not_together() {
220 assert!(check_args(["drep", "check", "--cache-only"]).cache_only);
221 assert!(check_args(["drep", "check", "--push-gate"]).push_gate);
222 assert!(Cli::try_parse_from(["drep", "check", "--cache-only", "--push-gate"]).is_err());
223 }
224
225 #[test]
226 fn default_repository_config_path_is_relative_to_the_requested_root() {
227 assert!(config::default_config_path().is_relative());
228 }
229
230 #[test]
231 fn fail_on_accepts_the_whole_severity_vocabulary() {
232 for expected in Severity::ALL {
235 let args = check_args(["drep", "check", "--fail-on", expected.as_str()]);
236 assert_eq!(args.fail_on, Some(expected));
237 }
238 assert!(Cli::try_parse_from(["drep", "check", "--fail-on", "critical"]).is_err());
239 }
240
241 #[test]
242 fn every_command_dispatches_to_an_implementation() {
243 let names: Vec<String> = Cli::command()
249 .get_subcommands()
250 .map(|c| c.get_name().to_owned())
251 .collect();
252 assert_eq!(
253 names,
254 vec![
255 "check",
256 "lint-docs",
257 "doctor",
258 "init",
259 "auth",
260 "acknowledge"
261 ]
262 );
263 }
264
265 #[test]
266 fn lint_docs_takes_paths_and_strict() {
267 let cli = Cli::try_parse_from(["drep", "lint-docs", "--strict", "a.md"]).unwrap();
268 match cli.command {
269 Command::LintDocs(args) => {
270 assert!(args.strict);
271 assert_eq!(args.paths.len(), 1);
272 }
273 other => panic!("expected lint-docs, got {other:?}"),
274 }
275 let cli = Cli::try_parse_from(["drep", "lint-docs"]).unwrap();
276 match cli.command {
277 Command::LintDocs(args) => {
278 assert!(!args.strict, "report-only is the default");
279 assert!(args.paths.is_empty());
280 }
281 other => panic!("expected lint-docs, got {other:?}"),
282 }
283 }
284}