cpp_linter/cli/
structs.rs

1use std::{fmt::Display, path::PathBuf, str::FromStr};
2
3use anyhow::{anyhow, Error};
4use clap::{builder::PossibleValue, ValueEnum};
5use semver::VersionReq;
6
7use super::Cli;
8use crate::{
9    clang_tools::clang_tidy::CompilationUnit,
10    common_fs::{normalize_path, FileFilter},
11};
12
13#[derive(Debug, Clone, PartialEq, Eq, Default)]
14pub enum RequestedVersion {
15    /// A specific path to the clang tool binary.
16    Path(PathBuf),
17
18    /// Whatever the system default uses (if any).
19    #[default]
20    SystemDefault,
21
22    /// A specific version requirement for the clang tool binary.
23    ///
24    /// For example, `=12.0.1`, `>=10.0.0, <13.0.0`.
25    Requirement(VersionReq),
26
27    /// A sentinel when no value is given.
28    ///
29    /// This is used internally to differentiate when the user intended
30    /// to invoke the `version` subcommand instead.
31    NoValue,
32}
33
34impl FromStr for RequestedVersion {
35    type Err = Error;
36
37    fn from_str(input: &str) -> Result<Self, Self::Err> {
38        if input.is_empty() {
39            Ok(Self::SystemDefault)
40        } else if input == "CPP-LINTER-VERSION" {
41            Ok(Self::NoValue)
42        } else if let Ok(req) = VersionReq::parse(input) {
43            Ok(Self::Requirement(req))
44        } else if let Ok(req) = VersionReq::parse(format!("={input}").as_str()) {
45            Ok(Self::Requirement(req))
46        } else {
47            let path = PathBuf::from(input);
48            if !path.exists() {
49                return Err(anyhow!(
50                    "The specified version is not a proper requirement or a valid path: {}",
51                    input
52                ));
53            }
54            let path = if !path.is_dir() {
55                path.parent()
56                    .ok_or(anyhow!(
57                        "Unknown parent directory of the given file path for `--version`: {}",
58                        input
59                    ))?
60                    .to_path_buf()
61            } else {
62                path
63            };
64            let path = match path.canonicalize() {
65                Ok(p) => Ok(normalize_path(&p)),
66                Err(e) => Err(anyhow!("Failed to canonicalize path '{input}': {e:?}")),
67            }?;
68            Ok(Self::Path(path))
69        }
70    }
71}
72
73/// An enum to describe `--lines-changed-only` CLI option's behavior.
74#[derive(PartialEq, Clone, Debug, Default)]
75pub enum LinesChangedOnly {
76    /// All lines are scanned
77    #[default]
78    Off,
79    /// Only lines in the diff are scanned
80    Diff,
81    /// Only lines in the diff with additions are scanned.
82    On,
83}
84
85impl ValueEnum for LinesChangedOnly {
86    /// Get a list possible value variants for display in `--help` output.
87    fn value_variants<'a>() -> &'a [Self] {
88        &[
89            LinesChangedOnly::Off,
90            LinesChangedOnly::Diff,
91            LinesChangedOnly::On,
92        ]
93    }
94
95    /// Get a display value (for `--help` output) of the enum variant.
96    fn to_possible_value(&self) -> Option<PossibleValue> {
97        match self {
98            LinesChangedOnly::Off => Some(
99                PossibleValue::new("false")
100                    .help("All lines in a file are analyzed.")
101                    .aliases(["off", "0"]),
102            ),
103            LinesChangedOnly::Diff => Some(PossibleValue::new("diff").help(
104                "All lines in the diff are analyzed \
105                    (including unchanged lines but not subtractions).",
106            )),
107            LinesChangedOnly::On => Some(
108                PossibleValue::new("true")
109                    .help("Only lines in the diff that contain additions are analyzed.")
110                    .aliases(["on", "1"]),
111            ),
112        }
113    }
114
115    /// Parse a string into a [`LinesChangedOnly`] enum variant.
116    fn from_str(val: &str, ignore_case: bool) -> Result<LinesChangedOnly, String> {
117        let val = if ignore_case {
118            val.to_lowercase()
119        } else {
120            val.to_string()
121        };
122        match val.as_str() {
123            "true" | "on" | "1" => Ok(LinesChangedOnly::On),
124            "diff" => Ok(LinesChangedOnly::Diff),
125            _ => Ok(LinesChangedOnly::Off),
126        }
127    }
128}
129
130impl LinesChangedOnly {
131    pub fn is_change_valid(&self, added_lines: bool, diff_chunks: bool) -> bool {
132        match self {
133            LinesChangedOnly::Off => true,
134            LinesChangedOnly::Diff => diff_chunks,
135            LinesChangedOnly::On => added_lines,
136        }
137    }
138}
139
140impl Display for LinesChangedOnly {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        match self {
143            LinesChangedOnly::Off => write!(f, "false"),
144            LinesChangedOnly::Diff => write!(f, "diff"),
145            LinesChangedOnly::On => write!(f, "true"),
146        }
147    }
148}
149
150/// An enum to describe `--thread-comments` CLI option's behavior.
151#[derive(PartialEq, Clone, Debug)]
152pub enum ThreadComments {
153    /// Always post a new comment and delete any outdated ones.
154    On,
155    /// Do not post thread comments.
156    Off,
157    /// Only update existing thread comments.
158    /// If none exist, then post a new one.
159    Update,
160}
161
162impl ValueEnum for ThreadComments {
163    /// Get a list possible value variants for display in `--help` output.
164    fn value_variants<'a>() -> &'a [Self] {
165        &[Self::On, Self::Off, Self::Update]
166    }
167
168    /// Get a display value (for `--help` output) of the enum variant.
169    fn to_possible_value(&self) -> Option<PossibleValue> {
170        match self {
171            ThreadComments::On => Some(
172                PossibleValue::new("true")
173                    .help("Always post a new comment and delete any outdated ones.")
174                    .aliases(["on", "1"]),
175            ),
176            ThreadComments::Off => Some(
177                PossibleValue::new("false")
178                    .help("Do not post thread comments.")
179                    .aliases(["off", "0"]),
180            ),
181            ThreadComments::Update => {
182                Some(PossibleValue::new("update").help(
183                    "Only update existing thread comments. If none exist, then post a new one.",
184                ))
185            }
186        }
187    }
188
189    /// Parse a string into a [`ThreadComments`] enum variant.
190    fn from_str(val: &str, ignore_case: bool) -> Result<ThreadComments, String> {
191        let val = if ignore_case {
192            val.to_lowercase()
193        } else {
194            val.to_string()
195        };
196        match val.as_str() {
197            "true" | "on" | "1" => Ok(ThreadComments::On),
198            "update" => Ok(ThreadComments::Update),
199            _ => Ok(ThreadComments::Off),
200        }
201    }
202}
203
204impl Display for ThreadComments {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        match self {
207            ThreadComments::On => write!(f, "true"),
208            ThreadComments::Off => write!(f, "false"),
209            ThreadComments::Update => write!(f, "update"),
210        }
211    }
212}
213
214/// A data structure to contain CLI options that relate to
215/// clang-tidy or clang-format arguments.
216#[derive(Debug, Clone, Default)]
217pub struct ClangParams {
218    pub tidy_checks: String,
219    pub lines_changed_only: LinesChangedOnly,
220    pub database: Option<PathBuf>,
221    pub extra_args: Vec<String>,
222    pub database_json: Option<Vec<CompilationUnit>>,
223    pub style: String,
224    pub clang_tidy_command: Option<PathBuf>,
225    pub clang_format_command: Option<PathBuf>,
226    pub tidy_filter: Option<FileFilter>,
227    pub format_filter: Option<FileFilter>,
228    pub tidy_review: bool,
229    pub format_review: bool,
230}
231
232impl From<&Cli> for ClangParams {
233    /// Construct a [`ClangParams`] instance from a [`Cli`] instance.
234    fn from(args: &Cli) -> Self {
235        ClangParams {
236            tidy_checks: args.tidy_options.tidy_checks.clone(),
237            lines_changed_only: args.source_options.lines_changed_only.clone(),
238            database: args.tidy_options.database.clone(),
239            extra_args: args.tidy_options.extra_arg.clone(),
240            database_json: None,
241            style: args.format_options.style.clone(),
242            clang_tidy_command: None,
243            clang_format_command: None,
244            tidy_filter: args.tidy_options.ignore_tidy.as_ref().map(|ignore_tidy| {
245                FileFilter::new(ignore_tidy, args.source_options.extensions.clone())
246            }),
247            format_filter: args
248                .format_options
249                .ignore_format
250                .as_ref()
251                .map(|ignore_format| {
252                    FileFilter::new(ignore_format, args.source_options.extensions.clone())
253                }),
254            tidy_review: args.feedback_options.tidy_review,
255            format_review: args.feedback_options.format_review,
256        }
257    }
258}
259
260/// A struct to contain CLI options that relate to
261/// [`ResApiClient.post_feedback()`](fn@crate::rest_api::ResApiClient.post_feedback()).
262pub struct FeedbackInput {
263    pub thread_comments: ThreadComments,
264    pub no_lgtm: bool,
265    pub step_summary: bool,
266    pub file_annotations: bool,
267    pub style: String,
268    pub tidy_review: bool,
269    pub format_review: bool,
270    pub passive_reviews: bool,
271}
272
273impl From<&Cli> for FeedbackInput {
274    /// Construct a [`FeedbackInput`] instance from a [`Cli`] instance.
275    fn from(args: &Cli) -> Self {
276        FeedbackInput {
277            style: args.format_options.style.clone(),
278            no_lgtm: args.feedback_options.no_lgtm,
279            step_summary: args.feedback_options.step_summary,
280            thread_comments: args.feedback_options.thread_comments.clone(),
281            file_annotations: args.feedback_options.file_annotations,
282            tidy_review: args.feedback_options.tidy_review,
283            format_review: args.feedback_options.format_review,
284            passive_reviews: args.feedback_options.passive_reviews,
285        }
286    }
287}
288
289impl Default for FeedbackInput {
290    /// Construct a [`FeedbackInput`] instance with default values.
291    fn default() -> Self {
292        FeedbackInput {
293            thread_comments: ThreadComments::Off,
294            no_lgtm: true,
295            step_summary: false,
296            file_annotations: true,
297            style: "llvm".to_string(),
298            tidy_review: false,
299            format_review: false,
300            passive_reviews: false,
301        }
302    }
303}
304
305#[cfg(test)]
306mod test {
307    // use crate::cli::get_arg_parser;
308
309    use std::{path::PathBuf, str::FromStr};
310
311    use crate::{cli::RequestedVersion, common_fs::normalize_path};
312
313    use super::{Cli, LinesChangedOnly, ThreadComments};
314    use clap::{Parser, ValueEnum};
315
316    #[test]
317    fn parse_positional() {
318        let cli = Cli::parse_from(["cpp-linter", "file1.c", "file2.h"]);
319        let not_ignored = cli.not_ignored.expect("failed to parse positional args");
320        assert!(!not_ignored.is_empty());
321        assert!(not_ignored.contains(&String::from("file1.c")));
322        assert!(not_ignored.contains(&String::from("file2.h")));
323    }
324
325    #[test]
326    fn display_lines_changed_only_enum() {
327        let input = "Diff";
328        assert_eq!(
329            LinesChangedOnly::from_str(&input, true).unwrap(),
330            LinesChangedOnly::Diff
331        );
332        assert_eq!(format!("{}", LinesChangedOnly::Diff), input.to_lowercase());
333
334        assert_eq!(
335            LinesChangedOnly::from_str(&input, false).unwrap(),
336            LinesChangedOnly::Off
337        );
338    }
339
340    #[test]
341    fn display_thread_comments_enum() {
342        let input = "Update";
343        assert_eq!(
344            ThreadComments::from_str(input, true).unwrap(),
345            ThreadComments::Update
346        );
347        assert_eq!(format!("{}", ThreadComments::Update), input.to_lowercase());
348        assert_eq!(
349            ThreadComments::from_str(input, false).unwrap(),
350            ThreadComments::Off
351        );
352    }
353
354    #[test]
355    fn validate_version_path() {
356        let this_path_str = "src/cli/structs.rs";
357        let this_path = PathBuf::from(this_path_str);
358        let this_canonical = this_path.canonicalize().unwrap();
359        let parent = this_canonical.parent().unwrap();
360        let expected = normalize_path(parent);
361        let req_ver = RequestedVersion::from_str(this_path_str).unwrap();
362        if let RequestedVersion::Path(parsed) = req_ver {
363            assert_eq!(&parsed, &expected);
364        }
365
366        assert!(RequestedVersion::from_str("file.rs").is_err());
367    }
368}