Skip to content

Commit e5c6717

Browse files
committed
assemble comments
1 parent e0fb899 commit e5c6717

File tree

7 files changed

+521
-133
lines changed

7 files changed

+521
-133
lines changed

cpp-linter-lib/src/clang_tools/clang_format.rs

+48-7
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ use std::{
66
sync::{Arc, Mutex},
77
};
88

9+
use log::Level;
910
// non-std crates
1011
use serde::Deserialize;
1112
use serde_xml_rs::de::Deserializer;
1213

1314
// project-specific crates/modules
15+
use super::MakeSuggestions;
1416
use crate::{
15-
cli::LinesChangedOnly,
17+
cli::ClangParams,
1618
common_fs::{get_line_cols_from_offset, FileObj},
1719
};
1820

@@ -23,6 +25,14 @@ pub struct FormatAdvice {
2325
/// A list of [`Replacement`]s that clang-tidy wants to make.
2426
#[serde(rename = "$value")]
2527
pub replacements: Vec<Replacement>,
28+
29+
pub patched: Option<Vec<u8>>,
30+
}
31+
32+
impl MakeSuggestions for FormatAdvice {
33+
fn get_suggestion_help(&self, _start_line: u32, _end_line: u32) -> String {
34+
String::from("### clang-format suggestions\n")
35+
}
2636
}
2737

