Skip to content

feat: merge review suggestions from both clang tools#358

Open
2bndy5 wants to merge 1 commit into
mainfrom
join-tools-reviews
Open

feat: merge review suggestions from both clang tools#358
2bndy5 wants to merge 1 commit into
mainfrom
join-tools-reviews

Conversation

@2bndy5

@2bndy5 2bndy5 commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

This is a culmination of

As this builds on the plan discussed in cpp-linter/cpp-linter#82, it it the last step needed before providing a patch that consumers can push to auto-fix lints.

Intended flow of data

  1. run clang-tidy before running clang-format
  2. after running clang-tidy save the patched file to cache
  3. after running clang-format:
    • check for clang-tidy patch in cache.
    • if cached patch is present, run clang-format on the clang-tidy patch. This step includes formatting lines that clang-tidy changed and lines that clang-format changed.
    • if cached patch is not present (clang-tidy was not run on the file), then cache the clang-format fixes instead.
    • --lines-changed-only is respected, but this may need tweaking in the future.
  4. when creating a PR review,
    • only add hunks to the patch that are not present in the review comments.
    • I added a parameter to also calculate (and display) how many review comments were reused in previous PR reviews.
    • PR review summaries are worded better if the env var CPP_LINTER_PR_REVIEW_SUMMARY_ONLY = 1. If PR review is a summary only, then the patch will include all fixes that would have been posted in the diff.

Adjusted tests accordingly.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of code review comment generation and patch management.
  • Chores

    • Updated documentation dependency.
    • Internal refactoring of caching and output processing logic for enhanced efficiency.
  • Tests

    • Updated test suite to validate review message handling.

@2bndy5 2bndy5 added the enhancement New feature or request label Jun 14, 2026
@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.63347% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.43%. Comparing base (badba09) to head (9254f94).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
cpp-linter/src/clang_tools/clang_format.rs 79.36% 13 Missing ⚠️
cpp-linter/src/common_fs.rs 89.87% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #358      +/-   ##
==========================================
- Coverage   92.81%   92.43%   -0.38%     
==========================================
  Files          23       23              
  Lines        3450     3528      +78     
==========================================
+ Hits         3202     3261      +59     
- Misses        248      267      +19     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@2bndy5 2bndy5 force-pushed the join-tools-reviews branch 3 times, most recently from b5e7407 to e48fbfc Compare June 16, 2026 00:01
This is a culmination of
- #354
- #347

It it the last step needed before providing a patch that consumers can push to auto-fix lints.

# Intended flow of data
1. run clang-tidy before running clang-format
2. after running clang-tidy save the patched file to cache
3. after running clang-format:
    - check for clang-tidy patch in cache.
    - if cached patch is present, run clang-format on the clang-tidy patch. This step includes formatting lines that clang-tidy changed and lines that clang-format changed.
    - if cached patch is not present (clang-tidy was not run on the file), then cache the clang-format fixes instead.
    - `--lines-changed-only` is respected, but this may need tweaking in the future.
4. when creating a PR review, only add hunks that are not present in the review comments. I added a parameter to also calculate (and display) how many review comments were reused in previous PR reviews. If PR review is a summary only, then the patch will include all fixes that would have been posted in the diff.

Adjusted tests accordingly.
@2bndy5 2bndy5 force-pushed the join-tools-reviews branch from dd4c541 to 9254f94 Compare June 16, 2026 01:16
@2bndy5 2bndy5 marked this pull request as ready for review June 16, 2026 02:23
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR refactors the clang tool pipeline to consolidate cached patched-file ownership into a new FileObj.patched_path field, removing per-tool patched: PathBuf fields from TidyAdvice and FormatAdvice, and deleting the MakeSuggestions trait. ReviewComments aggregation fields are unified to single tool_total/full_patch values, and the summarize signature gains total_review_comments and summary_only parameters. A new get_suggestions helper drives hunk-by-hunk suggestion generation. mkdocs-material is bumped to 9.7.6.

Changes

Unified patched_path & ReviewComments refactor

