use anyhow::Result;
use std::path::Path;
use syn::{visit::Visit, Attribute, Expr, Item, ItemFn, Stmt};
use walkdir::WalkDir;
pub struct AccurateComplexityAnalyzer {
exclude_tests: bool,
respect_annotations: bool,
}
impl Default for AccurateComplexityAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl AccurateComplexityAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
exclude_tests: false,
respect_annotations: false,
}
}
#[must_use]
pub fn exclude_tests(mut self, exclude: bool) -> Self {
self.exclude_tests = exclude;
self
}
#[must_use]
pub fn respect_annotations(mut self, respect: bool) -> Self {
self.respect_annotations = respect;
self
}
pub async fn analyze_file(&self, path: &Path) -> Result<FileComplexityResult> {
let content = tokio::fs::read_to_string(path).await?;
let ast = syn::parse_file(&content)?;
let mut functions = Vec::new();
for item in ast.items {
if let Item::Fn(func) = item {
let metrics = self.analyze_function(&func);
functions.push(metrics);
}
}
Ok(FileComplexityResult {
functions,
file_path: path.display().to_string(),
})
}
pub async fn analyze_project(&self, path: &Path) -> Result<ProjectComplexityResult> {
let mut file_metrics = Vec::new();
let mut files_analyzed = 0;
for entry in WalkDir::new(path)
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
{
let file_path = entry.path();
if self.exclude_tests && self.is_test_file(file_path) {
continue;
}
if let Ok(result) = self.analyze_file(file_path).await {
files_analyzed += 1;
file_metrics.push(result);
}
}
Ok(ProjectComplexityResult {
files_analyzed,
file_metrics,
})
}
fn analyze_function(&self, func: &ItemFn) -> FunctionMetrics {
let name = func.sig.ident.to_string();
let suppressed = self.respect_annotations && self.has_suppress_annotation(&func.attrs);
let mut visitor = ComplexityVisitor::new();
visitor.visit_item_fn(func);
FunctionMetrics {
name,
cyclomatic_complexity: visitor.cyclomatic,
cognitive_complexity: visitor.cognitive,
suppressed,
}
}
fn is_test_file(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
path_str.contains("/tests/")
|| path_str.contains("/test/")
|| path_str.ends_with("_test.rs")
|| path_str.ends_with("_tests.rs")
|| path_str.contains("test_")
|| path_str.contains("tests.rs")
}
fn has_suppress_annotation(&self, attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
if attr.path().is_ident("allow") {
let tokens_str = attr
.meta
.require_list()
.map(|list| list.tokens.to_string())
.unwrap_or_default();
tokens_str.contains("complex_function")
} else {
false
}
})
}
}
struct ComplexityVisitor {
cyclomatic: u32,
cognitive: u32,
nesting_level: u32,
}
impl ComplexityVisitor {
fn new() -> Self {
Self {
cyclomatic: 1, cognitive: 0,
nesting_level: 0,
}
}
fn add_cyclomatic(&mut self, amount: u32) {
self.cyclomatic += amount;
}
fn add_cognitive(&mut self, base: u32) {
self.cognitive += base;
}
}
impl<'ast> Visit<'ast> for ComplexityVisitor {
fn visit_expr(&mut self, expr: &'ast Expr) {
match expr {
Expr::If(_if_expr) => {
self.add_cyclomatic(1);
self.add_cognitive(1);
syn::visit::visit_expr(self, expr);
}
Expr::Match(match_expr) => {
self.add_cyclomatic(1);
self.add_cognitive(1);
for arm in &match_expr.arms {
if arm.guard.is_some() {
self.add_cyclomatic(1);
self.add_cognitive(1);
}
}
syn::visit::visit_expr(self, expr);
}
Expr::While(_) | Expr::ForLoop(_) => {
self.add_cyclomatic(1);
self.add_cognitive(1);
syn::visit::visit_expr(self, expr);
}
Expr::Loop(_) => {
self.add_cyclomatic(1);
self.add_cognitive(1);
syn::visit::visit_expr(self, expr);
}
Expr::Binary(bin) => {
use syn::BinOp;
match bin.op {
BinOp::And(_) | BinOp::Or(_) => {
self.add_cyclomatic(1);
self.add_cognitive(1);
}
_ => {}
}
syn::visit::visit_expr(self, expr);
}
Expr::Try(_) => {
self.add_cyclomatic(1);
self.add_cognitive(1);
syn::visit::visit_expr(self, expr);
}
Expr::Break(_) | Expr::Continue(_) => {
self.add_cognitive(1);
syn::visit::visit_expr(self, expr);
}
Expr::Return(_) => {
if self.nesting_level > 0 {
self.add_cognitive(1);
}
syn::visit::visit_expr(self, expr);
}
Expr::Call(_call) => {
self.add_cognitive(1);
syn::visit::visit_expr(self, expr);
}
_ => syn::visit::visit_expr(self, expr),
}
}
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
syn::visit::visit_stmt(self, stmt);
}
}
#[derive(Debug, Clone)]
pub struct FileComplexityResult {
pub functions: Vec<FunctionMetrics>,
pub file_path: String,
}
#[derive(Debug, Clone)]
pub struct FunctionMetrics {
pub name: String,
pub cyclomatic_complexity: u32,
pub cognitive_complexity: u32,
pub suppressed: bool,
}
#[derive(Debug, Clone)]
pub struct ProjectComplexityResult {
pub files_analyzed: usize,
pub file_metrics: Vec<FileComplexityResult>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_basic_complexity() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(
&test_file,
r#"
fn simple() -> i32 {
42
}
fn with_if(x: i32) -> i32 {
if x > 0 {
x
} else {
-x
}
}
"#,
)
.unwrap();
let analyzer = AccurateComplexityAnalyzer::new();
let result = analyzer.analyze_file(&test_file).await.unwrap();
assert_eq!(result.functions.len(), 2);
assert_eq!(result.functions[0].cyclomatic_complexity, 1);
assert_eq!(result.functions[1].cyclomatic_complexity, 2);
}
}
#[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);
}
}
}