2838
/// A single replacement that clang-format wants to make.
@@ -79,24 +89,53 @@ pub fn tally_format_advice(files: &[Arc<Mutex<FileObj>>]) -> u64 {
7989

8090
/// Run clang-tidy for a specific `file`, then parse and return it's XML output.
8191
pub fn run_clang_format(
82-
cmd: &mut Command,
8392
file: &mut Arc<Mutex<FileObj>>,
84-
style: &str,
85-
lines_changed_only: &LinesChangedOnly,
93+
clang_params: &ClangParams,
8694
) -> Vec<(log::Level, String)> {
95+
let mut cmd = Command::new(clang_params.clang_format_command.as_ref().unwrap());
8796
let mut logs = vec![];
8897
let mut file = file.lock().unwrap();
89-
cmd.args(["--style", style, "--output-replacements-xml"]);
90-
let ranges = file.get_ranges(lines_changed_only);
98+
cmd.args(["--style", &clang_params.style]);
99+
let ranges = file.get_ranges(&clang_params.lines_changed_only);
91100
for range in &ranges {
92101
cmd.arg(format!("--lines={}:{}", range.start(), range.end()));
93102
}
94103
cmd.arg(file.name.to_string_lossy().as_ref());
104+
let mut patched = None;
105+
if clang_params.format_review {
106+
logs.push((
107+
Level::Info,
108+
format!(
109+
"Getting format fixes with \"{} {}\"",
110+
clang_params
111+
.clang_format_command
112+
.as_ref()
113+
.unwrap()
114+
.to_str()
115+
.unwrap(),
116+
cmd.get_args()
117+
.map(|a| a.to_str().unwrap())
118+
.collect::<Vec<&str>>()
119+
.join(" ")
120+
),
121+
));
122+
patched = Some(
123+
cmd.output()
124+
.expect("Failed to get fixes from clang-format")
125+
.stdout,
126+
);
127+
}
128+
cmd.arg("--output-replacements-xml");
95129
logs.push((
96130
log::Level::Info,
97131
format!(
98132
"Running \"{} {}\"",
99-
cmd.get_program().to_string_lossy(),
133+
clang_params
134+
.clang_format_command
135+
.as_ref()
136+
.unwrap()
137+
.to_str()
138+
.unwrap(),
100139
cmd.get_args()
101140
.map(|x| x.to_str().unwrap())
102141
.collect::<Vec<&str>>()
@@ -129,6 +168,7 @@ pub fn run_clang_format(
129168
let mut format_advice = FormatAdvice::deserialize(&mut Deserializer::new(event_reader))
130169
.unwrap_or(FormatAdvice {
131170
replacements: vec![],
171+
patched,
132172
});
133173
if !format_advice.replacements.is_empty() {
134174
let mut filtered_replacements = Vec::new();
@@ -201,6 +241,7 @@ mod tests {
201241
cols: None,
202242
},
203243
],
244+
patched: None,
204245
};
205246
let config = serde_xml_rs::ParserConfig::new()
206247
.trim_whitespace(false)

cpp-linter-lib/src/clang_tools/clang_tidy.rs

+126-45
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
44
use std::{
55
env::{consts::OS, current_dir},
6+
fs,
67
path::PathBuf,
78
process::Command,
89
sync::{Arc, Mutex},
@@ -13,8 +14,9 @@ use regex::Regex;
1314
use serde::Deserialize;
1415

1516
// project-specific modules/crates
17+
use super::MakeSuggestions;
1618
use crate::{
17-
cli::LinesChangedOnly,
19+
cli::{ClangParams, LinesChangedOnly},
1820
common_fs::{normalize_path, FileObj},
1921
};
2022

@@ -64,6 +66,9 @@ pub struct TidyNotification {
6466
/// Sometimes, this code block doesn't exist. Sometimes, it contains suggested
6567
/// fixes/advice. This information is purely superfluous.
6668
pub suggestion: Vec<String>,
69+
70+
/// The list of line numbers that had fixes applied via `clang-tidy --fix-error`.
71+
pub fixed_lines: Vec<u64>,
6772
}
6873

6974
impl TidyNotification {
@@ -84,6 +89,27 @@ impl TidyNotification {
8489
pub struct TidyAdvice {
8590
/// A list of notifications parsed from clang-tidy stdout.
8691
pub notes: Vec<TidyNotification>,
92+
pub patched: Option<Vec<u8>>,
93+
}
94+
95+
impl MakeSuggestions for TidyAdvice {
96+
fn get_suggestion_help(&self, start_line: u32, end_line: u32) -> String {
97+
let mut diagnostics = vec![];
98+
for note in &self.notes {
99+
if (start_line..end_line).contains(&note.line) {
100+
diagnostics.push(format!("- {}\n", note.diagnostic_link()));
101+
}
102+
}
103+
format!(
104+
"### clang-tidy {}\n{}",
105+
if diagnostics.is_empty() {
106+
"suggestion"
107+
} else {
108+
"diagnostic(s)"
109+
},
110+
diagnostics.join("")
111+
)
112+
}
87113
}
88114

89115
/// Parses clang-tidy stdout.
@@ -95,6 +121,9 @@ fn parse_tidy_output(
95121
database_json: &Option<Vec<CompilationUnit>>,
96122
) -> Option<TidyAdvice> {
97123
let note_header = Regex::new(r"^(.+):(\d+):(\d+):\s(\w+):(.*)\[([a-zA-Z\d\-\.]+)\]$").unwrap();
124+
let fixed_note =
125+
Regex::new(r"^.+:(\d+):\d+:\snote: FIX-IT applied suggested code changes$").unwrap();
126+
let mut found_fix = false;
98127
let mut notification = None;
99128
let mut result = Vec::new();
100129
let cur_dir = current_dir().unwrap();
@@ -148,11 +177,29 @@ fn parse_tidy_output(
148177
rationale: String::from(&captured[5]).trim().to_string(),
149178
diagnostic: String::from(&captured[6]),
150179
suggestion: Vec::new(),
180+
fixed_lines: Vec::new(),
151181
});
152-
} else if let Some(note) = &mut notification {
153-
// append lines of code that are part of
154-
// the previous line's notification
155-
note.suggestion.push(line.to_string());
182+
// begin capturing subsequent lines as suggestions
183+
found_fix = false;
184+
} else if let Some(capture) = fixed_note.captures(line) {
185+
let fixed_line = capture[1]
186+
.parse()
187+
.expect("Failed to parse line number's string as integer");
188+
if let Some(note) = &mut notification {
189+
if !note.fixed_lines.contains(&fixed_line) {
190+
note.fixed_lines.push(fixed_line);
191+
}
192+
}
193+
// Suspend capturing subsequent lines as suggestions until
194+
// a new notification is constructed. If we found a note about applied fixes,
195+
// then the lines of suggestions for that notification have already been parsed.
196+
found_fix = true;
197+
} else if !found_fix {
198+
if let Some(note) = &mut notification {
199+
// append lines of code that are part of
200+
// the previous line's notification
201+
note.suggestion.push(line.to_string());
202+
}
156203
}
157204
}
158205
if let Some(note) = notification {
@@ -161,7 +208,10 @@ fn parse_tidy_output(
161208
if result.is_empty() {
162209
None
163210
} else {
164-
Some(TidyAdvice { notes: result })
211+
Some(TidyAdvice {
212+
notes: result,
213+
patched: None,
214+
})
165215
}
166216
}
167217

@@ -184,29 +234,25 @@ pub fn tally_tidy_advice(files: &[Arc<Mutex<FileObj>>]) -> u64 {
184234

185235
/// Run clang-tidy, then parse and return it's output.
186236
pub fn run_clang_tidy(
187-
cmd: &mut Command,
188237
file: &mut Arc<Mutex<FileObj>>,
189-
checks: &str,
190-
lines_changed_only: &LinesChangedOnly,
191-
database: &Option<PathBuf>,
192-
extra_args: &Option<Vec<String>>,
193-
database_json: &Option<Vec<CompilationUnit>>,
238+
clang_params: &ClangParams,
194239
) -> Vec<(log::Level, std::string::String)> {
240+
let mut cmd = Command::new(clang_params.clang_tidy_command.as_ref().unwrap());
195241
let mut logs = vec![];
196242
let mut file = file.lock().unwrap();
197-
if !checks.is_empty() {
198-
cmd.args(["-checks", checks]);
243+
if !clang_params.tidy_checks.is_empty() {
244+
cmd.args(["-checks", &clang_params.tidy_checks]);
199245
}
200-
if let Some(db) = database {
246+
if let Some(db) = &clang_params.database {
201247
cmd.args(["-p", &db.to_string_lossy()]);
202248
}
203-
if let Some(extras) = extra_args {
249+
if let Some(extras) = &clang_params.extra_args {
204250
for arg in extras {
205251
cmd.args(["--extra-arg", format!("\"{}\"", arg).as_str()]);
206252
}
207253
}
208-
if *lines_changed_only != LinesChangedOnly::Off {
209-
let ranges = file.get_ranges(lines_changed_only);
254+
if clang_params.lines_changed_only != LinesChangedOnly::Off {
255+
let ranges = file.get_ranges(&clang_params.lines_changed_only);
210256
let filter = format!(
211257
"[{{\"name\":{:?},\"lines\":{:?}}}]",
212258
&file
@@ -220,6 +266,17 @@ pub fn run_clang_tidy(
220266
);
221267
cmd.args(["--line-filter", filter.as_str()]);
222268
}
269+
let mut original_content = None;
270+
if clang_params.tidy_review {
271+
cmd.arg("--fix-errors");
272+
original_content =
273+
Some(fs::read_to_string(&file.name).expect(
274+
"Failed to cache file's original content before applying clang-tidy changes.",
275+
));
276+
}
277+
if !clang_params.style.is_empty() {
278+
cmd.args(["--format-style", clang_params.style.as_str()]);
279+
}
223280
cmd.arg(file.name.to_string_lossy().as_ref());
224281
logs.push((
225282
log::Level::Info,
@@ -249,7 +306,20 @@ pub fn run_clang_tidy(
249306
),
250307
));
251308
}
252-
file.tidy_advice = parse_tidy_output(&output.stdout, database_json);
309+
file.tidy_advice = parse_tidy_output(&output.stdout, &clang_params.database_json);
310+
if clang_params.tidy_review {
311+
let file_name = &file.name.to_owned();
312+
if let Some(tidy_advice) = &mut file.tidy_advice {
313+
// cache file changes in a buffer and restore the original contents for further analysis by clang-format
314+
tidy_advice.patched =
315+
Some(fs::read(file_name).expect("Failed to read changes from clang-tidy"));
316+
}
317+
fs::write(
318+
file_name,
319+
original_content.expect("original content of file was not cached"),
320+
)
321+
.expect("failed to restore file's original content.");
322+
}
253323
logs
254324
}
255325

@@ -258,13 +328,16 @@ mod test {
258328
use std::{
259329
env,
260330
path::PathBuf,
261-
process::Command,
262331
sync::{Arc, Mutex},
263332
};
264333

265334
use regex::Regex;
266335

267-
use crate::{clang_tools::get_clang_tool_exe, cli::LinesChangedOnly, common_fs::FileObj};
336+
use crate::{
337+
clang_tools::get_clang_tool_exe,
338+
cli::{ClangParams, LinesChangedOnly},
339+
common_fs::FileObj,
340+
};
268341

269342
use super::run_clang_tidy;
270343

@@ -297,33 +370,41 @@ mod test {
297370
env::var("CLANG_VERSION").unwrap_or("".to_string()).as_str(),
298371
)
299372
.unwrap();
300-
let mut cmd = Command::new(exe_path);
301373
let file = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
302374
let mut arc_ref = Arc::new(Mutex::new(file));
303375
let extra_args = vec!["-std=c++17".to_string(), "-Wall".to_string()];
304-
run_clang_tidy(
305-
&mut cmd,
306-
&mut arc_ref,
307-
"", // use .clang-tidy config file
308-
&LinesChangedOnly::Off, // check all lines
309-
&None, // no database path
310-
&Some(extra_args), // <---- the reason for this test
311-
&None, // no deserialized database
312-
);
313-
// since `cmd` was passed as a mutable reference, we can inspect the args that were added
314-
let locked_file = arc_ref.lock().unwrap();
315-
let mut args = cmd
316-
.get_args()
317-
.map(|arg| arg.to_str().unwrap())
376+
let clang_params = ClangParams {
377+
style: "".to_string(),
378+
tidy_checks: "".to_string(), // use .clang-tidy config file
379+
lines_changed_only: LinesChangedOnly::Off,
380+
database: None,
381+
extra_args: Some(extra_args.clone()), // <---- the reason for this test
382+
database_json: None,
383+
format_filter: None,
384+
tidy_filter: None,
385+
tidy_review: false,
386+
format_review: false,
387+
clang_tidy_command: Some(exe_path),
388+
clang_format_command: None,
389+
};
390+
let logs = run_clang_tidy(&mut arc_ref, &clang_params)
391+
.into_iter()
392+
.filter_map(|(_lvl, msg)| {
393+
if msg.contains("Running ") {
394+
Some(msg)
395+
} else {
396+
None
397+
}
398+
})
399+
.collect::<Vec<String>>();
400+
let args = &logs
401+
.first()
402+
.expect("expected a log message about invoked clang-tidy command")
403+
.split(' ')
318404
.collect::<Vec<&str>>();
319-
assert_eq!(locked_file.name.to_string_lossy(), args.pop().unwrap());
320-
assert_eq!(
321-
vec!["--extra-arg", "\"-std=c++17\"", "--extra-arg", "\"-Wall\""],
322-
args
323-
);
324-
assert!(!locked_file
325-
.tidy_advice
326-
.as_ref()
327-
.is_some_and(|advice| advice.notes.is_empty()));
405+
for arg in &extra_args {
406+
let extra_arg = format!("\"{arg}\"");
407+
assert!(args.contains(&extra_arg.as_str()));
408+
}
328409
}
329410
}

0 commit comments

Comments
 (0)