use crate::Result;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
#[derive(Debug, clap::Args)]
pub struct EndOfFileFixer {
#[clap(short, long, conflicts_with = "fix")]
pub diff: bool,
#[clap(short, long)]
pub fix: bool,
#[clap(required = true)]
pub files: Vec<PathBuf>,
}
impl EndOfFileFixer {
pub async fn run(&self) -> Result<()> {
let mut found_issues = false;
for file_path in &self.files {
if !is_text_file(file_path)? {
continue;
}
if self.fix {
fix_end_of_file(file_path)?;
} else if self.diff {
if let Some(diff) = generate_diff(file_path)? {
print!("{}", diff);
found_issues = true;
}
} else if !has_proper_ending(file_path)? {
println!("{}", file_path.display());
found_issues = true;
}
}
if !self.fix && found_issues {
std::process::exit(1);
}
Ok(())
}
}
fn is_text_file(path: &PathBuf) -> Result<bool> {
if !path.exists() || !path.is_file() {
return Ok(false);
}
let metadata = fs::metadata(path)?;
if metadata.len() == 0 {
return Ok(true); }
let mut file = fs::File::open(path)?;
let mut buffer = vec![0; 8192.min(metadata.len() as usize)];
file.read_exact(&mut buffer)?;
if buffer.contains(&0) {
return Ok(false);
}
Ok(std::str::from_utf8(&buffer).is_ok())
}
fn normalize_ending(content: &str) -> String {
let trimmed = content.trim_end_matches('\n');
format!("{trimmed}\n")
}
fn generate_diff(path: &PathBuf) -> Result<Option<String>> {
if has_proper_ending(path)? {
return Ok(None);
}
let original = fs::read_to_string(path)?;
let fixed = normalize_ending(&original);
let path_str = path.display().to_string();
let diff = crate::diff::render_unified_diff(
&original,
&fixed,
&format!("a/{}", path_str),
&format!("b/{}", path_str),
);
Ok(Some(diff))
}
fn has_proper_ending(path: &PathBuf) -> Result<bool> {
let metadata = fs::metadata(path)?;
if metadata.len() == 0 {
return Ok(true); }
use std::io::Seek;
let mut file = fs::File::open(path)?;
if metadata.len() == 1 {
let mut last_byte = [0u8; 1];
file.read_exact(&mut last_byte)?;
return Ok(last_byte[0] == b'\n');
}
let mut last_two = [0u8; 2];
file.seek(std::io::SeekFrom::End(-2))?;
file.read_exact(&mut last_two)?;
Ok(last_two[1] == b'\n' && last_two[0] != b'\n')
}
fn fix_end_of_file(path: &PathBuf) -> Result<()> {
let metadata = fs::metadata(path)?;
if metadata.len() == 0 {
return Ok(()); }
if has_proper_ending(path)? {
return Ok(());
}
let content = fs::read_to_string(path)?;
fs::write(path, normalize_ending(&content))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_has_proper_ending_true() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line1").unwrap();
writeln!(file, "line2").unwrap();
file.flush().unwrap();
let path = file.path().to_path_buf();
assert!(has_proper_ending(&path).unwrap());
}
#[test]
fn test_has_proper_ending_missing_newline() {
let mut file = NamedTempFile::new().unwrap();
write!(file, "line1\nline2").unwrap();
file.flush().unwrap();
let path = file.path().to_path_buf();
assert!(!has_proper_ending(&path).unwrap());
}
#[test]
fn test_has_proper_ending_extra_newlines() {
let mut file = NamedTempFile::new().unwrap();
write!(file, "line1\nline2\n\n").unwrap();
file.flush().unwrap();
let path = file.path().to_path_buf();
assert!(!has_proper_ending(&path).unwrap());
}
#[test]
fn test_has_proper_ending_single_newline_file() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file).unwrap();
file.flush().unwrap();
let path = file.path().to_path_buf();
assert!(has_proper_ending(&path).unwrap());
}
#[test]
fn test_fix_end_of_file() {
let mut file = NamedTempFile::new().unwrap();
write!(file, "line1\nline2").unwrap();
file.flush().unwrap();
let path = file.path().to_path_buf();
assert!(!has_proper_ending(&path).unwrap());
fix_end_of_file(&path).unwrap();
assert!(has_proper_ending(&path).unwrap());
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content, "line1\nline2\n");
}
#[test]
fn test_fix_already_correct() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line1").unwrap();
writeln!(file, "line2").unwrap();
file.flush().unwrap();
let path = file.path().to_path_buf();
assert!(has_proper_ending(&path).unwrap());
let content_before = fs::read_to_string(&path).unwrap();
fix_end_of_file(&path).unwrap();
let content_after = fs::read_to_string(&path).unwrap();
assert_eq!(content_before, content_after);
}
#[test]
fn test_fix_extra_trailing_newlines() {
let mut file = NamedTempFile::new().unwrap();
write!(file, "line1\nline2\n\n\n").unwrap();
file.flush().unwrap();
let path = file.path().to_path_buf();
assert!(!has_proper_ending(&path).unwrap());
fix_end_of_file(&path).unwrap();
assert!(has_proper_ending(&path).unwrap());
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content, "line1\nline2\n");
}
#[test]
fn test_empty_file() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
assert!(has_proper_ending(&path).unwrap());
fix_end_of_file(&path).unwrap();
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content, "");
}
#[test]
fn test_is_text_file_with_text() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "This is a text file").unwrap();
file.flush().unwrap();
let path = file.path().to_path_buf();
assert!(is_text_file(&path).unwrap());
}
#[test]
fn test_is_text_file_with_binary() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(&[0x00, 0x01, 0x02, 0x03, 0xFF]).unwrap();
file.flush().unwrap();
let path = file.path().to_path_buf();
assert!(!is_text_file(&path).unwrap());
}
}