use debtmap::analysis::functional_composition::{analyze_composition, FunctionalAnalysisConfig};
use debtmap::analyzers::rust::RustAnalyzer;
use debtmap::analyzers::Analyzer;
use std::path::PathBuf;
use std::time::Instant;
#[derive(Debug, Default)]
struct AccuracyMetrics {
true_positives: usize,
false_positives: usize,
true_negatives: usize,
false_negatives: usize,
}
impl AccuracyMetrics {
fn precision(&self) -> f64 {
let tp = self.true_positives as f64;
let fp = self.false_positives as f64;
if tp + fp == 0.0 {
return 1.0;
}
tp / (tp + fp)
}
fn recall(&self) -> f64 {
let tp = self.true_positives as f64;
let fn_count = self.false_negatives as f64;
if tp + fn_count == 0.0 {
return 1.0;
}
tp / (tp + fn_count)
}
fn f1_score(&self) -> f64 {
let p = self.precision();
let r = self.recall();
if p + r == 0.0 {
return 0.0;
}
2.0 * (p * r) / (p + r)
}
}
#[test]
fn test_functional_analysis_toggle() {
let code = r#"
fn process_data(items: Vec<i32>) -> Vec<i32> {
items.iter()
.filter(|&x| *x > 0)
.map(|x| x * 2)
.collect()
}
"#;
let analyzer_without = RustAnalyzer::new();
let analyzer_with = RustAnalyzer::new().with_functional_analysis(true);
let path = PathBuf::from("test.rs");
let ast_without = analyzer_without.parse(code, path.clone()).unwrap();
let ast_with = analyzer_with.parse(code, path).unwrap();
let metrics_without = analyzer_without.analyze(&ast_without);
let metrics_with = analyzer_with.analyze(&ast_with);
assert_eq!(
metrics_without.complexity.functions.len(),
metrics_with.complexity.functions.len()
);
let func_with = &metrics_with.complexity.functions[0];
assert!(
func_with.composition_metrics.is_some(),
"Functional analysis should add composition metrics"
);
let func_without = &metrics_without.complexity.functions[0];
assert!(
func_without.composition_metrics.is_none(),
"Without functional analysis, no composition metrics"
);
}
#[test]
fn test_iterator_chain_detection() {
let code = r#"
fn has_iterator_chain(items: Vec<i32>) -> i32 {
items.iter()
.filter(|&x| *x > 0)
.map(|x| x * 2)
.fold(0, |acc, x| acc + x)
}
"#;
let analyzer = RustAnalyzer::new().with_functional_analysis(true);
let path = PathBuf::from("test.rs");
let ast = analyzer.parse(code, path).unwrap();
let metrics = analyzer.analyze(&ast);
let func = &metrics.complexity.functions[0];
let comp = func
.composition_metrics
.as_ref()
.expect("Should have composition metrics");
assert!(
!comp.pipelines.is_empty(),
"Should detect iterator pipeline"
);
assert!(
comp.pipelines[0].depth >= 3,
"Pipeline should have depth of at least 3"
);
assert!(
comp.composition_quality > 0.5,
"Should have good composition quality"
);
}
#[test]
fn test_imperative_not_detected_as_functional() {
let code = r#"
fn imperative_loop(items: Vec<i32>) -> i32 {
let mut sum = 0;
for item in items {
if item > 0 {
sum += item * 2;
}
}
sum
}
"#;
let analyzer = RustAnalyzer::new().with_functional_analysis(true);
let path = PathBuf::from("test.rs");
let ast = analyzer.parse(code, path).unwrap();
let metrics = analyzer.analyze(&ast);
let func = &metrics.complexity.functions[0];
let comp = func
.composition_metrics
.as_ref()
.expect("Should have composition metrics");
if !comp.pipelines.is_empty() {
println!(
"Warning: Detected {} pipelines in imperative code (may need tuning)",
comp.pipelines.len()
);
}
assert!(
comp.composition_quality < 0.7,
"Imperative code should have lower composition quality, got {}",
comp.composition_quality
);
}
#[test]
fn test_purity_detection() {
let pure_code = r#"
fn pure_function(x: i32, y: i32) -> i32 {
x + y
}
"#;
let impure_code = r#"
fn impure_function(x: i32) -> i32 {
println!("Side effect!");
x + 1
}
"#;
let analyzer = RustAnalyzer::new().with_functional_analysis(true);
let path = PathBuf::from("test.rs");
let pure_ast = analyzer.parse(pure_code, path.clone()).unwrap();
let impure_ast = analyzer.parse(impure_code, path).unwrap();
let pure_metrics = analyzer.analyze(&pure_ast);
let impure_metrics = analyzer.analyze(&impure_ast);
let pure_func = &pure_metrics.complexity.functions[0];
let impure_func = &impure_metrics.complexity.functions[0];
let pure_comp = pure_func.composition_metrics.as_ref().unwrap();
let impure_comp = impure_func.composition_metrics.as_ref().unwrap();
println!(
"Purity scores - pure: {:.2}, impure: {:.2}",
pure_comp.purity_score, impure_comp.purity_score
);
assert!(
pure_comp.purity_score >= impure_comp.purity_score,
"Pure function should have higher purity score than impure function"
);
assert!(
pure_comp.purity_score > 0.7,
"Pure function should have reasonably high purity score, got {:.2}",
pure_comp.purity_score
);
}
#[test]
#[ignore]
fn test_performance_overhead_under_10_percent() {
let code = r#"
fn benchmark_function(items: Vec<i32>) -> Vec<i32> {
items.iter()
.filter(|&x| *x > 0)
.map(|x| x * 2)
.collect()
}
"#;
let analyzer_without = RustAnalyzer::new();
let analyzer_with = RustAnalyzer::new().with_functional_analysis(true);
let path = PathBuf::from("test.rs");
let start = Instant::now();
for _ in 0..100 {
let ast = analyzer_without.parse(code, path.clone()).unwrap();
let _ = analyzer_without.analyze(&ast);
}
let without_duration = start.elapsed();
let start = Instant::now();
for _ in 0..100 {
let ast = analyzer_with.parse(code, path.clone()).unwrap();
let _ = analyzer_with.analyze(&ast);
}
let with_duration = start.elapsed();
let overhead_pct = (with_duration.as_secs_f64() - without_duration.as_secs_f64())
/ without_duration.as_secs_f64()
* 100.0;
println!(
"Performance overhead: {:.2}% (without: {:?}, with: {:?})",
overhead_pct, without_duration, with_duration
);
assert!(
overhead_pct < 10.0,
"Performance overhead should be less than 10%, got {:.2}%",
overhead_pct
);
}
#[test]
fn test_accuracy_metrics() {
let analyzer = RustAnalyzer::new().with_functional_analysis(true);
let mut metrics = AccuracyMetrics::default();
let positive_examples = vec![
r#"fn map_filter(items: Vec<i32>) -> Vec<i32> {
items.iter().filter(|&x| *x > 0).map(|x| x * 2).collect()
}"#,
r#"fn fold_example(items: Vec<i32>) -> i32 {
items.iter().fold(0, |acc, x| acc + x)
}"#,
r#"fn chain_example(items: Vec<i32>) -> Vec<i32> {
items.into_iter().filter(|&x| x > 0).map(|x| x * 2).take(10).collect()
}"#,
];
let negative_examples = vec![
r#"fn imperative_loop(items: Vec<i32>) -> i32 {
let mut sum = 0;
for item in items {
sum += item;
}
sum
}"#,
r#"fn mutable_state(items: &mut Vec<i32>) {
for i in 0..items.len() {
items[i] *= 2;
}
}"#,
r#"fn simple_return(x: i32) -> i32 { x + 1 }"#,
];
for code in &positive_examples {
let path = PathBuf::from("test.rs");
let ast = analyzer.parse(code, path).unwrap();
let result = analyzer.analyze(&ast);
if let Some(func) = result.complexity.functions.first() {
if let Some(comp) = &func.composition_metrics {
if !comp.pipelines.is_empty() || comp.composition_quality > 0.5 {
metrics.true_positives += 1;
} else {
metrics.false_negatives += 1;
}
} else {
metrics.false_negatives += 1;
}
}
}
for code in &negative_examples {
let path = PathBuf::from("test.rs");
let ast = analyzer.parse(code, path).unwrap();
let result = analyzer.analyze(&ast);
if let Some(func) = result.complexity.functions.first() {
if let Some(comp) = &func.composition_metrics {
if comp.pipelines.is_empty() && comp.composition_quality < 0.3 {
metrics.true_negatives += 1;
} else {
metrics.false_positives += 1;
}
} else {
metrics.true_negatives += 1;
}
}
}
let precision = metrics.precision();
let recall = metrics.recall();
let f1 = metrics.f1_score();
println!("Accuracy Metrics:");
println!(" Precision: {:.2}%", precision * 100.0);
println!(" Recall: {:.2}%", recall * 100.0);
println!(" F1 Score: {:.4}", f1);
println!(" Details: {:?}", metrics);
println!(
"Note: This is a minimal validation with {} positive and {} negative examples",
positive_examples.len(),
negative_examples.len()
);
assert!(
metrics.true_positives > 0,
"Should detect at least some functional patterns"
);
if precision >= 0.90 && recall >= 0.85 && f1 >= 0.87 {
println!("✓ Meets spec requirements (precision ≥90%, recall ≥85%, F1 ≥0.87)");
} else {
println!(
"⚠ Current accuracy with minimal test set: precision={:.1}%, recall={:.1}%, F1={:.2}",
precision * 100.0,
recall * 100.0,
f1
);
println!(" Full validation requires comprehensive test corpus (spec 111)");
}
}
#[test]
fn test_full_corpus_accuracy() {
use std::fs;
use std::path::Path;
let analyzer = RustAnalyzer::new().with_functional_analysis(true);
let mut metrics = AccuracyMetrics::default();
let positive_dir = Path::new("tests/fixtures/functional_patterns/positive");
if positive_dir.exists() {
for entry in fs::read_dir(positive_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().map(|e| e == "rs").unwrap_or(false) {
if let Ok(code) = fs::read_to_string(&path) {
if let Ok(ast) = analyzer.parse(&code, path.clone()) {
let result = analyzer.analyze(&ast);
if let Some(func) = result.complexity.functions.first() {
if let Some(comp) = &func.composition_metrics {
if !comp.pipelines.is_empty() || comp.composition_quality > 0.5 {
metrics.true_positives += 1;
} else {
metrics.false_negatives += 1;
}
} else {
metrics.false_negatives += 1;
}
}
}
}
}
}
}
let negative_dir = Path::new("tests/fixtures/functional_patterns/negative");
if negative_dir.exists() {
for entry in fs::read_dir(negative_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().map(|e| e == "rs").unwrap_or(false) {
if let Ok(code) = fs::read_to_string(&path) {
if let Ok(ast) = analyzer.parse(&code, path.clone()) {
let result = analyzer.analyze(&ast);
if let Some(func) = result.complexity.functions.first() {
if let Some(comp) = &func.composition_metrics {
if comp.pipelines.is_empty() && comp.composition_quality < 0.3 {
metrics.true_negatives += 1;
} else {
metrics.false_positives += 1;
}
} else {
metrics.true_negatives += 1;
}
}
}
}
}
}
}
let precision = metrics.precision();
let recall = metrics.recall();
let f1 = metrics.f1_score();
println!("\nFull Corpus Accuracy Metrics:");
println!(" Precision: {:.2}%", precision * 100.0);
println!(" Recall: {:.2}%", recall * 100.0);
println!(" F1 Score: {:.4}", f1);
println!(" Details: {:?}", metrics);
println!(
" Total samples: {}",
metrics.true_positives
+ metrics.false_positives
+ metrics.true_negatives
+ metrics.false_negatives
);
println!("\nSpec 111 Requirements:");
println!(
" Precision ≥ 90%: {}",
if precision >= 0.90 {
"✓ PASS"
} else {
"✗ FAIL"
}
);
println!(
" Recall ≥ 85%: {}",
if recall >= 0.85 {
"✓ PASS"
} else {
"✗ FAIL"
}
);
println!(
" F1 Score ≥ 0.87: {}",
if f1 >= 0.87 { "✓ PASS" } else { "✗ FAIL" }
);
assert!(
precision >= 0.90,
"Precision requirement not met: {:.2}% < 90%",
precision * 100.0
);
assert!(
recall >= 0.85,
"Recall requirement not met: {:.2}% < 85%",
recall * 100.0
);
assert!(f1 >= 0.87, "F1 score requirement not met: {:.4} < 0.87", f1);
}
#[test]
fn test_analysis_profiles() {
let code = r#"
fn small_pipeline(items: Vec<i32>) -> Vec<i32> {
items.iter().map(|x| x * 2).collect()
}
"#;
let analyzer = RustAnalyzer::new().with_functional_analysis(true);
let path = PathBuf::from("test.rs");
if let Ok(_ast) = analyzer.parse(code, path) {
if let Ok(item_fn) = syn::parse_str::<syn::ItemFn>(code) {
let strict = FunctionalAnalysisConfig::strict();
let strict_metrics = analyze_composition(&item_fn, &strict);
let balanced = FunctionalAnalysisConfig::balanced();
let balanced_metrics = analyze_composition(&item_fn, &balanced);
let lenient = FunctionalAnalysisConfig::lenient();
let lenient_metrics = analyze_composition(&item_fn, &lenient);
assert!(
lenient_metrics.pipelines.len() >= strict_metrics.pipelines.len(),
"Lenient profile should detect at least as many patterns as strict"
);
assert!(strict_metrics.composition_quality >= 0.0);
assert!(balanced_metrics.composition_quality >= 0.0);
assert!(lenient_metrics.composition_quality >= 0.0);
}
}
}