Layer / File(s) Summary
ClangParams cache path infrastructure
cpp-linter/src/cli/structs.rs, cpp-linter/src/run.rs
Adds CACHE_DIR constant and get_cache_path() method to ClangParams; updates run.rs to use ClangParams::CACHE_DIR instead of the now-removed clang_tools::CACHE_DIR.
FileObj.patched_path, DisplayStringFailed error, and get_suggestions helper
cpp-linter/src/error.rs, cpp-linter/src/common_fs.rs
Extends FileObj with patched_path: Option<PathBuf>, adds the DisplayStringFailed error variant, restructures make_suggestions_from_patch to read from patched_path, and introduces the get_suggestions helper that iterates diff hunks, builds suggestions with tidy help text, and accumulates full_patch/tool_total.
clang-tidy: remove patched field, store path on FileObj
cpp-linter/src/clang_tools/clang_tidy.rs
Removes patched: PathBuf from TidyAdvice, drops MakeSuggestions for TidyAdvice, adds the inherent get_suggestion_help method, changes cache path construction to get_cache_path(), and stores the result in file.patched_path. Updates test setup to pre-create the cache directory.
clang-format: remove patched field, branch on file.patched_path
cpp-linter/src/clang_tools/clang_format.rs
Removes patched: PathBuf from FormatAdvice and MakeSuggestions for FormatAdvice. run_clang_format uses get_cache_path() and make_patch, then branches: runs clang-format -i in-place when file.patched_path exists (prior clang-tidy output), otherwise writes stdout to a new cache path and sets file.patched_path.
ReviewComments model, trait removal, analyze_single_file, and summarize
cpp-linter/src/clang_tools/mod.rs
Removes CACHE_DIR constant and MakeSuggestions trait, reorders analyze_single_file to run clang-tidy before clang-format, fixes Suggestion::as_review_comment line_start edge case, simplifies ReviewComments to single tool_total/full_patch, rewrites summarize with total_review_comments/summary_only params, and adds summarize_reused_reviews unit test.
post_feedback and integration test updates
cpp-linter/src/rest_client.rs, cpp-linter/tests/reviews.rs
Updates post_feedback to compute total_review_comments, derive has_changes from unified tool_total, and call summarize with the expanded signature. Removes patched fields from test fixtures. Updates integration tests to replace per-tool summary helpers with an inline conditional tool_summary regex.

Docs dependency bump

