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 Path(PathBuf),
17
18 #[default]
20 SystemDefault,
21
22 Requirement(VersionReq),
26
27 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#[derive(PartialEq, Clone, Debug, Default)]
75pub enum LinesChangedOnly {
76 #[default]
78 Off,
79 Diff,
81 On,
83}
84
85impl ValueEnum for LinesChangedOnly {
86 fn value_variants<'a>() -> &'a [Self] {
88 &[
89 LinesChangedOnly::Off,
90 LinesChangedOnly::Diff,
91 LinesChangedOnly::On,
92 ]
93 }
94
95 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 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#[derive(PartialEq, Clone, Debug)]
152pub enum ThreadComments {
153 On,
155 Off,
157 Update,
160}
161
162impl ValueEnum for ThreadComments {
163 fn value_variants<'a>() -> &'a [Self] {
165 &[Self::On, Self::Off, Self::Update]
166 }
167
168 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 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#[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 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
260pub 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 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 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 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}