use crate::analyzers::get_analyzer;
use crate::core::{FunctionMetrics, Language};
use crate::effects::AnalysisEffect;
use crate::env::RealEnv;
use crate::errors::AnalysisError;
use crate::io::effects::{
cache_get_effect, cache_set_effect, read_file_effect, walk_dir_with_config_effect,
};
use crate::risk::effects::load_coverage_optional_effect;
use std::path::PathBuf;
use stillwater::effect::prelude::*;
pub fn analyze_file_effect(path: PathBuf) -> AnalysisEffect<Vec<FunctionMetrics>> {
let path_for_error = path.clone();
let path_for_analysis = path.clone();
read_file_effect(path)
.and_then(move |content| {
from_fn(move |_env: &RealEnv| analyze_content_pure(&content, &path_for_analysis))
.boxed()
})
.map_err(move |e| {
AnalysisError::analysis(format!(
"Failed to analyze '{}': {}",
path_for_error.display(),
e.message()
))
})
.boxed()
}
fn analyze_content_pure(
content: &str,
path: &std::path::Path,
) -> Result<Vec<FunctionMetrics>, AnalysisError> {
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_context(e.to_string(), path.to_path_buf(), 0))?;
let metrics = analyzer.analyze(&ast);
Ok(metrics.complexity.functions)
}
pub fn analyze_file_cached_effect(path: PathBuf) -> AnalysisEffect<Vec<FunctionMetrics>> {
let cache_key = format!("analysis:{}", path.display());
let path_for_analysis = path.clone();
let cache_key_for_set = cache_key.clone();
cache_get_effect::<Vec<FunctionMetrics>>(cache_key)
.and_then(move |cached| match cached {
Some(metrics) => pure(metrics).boxed(),
None => {
analyze_file_effect(path_for_analysis)
.and_then(move |metrics| {
cache_set_effect(cache_key_for_set, metrics.clone())
.map(move |_| metrics)
.boxed()
})
.boxed()
}
})
.boxed()
}
pub fn analyze_files_effect(paths: Vec<PathBuf>) -> AnalysisEffect<Vec<Vec<FunctionMetrics>>> {
if paths.is_empty() {
return pure(Vec::new()).boxed();
}
let mut effects: Vec<AnalysisEffect<Vec<FunctionMetrics>>> =
paths.into_iter().map(analyze_file_effect).collect();
let first = effects.remove(0);
effects
.into_iter()
.fold(first.map(|m| vec![m]).boxed(), |acc, eff| {
acc.and_then(move |mut results| {
eff.map(move |m| {
results.push(m);
results
})
.boxed()
})
.boxed()
})
}
pub fn analyze_files_parallel_effect(
paths: Vec<PathBuf>,
) -> AnalysisEffect<Vec<Vec<FunctionMetrics>>> {
if paths.is_empty() {
return pure(Vec::new()).boxed();
}
let read_effects: Vec<AnalysisEffect<(PathBuf, String)>> = paths
.into_iter()
.map(|p| {
let path = p.clone();
read_file_effect(p)
.map(move |content| (path, content))
.boxed()
})
.collect();
sequence_effects(read_effects)
.and_then(|file_contents| {
from_fn(move |_env: &RealEnv| {
use rayon::prelude::*;
let results: Vec<Result<Vec<FunctionMetrics>, AnalysisError>> = file_contents
.par_iter()
.map(|(path, content)| analyze_content_pure(content, path))
.collect();
results.into_iter().collect()
})
.boxed()
})
.boxed()
}
pub fn analyze_directory_effect(
path: PathBuf,
languages: Vec<Language>,
) -> AnalysisEffect<Vec<Vec<FunctionMetrics>>> {
walk_dir_with_config_effect(path, languages)
.and_then(analyze_files_parallel_effect)
.boxed()
}
#[derive(Debug, Clone)]
pub struct FileAnalysisWithCoverage {
pub path: PathBuf,
pub functions: Vec<FunctionMetrics>,
pub coverage_percent: Option<f64>,
}
pub fn analyze_file_with_coverage_effect(
path: PathBuf,
coverage_path: PathBuf,
) -> AnalysisEffect<FileAnalysisWithCoverage> {
let path_for_result = path.clone();
analyze_file_effect(path)
.and_then(move |functions| {
load_coverage_optional_effect(coverage_path)
.map(move |coverage| {
let coverage_percent =
coverage.get_file_coverage(std::path::Path::new(&path_for_result));
FileAnalysisWithCoverage {
path: path_for_result,
functions,
coverage_percent,
}
})
.boxed()
})
.boxed()
}
fn sequence_effects<T>(effects: Vec<AnalysisEffect<T>>) -> AnalysisEffect<Vec<T>>
where
T: Send + 'static,
{
if effects.is_empty() {
return pure(Vec::new()).boxed();
}
let mut iter = effects.into_iter();
let first = iter.next().unwrap();
iter.fold(first.map(|x| vec![x]).boxed(), |acc, eff| {
acc.and_then(move |mut results| {
eff.map(move |x| {
results.push(x);
results
})
.boxed()
})
.boxed()
})
}
pub fn analyze_file(path: &std::path::Path) -> anyhow::Result<Vec<FunctionMetrics>> {
let config = crate::config::DebtmapConfig::default();
crate::effects::run_effect(analyze_file_effect(path.to_path_buf()), config)
}
pub fn analyze_files(paths: &[PathBuf]) -> anyhow::Result<Vec<Vec<FunctionMetrics>>> {
let config = crate::config::DebtmapConfig::default();
crate::effects::run_effect(analyze_files_parallel_effect(paths.to_vec()), config)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::DebtmapConfig;
use crate::effects::run_effect;
use tempfile::TempDir;
fn create_test_env() -> (TempDir, DebtmapConfig) {
let temp_dir = TempDir::new().unwrap();
(temp_dir, DebtmapConfig::default())
}
fn create_rust_file_content() -> &'static str {
r#"
fn simple_function() {
println!("hello");
}
fn complex_function(x: i32) -> i32 {
if x > 0 {
if x > 10 {
x * 2
} else {
x + 1
}
} else {
0
}
}
"#
}
#[test]
fn test_analyze_file_effect() {
let (temp_dir, config) = create_test_env();
let file_path = temp_dir.path().join("test.rs");
std::fs::write(&file_path, create_rust_file_content()).unwrap();
let effect = analyze_file_effect(file_path);
let result = run_effect(effect, config);
assert!(result.is_ok());
let metrics = result.unwrap();
assert_eq!(metrics.len(), 2);
let complex_func = metrics.iter().find(|m| m.name == "complex_function");
assert!(complex_func.is_some());
let complex_func = complex_func.unwrap();
assert!(complex_func.cyclomatic > 1);
}
#[test]
fn test_analyze_file_effect_not_found() {
let config = DebtmapConfig::default();
let effect = analyze_file_effect("/nonexistent/test.rs".into());
let result = run_effect(effect, config);
assert!(result.is_err());
}
#[test]
fn test_analyze_files_effect() {
let (temp_dir, config) = create_test_env();
let file1 = temp_dir.path().join("a.rs");
let file2 = temp_dir.path().join("b.rs");
std::fs::write(&file1, "fn a() {}").unwrap();
std::fs::write(&file2, "fn b() {}").unwrap();
let effect = analyze_files_effect(vec![file1, file2]);
let result = run_effect(effect, config);
assert!(result.is_ok());
let metrics = result.unwrap();
assert_eq!(metrics.len(), 2);
assert_eq!(metrics[0].len(), 1);
assert_eq!(metrics[1].len(), 1);
}
#[test]
fn test_analyze_files_parallel_effect() {
let (temp_dir, config) = create_test_env();
let files: Vec<PathBuf> = (0..5)
.map(|i| {
let path = temp_dir.path().join(format!("file{}.rs", i));
std::fs::write(&path, format!("fn func{}() {{}}", i)).unwrap();
path
})
.collect();
let effect = analyze_files_parallel_effect(files);
let result = run_effect(effect, config);
assert!(result.is_ok());
let metrics = result.unwrap();
assert_eq!(metrics.len(), 5);
}
#[test]
fn test_analyze_directory_effect() {
let (temp_dir, config) = create_test_env();
let src_dir = temp_dir.path().join("src");
std::fs::create_dir(&src_dir).unwrap();
std::fs::write(src_dir.join("main.rs"), "fn main() {}").unwrap();
std::fs::write(src_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
let effect = analyze_directory_effect(src_dir, vec![Language::Rust]);
let result = run_effect(effect, config);
assert!(result.is_ok());
let metrics = result.unwrap();
assert_eq!(metrics.len(), 2);
}
#[test]
fn test_analyze_file_cached_effect() {
let (temp_dir, config) = create_test_env();
let file_path = temp_dir.path().join("cached.rs");
std::fs::write(&file_path, "fn cached() {}").unwrap();
let effect1 = analyze_file_cached_effect(file_path.clone());
let result1 = run_effect(effect1, config.clone());
assert!(result1.is_ok());
let effect2 = analyze_file_cached_effect(file_path);
let result2 = run_effect(effect2, config);
assert!(result2.is_ok());
assert_eq!(result1.unwrap().len(), result2.unwrap().len());
}
#[test]
fn test_analyze_content_pure() {
let content = "fn test() { if true { println!(\"hi\"); } }";
let path = PathBuf::from("test.rs");
let result = analyze_content_pure(content, &path);
assert!(result.is_ok());
let metrics = result.unwrap();
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].name, "test");
}
#[test]
fn test_backwards_compatible_analyze_file() {
let (temp_dir, _) = create_test_env();
let file_path = temp_dir.path().join("compat.rs");
std::fs::write(&file_path, "fn compat() {}").unwrap();
let result = analyze_file(&file_path);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
fn test_sequence_effects_empty() {
let config = DebtmapConfig::default();
let effect: AnalysisEffect<Vec<i32>> = sequence_effects(vec![]);
let result = run_effect(effect, config);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
}