1use std::{
5 fs,
6 ops::RangeInclusive,
7 process::Command,
8 sync::{Arc, Mutex, MutexGuard},
9};
10
11use gix_imara_diff::Diff;
12use log::Level;
13
14use crate::{
16 clang_tools::make_patch, cli::ClangParams, common_fs::FileObj, error::ClangCaptureError,
17};
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct FormatAdvice {
22 pub replacements: Vec<RangeInclusive<u32>>,
24}
25
26pub fn summarize_style(style: &str) -> String {
28 let mut char_iter = style.chars();
29 if ["google", "chromium", "microsoft", "mozilla", "webkit"].contains(&style)
30 && let Some(first_char) = char_iter.next()
31 {
32 first_char.to_ascii_uppercase().to_string() + char_iter.as_str()
34 } else if style == "llvm" || style == "gnu" {
35 style.to_ascii_uppercase()
36 } else {
37 String::from("Custom")
38 }
39}
40
41pub fn tally_format_advice(files: &[Arc<Mutex<FileObj>>]) -> Result<u64, String> {
43 let mut total = 0;
44 for file in files {
45 let file = file.lock().map_err(|e| e.to_string())?;
46 if let Some(advice) = &file.format_advice
47 && !advice.replacements.is_empty()
48 {
49 total += 1;
50 }
51 }
52 Ok(total)
53}
54
55pub fn run_clang_format(
57 file: &mut MutexGuard<FileObj>,
58 clang_params: &ClangParams,
59) -> Result<Vec<(log::Level, String)>, ClangCaptureError> {
60 let cmd_path = clang_params
61 .clang_format_command
62 .as_ref()
63 .ok_or(ClangCaptureError::ToolPathUnknown("clang-format"))?;
64 let mut cmd = Command::new(cmd_path);
65 cmd.current_dir(&clang_params.repo_root);
66 let mut logs = vec![];
67 cmd.args(["--style", &clang_params.style]);
68 let ranges = file.get_ranges(&clang_params.lines_changed_only);
69 for range in &ranges {
70 cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
71 }
72 let cache_path = clang_params.get_cache_path();
73 let file_name = file.name.to_string_lossy().to_string();
74 cmd.arg(file.name.to_path_buf().as_os_str());
75 logs.push((
76 Level::Info,
77 format!(
78 "Getting format fixes with \"{} {}\"",
79 cmd.get_program().to_string_lossy(),
80 cmd.get_args()
81 .map(|a| a.to_string_lossy())
82 .collect::<Vec<_>>()
83 .join(" ")
84 ),
85 ));
86 let output = cmd
87 .output()
88 .map_err(|e| ClangCaptureError::FailedToRunCommand {
89 task: format!("get fixes from clang-format {file_name}"),
90 source: e,
91 })?;
92
93 if !output.stderr.is_empty() || !output.status.success() {
94 logs.push((
95 log::Level::Debug,
96 format!(
97 "clang-format raised the follow errors:\n{}",
98 String::from_utf8_lossy(&output.stderr)
99 ),
100 ));
101 }
102
103 let original_contents =
105 fs::read_to_string(clang_params.repo_root.join(&file.name)).map_err(|e| {
106 ClangCaptureError::ReadFileFailed {
107 file_name: file_name.clone(),
108 source: e,
109 }
110 })?;
111 let patched_contents = String::from_utf8(output.stdout.to_vec()).map_err(|e| {
112 ClangCaptureError::NonUtf8Output {
113 task: "clang-format".to_string(),
114 source: e,
115 }
116 })?;
117 let (diff, _) = make_patch(&patched_contents, &original_contents);
118 let format_advice = FormatAdvice {
119 replacements: diff
120 .hunks()
121 .filter_map(|hunk| {
122 let altered_start = hunk.after.start.saturating_add(1); let og_start = hunk.before.start.saturating_add(1); let og_end = hunk.before.end; let replacement = if hunk.is_pure_insertion() {
126 RangeInclusive::new(altered_start, altered_start)
127 } else {
128 RangeInclusive::new(og_start, og_end)
129 };
130 if ranges.is_empty() {
131 Some(replacement)
132 } else {
133 if ranges.iter().any(|range| {
135 range.contains(replacement.start()) && range.contains(replacement.end())
136 }) {
137 Some(replacement)
138 } else {
139 None
140 }
141 }
142 })
143 .collect(),
144 };
145
146 if let Some(patched_path) = &file.patched_path
150 && patched_path.exists()
151 {
152 let mut cmd = Command::new(cmd_path);
153 cmd.current_dir(&cache_path);
154 cmd.args(["--style", &clang_params.style, "-i"]);
156 if !ranges.is_empty() {
158 let tidy_patch_contents = fs::read_to_string(patched_path).map_err(|e| {
159 ClangCaptureError::ReadFileFailed {
160 file_name: patched_path.to_string_lossy().to_string(),
161 source: e,
162 }
163 })?;
164 let (tidy_diff, _) = make_patch(&tidy_patch_contents, &original_contents);
165 let joint_ranges = three_way_diff(&ranges, tidy_diff);
166 for range in &joint_ranges {
167 cmd.arg(format!("--lines={}:{}", range.start(), range.end()).as_str());
168 }
169 }
170 cmd.arg(&file_name);
171 let output = cmd
172 .output()
173 .map_err(|e| ClangCaptureError::FailedToRunCommand {
174 task: format!("apply clang-format to clang-tidy fixes ({file_name})"),
175 source: e,
176 })?;
177 if !output.stderr.is_empty() || !output.status.success() {
178 logs.push((
179 log::Level::Debug,
180 format!(
181 "clang-format raised the follow errors about clang-tidy fixes:\n{}",
182 String::from_utf8_lossy(&output.stderr)
183 ),
184 ));
185 }
186 } else {
187 let cache_format_fixes = cache_path.join(&file.name);
190 fs::create_dir_all(
191 cache_format_fixes
192 .parent()
193 .ok_or(ClangCaptureError::UnknownCacheParentPath)?,
194 )
195 .map_err(ClangCaptureError::MkDirFailed)?;
196 fs::write(&cache_format_fixes, &output.stdout).map_err(|e| {
197 ClangCaptureError::WriteFileFailed {
198 file_name: cache_format_fixes.to_string_lossy().to_string(),
199 source: e,
200 }
201 })?;
202 file.patched_path = Some(cache_format_fixes);
203 }
204
205 file.format_advice = Some(format_advice);
206 Ok(logs)
207}
208
209fn three_way_diff(ranges: &[RangeInclusive<u32>], tidy_diff: Diff) -> Vec<RangeInclusive<u32>> {
215 let mut joint_ranges = vec![];
227 let mut tidy_iter = tidy_diff.hunks().peekable();
228 let mut line_shift = 0i32;
229
230 fn maybe_push_range(joint_ranges: &mut Vec<RangeInclusive<u32>>, start: u32, end: u32) {
232 if start <= end {
233 joint_ranges.push(RangeInclusive::new(start, end));
234 }
235 }
236
237 for og_range in ranges {
238 let og_start = *og_range.start();
239 let og_end = *og_range.end();
240
241 let mut merged_start = (og_start as i32 + line_shift) as u32;
243 let mut merged_end = (og_end as i32 + line_shift) as u32;
244
245 while let Some(tidy_hunk) = tidy_iter.peek() {
246 let before_start = tidy_hunk.before.start.saturating_add(1); let before_end = tidy_hunk.before.end; let after_start = tidy_hunk.after.start.saturating_add(1); let after_end = tidy_hunk.after.end; let delta = tidy_hunk.after.len() as i32 - tidy_hunk.before.len() as i32;
252
253 if tidy_hunk.is_pure_removal() && before_start <= og_start && before_end >= og_end {
255 line_shift += delta;
258 merged_end = 0; tidy_iter.next(); break; }
262
263 if before_end < og_start {
265 maybe_push_range(&mut joint_ranges, after_start, after_end);
266 line_shift += delta;
267 tidy_iter.next();
268 continue;
269 }
270
271 if before_start > og_end {
273 break;
275 }
276
277 if (before_start..=before_end).contains(&og_start) {
279 merged_start = after_start;
280 }
281
282 line_shift += delta;
284
285 if (before_start..=before_end).contains(&og_end) {
287 merged_end = after_end;
288 tidy_iter.next(); break; }
291
292 merged_end = (og_end as i32 + line_shift) as u32;
295 tidy_iter.next();
296 }
297
298 maybe_push_range(&mut joint_ranges, merged_start, merged_end);
299 }
300
301 for tidy_hunk in tidy_iter {
303 maybe_push_range(
304 &mut joint_ranges,
305 tidy_hunk.after.start.saturating_add(1), tidy_hunk.after.end, );
308 }
309
310 joint_ranges
311}
312
313#[cfg(test)]
314mod tests {
315 #![allow(clippy::unwrap_used)]
316
317 use std::ops::RangeInclusive;
318
319 use super::{summarize_style, three_way_diff};
320 use crate::clang_tools::make_patch;
321
322 fn formalize_style(style: &str, expected: &str) {
323 assert_eq!(summarize_style(style), expected);
324 }
325
326 #[test]
327 fn formalize_llvm_style() {
328 formalize_style("llvm", "LLVM");
329 }
330
331 #[test]
332 fn formalize_google_style() {
333 formalize_style("google", "Google");
334 }
335
336 #[test]
337 fn formalize_custom_style() {
338 formalize_style("file", "Custom");
339 }
340
341 #[test]
342 fn three_way_diff_mixed() {
343 const OG_SRC: &str =
344 "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12";
345 const TIDY_SRC: &str =
348 "line1\nStringA\nStringB\nline4\nline5\nline6\nline7\nline8\nStringC\nStringD\nline11";
349 let (tidy_diff, _input) = make_patch(TIDY_SRC, OG_SRC);
350 #[cfg(feature = "bin")]
351 print_diff(OG_SRC, TIDY_SRC, &tidy_diff, &_input);
352 let ranges = vec![RangeInclusive::new(3, 5), RangeInclusive::new(7, 9)];
353 println!("tidy diff: {tidy_diff:#?}\ncompared to og ranges: {ranges:?}");
354 let joint_ranges = three_way_diff(&ranges, tidy_diff);
355 println!("joint ranges: {joint_ranges:#?}");
356 assert_eq!(joint_ranges, vec![2..=5, 7..=11]);
357 }
358
359 #[test]
360 fn three_way_diff_separated() {
361 const OG_SRC: &str =
362 "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11";
363 const TIDY_SRC: &str =
366 "line1\nline2\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nStringE";
367 let (tidy_diff, _input) = make_patch(TIDY_SRC, OG_SRC);
368 #[cfg(feature = "bin")]
369 print_diff(OG_SRC, TIDY_SRC, &tidy_diff, &_input);
370 let ranges = vec![3..=3, 5..=8];
371 println!("tidy diff: {tidy_diff:#?}\ncompared to og ranges: {ranges:?}");
372 let joint_ranges = three_way_diff(&ranges, tidy_diff);
373 println!("joint ranges: {joint_ranges:#?}");
374 assert_eq!(joint_ranges, vec![4..=7, 10..=11]);
375 }
376
377 #[cfg(feature = "bin")]
378 fn print_diff(
379 og: &str,
380 altered: &str,
381 diff: &gix_imara_diff::Diff,
382 input: &gix_imara_diff::InternedInput<&str>,
383 ) {
384 use clap::builder::styling::{AnsiColor, Color, Style};
385 use gix_imara_diff::{BasicLineDiffPrinter, UnifiedDiffConfig};
386
387 println!("---\nOG SRC:");
388 for (i, l) in og.lines().enumerate() {
389 println!("{i:>2}|{l}");
390 }
391 println!("---\nALTERED SRC:");
392 for (i, l) in altered.lines().enumerate() {
393 println!("{i:>2}|{l}");
394 }
395 let printer = BasicLineDiffPrinter(&input.interner);
396 let mut config = UnifiedDiffConfig::default();
397 config.context_len(0);
398 let unified = diff.unified_diff(&printer, config, input).to_string();
399 for l in unified.lines() {
400 let style = if l.starts_with('+') {
401 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)))
402 } else if l.starts_with('-') {
403 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red)))
404 } else {
405 Style::new()
406 };
407 println!("{style}{l}{style:#}");
408 }
409 }
410}