Skip to content

Commit 44fd585

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. Adjusted tests accordingly.
1 parent badba09 commit 44fd585

10 files changed

Lines changed: 308 additions & 255 deletions

File tree

cpp-linter/src/clang_tools/clang_format.rs

Lines changed: 84 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,22 @@
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 crate::{
15+
clang_tools::make_patch, cli::ClangParams, common_fs::FileObj, error::ClangCaptureError,
16+
};
1817

1918
/// A struct to hold clang-format advice for a single file.
2019
#[derive(Debug, Clone, PartialEq, Eq, Default)]
2120
pub struct FormatAdvice {
2221
/// A list of line ranges that clang-format wants to replace.
2322
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-
}
3723
}
3824

3925
/// Get a string that summarizes the given `--style`
@@ -82,14 +68,7 @@ pub fn run_clang_format(
8268
for range in &ranges {
8369
cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
8470
}
85-
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)?;
71+
let cache_path = clang_params.get_cache_path();
9372
let file_name = file.name.to_string_lossy().to_string();
9473
cmd.arg(file.name.to_path_buf().as_os_str());
9574
logs.push((
@@ -109,12 +88,6 @@ pub fn run_clang_format(
10988
task: format!("get fixes from clang-format {file_name}"),
11089
source: e,
11190
})?;
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-
})?;
11891

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

cpp-linter/src/clang_tools/clang_tidy.rs

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

1717
// project-specific modules/crates
18-
use super::MakeSuggestions;
19-
use crate::{
20-
clang_tools::CACHE_DIR, cli::ClangParams, common_fs::FileObj, error::ClangCaptureError,
21-
};
18+
use crate::{cli::ClangParams, common_fs::FileObj, error::ClangCaptureError};
2219

2320
/// Used to deserialize a json compilation database's translation unit.
2421
///
@@ -104,13 +101,10 @@ impl TidyNotification {
104101
pub struct TidyAdvice {
105102
/// A list of notifications parsed from clang-tidy stdout.
106103
pub notes: Vec<TidyNotification>,
107-
108-
/// A path to the cached contents of the file after applying clang-tidy fixes.
109-
pub patched: PathBuf,
110104
}
111105

112-
impl MakeSuggestions for TidyAdvice {
113-
fn get_suggestion_help(&self, start_line: u32, end_line: u32) -> String {
106+
impl TidyAdvice {
107+
pub(crate) fn get_suggestion_help(&self, start_line: u32, end_line: u32) -> String {
114108
let mut diagnostics = vec![];
115109
for note in &self.notes {
116110
for fixed_line in &note.fixed_lines {
@@ -133,10 +127,6 @@ impl MakeSuggestions for TidyAdvice {
133127
diagnostics.join("")
134128
)
135129
}
136-
137-
fn get_tool_name(&self) -> String {
138-
"clang-tidy".to_string()
139-
}
140130
}
141131

142132
/// A regex pattern to capture the clang-tidy note header.
@@ -336,10 +326,8 @@ pub fn run_clang_tidy(
336326
.join(" ")
337327
),
338328
));
339-
let cache_patch_path = clang_params
340-
.repo_root
341-
.join(CACHE_DIR)
342-
.join(file.name.with_added_extension("tidy"));
329+
let cache_path = clang_params.get_cache_path();
330+
let cache_patch_path = cache_path.join(&file.name);
343331
fs::create_dir_all(
344332
cache_patch_path
345333
.parent()
@@ -403,10 +391,8 @@ pub fn run_clang_tidy(
403391
&clang_params.repo_root,
404392
)?;
405393

406-
let tidy_advice = TidyAdvice {
407-
notes,
408-
patched: cache_patch_path.to_path_buf(),
409-
};
394+
let tidy_advice = TidyAdvice { notes };
395+
file.patched_path = Some(cache_patch_path.to_path_buf());
410396
file.tidy_advice = Some(tidy_advice);
411397
Ok(logs)
412398
}
@@ -539,6 +525,7 @@ mod test {
539525
repo_root: tmp_workspace.path().to_path_buf(),
540526
};
541527
let mut file_lock = arc_file.lock().unwrap();
528+
fs::create_dir_all(clang_params.get_cache_path()).unwrap();
542529
let logs = run_clang_tidy(&mut file_lock, &clang_params)
543530
.unwrap()
544531
.into_iter()

0 commit comments

Comments
 (0)