1pub mod auth;
11pub mod check;
12pub mod doctor;
13pub mod init;
14pub mod lint_docs;
15pub mod render;
16
17use anyhow::Result;
18use clap::builder::{PossibleValuesParser, TypedValueParser};
19use clap::{Parser, Subcommand, ValueEnum};
20
21use crate::analysis::findings::Severity;
22
23use crate::Exit;
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}
55
56pub(crate) fn severity_parser() -> impl TypedValueParser<Value = Severity> {
63 PossibleValuesParser::new(Severity::ALL.map(Severity::as_str))
64 .map(|name| name.parse::<Severity>().expect("possible values parse"))
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
69pub enum OutputFormat {
70 Text,
72 Json,
75}
76
77pub async fn run(cli: Cli) -> Result<Exit> {
79 match cli.command {
80 Command::Check(args) => check::run(&args, std::path::Path::new(".")).await,
81 Command::LintDocs(args) => lint_docs::run(&args, std::path::Path::new(".")).await,
82 Command::Doctor(args) => doctor::run(&args),
83 Command::Init(args) => init::run(&args).await,
84 Command::Auth(args) => auth::run(&args),
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::analysis::findings::Severity;
92 use clap::CommandFactory;
93
94 fn check_args<const N: usize>(argv: [&str; N]) -> CheckArgs {
96 let cli = Cli::try_parse_from(argv).expect("should parse");
97 match cli.command {
98 Command::Check(args) => args,
99 other => panic!("expected check, got {other:?}"),
100 }
101 }
102
103 fn lint_docs_args<const N: usize>(argv: [&str; N]) -> LintDocsArgs {
105 let cli = Cli::try_parse_from(argv).expect("should parse");
106 match cli.command {
107 Command::LintDocs(args) => args,
108 other => panic!("expected lint-docs, got {other:?}"),
109 }
110 }
111
112 #[test]
113 fn lint_docs_gating_is_off_by_default() {
114 let args = lint_docs_args(["drep", "lint-docs"]);
115 assert!(!args.strict);
116 assert_eq!(args.fail_on, None);
117 assert_eq!(args.threshold(), None);
118 }
119
120 #[test]
121 fn lint_docs_fail_on_accepts_the_whole_severity_vocabulary() {
122 for expected in Severity::ALL {
123 let args = lint_docs_args(["drep", "lint-docs", "--fail-on", expected.as_str()]);
124 assert_eq!(args.fail_on, Some(expected));
125 assert_eq!(args.threshold(), Some(expected));
126 }
127 assert!(Cli::try_parse_from(["drep", "lint-docs", "--fail-on", "critical"]).is_err());
128 }
129
130 #[test]
132 fn lint_docs_strict_is_fail_on_info() {
133 assert_eq!(
134 lint_docs_args(["drep", "lint-docs", "--strict"]).threshold(),
135 Some(Severity::Info)
136 );
137 }
138
139 #[test]
142 fn lint_docs_strict_and_fail_on_are_mutually_exclusive() {
143 assert!(
144 Cli::try_parse_from(["drep", "lint-docs", "--strict", "--fail-on", "error"]).is_err()
145 );
146 }
147
148 #[test]
149 fn cli_definition_is_valid() {
150 Cli::command().debug_assert();
151 }
152
153 #[test]
154 fn input_modes_are_mutually_exclusive() {
155 assert!(Cli::try_parse_from(["drep", "check", "--staged", "a.rs"]).is_err());
156 assert!(Cli::try_parse_from(["drep", "check", "--staged", "--diff", "main"]).is_err());
157 assert!(Cli::try_parse_from(["drep", "check", "--diff", "main", "a.rs"]).is_err());
158 }
159
160 #[test]
161 fn each_input_mode_parses_alone() {
162 assert_eq!(check_args(["drep", "check", "a.rs", "src/"]).paths.len(), 2);
163 assert!(check_args(["drep", "check", "--staged"]).staged);
164 assert_eq!(
165 check_args(["drep", "check", "--diff", "origin/main"])
166 .diff
167 .as_deref(),
168 Some("origin/main")
169 );
170 assert!(check_args(["drep", "check"]).paths.is_empty());
171 }
172
173 #[test]
174 fn format_defaults_to_text_and_fail_on_defaults_to_off() {
175 let args = check_args(["drep", "check"]);
176 assert_eq!(args.format, OutputFormat::Text);
177 assert_eq!(args.fail_on, None);
178 }
179
180 #[test]
181 fn fail_on_accepts_the_whole_severity_vocabulary() {
182 for expected in Severity::ALL {
185 let args = check_args(["drep", "check", "--fail-on", expected.as_str()]);
186 assert_eq!(args.fail_on, Some(expected));
187 }
188 assert!(Cli::try_parse_from(["drep", "check", "--fail-on", "critical"]).is_err());
189 }
190
191 #[test]
192 fn every_command_dispatches_to_an_implementation() {
193 let names: Vec<String> = Cli::command()
201 .get_subcommands()
202 .map(|c| c.get_name().to_owned())
203 .collect();
204 assert_eq!(names, vec!["check", "lint-docs", "doctor", "init", "auth"]);
205 }
206
207 #[test]
208 fn lint_docs_takes_paths_and_strict() {
209 let cli = Cli::try_parse_from(["drep", "lint-docs", "--strict", "a.md"]).unwrap();
210 match cli.command {
211 Command::LintDocs(args) => {
212 assert!(args.strict);
213 assert_eq!(args.paths.len(), 1);
214 }
215 other => panic!("expected lint-docs, got {other:?}"),
216 }
217 let cli = Cli::try_parse_from(["drep", "lint-docs"]).unwrap();
218 match cli.command {
219 Command::LintDocs(args) => {
220 assert!(!args.strict, "report-only is the default");
221 assert!(args.paths.is_empty());
222 }
223 other => panic!("expected lint-docs, got {other:?}"),
224 }
225 }
226}