1use std::{
4 fmt::Debug,
5 fs,
6 ops::RangeInclusive,
7 path::{Path, PathBuf},
8};
9
10use crate::{
11 clang_tools::{
12 MakeSuggestions, ReviewComments, Suggestion, clang_format::FormatAdvice,
13 clang_tidy::TidyAdvice, make_patch,
14 },
15 cli::LinesChangedOnly,
16 error::FileObjError,
17};
18use git2::DiffHunk;
19
20#[derive(Debug, Clone)]
22pub struct FileObj {
23 pub name: PathBuf,
25
26 pub added_lines: Vec<u32>,
28
29 pub added_ranges: Vec<RangeInclusive<u32>>,
31
32 pub diff_chunks: Vec<RangeInclusive<u32>>,
34
35 pub format_advice: Option<FormatAdvice>,
37
38 pub tidy_advice: Option<TidyAdvice>,
40}
41
42impl FileObj {
43 pub fn new(name: PathBuf) -> Self {
47 FileObj {
48 name,
49 added_lines: Vec::<u32>::new(),
50 added_ranges: Vec::<RangeInclusive<u32>>::new(),
51 diff_chunks: Vec::<RangeInclusive<u32>>::new(),
52 format_advice: None,
53 tidy_advice: None,
54 }
55 }
56
57 pub fn from(
59 name: PathBuf,
60 added_lines: Vec<u32>,
61 diff_chunks: Vec<RangeInclusive<u32>>,
62 ) -> Self {
63 let added_ranges = FileObj::consolidate_numbers_to_ranges(&added_lines);
64 FileObj {
65 name,
66 added_lines,
67 added_ranges,
68 diff_chunks,
69 format_advice: None,
70 tidy_advice: None,
71 }
72 }
73
74 fn consolidate_numbers_to_ranges(lines: &[u32]) -> Vec<RangeInclusive<u32>> {
78 let mut range_start = None;
79 let mut ranges: Vec<RangeInclusive<u32>> = Vec::new();
80 for (index, number) in lines.iter().enumerate() {
81 if index == 0 {
82 range_start = Some(*number);
83 } else if number - 1 != lines[index - 1] {
84 ranges.push(RangeInclusive::new(range_start.unwrap(), lines[index - 1]));
85 range_start = Some(*number);
86 }
87 if index == lines.len() - 1 {
88 ranges.push(RangeInclusive::new(range_start.unwrap(), *number));
89 }
90 }
91 ranges
92 }
93
94 pub fn get_ranges(&self, lines_changed_only: &LinesChangedOnly) -> Vec<RangeInclusive<u32>> {
95 match lines_changed_only {
96 LinesChangedOnly::Diff => self.diff_chunks.to_vec(),
97 LinesChangedOnly::On => self.added_ranges.to_vec(),
98 _ => Vec::new(),
99 }
100 }
101
102 pub fn is_hunk_in_diff(&self, hunk: &DiffHunk) -> Option<(u32, u32)> {
105 let (start_line, end_line) = if hunk.old_lines() > 0 {
106 let start = hunk.old_start();
108 (start, start + hunk.old_lines() - 1)
109 } else {
110 let start = hunk.new_start();
112 (start, start)
114 };
115 for range in &self.diff_chunks {
116 if range.contains(&start_line) && range.contains(&end_line) {
117 return Some((start_line, end_line));
118 }
119 }
120 None
121 }
122
123 fn is_line_in_diff(&self, line: &u32) -> bool {
129 for range in &self.diff_chunks {
130 if range.contains(line) {
131 return true;
132 }
133 }
134 false
135 }
136
137 pub fn make_suggestions_from_patch(
144 &self,
145 review_comments: &mut ReviewComments,
146 summary_only: bool,
147 ) -> Result<(), FileObjError> {
148 let original_content = fs::read(&self.name).map_err(FileObjError::ReadFile)?;
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 && let Some(patched) = &advice.patched
153 {
154 let mut patch = make_patch(file_path, patched, &original_content)
155 .map_err(|e| FileObjError::MakePatchFailed(file_name.clone(), e))?;
156 advice.get_suggestions(review_comments, self, &mut patch, summary_only)?;
157 }
158
159 if let Some(advice) = &self.tidy_advice {
160 if let Some(patched) = &advice.patched {
161 let mut patch = make_patch(file_path, patched, &original_content)
162 .map_err(|e| FileObjError::MakePatchFailed(file_name.clone(), e))?;
163 advice.get_suggestions(review_comments, self, &mut patch, summary_only)?;
164 }
165
166 if summary_only {
167 return Ok(());
168 }
169
170 let file_ext = self
172 .name
173 .extension()
174 .unwrap_or_default()
175 .to_str()
176 .unwrap_or_default();
177 let mut total = 0;
179 for note in &advice.notes {
180 if note.fixed_lines.is_empty() && self.is_line_in_diff(¬e.line) {
181 let mut suggestion = format!(
183 "### clang-tidy diagnostic\n**{file_name}:{}:{}** {}: [{}]\n\n> {}\n",
184 ¬e.line,
185 ¬e.cols,
186 ¬e.severity,
187 note.diagnostic_link(),
188 ¬e.rationale
189 );
190 if !note.suggestion.is_empty() {
191 suggestion.push_str(
192 format!("\n```{file_ext}\n{}\n```\n", ¬e.suggestion.join("\n"))
193 .as_str(),
194 );
195 }
196 total += 1;
197 let mut is_merged = false;
198 for s in &mut review_comments.comments {
199 if s.path == file_name
200 && s.line_end >= note.line
201 && s.line_start <= note.line
202 {
203 s.suggestion.push_str(suggestion.as_str());
204 is_merged = true;
205 break;
206 }
207 }
208 if !is_merged {
209 review_comments.comments.push(Suggestion {
210 line_start: note.line,
211 line_end: note.line,
212 suggestion,
213 path: file_name.to_owned(),
214 });
215 }
216 }
217 }
218 review_comments.tool_total[1] =
219 Some(review_comments.tool_total[1].unwrap_or_default() + total);
220 }
221 Ok(())
222 }
223}
224
225pub fn get_line_count_from_offset(contents: &[u8], offset: u32) -> u32 {
232 let offset = (offset as usize).min(contents.len());
233 let lines = contents[0..offset].split(|byte| byte == &b'\n');
234 lines.count() as u32
235}
236
237#[cfg(test)]
238mod test {
239 use std::{fs, path::PathBuf};
240
241 use super::{FileObj, get_line_count_from_offset};
242 use crate::cli::LinesChangedOnly;
243
244 #[test]
247 fn translate_byte_offset() {
248 let contents = fs::read(PathBuf::from("tests/demo/demo.cpp")).unwrap();
249 let lines = get_line_count_from_offset(&contents, 144);
250 assert_eq!(lines, 13);
251 }
252
253 #[test]
254 fn get_line_count_edge_cases() {
255 assert_eq!(get_line_count_from_offset(&[], 0), 1);
257
258 assert_eq!(get_line_count_from_offset(b"abc", 3), 1);
260
261 assert_eq!(get_line_count_from_offset(b"a\n\nb", 3), 3);
263
264 assert_eq!(get_line_count_from_offset(b"a\nb\n", 10), 3);
266 }
267 #[test]
270 fn get_ranges_none() {
271 let file_obj = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
272 let ranges = file_obj.get_ranges(&LinesChangedOnly::Off);
273 assert!(ranges.is_empty());
274 }
275
276 #[test]
277 fn get_ranges_diff() {
278 let diff_chunks = vec![1..=10];
279 let added_lines = vec![4, 5, 9];
280 let file_obj = FileObj::from(
281 PathBuf::from("tests/demo/demo.cpp"),
282 added_lines,
283 diff_chunks.clone(),
284 );
285 let ranges = file_obj.get_ranges(&LinesChangedOnly::Diff);
286 assert_eq!(ranges, diff_chunks);
287 }
288
289 #[test]
290 fn get_ranges_added() {
291 let diff_chunks = vec![1..=10];
292 let added_lines = vec![4, 5, 9];
293 let file_obj = FileObj::from(
294 PathBuf::from("tests/demo/demo.cpp"),
295 added_lines,
296 diff_chunks,
297 );
298 let ranges = file_obj.get_ranges(&LinesChangedOnly::On);
299 assert_eq!(ranges, vec![4..=5, 9..=9]);
300 }
301
302 #[test]
303 fn line_not_in_diff() {
304 let file_obj = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
305 assert!(!file_obj.is_line_in_diff(&42));
306 }
307}