Layer / File(s) Summary
mkdocs-material version bump
docs/pyproject.toml
Bumps mkdocs-material from 9.7.1 to 9.7.6.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • cpp-linter/cpp-linter-rs#347: Modifies the same run_clang_format caching/diff workflow and FormatAdvice structure that this PR further refactors.
  • cpp-linter/cpp-linter-rs#354: Directly connected at the clang_tidy.rs level — refactors TidyAdvice's cached patched path and wires it into patch/suggestion generation, the exact pattern this PR completes.
  • cpp-linter/cpp-linter-rs#349: Changes how working/cache directories are rooted in --repo-root across clang_format.rs and clang_tidy.rs, which is directly related to this PR's get_cache_path() consolidation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: merge review suggestions from both clang tools' directly and accurately describes the main objective of the changeset: merging suggestions from clang-tidy and clang-format into unified PR reviews.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch join-tools-reviews

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp-linter/src/clang_tools/clang_format.rs`:
- Around line 154-185: The joint_ranges are built using hunk.before which
contains original-file line coordinates, but the code is formatting the cached
tidy-patched file where line numbers have shifted due to tidy
insertions/deletions. Replace hunk.before with hunk.after in the map function
that builds joint_ranges to use the correct patched-file coordinates.
Additionally, translate any non-overlapping original ranges from the ranges
parameter before adding them to joint_ranges, since they also refer to
original-file coordinates and must be converted to patched-file coordinates
before being passed as --lines arguments to clang-format.

In `@cpp-linter/src/common_fs.rs`:
- Around line 261-263: The UnifiedDiffPrinter's display_hunk() method requires
that display_header() be called first to emit the hunk header line (the @@ -a,b
+c,d @@ syntax). Locate where display_hunk() is being called on the
BasicLineDiffPrinter instance and ensure that display_header() is invoked on the
printer before any display_hunk() calls to generate valid unified diff output
that can be applied as a patch.

In `@cpp-linter/src/rest_client.rs`:
- Around line 218-233: In summary-only mode, clang-tidy diagnostics without
fixes are not being added to review_comments before the decision logic runs,
causing review_comments.tool_total to be zero and allowing invalid PR approvals.
Before the block that determines options.action and options.summary (which uses
has_changes derived from review_comments.tool_total), ensure that no-fix
clang-tidy diagnostics are included in the review_comments concern count in
summary-only mode. This likely requires modifying make_suggestions_from_patch or
the code that populates review_comments to include no-fix diagnostics in the
tool_total count even when summary_only is true, so the action and summary
decisions reflect all tidy warnings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 94621f1a-8c9c-4e79-9abb-b55f70c74f3d

📥 Commits

Reviewing files that changed from the base of the PR and between d45a232 and 9254f94.

📒 Files selected for processing (10)
  • cpp-linter/src/clang_tools/clang_format.rs
  • cpp-linter/src/clang_tools/clang_tidy.rs
  • cpp-linter/src/clang_tools/mod.rs
  • cpp-linter/src/cli/structs.rs
  • cpp-linter/src/common_fs.rs
  • cpp-linter/src/error.rs
  • cpp-linter/src/rest_client.rs
  • cpp-linter/src/run.rs
  • cpp-linter/tests/reviews.rs
  • docs/pyproject.toml

Comment on lines +154 to +185
let mut cmd = Command::new(cmd_path);
cmd.current_dir(&cache_path);
// edit the clang-tody patched file in-place (`-i`)
cmd.args(["--style", &clang_params.style, "-i"]);
// if ranges is empty, then we're just formatting the entire file.
if !ranges.is_empty() {
// We're concerned about formatting what clang-tidy changed (tidy_diff.hunks().before),
// but we also want to include any clang-format changes that do not overlap clang-tidy fixes.
let mut joint_ranges = tidy_diff
.hunks()
// hunk is partially inclusive: [start, end),
// but clang-format expects fully inclusive line ranges.
// subtract 1 from hunk.before.end
.map(|hunk| {
RangeInclusive::new(hunk.before.start, hunk.before.end.saturating_sub(1))
})
.collect::<Vec<_>>();
for range in &ranges {
let mut contained = false;
for hunk in tidy_diff.hunks() {
if hunk.before.contains(range.start()) && hunk.before.contains(range.end()) {
contained = true;
break;
}
}
if !contained {
joint_ranges.push(range.clone());
}
}
for range in &joint_ranges {
cmd.arg(format!("--lines={}:{}", range.start(), range.end()).as_str());
}

@coderabbitai coderabbitai Bot Jun 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the full context of the file to understand the function
cd cpp-linter && head -n 200 src/clang_tools/clang_format.rs | tail -n 100

Repository: cpp-linter/cpp-linter-rs

Length of output: 4401


🏁 Script executed:

# Let's see the complete function and understand what tidy_diff is
grep -n "tidy_diff" src/clang_tools/clang_format.rs | head -20

Repository: cpp-linter/cpp-linter-rs

Length of output: 133


🏁 Script executed:

# Search for the make_patch function to understand tidy_diff structure
rg "make_patch" --type rust -A 5 -B 2

Repository: cpp-linter/cpp-linter-rs

Length of output: 3857


🏁 Script executed:

# Look for imports to understand the diff library being used
head -50 src/clang_tools/clang_format.rs

Repository: cpp-linter/cpp-linter-rs

Length of output: 159


🏁 Script executed:

# Search for how clang-format is actually invoked and what file is passed
rg "clang-format|--lines=" src/clang_tools/clang_format.rs -B 3 -A 3

Repository: cpp-linter/cpp-linter-rs

Length of output: 140


🏁 Script executed:

# Find test files related to clang_format
find cpp-linter -name "*test*" -o -name "*tests*" | grep -i format

Repository: cpp-linter/cpp-linter-rs

Length of output: 50


🏁 Script executed:

# Search for tests involving tidy and format together
rg "tidy.*format|format.*tidy" cpp-linter --type rust -l

Repository: cpp-linter/cpp-linter-rs

Length of output: 324


🏁 Script executed:

# Check the format_advice logic more carefully to see how it handles hunk.before/hunk.after
rg "is_pure_insertion|hunk.after|hunk.before" cpp-linter/src/clang_tools/clang_format.rs -B 2 -A 2

Repository: cpp-linter/cpp-linter-rs

Length of output: 1074


🏁 Script executed:

# Look for InternedInput to understand the before/after semantics
rg "InternedInput" cpp-linter --type rust -B 3 -A 3

Repository: cpp-linter/cpp-linter-rs

Length of output: 2190


🏁 Script executed:

# Check if there are any range translation utilities
rg "translate|convert.*range" cpp-linter/src/clang_tools/ --type rust -A 2

Repository: cpp-linter/cpp-linter-rs

Length of output: 50


Use patched-file line ranges (hunk.after) when formatting the cached tidy output.

cmd.current_dir(&cache_path) formats the cached tidy-patched file, but joint_ranges is built from tidy_diff.hunks().before, which contains original-file coordinates. Since make_patch(&tidy_patch_contents, &original_contents) computes before as original-file lines and after as patched-file lines, using hunk.before targets the wrong lines. Tidy fixes that insert or delete lines shift line numbers in the patched file; this mismatch can cause clang-format to skip formatting or produce invalid ranges like --lines=10:9 for pure insertions.

Suggested direction
-            let mut joint_ranges = tidy_diff
+            let mut joint_ranges = tidy_diff
                 .hunks()
-                // hunk is partially inclusive: [start, end),
-                // but clang-format expects fully inclusive line ranges.
-                // subtract 1 from hunk.before.end
-                .map(|hunk| {
-                    RangeInclusive::new(hunk.before.start, hunk.before.end.saturating_sub(1))
+                // clang-format is run against the cached patched file, so use
+                // patched-file coordinates and skip empty after-ranges.
+                .filter_map(|hunk| {
+                    let end = hunk.after.end.checked_sub(1)?;
+                    (hunk.after.start <= end).then(|| RangeInclusive::new(hunk.after.start, end))
                 })
                 .collect::<Vec<_>>();

Also translate any non-overlapping original ranges before adding them to joint_ranges, so every --lines argument refers to the cached patched file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp-linter/src/clang_tools/clang_format.rs` around lines 154 - 185, The
joint_ranges are built using hunk.before which contains original-file line
coordinates, but the code is formatting the cached tidy-patched file where line
numbers have shifted due to tidy insertions/deletions. Replace hunk.before with
hunk.after in the map function that builds joint_ranges to use the correct
patched-file coordinates. Additionally, translate any non-overlapping original
ranges from the ranges parameter before adding them to joint_ranges, since they
also refer to original-file coordinates and must be converted to patched-file
coordinates before being passed as --lines arguments to clang-format.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is my main concern. I think I need to adjust the ranges (declared prior to this block) to use the line numbers in the patched file by clang-tidy. Because some of the insertions/deletions may alter the original line numbers, the ranges of original line numbers may be invalid.

