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#[derive(PartialEq, Clone, Debug, Default)]
14pub enum LinesChangedOnly {
15 #[default]
17 Off,
18 Diff,
20 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 fn value_variants<'a>() -> &'a [Self] {
38 &[
39 LinesChangedOnly::Off,
40 LinesChangedOnly::Diff,
41 LinesChangedOnly::On,
42 ]
43 }
44
45 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 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 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#[derive(PartialEq, Clone, Debug)]
103pub enum ThreadComments {
104 On,
106 Off,
108 Update,
111}
112
113#[cfg(feature = "bin")]
114impl ValueEnum for ThreadComments {
115 fn value_variants<'a>() -> &'a [Self] {
117 &[Self::On, Self::Off, Self::Update]
118 }
119
120 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 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#[derive(Debug, Clone, Default)]
171pub struct ClangParams {
172 pub tidy_checks: String,
176
177 pub lines_changed_only: LinesChangedOnly,
179
180 pub database: Option<PathBuf>,
182
183 pub extra_args: Vec<String>,
187
188 pub database_json: Option<Vec<CompilationUnit>>,
195
196 pub style: String,
200
201 pub clang_tidy_command: Option<PathBuf>,
207
208 pub clang_format_command: Option<PathBuf>,
214
215 pub tidy_filter: Option<FileFilter>,
217
218 pub format_filter: Option<FileFilter>,
220
221 pub repo_root: PathBuf,
225}
226
227impl ClangParams {
228 pub(crate) const CACHE_DIR: &str = ".cpp-linter-cache";
230
231 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 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
290pub struct FeedbackInput {
293 pub thread_comments: ThreadComments,
295
296 pub no_lgtm: bool,
298
299 pub step_summary: bool,
301
302 pub file_annotations: bool,
304
305 pub style: String,
307
308 pub pr_review: bool,
310
311 pub passive_reviews: bool,
315
316 pub repo_root: PathBuf,
318}
319
320#[cfg(feature = "bin")]
321impl From<&Cli> for FeedbackInput {
322 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 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}