Skip to main content

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}