Skip to main content

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}