Skip to content

Commit 668f4d9

Browse files
feat: capture and output clang tool's version number (#54)
This affects thread-comments, step-summary, and PR review summary. I also added tests to ensure PR review summary only shows relevant information about the tools that were actually used. --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent d33a656 commit 668f4d9

File tree

9 files changed

+181
-73
lines changed

9 files changed

+181
-73
lines changed

cpp-linter/src/clang_tools/clang_tidy.rs

+9-10
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ impl MakeSuggestions for TidyAdvice {
133133
fn parse_tidy_output(
134134
tidy_stdout: &[u8],
135135
database_json: &Option<Vec<CompilationUnit>>,
136-
) -> Result<Option<TidyAdvice>> {
136+
) -> Result<TidyAdvice> {
137137
let note_header = Regex::new(r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+)\]$").unwrap();
138138
let fixed_note =
139139
Regex::new(r"^.+:(\d+):\d+:\snote: FIX-IT applied suggested code changes$").unwrap();
@@ -218,14 +218,10 @@ fn parse_tidy_output(
218218
if let Some(note) = notification {
219219
result.push(note);
220220
}
221-
if result.is_empty() {
222-
Ok(None)
223-
} else {
224-
Ok(Some(TidyAdvice {
225-
notes: result,
226-
patched: None,
227-
}))
228-
}
221+
Ok(TidyAdvice {
222+
notes: result,
223+
patched: None,
224+
})
229225
}
230226

231227
/// Get a total count of clang-tidy advice from the given list of [FileObj]s.
@@ -316,7 +312,10 @@ pub fn run_clang_tidy(
316312
),
317313
));
318314
}
319-
file.tidy_advice = parse_tidy_output(&output.stdout, &clang_params.database_json)?;
315+
file.tidy_advice = Some(parse_tidy_output(
316+
&output.stdout,
317+
&clang_params.database_json,
318+
)?);
320319
if clang_params.tidy_review {
321320
if let Some(tidy_advice) = &mut file.tidy_advice {
322321
// cache file changes in a buffer and restore the original contents for further analysis by clang-format

cpp-linter/src/clang_tools/mod.rs

+68-27
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use anyhow::{anyhow, Context, Result};
1313
use git2::{DiffOptions, Patch};
1414
// non-std crates
1515
use lenient_semver;
16+
use regex::Regex;
1617
use semver::Version;
1718
use tokio::task::JoinSet;
1819
use which::{which, which_in};
@@ -135,6 +136,28 @@ fn analyze_single_file(
135136
Ok((file.name.clone(), logs))
136137
}
137138

139+
/// A struct to contain the version numbers of the clang-tools used
140+
#[derive(Default)]
141+
pub struct ClangVersions {
142+
/// The clang-format version used.
143+
pub format_version: Option<String>,
144+
145+
/// The clang-tidy version used.
146+
pub tidy_version: Option<String>,
147+
}
148+
149+
/// Run `clang-tool --version`, then extract and return the version number.
150+
fn capture_clang_version(clang_tool: &PathBuf) -> Result<String> {
151+
let output = Command::new(clang_tool).arg("--version").output()?;
152+
let stdout = String::from_utf8_lossy(&output.stdout);
153+
let version_pattern = Regex::new(r"(?i)version\s*([\d.]+)").unwrap();
154+
let captures = version_pattern.captures(&stdout).ok_or(anyhow!(
155+
"Failed to find version number in `{} --version` output",
156+
clang_tool.to_string_lossy()
157+
))?;
158+
Ok(captures.get(1).unwrap().as_str().to_string())
159+
}
160+
138161
/// Runs clang-tidy and/or clang-format and returns the parsed output from each.
139162
///
140163
/// If `tidy_checks` is `"-*"` then clang-tidy is not executed.
@@ -144,30 +167,29 @@ pub async fn capture_clang_tools_output(
144167
version: &str,
145168
clang_params: &mut ClangParams,
146169
rest_api_client: &impl RestApiClient,
147-
) -> Result<()> {
170+
) -> Result<ClangVersions> {
171+
let mut clang_versions = ClangVersions::default();
148172
// find the executable paths for clang-tidy and/or clang-format and show version
149173
// info as debugging output.
150174
if clang_params.tidy_checks != "-*" {
151-
clang_params.clang_tidy_command = {
152-
let cmd = get_clang_tool_exe("clang-tidy", version)?;
153-
log::debug!(
154-
"{} --version\n{}",
155-
&cmd.to_string_lossy(),
156-
String::from_utf8_lossy(&Command::new(&cmd).arg("--version").output()?.stdout)
157-
);
158-
Some(cmd)
159-
}
175+
let exe_path = get_clang_tool_exe("clang-tidy", version)?;
176+
let version_found = capture_clang_version(&exe_path)?;
177+
log::debug!(
178+
"{} --version: v{version_found}",
179+
&exe_path.to_string_lossy()
180+
);
181+
clang_versions.tidy_version = Some(version_found);
182+
clang_params.clang_tidy_command = Some(exe_path);
160183
};
161184
if !clang_params.style.is_empty() {
162-
clang_params.clang_format_command = {
163-
let cmd = get_clang_tool_exe("clang-format", version)?;
164-
log::debug!(
165-
"{} --version\n{}",
166-
&cmd.to_string_lossy(),
167-
String::from_utf8_lossy(&Command::new(&cmd).arg("--version").output()?.stdout)
168-
);
169-
Some(cmd)
170-
}
185+
let exe_path = get_clang_tool_exe("clang-format", version)?;
186+
let version_found = capture_clang_version(&exe_path)?;
187+
log::debug!(
188+
"{} --version: v{version_found}",
189+
&exe_path.to_string_lossy()
190+
);
191+
clang_versions.format_version = Some(version_found);
192+
clang_params.clang_format_command = Some(exe_path);
171193
};
172194

173195
// parse database (if provided) to match filenames when parsing clang-tidy's stdout
@@ -199,7 +221,7 @@ pub async fn capture_clang_tools_output(
199221
rest_api_client.end_log_group();
200222
}
201223
}
202-
Ok(())
224+
Ok(clang_versions)
203225
}
204226

205227
/// A struct to describe a single suggestion in a pull_request review.
@@ -221,7 +243,7 @@ pub struct ReviewComments {
221243
///
222244
/// This differs from `comments.len()` because some suggestions may
223245
/// not fit within the file's diff.
224-
pub tool_total: [u32; 2],
246+
pub tool_total: [Option<u32>; 2],
225247
/// A list of comment suggestions to be posted.
226248
///
227249
/// These suggestions are guaranteed to fit in the file's diff.
@@ -234,11 +256,28 @@ pub struct ReviewComments {
234256
}
235257

236258
impl ReviewComments {
237-
pub fn summarize(&self) -> String {
259+
pub fn summarize(&self, clang_versions: &ClangVersions) -> String {
238260
let mut body = format!("{COMMENT_MARKER}## Cpp-linter Review\n");
239261
for t in 0u8..=1 {
240262
let mut total = 0;
241-
let tool_name = if t == 0 { "clang-format" } else { "clang-tidy" };
263+
let (tool_name, tool_version) = if t == 0 {
264+
("clang-format", clang_versions.format_version.as_ref())
265+
} else {
266+
("clang-tidy", clang_versions.tidy_version.as_ref())
267+
};
268+
269+
let tool_total = if let Some(total) = self.tool_total[t as usize] {
270+
total
271+
} else {
272+
// review was not requested from this tool or the tool was not used at all
273+
continue;
274+
};
275+
276+
// If the tool's version is unknown, then we don't need to output this line.
277+
// NOTE: If the tool was invoked at all, then the tool's version shall be known.
278+
if let Some(ver_str) = tool_version {
279+
body.push_str(format!("### Used {tool_name} {ver_str}\n").as_str());
280+
}
242281
for comment in &self.comments {
243282
if comment
244283
.suggestion
@@ -248,11 +287,10 @@ impl ReviewComments {
248287
}
249288
}
250289

251-
if total != self.tool_total[t as usize] {
290+
if total != tool_total {
252291
body.push_str(
253292
format!(
254-
"\nOnly {} out of {} {tool_name} concerns fit within this pull request's diff.\n",
255-
self.tool_total[t as usize], total
293+
"\nOnly {total} out of {tool_total} {tool_name} concerns fit within this pull request's diff.\n",
256294
)
257295
.as_str(),
258296
);
@@ -351,6 +389,7 @@ pub trait MakeSuggestions {
351389
.with_context(|| format!("Failed to convert patch to string: {file_name}"))?
352390
.as_str(),
353391
);
392+
review_comments.tool_total[is_tidy_tool as usize].get_or_insert(0);
354393
if summary_only {
355394
return Ok(());
356395
}
@@ -408,7 +447,9 @@ pub trait MakeSuggestions {
408447
review_comments.comments.push(comment);
409448
}
410449
}
411-
review_comments.tool_total[is_tidy_tool as usize] += hunks_in_patch;
450+
review_comments.tool_total[is_tidy_tool as usize] = Some(
451+
review_comments.tool_total[is_tidy_tool as usize].unwrap_or_default() + hunks_in_patch,
452+
);
412453
Ok(())
413454
}
414455
}

cpp-linter/src/common_fs/mod.rs

+10-7
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,13 @@ impl FileObj {
158158
.unwrap_or_default()
159159
.to_str()
160160
.unwrap_or_default();
161+
// Count of clang-tidy diagnostics that had no fixes applied
162+
let mut total = 0;
161163
for note in &advice.notes {
162164
if note.fixed_lines.is_empty() {
163165
// notification had no suggestion applied in `patched`
164166
let mut suggestion = format!(
165-
"### clang-tidy diagnostic\n**{}:{}:{}** {}: [{}]\n> {}",
166-
file_name,
167+
"### clang-tidy diagnostic\n**{file_name}:{}:{}** {}: [{}]\n> {}",
167168
&note.line,
168169
&note.cols,
169170
&note.severity,
@@ -172,10 +173,10 @@ impl FileObj {
172173
);
173174
if !note.suggestion.is_empty() {
174175
suggestion.push_str(
175-
format!("```{}\n{}```", file_ext, &note.suggestion.join("\n")).as_str(),
176+
format!("```{file_ext}\n{}```", &note.suggestion.join("\n")).as_str(),
176177
);
177178
}
178-
review_comments.tool_total[1] += 1;
179+
total += 1;
179180
let mut is_merged = false;
180181
for s in &mut review_comments.comments {
181182
if s.path == file_name
@@ -197,6 +198,8 @@ impl FileObj {
197198
}
198199
}
199200
}
201+
review_comments.tool_total[1] =
202+
Some(review_comments.tool_total[1].unwrap_or_default() + total);
200203
}
201204
Ok(())
202205
}
@@ -308,14 +311,14 @@ mod test {
308311
// *********************** tests for FileObj::get_ranges()
309312

310313
#[test]
311-
fn get_ranges_0() {
314+
fn get_ranges_none() {
312315
let file_obj = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
313316
let ranges = file_obj.get_ranges(&LinesChangedOnly::Off);
314317
assert!(ranges.is_empty());
315318
}
316319

317320
#[test]
318-
fn get_ranges_2() {
321+
fn get_ranges_diff() {
319322
let diff_chunks = vec![1..=10];
320323
let added_lines = vec![4, 5, 9];
321324
let file_obj = FileObj::from(
@@ -328,7 +331,7 @@ mod test {
328331
}
329332

330333
#[test]
331-
fn get_ranges_1() {
334+
fn get_ranges_added() {
332335
let diff_chunks = vec![1..=10];
333336
let added_lines = vec![4, 5, 9];
334337
let file_obj = FileObj::from(

cpp-linter/src/rest_api/github/mod.rs

+18-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use serde_json;
2020
use super::{RestApiClient, RestApiRateLimitHeaders};
2121
use crate::clang_tools::clang_format::tally_format_advice;
2222
use crate::clang_tools::clang_tidy::tally_tidy_advice;
23+
use crate::clang_tools::ClangVersions;
2324
use crate::cli::{FeedbackInput, ThreadComments};
2425
use crate::common_fs::{FileFilter, FileObj};
2526
use crate::git::{get_diff, open_repo, parse_diff, parse_diff_from_buf};
@@ -230,6 +231,7 @@ impl RestApiClient for GithubApiClient {
230231
&self,
231232
files: &[Arc<Mutex<FileObj>>],
232233
feedback_inputs: FeedbackInput,
234+
clang_versions: ClangVersions,
233235
) -> Result<u64> {
234236
let tidy_checks_failed = tally_tidy_advice(files);
235237
let format_checks_failed = tally_format_advice(files);
@@ -239,8 +241,13 @@ impl RestApiClient for GithubApiClient {
239241
self.post_annotations(files, feedback_inputs.style.as_str());
240242
}
241243
if feedback_inputs.step_summary {
242-
comment =
243-
Some(self.make_comment(files, format_checks_failed, tidy_checks_failed, None));
244+
comment = Some(self.make_comment(
245+
files,
246+
format_checks_failed,
247+
tidy_checks_failed,
248+
&clang_versions,
249+
None,
250+
));
244251
self.post_step_summary(comment.as_ref().unwrap());
245252
}
246253
self.set_exit_code(
@@ -256,6 +263,7 @@ impl RestApiClient for GithubApiClient {
256263
files,
257264
format_checks_failed,
258265
tidy_checks_failed,
266+
&clang_versions,
259267
Some(65535),
260268
));
261269
}
@@ -284,7 +292,8 @@ impl RestApiClient for GithubApiClient {
284292
if self.event_name == "pull_request"
285293
&& (feedback_inputs.tidy_review || feedback_inputs.format_review)
286294
{
287-
self.post_review(files, &feedback_inputs).await?;
295+
self.post_review(files, &feedback_inputs, &clang_versions)
296+
.await?;
288297
}
289298
Ok(format_checks_failed + tidy_checks_failed)
290299
}
@@ -308,6 +317,7 @@ mod test {
308317
clang_tools::{
309318
clang_format::{FormatAdvice, Replacement},
310319
clang_tidy::{TidyAdvice, TidyNotification},
320+
ClangVersions,
311321
},
312322
cli::FeedbackInput,
313323
common_fs::{FileFilter, FileObj},
@@ -389,8 +399,12 @@ mod test {
389399
gh_out_path.path()
390400
},
391401
);
402+
let clang_versions = ClangVersions {
403+
format_version: Some("x.y.z".to_string()),
404+
tidy_version: Some("x.y.z".to_string()),
405+
};
392406
rest_api_client
393-
.post_feedback(&files, feedback_inputs)
407+
.post_feedback(&files, feedback_inputs, clang_versions)
394408
.await
395409
.unwrap();
396410
let mut step_summary_content = String::new();

cpp-linter/src/rest_api/github/specific_api.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use anyhow::{anyhow, Context, Result};
1212
use reqwest::{Client, Method, Url};
1313

1414
use crate::{
15-
clang_tools::{clang_format::summarize_style, ReviewComments},
15+
clang_tools::{clang_format::summarize_style, ClangVersions, ReviewComments},
1616
cli::FeedbackInput,
1717
common_fs::FileObj,
1818
rest_api::{RestApiRateLimitHeaders, COMMENT_MARKER, USER_AGENT},
@@ -305,6 +305,7 @@ impl GithubApiClient {
305305
&self,
306306
files: &[Arc<Mutex<FileObj>>],
307307
feedback_input: &FeedbackInput,
308+
clang_versions: &ClangVersions,
308309
) -> Result<()> {
309310
let url = self
310311
.api_url
@@ -370,15 +371,16 @@ impl GithubApiClient {
370371
let mut payload = FullReview {
371372
event: if feedback_input.passive_reviews {
372373
String::from("COMMENT")
373-
} else if has_no_changes {
374+
} else if has_no_changes && review_comments.comments.is_empty() {
375+
// if patches have no changes AND there are no comments about clang-tidy diagnostics
374376
String::from("APPROVE")
375377
} else {
376378
String::from("REQUEST_CHANGES")
377379
},
378380
body: String::new(),
379381
comments: vec![],
380382
};
381-
payload.body = review_comments.summarize();
383+
payload.body = review_comments.summarize(clang_versions);
382384
if !summary_only {
383385
payload.comments = {
384386
let mut comments = vec![];

0 commit comments

Comments
 (0)