use std::fmt::Debug;
use std::fs;
use std::io::Read;
use std::path::{Component, Path};
use std::{ops::RangeInclusive, path::PathBuf};
use crate::clang_tools::clang_format::FormatAdvice;
use crate::clang_tools::clang_tidy::TidyAdvice;
use crate::clang_tools::{make_patch, MakeSuggestions, ReviewComments, Suggestion};
use crate::cli::LinesChangedOnly;
mod file_filter;
pub use file_filter::FileFilter;
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
}
pub fn make_suggestions_from_patch(
&self,
review_comments: &mut ReviewComments,
summary_only: bool,
) {
let original_content =
fs::read(&self.name).expect("Failed to read original contents of file");
let file_name = self
.name
.to_str()
.expect("Failed to convert file extension to string")
.replace("\\", "/");
let file_path = Path::new(&file_name);
if let Some(advice) = &self.format_advice {
if let Some(patched) = &advice.patched {
let mut patch = make_patch(file_path, patched, &original_content);
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);
advice.get_suggestions(review_comments, self, &mut patch, summary_only);
}
if summary_only {
return;
}
let file_ext = self
.name
.extension()
.unwrap_or_default()
.to_str()
.expect("Failed to convert file extension to string");
for note in &advice.notes {
if note.fixed_lines.is_empty() {
let mut suggestion = format!(
"### clang-tidy diagnostic\n**{}:{}:{}** {}: [{}]\n> {}",
file_name,
¬e.line,
¬e.cols,
¬e.severity,
note.diagnostic_link(),
¬e.rationale
);
if !note.suggestion.is_empty() {
suggestion.push_str(
format!("```{}\n{}```", file_ext, ¬e.suggestion.join("\n")).as_str(),
);
}
review_comments.tool_total[1] += 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(),
});
}
}
}
}
}
}
pub fn get_line_cols_from_offset(file_path: &PathBuf, offset: usize) -> (usize, usize) {
let mut file_buf = vec![0; offset];
fs::File::open(file_path)
.unwrap()
.read_exact(&mut file_buf)
.unwrap();
let lines = file_buf.split(|byte| byte == &b'\n');
let line_count = lines.clone().count();
let column_count = lines.last().unwrap_or(&[]).len() + 1; (line_count, column_count)
}
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
#[cfg(test)]
mod test {
use std::env::current_dir;
use std::path::PathBuf;
use super::{get_line_cols_from_offset, normalize_path, FileObj};
use crate::cli::LinesChangedOnly;
#[test]
fn normalize_redirects() {
let mut src = current_dir().unwrap();
src.push("..");
src.push(
current_dir()
.unwrap()
.strip_prefix(current_dir().unwrap().parent().unwrap())
.unwrap(),
);
println!("relative path = {}", src.to_str().unwrap());
assert_eq!(normalize_path(&src), current_dir().unwrap());
}
#[test]
fn normalize_no_root() {
let src = PathBuf::from("../cpp-linter");
let mut cur_dir = current_dir().unwrap();
cur_dir = cur_dir
.strip_prefix(current_dir().unwrap().parent().unwrap())
.unwrap()
.to_path_buf();
println!("relative path = {}", src.to_str().unwrap());
assert_eq!(normalize_path(&src), cur_dir);
}
#[test]
fn normalize_current_redirect() {
let src = PathBuf::from("tests/./ignored_paths");
println!("relative path = {}", src.to_str().unwrap());
assert_eq!(normalize_path(&src), PathBuf::from("tests/ignored_paths"));
}
#[test]
fn translate_byte_offset() {
let (lines, cols) = get_line_cols_from_offset(&PathBuf::from("tests/demo/demo.cpp"), 144);
println!("lines: {lines}, cols: {cols}");
assert_eq!(lines, 13);
assert_eq!(cols, 5);
}
#[test]
fn get_ranges_0() {
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_2() {
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_1() {
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]);
}
}