#![cfg_attr(coverage_nightly, coverage(off))]
use super::{Analyzer, AnalyzerInfo, ProjectAnalyzer, ProjectConfig, ProjectInput};
use crate::services::ast_rust::analyze_rust_file_with_complexity;
use crate::services::complexity::ComplexityMetrics as ComplexityService;
use crate::services::verified_complexity::VerifiedComplexityAnalyzer as OriginalAnalyzer;
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
pub struct ComplexityAnalyzer {
#[allow(dead_code)]
inner: OriginalAnalyzer,
}
impl ComplexityAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
inner: OriginalAnalyzer::new(),
}
}
#[allow(dead_code)]
const DEFAULT_THRESHOLD: u32 = 10;
}
impl Default for ComplexityAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityConfig {
pub base: ProjectConfig,
pub max_cyclomatic: u32,
pub max_cognitive: u32,
pub include_halstead: bool,
}
impl Default for ComplexityConfig {
fn default() -> Self {
Self {
base: ProjectConfig::default(),
max_cyclomatic: 10,
max_cognitive: 15,
include_halstead: true,
}
}
}
#[derive(Debug, Clone)]
struct FileMetric {
path: PathBuf,
#[allow(dead_code)]
functions: usize,
#[allow(dead_code)]
average_complexity: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityOutput {
pub project_path: std::path::PathBuf,
pub file_metrics: Vec<FileComplexityReport>,
pub summary: ComplexitySummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileComplexityReport {
pub file_path: String,
pub functions: Vec<FunctionComplexityReport>,
pub file_total: ComplexityService,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionComplexityReport {
pub name: String,
pub line_start: u32,
pub line_end: u32,
pub metrics: ComplexityService,
pub violations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexitySummary {
pub total_functions: usize,
pub high_complexity_functions: usize,
pub average_cyclomatic: f64,
pub average_cognitive: f64,
pub max_cyclomatic: u32,
pub max_cognitive: u32,
}
async fn find_source_files(root: &Path, extensions: &[String]) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
let root = root.to_path_buf();
let extensions = extensions.to_vec();
for entry in WalkDir::new(&root)
.follow_links(true)
.into_iter()
.filter_map(std::result::Result::ok)
{
let path = entry.path();
if !path.is_file() {
continue;
}
let path_str = path.to_string_lossy();
if path_str.contains("/target/")
|| path_str.contains("/node_modules/")
|| path_str.contains("/.git/")
|| path_str.contains("/vendor/")
{
continue;
}
if let Some(ext) = path.extension() {
if extensions
.iter()
.any(|e| e == ext.to_string_lossy().as_ref())
{
files.push(path.to_path_buf());
}
}
}
Ok(files)
}
fn should_analyze_file(file_path: &Path) -> bool {
let path_str = file_path.to_string_lossy();
!path_str.contains("/tests/")
&& !path_str.contains("/test/")
&& !path_str.ends_with("_test.rs")
&& !path_str.ends_with("_tests.rs")
}
fn process_function_metrics(
func: &crate::services::complexity::FunctionComplexity,
high_complexity_functions: &mut usize,
total_cyclomatic: &mut u32,
total_cognitive: &mut u32,
max_cyclomatic: &mut u32,
max_cognitive: &mut u32,
) {
let cyclo = u32::from(func.metrics.cyclomatic);
let cogn = u32::from(func.metrics.cognitive);
if cyclo > 20 || cogn > 15 {
*high_complexity_functions += 1;
}
*total_cyclomatic += cyclo;
*total_cognitive += cogn;
*max_cyclomatic = (*max_cyclomatic).max(cyclo);
*max_cognitive = (*max_cognitive).max(cogn);
}
fn calculate_averages(total: u32, count: usize) -> f64 {
if count > 0 {
f64::from(total) / count as f64
} else {
0.0
}
}
#[async_trait]
impl Analyzer for ComplexityAnalyzer {
type Input = ProjectInput;
type Output = ComplexityOutput;
type Config = ProjectConfig;
async fn analyze(&self, input: Self::Input, _config: Self::Config) -> Result<Self::Output> {
let source_files = find_source_files(&input.project_path, &["rs".to_string()]).await?;
let mut file_metrics = Vec::new();
let mut total_functions = 0;
let mut high_complexity_functions = 0;
let mut total_cyclomatic = 0u32;
let mut total_cognitive = 0u32;
let mut max_cyclomatic = 0u32;
let mut max_cognitive = 0u32;
let mut function_count = 0;
for file_path in source_files {
if !should_analyze_file(&file_path) {
continue;
}
if let Ok(metrics) = analyze_rust_file_with_complexity(&file_path).await {
total_functions += metrics.functions.len();
function_count += metrics.functions.len();
for func in &metrics.functions {
process_function_metrics(
func,
&mut high_complexity_functions,
&mut total_cyclomatic,
&mut total_cognitive,
&mut max_cyclomatic,
&mut max_cognitive,
);
}
let avg_complexity = if metrics.functions.is_empty() {
0.0
} else {
metrics
.functions
.iter()
.map(|f| f64::from(f.metrics.cyclomatic))
.sum::<f64>()
/ metrics.functions.len() as f64
};
file_metrics.push(FileMetric {
path: file_path.clone(),
functions: metrics.functions.len(),
average_complexity: avg_complexity,
});
}
}
let avg_cyclomatic = calculate_averages(total_cyclomatic, function_count);
let avg_cognitive = calculate_averages(total_cognitive, function_count);
let file_complexity_reports = file_metrics
.into_iter()
.map(|fm| {
FileComplexityReport {
file_path: fm.path.to_string_lossy().to_string(),
functions: Vec::new(), file_total: ComplexityService::default(),
}
})
.collect();
Ok(ComplexityOutput {
project_path: input.project_path.clone(),
file_metrics: file_complexity_reports,
summary: ComplexitySummary {
total_functions,
high_complexity_functions,
average_cyclomatic: avg_cyclomatic,
average_cognitive: avg_cognitive,
max_cyclomatic,
max_cognitive,
},
})
}
fn name(&self) -> &'static str {
"complexity"
}
}
#[async_trait]
impl ProjectAnalyzer for ComplexityAnalyzer {
async fn analyze_project(&self, project_path: &Path) -> Result<Self::Output> {
let input = ProjectInput {
project_path: project_path.to_path_buf(),
};
let config = ProjectConfig::default();
self.analyze(input, config).await
}
}
impl AnalyzerInfo for ComplexityAnalyzer {
fn name(&self) -> &'static str {
"complexity"
}
fn version(&self) -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn description(&self) -> &'static str {
"Analyzes code complexity using cyclomatic, cognitive, and Halstead metrics"
}
}
pub struct ComplexityAnalyzerFactory;
impl ComplexityAnalyzerFactory {
#[must_use]
pub fn create() -> ComplexityAnalyzer {
ComplexityAnalyzer::new()
}
#[must_use]
pub fn create_with_thresholds(_max_cyclomatic: u32, _max_cognitive: u32) -> ComplexityAnalyzer {
ComplexityAnalyzer::new()
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_complexity_analyzer_creation() {
let analyzer = ComplexityAnalyzer::new();
assert_eq!(Analyzer::name(&analyzer), "complexity");
assert_eq!(Analyzer::version(&analyzer), env!("CARGO_PKG_VERSION"));
}
#[tokio::test]
async fn test_complexity_config_default() {
let config = ComplexityConfig::default();
assert_eq!(config.max_cyclomatic, 10);
assert_eq!(config.max_cognitive, 15);
assert!(config.include_halstead);
}
#[tokio::test]
async fn test_analyzer_info() {
let analyzer = ComplexityAnalyzer::new();
assert_eq!(Analyzer::name(&analyzer), "complexity");
assert!(AnalyzerInfo::description(&analyzer).contains("complexity"));
}
#[tokio::test]
async fn test_factory_creation() {
let analyzer = ComplexityAnalyzerFactory::create();
assert_eq!(Analyzer::name(&analyzer), "complexity");
let analyzer_with_thresholds = ComplexityAnalyzerFactory::create_with_thresholds(15, 20);
assert_eq!(Analyzer::name(&analyzer_with_thresholds), "complexity");
}
#[tokio::test]
async fn test_project_analysis() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(
&test_file,
r#"
fn simple_function() -> i32 { 42 }
fn complex_function(x: i32) -> i32 {
if x > 0 {
if x > 10 {
return x * 2;
} else {
return x + 1;
}
} else {
return 0;
}
}
"#,
)
.unwrap();
let analyzer = ComplexityAnalyzer::new();
let result = analyzer.analyze_project(temp_dir.path()).await.unwrap();
assert_eq!(result.project_path, temp_dir.path());
assert!(!result.file_metrics.is_empty());
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}