use crate::analyzers::get_analyzer;
use crate::config::{BatchAnalysisConfig, ParallelConfig};
use crate::core::{DebtItem, FileMetrics, Language};
use crate::effects::{
validation_failure, validation_failures, validation_success, AnalysisEffect, AnalysisValidation,
};
use crate::env::{AnalysisEnv, RealEnv};
use crate::errors::AnalysisError;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use stillwater::effect::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileAnalysisResult {
pub path: PathBuf,
pub metrics: FileMetrics,
pub debt_items: Vec<DebtItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub analysis_time: Option<Duration>,
}
impl FileAnalysisResult {
pub fn new(path: PathBuf, metrics: FileMetrics, debt_items: Vec<DebtItem>) -> Self {
Self {
path,
metrics,
debt_items,
analysis_time: None,
}
}
pub fn with_timing(
path: PathBuf,
metrics: FileMetrics,
debt_items: Vec<DebtItem>,
analysis_time: Duration,
) -> Self {
Self {
path,
metrics,
debt_items,
analysis_time: Some(analysis_time),
}
}
}
#[derive(Debug, Clone)]
pub struct ValidatedFile {
pub path: PathBuf,
pub content: String,
pub language: Language,
}
impl ValidatedFile {
pub fn new(path: PathBuf, content: String, language: Language) -> Self {
Self {
path,
content,
language,
}
}
}
pub fn analyze_files_effect(paths: Vec<PathBuf>) -> AnalysisEffect<Vec<FileAnalysisResult>> {
from_fn(move |env: &RealEnv| {
let config = env.config();
let parallel_config = config
.batch_analysis
.as_ref()
.map(|b| &b.parallelism)
.cloned()
.unwrap_or_default();
let collect_timing = config
.batch_analysis
.as_ref()
.map(|b| b.collect_timing)
.unwrap_or(false);
analyze_files_parallel(&paths, ¶llel_config, collect_timing)
})
.boxed()
}
pub fn analyze_single_file_effect(path: PathBuf) -> AnalysisEffect<FileAnalysisResult> {
let path_display = path.display().to_string();
from_fn(move |env: &RealEnv| {
let content = env.file_system().read_to_string(&path).map_err(|e| {
AnalysisError::io_with_path(format!("Failed to read file: {}", e.message()), &path)
})?;
analyze_file_content(&path, &content, false).map_err(|e| {
AnalysisError::analysis(format!("Analysis failed for '{}': {}", path_display, e))
})
})
.boxed()
}
pub fn analyze_files_with_config_effect(
paths: Vec<PathBuf>,
_config: BatchAnalysisConfig,
) -> AnalysisEffect<Vec<FileAnalysisResult>> {
analyze_files_effect(paths)
}
pub fn validate_files(paths: &[PathBuf]) -> AnalysisValidation<Vec<ValidatedFile>> {
let validations: Vec<AnalysisValidation<ValidatedFile>> =
paths.iter().map(|p| validate_single_file(p)).collect();
combine_validations_preserve_successes(validations)
}
pub fn validate_single_file(path: &Path) -> AnalysisValidation<ValidatedFile> {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
return validation_failure(AnalysisError::io_with_path(
format!("Failed to read file: {}", e),
path,
))
}
};
let language = Language::from_path(path);
if language == Language::Unknown {
return validation_failure(AnalysisError::validation_with_path(
"Unsupported file type",
path,
));
}
if let Err(e) = validate_syntax(&content, language, path) {
return validation_failure(e);
}
validation_success(ValidatedFile::new(path.to_path_buf(), content, language))
}
fn validate_syntax(content: &str, language: Language, path: &Path) -> Result<(), AnalysisError> {
match language {
Language::Rust => {
crate::extraction::UnifiedFileExtractor::extract(path, content).map_err(|e| {
AnalysisError::parse_with_path(format!("Rust syntax error: {}", e), path)
})?;
Ok(())
}
Language::Python => {
if content.contains("def ") || content.contains("class ") || content.trim().is_empty() {
Ok(())
} else {
Ok(())
}
}
Language::JavaScript | Language::TypeScript => {
use crate::analyzers::typescript::parser::{detect_variant, parse_source};
let variant = detect_variant(path);
parse_source(content, path, variant).map_err(|e| {
AnalysisError::parse_with_path(
format!("JavaScript/TypeScript syntax error: {}", e),
path,
)
})?;
Ok(())
}
Language::Unknown => Err(AnalysisError::validation_with_path(
"Cannot validate unknown language",
path,
)),
}
}
pub fn validate_and_analyze_files(
paths: &[PathBuf],
) -> AnalysisValidation<Vec<FileAnalysisResult>> {
let validated = validate_files(paths);
match validated {
stillwater::Validation::Success(files) => {
let results: Vec<Result<FileAnalysisResult, AnalysisError>> = files
.into_iter()
.map(|f| analyze_validated_file(&f))
.collect();
let mut successes = Vec::new();
let mut failures = Vec::new();
for result in results {
match result {
Ok(r) => successes.push(r),
Err(e) => failures.push(e),
}
}
if failures.is_empty() {
validation_success(successes)
} else {
validation_failures(failures)
}
}
stillwater::Validation::Failure(errors) => stillwater::Validation::Failure(errors),
}
}
fn analyze_files_parallel(
paths: &[PathBuf],
config: &ParallelConfig,
collect_timing: bool,
) -> Result<Vec<FileAnalysisResult>, AnalysisError> {
if !config.enabled || paths.len() <= 1 {
paths
.iter()
.map(|path| analyze_file_from_path(path, collect_timing))
.collect()
} else {
let batch_size = config.effective_batch_size();
if paths.len() <= batch_size {
paths
.par_iter()
.map(|path| analyze_file_from_path(path, collect_timing))
.collect()
} else {
paths
.chunks(batch_size)
.flat_map(|chunk| {
chunk
.par_iter()
.map(|path| analyze_file_from_path(path, collect_timing))
.collect::<Vec<_>>()
})
.collect()
}
}
}
fn analyze_file_from_path(
path: &Path,
collect_timing: bool,
) -> Result<FileAnalysisResult, AnalysisError> {
let content = std::fs::read_to_string(path)
.map_err(|e| AnalysisError::io_with_path(format!("Failed to read file: {}", e), path))?;
analyze_file_content(path, &content, collect_timing)
}
fn analyze_file_content(
path: &Path,
content: &str,
collect_timing: bool,
) -> Result<FileAnalysisResult, AnalysisError> {
let start = if collect_timing {
Some(Instant::now())
} else {
None
};
let language = Language::from_path(path);
let analyzer = get_analyzer(language);
let ast = analyzer
.parse(content, path.to_path_buf())
.map_err(|e| AnalysisError::parse_with_path(format!("Parse failed: {}", e), path))?;
let metrics = analyzer.analyze(&ast);
let debt_items = metrics.debt_items.clone();
crate::core::parsing::reset_span_locations();
let analysis_time = start.map(|s| s.elapsed());
Ok(FileAnalysisResult {
path: path.to_path_buf(),
metrics,
debt_items,
analysis_time,
})
}
fn analyze_validated_file(file: &ValidatedFile) -> Result<FileAnalysisResult, AnalysisError> {
analyze_file_content(&file.path, &file.content, false)
}
fn combine_validations_preserve_successes<T>(
validations: Vec<AnalysisValidation<T>>,
) -> AnalysisValidation<Vec<T>> {
let mut successes = Vec::new();
let mut failures: Vec<AnalysisError> = Vec::new();
for v in validations {
match v {
stillwater::Validation::Success(value) => successes.push(value),
stillwater::Validation::Failure(errors) => {
for err in errors {
failures.push(err);
}
}
}
}
if failures.is_empty() {
validation_success(successes)
} else {
validation_failures(failures)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::effects::run_validation;
use std::fs;
use tempfile::TempDir;
fn create_test_dir() -> TempDir {
TempDir::new().unwrap()
}
#[test]
fn test_file_analysis_result_new() {
let path = PathBuf::from("test.rs");
let metrics = FileMetrics {
path: path.clone(),
language: Language::Rust,
complexity: Default::default(),
debt_items: vec![],
dependencies: vec![],
duplications: vec![],
total_lines: 0,
module_scope: None,
classes: None,
};
let result = FileAnalysisResult::new(path.clone(), metrics, vec![]);
assert_eq!(result.path, path);
assert!(result.analysis_time.is_none());
}
#[test]
fn test_file_analysis_result_with_timing() {
let path = PathBuf::from("test.rs");
let metrics = FileMetrics {
path: path.clone(),
language: Language::Rust,
complexity: Default::default(),
debt_items: vec![],
dependencies: vec![],
duplications: vec![],
total_lines: 0,
module_scope: None,
classes: None,
};
let duration = Duration::from_millis(100);
let result = FileAnalysisResult::with_timing(path.clone(), metrics, vec![], duration);
assert_eq!(result.path, path);
assert_eq!(result.analysis_time, Some(duration));
}
#[test]
fn test_validated_file_new() {
let file = ValidatedFile::new(
PathBuf::from("test.rs"),
"fn main() {}".to_string(),
Language::Rust,
);
assert_eq!(file.path, PathBuf::from("test.rs"));
assert_eq!(file.content, "fn main() {}");
assert_eq!(file.language, Language::Rust);
}
#[test]
fn test_validate_single_file_success() {
let temp_dir = create_test_dir();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}").unwrap();
let result = validate_single_file(&file_path);
assert!(result.is_success());
match result {
stillwater::Validation::Success(file) => {
assert_eq!(file.path, file_path);
assert_eq!(file.language, Language::Rust);
}
_ => panic!("Expected success"),
}
}
#[test]
fn test_validate_single_file_not_found() {
let result = validate_single_file(Path::new("/nonexistent/file.rs"));
assert!(result.is_failure());
}
#[test]
fn test_validate_single_file_unknown_language() {
let temp_dir = create_test_dir();
let file_path = temp_dir.path().join("test.unknown");
fs::write(&file_path, "some content").unwrap();
let result = validate_single_file(&file_path);
assert!(result.is_failure());
}
#[test]
fn test_validate_single_file_syntax_error() {
let temp_dir = create_test_dir();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main( { }").unwrap();
let result = validate_single_file(&file_path);
assert!(result.is_failure());
}
#[test]
fn test_validate_files_all_success() {
let temp_dir = create_test_dir();
let file1 = temp_dir.path().join("a.rs");
let file2 = temp_dir.path().join("b.rs");
fs::write(&file1, "fn a() {}").unwrap();
fs::write(&file2, "fn b() {}").unwrap();
let paths = vec![file1, file2];
let result = run_validation(validate_files(&paths));
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 2);
}
#[test]
fn test_validate_files_accumulates_errors() {
let temp_dir = create_test_dir();
let valid_file = temp_dir.path().join("valid.rs");
fs::write(&valid_file, "fn valid() {}").unwrap();
let paths = vec![
valid_file,
PathBuf::from("/nonexistent/a.rs"),
PathBuf::from("/nonexistent/b.rs"),
];
let result = validate_files(&paths);
match result {
stillwater::Validation::Failure(errors) => {
let errors_vec: Vec<_> = errors.into_iter().collect();
assert_eq!(errors_vec.len(), 2);
}
_ => panic!("Expected failure with accumulated errors"),
}
}
#[test]
fn test_analyze_file_from_path() {
let temp_dir = create_test_dir();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() { let x = 1; }").unwrap();
let result = analyze_file_from_path(&file_path, false);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.path, file_path);
assert!(analysis.analysis_time.is_none());
}
#[test]
fn test_analyze_file_from_path_with_timing() {
let temp_dir = create_test_dir();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}").unwrap();
let result = analyze_file_from_path(&file_path, true);
assert!(result.is_ok());
let analysis = result.unwrap();
assert!(analysis.analysis_time.is_some());
}
#[test]
fn test_analyze_files_parallel_sequential() {
let temp_dir = create_test_dir();
let file1 = temp_dir.path().join("a.rs");
let file2 = temp_dir.path().join("b.rs");
fs::write(&file1, "fn a() {}").unwrap();
fs::write(&file2, "fn b() {}").unwrap();
let config = ParallelConfig::sequential();
let result = analyze_files_parallel(&[file1, file2], &config, false);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 2);
}
#[test]
fn test_analyze_files_parallel_enabled() {
let temp_dir = create_test_dir();
let file1 = temp_dir.path().join("a.rs");
let file2 = temp_dir.path().join("b.rs");
let file3 = temp_dir.path().join("c.rs");
fs::write(&file1, "fn a() {}").unwrap();
fs::write(&file2, "fn b() {}").unwrap();
fs::write(&file3, "fn c() {}").unwrap();
let config = ParallelConfig::default();
let result = analyze_files_parallel(&[file1, file2, file3], &config, false);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 3);
}
#[test]
fn test_validate_and_analyze_files_success() {
let temp_dir = create_test_dir();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}").unwrap();
let result = run_validation(validate_and_analyze_files(std::slice::from_ref(&file_path)));
assert!(result.is_ok());
let results = result.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].path, file_path);
}
#[test]
fn test_validate_and_analyze_files_validation_failure() {
let paths = vec![PathBuf::from("/nonexistent/file.rs")];
let result = run_validation(validate_and_analyze_files(&paths));
assert!(result.is_err());
}
#[test]
fn test_validate_syntax_rust_valid() {
let content = "fn main() {}";
let result = validate_syntax(content, Language::Rust, Path::new("test.rs"));
assert!(result.is_ok());
}
#[test]
fn test_validate_syntax_rust_invalid() {
let content = "fn main( { }"; let result = validate_syntax(content, Language::Rust, Path::new("test.rs"));
assert!(result.is_err());
}
#[test]
fn test_validate_syntax_python() {
let content = "def hello():\n pass";
let result = validate_syntax(content, Language::Python, Path::new("test.py"));
assert!(result.is_ok());
}
#[test]
fn test_combine_validations_preserve_successes() {
let validations: Vec<AnalysisValidation<i32>> = vec![
validation_success(1),
validation_success(2),
validation_success(3),
];
let result = combine_validations_preserve_successes(validations);
match result {
stillwater::Validation::Success(values) => {
assert_eq!(values, vec![1, 2, 3]);
}
_ => panic!("Expected success"),
}
}
#[test]
fn test_combine_validations_with_failures() {
let validations: Vec<AnalysisValidation<i32>> = vec![
validation_success(1),
validation_failure(AnalysisError::validation("Error 1")),
validation_success(3),
validation_failure(AnalysisError::validation("Error 2")),
];
let result = combine_validations_preserve_successes(validations);
match result {
stillwater::Validation::Failure(errors) => {
let errors_vec: Vec<_> = errors.into_iter().collect();
assert_eq!(errors_vec.len(), 2);
}
_ => panic!("Expected failure"),
}
}
}