Can you suggestion a function that abstracts this block, returns joint_ranges with the adjusted &ranges as compared to the &tidy_diff?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find make_patch definition
rg -n "pub fn make_patch" --type rust -A 10

# Also look for what Diff/UnifiedDiff type is imported/returned
rg -n "UnifiedDiff\|imara_diff\|make_patch" --type rust -B 2 -A 5

Length of output: 5256



@2bndy5 Here's a proposed compute_format_ranges helper. The key idea:

  • Part 1 — Every region clang-tidy changed, expressed in patched-file coordinates using hunk.after (not hunk.before). Pure-deletion hunks (where after.start == after.end) produce nothing for clang-format.
  • Part 2 — Each original range that is not fully contained inside a tidy hunk, translated to patched-file coordinates by accumulating the net line-count shift from all preceding tidy hunks.
/// Compute the `--lines` ranges (in **patched-file coordinates**) to pass to
/// `clang-format -i` when formatting the clang-tidy-patched file.
///
/// # Arguments
/// * `tidy_diff` – diff computed as `make_patch(&tidy_patched, &original)`, so
///   `hunk.before` = original-file range (exclusive end),
///   `hunk.after`  = patched-file range (exclusive end).
/// * `ranges` – original-file line ranges from `--lines-changed-only`.
///   **Caller must only invoke this when `ranges` is non-empty.**
///
/// Returns a vector containing:
/// 1. Every region clang-tidy changed, in patched-file coordinates.
/// 2. Every region from `ranges` that does **not** overlap a clang-tidy hunk,
///    translated to patched-file coordinates.
fn compute_format_ranges(tidy_diff: &Diff, ranges: &[RangeInclusive<u32>]) -> Vec<RangeInclusive<u32>> {
    let tidy_hunks: Vec<_> = tidy_diff.hunks().collect();

    // Part 1 – tidy-changed regions in patched-file coordinates.
    // hunk.after is [start, end); convert to fully-inclusive for clang-format.
    // Skip pure-deletion hunks where after.start == after.end (nothing to format).
    let mut joint_ranges: Vec<RangeInclusive<u32>> = tidy_hunks
        .iter()
        .filter_map(|hunk| {
            let end = hunk.after.end.saturating_sub(1);
            (hunk.after.start <= end).then(|| RangeInclusive::new(hunk.after.start, end))
        })
        .collect();

    // Part 2 – original ranges not covered by tidy, translated to patched coords.
    for range in ranges {
        // Skip ranges fully contained within a tidy hunk's before-range;
        // they are already covered by Part 1 above.
        let contained = tidy_hunks.iter().any(|hunk| {
            hunk.before.contains(range.start()) && hunk.before.contains(range.end())
        });
        if contained {
            continue;
        }

        // Translate by summing the net line-count change of every tidy hunk
        // that ends strictly before this range's start.
        // hunk.before / hunk.after are [start, end) so before.end is exclusive.
        let mut shift: i64 = 0;
        for hunk in &tidy_hunks {
            if hunk.before.end <= *range.start() {
                let before_len = hunk.before.end as i64 - hunk.before.start as i64;
                let after_len  = hunk.after.end  as i64 - hunk.after.start  as i64;
                shift += after_len - before_len;
            }
        }
        let translated_start = (*range.start() as i64 + shift) as u32;
        let translated_end   = (*range.end()   as i64 + shift) as u32;
        joint_ranges.push(RangeInclusive::new(translated_start, translated_end));
    }

    joint_ranges
}

