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