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}