Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 86 additions & 35 deletions cpp-linter/src/clang_tools/clang_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,22 @@
use std::{
fs,
ops::RangeInclusive,
path::PathBuf,
process::Command,
sync::{Arc, Mutex, MutexGuard},
};

use gix_imara_diff::{Diff, InternedInput};
use log::Level;

// project-specific crates/modules
use super::{CACHE_DIR, MakeSuggestions};
use crate::{cli::ClangParams, common_fs::FileObj, error::ClangCaptureError};
use crate::{
clang_tools::make_patch, cli::ClangParams, common_fs::FileObj, error::ClangCaptureError,
};

/// A struct to hold clang-format advice for a single file.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FormatAdvice {
/// A list of line ranges that clang-format wants to replace.
pub replacements: Vec<RangeInclusive<u32>>,

/// A path to a cached file containing the full contents of the file after applying clang-format fixes.
pub patched: PathBuf,
}

impl MakeSuggestions for FormatAdvice {
fn get_suggestion_help(&self, _start_line: u32, _end_line: u32) -> String {
String::from("### clang-format suggestions\n")
}

fn get_tool_name(&self) -> String {
"clang-format".to_string()
}
}

/// Get a string that summarizes the given `--style`
Expand Down Expand Up @@ -82,14 +68,7 @@ pub fn run_clang_format(
for range in &ranges {
cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
}
let cache_path = clang_params.repo_root.join(CACHE_DIR).join("patches");
let cache_format_fixes = cache_path.join(file.name.with_added_extension("format"));
fs::create_dir_all(
cache_format_fixes
.parent()
.ok_or(ClangCaptureError::UnknownCacheParentPath)?,
)
.map_err(ClangCaptureError::MkDirFailed)?;
let cache_path = clang_params.get_cache_path();
let file_name = file.name.to_string_lossy().to_string();
cmd.arg(file.name.to_path_buf().as_os_str());
logs.push((
Expand All @@ -109,12 +88,6 @@ pub fn run_clang_format(
task: format!("get fixes from clang-format {file_name}"),
source: e,
})?;
fs::write(&cache_format_fixes, &output.stdout).map_err(|e| {
ClangCaptureError::WriteFileFailed {
file_name: cache_format_fixes.to_string_lossy().to_string(),
source: e,
}
})?;

if !output.stderr.is_empty() || !output.status.success() {
logs.push((
Expand All @@ -140,9 +113,7 @@ pub fn run_clang_format(
source: e,
}
})?;
let input = InternedInput::new(original_contents.as_str(), patched_contents.as_str());
let mut diff = Diff::compute(gix_imara_diff::Algorithm::Histogram, &input);
diff.postprocess_lines(&input);
let (diff, _) = make_patch(&patched_contents, &original_contents);
let format_advice = FormatAdvice {
replacements: diff
.hunks()
Expand All @@ -166,8 +137,88 @@ pub fn run_clang_format(
}
})
.collect(),
patched: cache_format_fixes,
};

// if a clang-tidy patched file exists in cache,
// get the diff between it and the original file,
// then format both clang-tidy fixes and any other changes by clang-format fixes.
if let Some(patched_path) = &file.patched_path
&& patched_path.exists()
{
let tidy_patch_contents =
fs::read_to_string(patched_path).map_err(|e| ClangCaptureError::ReadFileFailed {
file_name: patched_path.to_string_lossy().to_string(),
source: e,
})?;
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"]);
// 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());
}
Comment on lines +154 to +185

@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`).

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

}
cmd.arg(&file_name);
let output = cmd
.output()
.map_err(|e| ClangCaptureError::FailedToRunCommand {
task: format!("apply clang-format to clang-tidy fixes ({file_name})"),
source: e,
})?;
if !output.stderr.is_empty() || !output.status.success() {
logs.push((
log::Level::Debug,
format!(
"clang-format raised the follow errors about clang-tidy fixes:\n{}",
String::from_utf8_lossy(&output.stderr)
),
));
}
} else {
// clang-tidy was not run on this file,
// so just use the clang-format fixes as the patched content.
let cache_format_fixes = cache_path.join(&file.name);
fs::create_dir_all(
cache_format_fixes
.parent()
.ok_or(ClangCaptureError::UnknownCacheParentPath)?,
)
.map_err(ClangCaptureError::MkDirFailed)?;
fs::write(&cache_format_fixes, &output.stdout).map_err(|e| {
ClangCaptureError::WriteFileFailed {
file_name: cache_format_fixes.to_string_lossy().to_string(),
source: e,
}
})?;
file.patched_path = Some(cache_format_fixes);
}

file.format_advice = Some(format_advice);
Ok(logs)
}
Expand Down
29 changes: 8 additions & 21 deletions cpp-linter/src/clang_tools/clang_tidy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ use regex::Regex;
use serde::Deserialize;

// project-specific modules/crates
use super::MakeSuggestions;
use crate::{
clang_tools::CACHE_DIR, cli::ClangParams, common_fs::FileObj, error::ClangCaptureError,
};
use crate::{cli::ClangParams, common_fs::FileObj, error::ClangCaptureError};

/// Used to deserialize a json compilation database's translation unit.
///
Expand Down Expand Up @@ -104,13 +101,10 @@ impl TidyNotification {
pub struct TidyAdvice {
/// A list of notifications parsed from clang-tidy stdout.
pub notes: Vec<TidyNotification>,

/// A path to the cached contents of the file after applying clang-tidy fixes.
pub patched: PathBuf,
}

impl MakeSuggestions for TidyAdvice {
fn get_suggestion_help(&self, start_line: u32, end_line: u32) -> String {
impl TidyAdvice {
pub(crate) fn get_suggestion_help(&self, start_line: u32, end_line: u32) -> String {
let mut diagnostics = vec![];
for note in &self.notes {
for fixed_line in &note.fixed_lines {
Expand All @@ -133,10 +127,6 @@ impl MakeSuggestions for TidyAdvice {
diagnostics.join("")
)
}

fn get_tool_name(&self) -> String {
"clang-tidy".to_string()
}
}

/// A regex pattern to capture the clang-tidy note header.
Expand Down Expand Up @@ -336,10 +326,8 @@ pub fn run_clang_tidy(
.join(" ")
),
));
let cache_patch_path = clang_params
.repo_root
.join(CACHE_DIR)
.join(file.name.with_added_extension("tidy"));
let cache_path = clang_params.get_cache_path();
let cache_patch_path = cache_path.join(&file.name);
fs::create_dir_all(
cache_patch_path
.parent()
Expand Down Expand Up @@ -403,10 +391,8 @@ pub fn run_clang_tidy(
&clang_params.repo_root,
)?;

let tidy_advice = TidyAdvice {
notes,
patched: cache_patch_path.to_path_buf(),
};
let tidy_advice = TidyAdvice { notes };
file.patched_path = Some(cache_patch_path.to_path_buf());
file.tidy_advice = Some(tidy_advice);
Ok(logs)
}
Expand Down Expand Up @@ -539,6 +525,7 @@ mod test {
repo_root: tmp_workspace.path().to_path_buf(),
};
let mut file_lock = arc_file.lock().unwrap();
fs::create_dir_all(clang_params.get_cache_path()).unwrap();
let logs = run_clang_tidy(&mut file_lock, &clang_params)
.unwrap()
.into_iter()
Expand Down
Loading
Loading