1use std::path::{Path, PathBuf};
29
30use crate::core::Language;
31use crate::effects::{
32 combine_validations, run_validation, validation_failure, validation_success, AnalysisValidation,
33};
34use crate::errors::AnalysisError;
35
36#[derive(Debug, Clone)]
38pub struct FileContent {
39 pub path: PathBuf,
41 pub content: String,
43 pub language: Language,
45}
46
47pub fn validate_files_readable(files: &[PathBuf]) -> AnalysisValidation<Vec<FileContent>> {
66 let validations: Vec<AnalysisValidation<FileContent>> = files
67 .iter()
68 .map(|path| validate_single_file_readable(path))
69 .collect();
70
71 combine_validations(validations)
72}
73
74fn validate_single_file_readable(path: &Path) -> AnalysisValidation<FileContent> {
76 if !path.exists() {
78 return validation_failure(AnalysisError::io_with_path(
79 format!("File not found: {}", path.display()),
80 path,
81 ));
82 }
83
84 if !path.is_file() {
86 return validation_failure(AnalysisError::io_with_path(
87 format!("Path is not a file: {}", path.display()),
88 path,
89 ));
90 }
91
92 match std::fs::read_to_string(path) {
94 Ok(content) => {
95 let language = Language::from_path(path);
96 validation_success(FileContent {
97 path: path.to_path_buf(),
98 content,
99 language,
100 })
101 }
102 Err(e) => validation_failure(AnalysisError::io_with_path(
103 format!("Cannot read file: {}", e),
104 path,
105 )),
106 }
107}
108
109pub fn validate_files_readable_result(files: &[PathBuf]) -> anyhow::Result<Vec<FileContent>> {
111 run_validation(validate_files_readable(files))
112}
113
114#[derive(Debug, Clone)]
116pub struct FileReadSummary {
117 pub successful: usize,
119 pub failed: usize,
121 pub total: usize,
123 pub errors: Vec<AnalysisError>,
125 pub files: Vec<FileContent>,
127}
128
129impl FileReadSummary {
130 pub fn all_successful(&self) -> bool {
132 self.failed == 0
133 }
134
135 pub fn format_summary(&self) -> String {
137 if self.all_successful() {
138 format!("Successfully read {} files", self.successful)
139 } else {
140 format!(
141 "Read {} of {} files ({} failed)",
142 self.successful, self.total, self.failed
143 )
144 }
145 }
146}
147
148pub fn read_files_with_summary(files: &[PathBuf]) -> FileReadSummary {
160 let mut successful_files = Vec::new();
161 let mut errors = Vec::new();
162
163 for path in files {
164 match validate_single_file_readable(path) {
165 stillwater::Validation::Success(file_content) => {
166 successful_files.push(file_content);
167 }
168 stillwater::Validation::Failure(errs) => {
169 for err in errs {
170 errors.push(err);
171 }
172 }
173 }
174 }
175
176 let successful = successful_files.len();
177 let failed = errors.len();
178 let total = files.len();
179
180 FileReadSummary {
181 successful,
182 failed,
183 total,
184 errors,
185 files: successful_files,
186 }
187}
188
189pub fn validate_sources_parseable(files: &[FileContent]) -> AnalysisValidation<Vec<FileContent>> {
199 let validations: Vec<AnalysisValidation<FileContent>> =
200 files.iter().map(validate_single_source_parseable).collect();
201
202 combine_validations(validations)
203}
204
205fn validate_single_source_parseable(file: &FileContent) -> AnalysisValidation<FileContent> {
207 match file.language {
208 Language::Rust => validate_rust_parseable(file),
209 Language::Python => validate_python_parseable(file),
210 Language::Unknown => {
211 validation_success(file.clone())
213 }
214 }
215}
216
217fn validate_rust_parseable(file: &FileContent) -> AnalysisValidation<FileContent> {
219 match syn::parse_file(&file.content) {
221 Ok(_) => validation_success(file.clone()),
222 Err(e) => {
223 let line = e.span().start().line;
224 validation_failure(AnalysisError::parse_with_context(
225 format!("Rust parse error: {}", e),
226 &file.path,
227 line,
228 ))
229 }
230 }
231}
232
233fn validate_python_parseable(file: &FileContent) -> AnalysisValidation<FileContent> {
235 for (line_num, line) in file.content.lines().enumerate() {
239 let trimmed = line.trim();
241 if trimmed.is_empty() || trimmed.starts_with('#') {
242 continue;
243 }
244
245 if line.starts_with(' ') && line.contains('\t') {
247 return validation_failure(AnalysisError::parse_with_context(
248 "Mixed tabs and spaces in indentation".to_string(),
249 &file.path,
250 line_num + 1,
251 ));
252 }
253 }
254
255 validation_success(file.clone())
258}
259
260pub fn validate_sources_parseable_result(
262 files: &[FileContent],
263) -> anyhow::Result<Vec<FileContent>> {
264 run_validation(validate_sources_parseable(files))
265}
266
267pub fn validate_files_full(files: &[PathBuf]) -> AnalysisValidation<Vec<FileContent>> {
272 let readable = validate_files_readable(files);
274
275 match readable {
277 stillwater::Validation::Success(contents) => validate_sources_parseable(&contents),
278 stillwater::Validation::Failure(errors) => stillwater::Validation::Failure(errors),
279 }
280}
281
282pub fn validate_files_full_result(files: &[PathBuf]) -> anyhow::Result<Vec<FileContent>> {
284 run_validation(validate_files_full(files))
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use std::fs;
291 use stillwater::Validation;
292 use tempfile::TempDir;
293
294 #[test]
295 fn test_validate_files_readable_all_exist() {
296 let temp_dir = TempDir::new().unwrap();
297 let file1 = temp_dir.path().join("file1.rs");
298 let file2 = temp_dir.path().join("file2.rs");
299
300 fs::write(&file1, "fn main() {}").unwrap();
301 fs::write(&file2, "fn test() {}").unwrap();
302
303 let files = vec![file1, file2];
304 let result = validate_files_readable(&files);
305
306 assert!(result.is_success());
307 if let Validation::Success(contents) = result {
308 assert_eq!(contents.len(), 2);
309 }
310 }
311
312 #[test]
313 fn test_validate_files_readable_accumulates_errors() {
314 let files = vec![
315 PathBuf::from("/nonexistent/path1.rs"),
316 PathBuf::from("/nonexistent/path2.rs"),
317 PathBuf::from("/nonexistent/path3.rs"),
318 ];
319
320 let result = validate_files_readable(&files);
321
322 match result {
323 Validation::Failure(errors) => {
324 assert_eq!(errors.len(), 3, "Expected 3 file read errors");
325 }
326 Validation::Success(_) => panic!("Expected failure for nonexistent files"),
327 }
328 }
329
330 #[test]
331 fn test_read_files_with_summary_partial_success() {
332 let temp_dir = TempDir::new().unwrap();
333 let good_file = temp_dir.path().join("good.rs");
334 fs::write(&good_file, "fn main() {}").unwrap();
335
336 let files = vec![good_file, PathBuf::from("/nonexistent/path.rs")];
337
338 let summary = read_files_with_summary(&files);
339
340 assert_eq!(summary.successful, 1);
341 assert_eq!(summary.failed, 1);
342 assert_eq!(summary.total, 2);
343 assert!(!summary.all_successful());
344 assert_eq!(summary.files.len(), 1);
345 assert_eq!(summary.errors.len(), 1);
346 }
347
348 #[test]
349 fn test_read_files_with_summary_format() {
350 let summary = FileReadSummary {
351 successful: 5,
352 failed: 2,
353 total: 7,
354 errors: vec![],
355 files: vec![],
356 };
357
358 let message = summary.format_summary();
359 assert!(message.contains("5"));
360 assert!(message.contains("7"));
361 assert!(message.contains("2"));
362 }
363
364 #[test]
365 fn test_validate_rust_parseable_success() {
366 let file = FileContent {
367 path: PathBuf::from("test.rs"),
368 content: "fn main() { println!(\"Hello\"); }".to_string(),
369 language: Language::Rust,
370 };
371
372 let result = validate_rust_parseable(&file);
373 assert!(result.is_success());
374 }
375
376 #[test]
377 fn test_validate_rust_parseable_failure() {
378 let file = FileContent {
379 path: PathBuf::from("test.rs"),
380 content: "fn main() { incomplete".to_string(),
381 language: Language::Rust,
382 };
383
384 let result = validate_rust_parseable(&file);
385 assert!(result.is_failure());
386 }
387
388 #[test]
389 fn test_validate_sources_parseable_accumulates_errors() {
390 let files = vec![
391 FileContent {
392 path: PathBuf::from("good.rs"),
393 content: "fn main() {}".to_string(),
394 language: Language::Rust,
395 },
396 FileContent {
397 path: PathBuf::from("bad1.rs"),
398 content: "fn main() {".to_string(), language: Language::Rust,
400 },
401 FileContent {
402 path: PathBuf::from("bad2.rs"),
403 content: "fn incomplete(".to_string(), language: Language::Rust,
405 },
406 ];
407
408 let result = validate_sources_parseable(&files);
409
410 match result {
411 Validation::Failure(errors) => {
412 assert_eq!(errors.len(), 2, "Expected 2 parse errors");
413 }
414 Validation::Success(_) => panic!("Expected failure for invalid Rust"),
415 }
416 }
417
418 #[test]
419 fn test_validate_files_full_integration() {
420 let temp_dir = TempDir::new().unwrap();
421 let good_file = temp_dir.path().join("good.rs");
422 fs::write(&good_file, "fn main() {}").unwrap();
423
424 let files = vec![good_file];
425 let result = validate_files_full(&files);
426
427 assert!(result.is_success());
428 }
429
430 #[test]
431 fn test_file_content_language_detection() {
432 let temp_dir = TempDir::new().unwrap();
433
434 let rust_file1 = temp_dir.path().join("test1.rs");
436 let rust_file2 = temp_dir.path().join("test2.rs");
437
438 fs::write(&rust_file1, "fn main() {}").unwrap();
439 fs::write(&rust_file2, "fn another() { let x = 5; }").unwrap();
440
441 let files = vec![rust_file1, rust_file2];
442 let result = validate_files_readable(&files);
443
444 if let Validation::Success(contents) = result {
445 assert_eq!(contents[0].language, Language::Rust);
446 assert_eq!(contents[1].language, Language::Rust);
447 } else {
448 panic!("Expected success");
449 }
450 }
451}