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 replacement = if hunk.is_pure_insertion() {
123 RangeInclusive::new(hunk.after.start, hunk.after.start)
124 } else {
125 RangeInclusive::new(hunk.before.start, hunk.before.end.saturating_sub(1))
126 };
127 if ranges.is_empty() {
128 Some(replacement)
129 } else {
130 if ranges.iter().any(|range| {
132 range.contains(replacement.start()) && range.contains(replacement.end())
133 }) {
134 Some(replacement)
135 } else {
136 None
137 }
138 }
139 })
140 .collect(),
141 };
142
143 if let Some(patched_path) = &file.patched_path
147 && patched_path.exists()
148 {
149 let mut cmd = Command::new(cmd_path);
150 cmd.current_dir(&cache_path);
151 cmd.args(["--style", &clang_params.style, "-i"]);
153 if !ranges.is_empty() {
155 let tidy_patch_contents = fs::read_to_string(patched_path).map_err(|e| {
156 ClangCaptureError::ReadFileFailed {
157 file_name: patched_path.to_string_lossy().to_string(),
158 source: e,
159 }
160 })?;
161 let (tidy_diff, _) = make_patch(&tidy_patch_contents, &original_contents);
162 let joint_ranges = three_way_diff(&ranges, tidy_diff);
163 for range in &joint_ranges {
164 cmd.arg(format!("--lines={}:{}", range.start(), range.end()).as_str());
165 }
166 }
167 cmd.arg(&file_name);
168 let output = cmd
169 .output()
170 .map_err(|e| ClangCaptureError::FailedToRunCommand {
171 task: format!("apply clang-format to clang-tidy fixes ({file_name})"),
172 source: e,
173 })?;
174 if !output.stderr.is_empty() || !output.status.success() {
175 logs.push((
176 log::Level::Debug,
177 format!(
178 "clang-format raised the follow errors about clang-tidy fixes:\n{}",
179 String::from_utf8_lossy(&output.stderr)
180 ),
181 ));
182 }
183 } else {
184 let cache_format_fixes = cache_path.join(&file.name);
187 fs::create_dir_all(
188 cache_format_fixes
189 .parent()
190 .ok_or(ClangCaptureError::UnknownCacheParentPath)?,
191 )
192 .map_err(ClangCaptureError::MkDirFailed)?;
193 fs::write(&cache_format_fixes, &output.stdout).map_err(|e| {
194 ClangCaptureError::WriteFileFailed {
195 file_name: cache_format_fixes.to_string_lossy().to_string(),
196 source: e,
197 }
198 })?;
199 file.patched_path = Some(cache_format_fixes);
200 }
201
202 file.format_advice = Some(format_advice);
203 Ok(logs)
204}
205
206fn three_way_diff(ranges: &[RangeInclusive<u32>], tidy_diff: Diff) -> Vec<RangeInclusive<u32>> {
212 let mut joint_ranges = vec![];
224 let mut tidy_iter = tidy_diff.hunks().peekable();
225 let mut line_shift = 0i32;
226
227 fn maybe_push_range(joint_ranges: &mut Vec<RangeInclusive<u32>>, start: u32, end: u32) {
229 if start <= end {
230 joint_ranges.push(RangeInclusive::new(start, end));
231 }
232 }
233
234 for og_range in ranges {
235 let og_start = *og_range.start();
236 let og_end = *og_range.end();
237
238 let mut merged_start = (og_start as i32 + line_shift) as u32;
240 let mut merged_end = (og_end as i32 + line_shift) as u32;
241
242 while let Some(tidy_hunk) = tidy_iter.peek() {
243 let before_start = tidy_hunk.before.start;
245 let before_end = tidy_hunk.before.end.saturating_sub(1);
246 let after_start = tidy_hunk.after.start;
247 let after_end = tidy_hunk.after.end.saturating_sub(1);
248 let delta = tidy_hunk.after.len() as i32 - tidy_hunk.before.len() as i32;
249
250 if tidy_hunk.is_pure_removal() && before_start == og_start && before_end == og_end {
252 line_shift += delta;
255 merged_end = 0; tidy_iter.next(); break; }
259
260 if before_end < og_start {
262 maybe_push_range(&mut joint_ranges, after_start, after_end);
263 line_shift += delta;
264 tidy_iter.next();
265 continue;
266 }
267
268 if before_start > og_end {
270 break;
272 }
273
274 if tidy_hunk.before.contains(&og_start) {
276 merged_start = after_start;
277 }
278
279 line_shift += delta;
281
282 if tidy_hunk.before.contains(&og_end) {
284 merged_end = after_end;
285 tidy_iter.next(); break; }
288
289 merged_end = (og_end as i32 + line_shift) as u32;
292 tidy_iter.next();
293 }
294
295 maybe_push_range(&mut joint_ranges, merged_start, merged_end);
296 }
297
298 for tidy_hunk in tidy_iter {
300 maybe_push_range(
301 &mut joint_ranges,
302 tidy_hunk.after.start,
303 tidy_hunk.after.end.saturating_sub(1),
304 );
305 }
306
307 joint_ranges
308}
309
310#[cfg(test)]
311mod tests {
312 #![allow(clippy::unwrap_used)]
313
314 use std::ops::RangeInclusive;
315
316 use gix_imara_diff::{Diff, InternedInput};
317
318 use super::{summarize_style, three_way_diff};
319
320 fn formalize_style(style: &str, expected: &str) {
321 assert_eq!(summarize_style(style), expected);
322 }
323
324 #[test]
325 fn formalize_llvm_style() {
326 formalize_style("llvm", "LLVM");
327 }
328
329 #[test]
330 fn formalize_google_style() {
331 formalize_style("google", "Google");
332 }
333
334 #[test]
335 fn formalize_custom_style() {
336 formalize_style("file", "Custom");
337 }
338
339 #[test]
340 fn three_way_diff_mixed() {
341 const OG_SRC: &str =
342 "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11";
343 const TIDY_SRC: &str =
348 "line1\nline2\nStringA\nline4\nline5\nline6\nline7\nStringB\nStringC\nline11\nStringE";
349 let input = InternedInput::new(OG_SRC, TIDY_SRC);
350 let mut tidy_diff = Diff::compute(gix_imara_diff::Algorithm::Histogram, &input);
351 tidy_diff.postprocess_lines(&input);
352 let ranges = vec![RangeInclusive::new(2, 4), RangeInclusive::new(6, 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..=4, 6..=10]);
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 input = InternedInput::new(OG_SRC, TIDY_SRC);
368 let mut tidy_diff = Diff::compute(gix_imara_diff::Algorithm::Histogram, &input);
369 tidy_diff.postprocess_lines(&input);
370 let ranges = vec![2..=2, 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, 9..=10]);
375 }
376}