cpp_linter/cli/mod.rs
1//! This module holds the Command Line Interface design.
2use std::{path::PathBuf, str::FromStr};
3
4// non-std crates
5use clap::{
6 builder::{FalseyValueParser, NonEmptyStringValueParser},
7 value_parser, ArgAction, Args, Parser, Subcommand, ValueEnum,
8};
9
10mod structs;
11pub use structs::{ClangParams, FeedbackInput, LinesChangedOnly, RequestedVersion, ThreadComments};
12
13#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)]
14pub enum Verbosity {
15 Info,
16 Debug,
17}
18
19impl Verbosity {
20 pub fn is_debug(&self) -> bool {
21 matches!(self, Verbosity::Debug)
22 }
23}
24
25/// A structure to contain parsed CLI options.
26#[derive(Debug, Clone, Parser)]
27#[command(author, about)]
28pub struct Cli {
29 #[command(flatten)]
30 pub general_options: GeneralOptions,
31
32 #[command(flatten)]
33 pub source_options: SourceOptions,
34
35 #[command(flatten)]
36 pub format_options: FormatOptions,
37
38 #[command(flatten)]
39 pub tidy_options: TidyOptions,
40
41 #[command(flatten)]
42 pub feedback_options: FeedbackOptions,
43
44 /// An explicit path to a file.
45 ///
46 /// This can be specified zero or more times, resulting in a list of files.
47 /// The list of files is appended to the internal list of 'not ignored' files.
48 /// Further filtering can still be applied (see [Source options](#source-options)).
49 #[arg(
50 name = "files",
51 value_name = "FILE",
52 action = ArgAction::Append,
53 verbatim_doc_comment,
54 )]
55 pub not_ignored: Option<Vec<String>>,
56
57 #[command(subcommand)]
58 pub commands: Option<CliCommand>,
59}
60
61#[derive(Debug, Subcommand, Clone)]
62pub enum CliCommand {
63 /// Display the version of cpp-linter and exit.
64 Version,
65}
66
67#[derive(Debug, Clone, Args)]
68#[group(id = "General options", multiple = true, required = false)]
69pub struct GeneralOptions {
70 /// The desired version of the clang tools to use.
71 ///
72 /// Accepted options are:
73 ///
74 /// - A semantic version specifier, eg. `>=10, <13`, `=12.0.1`, or simply `16`.
75 /// - A blank string (`''`) to use the platform's default
76 /// installed version.
77 /// - A path to where the clang tools are
78 /// installed (if using a custom install location).
79 /// All paths specified here are converted to absolute.
80 /// - If this option is specified without a value, then
81 /// the cpp-linter version is printed and the program exits.
82 #[arg(
83 short = 'V',
84 long,
85 default_missing_value = "CPP-LINTER-VERSION",
86 num_args = 0..=1,
87 value_parser = RequestedVersion::from_str,
88 default_value = "",
89 help_heading = "General options",
90 verbatim_doc_comment
91 )]
92 pub version: RequestedVersion,
93
94 /// This controls the action's verbosity in the workflow's logs.
95 ///
96 /// This option does not affect the verbosity of resulting
97 /// thread comments or file annotations.
98 #[arg(
99 short = 'v',
100 long,
101 default_value = "info",
102 default_missing_value = "debug",
103 num_args = 0..=1,
104 help_heading = "General options"
105 )]
106 pub verbosity: Verbosity,
107}
108
109#[derive(Debug, Clone, Args)]
110#[group(id = "Source options", multiple = true, required = false)]
111pub struct SourceOptions {
112 /// A comma-separated list of file extensions to analyze.
113 #[arg(
114 short,
115 long,
116 value_delimiter = ',',
117 default_value = "c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx",
118 value_parser = NonEmptyStringValueParser::new(),
119 help_heading = "Source options"
120 )]
121 pub extensions: Vec<String>,
122
123 /// The relative path to the repository root directory.
124 ///
125 /// This path is relative to the runner's `GITHUB_WORKSPACE`
126 /// environment variable (or the current working directory if
127 /// not using a CI runner).
128 #[arg(short, long, default_value = ".", help_heading = "Source options")]
129 pub repo_root: String,
130
131 /// This controls what part of the files are analyzed.
132 #[arg(
133 short,
134 long,
135 default_value = "true",
136 help_heading = "Source options",
137 ignore_case = true,
138 verbatim_doc_comment
139 )]
140 pub lines_changed_only: LinesChangedOnly,
141
142 /// Set this option to false to analyze any source files in the repo.
143 ///
144 /// This is automatically enabled if
145 /// [`--lines-changed-only`](#-l-lines-changed-only) is enabled.
146 ///
147 /// > [!NOTE]
148 /// > The `GITHUB_TOKEN` should be supplied when running on a
149 /// > private repository with this option enabled, otherwise the runner
150 /// > does not not have the privilege to list the changed files for an event.
151 /// >
152 /// > See [Authenticating with the `GITHUB_TOKEN`](
153 /// > https://docs.github.com/en/actions/reference/authentication-in-a-workflow).
154 #[arg(
155 short,
156 long,
157 default_value = "false",
158 default_missing_value = "true",
159 default_value_ifs = [
160 ("lines-changed-only", "true", "true"),
161 ("lines-changed-only", "on", "true"),
162 ("lines-changed-only", "1", "true"),
163 ("lines-changed-only", "diff", "true"),
164 ],
165 num_args = 0..=1,
166 action = ArgAction::Set,
167 value_parser = FalseyValueParser::new(),
168 help_heading = "Source options",
169 verbatim_doc_comment,
170 )]
171 pub files_changed_only: bool,
172
173 /// Set this option with path(s) to ignore (or not ignore).
174 ///
175 /// - In the case of multiple paths, you can use `|` to separate each path.
176 /// - There is no need to use `./` for each entry; a blank string (`''`)
177 /// represents the repo-root path.
178 /// - This can also have files, but the file's path (relative to
179 /// the [`--repo-root`](#-r-repo-root)) has to be specified with the filename.
180 /// - Submodules are automatically ignored. Hidden directories (beginning
181 /// with a `.`) are also ignored automatically.
182 /// - Prefix a path with `!` to explicitly not ignore it. This can be
183 /// applied to a submodule's path (if desired) but not hidden directories.
184 /// - Glob patterns are supported here. Path separators in glob patterns should
185 /// use `/` because `\` represents an escaped literal.
186 #[arg(
187 short,
188 long,
189 value_delimiter = '|',
190 default_value = ".github|target",
191 help_heading = "Source options",
192 verbatim_doc_comment
193 )]
194 pub ignore: Vec<String>,
195}
196
197#[derive(Debug, Clone, Args)]
198#[group(id = "Clang-format options", multiple = true, required = false)]
199pub struct FormatOptions {
200 /// The style rules to use.
201 ///
202 /// - Set this to `file` to have clang-format use the closest relative
203 /// .clang-format file. Same as passing no value to this option.
204 /// - Set this to a blank string (`''`) to disable using clang-format
205 /// entirely.
206 ///
207 /// > [!NOTE]
208 /// > If this is not a blank string, then it is also passed to clang-tidy
209 /// > (if [`--tidy_checks`](#-c-tidy-checks) is not `-*`).
210 /// > This is done to ensure suggestions from both clang-tidy and
211 /// > clang-format are consistent.
212 #[arg(
213 short,
214 long,
215 default_value = "llvm",
216 default_missing_value = "file",
217 num_args = 0..=1,
218 help_heading = "Clang-format options",
219 verbatim_doc_comment
220 )]
221 pub style: String,
222
223 /// Similar to [`--ignore`](#-i-ignore) but applied
224 /// exclusively to files analyzed by clang-format.
225 #[arg(
226 short = 'M',
227 long,
228 value_delimiter = '|',
229 help_heading = "Clang-format options"
230 )]
231 pub ignore_format: Option<Vec<String>>,
232}
233
234#[derive(Debug, Clone, Args)]
235#[group(id = "Clang-tidy options", multiple = true, required = false)]
236pub struct TidyOptions {
237 /// Similar to [`--ignore`](#-i-ignore) but applied
238 /// exclusively to files analyzed by clang-tidy.
239 #[arg(
240 short = 'D',
241 long,
242 value_delimiter = '|',
243 help_heading = "Clang-tidy options"
244 )]
245 pub ignore_tidy: Option<Vec<String>>,
246
247 /// A comma-separated list of globs with optional `-` prefix.
248 ///
249 /// Globs are processed in order of appearance in the list.
250 /// Globs without `-` prefix add checks with matching names to the set,
251 /// globs with the `-` prefix remove checks with matching names from the set of
252 /// enabled checks. This option's value is appended to the value of the 'Checks'
253 /// option in a .clang-tidy file (if any).
254 ///
255 /// - It is possible to disable clang-tidy entirely by setting this option to
256 /// `'-*'`.
257 /// - It is also possible to rely solely on a .clang-tidy config file by
258 /// specifying this option as a blank string (`''`).
259 ///
260 /// See also clang-tidy docs for more info.
261 #[arg(
262 short = 'c',
263 long,
264 default_value = "boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*,clang-analyzer-*,cppcoreguidelines-*",
265 default_missing_value = "",
266 num_args = 0..=1,
267 help_heading = "Clang-tidy options",
268 verbatim_doc_comment
269 )]
270 pub tidy_checks: String,
271
272 /// The path that is used to read a compile command database.
273 ///
274 /// For example, it can be a CMake build directory in which a file named
275 /// compile_commands.json exists (set `CMAKE_EXPORT_COMPILE_COMMANDS` to `ON`).
276 /// When no build path is specified, a search for compile_commands.json will be
277 /// attempted through all parent paths of the first input file. See [LLVM docs about
278 /// setup tooling](https://clang.llvm.org/docs/HowToSetupToolingForLLVM.html)
279 /// for an example of setting up Clang Tooling on a source tree.
280 #[arg(
281 short = 'p',
282 long,
283 value_name = "PATH",
284 value_parser = value_parser!(PathBuf),
285 help_heading = "Clang-tidy options",
286 )]
287 pub database: Option<PathBuf>,
288
289 /// A string of extra arguments passed to clang-tidy for use as compiler arguments.
290 ///
291 /// This can be specified more than once for each
292 /// additional argument. Recommend using quotes around the value and
293 /// avoid using spaces between name and value (use `=` instead):
294 ///
295 /// ```shell
296 /// cpp-linter --extra-arg="-std=c++17" --extra-arg="-Wall"
297 /// ```
298 #[arg(
299 short = 'x',
300 long,
301 action = ArgAction::Append,
302 help_heading = "Clang-tidy options",
303 verbatim_doc_comment
304 )]
305 pub extra_arg: Vec<String>,
306}
307
308#[derive(Debug, Clone, Args)]
309#[group(id = "Feedback options", multiple = true, required = false)]
310pub struct FeedbackOptions {
311 /// Set this option to true to enable the use of thread comments as feedback.
312 ///
313 /// > [!NOTE]
314 /// > To use thread comments, the `GITHUB_TOKEN` (provided by
315 /// > Github to each repository) must be declared as an environment
316 /// > variable.
317 /// >
318 /// > See [Authenticating with the `GITHUB_TOKEN`](
319 /// > https://docs.github.com/en/actions/reference/authentication-in-a-workflow).
320 #[arg(
321 short = 'g',
322 long,
323 default_value = "false",
324 default_missing_value = "update",
325 num_args = 0..=1,
326 help_heading = "Feedback options",
327 ignore_case = true,
328 verbatim_doc_comment
329 )]
330 pub thread_comments: ThreadComments,
331
332 /// Set this option to true or false to enable or disable the use of a
333 /// thread comment that basically says 'Looks Good To Me' (when all checks pass).
334 ///
335 /// > [!IMPORTANT]
336 /// > The [`--thread-comments`](#-g-thread-comments)
337 /// > option also notes further implications.
338 #[arg(
339 short = 't',
340 long,
341 default_value_t = true,
342 action = ArgAction::Set,
343 value_parser = FalseyValueParser::new(),
344 help_heading = "Feedback options",
345 verbatim_doc_comment,
346 )]
347 pub no_lgtm: bool,
348
349 /// Set this option to true or false to enable or disable the use of
350 /// a workflow step summary when the run has concluded.
351 #[arg(
352 short = 'w',
353 long,
354 default_value_t = false,
355 default_missing_value = "true",
356 num_args = 0..=1,
357 action = ArgAction::Set,
358 value_parser = FalseyValueParser::new(),
359 help_heading = "Feedback options",
360 )]
361 pub step_summary: bool,
362
363 /// Set this option to false to disable the use of
364 /// file annotations as feedback.
365 #[arg(
366 short = 'a',
367 long,
368 default_value_t = true,
369 action = ArgAction::Set,
370 value_parser = FalseyValueParser::new(),
371 help_heading = "Feedback options",
372 )]
373 pub file_annotations: bool,
374
375 /// Set to `true` to enable Pull Request reviews from clang-tidy.
376 #[arg(
377 short = 'd',
378 long,
379 default_value_t = false,
380 default_missing_value = "true",
381 num_args = 0..=1,
382 action = ArgAction::Set,
383 value_parser = FalseyValueParser::new(),
384 help_heading = "Feedback options",
385 )]
386 pub tidy_review: bool,
387
388 /// Set to `true` to enable Pull Request reviews from clang-format.
389 #[arg(
390 short = 'm',
391 long,
392 default_value_t = false,
393 default_missing_value = "true",
394 num_args = 0..=1,
395 action = ArgAction::Set,
396 value_parser = FalseyValueParser::new(),
397 help_heading = "Feedback options",
398 )]
399 pub format_review: bool,
400
401 /// Set to `true` to prevent Pull Request reviews from
402 /// approving or requesting changes.
403 #[arg(
404 short = 'R',
405 long,
406 default_value_t = false,
407 default_missing_value = "true",
408 num_args = 0..=1,
409 action = ArgAction::Set,
410 value_parser = FalseyValueParser::new(),
411 help_heading = "Feedback options",
412 )]
413 pub passive_reviews: bool,
414}
415
416/// Converts the parsed value of the `--extra-arg` option into an optional vector of strings.
417///
418/// This is for adapting to 2 scenarios where `--extra-arg` is either
419///
420/// - specified multiple times
421/// - each val is appended to a [`Vec`] (by clap crate)
422/// - specified once with multiple space-separated values
423/// - resulting [`Vec`] is made from splitting at the spaces between
424/// - not specified at all (returns empty [`Vec`])
425///
426/// It is preferred that the values specified in either situation do not contain spaces and are
427/// quoted:
428///
429/// ```shell
430/// --extra-arg="-std=c++17" --extra-arg="-Wall"
431/// # or equivalently
432/// --extra-arg="-std=c++17 -Wall"
433/// ```
434///
435/// The cpp-linter-action (for Github CI workflows) can only use 1 `extra-arg` input option, so
436/// the value will be split at spaces.
437pub fn convert_extra_arg_val(args: &[String]) -> Vec<String> {
438 let mut val = args.iter();
439 if val.len() == 1 {
440 // specified once; split and return result
441 val.next()
442 .unwrap()
443 .trim_matches('\'')
444 .trim_matches('"')
445 .split(' ')
446 .map(|i| i.to_string())
447 .collect()
448 } else {
449 // specified multiple times; just return
450 val.map(|i| i.to_string()).collect()
451 }
452}
453
454#[cfg(test)]
455mod test {
456 use super::{convert_extra_arg_val, Cli};
457 use clap::Parser;
458
459 #[test]
460 fn error_on_blank_extensions() {
461 let cli = Cli::try_parse_from(vec!["cpp-linter", "-e", "c,,h"]);
462 assert!(cli.is_err());
463 println!("{}", cli.unwrap_err());
464 }
465
466 #[test]
467 fn extra_arg_0() {
468 let args = Cli::parse_from(vec!["cpp-linter"]);
469 let extras = convert_extra_arg_val(&args.tidy_options.extra_arg);
470 assert!(extras.is_empty());
471 }
472
473 #[test]
474 fn extra_arg_1() {
475 let args = Cli::parse_from(vec!["cpp-linter", "--extra-arg='-std=c++17 -Wall'"]);
476 let extra_args = convert_extra_arg_val(&args.tidy_options.extra_arg);
477 assert_eq!(extra_args.len(), 2);
478 assert_eq!(extra_args, ["-std=c++17", "-Wall"])
479 }
480
481 #[test]
482 fn extra_arg_2() {
483 let args = Cli::parse_from(vec![
484 "cpp-linter",
485 "--extra-arg=-std=c++17",
486 "--extra-arg=-Wall",
487 ]);
488 let extra_args = convert_extra_arg_val(&args.tidy_options.extra_arg);
489 assert_eq!(extra_args.len(), 2);
490 assert_eq!(extra_args, ["-std=c++17", "-Wall"])
491 }
492}