use std::{
fmt::Debug,
fs,
ops::RangeInclusive,
path::{Path, PathBuf},
};
use crate::{
clang_tools::{
MakeSuggestions, ReviewComments, Suggestion, clang_format::FormatAdvice,
clang_tidy::TidyAdvice, make_patch,
},
cli::LinesChangedOnly,
error::FileObjError,
};
use git2::DiffHunk;
#[derive(Debug, Clone)]
pub struct FileObj {
pub name: PathBuf,
pub added_lines: Vec<u32>,
pub added_ranges: Vec<RangeInclusive<u32>>,
pub diff_chunks: Vec<RangeInclusive<u32>>,
pub format_advice: Option<FormatAdvice>,
pub tidy_advice: Option<TidyAdvice>,
}
impl FileObj {
pub fn new(name: PathBuf) -> Self {
FileObj {
name,
added_lines: Vec::<u32>::new(),
added_ranges: Vec::<RangeInclusive<u32>>::new(),
diff_chunks: Vec::<RangeInclusive<u32>>::new(),
format_advice: None,
tidy_advice: None,
}
}
pub fn from(
name: PathBuf,
added_lines: Vec<u32>,
diff_chunks: Vec<RangeInclusive<u32>>,
) -> Self {
let added_ranges = FileObj::consolidate_numbers_to_ranges(&added_lines);
FileObj {
name,
added_lines,
added_ranges,
diff_chunks,
format_advice: None,
tidy_advice: None,
}
}
fn consolidate_numbers_to_ranges(lines: &[u32]) -> Vec<RangeInclusive<u32>> {
let mut range_start = None;
let mut ranges: Vec<RangeInclusive<u32>> = Vec::new();
for (index, number) in lines.iter().enumerate() {
if index == 0 {
range_start = Some(*number);
} else if number - 1 != lines[index - 1] {
ranges.push(RangeInclusive::new(range_start.unwrap(), lines[index - 1]));
range_start = Some(*number);
}
if index == lines.len() - 1 {
ranges.push(RangeInclusive::new(range_start.unwrap(), *number));
}
}
ranges
}
pub fn get_ranges(&self, lines_changed_only: &LinesChangedOnly) -> Vec<RangeInclusive<u32>> {
match lines_changed_only {
LinesChangedOnly::Diff => self.diff_chunks.to_vec(),
LinesChangedOnly::On => self.added_ranges.to_vec(),
_ => Vec::new(),
}
}
pub fn is_hunk_in_diff(&self, hunk: &DiffHunk) -> Option<(u32, u32)> {
let (start_line, end_line) = if hunk.old_lines() > 0 {
let start = hunk.old_start();
(start, start + hunk.old_lines() - 1)
} else {
let start = hunk.new_start();
(start, start)
};
for range in &self.diff_chunks {
if range.contains(&start_line) && range.contains(&end_line) {
return Some((start_line, end_line));
}
}
None
}
fn is_line_in_diff(&self, line: &u32) -> bool {
for range in &self.diff_chunks {
if range.contains(line) {
return true;
}
}
false
}
pub fn make_suggestions_from_patch(
&self,
review_comments: &mut ReviewComments,
summary_only: bool,
) -> Result<(), FileObjError> {
let original_content = fs::read(&self.name).map_err(FileObjError::ReadFile)?;
let file_name = self.name.to_str().unwrap_or_default().replace("\\", "/");
let file_path = Path::new(&file_name);
if let Some(advice) = &self.format_advice
&& let Some(patched) = &advice.patched
{
let mut patch = make_patch(file_path, patched, &original_content)
.map_err(|e| FileObjError::MakePatchFailed(file_name.clone(), e))?;
advice.get_suggestions(review_comments, self, &mut patch, summary_only)?;
}
if let Some(advice) = &self.tidy_advice {
if let Some(patched) = &advice.patched {
let mut patch = make_patch(file_path, patched, &original_content)
.map_err(|e| FileObjError::MakePatchFailed(file_name.clone(), e))?;
advice.get_suggestions(review_comments, self, &mut patch, summary_only)?;
}
if summary_only {
return Ok(());
}
let file_ext = self
.name
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
let mut total = 0;
for note in &advice.notes {
if note.fixed_lines.is_empty() && self.is_line_in_diff(¬e.line) {
let mut suggestion = format!(
"### clang-tidy diagnostic\n**{file_name}:{}:{}** {}: [{}]\n\n> {}\n",
¬e.line,
¬e.cols,
¬e.severity,
note.diagnostic_link(),
¬e.rationale
);
if !note.suggestion.is_empty() {
suggestion.push_str(
format!("\n```{file_ext}\n{}\n```\n", ¬e.suggestion.join("\n"))
.as_str(),
);
}
total += 1;
let mut is_merged = false;
for s in &mut review_comments.comments {
if s.path == file_name
&& s.line_end >= note.line
&& s.line_start <= note.line
{
s.suggestion.push_str(suggestion.as_str());
is_merged = true;
break;
}
}
if !is_merged {
review_comments.comments.push(Suggestion {
line_start: note.line,
line_end: note.line,
suggestion,
path: file_name.to_owned(),
});
}
}
}
review_comments.tool_total[1] =
Some(review_comments.tool_total[1].unwrap_or_default() + total);
}
Ok(())
}
}
pub fn get_line_count_from_offset(contents: &[u8], offset: u32) -> u32 {
let offset = (offset as usize).min(contents.len());
let lines = contents[0..offset].split(|byte| byte == &b'\n');
lines.count() as u32
}
#[cfg(test)]
mod test {
use std::{fs, path::PathBuf};
use super::{FileObj, get_line_count_from_offset};
use crate::cli::LinesChangedOnly;
#[test]
fn translate_byte_offset() {
let contents = fs::read(PathBuf::from("tests/demo/demo.cpp")).unwrap();
let lines = get_line_count_from_offset(&contents, 144);
assert_eq!(lines, 13);
}
#[test]
fn get_line_count_edge_cases() {
assert_eq!(get_line_count_from_offset(&[], 0), 1);
assert_eq!(get_line_count_from_offset(b"abc", 3), 1);
assert_eq!(get_line_count_from_offset(b"a\n\nb", 3), 3);
assert_eq!(get_line_count_from_offset(b"a\nb\n", 10), 3);
}
#[test]
fn get_ranges_none() {
let file_obj = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
let ranges = file_obj.get_ranges(&LinesChangedOnly::Off);
assert!(ranges.is_empty());
}
#[test]
fn get_ranges_diff() {
let diff_chunks = vec![1..=10];
let added_lines = vec![4, 5, 9];
let file_obj = FileObj::from(
PathBuf::from("tests/demo/demo.cpp"),
added_lines,
diff_chunks.clone(),
);
let ranges = file_obj.get_ranges(&LinesChangedOnly::Diff);
assert_eq!(ranges, diff_chunks);
}
#[test]
fn get_ranges_added() {
let diff_chunks = vec![1..=10];
let added_lines = vec![4, 5, 9];
let file_obj = FileObj::from(
PathBuf::from("tests/demo/demo.cpp"),
added_lines,
diff_chunks,
);
let ranges = file_obj.get_ranges(&LinesChangedOnly::On);
assert_eq!(ranges, vec![4..=5, 9..=9]);
}
#[test]
fn line_not_in_diff() {
let file_obj = FileObj::new(PathBuf::from("tests/demo/demo.cpp"));
assert!(!file_obj.is_line_in_diff(&42));
}
}