use std::path::{Path, PathBuf};
use crate::core::Language;
use crate::effects::{
combine_validations, run_validation, validation_failure, validation_success, AnalysisValidation,
};
use crate::errors::AnalysisError;
#[derive(Debug, Clone)]
pub struct FileContent {
pub path: PathBuf,
pub content: String,
pub language: Language,
}
pub fn validate_files_readable(files: &[PathBuf]) -> AnalysisValidation<Vec<FileContent>> {
let validations: Vec<AnalysisValidation<FileContent>> = files
.iter()
.map(|path| validate_single_file_readable(path))
.collect();
combine_validations(validations)
}
fn validate_single_file_readable(path: &Path) -> AnalysisValidation<FileContent> {
if !path.exists() {
return validation_failure(AnalysisError::io_with_path(
format!("File not found: {}", path.display()),
path,
));
}
if !path.is_file() {
return validation_failure(AnalysisError::io_with_path(
format!("Path is not a file: {}", path.display()),
path,
));
}
match std::fs::read_to_string(path) {
Ok(content) => {
let language = Language::from_path(path);
validation_success(FileContent {
path: path.to_path_buf(),
content,
language,
})
}
Err(e) => validation_failure(AnalysisError::io_with_path(
format!("Cannot read file: {}", e),
path,
)),
}
}
pub fn validate_files_readable_result(files: &[PathBuf]) -> anyhow::Result<Vec<FileContent>> {
run_validation(validate_files_readable(files))
}
#[derive(Debug, Clone)]
pub struct FileReadSummary {
pub successful: usize,
pub failed: usize,
pub total: usize,
pub errors: Vec<AnalysisError>,
pub files: Vec<FileContent>,
}
impl FileReadSummary {
pub fn all_successful(&self) -> bool {
self.failed == 0
}
pub fn format_summary(&self) -> String {
if self.all_successful() {
format!("Successfully read {} files", self.successful)
} else {
format!(
"Read {} of {} files ({} failed)",
self.successful, self.total, self.failed
)
}
}
}
pub fn read_files_with_summary(files: &[PathBuf]) -> FileReadSummary {
let mut successful_files = Vec::new();
let mut errors = Vec::new();
for path in files {
match validate_single_file_readable(path) {
stillwater::Validation::Success(file_content) => {
successful_files.push(file_content);
}
stillwater::Validation::Failure(errs) => {
for err in errs {
errors.push(err);
}
}
}
}
let successful = successful_files.len();
let failed = errors.len();
let total = files.len();
FileReadSummary {
successful,
failed,
total,
errors,
files: successful_files,
}
}
pub fn validate_sources_parseable(files: &[FileContent]) -> AnalysisValidation<Vec<FileContent>> {
let validations: Vec<AnalysisValidation<FileContent>> =
files.iter().map(validate_single_source_parseable).collect();
combine_validations(validations)
}
fn validate_single_source_parseable(file: &FileContent) -> AnalysisValidation<FileContent> {
match file.language {
Language::Rust => validate_rust_parseable(file),
Language::Python => validate_python_parseable(file),
Language::JavaScript | Language::TypeScript => validate_js_ts_parseable(file),
Language::Unknown => {
validation_success(file.clone())
}
}
}
fn validate_rust_parseable(file: &FileContent) -> AnalysisValidation<FileContent> {
match crate::extraction::UnifiedFileExtractor::extract(&file.path, &file.content) {
Ok(_) => validation_success(file.clone()),
Err(e) => {
let line = 0; validation_failure(AnalysisError::parse_with_context(
format!("Rust parse error: {}", e),
&file.path,
line,
))
}
}
}
fn validate_python_parseable(file: &FileContent) -> AnalysisValidation<FileContent> {
for (line_num, line) in file.content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if line.starts_with(' ') && line.contains('\t') {
return validation_failure(AnalysisError::parse_with_context(
"Mixed tabs and spaces in indentation".to_string(),
&file.path,
line_num + 1,
));
}
}
validation_success(file.clone())
}
fn validate_js_ts_parseable(file: &FileContent) -> AnalysisValidation<FileContent> {
use crate::analyzers::typescript::parser::{detect_variant, parse_source};
let variant = detect_variant(&file.path);
match parse_source(&file.content, &file.path, variant) {
Ok(_) => validation_success(file.clone()),
Err(e) => validation_failure(AnalysisError::parse_with_context(
format!("JavaScript/TypeScript parse error: {}", e),
&file.path,
0,
)),
}
}
pub fn validate_sources_parseable_result(
files: &[FileContent],
) -> anyhow::Result<Vec<FileContent>> {
run_validation(validate_sources_parseable(files))
}
pub fn validate_files_full(files: &[PathBuf]) -> AnalysisValidation<Vec<FileContent>> {
let readable = validate_files_readable(files);
match readable {
stillwater::Validation::Success(contents) => validate_sources_parseable(&contents),
stillwater::Validation::Failure(errors) => stillwater::Validation::Failure(errors),
}
}
pub fn validate_files_full_result(files: &[PathBuf]) -> anyhow::Result<Vec<FileContent>> {
run_validation(validate_files_full(files))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use stillwater::Validation;
use tempfile::TempDir;
#[test]
fn test_validate_files_readable_all_exist() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("file1.rs");
let file2 = temp_dir.path().join("file2.rs");
fs::write(&file1, "fn main() {}").unwrap();
fs::write(&file2, "fn test() {}").unwrap();
let files = vec![file1, file2];
let result = validate_files_readable(&files);
assert!(result.is_success());
if let Validation::Success(contents) = result {
assert_eq!(contents.len(), 2);
}
}
#[test]
fn test_validate_files_readable_accumulates_errors() {
let files = vec![
PathBuf::from("/nonexistent/path1.rs"),
PathBuf::from("/nonexistent/path2.rs"),
PathBuf::from("/nonexistent/path3.rs"),
];
let result = validate_files_readable(&files);
match result {
Validation::Failure(errors) => {
assert_eq!(errors.len(), 3, "Expected 3 file read errors");
}
Validation::Success(_) => panic!("Expected failure for nonexistent files"),
}
}
#[test]
fn test_read_files_with_summary_partial_success() {
let temp_dir = TempDir::new().unwrap();
let good_file = temp_dir.path().join("good.rs");
fs::write(&good_file, "fn main() {}").unwrap();
let files = vec![good_file, PathBuf::from("/nonexistent/path.rs")];
let summary = read_files_with_summary(&files);
assert_eq!(summary.successful, 1);
assert_eq!(summary.failed, 1);
assert_eq!(summary.total, 2);
assert!(!summary.all_successful());
assert_eq!(summary.files.len(), 1);
assert_eq!(summary.errors.len(), 1);
}
#[test]
fn test_read_files_with_summary_format() {
let summary = FileReadSummary {
successful: 5,
failed: 2,
total: 7,
errors: vec![],
files: vec![],
};
let message = summary.format_summary();
assert!(message.contains("5"));
assert!(message.contains("7"));
assert!(message.contains("2"));
}
#[test]
fn test_validate_rust_parseable_success() {
let file = FileContent {
path: PathBuf::from("test.rs"),
content: "fn main() { println!(\"Hello\"); }".to_string(),
language: Language::Rust,
};
let result = validate_rust_parseable(&file);
assert!(result.is_success());
}
#[test]
fn test_validate_rust_parseable_failure() {
let file = FileContent {
path: PathBuf::from("test.rs"),
content: "fn main() { incomplete".to_string(),
language: Language::Rust,
};
let result = validate_rust_parseable(&file);
assert!(result.is_failure());
}
#[test]
fn test_validate_sources_parseable_accumulates_errors() {
let files = vec![
FileContent {
path: PathBuf::from("good.rs"),
content: "fn main() {}".to_string(),
language: Language::Rust,
},
FileContent {
path: PathBuf::from("bad1.rs"),
content: "fn main() {".to_string(), language: Language::Rust,
},
FileContent {
path: PathBuf::from("bad2.rs"),
content: "fn incomplete(".to_string(), language: Language::Rust,
},
];
let result = validate_sources_parseable(&files);
match result {
Validation::Failure(errors) => {
assert_eq!(errors.len(), 2, "Expected 2 parse errors");
}
Validation::Success(_) => panic!("Expected failure for invalid Rust"),
}
}
#[test]
fn test_validate_files_full_integration() {
let temp_dir = TempDir::new().unwrap();
let good_file = temp_dir.path().join("good.rs");
fs::write(&good_file, "fn main() {}").unwrap();
let files = vec![good_file];
let result = validate_files_full(&files);
assert!(result.is_success());
}
#[test]
fn test_file_content_language_detection() {
let temp_dir = TempDir::new().unwrap();
let rust_file1 = temp_dir.path().join("test1.rs");
let rust_file2 = temp_dir.path().join("test2.rs");
fs::write(&rust_file1, "fn main() {}").unwrap();
fs::write(&rust_file2, "fn another() { let x = 5; }").unwrap();
let files = vec![rust_file1, rust_file2];
let result = validate_files_readable(&files);
if let Validation::Success(contents) = result {
assert_eq!(contents[0].language, Language::Rust);
assert_eq!(contents[1].language, Language::Rust);
} else {
panic!("Expected success");
}
}
}