The call-site then becomes:

if !ranges.is_empty() {
    for range in compute_format_ranges(&tidy_diff, &ranges) {
        cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
    }
}

A couple of notes:

  • The shift accumulation intentionally only counts hunks whose before.end <= range.start() — i.e., hunks completely before the range. Since we already skipped ranges overlapping a tidy hunk, the shift at range.start and range.end is the same (they sit in the same inter-hunk gap), so one shift value suffices for both endpoints.
  • If you want to cover ranges that are partially overlapping a tidy hunk (currently silently dropped), that would need an extra split/merge step — but that's probably an edge case worth a follow-up issue rather than bloating this PR.

🧬 Code Graph Analysis Results

cpp-linter/src/clang_tools/clang_format.rs

Joint-range construction block (the part your comment is concerned with)

Lines 159-186

        // if ranges is empty, then we're just formatting the entire file.
        if !ranges.is_empty() {
            // We're concerned about formatting what clang-tidy changed (tidy_diff.hunks().before),
            // but we also want to include any clang-format changes that do not overlap clang-tidy fixes.
            let mut joint_ranges = tidy_diff
                .hunks()
                // hunk is partially inclusive: [start, end),
                // but clang-format expects fully inclusive line ranges.
                // subtract 1 from hunk.before.end
                .map(|hunk| {
                    RangeInclusive::new(hunk.before.start, hunk.before.end.saturating_sub(1))
                })
                .collect::<Vec<_>>();
            for range in &ranges {
                let mut contained = false;
                for hunk in tidy_diff.hunks() {
                    if hunk.before.contains(range.start()) && hunk.before.contains(range.end()) {
                        contained = true;
                        break;
                    }
                }
                if !contained {
                    joint_ranges.push(range.clone());
                }
            }
            for range in &joint_ranges {
                cmd.arg(format!("--lines={}:{}", range.start(), range.end()).as_str());
            }
        }

Where ranges comes from

Lines 67-70

    let ranges = file.get_ranges(&clang_params.lines_changed_only);
    for range in &ranges {
        cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
    }

Where tidy_diff comes from

Lines 153-158

        let (tidy_diff, _) = make_patch(&tidy_patch_contents, &original_contents);
        let mut cmd = Command::new(cmd_path);
        cmd.current_dir(&cache_path);
        // edit the clang-tody patched file in-place (`-i`)
        cmd.args(["--style", &clang_params.style, "-i"]);

cpp-linter/src/common_fs.rs

FileObj fields that ultimately determine what get_ranges() returns

Lines 25-46

pub struct FileObj {
    /// The path to the file.
    pub name: PathBuf,

    /// The list of lines with additions.
    pub added_lines: Vec<u32>,

    /// The list of ranges that span only lines with additions.
    pub added_ranges: Vec<RangeInclusive<u32>>,

    /// The list of ranges that span the lines present in diff chunks.
    pub diff_chunks: Vec<RangeInclusive<u32>>,

    /// The collection of clang-format advice for this file.
    pub format_advice: Option<FormatAdvice>,

    /// The collection of clang-format advice for this file.
    pub tidy_advice: Option<TidyAdvice>,

