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