Skip to main content

cpp_linter/cli/
structs.rs

1use std::{fmt::Display, path::PathBuf};
2
3#[cfg(feature = "bin")]
4use clap::{ValueEnum, builder::PossibleValue};
5
6#[cfg(feature = "bin")]
7use super::Cli;
8use crate::clang_tools::clang_tidy::CompilationUnit;
9
10use git_bot_feedback::FileFilter;
11
12/// An enum to describe `--lines-changed-only` CLI option's behavior.
13#[derive(PartialEq, Clone, Debug, Default)]
14pub enum LinesChangedOnly {
15    /// All lines are scanned
16    #[default]
17    Off,
18    /// Only lines in the diff are scanned
19    Diff,
20    /// Only lines in the diff with additions are scanned.
21    On,
22}
23
24impl From<LinesChangedOnly> for git_bot_feedback::LinesChangedOnly {
25    fn from(val: LinesChangedOnly) -> Self {
26        match val {
27            LinesChangedOnly::Off => git_bot_feedback::LinesChangedOnly::Off,
28            LinesChangedOnly::Diff => git_bot_feedback::LinesChangedOnly::Diff,
29            LinesChangedOnly::On => git_bot_feedback::LinesChangedOnly::On,
30        }
31    }
32}
33
34#[cfg(feature = "bin")]
35impl ValueEnum for LinesChangedOnly {
36    /// Get a list possible value variants for display in `--help` output.
37    fn value_variants<'a>() -> &'a [Self] {
38        &[
39            LinesChangedOnly::Off,
40            LinesChangedOnly::Diff,
41            LinesChangedOnly::On,
42        ]
43    }
44
45    /// Get a display value (for `--help` output) of the enum variant.
46    fn to_possible_value(&self) -> Option<PossibleValue> {
47        match self {
48            LinesChangedOnly::Off => Some(
49                PossibleValue::new("false")
50                    .help("All lines in a file are analyzed.")
51                    .aliases(["off", "0"]),
52            ),
53            LinesChangedOnly::Diff => Some(PossibleValue::new("diff").help(
54                "All lines in the diff are analyzed \
55                    (including unchanged lines but not subtractions).",
56            )),
57            LinesChangedOnly::On => Some(
58                PossibleValue::new("true")
59                    .help("Only lines in the diff that contain additions are analyzed.")
60                    .aliases(["on", "1"]),
61            ),
62        }
63    }
64
65    /// Parse a string into a [`LinesChangedOnly`] enum variant.
66    fn from_str(val: &str, ignore_case: bool) -> Result<LinesChangedOnly, String> {
67        let val = if ignore_case {
68            val.to_lowercase()
69        } else {
70            val.to_string()
71        };
72        match val.as_str() {
73            "true" | "on" | "1" => Ok(LinesChangedOnly::On),
74            "diff" => Ok(LinesChangedOnly::Diff),
75            _ => Ok(LinesChangedOnly::Off),
76        }
77    }
78}
79
80impl LinesChangedOnly {
81    /// Is the instance valid for under the given conditions/flags?
82    pub fn is_change_valid(&self, added_lines: bool, diff_chunks: bool) -> bool {
83        match self {
84            LinesChangedOnly::Off => true,
85            LinesChangedOnly::Diff => diff_chunks,
86            LinesChangedOnly::On => added_lines,
87        }
88    }
89}
90
91impl Display for LinesChangedOnly {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        match self {
94            LinesChangedOnly::Off => write!(f, "false"),
95            LinesChangedOnly::Diff => write!(f, "diff"),
96            LinesChangedOnly::On => write!(f, "true"),
97        }
98    }
99}
100
101/// An enum to describe `--thread-comments` CLI option's behavior.
102#[derive(PartialEq, Clone, Debug)]
103pub enum ThreadComments {
104    /// Always post a new comment and delete any outdated ones.
105    On,
106    /// Do not post thread comments.
107    Off,
108    /// Only update existing thread comments.
109    /// If none exist, then post a new one.
110    Update,
111}
112
113#[cfg(feature = "bin")]
114impl ValueEnum for ThreadComments {
115    /// Get a list possible value variants for display in `--help` output.
116    fn value_variants<'a>() -> &'a [Self] {
117        &[Self::On, Self::Off, Self::Update]
118    }
119
120    /// Get a display value (for `--help` output) of the enum variant.
121    fn to_possible_value(&self) -> Option<PossibleValue> {
122        match self {
123            ThreadComments::On => Some(
124                PossibleValue::new("true")
125                    .help("Always post a new comment and delete any outdated ones.")
126                    .aliases(["on", "1"]),
127            ),
128            ThreadComments::Off => Some(
129                PossibleValue::new("false")
130                    .help("Do not post thread comments.")
131                    .aliases(["off", "0"]),
132            ),
133            ThreadComments::Update => {
134                Some(PossibleValue::new("update").help(
135                    "Only update existing thread comments. If none exist, then post a new one.",
136                ))
137            }
138        }
139    }
140
141    /// Parse a string into a [`ThreadComments`] enum variant.
142    fn from_str(val: &str, ignore_case: bool) -> Result<ThreadComments, String> {
143        let val = if ignore_case {
144            val.to_lowercase()
145        } else {
146            val.to_string()
147        };
148        match val.as_str() {
149            "true" | "on" | "1" => Ok(ThreadComments::On),
150            "update" => Ok(ThreadComments::Update),
151            _ => Ok(ThreadComments::Off),
152        }
153    }
154}
155
156impl Display for ThreadComments {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            ThreadComments::On => write!(f, "true"),
160            ThreadComments::Off => write!(f, "false"),
161            ThreadComments::Update => write!(f, "update"),
162        }
163    }
164}
165
166/// A data structure to contain CLI options that relate to
167/// clang-tidy or clang-format arguments.
168///
169/// This struct is designed to be a thread-safe vehicle for common clang arguments and configurations.
170#[derive(Debug, Clone, Default)]
171pub struct ClangParams {
172    /// The clang-tidy checks to run.
173    ///
174    /// Format of this string follows the `-checks` argument of clang-tidy.
175    pub tidy_checks: String,
176
177    /// Focus on changed lines or entire files.
178    pub lines_changed_only: LinesChangedOnly,
179
180    /// An optional path to a compilation database, used for clang-tidy.
181    pub database: Option<PathBuf>,
182
183    /// Extra arguments to pass to clang-tidy.
184    ///
185    /// Format of these strings follows the `-extra-arg` argument of clang-tidy.
186    pub extra_args: Vec<String>,
187
188    /// An optional list of compilation units, used for clang-tidy.
189    ///
190    /// This can be set to None initially, but it will be populated by
191    /// [`capture_clang_tools_output()`](crate::clang_tools::capture_clang_tools_output),
192    /// if the [`Self::database`] is given [`Some`] valid value (and the
193    /// compile_commands.json file is parsed successfully).
194    pub database_json: Option<Vec<CompilationUnit>>,
195
196    /// The clang-format style to use.
197    ///
198    /// Format of this string follows the `-style` argument of clang-format.
199    pub style: String,
200
201    /// An optional path to the clang-tidy executable.
202    ///
203    /// If [`Self::tidy_checks`] is not `-*`, then this will be populated by
204    /// [`capture_clang_tools_output()`](crate::clang_tools::capture_clang_tools_output),
205    /// regardless if this is given [`Some`] value.
206    pub clang_tidy_command: Option<PathBuf>,
207
208    /// An optional path to the clang-format executable.
209    ///
210    /// If [`Self::style`] is not an empty string, then this will be populated by
211    /// [`capture_clang_tools_output()`](crate::clang_tools::capture_clang_tools_output),
212    /// regardless if this is given [`Some`] value.
213    pub clang_format_command: Option<PathBuf>,
214
215    /// An optional [`FileFilter`] to exclude files only from clang-tidy analysis.
216    pub tidy_filter: Option<FileFilter>,
217
218    /// An optional [`FileFilter`] to exclude files only from clang-format analysis.
219    pub format_filter: Option<FileFilter>,
220
221    /// The root of the repository, used to locate relative file paths in processing.
222    ///
223    /// A project-specific cache folder is created in this path.
224    pub repo_root: PathBuf,
225}
226
227impl ClangParams {
228    /// The directory name to use for caching clang-tidy and clang-format results.
229    pub(crate) const CACHE_DIR: &str = ".cpp-linter-cache";
230
231    /// The file name for aggregating auto-fixes into a unified patch.
232    pub(crate) const AUTO_FIX_PATCH: &str = "auto-fix.patch";
233
234    pub(crate) fn get_cache_path(&self) -> PathBuf {
235        self.repo_root.join(Self::CACHE_DIR).join("patched")
236    }
237}
238
239#[cfg(feature = "bin")]
240impl From<&Cli> for ClangParams {
241    /// Construct a [`ClangParams`] instance from a [`Cli`] instance.
242    fn from(args: &Cli) -> Self {
243        let extensions: Vec<&str> = args
244            .source_options
245            .extensions
246            .iter()
247            .map(|ext| ext.as_str())
248            .collect();
249        let tidy_filter = args.tidy_options.ignore_tidy.as_ref().map(|ignore_tidy| {
250            let ignore_tidy: Vec<&str> = ignore_tidy.iter().map(|s| s.as_str()).collect();
251            FileFilter::new(&ignore_tidy, &extensions.clone(), Some("clang-tidy"))
252        });
253        let format_filter = args
254            .format_options
255            .ignore_format
256            .as_ref()
257            .map(|ignore_format| {
258                let ignore_format: Vec<&str> = ignore_format.iter().map(|s| s.as_str()).collect();
259                FileFilter::new(&ignore_format, &extensions, Some("clang-format"))
260            });
261        let repo_root = args.source_options.repo_root.clone();
262        let database = args
263            .tidy_options
264            .database
265            .as_ref()
266            .map(PathBuf::from)
267            .map(|db| {
268                if db.is_relative() {
269                    repo_root.join(db)
270                } else {
271                    db
272                }
273            });
274        ClangParams {
275            tidy_checks: args.tidy_options.tidy_checks.clone(),
276            lines_changed_only: args.source_options.lines_changed_only.clone(),
277            database,
278            extra_args: args.tidy_options.extra_arg.clone(),
279            database_json: None,
280            style: args.format_options.style.clone(),
281            clang_tidy_command: None,
282            clang_format_command: None,
283            tidy_filter,
284            format_filter,
285            repo_root,
286        }
287    }
288}
289
290/// A struct to contain CLI options that relate to
291/// [`RestClient.post_feedback()`](fn@crate::rest_api::RestClient.post_feedback()).
292pub struct FeedbackInput {
293    /// How thread comments are created or updated.
294    pub thread_comments: ThreadComments,
295
296    /// Whether to omit a "LGTM" type message.
297    pub no_lgtm: bool,
298
299    /// Whether to post a step summary comment.
300    pub step_summary: bool,
301
302    /// Whether to post file annotations.
303    pub file_annotations: bool,
304
305    /// The clang-format style to show in file annotations.
306    pub style: String,
307
308    /// Whether to post a PR review.
309    pub pr_review: bool,
310
311    /// Should PR reviews be commentary?
312    ///
313    /// If false, reviews will approve or request changes.
314    pub passive_reviews: bool,
315
316    /// The root of the repository, used to locate relative file paths in processing.
317    pub repo_root: PathBuf,
318}
319
320#[cfg(feature = "bin")]
321impl From<&Cli> for FeedbackInput {
322    /// Construct a [`FeedbackInput`] instance from a [`Cli`] instance.
323    fn from(args: &Cli) -> Self {
324        FeedbackInput {
325            style: args.format_options.style.clone(),
326            no_lgtm: args.feedback_options.no_lgtm,
327            step_summary: args.feedback_options.step_summary,
328            thread_comments: args.feedback_options.thread_comments.clone(),
329            file_annotations: args.feedback_options.file_annotations,
330            pr_review: args.feedback_options.pr_review,
331            passive_reviews: args.feedback_options.passive_reviews,
332            repo_root: args.source_options.repo_root.clone(),
333        }
334    }
335}
336
337impl Default for FeedbackInput {
338    /// Construct a [`FeedbackInput`] instance with default values.
339    fn default() -> Self {
340        FeedbackInput {
341            thread_comments: ThreadComments::Off,
342            no_lgtm: true,
343            step_summary: false,
344            file_annotations: true,
345            style: "llvm".to_string(),
346            pr_review: false,
347            passive_reviews: false,
348            repo_root: PathBuf::from("."),
349        }
350    }
351}
352
353#[cfg(all(test, feature = "bin"))]
354mod test {
355    #![allow(clippy::unwrap_used, clippy::expect_used)]
356
357    use clap::{Parser, ValueEnum};
358
359    use super::{ClangParams, Cli, LinesChangedOnly, ThreadComments};
360
361    #[test]
362    fn parse_positional() {
363        let cli = Cli::parse_from(["cpp-linter", "file1.c", "file2.h"]);
364        let not_ignored = cli.not_ignored.expect("failed to parse positional args");
365        assert!(!not_ignored.is_empty());
366        assert!(not_ignored.contains(&String::from("file1.c")));
367        assert!(not_ignored.contains(&String::from("file2.h")));
368    }
369
370    #[test]
371    fn display_lines_changed_only_enum() {
372        let input = "Diff";
373        assert_eq!(
374            LinesChangedOnly::from_str(input, true).unwrap(),
375            LinesChangedOnly::Diff
376        );
377        assert_eq!(format!("{}", LinesChangedOnly::Diff), input.to_lowercase());
378
379        assert_eq!(
380            LinesChangedOnly::from_str(input, false).unwrap(),
381            LinesChangedOnly::Off
382        );
383    }
384
385    #[test]
386    fn display_thread_comments_enum() {
387        let input = "Update";
388        assert_eq!(
389            ThreadComments::from_str(input, true).unwrap(),
390            ThreadComments::Update
391        );
392        assert_eq!(format!("{}", ThreadComments::Update), input.to_lowercase());
393        assert_eq!(
394            ThreadComments::from_str(input, false).unwrap(),
395            ThreadComments::Off
396        );
397    }
398
399    #[test]
400    fn absolute_db_path() {
401        let tmp_dir = tempfile::tempdir().unwrap();
402        let cli = Cli::parse_from(["cpp-linter", "--database", tmp_dir.path().to_str().unwrap()]);
403        let clang_params = ClangParams::from(&cli);
404        assert_eq!(clang_params.database, Some(tmp_dir.path().to_path_buf()));
405    }
406}