1use std::{
5 env::{consts::OS, current_dir},
6 fs,
7 path::PathBuf,
8 process::Command,
9 sync::{Arc, Mutex, MutexGuard},
10};
11
12use anyhow::{Context, Result};
13use regex::Regex;
15use serde::Deserialize;
16
17use super::MakeSuggestions;
19use crate::{
20 cli::{ClangParams, LinesChangedOnly},
21 common_fs::{normalize_path, FileObj},
22};
23
24#[derive(Deserialize, Debug, Clone)]
29pub struct CompilationUnit {
30 directory: String,
32
33 file: String,
41}
42
43#[derive(Debug, Clone)]
45pub struct TidyNotification {
46 pub filename: String,
48
49 pub line: u32,
51
52 pub cols: u32,
54
55 pub severity: String,
58
59 pub rationale: String,
61
62 pub diagnostic: String,
64
65 pub suggestion: Vec<String>,
70
71 pub fixed_lines: Vec<u32>,
73}
74
75impl TidyNotification {
76 pub fn diagnostic_link(&self) -> String {
77 if self.diagnostic.starts_with("clang-diagnostic") {
78 return self.diagnostic.clone();
79 }
80 let (category, name) = if self.diagnostic.starts_with("clang-analyzer-") {
81 (
82 "clang-analyzer",
83 self.diagnostic.strip_prefix("clang-analyzer-").unwrap(),
84 )
85 } else {
86 self.diagnostic.split_once('-').unwrap()
87 };
88 format!(
89 "[{}](https://clang.llvm.org/extra/clang-tidy/checks/{category}/{name}.html)",
90 self.diagnostic
91 )
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct TidyAdvice {
98 pub notes: Vec<TidyNotification>,
100 pub patched: Option<Vec<u8>>,
101}
102
103impl MakeSuggestions for TidyAdvice {
104 fn get_suggestion_help(&self, start_line: u32, end_line: u32) -> String {
105 let mut diagnostics = vec![];
106 for note in &self.notes {
107 for fixed_line in ¬e.fixed_lines {
108 if (start_line..=end_line).contains(fixed_line) {
109 diagnostics.push(format!("- {}\n", note.diagnostic_link()));
110 }
111 }
112 }
113 format!(
114 "### clang-tidy {}\n{}",
115 if diagnostics.is_empty() {
116 "suggestion"
117 } else {
118 "diagnostic(s)"
119 },
120 diagnostics.join("")
121 )
122 }
123
124 fn get_tool_name(&self) -> String {
125 "clang-tidy".to_string()
126 }
127}
128
129fn parse_tidy_output(
134 tidy_stdout: &[u8],
135 database_json: &Option<Vec<CompilationUnit>>,
136) -> Result<TidyAdvice> {
137 let note_header = Regex::new(r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+)\]$").unwrap();
138 let fixed_note =
139 Regex::new(r"^.+:(\d+):\d+:\snote: FIX-IT applied suggested code changes$").unwrap();
140 let mut found_fix = false;
141 let mut notification = None;
142 let mut result = Vec::new();
143 let cur_dir = current_dir().unwrap();
144 for line in String::from_utf8(tidy_stdout.to_vec()).unwrap().lines() {
145 if let Some(captured) = note_header.captures(line) {
146 if let Some(note) = notification {
147 result.push(note);
148 }
149
150 let mut filename = PathBuf::from(&captured[1]);
152 if let Some(db_json) = &database_json {
154 let mut found_unit = false;
155 for unit in db_json {
156 let unit_path =
157 PathBuf::from_iter([unit.directory.as_str(), unit.file.as_str()]);
158 if unit_path == filename {
159 filename =
160 normalize_path(&PathBuf::from_iter([&unit.directory, &unit.file]));
161 found_unit = true;
162 break;
163 }
164 }
165 if !found_unit {
166 filename = normalize_path(&PathBuf::from_iter([&cur_dir, &filename]));
170 }
171 } else {
172 filename = normalize_path(&PathBuf::from_iter([&cur_dir, &filename]));
175 }
176 assert!(filename.is_absolute());
177 if filename.is_absolute() && filename.starts_with(&cur_dir) {
178 filename = filename
181 .strip_prefix(&cur_dir)
182 .unwrap()
184 .to_path_buf();
185 }
186
187 notification = Some(TidyNotification {
188 filename: filename.to_string_lossy().to_string().replace('\\', "/"),
189 line: captured[2].parse()?,
190 cols: captured[3].parse()?,
191 severity: String::from(&captured[4]),
192 rationale: String::from(&captured[5]).trim().to_string(),
193 diagnostic: String::from(&captured[6]),
194 suggestion: Vec::new(),
195 fixed_lines: Vec::new(),
196 });
197 found_fix = false;
199 } else if let Some(capture) = fixed_note.captures(line) {
200 let fixed_line = capture[1].parse()?;
201 if let Some(note) = &mut notification {
202 if !note.fixed_lines.contains(&fixed_line) {
203 note.fixed_lines.push(fixed_line);
204 }
205 }
206 found_fix = true;
210 } else if !found_fix {
211 if let Some(note) = &mut notification {
212 note.suggestion.push(line.to_string());
215 }
216 }
217 }
218 if let Some(note) = notification {
219 result.push(note);
220 }
221 Ok(TidyAdvice {
222 notes: result,
223 patched: None,
224 })
225}
226
227pub fn tally_tidy_advice(files: &[Arc<Mutex<FileObj>>]) -> u64 {
229 let mut total = 0;
230 for file in files {
231 let file = file.lock().unwrap();
232 if let Some(advice) = &file.tidy_advice {
233 for tidy_note in &advice.notes {
234 let file_path = PathBuf::from(&tidy_note.filename);
235 if file_path == file.name {
236 total += 1;
237 }
238 }
239 }
240 }
241 total
242}
243
244pub fn run_clang_tidy(
246 file: &mut MutexGuard<FileObj>,
247 clang_params: &ClangParams,
248) -> Result<Vec<(log::Level, std::string::String)>> {
249 let mut cmd = Command::new(clang_params.clang_tidy_command.as_ref().unwrap());
250 let mut logs = vec![];
251 if !clang_params.tidy_checks.is_empty() {
252 cmd.args(["-checks", &clang_params.tidy_checks]);
253 }
254 if let Some(db) = &clang_params.database {
255 cmd.args(["-p", &db.to_string_lossy()]);
256 }
257 for arg in &clang_params.extra_args {
258 cmd.args(["--extra-arg", format!("\"{}\"", arg).as_str()]);
259 }
260 let file_name = file.name.to_string_lossy().to_string();
261 if clang_params.lines_changed_only != LinesChangedOnly::Off {
262 let ranges = file.get_ranges(&clang_params.lines_changed_only);
263 let filter = format!(
264 "[{{\"name\":{:?},\"lines\":{:?}}}]",
265 &file_name.replace('/', if OS == "windows" { "\\" } else { "/" }),
266 ranges
267 .iter()
268 .map(|r| [r.start(), r.end()])
269 .collect::<Vec<_>>()
270 );
271 cmd.args(["--line-filter", filter.as_str()]);
272 }
273 let mut original_content = None;
274 if clang_params.tidy_review {
275 cmd.arg("--fix-errors");
276 original_content = Some(fs::read_to_string(&file.name).with_context(|| {
277 format!(
278 "Failed to cache file's original content before applying clang-tidy changes: {}",
279 file_name.clone()
280 )
281 })?);
282 }
283 if !clang_params.style.is_empty() {
284 cmd.args(["--format-style", clang_params.style.as_str()]);
285 }
286 cmd.arg(file.name.to_string_lossy().as_ref());
287 logs.push((
288 log::Level::Info,
289 format!(
290 "Running \"{} {}\"",
291 cmd.get_program().to_string_lossy(),
292 cmd.get_args()
293 .map(|x| x.to_str().unwrap())
294 .collect::<Vec<&str>>()
295 .join(" ")
296 ),
297 ));
298 let output = cmd.output().unwrap();
299 logs.push((
300 log::Level::Debug,
301 format!(
302 "Output from clang-tidy:\n{}",
303 String::from_utf8_lossy(&output.stdout)
304 ),
305 ));
306 if !output.stderr.is_empty() {
307 logs.push((
308 log::Level::Debug,
309 format!(
310 "clang-tidy made the following summary:\n{}",
311 String::from_utf8_lossy(&output.stderr)
312 ),
313 ));
314 }
315 file.tidy_advice = Some(parse_tidy_output(
316 &output.stdout,
317 &clang_params.database_json,
318 )?);
319 if clang_params.tidy_review {
320 if let Some(tidy_advice) = &mut file.tidy_advice {
321 tidy_advice.patched =
323 Some(fs::read(&file_name).with_context(|| {
324 format!("Failed to read changes from clang-tidy: {file_name}")
325 })?);
326 }
327 fs::write(&file_name, original_content.unwrap())
329 .with_context(|| format!("Failed to restore file's original content: {file_name}"))?;
330 }
331 Ok(logs)
332}
333
334#[cfg(test)]
335mod test {
336 use std::{
337 env,
338 path::PathBuf,
339 sync::{Arc, Mutex},
340 };
341
342 use regex::Regex;
343
344 use crate::{
345 clang_tools::get_clang_tool_exe,
346 cli::{ClangParams, LinesChangedOnly},
347 common_fs::FileObj,
348 };
349
350 use super::run_clang_tidy;
351 use super::TidyNotification;
352
353 #[test]
354 fn clang_diagnostic_link() {
355 let note = TidyNotification {
356 filename: String::from("some_src.cpp"),
357 line: 1504,
358 cols: 9,
359 rationale: String::from("file not found"),
360 severity: String::from("error"),
361 diagnostic: String::from("clang-diagnostic-error"),
362 suggestion: vec![],
363 fixed_lines: vec![],
364 };
365 assert_eq!(note.diagnostic_link(), note.diagnostic);
366 }
367
368 #[test]
369 fn clang_analyzer_link() {
370 let note = TidyNotification {
371 filename: String::from("some_src.cpp"),
372 line: 1504,
373 cols: 9,
374 rationale: String::from(
375 "Dereference of null pointer (loaded from variable 'pipe_num')",
376 ),
377 severity: String::from("warning"),
378 diagnostic: String::from("clang-analyzer-core.NullDereference"),
379 suggestion: vec![],
380 fixed_lines: vec![],
381 };
382 let expected = format!(
383 "[{}](https://clang.llvm.org/extra/clang-tidy/checks/{}/{}.html)",
384 note.diagnostic, "clang-analyzer", "core.NullDereference",
385 );
386 assert_eq!(note.diagnostic_link(), expected);
387 }
388
389 #[test]
392 fn test_capture() {
393 let src = "tests/demo/demo.hpp:11:11: warning: use a trailing return type for this function [modernize-use-trailing-return-type]";
394 let pat = Regex::new(r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+)\]$").unwrap();
395 let cap = pat.captures(src).unwrap();
396 assert_eq!(
397 cap.get(0).unwrap().as_str(),
398 format!(
399 "{}:{}:{}: {}:{}[{}]",
400 cap.get(1).unwrap().as_str(),
401 cap.get(2).unwrap().as_str(),
402 cap.get(3).unwrap().as_str(),
403 cap.get(4).unwrap().as_str(),
404 cap.get(5).unwrap().as_str(),
405 cap.get(6).unwrap().as_str()
406 )
407 .as_str()
408 )
409 }
410
411 #[test]
412 fn use_extra_args() {
413 let exe_path = get_clang_tool_exe(
414 "clang-tidy",
415 env::var("CLANG_VERSION").unwrap_or("".to_string()).as_str(),
416 )
417 .unwrap();
418 let file = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
419 let arc_ref = Arc::new(Mutex::new(file));
420 let extra_args = vec!["-std=c++17".to_string(), "-Wall".to_string()];
421 let clang_params = ClangParams {
422 style: "".to_string(),
423 tidy_checks: "".to_string(), lines_changed_only: LinesChangedOnly::Off,
425 database: None,
426 extra_args: extra_args.clone(), database_json: None,
428 format_filter: None,
429 tidy_filter: None,
430 tidy_review: false,
431 format_review: false,
432 clang_tidy_command: Some(exe_path),
433 clang_format_command: None,
434 };
435 let mut file_lock = arc_ref.lock().unwrap();
436 let logs = run_clang_tidy(&mut file_lock, &clang_params)
437 .unwrap()
438 .into_iter()
439 .filter_map(|(_lvl, msg)| {
440 if msg.contains("Running ") {
441 Some(msg)
442 } else {
443 None
444 }
445 })
446 .collect::<Vec<String>>();
447 let args = &logs
448 .first()
449 .expect("expected a log message about invoked clang-tidy command")
450 .split(' ')
451 .collect::<Vec<&str>>();
452 for arg in &extra_args {
453 let extra_arg = format!("\"{arg}\"");
454 assert!(args.contains(&extra_arg.as_str()));
455 }
456 }
457}