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