1use std::fmt::Debug;
4use std::fs;
5use std::io::Read;
6use std::path::{Component, Path};
7use std::{ops::RangeInclusive, path::PathBuf};
8
9use anyhow::{Context, Result};
10
11use crate::clang_tools::clang_format::FormatAdvice;
12use crate::clang_tools::clang_tidy::TidyAdvice;
13use crate::clang_tools::{make_patch, MakeSuggestions, ReviewComments, Suggestion};
14use crate::cli::LinesChangedOnly;
15mod file_filter;
16pub use file_filter::FileFilter;
17use git2::DiffHunk;
18
19#[derive(Debug, Clone)]
21pub struct FileObj {
22 pub name: PathBuf,
24
25 pub added_lines: Vec<u32>,
27
28 pub added_ranges: Vec<RangeInclusive<u32>>,
30
31 pub diff_chunks: Vec<RangeInclusive<u32>>,
33
34 pub format_advice: Option<FormatAdvice>,
36
37 pub tidy_advice: Option<TidyAdvice>,
39}
40
41impl FileObj {
42 pub fn new(name: PathBuf) -> Self {
46 FileObj {
47 name,
48 added_lines: Vec::<u32>::new(),
49 added_ranges: Vec::<RangeInclusive<u32>>::new(),
50 diff_chunks: Vec::<RangeInclusive<u32>>::new(),
51 format_advice: None,
52 tidy_advice: None,
53 }
54 }
55
56 pub fn from(
58 name: PathBuf,
59 added_lines: Vec<u32>,
60 diff_chunks: Vec<RangeInclusive<u32>>,
61 ) -> Self {
62 let added_ranges = FileObj::consolidate_numbers_to_ranges(&added_lines);
63 FileObj {
64 name,
65 added_lines,
66 added_ranges,
67 diff_chunks,
68 format_advice: None,
69 tidy_advice: None,
70 }
71 }
72
73 fn consolidate_numbers_to_ranges(lines: &[u32]) -> Vec<RangeInclusive<u32>> {
77 let mut range_start = None;
78 let mut ranges: Vec<RangeInclusive<u32>> = Vec::new();
79 for (index, number) in lines.iter().enumerate() {
80 if index == 0 {
81 range_start = Some(*number);
82 } else if number - 1 != lines[index - 1] {
83 ranges.push(RangeInclusive::new(range_start.unwrap(), lines[index - 1]));
84 range_start = Some(*number);
85 }
86 if index == lines.len() - 1 {
87 ranges.push(RangeInclusive::new(range_start.unwrap(), *number));
88 }
89 }
90 ranges
91 }
92
93 pub fn get_ranges(&self, lines_changed_only: &LinesChangedOnly) -> Vec<RangeInclusive<u32>> {
94 match lines_changed_only {
95 LinesChangedOnly::Diff => self.diff_chunks.to_vec(),
96 LinesChangedOnly::On => self.added_ranges.to_vec(),
97 _ => Vec::new(),
98 }
99 }
100
101 pub fn is_hunk_in_diff(&self, hunk: &DiffHunk) -> Option<(u32, u32)> {
104 let (start_line, end_line) = if hunk.old_lines() > 0 {
105 let start = hunk.old_start();
107 (start, start + hunk.old_lines() - 1)
108 } else {
109 let start = hunk.new_start();
111 (start, start)
113 };
114 for range in &self.diff_chunks {
115 if range.contains(&start_line) && range.contains(&end_line) {
116 return Some((start_line, end_line));
117 }
118 }
119 None
120 }
121
122 fn is_line_in_diff(&self, line: &u32) -> bool {
128 for range in &self.diff_chunks {
129 if range.contains(line) {
130 return true;
131 }
132 }
133 false
134 }
135
136 pub fn make_suggestions_from_patch(
143 &self,
144 review_comments: &mut ReviewComments,
145 summary_only: bool,
146 ) -> Result<()> {
147 let original_content =
148 fs::read(&self.name).with_context(|| "Failed to read original contents of file")?;
149 let file_name = self.name.to_str().unwrap_or_default().replace("\\", "/");
150 let file_path = Path::new(&file_name);
151 if let Some(advice) = &self.format_advice {
152 if let Some(patched) = &advice.patched {
153 let mut patch = make_patch(file_path, patched, &original_content)?;
154 advice.get_suggestions(review_comments, self, &mut patch, summary_only)?;
155 }
156 }
157
158 if let Some(advice) = &self.tidy_advice {
159 if let Some(patched) = &advice.patched {
160 let mut patch = make_patch(file_path, patched, &original_content)?;
161 advice.get_suggestions(review_comments, self, &mut patch, summary_only)?;
162 }
163
164 if summary_only {
165 return Ok(());
166 }
167
168 let file_ext = self
170 .name
171 .extension()
172 .unwrap_or_default()
173 .to_str()
174 .unwrap_or_default();
175 let mut total = 0;
177 for note in &advice.notes {
178 if note.fixed_lines.is_empty() && self.is_line_in_diff(¬e.line) {
179 let mut suggestion = format!(
181 "### clang-tidy diagnostic\n**{file_name}:{}:{}** {}: [{}]\n\n> {}\n",
182 ¬e.line,
183 ¬e.cols,
184 ¬e.severity,
185 note.diagnostic_link(),
186 ¬e.rationale
187 );
188 if !note.suggestion.is_empty() {
189 suggestion.push_str(
190 format!("\n```{file_ext}\n{}\n```\n", ¬e.suggestion.join("\n"))
191 .as_str(),
192 );
193 }
194 total += 1;
195 let mut is_merged = false;
196 for s in &mut review_comments.comments {
197 if s.path == file_name
198 && s.line_end >= note.line
199 && s.line_start <= note.line
200 {
201 s.suggestion.push_str(suggestion.as_str());
202 is_merged = true;
203 break;
204 }
205 }
206 if !is_merged {
207 review_comments.comments.push(Suggestion {
208 line_start: note.line,
209 line_end: note.line,
210 suggestion,
211 path: file_name.to_owned(),
212 });
213 }
214 }
215 }
216 review_comments.tool_total[1] =
217 Some(review_comments.tool_total[1].unwrap_or_default() + total);
218 }
219 Ok(())
220 }
221}
222
223pub fn get_line_cols_from_offset(file_path: &PathBuf, offset: usize) -> (usize, usize) {
232 let mut file_buf = vec![0; offset];
233 fs::File::open(file_path)
234 .unwrap()
235 .read_exact(&mut file_buf)
236 .unwrap();
237 let lines = file_buf.split(|byte| byte == &b'\n');
238 let line_count = lines.clone().count();
239 let column_count = lines.last().unwrap_or(&[]).len() + 1; (line_count, column_count)
241}
242
243pub fn normalize_path(path: &Path) -> PathBuf {
247 let mut components = path.components().peekable();
248 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
249 components.next();
250 PathBuf::from(c.as_os_str())
251 } else {
252 PathBuf::new()
253 };
254
255 for component in components {
256 match component {
257 Component::Prefix(..) => unreachable!(),
258 Component::RootDir => {
259 ret.push(component.as_os_str());
260 }
261 Component::CurDir => {}
262 Component::ParentDir => {
263 ret.pop();
264 }
265 Component::Normal(c) => {
266 ret.push(c);
267 }
268 }
269 }
270 ret
271}
272
273#[cfg(test)]
274mod test {
275 use std::env::current_dir;
276 use std::path::PathBuf;
277
278 use super::{get_line_cols_from_offset, normalize_path, FileObj};
279 use crate::cli::LinesChangedOnly;
280
281 #[test]
284 fn normalize_redirects() {
285 let mut src = current_dir().unwrap();
286 src.push("..");
287 src.push(
288 current_dir()
289 .unwrap()
290 .strip_prefix(current_dir().unwrap().parent().unwrap())
291 .unwrap(),
292 );
293 println!("relative path = {}", src.to_str().unwrap());
294 assert_eq!(normalize_path(&src), current_dir().unwrap());
295 }
296
297 #[test]
298 fn normalize_no_root() {
299 let src = PathBuf::from("../cpp-linter");
300 let mut cur_dir = current_dir().unwrap();
301 cur_dir = cur_dir
302 .strip_prefix(current_dir().unwrap().parent().unwrap())
303 .unwrap()
304 .to_path_buf();
305 println!("relative path = {}", src.to_str().unwrap());
306 assert_eq!(normalize_path(&src), cur_dir);
307 }
308
309 #[test]
310 fn normalize_current_redirect() {
311 let src = PathBuf::from("tests/./ignored_paths");
312 println!("relative path = {}", src.to_str().unwrap());
313 assert_eq!(normalize_path(&src), PathBuf::from("tests/ignored_paths"));
314 }
315
316 #[test]
319 fn translate_byte_offset() {
320 let (lines, cols) = get_line_cols_from_offset(&PathBuf::from("tests/demo/demo.cpp"), 144);
321 println!("lines: {lines}, cols: {cols}");
322 assert_eq!(lines, 13);
323 assert_eq!(cols, 5);
324 }
325
326 #[test]
329 fn get_ranges_none() {
330 let file_obj = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
331 let ranges = file_obj.get_ranges(&LinesChangedOnly::Off);
332 assert!(ranges.is_empty());
333 }
334
335 #[test]
336 fn get_ranges_diff() {
337 let diff_chunks = vec![1..=10];
338 let added_lines = vec![4, 5, 9];
339 let file_obj = FileObj::from(
340 PathBuf::from("tests/demo/demo.cpp"),
341 added_lines,
342 diff_chunks.clone(),
343 );
344 let ranges = file_obj.get_ranges(&LinesChangedOnly::Diff);
345 assert_eq!(ranges, diff_chunks);
346 }
347
348 #[test]
349 fn get_ranges_added() {
350 let diff_chunks = vec![1..=10];
351 let added_lines = vec![4, 5, 9];
352 let file_obj = FileObj::from(
353 PathBuf::from("tests/demo/demo.cpp"),
354 added_lines,
355 diff_chunks,
356 );
357 let ranges = file_obj.get_ranges(&LinesChangedOnly::On);
358 assert_eq!(ranges, vec![4..=5, 9..=9]);
359 }
360
361 #[test]
362 fn line_not_in_diff() {
363 let file_obj = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
364 assert!(!file_obj.is_line_in_diff(&42));
365 }
366}