    /// A path to a cached file with all/any patches applied.
    pub(crate) patched_path: Option<PathBuf>,
}

get_ranges() implementation (used to produce ranges in clang_format.rs)

Lines 48-324 (relevant excerpt: get_ranges)

    /// Get the list of line ranges to consider based on the given
    /// [`LinesChangedOnly`] configuration.
    pub fn get_ranges(&self, lines_changed_only: &LinesChangedOnly) -> Vec<RangeInclusive<u32>> {
        match lines_changed_only {
            LinesChangedOnly::Diff => self.diff_chunks.to_vec(),
            LinesChangedOnly::On => self.added_ranges.to_vec(),
            _ => Vec::new(),
        }
    }

cpp-linter/src/clang_tools/mod.rs

Patch/diff creation used to compute tidy_diff and the formatting diff hunks

Lines 328-336

pub fn make_patch<'buffer>(
    patched: &'buffer str,
    original_content: &'buffer str,
) -> (Diff, InternedInput<&'buffer str>) {
    let input = InternedInput::new(original_content, patched);
    let mut diff = Diff::compute(gix_imara_diff::Algorithm::Histogram, &input);
    diff.postprocess_lines(&input);
    (diff, input)
}

🧠 Learnings used
Learnt from: 2bndy5
Repo: cpp-linter/cpp-linter-rs PR: 279
File: clang-installer/src/version.rs:301-304
Timestamp: 2026-03-30T06:22:32.553Z
Learning: In this repo’s Rust tests (run via `cargo-nextest` configured by `.config/nextest.toml`), each test runs in its own process. Therefore, `unsafe { std::env::set_var(...) }` used inside tests is safe with respect to cross-test environment variable pollution, and you generally do not need to implement a save-and-restore guard for env vars in test code.

Learnt from: 2bndy5
Repo: cpp-linter/cpp-linter-rs PR: 279
File: cpp-linter/src/run.rs:6-6
Timestamp: 2026-03-30T06:29:55.617Z
Learning: When reviewing Rust code in this repo, don’t assume `#![cfg(feature = "bin")]` feature-gated modules are excluded from integration test execution. The project’s test runner (`nur test`, from `nurfile`) always invokes `cargo llvm-cov nextest` with `--features bin`, so code gated by the `bin` feature will be compiled during tests. Only flag test-coverage/test-discovery concerns if the gating is not controlled by the `bin` feature (or if the test invocation in CI is changed to not pass `--features bin`).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to cover ranges that are partially overlapping a tidy hunk (currently silently dropped), that would need an extra split/merge step — but that's probably an edge case worth a follow-up issue rather than bloating this PR.

That's not an edge case. Its the complexity that I was implicating. I'm not dismissing the proposal, rather I was expecting more (thus the new function to abstract the complexity).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@2bndy5, fair point — partial overlap is the real scenario to handle. Here's an updated compute_format_ranges that fully handles it by splitting each range around every overlapping tidy hunk, dropping the covered fragments, and translating the survivors to patched-file coordinates:

