use crate::checker::{CheckResult, CheckerCore, FileMetadata, FileReader, Issue};
use crate::config::Config;
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::Path;
pub struct FileChecker<R: FileReader> {
file_reader: R,
config: Config,
}
impl<R: FileReader> FileChecker<R> {
pub fn new(file_reader: R, config: Config) -> Self {
Self {
file_reader,
config,
}
}
pub fn check_file(&self, path: &Path) -> CheckResult {
let metadata = match self.file_reader.metadata(path) {
Ok(metadata) => metadata,
Err(e) => {
return CheckResult {
file_path: path.to_path_buf(),
issues: vec![],
error: Some(format!("{}: {}", path.display(), e)),
};
},
};
if metadata.len > 10 * 1024 * 1024 {
self.check_file_streaming(path)
} else {
self.check_file_in_memory(path)
}
}
fn check_file_in_memory(&self, path: &Path) -> CheckResult {
let content = match self.file_reader.read_to_string(path) {
Ok(content) => content,
Err(e) => {
return CheckResult {
file_path: path.to_path_buf(),
issues: vec![],
error: Some(format!("{}: {}", path.display(), e)),
};
},
};
let checker = CheckerCore::new(self.config.clone());
let issues = checker.check_content(&content);
CheckResult {
file_path: path.to_path_buf(),
issues,
error: None,
}
}
fn check_file_streaming(&self, path: &Path) -> CheckResult {
let mut issues = Vec::new();
let reader = match self.file_reader.open(path) {
Ok(reader) => reader,
Err(e) => {
return CheckResult {
file_path: path.to_path_buf(),
issues,
error: Some(format!("{}: {}", path.display(), e)),
};
},
};
let buf_reader = BufReader::new(reader);
let mut line_number = 0;
let mut has_content = false;
let checker = CheckerCore::new(self.config.clone());
for line_result in buf_reader.lines() {
match line_result {
Ok(line) => {
has_content = true;
line_number += 1;
if let Some(issue) = checker.check_line_trailing_whitespace(&line, line_number)
{
issues.push(issue);
}
},
Err(_) => break,
}
}
if self.config.checks.newline_ending && has_content {
if let Some(issue) = self.check_final_newline_streaming(path) {
issues.push(issue);
}
}
CheckResult {
file_path: path.to_path_buf(),
issues,
error: None,
}
}
fn check_final_newline_streaming(&self, path: &Path) -> Option<Issue> {
if let Ok(mut file) = File::open(path) {
if let Ok(metadata) = file.metadata() {
let file_size = metadata.len();
if file_size == 0 {
return None;
}
if file_size == 1 {
let mut buffer = [0u8; 1];
if let Ok(bytes_read) = file.read(&mut buffer)
&& bytes_read == 1
{
let ends_with_newline = buffer[0] == b'\n';
if !ends_with_newline {
return Some(Issue {
issue_type: crate::IssueType::MissingNewline,
line: None,
message: "Missing newline at end of file".to_string(),
});
}
}
return None;
}
let seek_pos = if file_size >= 2 {
-2
} else {
-(file_size as i64)
};
if file.seek(SeekFrom::End(seek_pos)).is_ok() {
let mut buffer = [0u8; 2];
if let Ok(bytes_read) = file.read(&mut buffer) {
let end_bytes = &buffer[..bytes_read];
let ends_with_newline = end_bytes.last() == Some(&b'\n');
let ends_with_double_newline = bytes_read == 2 && end_bytes == b"\n\n";
if !ends_with_newline {
return Some(Issue {
issue_type: crate::IssueType::MissingNewline,
line: None,
message: "Missing newline at end of file".to_string(),
});
} else if ends_with_double_newline {
return Some(Issue {
issue_type: crate::IssueType::MultipleNewlines,
line: None,
message: "Multiple newlines at end of file".to_string(),
});
}
}
}
}
}
None
}
}
pub struct StdFileReader;
impl FileReader for StdFileReader {
fn read_to_string(&self, path: &Path) -> io::Result<String> {
fs::read_to_string(path)
}
fn open(&self, path: &Path) -> io::Result<Box<dyn io::Read>> {
File::open(path).map(|f| Box::new(f) as Box<dyn io::Read>)
}
fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
let metadata = fs::metadata(path)?;
Ok(FileMetadata {
len: metadata.len(),
is_file: metadata.is_file(),
is_dir: metadata.is_dir(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::builders::TestFileBuilder;
use crate::testing::mocks::MockFileSystem;
use std::path::PathBuf;
#[test]
fn test_file_checker_no_issues() {
let mut fs = MockFileSystem::new();
fs.add_file(
"test.txt",
TestFileBuilder::new("test.txt")
.with_line("clean line")
.build()
.1,
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("test.txt"));
assert!(result.error.is_none());
assert!(result.issues.is_empty());
}
#[test]
fn test_file_checker_trailing_spaces() {
let mut fs = MockFileSystem::new();
fs.add_file(
"test.txt",
TestFileBuilder::new("test.txt")
.with_line("line with spaces ")
.build()
.1,
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("test.txt"));
assert!(result.error.is_none());
assert_eq!(result.issues.len(), 1);
assert_eq!(result.issues[0].issue_type, crate::IssueType::TrailingSpace);
}
#[test]
fn test_file_checker_missing_newline() {
let mut fs = MockFileSystem::new();
fs.add_file(
"test.txt",
TestFileBuilder::new("test.txt")
.with_line("no newline")
.without_final_newline()
.build()
.1,
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("test.txt"));
assert!(result.error.is_none());
assert_eq!(result.issues.len(), 1);
assert_eq!(
result.issues[0].issue_type,
crate::IssueType::MissingNewline
);
}
#[test]
fn test_file_checker_multiple_issues() {
let mut fs = MockFileSystem::new();
fs.add_file(
"test.txt",
TestFileBuilder::new("test.txt")
.with_line("clean line")
.with_trailing_spaces()
.with_trailing_tabs()
.without_final_newline()
.build()
.1,
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("test.txt"));
assert!(result.error.is_none());
assert_eq!(result.issues.len(), 3); }
#[test]
fn test_file_checker_file_not_found() {
let fs = MockFileSystem::new();
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("nonexistent.txt"));
assert!(result.error.is_some());
let error = result.error.unwrap();
assert!(error.contains("nonexistent.txt") && error.contains("not found"));
assert!(result.issues.is_empty());
}
#[test]
fn test_file_checker_disabled_checks() {
let mut config = Config::default();
config.checks.trailing_spaces = false;
config.checks.newline_ending = false;
let mut fs = MockFileSystem::new();
fs.add_file(
"test.txt",
TestFileBuilder::new("test.txt")
.with_trailing_spaces()
.without_final_newline()
.build()
.1,
);
let checker = FileChecker::new(fs, config);
let result = checker.check_file(&PathBuf::from("test.txt"));
assert!(result.error.is_none());
assert!(result.issues.is_empty());
}
#[test]
fn test_file_checker_large_file() {
let mut fs = MockFileSystem::new();
let content = "line1\nline2 \nline3";
fs.add_file("large.txt", content.to_string());
fs.set_metadata(
"large.txt",
FileMetadata {
len: 11 * 1024 * 1024, is_file: true,
is_dir: false,
},
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("large.txt"));
assert!(result.error.is_none());
assert!(!result.issues.is_empty());
}
#[test]
fn test_std_file_reader() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "test content").unwrap();
temp_file.flush().unwrap();
let reader = StdFileReader;
let content = reader.read_to_string(temp_file.path()).unwrap();
assert_eq!(content, "test content\n");
let metadata = reader.metadata(temp_file.path()).unwrap();
assert!(metadata.is_file);
assert!(metadata.len > 0);
}
#[test]
fn test_std_file_reader_open() {
use std::io::{Read, Write};
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "test open").unwrap();
temp_file.flush().unwrap();
let reader = StdFileReader;
let mut file = reader.open(temp_file.path()).unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
assert_eq!(content, "test open");
}
#[test]
fn test_std_file_reader_nonexistent() {
let reader = StdFileReader;
assert!(
reader
.read_to_string(&PathBuf::from("/nonexistent"))
.is_err()
);
assert!(reader.open(&PathBuf::from("/nonexistent")).is_err());
assert!(reader.metadata(&PathBuf::from("/nonexistent")).is_err());
}
#[test]
fn test_file_checker_streaming_large_file_with_issues() {
let mut fs = MockFileSystem::new();
let mut content = String::new();
for i in 0..100 {
if i == 50 {
content.push_str("line with trailing spaces \n");
} else {
content.push_str(&format!("line {i}\n"));
}
}
content.push_str("last line");
fs.add_file("large.txt", content);
fs.set_metadata(
"large.txt",
FileMetadata {
len: 11 * 1024 * 1024, is_file: true,
is_dir: false,
},
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("large.txt"));
assert!(result.error.is_none());
assert!(!result.issues.is_empty());
assert!(
result
.issues
.iter()
.any(|i| i.issue_type == crate::IssueType::TrailingSpace)
);
}
#[test]
fn test_file_checker_streaming_error_handling() {
let mut fs = MockFileSystem::new();
fs.add_error(
"error.txt",
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Access denied"),
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("error.txt"));
assert!(result.error.is_some());
assert!(result.error.unwrap().contains("Access denied"));
}
#[test]
fn test_check_final_newline_streaming_coverage() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "no newline").unwrap();
temp_file.flush().unwrap();
let checker = FileChecker::new(StdFileReader, Config::default());
let result = checker.check_file(temp_file.path());
assert!(result.error.is_none());
assert!(
result
.issues
.iter()
.any(|i| i.issue_type == crate::IssueType::MissingNewline)
);
let mut temp_file2 = NamedTempFile::new().unwrap();
write!(temp_file2, "content\n\n").unwrap();
temp_file2.flush().unwrap();
let result2 = checker.check_file(temp_file2.path());
assert!(
result2
.issues
.iter()
.any(|i| i.issue_type == crate::IssueType::MultipleNewlines)
);
}
#[test]
fn test_file_checker_various_configs() {
let mut fs = MockFileSystem::new();
fs.add_file("test.txt", "trailing \nno newline");
let mut config1 = Config::default();
config1.checks.trailing_spaces = true;
config1.checks.newline_ending = false;
let checker1 = FileChecker::new(fs.clone(), config1);
let result1 = checker1.check_file(&PathBuf::from("test.txt"));
assert_eq!(result1.issues.len(), 1);
assert_eq!(
result1.issues[0].issue_type,
crate::IssueType::TrailingSpace
);
let mut config2 = Config::default();
config2.checks.trailing_spaces = false;
config2.checks.newline_ending = true;
let checker2 = FileChecker::new(fs, config2);
let result2 = checker2.check_file(&PathBuf::from("test.txt"));
assert_eq!(result2.issues.len(), 1);
assert_eq!(
result2.issues[0].issue_type,
crate::IssueType::MissingNewline
);
}
#[test]
fn test_check_file_streaming_metadata_error() {
let mut fs = MockFileSystem::new();
fs.add_error(
"error.txt",
std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Metadata access denied",
),
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("error.txt"));
assert!(result.error.is_some());
assert!(result.error.unwrap().contains("Metadata access denied"));
assert!(result.issues.is_empty());
}
#[test]
fn test_check_file_streaming_mode_with_no_newline_check_disabled() {
let mut fs = MockFileSystem::new();
let content = "a".repeat(100); fs.add_file("large.txt", content);
fs.set_metadata(
"large.txt",
FileMetadata {
len: 11 * 1024 * 1024, is_file: true,
is_dir: false,
},
);
let mut config = Config::default();
config.checks.newline_ending = false;
let checker = FileChecker::new(fs, config);
let result = checker.check_file(&PathBuf::from("large.txt"));
assert!(result.error.is_none());
assert!(result.issues.is_empty());
}
#[test]
fn test_check_file_streaming_lines_error() {
let mut fs = MockFileSystem::new();
fs.add_error(
"stream_error.txt",
std::io::Error::new(std::io::ErrorKind::BrokenPipe, "Stream error"),
);
fs.set_metadata(
"stream_error.txt",
FileMetadata {
len: 11 * 1024 * 1024,
is_file: true,
is_dir: false,
},
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("stream_error.txt"));
assert!(result.error.is_some());
assert!(result.error.unwrap().contains("Stream error"));
}
#[test]
fn test_check_final_newline_streaming_seek_error() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "x").unwrap(); temp_file.flush().unwrap();
let checker = FileChecker::new(StdFileReader, Config::default());
let result = checker.check_final_newline_streaming(temp_file.path());
assert!(result.is_some());
assert_eq!(result.unwrap().issue_type, crate::IssueType::MissingNewline);
}
#[test]
fn test_check_file_in_memory_read_error() {
let mut fs = MockFileSystem::new();
fs.add_error(
"read_error.txt",
std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid UTF-8"),
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("read_error.txt"));
assert!(result.error.is_some());
assert!(result.error.unwrap().contains("Invalid UTF-8"));
assert!(result.issues.is_empty());
}
#[test]
fn test_check_file_streaming_empty_file() {
let mut fs = MockFileSystem::new();
fs.add_file("empty.txt", "");
fs.set_metadata(
"empty.txt",
FileMetadata {
len: 11 * 1024 * 1024, is_file: true,
is_dir: false,
},
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("empty.txt"));
assert!(result.error.is_none());
assert!(result.issues.is_empty()); }
#[test]
fn test_check_final_newline_streaming_with_single_byte() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "a").unwrap(); temp_file.flush().unwrap();
let checker = FileChecker::new(StdFileReader, Config::default());
let result = checker.check_final_newline_streaming(temp_file.path());
assert!(result.is_some());
assert_eq!(result.unwrap().issue_type, crate::IssueType::MissingNewline);
}
#[test]
fn test_check_final_newline_streaming_nonexistent_file() {
let checker = FileChecker::new(StdFileReader, Config::default());
let result = checker.check_final_newline_streaming(&PathBuf::from("/nonexistent/file.txt"));
assert!(result.is_none());
}
#[test]
fn test_file_checker_force_all_code_paths() {
let mut fs = MockFileSystem::new();
let mut content = String::new();
for i in 0..10 {
if i % 2 == 0 {
content.push_str(&format!("line {i} with spaces \n"));
} else {
content.push_str(&format!("line {i} clean\n"));
}
}
fs.add_file("streaming.txt", content);
fs.set_metadata(
"streaming.txt",
FileMetadata {
len: 11 * 1024 * 1024,
is_file: true,
is_dir: false,
},
);
let checker = FileChecker::new(fs.clone(), Config::default());
let result = checker.check_file(&PathBuf::from("streaming.txt"));
assert!(result.error.is_none());
assert_eq!(result.issues.len(), 5);
fs.set_metadata(
"streaming.txt",
FileMetadata {
len: 1024, is_file: true,
is_dir: false,
},
);
let result2 = checker.check_file(&PathBuf::from("streaming.txt"));
assert!(result2.error.is_none());
assert_eq!(result2.issues.len(), 5); }
#[test]
fn test_check_final_newline_streaming_coverage_gaps() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
write!(temp_file, "\n\n").unwrap();
temp_file.flush().unwrap();
let checker = FileChecker::new(StdFileReader, Config::default());
let result = checker.check_final_newline_streaming(temp_file.path());
assert!(result.is_some());
assert_eq!(
result.unwrap().issue_type,
crate::IssueType::MultipleNewlines
);
let mut temp_file2 = NamedTempFile::new().unwrap();
writeln!(temp_file2, "content").unwrap();
temp_file2.flush().unwrap();
let result2 = checker.check_final_newline_streaming(temp_file2.path());
assert!(result2.is_none());
let mut temp_file3 = NamedTempFile::new().unwrap();
write!(temp_file3, "x").unwrap();
temp_file3.flush().unwrap();
let result3 = checker.check_final_newline_streaming(temp_file3.path());
assert!(result3.is_some());
assert_eq!(
result3.unwrap().issue_type,
crate::IssueType::MissingNewline
);
}
#[test]
fn test_check_file_streaming_with_broken_lines() {
let mut fs = MockFileSystem::new();
fs.add_file("broken.txt", "line1\nline2 with spaces \nline3\n");
fs.set_metadata(
"broken.txt",
FileMetadata {
len: 11 * 1024 * 1024, is_file: true,
is_dir: false,
},
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("broken.txt"));
assert!(result.error.is_none());
assert!(
result
.issues
.iter()
.any(|i| i.issue_type == crate::IssueType::TrailingSpace && i.line == Some(2))
);
}
#[test]
fn test_check_file_streaming_has_content_flag() {
let mut fs = MockFileSystem::new();
fs.add_file("double_newline.txt", "line1\nline2\n\n");
fs.set_metadata(
"double_newline.txt",
FileMetadata {
len: 11 * 1024 * 1024,
is_file: true,
is_dir: false,
},
);
let checker = FileChecker::new(fs.clone(), Config::default());
let result = checker.check_file(&PathBuf::from("double_newline.txt"));
assert!(result.error.is_none());
let mut fs2 = MockFileSystem::new();
fs2.add_file("normal.txt", "line1\nline2\n");
fs2.set_metadata(
"normal.txt",
FileMetadata {
len: 11 * 1024 * 1024,
is_file: true,
is_dir: false,
},
);
let checker2 = FileChecker::new(fs2, Config::default());
let result2 = checker2.check_file(&PathBuf::from("normal.txt"));
assert!(result2.error.is_none());
}
#[test]
fn test_file_checker_direct_method_calls() {
let mut fs = MockFileSystem::new();
fs.add_file("test.txt", "content with spaces \nno final newline");
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file_in_memory(&PathBuf::from("test.txt"));
assert!(result.error.is_none());
assert_eq!(result.issues.len(), 2); }
#[test]
fn test_check_final_newline_streaming_real_file_variations() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.flush().unwrap();
let checker = FileChecker::new(StdFileReader, Config::default());
let result = checker.check_final_newline_streaming(temp_file.path());
assert!(result.is_none() || result.is_some());
}
#[test]
fn test_file_checker_force_uncovered_paths() {
let mut fs = MockFileSystem::new();
fs.add_file("edge.txt", "x".repeat(100));
fs.set_metadata(
"edge.txt",
FileMetadata {
len: 10 * 1024 * 1024, is_file: true,
is_dir: false,
},
);
let checker = FileChecker::new(fs.clone(), Config::default());
let result = checker.check_file(&PathBuf::from("edge.txt"));
assert!(result.error.is_none());
fs.set_metadata(
"edge.txt",
FileMetadata {
len: 10 * 1024 * 1024 + 1, is_file: true,
is_dir: false,
},
);
let result2 = checker.check_file(&PathBuf::from("edge.txt"));
assert!(result2.error.is_none());
}
#[test]
fn test_check_file_streaming_line_error_simulation() {
let mut fs = MockFileSystem::new();
let content = "line1\nline2\x00embedded null\nline3 spaces \n";
fs.add_file("special.txt", content);
fs.set_metadata(
"special.txt",
FileMetadata {
len: 11 * 1024 * 1024,
is_file: true,
is_dir: false,
},
);
let checker = FileChecker::new(fs, Config::default());
let result = checker.check_file(&PathBuf::from("special.txt"));
assert!(result.error.is_none());
assert!(result.issues.iter().any(|i| i.line == Some(3)));
}
#[test]
fn test_check_final_newline_streaming_edge_cases() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file).unwrap();
temp_file.flush().unwrap();
let checker = FileChecker::new(StdFileReader, Config::default());
let result = checker.check_final_newline_streaming(temp_file.path());
assert!(result.is_none()); }
}