1use std::{
4 fmt::Debug,
5 fs,
6 io::Write,
7 num::NonZeroU32,
8 ops::RangeInclusive,
9 path::{Path, PathBuf},
10};
11
12use gix_imara_diff::{
13 BasicLineDiffPrinter, Diff, Hunk, InternedInput, UnifiedDiffConfig, UnifiedDiffPrinter,
14};
15
16use crate::{
17 clang_tools::{
18 ReviewComments, Suggestion, clang_format::FormatAdvice, clang_tidy::TidyAdvice, make_patch,
19 },
20 cli::{ClangParams, LinesChangedOnly},
21 error::FileObjError,
22};
23
24#[derive(Debug, Clone)]
26pub struct FileObj {
27 pub name: PathBuf,
29
30 pub added_lines: Vec<u32>,
32
33 pub added_ranges: Vec<RangeInclusive<u32>>,
35
36 pub diff_chunks: Vec<RangeInclusive<u32>>,
38
39 pub format_advice: Option<FormatAdvice>,
41
42 pub tidy_advice: Option<TidyAdvice>,
44
45 pub(crate) patched_path: Option<PathBuf>,
47}
48
49impl FileObj {
50 pub fn new(name: PathBuf) -> Self {
54 FileObj {
55 name,
56 added_lines: Vec::<u32>::new(),
57 added_ranges: Vec::<RangeInclusive<u32>>::new(),
58 diff_chunks: Vec::<RangeInclusive<u32>>::new(),
59 format_advice: None,
60 tidy_advice: None,
61 patched_path: None,
62 }
63 }
64
65 pub fn from(
67 name: PathBuf,
68 added_lines: Vec<u32>,
69 diff_chunks: Vec<RangeInclusive<u32>>,
70 ) -> Self {
71 let added_lines: Vec<NonZeroU32> = added_lines
73 .into_iter()
74 .filter_map(NonZeroU32::new)
75 .collect();
76 let added_ranges = FileObj::consolidate_numbers_to_ranges(&added_lines);
77 FileObj {
78 name,
79 added_lines: added_lines.into_iter().map(|v| v.get()).collect(),
80 added_ranges,
81 diff_chunks,
82 format_advice: None,
83 tidy_advice: None,
84 patched_path: None,
85 }
86 }
87
88 fn consolidate_numbers_to_ranges(lines: &[NonZeroU32]) -> Vec<RangeInclusive<u32>> {
92 let mut ranges: Vec<RangeInclusive<u32>> = Vec::new();
93 let mut line_iter = lines.iter().enumerate();
94 let mut range_start = match line_iter.next() {
95 Some((_, number)) => number.get(),
96 None => return ranges, };
98 let last_index = lines.len() - 1;
100 if last_index == 0 {
101 ranges.push(RangeInclusive::new(range_start, range_start));
103 return ranges;
104 }
105 for (index, number) in line_iter {
106 if let Some(prev_line) = lines.get(index - 1)
109 && number.get() - 1 != prev_line.get()
110 {
111 ranges.push(RangeInclusive::new(range_start, prev_line.get()));
112 range_start = number.get();
113 }
114 if index == last_index {
115 ranges.push(RangeInclusive::new(range_start, number.get()));
116 }
117 }
118 ranges
119 }
120
121 pub fn get_ranges(&self, lines_changed_only: &LinesChangedOnly) -> Vec<RangeInclusive<u32>> {
124 match lines_changed_only {
125 LinesChangedOnly::Diff => self.diff_chunks.to_vec(),
126 LinesChangedOnly::On => self.added_ranges.to_vec(),
127 _ => Vec::new(),
128 }
129 }
130
131 pub fn is_hunk_in_diff(&self, hunk: &Hunk) -> Option<(u32, u32)> {
134 let (start_line, end_line) = if !hunk.before.is_empty() {
135 let start = hunk.before.start;
137 (start, start + hunk.before.len() as u32 - 1)
138 } else {
139 let start = hunk.after.start;
141 (start, start)
143 };
144 for range in &self.diff_chunks {
145 if range.contains(&start_line) && range.contains(&end_line) {
146 return Some((start_line, end_line));
147 }
148 }
149 None
150 }
151
152 fn is_line_in_diff(&self, line: &u32) -> bool {
158 for range in &self.diff_chunks {
159 if range.contains(line) {
160 return true;
161 }
162 }
163 false
164 }
165
166 pub fn maybe_append_patch(&self, repo_root: &Path) -> Result<(), FileObjError> {
173 let patched = match &self.patched_path {
174 Some(patched_path) if patched_path.exists() => {
175 fs::read_to_string(patched_path).map_err(FileObjError::ReadFile)?
176 }
177 _ => return Ok(()),
178 };
179 let original_content =
180 fs::read_to_string(repo_root.join(&self.name)).map_err(FileObjError::ReadFile)?;
181 let (diff, input) = make_patch(patched.as_str(), &original_content);
182 let file_name = self.name.to_string_lossy().replace("\\", "/");
183 Self::append_patch(&file_name, &input, &diff, repo_root)?;
184 Ok(())
185 }
186
187 fn append_patch(
189 file_name: &str,
190 input: &InternedInput<&str>,
191 diff: &Diff,
192 repo_root: &Path,
193 ) -> Result<(), FileObjError> {
194 let printer = BasicLineDiffPrinter(&input.interner);
195 let mut diff_config = UnifiedDiffConfig::default();
196 diff_config.context_len(0);
197 let unified_diff = diff.unified_diff(&printer, diff_config, input).to_string();
198 if !unified_diff.is_empty() {
199 let patch_path_parent = repo_root.join(ClangParams::CACHE_DIR);
200 fs::create_dir_all(&patch_path_parent).map_err(FileObjError::MkDirFailed)?;
201 let patch_file_path = patch_path_parent.join(ClangParams::AUTO_FIX_PATCH);
202 let mut patch_file = fs::OpenOptions::new()
203 .append(true)
204 .create(true)
205 .truncate(false)
206 .open(&patch_file_path)
207 .map_err(FileObjError::OpenPatchFileFailed)?;
208 patch_file
209 .write_all(
210 format!("--- a/{file_name}\n+++ b/{file_name}\n{unified_diff}",).as_bytes(),
211 )
212 .map_err(FileObjError::WritePatchFailed)?;
213 }
214 Ok(())
215 }
216
217 pub fn make_suggestions_from_patch(
224 &self,
225 review_comments: &mut ReviewComments,
226 summary_only: bool,
227 repo_root: &Path,
228 ) -> Result<(), FileObjError> {
229 let patched = match &self.patched_path {
230 Some(patched_path) if patched_path.exists() => {
231 fs::read_to_string(patched_path).map_err(FileObjError::ReadFile)?
232 }
233 _ => return Ok(()),
234 };
235 let original_content =
236 fs::read_to_string(repo_root.join(&self.name)).map_err(FileObjError::ReadFile)?;
237 let (diff, input) = make_patch(patched.as_str(), &original_content);
238 let file_name = self.name.to_string_lossy().replace("\\", "/");
239 Self::append_patch(&file_name, &input, &diff, repo_root)?;
240
241 self.get_suggestions(review_comments, &diff, &input, summary_only)
242 .map_err(FileObjError::DisplayStringFailed)?;
243 if let Some(advice) = &self.tidy_advice {
244 let file_ext = self
246 .name
247 .extension()
248 .unwrap_or_default()
249 .to_str()
250 .unwrap_or_default();
251 let mut total = 0;
253 for note in &advice.notes {
254 if note.fixed_lines.is_empty() && self.is_line_in_diff(¬e.line) {
255 total += 1;
257 if summary_only {
258 continue;
259 }
260 let mut suggestion = format!(
261 "### clang-tidy diagnostic\n**{file_name}:{}:{}** {}: [{}]\n\n> {}\n",
262 ¬e.line,
263 ¬e.cols,
264 ¬e.severity,
265 note.diagnostic_link(),
266 ¬e.rationale
267 );
268 if !note.suggestion.is_empty() {
269 suggestion.push_str(
270 format!("\n```{file_ext}\n{}\n```\n", ¬e.suggestion.join("\n"))
271 .as_str(),
272 );
273 }
274 let mut is_merged = false;
275 for s in &mut review_comments.comments {
276 if s.path == file_name
277 && s.line_end >= note.line
278 && s.line_start <= note.line
279 {
280 s.suggestion.push_str(suggestion.as_str());
281 is_merged = true;
282 break;
283 }
284 }
285 if !is_merged {
286 review_comments.comments.push(Suggestion {
287 line_start: note.line,
288 line_end: note.line,
289 suggestion,
290 path: file_name.to_owned(),
291 });
292 }
293 }
294 }
295 review_comments.tool_total += total;
296 }
297 Ok(())
298 }
299
300 fn get_suggestions(
302 &self,
303 review_comments: &mut ReviewComments,
304 diff: &Diff,
305 input: &InternedInput<&str>,
306 summary_only: bool,
307 ) -> Result<(), std::fmt::Error> {
308 let file_name = self
309 .name
310 .to_string_lossy()
311 .replace("\\", "/")
312 .trim_start_matches("./")
313 .to_owned();
314 let mut config = UnifiedDiffConfig::default();
315 config.context_len(0);
316 let printer = BasicLineDiffPrinter(&input.interner);
317 let mut patch_buff = String::new();
318 let mut hunks_in_patch = 0u32;
319 for hunk in diff.hunks() {
320 hunks_in_patch += 1;
321 let hunk_range = self.is_hunk_in_diff(&hunk);
322 match hunk_range {
323 Some((start_line, end_line)) if !summary_only => {
324 let mut suggestion = String::new();
325 let suggestion_help = self
326 .tidy_advice
327 .as_ref()
328 .map(|t| t.get_suggestion_help(start_line, end_line))
329 .unwrap_or_default();
330 if hunk.is_pure_removal() {
331 suggestion.push_str(
332 format!(
333 "Please remove the line(s)\n- {}",
334 hunk.before
335 .map(|l| l.to_string())
336 .collect::<Vec<String>>()
337 .join("\n- ")
338 )
339 .as_str(),
340 );
341 } else {
342 suggestion.push_str("```suggestion\n");
343 for token in
344 &input.after[hunk.after.start as usize..hunk.after.end as usize]
345 {
346 let line = &input.interner[*token];
347 suggestion.push_str(line);
348 }
349 suggestion.push_str("```\n");
350 }
351 let comment = Suggestion {
352 line_start: start_line,
353 line_end: end_line,
354 suggestion: format!("{suggestion_help}\n{suggestion}"),
355 path: file_name.clone(),
356 };
357 if !review_comments.is_comment_in_suggestions(&comment) {
358 review_comments.comments.push(comment);
359 }
360 }
361 _ => {
362 printer.display_header(
363 &mut patch_buff,
364 hunk.before.start,
365 hunk.after.start,
366 hunk.before.len() as u32,
367 hunk.after.len() as u32,
368 )?;
369 printer.display_hunk(
370 &mut patch_buff,
371 &input.before[hunk.before.start as usize..hunk.before.end as usize],
372 &input.after[hunk.after.start as usize..hunk.after.end as usize],
373 )?;
374 }
375 }
376 }
377 if !patch_buff.is_empty() {
378 let patch_buf = format!("--- a/{file_name}\n+++ b/{file_name}\n{patch_buff}");
379 review_comments.full_patch.push_str(patch_buf.as_str());
380 }
381 review_comments.tool_total += hunks_in_patch;
382 Ok(())
383 }
384}
385
386#[cfg(test)]
387mod test {
388 use std::path::PathBuf;
389
390 use super::FileObj;
391 use crate::cli::LinesChangedOnly;
392
393 #[test]
396 fn get_ranges_none() {
397 let file_obj = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
398 let ranges = file_obj.get_ranges(&LinesChangedOnly::Off);
399 assert!(ranges.is_empty());
400 }
401
402 #[test]
403 fn get_ranges_diff() {
404 let diff_chunks = vec![1..=10];
405 let added_lines = vec![4, 5, 9];
406 let file_obj = FileObj::from(
407 PathBuf::from("tests/demo/demo.cpp"),
408 added_lines,
409 diff_chunks.clone(),
410 );
411 let ranges = file_obj.get_ranges(&LinesChangedOnly::Diff);
412 assert_eq!(ranges, diff_chunks);
413 }
414
415 #[test]
416 fn get_ranges_added() {
417 let diff_chunks = vec![1..=10];
418 let added_lines = vec![4, 5, 9];
419 let file_obj = FileObj::from(
420 PathBuf::from("tests/demo/demo.cpp"),
421 added_lines,
422 diff_chunks,
423 );
424 let ranges = file_obj.get_ranges(&LinesChangedOnly::On);
425 assert_eq!(ranges, vec![4..=5, 9..=9]);
426 }
427
428 #[test]
429 fn get_ranges_single_added_line() {
430 let added_lines = vec![5];
431 let file_obj = FileObj::from(PathBuf::from("tests/demo/demo.cpp"), added_lines, vec![]);
432 let ranges = file_obj.get_ranges(&LinesChangedOnly::On);
433 assert_eq!(ranges, vec![5..=5]);
434 }
435
436 #[test]
437 fn line_not_in_diff() {
438 let file_obj = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
439 assert!(!file_obj.is_line_in_diff(&42));
440 }
441}