/// Compute the `--lines` ranges (in **patched-file coordinates**) to pass to
/// `clang-format -i` when formatting the clang-tidy-patched file.
///
/// # Arguments
/// * `tidy_diff` – diff computed as `make_patch(&tidy_patched, &original)`, so:
///   - `hunk.before` = original-file range `[start, end)` (exclusive end)
///   - `hunk.after`  = patched-file range `[start, end)` (exclusive end)
/// * `ranges` – original-file line ranges from `--lines-changed-only`.
///   **Caller is responsible for only calling this when `ranges` is non-empty.**
///
/// Returns the minimal set of fully-inclusive line ranges for the patched file,
/// covering:
/// 1. Every region clang-tidy changed (`hunk.after`).
/// 2. Every fragment of `ranges` that does NOT overlap a tidy hunk,
///    translated to patched-file coordinates.
fn compute_format_ranges(tidy_diff: &Diff, ranges: &[RangeInclusive<u32>]) -> Vec<RangeInclusive<u32>> {
    let tidy_hunks: Vec<_> = tidy_diff.hunks().collect();

    // Part 1 – tidy-changed regions in patched-file (after) coordinates.
    // hunk.after is [start, end) exclusive; convert to fully-inclusive.
    // Pure-deletion hunks (after.start == after.end) produce nothing to format.
    let mut joint_ranges: Vec<RangeInclusive<u32>> = tidy_hunks
        .iter()
        .filter_map(|hunk| {
            let end = hunk.after.end.checked_sub(1)?;
            (hunk.after.start <= end).then(|| RangeInclusive::new(hunk.after.start, end))
        })
        .collect();

    // Part 2 – fragments of each original `range` that lie entirely in the
    // gaps *between* tidy hunks, translated to patched-file coordinates.
    //
    // Algorithm:
    //   For each range, progressively subtract every tidy hunk's before-range
    //   [hunk.before.start, hunk.before.end), keeping only the leftover
    //   sub-segments.  Then translate each surviving sub-segment by the
    //   cumulative net-line-count change of all preceding tidy hunks.
    for range in ranges {
        // Work with inclusive coordinates throughout.
        let mut remaining: Vec<RangeInclusive<u32>> = vec![range.clone()];

        for hunk in &tidy_hunks {
            // hunk.before = [start, end) in original coords.
            let hunk_start = hunk.before.start;
            let hunk_end_excl = hunk.before.end; // exclusive

            let mut survivors = Vec::new();
            for seg in remaining {
                // Fragment of `seg` strictly before this hunk.
                if *seg.start() < hunk_start {
                    let end = (*seg.end()).min(hunk_start.saturating_sub(1));
                    survivors.push(RangeInclusive::new(*seg.start(), end));
                }
                // Fragment of `seg` strictly after this hunk.
                if *seg.end() >= hunk_end_excl {
                    let start = (*seg.start()).max(hunk_end_excl);
                    survivors.push(RangeInclusive::new(start, *seg.end()));
                }
                // The overlapping fragment is intentionally dropped — it is
                // already covered by Part 1 (hunk.after).
            }
            remaining = survivors;
        }

        // Translate each surviving sub-segment to patched-file coordinates.
        // The shift at a point P is the sum of (after_len - before_len) for
        // every tidy hunk whose before-range ends strictly before P.
        // Because each sub-segment lies entirely within a single inter-hunk
        // gap, both its start and end have the same shift.
        for seg in remaining {
            let mut shift: i64 = 0;
            for hunk in &tidy_hunks {
                if hunk.before.end <= *seg.start() {
                    let before_len = hunk.before.end as i64 - hunk.before.start as i64;
                    let after_len  = hunk.after.end  as i64 - hunk.after.start  as i64;
                    shift += after_len - before_len;
                }
            }
            let translated_start = (*seg.start() as i64 + shift) as u32;
            let translated_end   = (*seg.end()   as i64 + shift) as u32;
            joint_ranges.push(RangeInclusive::new(translated_start, translated_end));
        }
    }

    joint_ranges
}

The call-site simplifies to:

if !ranges.is_empty() {
    for range in compute_format_ranges(&tidy_diff, &ranges) {
        cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
    }
}

How the three cases are handled:

Case Behaviour
Range entirely inside a tidy hunk's before Splits leave nothing; already covered by Part 1
Range with no overlap with any tidy hunk No splits; translated by cumulative shift of preceding hunks
Range partially overlapping one or more hunks Split into fragments; overlapping fragments dropped (covered by Part 1), outer fragments translated by appropriate shift

The shift accumulation is safe because each surviving sub-segment sits in a gap between two adjacent hunks (or before the first / after the last), so every point within it has the same cumulative shift.

🐇 ~(__)


🧠 Learnings used
Learnt from: 2bndy5
Repo: cpp-linter/cpp-linter-rs PR: 279
File: clang-installer/src/version.rs:301-304
Timestamp: 2026-03-30T06:22:32.553Z
Learning: In this repo’s Rust tests (run via `cargo-nextest` configured by `.config/nextest.toml`), each test runs in its own process. Therefore, `unsafe { std::env::set_var(...) }` used inside tests is safe with respect to cross-test environment variable pollution, and you generally do not need to implement a save-and-restore guard for env vars in test code.

Learnt from: 2bndy5
Repo: cpp-linter/cpp-linter-rs PR: 279
File: cpp-linter/src/run.rs:6-6
Timestamp: 2026-03-30T06:29:55.617Z
Learning: When reviewing Rust code in this repo, don’t assume `#![cfg(feature = "bin")]` feature-gated modules are excluded from integration test execution. The project’s test runner (`nur test`, from `nurfile`) always invokes `cargo llvm-cov nextest` with `--features bin`, so code gated by the `bin` feature will be compiled during tests. Only flag test-coverage/test-discovery concerns if the gating is not controlled by the `bin` feature (or if the test invocation in CI is changed to not pass `--features bin`).

