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 assert!(check_args(["drep", "check", "--pre-commit-push"]).pre_commit_push);
178 }
179
180 #[test]
181 fn pre_commit_push_is_an_input_mode_not_a_modifier() {
182 assert!(Cli::try_parse_from(["drep", "check", "--pre-commit-push", "a.rs"]).is_err());
183 assert!(Cli::try_parse_from(["drep", "check", "--pre-commit-push", "--staged"]).is_err());
184 assert!(
185 Cli::try_parse_from(["drep", "check", "--pre-commit-push", "--diff", "main",]).is_err()
186 );
187 }
188
189 #[test]
190 fn format_defaults_to_text_and_fail_on_defaults_to_off() {
191 let args = check_args(["drep", "check"]);
192 assert_eq!(args.format, OutputFormat::Text);
193 assert_eq!(args.fail_on, None);
194 assert!(!args.cache_only);
195 assert!(!args.push_gate);
196 }
197
198 #[test]
199 fn cache_only_and_push_gate_parse_individually_but_not_together() {
200 assert!(check_args(["drep", "check", "--cache-only"]).cache_only);
201 assert!(check_args(["drep", "check", "--push-gate"]).push_gate);
202 assert!(Cli::try_parse_from(["drep", "check", "--cache-only", "--push-gate"]).is_err());
203 }
204
205 #[test]
206 fn default_repository_config_path_is_relative_to_the_requested_root() {
207 assert!(config::default_config_path().is_relative());
208 }
209
210 #[test]
211 fn fail_on_accepts_the_whole_severity_vocabulary() {
212 for expected in Severity::ALL {
215 let args = check_args(["drep", "check", "--fail-on", expected.as_str()]);
216 assert_eq!(args.fail_on, Some(expected));
217 }
218 assert!(Cli::try_parse_from(["drep", "check", "--fail-on", "critical"]).is_err());
219 }
220
221 #[test]
222 fn every_command_dispatches_to_an_implementation() {
223 let names: Vec<String> = Cli::command()
231 .get_subcommands()
232 .map(|c| c.get_name().to_owned())
233 .collect();
234 assert_eq!(
235 names,
236 vec![
237 "check",
238 "lint-docs",
239 "doctor",
240 "init",
241 "auth",
242 "acknowledge"
243 ]
244 );
245 }
246
247 #[test]
248 fn lint_docs_takes_paths_and_strict() {
249 let cli = Cli::try_parse_from(["drep", "lint-docs", "--strict", "a.md"]).unwrap();
250 match cli.command {
251 Command::LintDocs(args) => {
252 assert!(args.strict);
253 assert_eq!(args.paths.len(), 1);
254 }
255 other => panic!("expected lint-docs, got {other:?}"),
256 }
257 let cli = Cli::try_parse_from(["drep", "lint-docs"]).unwrap();
258 match cli.command {
259 Command::LintDocs(args) => {
260 assert!(!args.strict, "report-only is the default");
261 assert!(args.paths.is_empty());
262 }
263 other => panic!("expected lint-docs, got {other:?}"),
264 }
265 }
266}