Skip to content

Commit b4d99f7

Browse files
committed
feat: merge review suggestions from both clang tools
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.
1 parent 790bc1d commit b4d99f7

6 files changed

Lines changed: 262 additions & 224 deletions

File tree

cpp-linter/src/clang_tools/clang_format.rs

Lines changed: 84 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,23 @@
44
use std::{
55
fs,
66
ops::RangeInclusive,
7-
path::PathBuf,
87
process::Command,
98
sync::{Arc, Mutex, MutexGuard},
109
};
1110

12-
use gix_imara_diff::{Diff, InternedInput};
1311
use log::Level;
1412

1513
// project-specific crates/modules
16-
use super::{CACHE_DIR, MakeSuggestions};
17-
use crate::{cli::ClangParams, common_fs::FileObj, error::ClangCaptureError};
14+
use super::CACHE_DIR;
15+
use crate::{
16+
clang_tools::make_patch, cli::ClangParams, common_fs::FileObj, error::ClangCaptureError,
17+
};
1818

1919
/// A struct to hold clang-format advice for a single file.
2020
#[derive(Debug, Clone, PartialEq, Eq, Default)]
2121
pub struct FormatAdvice {
2222
/// A list of line ranges that clang-format wants to replace.
2323
pub replacements: Vec<RangeInclusive<u32>>,
24-
25-
/// A path to a cached file containing the full contents of the file after applying clang-format fixes.
26-
pub patched: PathBuf,
27-
}
28-
29-
impl MakeSuggestions for FormatAdvice {
30-
fn get_suggestion_help(&self, _start_line: u32, _end_line: u32) -> String {
31-
String::from("### clang-format suggestions\n")
32-
}
33-
34-
fn get_tool_name(&self) -> String {
35-
"clang-format".to_string()
36-
}
3724
}
3825

3926
/// Get a string that summarizes the given `--style`
@@ -83,13 +70,6 @@ pub fn run_clang_format(
8370
cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
8471
}
8572
let cache_path = clang_params.repo_root.join(CACHE_DIR).join("patches");
86-
let cache_format_fixes = cache_path.join(file.name.with_added_extension("format"));
87-
fs::create_dir_all(
88-
cache_format_fixes
89-
.parent()
90-
.ok_or(ClangCaptureError::UnknownCacheParentPath)?,
91-
)
92-
.map_err(ClangCaptureError::MkDirFailed)?;
9373
let file_name = file.name.to_string_lossy().to_string();
9474
cmd.arg(file.name.to_path_buf().as_os_str());
9575
logs.push((
@@ -109,12 +89,6 @@ pub fn run_clang_format(
10989
task: format!("get fixes from clang-format {file_name}"),
11090
source: e,
11191
})?;
112-
fs::write(&cache_format_fixes, &output.stdout).map_err(|e| {
113-
ClangCaptureError::WriteFileFailed {
114-
file_name: cache_format_fixes.to_string_lossy().to_string(),
115-
source: e,
116-
}
117-
})?;
11892

11993
if !output.stderr.is_empty() || !output.status.success() {
12094
logs.push((
@@ -140,9 +114,7 @@ pub fn run_clang_format(
140114
source: e,
141115
}
142116
})?;
143-
let input = InternedInput::new(original_contents.as_str(), patched_contents.as_str());
144-
let mut diff = Diff::compute(gix_imara_diff::Algorithm::Histogram, &input);
145-
diff.postprocess_lines(&input);
117+
let (diff, _) = make_patch(&patched_contents, &original_contents);
146118
let format_advice = FormatAdvice {
147119
replacements: diff
148120
.hunks()
@@ -166,8 +138,86 @@ pub fn run_clang_format(
166138
}
167139
})
168140
.collect(),
169-
patched: cache_format_fixes,
170141
};
142+
143+
// if a clang-tidy patched file exists in cache,
144+
// get the diff between it and the original file,
145+
// then format both clang-tidy fixes and any other changes by clang-format fixes.
146+
if let Some(patched_path) = &file.patched_path
147+
&& patched_path.exists()
148+
{
149+
let tidy_patch_contents =
150+
fs::read_to_string(patched_path).map_err(|e| ClangCaptureError::ReadFileFailed {
151+
file_name: patched_path.to_string_lossy().to_string(),
152+
source: e,
153+
})?;
154+
let (tidy_diff, _) = make_patch(&tidy_patch_contents, &original_contents);
155+
let mut cmd = Command::new(cmd_path);
156+
cmd.current_dir(&cache_path);
157+
// edit the clang-tody patched file in-place (`-i`)
158+
cmd.args(["--style", &clang_params.style, "-i"]);
159+
// if ranges is empty, then we're just formatting the entire file.
160+
if !ranges.is_empty() {
161+
// We're concerned about formatting what clang-tidy changed (tidy_diff.hunks().after),
162+
// but we also want to include any clang-format changes that do not overlap clang-tidy fixes.
163+
let mut joint_ranges = tidy_diff
164+
.hunks()
165+
// hunk is partially inclusive: [start, end),
166+
// but clang-format expects fully inclusive line ranges.
167+
// subtract 1 from hunk.after.end
168+
.map(|hunk| RangeInclusive::new(hunk.after.start, hunk.after.end.saturating_sub(1)))
169+
.collect::<Vec<_>>();
170+
for range in &ranges {
171+
let mut contained = false;
172+
for hunk in tidy_diff.hunks() {
173+
if hunk.after.contains(range.start()) && hunk.after.contains(range.end()) {
174+
contained = true;
175+
break;
176+
}
177+
}
178+
if !contained {
179+
joint_ranges.push(range.clone());
180+
}
181+
}
182+
for range in &joint_ranges {
183+
cmd.arg(format!("--lines={}:{}", range.start(), range.end()).as_str());
184+
}
185+
}
186+
cmd.arg(&file_name);
187+
let output = cmd
188+
.output()
189+
.map_err(|e| ClangCaptureError::FailedToRunCommand {
190+
task: format!("apply clang-format to clang-tidy fixes ({file_name})"),
191+
source: e,
192+
})?;
193+
if !output.stderr.is_empty() || !output.status.success() {
194+
logs.push((
195+
log::Level::Debug,
196+
format!(
197+
"clang-format raised the follow errors about clang-tidy fixes:\n{}",
198+
String::from_utf8_lossy(&output.stderr)
199+
),
200+
));
201+
}
202+
} else {
203+
// clang-tidy was not run on this file,
204+
// so just use the clang-format fixes as the patched content.
205+
let cache_format_fixes = cache_path.join(&file.name);
206+
fs::create_dir_all(
207+
cache_format_fixes
208+
.parent()
209+
.ok_or(ClangCaptureError::UnknownCacheParentPath)?,
210+
)
211+
.map_err(ClangCaptureError::MkDirFailed)?;
212+
fs::write(&cache_format_fixes, &output.stdout).map_err(|e| {
213+
ClangCaptureError::WriteFileFailed {
214+
file_name: cache_format_fixes.to_string_lossy().to_string(),
215+
source: e,
216+
}
217+
})?;
218+
file.patched_path = Some(cache_format_fixes);
219+
}
220+
171221
file.format_advice = Some(format_advice);
172222
Ok(logs)
173223
}

