complexity.rs•9.41 kB
//! Complexity analysis and metrics calculation
#[cfg(feature = "napi-bindings")]
use napi_derive::napi;
use crate::types::{SemanticConcept, ComplexityMetrics};
use std::collections::HashMap;
/// Analyzer for calculating code complexity metrics
#[cfg_attr(feature = "napi-bindings", napi)]
pub struct ComplexityAnalyzer;
#[cfg_attr(feature = "napi-bindings", napi)]
impl ComplexityAnalyzer {
#[cfg_attr(feature = "napi-bindings", napi(constructor))]
pub fn new() -> Self {
ComplexityAnalyzer
}
/// Calculate complexity metrics from a set of semantic concepts
pub fn calculate_complexity(concepts: &Vec<SemanticConcept>) -> ComplexityMetrics {
let mut function_count = 0;
let mut class_count = 0;
let mut file_count = HashMap::new();
let mut total_lines = 0;
let mut max_depth = 0;
for concept in concepts {
match concept.concept_type.as_str() {
"function" | "method" | "procedure" => function_count += 1,
"class" | "interface" | "struct" | "enum" => class_count += 1,
_ => {}
}
// Count unique files
file_count.insert(&concept.file_path, true);
// Track line ranges for total lines calculation
let concept_lines = concept.line_range.end - concept.line_range.start + 1;
total_lines += concept_lines;
// Calculate depth from relationships (simplified metric)
let relationship_depth = concept.relationships.len() as u32;
if relationship_depth > max_depth {
max_depth = relationship_depth;
}
}
let file_count = file_count.len() as u32;
let avg_functions_per_file = if file_count > 0 {
function_count as f64 / file_count as f64
} else {
0.0
};
let avg_lines_per_concept = if !concepts.is_empty() {
total_lines as f64 / concepts.len() as f64
} else {
0.0
};
ComplexityMetrics {
cyclomatic_complexity: Self::estimate_cyclomatic_complexity(concepts),
cognitive_complexity: Self::estimate_cognitive_complexity(concepts),
function_count,
class_count,
file_count,
avg_functions_per_file,
avg_lines_per_concept,
max_nesting_depth: max_depth,
}
}
/// Estimate cyclomatic complexity based on concept analysis
fn estimate_cyclomatic_complexity(concepts: &Vec<SemanticConcept>) -> f64 {
let mut total_complexity = 0.0;
let mut function_count = 0;
for concept in concepts {
if concept.concept_type == "function" || concept.concept_type == "method" {
function_count += 1;
// Base complexity of 1 for each function
let mut complexity = 1.0;
// Add complexity based on metadata patterns
if let Some(body) = concept.metadata.get("body") {
complexity += Self::count_decision_points(body);
}
// Factor in confidence - lower confidence might indicate more complex code
complexity *= 2.0 - concept.confidence;
total_complexity += complexity;
}
}
if function_count > 0 {
total_complexity / function_count as f64
} else {
1.0
}
}
/// Estimate cognitive complexity based on nesting and control flow
fn estimate_cognitive_complexity(concepts: &Vec<SemanticConcept>) -> f64 {
let mut total_cognitive = 0.0;
let mut function_count = 0;
for concept in concepts {
if concept.concept_type == "function" || concept.concept_type == "method" {
function_count += 1;
let mut cognitive = 0.0;
// Base cognitive load
cognitive += 1.0;
// Add load based on relationships (dependencies increase cognitive load)
cognitive += concept.relationships.len() as f64 * 0.5;
// Add load based on line span (longer functions are harder to understand)
let line_span = concept.line_range.end - concept.line_range.start;
if line_span > 20 {
cognitive += (line_span as f64 / 20.0) * 0.3;
}
total_cognitive += cognitive;
}
}
if function_count > 0 {
total_cognitive / function_count as f64
} else {
1.0
}
}
/// Count decision points in code (simplified heuristic)
fn count_decision_points(body: &str) -> f64 {
let mut count = 0.0;
// Look for common control flow keywords
for keyword in &["if", "while", "for", "switch", "case", "catch", "&&", "||"] {
count += body.matches(keyword).count() as f64;
}
// Ternary operators
count += body.matches('?').count() as f64;
count
}
}
impl Default for ComplexityAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::LineRange;
use std::collections::HashMap;
fn create_test_concept(name: &str, concept_type: &str, file_path: &str, start: u32, end: u32) -> SemanticConcept {
SemanticConcept {
id: format!("test_{}", name),
name: name.to_string(),
concept_type: concept_type.to_string(),
confidence: 0.8,
file_path: file_path.to_string(),
line_range: LineRange { start, end },
relationships: HashMap::new(),
metadata: HashMap::new(),
}
}
#[test]
fn test_complexity_analyzer_creation() {
let _analyzer = ComplexityAnalyzer::new();
assert!(true); // Constructor should work
}
#[test]
fn test_calculate_basic_complexity() {
let concepts = vec![
create_test_concept("test_function", "function", "test.rs", 1, 10),
create_test_concept("TestClass", "class", "test.rs", 15, 30),
];
let metrics = ComplexityAnalyzer::calculate_complexity(&concepts);
assert_eq!(metrics.function_count, 1);
assert_eq!(metrics.class_count, 1);
assert_eq!(metrics.file_count, 1);
assert!(metrics.cyclomatic_complexity > 0.0);
assert!(metrics.cognitive_complexity > 0.0);
}
#[test]
fn test_empty_concepts() {
let concepts = vec![];
let metrics = ComplexityAnalyzer::calculate_complexity(&concepts);
assert_eq!(metrics.function_count, 0);
assert_eq!(metrics.class_count, 0);
assert_eq!(metrics.file_count, 0);
assert_eq!(metrics.avg_functions_per_file, 0.0);
assert_eq!(metrics.avg_lines_per_concept, 0.0);
}
#[test]
fn test_multiple_files() {
let concepts = vec![
create_test_concept("func1", "function", "file1.rs", 1, 10),
create_test_concept("func2", "function", "file2.rs", 1, 15),
create_test_concept("Class1", "class", "file1.rs", 20, 40),
];
let metrics = ComplexityAnalyzer::calculate_complexity(&concepts);
assert_eq!(metrics.function_count, 2);
assert_eq!(metrics.class_count, 1);
assert_eq!(metrics.file_count, 2);
assert_eq!(metrics.avg_functions_per_file, 1.0);
}
#[test]
fn test_complex_function_with_metadata() {
let mut concept = create_test_concept("complex_func", "function", "test.rs", 1, 50);
concept.metadata.insert("body".to_string(), "if (x > 0) { while (y < 10) { if (z) return; } }".to_string());
let concepts = vec![concept];
let metrics = ComplexityAnalyzer::calculate_complexity(&concepts);
// Should have higher complexity due to control flow
assert!(metrics.cyclomatic_complexity > 1.0);
assert!(metrics.cognitive_complexity > 1.0);
}
#[test]
fn test_count_decision_points() {
let body1 = "if (x > 0) return x;";
assert_eq!(ComplexityAnalyzer::count_decision_points(body1), 1.0);
let body2 = "if (x > 0 && y < 5) { while (z) { for (i = 0; i < 10; i++) {} } }";
assert!(ComplexityAnalyzer::count_decision_points(body2) >= 4.0);
let body3 = "return x > 0 ? x : -x;";
assert!(ComplexityAnalyzer::count_decision_points(body3) >= 1.0);
}
#[test]
fn test_relationships_impact_complexity() {
let mut concept = create_test_concept("connected_func", "function", "test.rs", 1, 20);
concept.relationships.insert("calls".to_string(), "other_func".to_string());
concept.relationships.insert("uses".to_string(), "SomeClass".to_string());
let concepts = vec![concept];
let metrics = ComplexityAnalyzer::calculate_complexity(&concepts);
// Relationships should increase cognitive complexity
assert!(metrics.cognitive_complexity > 1.0);
assert_eq!(metrics.max_nesting_depth, 2);
}
}