Comment on lines +261 to +263
let mut config = UnifiedDiffConfig::default();
config.context_len(0);
let printer = BasicLineDiffPrinter(&input.interner);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In gix-imara-diff 0.2.2, does UnifiedDiffPrinter::display_hunk() include @@ hunk headers, or must display_header() be called separately to produce unified-diff-compliant output?

💡 Result:

In gix-imara-diff 0.2.2, UnifiedDiffPrinter::display_hunk does NOT include the unified-diff hunk header line (the “@@ -a,b +c,d @@” line). For unified-diff-compliant output, display_header must be called separately before display_hunk. [1][2] Evidence: the trait defines three separate required formatting hooks—display_header, display_context_token, and display_hunk—with display_header taking the before/after start and lengths needed to construct the “@@ -… +…” header, while display_hunk only receives the token slices for the before/after ranges (not the header metadata). [1] Additionally, gix-imara-diff’s built-in unified diff output (shown in the crate docs example) has a header line starting with “@@ … @@” before the hunk’s changed lines, consistent with header emission separate from hunk body emission. [3] So: call display_header to emit the “@@ … @@” line, then call display_hunk to emit the context/add/remove lines for that hunk.

Citations:


Call display_header() before display_hunk() to generate valid unified diff hunks.

In gix-imara-diff 0.2.2, the UnifiedDiffPrinter::display_hunk() method does not emit the hunk header line (@@ -a,b +c,d @@). The display_header() method must be called separately before display_hunk() to produce unified-diff-compliant output. Without the header, ReviewComments.full_patch becomes an unapplyable patch fragment.

Suggested fix
-                    printer.display_hunk(
+                    printer.display_header(
+                        &mut patch_buff,
+                        hunk.before.start,
+                        hunk.after.start,
+                        hunk.before.len() as u32,
+                        hunk.after.len() as u32,
+                    )?;
+                    printer.display_hunk(
                         &mut patch_buff,
                         &input.before[hunk.before.start as usize..hunk.before.end as usize],
                         &input.after[hunk.after.start as usize..hunk.after.end as usize],
                     )?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp-linter/src/common_fs.rs` around lines 261 - 263, The UnifiedDiffPrinter's
display_hunk() method requires that display_header() be called first to emit the
hunk header line (the @@ -a,b +c,d @@ syntax). Locate where display_hunk() is
being called on the BasicLineDiffPrinter instance and ensure that
display_header() is invoked on the printer before any display_hunk() calls to
generate valid unified diff output that can be applied as a patch.

Source: MCP tools

Comment on lines +218 to +233
let total_review_comments = options.comments.len() as u32;
self.client.cull_pr_reviews(&mut options).await?;
let has_changes = review_comments.full_patch.iter().any(|p| !p.is_empty());
let has_changes = review_comments.tool_total > 0;
options.action = if feedback_inputs.passive_reviews {
ReviewAction::Comment
} else if options.comments.is_empty() && !has_changes {
ReviewAction::Approve
} else {
ReviewAction::RequestChanges
};
options.summary = review_comments.summarize(&clang_versions, &options.comments);
options.summary = review_comments.summarize(
&clang_versions,
&options.comments,
total_review_comments,
summary_only,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t let summary-only reviews drop no-fix clang-tidy diagnostics.

With CPP_LINTER_PR_REVIEW_SUMMARY_ONLY=1, make_suggestions_from_patch returns before adding clang-tidy diagnostics that have no fixes. Since this block now derives both options.action and options.summary from review_comments.tool_total, a PR with only non-fixable tidy warnings can be approved and summarized as clean.

Fix this by ensuring no-fix tidy diagnostics still contribute to the review concern count in summary-only mode before this action/summary decision is made.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp-linter/src/rest_client.rs` around lines 218 - 233, In summary-only mode,
clang-tidy diagnostics without fixes are not being added to review_comments
before the decision logic runs, causing review_comments.tool_total to be zero
and allowing invalid PR approvals. Before the block that determines
options.action and options.summary (which uses has_changes derived from
review_comments.tool_total), ensure that no-fix clang-tidy diagnostics are
included in the review_comments concern count in summary-only mode. This likely
requires modifying make_suggestions_from_patch or the code that populates
review_comments to include no-fix diagnostics in the tool_total count even when
summary_only is true, so the action and summary decisions reflect all tidy
warnings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant