Skip to content

Commit aa76d57

Browse files
committed
impl 3-way diff and add tests
and some other review changes
1 parent 9254f94 commit aa76d57

2 files changed

Lines changed: 166 additions & 33 deletions

File tree

cpp-linter/src/clang_tools/clang_format.rs

Lines changed: 155 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::{
88
sync::{Arc, Mutex, MutexGuard},
99
};
1010

11+
use gix_imara_diff::Diff;
1112
use log::Level;
1213

1314
// project-specific crates/modules
@@ -145,41 +146,20 @@ pub fn run_clang_format(
145146
if let Some(patched_path) = &file.patched_path
146147
&& patched_path.exists()
147148
{
148-
let tidy_patch_contents =
149-
fs::read_to_string(patched_path).map_err(|e| ClangCaptureError::ReadFileFailed {
150-
file_name: patched_path.to_string_lossy().to_string(),
151-
source: e,
152-
})?;
153-
let (tidy_diff, _) = make_patch(&tidy_patch_contents, &original_contents);
154149
let mut cmd = Command::new(cmd_path);
155150
cmd.current_dir(&cache_path);
156151
// edit the clang-tody patched file in-place (`-i`)
157152
cmd.args(["--style", &clang_params.style, "-i"]);
158153
// if ranges is empty, then we're just formatting the entire file.
159154
if !ranges.is_empty() {
160-
// We're concerned about formatting what clang-tidy changed (tidy_diff.hunks().before),
161-
// but we also want to include any clang-format changes that do not overlap clang-tidy fixes.
162-
let mut joint_ranges = tidy_diff
163-
.hunks()
164-
// hunk is partially inclusive: [start, end),
165-
// but clang-format expects fully inclusive line ranges.
166-
// subtract 1 from hunk.before.end
167-
.map(|hunk| {
168-
RangeInclusive::new(hunk.before.start, hunk.before.end.saturating_sub(1))
169-
})
170-
.collect::<Vec<_>>();
171-
for range in &ranges {
172-
let mut contained = false;
173-
for hunk in tidy_diff.hunks() {
174-
if hunk.before.contains(range.start()) && hunk.before.contains(range.end()) {
175-
contained = true;
176-
break;
177-
}
178-
}
179-
if !contained {
180-
joint_ranges.push(range.clone());
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,
181159
}
182-
}
160+
})?;
161+
let (tidy_diff, _) = make_patch(&tidy_patch_contents, &original_contents);
162+
let joint_ranges = three_way_diff(&ranges, tidy_diff);
183163
for range in &joint_ranges {
184164
cmd.arg(format!("--lines={}:{}", range.start(), range.end()).as_str());
185165
}
@@ -223,11 +203,119 @@ pub fn run_clang_format(
223203
Ok(logs)
224204
}
225205

206+
/// Essentially does a three way diff without the original source that generated the given `ranges` (simplified hunks).
207+
///
208+
/// The returned list of ranges are lines that need formatting in the clang-tidy patched file,
209+
/// provided by the `tidy_diff`. The given `ranges` are the line numbers in the original file
210+
/// that clang-tidy patched.
211+
fn three_way_diff(ranges: &[RangeInclusive<u32>], tidy_diff: Diff) -> Vec<RangeInclusive<u32>> {
212+
// We're concerned about the formatting cases:
213+
//
214+
// 1. changes that clang-tidy made: `tidy_diff.hunks().after`
215+
// 2. changes in the current CI event's diff (`ranges`)
216+
// that clang-tidy did not touch (`tidy_diff.hunks().before`)
217+
// 3. changes that do not overlap clang-tidy fixes: `ranges` - `tidy_diff.hunks().before`
218+
// 4. changes that overlap with clang-tidy fixes. This one is complex because
219+
// - tidy fixes can prefix an og range
220+
// - tidy fixes can suffix an og range
221+
// - tidy fixes can be contained within an og range
222+
// - multiple tidy fixes can (in order) suffix, be contained within, and prefix an og range
223+
let mut joint_ranges = vec![];
224+
let mut tidy_iter = tidy_diff.hunks().peekable();
225+
let mut line_shift = 0i32;
226+
227+
/// Prevent pure removals from causing invalid inclusive ranges.
228+
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+
// track the start and end of a merged range that gets pushed into joint_ranges.
239+
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+
// alias for readability and prevent some repeated calculations
244+
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+
// The tidy hunk is a pure removal that exactly matches the og range.
251+
if tidy_hunk.is_pure_removal() && before_start == og_start && before_end == og_end {
252+
// Skip the og range and tidy hunk entirely.
253+
// The line shift must still be adjusted for the pure removal though
254+
line_shift += delta;
255+
merged_end = 0; // causes invalid inclusive range which does not get pushed.
256+
tidy_iter.next(); // skip this tidy hunk
257+
break; // skip og range and iterate to the next one.
258+
}
259+
260+
// tidy hunk is before the og range.
261+
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+
// tidy hunk is after the og range.
269+
if before_start > og_end {
270+
// handle the og range before iterating the next tidy hunk
271+
break;
272+
}
273+
274+
// tidy hunk overlaps with the og range in some way (case 4).
275+
if tidy_hunk.before.contains(&og_start) {
276+
merged_start = after_start;
277+
}
278+
279+
// commit the line shift now that the tidy hunk start is checked.
280+
line_shift += delta;
281+
282+
// tidy hunk suffixes the og range.
283+
if tidy_hunk.before.contains(&og_end) {
284+
merged_end = after_end;
285+
tidy_iter.next(); // this tidy hunk is handled.
286+
break; // break from loop to push the merged range into joint_ranges.
287+
}
288+
289+
// tidy hunk is contained within the og range.
290+
// so adjust the og range end accordingly and continue iterating tidy hunks
291+
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+
// handle any remaining tidy hunks that are after all og ranges.
299+
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+
226310
#[cfg(test)]
227311
mod tests {
228312
#![allow(clippy::unwrap_used)]
229313

230-
use super::summarize_style;
314+
use std::ops::RangeInclusive;
315+
316+
use gix_imara_diff::{Diff, InternedInput};
317+
318+
use super::{summarize_style, three_way_diff};
231319

232320
fn formalize_style(style: &str, expected: &str) {
233321
assert_eq!(summarize_style(style), expected);
@@ -247,4 +335,42 @@ mod tests {
247335
fn formalize_custom_style() {
248336
formalize_style("file", "Custom");
249337
}
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+
// TIDY_SRC replaces line3->StringA (hunk before=2..3) and
344+
// line8+line9+line10->StringB+StringC (hunk before=7..10), then appends StringE.
345+
// The second hunk's before=7..10 contains og_end=9 but not og_start=6,
346+
// which exercises the "tidy hunk suffixes og range" branch.
347+
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+
// TIDY_SRC removes "line3" (index 2) which decrements offsets in ranges[5,8] and removes ranges[2,2].
364+
// TIDY_SRC appends StringE, which handles remaining tidy hunks after done iterating ranges
365+
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+
}
250376
}

cpp-linter/src/common_fs.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,6 @@ impl FileObj {
187187

188188
self.get_suggestions(review_comments, &diff, &input, summary_only)
189189
.map_err(FileObjError::DisplayStringFailed)?;
190-
if summary_only {
191-
return Ok(());
192-
}
193190
if let Some(advice) = &self.tidy_advice {
194191
// now check for clang-tidy warnings with no fixes applied
195192
let file_ext = self
@@ -203,6 +200,10 @@ impl FileObj {
203200
for note in &advice.notes {
204201
if note.fixed_lines.is_empty() && self.is_line_in_diff(&note.line) {
205202
// notification had no suggestion applied in `patched`
203+
total += 1;
204+
if summary_only {
205+
continue;
206+
}
206207
let mut suggestion = format!(
207208
"### clang-tidy diagnostic\n**{file_name}:{}:{}** {}: [{}]\n\n> {}\n",
208209
&note.line,
@@ -217,7 +218,6 @@ impl FileObj {
217218
.as_str(),
218219
);
219220
}
220-
total += 1;
221221
let mut is_merged = false;
222222
for s in &mut review_comments.comments {
223223
if s.path == file_name
@@ -306,6 +306,13 @@ impl FileObj {
306306
}
307307
}
308308
_ => {
309+
printer.display_header(
310+
&mut patch_buff,
311+
hunk.before.start,
312+
hunk.after.start,
313+
hunk.before.len() as u32,
314+
hunk.after.len() as u32,
315+
)?;
309316
printer.display_hunk(
310317
&mut patch_buff,
311318
&input.before[hunk.before.start as usize..hunk.before.end as usize],

0 commit comments

Comments
 (0)