cpp-linter/src/clang_tools/clang_tidy.rs

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ use regex::Regex;
1515
use serde::Deserialize;
1616

1717
// project-specific modules/crates
18-
use super::MakeSuggestions;
1918
use crate::{
2019
clang_tools::CACHE_DIR, cli::ClangParams, common_fs::FileObj, error::ClangCaptureError,
2120
};
@@ -104,13 +103,10 @@ impl TidyNotification {
104103
pub struct TidyAdvice {
105104
/// A list of notifications parsed from clang-tidy stdout.
106105
pub notes: Vec<TidyNotification>,
107-
108-
/// A path to the cached contents of the file after applying clang-tidy fixes.
109-
pub patched: PathBuf,
110106
}
111107

112-
impl MakeSuggestions for TidyAdvice {
113-
fn get_suggestion_help(&self, start_line: u32, end_line: u32) -> String {
108+
impl TidyAdvice {
109+
pub(crate) fn get_suggestion_help(&self, start_line: u32, end_line: u32) -> String {
114110
let mut diagnostics = vec![];
115111
for note in &self.notes {
116112
for fixed_line in &note.fixed_lines {
@@ -133,10 +129,6 @@ impl MakeSuggestions for TidyAdvice {
133129
diagnostics.join("")
134130
)
135131
}
136-
137-
fn get_tool_name(&self) -> String {
138-
"clang-tidy".to_string()
139-
}
140132
}
141133

142134
/// A regex pattern to capture the clang-tidy note header.
@@ -336,10 +328,7 @@ pub fn run_clang_tidy(
336328
.join(" ")
337329
),
338330
));
339-
let cache_patch_path = clang_params
340-
.repo_root
341-
.join(CACHE_DIR)
342-
.join(file.name.with_added_extension("tidy"));
331+
let cache_patch_path = clang_params.repo_root.join(CACHE_DIR).join(&file.name);
343332
fs::create_dir_all(
344333
cache_patch_path
345334
.parent()
@@ -403,10 +392,8 @@ pub fn run_clang_tidy(
403392
&clang_params.repo_root,
404393
)?;
405394

406-
let tidy_advice = TidyAdvice {
407-
notes,
408-
patched: cache_patch_path.to_path_buf(),
409-
};
395+
let tidy_advice = TidyAdvice { notes };
396+
file.patched_path = Some(cache_patch_path.to_path_buf());
410397
file.tidy_advice = Some(tidy_advice);
411398
Ok(logs)
412399
}

0 commit comments

Comments
 (0)