Skip to content

Commit 339337a

Browse files
authored
fix: use diff line index as 1-based line numbers (#389)
After doing some experiments, I found that the `gix_imara_diff::Diff` API uses 0-based line indexes instead of 1-based line numbers. Furthermore the `gix_imara_diff::UnifiedDiff` API uses 1-based line numbers when printing hunk headers (`@@ -3,5 +3,6 @@`). Thus, printing the unified diff into a patch file remains the same as it was already accurate. So this patch increments the line indexes by 1 when they are be compared/normalized to ranges of 1-based line numbers. I left the `print_diff()` function in the test module as a helper/proof when analyzing future regressions.
1 parent 4958912 commit 339337a

3 files changed

Lines changed: 117 additions & 39 deletions

File tree

cpp-linter/src/clang_tools/clang_format.rs

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,13 @@ pub fn run_clang_format(
119119
replacements: diff
120120
.hunks()
121121
.filter_map(|hunk| {
122+
let altered_start = hunk.after.start.saturating_add(1); // convert to 1-based line numbers
123+
let og_start = hunk.before.start.saturating_add(1); // convert to 1-based line numbers
124+
let og_end = hunk.before.end; // exclusive end is inclusive for 1-based line numbers
122125
let replacement = if hunk.is_pure_insertion() {
123-
RangeInclusive::new(hunk.after.start, hunk.after.start)
126+
RangeInclusive::new(altered_start, altered_start)
124127
} else {
125-
RangeInclusive::new(hunk.before.start, hunk.before.end.saturating_sub(1))
128+
RangeInclusive::new(og_start, og_end)
126129
};
127130
if ranges.is_empty() {
128131
Some(replacement)
@@ -241,14 +244,14 @@ fn three_way_diff(ranges: &[RangeInclusive<u32>], tidy_diff: Diff) -> Vec<RangeI
241244

242245
while let Some(tidy_hunk) = tidy_iter.peek() {
243246
// 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);
247+
let before_start = tidy_hunk.before.start.saturating_add(1); // convert to 1-based line numbers
248+
let before_end = tidy_hunk.before.end; // exclusive end is inclusive for 1-based line numbers
249+
let after_start = tidy_hunk.after.start.saturating_add(1); // convert to 1-based line numbers
250+
let after_end = tidy_hunk.after.end; // exclusive end is inclusive for 1-based line numbers
248251
let delta = tidy_hunk.after.len() as i32 - tidy_hunk.before.len() as i32;
249252

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 {
253+
// The tidy hunk is a pure removal that encompasses the og range.
254+
if tidy_hunk.is_pure_removal() && before_start <= og_start && before_end >= og_end {
252255
// Skip the og range and tidy hunk entirely.
253256
// The line shift must still be adjusted for the pure removal though
254257
line_shift += delta;
@@ -272,15 +275,15 @@ fn three_way_diff(ranges: &[RangeInclusive<u32>], tidy_diff: Diff) -> Vec<RangeI
272275
}
273276

274277
// tidy hunk overlaps with the og range in some way (case 4).
275-
if tidy_hunk.before.contains(&og_start) {
278+
if (before_start..=before_end).contains(&og_start) {
276279
merged_start = after_start;
277280
}
278281

279282
// commit the line shift now that the tidy hunk start is checked.
280283
line_shift += delta;
281284

282285
// tidy hunk suffixes the og range.
283-
if tidy_hunk.before.contains(&og_end) {
286+
if (before_start..=before_end).contains(&og_end) {
284287
merged_end = after_end;
285288
tidy_iter.next(); // this tidy hunk is handled.
286289
break; // break from loop to push the merged range into joint_ranges.
@@ -299,8 +302,8 @@ fn three_way_diff(ranges: &[RangeInclusive<u32>], tidy_diff: Diff) -> Vec<RangeI
299302
for tidy_hunk in tidy_iter {
300303
maybe_push_range(
301304
&mut joint_ranges,
302-
tidy_hunk.after.start,
303-
tidy_hunk.after.end.saturating_sub(1),
305+
tidy_hunk.after.start.saturating_add(1), // convert to 1-based line numbers
306+
tidy_hunk.after.end, // exclusive end is inclusive for 1-based line numbers
304307
);
305308
}
306309

@@ -313,9 +316,8 @@ mod tests {
313316

314317
use std::ops::RangeInclusive;
315318

316-
use gix_imara_diff::{Diff, InternedInput};
317-
318319
use super::{summarize_style, three_way_diff};
320+
use crate::clang_tools::make_patch;
319321

320322
fn formalize_style(style: &str, expected: &str) {
321323
assert_eq!(summarize_style(style), expected);
@@ -339,38 +341,70 @@ mod tests {
339341
#[test]
340342
fn three_way_diff_mixed() {
341343
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.
344+
"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12";
345+
// The first hunk line2-3->StringA-B (hunk 2..4 prefixes og range[3..=5]).
346+
// The second hunk line8-11->StringC-D\nline11\n (hunk 8..12 suffixes og range[7..=9]).
347347
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)];
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)];
353353
println!("tidy diff: {tidy_diff:#?}\ncompared to og ranges: {ranges:?}");
354354
let joint_ranges = three_way_diff(&ranges, tidy_diff);
355355
println!("joint ranges: {joint_ranges:#?}");
356-
assert_eq!(joint_ranges, vec![2..=4, 6..=10]);
356+
assert_eq!(joint_ranges, vec![2..=5, 7..=11]);
357357
}
358358

359359
#[test]
360360
fn three_way_diff_separated() {
361361
const OG_SRC: &str =
362362
"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].
363+
// TIDY_SRC removes "line3" which decrements offsets in ranges[5,8] and removes ranges[3,3].
364364
// TIDY_SRC appends StringE, which handles remaining tidy hunks after done iterating ranges
365365
const TIDY_SRC: &str =
366366
"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];
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];
371371
println!("tidy diff: {tidy_diff:#?}\ncompared to og ranges: {ranges:?}");
372372
let joint_ranges = three_way_diff(&ranges, tidy_diff);
373373
println!("joint ranges: {joint_ranges:#?}");
374-
assert_eq!(joint_ranges, vec![4..=7, 9..=10]);
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+
}
375409
}
376410
}

cpp-linter/src/common_fs.rs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ impl FileObj {
133133
pub fn is_hunk_in_diff(&self, hunk: &Hunk) -> Option<(u32, u32)> {
134134
let (start_line, end_line) = if !hunk.before.is_empty() {
135135
// if old hunk's total lines is > 0
136-
let start = hunk.before.start;
137-
(start, start + hunk.before.len() as u32 - 1)
136+
let start = hunk.before.start.saturating_add(1); // convert to 1-based line numbers
137+
(start, hunk.before.start + hunk.before.len() as u32)
138138
} else {
139139
// old hunk's total lines is 0, meaning changes were only added
140-
let start = hunk.after.start;
140+
let start = hunk.after.start.saturating_add(1); // convert to 1-based line numbers
141141
// make old hunk's range span 1 line
142142
(start, start)
143143
};
@@ -207,7 +207,7 @@ impl FileObj {
207207
.map_err(FileObjError::OpenPatchFileFailed)?;
208208
patch_file
209209
.write_all(
210-
format!("--- a/{file_name}\n+++ b/{file_name}\n{unified_diff}",).as_bytes(),
210+
format!("--- a/{file_name}\n+++ b/{file_name}\n{unified_diff}").as_bytes(),
211211
)
212212
.map_err(FileObjError::WritePatchFailed)?;
213213
}
@@ -332,7 +332,7 @@ impl FileObj {
332332
format!(
333333
"Please remove the line(s)\n- {}",
334334
hunk.before
335-
.map(|l| l.to_string())
335+
.map(|l| l.saturating_add(1).to_string())
336336
.collect::<Vec<String>>()
337337
.join("\n- ")
338338
)
@@ -401,10 +401,12 @@ pub(crate) fn mk_path_abs<P: AsRef<Path>>(path: P) -> Result<PathBuf, std::io::E
401401
#[cfg(test)]
402402
mod test {
403403
#![allow(clippy::unwrap_used)]
404-
use std::path::PathBuf;
404+
use std::{fs, path::PathBuf};
405+
406+
use tempfile::{NamedTempFile, TempDir};
405407

406408
use super::FileObj;
407-
use crate::cli::LinesChangedOnly;
409+
use crate::{clang_tools::ReviewComments, cli::LinesChangedOnly};
408410

409411
// *********************** tests for FileObj::get_ranges()
410412

@@ -463,4 +465,35 @@ mod test {
463465
println!("Canonical path: {}", canonical_path.display());
464466
assert!(!canonical_path.to_string_lossy().starts_with(r"\\?\"));
465467
}
468+
469+
#[test]
470+
fn pure_removal_suggestion() {
471+
let repo_root = TempDir::new().unwrap();
472+
let file_name = PathBuf::from("test_file.cpp");
473+
474+
// Write original file with 3 lines
475+
let original_content = "line1\nline2\nline3\n";
476+
fs::write(repo_root.path().join(&file_name), original_content).unwrap();
477+
478+
// Patched file has line2 removed
479+
let patched_content = "line1\nline3\n";
480+
let patched_file = NamedTempFile::new().unwrap();
481+
fs::write(patched_file.path(), patched_content).unwrap();
482+
483+
// line2 is 1-indexed line 2; diff_chunks must contain it
484+
let mut file_obj = FileObj::from(file_name, vec![2], vec![2..=2]);
485+
file_obj.patched_path = Some(patched_file.path().to_path_buf());
486+
487+
let mut review_comments = ReviewComments::default();
488+
file_obj
489+
.make_suggestions_from_patch(&mut review_comments, false, repo_root.path())
490+
.unwrap();
491+
492+
assert_eq!(review_comments.comments.len(), 1);
493+
let suggestion = &review_comments.comments[0].suggestion;
494+
assert!(
495+
suggestion.contains("Please remove the line(s)\n- 2"),
496+
"unexpected suggestion: {suggestion}"
497+
);
498+
}
466499
}

cpp-linter/tests/comments.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![cfg(feature = "bin")]
22
use chrono::Utc;
3+
use clap::builder::styling::{AnsiColor, Color, Style};
34
use cpp_linter::rest_client::USER_OUTREACH;
45
use cpp_linter::run::run_main;
56
use cpp_linter::{cli::ThreadComments, rest_client::COMMENT_MARKER};
@@ -310,7 +311,17 @@ async fn setup(lib_root: &Path, tmp_dir: &TempDir, test_params: &TestParams) {
310311
);
311312
let patch_content =
312313
std::fs::read_to_string(patch_path).expect("Failed to read generated patch file.");
313-
println!("Generated patch content:\n{patch_content}");
314+
println!("Generated patch content:");
315+
for l in patch_content.lines() {
316+
let style = if l.starts_with('+') {
317+
Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green))) // Green for additions
318+
} else if l.starts_with('-') {
319+
Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))) // Red for deletions
320+
} else {
321+
Style::new() // Default style for context lines
322+
};
323+
println!("{style}{l}{style:#}");
324+
}
314325
}
315326

316327
let summary_out_file_abs_path = if test_params.relative_summary_out_file {

0 commit comments

Comments
 (0)