codesearch 0.1.15

A fast, intelligent CLI tool with multiple search modes (regex, fuzzy, semantic), code analysis, and dead code detection for popular programming languages
Documentation
//! Code Complexity Metrics
//!
//! Implements various complexity metrics including Cyclomatic, Halstead, Essential, and NPath.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityMetrics {
    pub cyclomatic_complexity: usize,
    pub halstead: HalsteadMetrics,
    pub essential_complexity: usize,
    pub npath_complexity: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(non_snake_case)] // Halstead notation: N1=total operators, N2=total operands
pub struct HalsteadMetrics {
    pub n1: usize,
    pub n2: usize,
    pub N1: usize,
    pub N2: usize,
    pub vocabulary: usize,
    pub length: usize,
    pub volume: f64,
    pub difficulty: f64,
    pub effort: f64,
    pub time: f64,
    pub bugs: f64,
}

impl HalsteadMetrics {
    pub fn calculate(
        operators: &HashMap<String, usize>,
        operands: &HashMap<String, usize>,
    ) -> Self {
        let n1 = operators.len();
        let n2 = operands.len();
        #[allow(non_snake_case)]
        let N1: usize = operators.values().sum();
        #[allow(non_snake_case)]
        let N2: usize = operands.values().sum();

        let vocabulary = n1 + n2;
        let length = N1 + N2;

        let volume = if vocabulary > 0 {
            length as f64 * (vocabulary as f64).log2()
        } else {
            0.0
        };

        let difficulty = if n2 > 0 {
            (n1 as f64 / 2.0) * (N2 as f64 / n2 as f64)
        } else {
            0.0
        };

        let effort = difficulty * volume;
        let time = effort / 18.0;
        let bugs = volume / 3000.0;

        Self {
            n1,
            n2,
            N1,
            N2,
            vocabulary,
            length,
            volume,
            difficulty,
            effort,
            time,
            bugs,
        }
    }
}

pub fn calculate_complexity_metrics(content: &str, ext: &str) -> ComplexityMetrics {
    let cyclomatic = calculate_cyclomatic_complexity(content, ext);
    let halstead = calculate_halstead_metrics(content, ext);
    let essential = calculate_essential_complexity(content, ext);
    let npath = calculate_npath_complexity(content, ext);

    ComplexityMetrics {
        cyclomatic_complexity: cyclomatic,
        halstead,
        essential_complexity: essential,
        npath_complexity: npath,
    }
}

pub fn calculate_cyclomatic_complexity(content: &str, ext: &str) -> usize {
    let decision_points = match ext {
        "rs" | "c" | "cpp" | "java" | "js" | "ts" | "go" | "kt" => {
            vec![
                r"\bif\b",
                r"\bfor\b",
                r"\bwhile\b",
                r"\bcase\b",
                r"\bmatch\b",
                r"\bcatch\b",
                r"\b\?\b",
                r"\b&&\b",
                r"\b\|\|\b",
            ]
        }
        "py" => {
            vec![
                r"\bif\b",
                r"\bfor\b",
                r"\bwhile\b",
                r"\belif\b",
                r"\bexcept\b",
                r"\band\b",
                r"\bor\b",
            ]
        }
        _ => vec![r"\bif\b", r"\bfor\b", r"\bwhile\b"],
    };

    let mut complexity = 1;

    for pattern in decision_points {
        if let Ok(re) = regex::Regex::new(pattern) {
            complexity += re.find_iter(content).count();
        }
    }

    complexity
}

fn calculate_halstead_metrics(content: &str, ext: &str) -> HalsteadMetrics {
    let mut operators: HashMap<String, usize> = HashMap::new();
    let mut operands: HashMap<String, usize> = HashMap::new();

    let operator_patterns = match ext {
        "rs" | "c" | "cpp" | "java" | "js" | "ts" | "go" | "kt" => {
            vec![
                r"\+", r"-", r"\*", r"/", r"%", r"=", r"==", r"!=", r"<", r">", r"<=", r">=",
                r"&&", r"\|\|", r"!", r"&", r"\|", r"\^", r"<<", r">>", r"\+=", r"-=", r"\*=",
                r"/=", r"\?", r":", r"\.", r"->", r"::",
            ]
        }
        "py" => {
            vec![
                r"\+", r"-", r"\*", r"/", r"%", r"=", r"==", r"!=", r"<", r">", r"<=", r">=",
                r"\band\b", r"\bor\b", r"\bnot\b", r"\bin\b", r"\bis\b",
            ]
        }
        _ => vec![r"\+", r"-", r"\*", r"/", r"="],
    };

    for pattern in operator_patterns {
        if let Ok(re) = regex::Regex::new(pattern) {
            let count = re.find_iter(content).count();
            if count > 0 {
                *operators.entry(pattern.to_string()).or_insert(0) += count;
            }
        }
    }

    if let Ok(identifier_re) = regex::Regex::new(r"\b[a-zA-Z_][a-zA-Z0-9_]*\b") {
        for cap in identifier_re.find_iter(content) {
            let operand = cap.as_str().to_string();
            if !is_keyword(&operand, ext) {
                *operands.entry(operand).or_insert(0) += 1;
            }
        }
    }

    if let Ok(number_re) = regex::Regex::new(r"\b\d+\.?\d*\b") {
        for cap in number_re.find_iter(content) {
            *operands.entry(cap.as_str().to_string()).or_insert(0) += 1;
        }
    }

    HalsteadMetrics::calculate(&operators, &operands)
}

fn calculate_essential_complexity(content: &str, _ext: &str) -> usize {
    let mut essential = 1;

    if content.contains("goto") {
        essential += content.matches("goto").count();
    }

    let max_nesting = crate::codemetrics::helpers::calculate_max_nesting(content);
    if max_nesting > 3 {
        essential += max_nesting - 3;
    }

    essential
}

fn calculate_npath_complexity(content: &str, ext: &str) -> u64 {
    let decision_patterns = match ext {
        "rs" | "c" | "cpp" | "java" | "js" | "ts" | "go" | "kt" => {
            vec![
                r"\bif\b",
                r"\bfor\b",
                r"\bwhile\b",
                r"\bcase\b",
                r"\bmatch\b",
            ]
        }
        "py" => {
            vec![r"\bif\b", r"\bfor\b", r"\bwhile\b", r"\belif\b"]
        }
        _ => vec![r"\bif\b", r"\bfor\b", r"\bwhile\b"],
    };

    let mut decision_count = 0;
    for pattern in decision_patterns {
        if let Ok(re) = regex::Regex::new(pattern) {
            decision_count += re.find_iter(content).count();
        }
    }

    2u64.saturating_pow(decision_count.min(30) as u32)
}

fn is_keyword(word: &str, ext: &str) -> bool {
    let keywords = match ext {
        "rs" => vec![
            "fn", "let", "mut", "if", "else", "for", "while", "loop", "match", "return", "struct",
            "enum", "impl", "trait", "pub", "use", "mod",
        ],
        "py" => vec![
            "def", "class", "if", "else", "elif", "for", "while", "return", "import", "from", "as",
            "with", "try", "except",
        ],
        "java" | "kt" => vec![
            "class",
            "interface",
            "public",
            "private",
            "protected",
            "static",
            "void",
            "if",
            "else",
            "for",
            "while",
            "return",
            "import",
        ],
        "js" | "ts" => vec![
            "function", "const", "let", "var", "if", "else", "for", "while", "return", "class",
            "import", "export",
        ],
        _ => vec![],
    };

    keywords.contains(&word)
}