Skip to main content

infiniloom_engine/analysis/
complexity.rs

1//! Code complexity metrics calculation for all supported languages
2//!
3//! Computes cyclomatic complexity, cognitive complexity, Halstead metrics,
4//! and maintainability index for functions/methods.
5
6use crate::analysis::types::{ComplexityMetrics, HalsteadMetrics, LocMetrics};
7use crate::parser::Language;
8use std::collections::HashSet;
9use tree_sitter::Node;
10
11/// Calculates complexity metrics from AST nodes
12pub struct ComplexityCalculator {
13    /// Source code being analyzed
14    source: String,
15}
16
17impl ComplexityCalculator {
18    /// Create a new calculator with the given source code
19    pub fn new(source: impl Into<String>) -> Self {
20        Self { source: source.into() }
21    }
22
23    /// Get text for a node
24    fn node_text(&self, node: &Node<'_>) -> &str {
25        node.utf8_text(self.source.as_bytes()).unwrap_or("")
26    }
27
28    /// Calculate all complexity metrics for a function node
29    pub fn calculate(&self, node: &Node<'_>, language: Language) -> ComplexityMetrics {
30        let cyclomatic = self.cyclomatic_complexity(node, language);
31        let cognitive = self.cognitive_complexity(node, language);
32        let halstead = self.halstead_metrics(node, language);
33        let loc = self.loc_metrics(node);
34        let max_nesting_depth = self.max_nesting_depth(node, language);
35        let parameter_count = self.parameter_count(node, language);
36        let return_count = self.return_count(node, language);
37
38        // Calculate maintainability index (MI)
39        // Formula: MI = 171 - 5.2 * ln(V) - 0.23 * CC - 16.2 * ln(LOC)
40        // Where V = Halstead Volume, CC = Cyclomatic Complexity, LOC = Lines of Code
41        let maintainability_index = halstead.as_ref().map(|h| {
42            let v = h.volume.max(1.0);
43            let cc = cyclomatic as f32;
44            let loc = loc.source.max(1) as f32;
45
46            let mi = 171.0 - 5.2 * v.ln() - 0.23 * cc - 16.2 * loc.ln();
47            // Normalize to 0-100 scale
48            (mi.max(0.0) * 100.0 / 171.0).min(100.0)
49        });
50
51        ComplexityMetrics {
52            cyclomatic,
53            cognitive,
54            halstead,
55            loc,
56            maintainability_index,
57            max_nesting_depth,
58            parameter_count,
59            return_count,
60        }
61    }
62
63    /// Calculate cyclomatic complexity (McCabe's complexity)
64    ///
65    /// CC = E - N + 2P (for a single function, P=1)
66    /// Simplified: CC = 1 + number of decision points
67    ///
68    /// Decision points: if, else if, while, for, case, catch, &&, ||, ?:
69    pub fn cyclomatic_complexity(&self, node: &Node<'_>, language: Language) -> u32 {
70        let mut complexity = 1; // Base complexity
71
72        self.walk_tree(node, &mut |child| {
73            if self.is_decision_point(child, language) {
74                complexity += 1;
75            }
76        });
77
78        complexity
79    }
80
81    /// Check if a node is a decision point (contributes to cyclomatic complexity)
82    fn is_decision_point(&self, node: &Node<'_>, language: Language) -> bool {
83        let kind = node.kind();
84
85        // Language-agnostic decision points
86        let common_decisions = [
87            "if_statement",
88            "if_expression",
89            "if",
90            "else_if",
91            "elif",
92            "elsif",
93            "while_statement",
94            "while_expression",
95            "while",
96            "for_statement",
97            "for_expression",
98            "for",
99            "for_in_statement",
100            "foreach",
101            "case",
102            "when",
103            "match_arm",
104            "catch_clause",
105            "except_clause",
106            "rescue",
107            "conditional_expression", // ternary
108            "ternary_expression",
109            "binary_expression",
110            "logical_and",
111            "logical_or",
112        ];
113
114        if common_decisions.contains(&kind) {
115            return true;
116        }
117
118        // Check for && and || operators in binary expressions
119        if kind == "binary_expression" || kind == "binary_operator" {
120            let text = self.node_text(node);
121            if text.contains("&&")
122                || text.contains("||")
123                || text.contains(" and ")
124                || text.contains(" or ")
125            {
126                return true;
127            }
128        }
129
130        // Language-specific decision points
131        match language {
132            Language::Rust => {
133                matches!(kind, "match_expression" | "if_let_expression" | "while_let_expression")
134            },
135            Language::Go => matches!(kind, "select_statement" | "type_switch_statement"),
136            Language::Swift => matches!(kind, "guard_statement" | "switch_statement"),
137            Language::Kotlin => matches!(kind, "when_expression"),
138            Language::Haskell => matches!(kind, "case_expression" | "guard"),
139            Language::Elixir => matches!(kind, "case" | "cond" | "with"),
140            Language::Clojure => matches!(kind, "cond" | "case"),
141            Language::OCaml => matches!(kind, "match_expression"),
142            _ => false,
143        }
144    }
145
146    /// Calculate cognitive complexity
147    ///
148    /// Cognitive complexity measures how hard code is to understand.
149    /// It penalizes nesting, breaks in linear flow, and complex control structures.
150    pub fn cognitive_complexity(&self, node: &Node<'_>, language: Language) -> u32 {
151        let mut complexity = 0;
152        self.cognitive_walk(node, language, 0, &mut complexity);
153        complexity
154    }
155
156    fn cognitive_walk(
157        &self,
158        node: &Node<'_>,
159        language: Language,
160        nesting: u32,
161        complexity: &mut u32,
162    ) {
163        let kind = node.kind();
164
165        // Increment for control flow structures
166        let is_control_flow = self.is_control_flow(kind, language);
167        if is_control_flow {
168            // Base increment
169            *complexity += 1;
170            // Nesting increment
171            *complexity += nesting;
172        }
173
174        // Increment for breaks in linear flow
175        if self.is_flow_break(kind, language) {
176            *complexity += 1;
177        }
178
179        // Recursion penalty
180        if self.is_recursion(node, language) {
181            *complexity += 1;
182        }
183
184        // Walk children with updated nesting
185        let new_nesting = if is_control_flow || self.is_nesting_structure(kind, language) {
186            nesting + 1
187        } else {
188            nesting
189        };
190
191        let mut cursor = node.walk();
192        for child in node.children(&mut cursor) {
193            self.cognitive_walk(&child, language, new_nesting, complexity);
194        }
195    }
196
197    fn is_control_flow(&self, kind: &str, language: Language) -> bool {
198        let common_control = [
199            "if_statement",
200            "if_expression",
201            "while_statement",
202            "while_expression",
203            "for_statement",
204            "for_expression",
205            "for_in_statement",
206            "switch_statement",
207            "match_expression",
208            "try_statement",
209        ];
210
211        if common_control.contains(&kind) {
212            return true;
213        }
214
215        match language {
216            Language::Rust => matches!(kind, "if_let_expression" | "while_let_expression"),
217            Language::Go => matches!(kind, "select_statement"),
218            Language::Swift => matches!(kind, "guard_statement"),
219            _ => false,
220        }
221    }
222
223    fn is_flow_break(&self, kind: &str, _language: Language) -> bool {
224        matches!(
225            kind,
226            "break_statement"
227                | "continue_statement"
228                | "goto_statement"
229                | "return_statement"
230                | "throw_statement"
231                | "raise"
232        )
233    }
234
235    fn is_nesting_structure(&self, kind: &str, _language: Language) -> bool {
236        matches!(
237            kind,
238            "lambda_expression"
239                | "anonymous_function"
240                | "closure_expression"
241                | "block"
242                | "arrow_function"
243                | "function_expression"
244        )
245    }
246
247    fn is_recursion(&self, node: &Node<'_>, _language: Language) -> bool {
248        // Check if this node is a function call to the current function
249        // This is a simplified check - full recursion detection would need function context
250        if node.kind() == "call_expression" || node.kind() == "function_call" {
251            // Would need to compare called function name with enclosing function name
252            // For now, return false - this would need more context
253        }
254        false
255    }
256
257    /// Calculate Halstead complexity metrics
258    pub fn halstead_metrics(&self, node: &Node<'_>, language: Language) -> Option<HalsteadMetrics> {
259        let mut operators = HashSet::new();
260        let mut operands = HashSet::new();
261        let mut total_operators = 0u32;
262        let mut total_operands = 0u32;
263
264        self.walk_tree(node, &mut |child| {
265            let kind = child.kind();
266            let text = self.node_text(child);
267
268            if self.is_operator(kind, language) {
269                operators.insert(text.to_owned());
270                total_operators += 1;
271            } else if self.is_operand(kind, language) {
272                operands.insert(text.to_owned());
273                total_operands += 1;
274            }
275        });
276
277        let n1 = operators.len() as u32; // distinct operators
278        let n2 = operands.len() as u32; // distinct operands
279        let nn1 = total_operators; // total operators
280        let nn2 = total_operands; // total operands
281
282        if n1 == 0 || n2 == 0 {
283            return None;
284        }
285
286        let vocabulary = n1 + n2;
287        let length = nn1 + nn2;
288
289        // Calculated length: n1 * log2(n1) + n2 * log2(n2)
290        let calculated_length = (n1 as f32) * (n1 as f32).log2() + (n2 as f32) * (n2 as f32).log2();
291
292        // Volume: N * log2(n)
293        let volume = (length as f32) * (vocabulary as f32).log2();
294
295        // Difficulty: (n1/2) * (N2/n2)
296        let difficulty = ((n1 as f32) / 2.0) * ((nn2 as f32) / (n2 as f32).max(1.0));
297
298        // Effort: D * V
299        let effort = difficulty * volume;
300
301        // Time to program: E / 18 (seconds)
302        let time = effort / 18.0;
303
304        // Estimated bugs: V / 3000
305        let bugs = volume / 3000.0;
306
307        Some(HalsteadMetrics {
308            distinct_operators: n1,
309            distinct_operands: n2,
310            total_operators: nn1,
311            total_operands: nn2,
312            vocabulary,
313            length,
314            calculated_length,
315            volume,
316            difficulty,
317            effort,
318            time,
319            bugs,
320        })
321    }
322
323    fn is_operator(&self, kind: &str, _language: Language) -> bool {
324        matches!(
325            kind,
326            "binary_operator"
327                | "unary_operator"
328                | "assignment_operator"
329                | "comparison_operator"
330                | "arithmetic_operator"
331                | "logical_operator"
332                | "bitwise_operator"
333                | "+"
334                | "-"
335                | "*"
336                | "/"
337                | "%"
338                | "="
339                | "=="
340                | "!="
341                | "<"
342                | ">"
343                | "<="
344                | ">="
345                | "&&"
346                | "||"
347                | "!"
348                | "&"
349                | "|"
350                | "^"
351                | "~"
352                | "<<"
353                | ">>"
354                | "+="
355                | "-="
356                | "*="
357                | "/="
358                | "."
359                | "->"
360                | "::"
361                | "?"
362                | ":"
363        )
364    }
365
366    fn is_operand(&self, kind: &str, _language: Language) -> bool {
367        matches!(
368            kind,
369            "identifier"
370                | "number"
371                | "integer"
372                | "float"
373                | "string"
374                | "string_literal"
375                | "number_literal"
376                | "integer_literal"
377                | "float_literal"
378                | "boolean"
379                | "true"
380                | "false"
381                | "nil"
382                | "null"
383                | "none"
384        )
385    }
386
387    /// Calculate lines of code metrics
388    pub fn loc_metrics(&self, node: &Node<'_>) -> LocMetrics {
389        let text = self.node_text(node);
390        let lines: Vec<&str> = text.lines().collect();
391
392        let mut source = 0u32;
393        let mut comments = 0u32;
394        let mut blank = 0u32;
395
396        for line in &lines {
397            let trimmed = line.trim();
398            if trimmed.is_empty() {
399                blank += 1;
400            } else if self.is_comment_line(trimmed) {
401                comments += 1;
402            } else {
403                source += 1;
404            }
405        }
406
407        LocMetrics { total: lines.len() as u32, source, comments, blank }
408    }
409
410    fn is_comment_line(&self, line: &str) -> bool {
411        line.starts_with("//")
412            || line.starts_with('#')
413            || line.starts_with("/*")
414            || line.starts_with('*')
415            || line.starts_with("*/")
416            || line.starts_with("--")
417            || line.starts_with(";;")
418            || line.starts_with("\"\"\"")
419            || line.starts_with("'''")
420    }
421
422    /// Calculate maximum nesting depth
423    pub fn max_nesting_depth(&self, node: &Node<'_>, language: Language) -> u32 {
424        let mut max_depth = 0;
425        self.nesting_walk(node, language, 0, &mut max_depth);
426        max_depth
427    }
428
429    fn nesting_walk(&self, node: &Node<'_>, language: Language, depth: u32, max_depth: &mut u32) {
430        let kind = node.kind();
431
432        let is_nesting =
433            self.is_control_flow(kind, language) || self.is_nesting_structure(kind, language);
434
435        let new_depth = if is_nesting { depth + 1 } else { depth };
436
437        if new_depth > *max_depth {
438            *max_depth = new_depth;
439        }
440
441        let mut cursor = node.walk();
442        for child in node.children(&mut cursor) {
443            self.nesting_walk(&child, language, new_depth, max_depth);
444        }
445    }
446
447    /// Count number of parameters
448    pub fn parameter_count(&self, node: &Node<'_>, _language: Language) -> u32 {
449        let mut count = 0;
450
451        // Find parameters node
452        if let Some(params) = node.child_by_field_name("parameters") {
453            let mut cursor = params.walk();
454            for child in params.children(&mut cursor) {
455                let kind = child.kind();
456                if kind.contains("parameter")
457                    || kind == "identifier"
458                    || kind == "typed_parameter"
459                    || kind == "formal_parameter"
460                {
461                    count += 1;
462                }
463            }
464        }
465
466        count
467    }
468
469    /// Count number of return statements
470    pub fn return_count(&self, node: &Node<'_>, _language: Language) -> u32 {
471        let mut count = 0;
472
473        self.walk_tree(node, &mut |child| {
474            if child.kind() == "return_statement" || child.kind() == "return" {
475                count += 1;
476            }
477        });
478
479        // If no explicit return but function has expression body, count as 1
480        if count == 0 {
481            count = 1;
482        }
483
484        count
485    }
486
487    /// Walk tree and apply callback to each node
488    fn walk_tree<F>(&self, node: &Node<'_>, callback: &mut F)
489    where
490        F: FnMut(&Node<'_>),
491    {
492        callback(node);
493
494        let mut cursor = node.walk();
495        for child in node.children(&mut cursor) {
496            self.walk_tree(&child, callback);
497        }
498    }
499}
500
501/// Calculate complexity for a function given its source code
502pub fn calculate_complexity(
503    source: &str,
504    node: &Node<'_>,
505    language: Language,
506) -> ComplexityMetrics {
507    let calculator = ComplexityCalculator::new(source);
508    calculator.calculate(node, language)
509}
510
511/// Calculate complexity for source code without needing a tree-sitter node
512///
513/// This is a convenience function that handles the parsing internally.
514/// Returns an error if the source cannot be parsed.
515pub fn calculate_complexity_from_source(
516    source: &str,
517    language: Language,
518) -> Result<ComplexityMetrics, String> {
519    // Get tree-sitter language for parsing
520    let ts_language = match language {
521        Language::Python => tree_sitter_python::LANGUAGE.into(),
522        Language::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
523        Language::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
524        Language::Rust => tree_sitter_rust::LANGUAGE.into(),
525        Language::Go => tree_sitter_go::LANGUAGE.into(),
526        Language::Java => tree_sitter_java::LANGUAGE.into(),
527        Language::C => tree_sitter_c::LANGUAGE.into(),
528        Language::Cpp => tree_sitter_cpp::LANGUAGE.into(),
529        Language::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
530        Language::Ruby => tree_sitter_ruby::LANGUAGE.into(),
531        Language::Php => tree_sitter_php::LANGUAGE_PHP.into(),
532        Language::Swift => tree_sitter_swift::LANGUAGE.into(),
533        Language::Kotlin => tree_sitter_kotlin_ng::LANGUAGE.into(),
534        Language::Scala => tree_sitter_scala::LANGUAGE.into(),
535        Language::Haskell => tree_sitter_haskell::LANGUAGE.into(),
536        Language::Elixir => tree_sitter_elixir::LANGUAGE.into(),
537        Language::Clojure => {
538            return Err(
539                "Clojure complexity analysis not available (tree-sitter-clojure incompatible with tree-sitter 0.26)"
540                    .to_owned(),
541            )
542        },
543        Language::OCaml => tree_sitter_ocaml::LANGUAGE_OCAML.into(),
544        Language::Lua => tree_sitter_lua::LANGUAGE.into(),
545        Language::R => tree_sitter_r::LANGUAGE.into(),
546        Language::Hcl => tree_sitter_hcl::LANGUAGE.into(),
547        Language::Zig => tree_sitter_zig::LANGUAGE.into(),
548        Language::Dart => tree_sitter_dart_orchard::LANGUAGE.into(),
549        Language::Bash => tree_sitter_bash::LANGUAGE.into(),
550        // FSharp doesn't have tree-sitter support yet
551        Language::FSharp => {
552            return Err(
553                "F# complexity analysis not yet supported (no tree-sitter parser available)"
554                    .to_owned(),
555            )
556        },
557    };
558
559    let mut ts_parser = tree_sitter::Parser::new();
560    ts_parser
561        .set_language(&ts_language)
562        .map_err(|e| format!("Failed to set language: {}", e))?;
563
564    let tree = ts_parser
565        .parse(source, None)
566        .ok_or_else(|| "Failed to parse source code".to_owned())?;
567
568    let calculator = ComplexityCalculator::new(source);
569    Ok(calculator.calculate(&tree.root_node(), language))
570}
571
572/// Thresholds for complexity warnings
573#[derive(Debug, Clone, Copy)]
574pub struct ComplexityThresholds {
575    /// Cyclomatic complexity warning threshold
576    pub cyclomatic_warn: u32,
577    /// Cyclomatic complexity error threshold
578    pub cyclomatic_error: u32,
579    /// Cognitive complexity warning threshold
580    pub cognitive_warn: u32,
581    /// Cognitive complexity error threshold
582    pub cognitive_error: u32,
583    /// Max nesting depth warning threshold
584    pub nesting_warn: u32,
585    /// Max nesting depth error threshold
586    pub nesting_error: u32,
587    /// Max parameter count warning threshold
588    pub params_warn: u32,
589    /// Max parameter count error threshold
590    pub params_error: u32,
591    /// Maintainability index warning threshold (below this)
592    pub maintainability_warn: f32,
593    /// Maintainability index error threshold (below this)
594    pub maintainability_error: f32,
595}
596
597impl Default for ComplexityThresholds {
598    fn default() -> Self {
599        Self {
600            cyclomatic_warn: 10,
601            cyclomatic_error: 20,
602            cognitive_warn: 15,
603            cognitive_error: 30,
604            nesting_warn: 4,
605            nesting_error: 6,
606            params_warn: 5,
607            params_error: 8,
608            maintainability_warn: 40.0,
609            maintainability_error: 20.0,
610        }
611    }
612}
613
614/// Severity of a complexity issue
615#[derive(Debug, Clone, Copy, PartialEq, Eq)]
616pub enum ComplexitySeverity {
617    Ok,
618    Warning,
619    Error,
620}
621
622/// Check complexity metrics against thresholds
623pub fn check_complexity(
624    metrics: &ComplexityMetrics,
625    thresholds: &ComplexityThresholds,
626) -> Vec<(String, ComplexitySeverity)> {
627    let mut issues = Vec::new();
628
629    // Cyclomatic complexity
630    if metrics.cyclomatic >= thresholds.cyclomatic_error {
631        issues.push((
632            format!(
633                "Cyclomatic complexity {} exceeds error threshold {}",
634                metrics.cyclomatic, thresholds.cyclomatic_error
635            ),
636            ComplexitySeverity::Error,
637        ));
638    } else if metrics.cyclomatic >= thresholds.cyclomatic_warn {
639        issues.push((
640            format!(
641                "Cyclomatic complexity {} exceeds warning threshold {}",
642                metrics.cyclomatic, thresholds.cyclomatic_warn
643            ),
644            ComplexitySeverity::Warning,
645        ));
646    }
647
648    // Cognitive complexity
649    if metrics.cognitive >= thresholds.cognitive_error {
650        issues.push((
651            format!(
652                "Cognitive complexity {} exceeds error threshold {}",
653                metrics.cognitive, thresholds.cognitive_error
654            ),
655            ComplexitySeverity::Error,
656        ));
657    } else if metrics.cognitive >= thresholds.cognitive_warn {
658        issues.push((
659            format!(
660                "Cognitive complexity {} exceeds warning threshold {}",
661                metrics.cognitive, thresholds.cognitive_warn
662            ),
663            ComplexitySeverity::Warning,
664        ));
665    }
666
667    // Nesting depth
668    if metrics.max_nesting_depth >= thresholds.nesting_error {
669        issues.push((
670            format!(
671                "Nesting depth {} exceeds error threshold {}",
672                metrics.max_nesting_depth, thresholds.nesting_error
673            ),
674            ComplexitySeverity::Error,
675        ));
676    } else if metrics.max_nesting_depth >= thresholds.nesting_warn {
677        issues.push((
678            format!(
679                "Nesting depth {} exceeds warning threshold {}",
680                metrics.max_nesting_depth, thresholds.nesting_warn
681            ),
682            ComplexitySeverity::Warning,
683        ));
684    }
685
686    // Parameter count
687    if metrics.parameter_count >= thresholds.params_error {
688        issues.push((
689            format!(
690                "Parameter count {} exceeds error threshold {}",
691                metrics.parameter_count, thresholds.params_error
692            ),
693            ComplexitySeverity::Error,
694        ));
695    } else if metrics.parameter_count >= thresholds.params_warn {
696        issues.push((
697            format!(
698                "Parameter count {} exceeds warning threshold {}",
699                metrics.parameter_count, thresholds.params_warn
700            ),
701            ComplexitySeverity::Warning,
702        ));
703    }
704
705    // Maintainability index
706    if let Some(mi) = metrics.maintainability_index {
707        if mi <= thresholds.maintainability_error {
708            issues.push((
709                format!(
710                    "Maintainability index {:.1} below error threshold {}",
711                    mi, thresholds.maintainability_error
712                ),
713                ComplexitySeverity::Error,
714            ));
715        } else if mi <= thresholds.maintainability_warn {
716            issues.push((
717                format!(
718                    "Maintainability index {:.1} below warning threshold {}",
719                    mi, thresholds.maintainability_warn
720                ),
721                ComplexitySeverity::Warning,
722            ));
723        }
724    }
725
726    issues
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732
733    // ---------------------------------------------------------------
734    // Helper: shorthand to get cyclomatic complexity from source
735    // ---------------------------------------------------------------
736    fn cc(source: &str, language: Language) -> u32 {
737        calculate_complexity_from_source(source, language)
738            .unwrap()
739            .cyclomatic
740    }
741
742    fn cog(source: &str, language: Language) -> u32 {
743        calculate_complexity_from_source(source, language)
744            .unwrap()
745            .cognitive
746    }
747
748    fn metrics(source: &str, language: Language) -> ComplexityMetrics {
749        calculate_complexity_from_source(source, language).unwrap()
750    }
751
752    // ===============================================================
753    //  1. Comment-line helper
754    // ===============================================================
755
756    #[test]
757    fn test_loc_metrics() {
758        let source = r#"
759fn example() {
760    // Comment
761    let x = 1;
762
763    /* Multi-line
764     * comment */
765    let y = 2;
766}
767"#;
768        let calculator = ComplexityCalculator::new(source);
769        assert!(calculator.is_comment_line("// Comment"));
770        assert!(calculator.is_comment_line("/* Multi-line"));
771        assert!(!calculator.is_comment_line("let x = 1;"));
772    }
773
774    // ===============================================================
775    //  2. Threshold / check_complexity tests
776    // ===============================================================
777
778    #[test]
779    fn test_thresholds_default() {
780        let thresholds = ComplexityThresholds::default();
781        assert_eq!(thresholds.cyclomatic_warn, 10);
782        assert_eq!(thresholds.cyclomatic_error, 20);
783        assert_eq!(thresholds.cognitive_warn, 15);
784        assert_eq!(thresholds.cognitive_error, 30);
785        assert_eq!(thresholds.nesting_warn, 4);
786        assert_eq!(thresholds.nesting_error, 6);
787        assert_eq!(thresholds.params_warn, 5);
788        assert_eq!(thresholds.params_error, 8);
789    }
790
791    #[test]
792    fn test_check_complexity_all_errors() {
793        let metrics = ComplexityMetrics {
794            cyclomatic: 25,
795            cognitive: 35,
796            max_nesting_depth: 7,
797            parameter_count: 10,
798            maintainability_index: Some(15.0),
799            ..Default::default()
800        };
801
802        let thresholds = ComplexityThresholds::default();
803        let issues = check_complexity(&metrics, &thresholds);
804
805        assert!(issues.len() >= 4);
806        assert!(issues
807            .iter()
808            .any(|(msg, sev)| msg.contains("Cyclomatic") && *sev == ComplexitySeverity::Error));
809        assert!(issues
810            .iter()
811            .any(|(msg, sev)| msg.contains("Cognitive") && *sev == ComplexitySeverity::Error));
812        assert!(issues
813            .iter()
814            .any(|(msg, sev)| msg.contains("Nesting") && *sev == ComplexitySeverity::Error));
815        assert!(issues
816            .iter()
817            .any(|(msg, sev)| msg.contains("Parameter") && *sev == ComplexitySeverity::Error));
818        assert!(
819            issues
820                .iter()
821                .any(|(msg, sev)| msg.contains("Maintainability")
822                    && *sev == ComplexitySeverity::Error)
823        );
824    }
825
826    #[test]
827    fn test_check_complexity_warnings() {
828        let metrics = ComplexityMetrics {
829            cyclomatic: 12,
830            cognitive: 18,
831            max_nesting_depth: 5,
832            parameter_count: 6,
833            maintainability_index: Some(35.0),
834            ..Default::default()
835        };
836
837        let thresholds = ComplexityThresholds::default();
838        let issues = check_complexity(&metrics, &thresholds);
839
840        // All should be warnings, not errors
841        for (_, sev) in &issues {
842            assert_eq!(*sev, ComplexitySeverity::Warning);
843        }
844        assert!(issues.len() >= 4);
845    }
846
847    #[test]
848    fn test_check_complexity_ok() {
849        let metrics = ComplexityMetrics {
850            cyclomatic: 3,
851            cognitive: 5,
852            max_nesting_depth: 2,
853            parameter_count: 2,
854            maintainability_index: Some(80.0),
855            ..Default::default()
856        };
857
858        let thresholds = ComplexityThresholds::default();
859        let issues = check_complexity(&metrics, &thresholds);
860        assert!(issues.is_empty());
861    }
862
863    #[test]
864    fn test_check_complexity_no_maintainability() {
865        let metrics = ComplexityMetrics {
866            cyclomatic: 3,
867            cognitive: 5,
868            maintainability_index: None,
869            ..Default::default()
870        };
871
872        let thresholds = ComplexityThresholds::default();
873        let issues = check_complexity(&metrics, &thresholds);
874        // No maintainability issue when index is None
875        assert!(!issues
876            .iter()
877            .any(|(msg, _)| msg.contains("Maintainability")));
878    }
879
880    // ===============================================================
881    //  3. Unsupported languages
882    // ===============================================================
883
884    #[test]
885    #[allow(deprecated)]
886    fn test_clojure_returns_error() {
887        let result = calculate_complexity_from_source("(defn foo [])", Language::Clojure);
888        assert!(result.is_err());
889        assert!(result.unwrap_err().contains("Clojure"));
890    }
891
892    #[test]
893    #[allow(deprecated)]
894    fn test_fsharp_returns_error() {
895        let result = calculate_complexity_from_source("let foo () = ()", Language::FSharp);
896        assert!(result.is_err());
897        assert!(result.unwrap_err().contains("F#"));
898    }
899
900    // ===============================================================
901    //  4. Python — boundary conditions
902    // ===============================================================
903
904    #[test]
905    fn test_python_empty_function() {
906        // An empty function body has base complexity 1 and no decision points
907        assert_eq!(cc("def foo():\n    pass", Language::Python), 1);
908    }
909
910    #[test]
911    fn test_python_single_statement() {
912        // A single assignment has no branching → complexity 1
913        assert_eq!(cc("x = 42", Language::Python), 1);
914    }
915
916    #[test]
917    fn test_python_single_if() {
918        // if_statement + binary_expression (comparison `>`) → 1 + 2 = 3
919        // NOTE: the implementation counts `binary_expression` nodes as decision
920        // points even for simple comparisons, which inflates the count.
921        let c = cc("def foo(x):\n    if x > 0:\n        return 1\n    return 0", Language::Python);
922        assert_eq!(c, 3);
923    }
924
925    #[test]
926    fn test_python_if_else() {
927        // Same as single if — else clause does NOT add to cyclomatic complexity
928        let c = cc(
929            "def foo(x):\n    if x > 0:\n        return 1\n    else:\n        return 0",
930            Language::Python,
931        );
932        assert_eq!(c, 3);
933    }
934
935    #[test]
936    fn test_python_if_elif_else() {
937        // if + elif each contribute; comparison operators also counted
938        let c = cc(
939            "def foo(x):\n    if x > 0:\n        return 1\n    elif x < 0:\n        return -1\n    else:\n        return 0",
940            Language::Python,
941        );
942        assert_eq!(c, 4);
943    }
944
945    #[test]
946    fn test_python_for_loop() {
947        // for_statement contributes +1; tree-sitter also produces binary_expression
948        let c = cc("def foo(xs):\n    for x in xs:\n        print(x)", Language::Python);
949        assert_eq!(c, 3);
950    }
951
952    #[test]
953    fn test_python_while_loop() {
954        // while_statement + comparison binary_expression
955        let c = cc("def foo(x):\n    while x > 0:\n        x -= 1", Language::Python);
956        assert_eq!(c, 3);
957    }
958
959    #[test]
960    fn test_python_try_except() {
961        // except_clause contributes +1
962        let c = cc(
963            "def foo():\n    try:\n        do_thing()\n    except ValueError:\n        pass",
964            Language::Python,
965        );
966        assert_eq!(c, 2);
967    }
968
969    // ===============================================================
970    //  5. Python — boolean operators
971    // ===============================================================
972
973    #[test]
974    fn test_python_boolean_and() {
975        // if_statement + boolean_operator("and") + possibly binary_expression
976        let c = cc("def foo(a, b):\n    if a and b:\n        return 1", Language::Python);
977        assert_eq!(c, 3);
978    }
979
980    #[test]
981    fn test_python_boolean_or() {
982        let c = cc("def foo(a, b):\n    if a or b:\n        return 1", Language::Python);
983        assert_eq!(c, 3);
984    }
985
986    // ===============================================================
987    //  6. Python — nesting
988    // ===============================================================
989
990    #[test]
991    fn test_python_nested_if() {
992        let c =
993            cc("def foo(a, b):\n    if a:\n        if b:\n            return 1", Language::Python);
994        assert_eq!(c, 5);
995    }
996
997    #[test]
998    fn test_python_three_sequential_ifs() {
999        let c = cc(
1000            "def foo(a, b, c):\n    if a:\n        pass\n    if b:\n        pass\n    if c:\n        pass",
1001            Language::Python,
1002        );
1003        assert_eq!(c, 7);
1004    }
1005
1006    #[test]
1007    fn test_python_deeply_nested_ifs() {
1008        let c = cc(
1009            "def foo(a, b, c):\n    if a:\n        if b:\n            if c:\n                return 1",
1010            Language::Python,
1011        );
1012        assert_eq!(c, 7);
1013    }
1014
1015    #[test]
1016    fn test_python_for_with_nested_if() {
1017        let c = cc(
1018            "def foo(xs):\n    for x in xs:\n        if x > 0:\n            print(x)",
1019            Language::Python,
1020        );
1021        assert_eq!(c, 5);
1022    }
1023
1024    #[test]
1025    fn test_python_cognitive_nested_ifs() {
1026        // Cognitive complexity penalizes nesting: 1+(0) + 1+(1) + 1+(2) = 6
1027        // plus return_statement flow break → higher total
1028        let c = cog(
1029            "def foo(a, b, c):\n    if a:\n        if b:\n            if c:\n                return 1",
1030            Language::Python,
1031        );
1032        assert_eq!(c, 13);
1033    }
1034
1035    #[test]
1036    fn test_python_cognitive_sequential_ifs() {
1037        // Sequential ifs don't increase nesting penalty
1038        let c = cog(
1039            "def foo(a, b, c):\n    if a:\n        pass\n    if b:\n        pass\n    if c:\n        pass",
1040            Language::Python,
1041        );
1042        assert_eq!(c, 6);
1043    }
1044
1045    // ===============================================================
1046    //  7. JavaScript — basic constructs
1047    // ===============================================================
1048
1049    #[test]
1050    fn test_js_empty_function() {
1051        assert_eq!(cc("function foo() {}", Language::JavaScript), 1);
1052    }
1053
1054    #[test]
1055    fn test_js_single_if() {
1056        // if_statement + binary_expression (comparison) + ternary-like nodes
1057        let c = cc("function foo(x) { if (x > 0) { return 1; } return 0; }", Language::JavaScript);
1058        assert_eq!(c, 4);
1059    }
1060
1061    #[test]
1062    fn test_js_switch_cases() {
1063        // Each case clause adds +1 to cyclomatic
1064        let c = cc(
1065            "function foo(x) { switch(x) { case 1: return 'a'; case 2: return 'b'; default: return 'c'; } }",
1066            Language::JavaScript,
1067        );
1068        assert_eq!(c, 3);
1069    }
1070
1071    #[test]
1072    fn test_js_try_catch() {
1073        // catch_clause adds +1
1074        let c = cc(
1075            "function foo() { try { doThing(); } catch(e) { handle(e); } }",
1076            Language::JavaScript,
1077        );
1078        assert_eq!(c, 2);
1079    }
1080
1081    #[test]
1082    fn test_js_ternary() {
1083        // ternary_expression + binary_expression from comparison
1084        let c = cc("function foo(x) { return x > 0 ? 1 : 0; }", Language::JavaScript);
1085        assert_eq!(c, 3);
1086    }
1087
1088    #[test]
1089    fn test_js_logical_and() {
1090        // if_statement + binary_expression("&&") + binary_expression parent
1091        let c = cc("function foo(a, b) { if (a && b) { return 1; } }", Language::JavaScript);
1092        assert_eq!(c, 4);
1093    }
1094
1095    #[test]
1096    fn test_js_logical_or() {
1097        let c = cc("function foo(a, b) { if (a || b) { return 1; } }", Language::JavaScript);
1098        assert_eq!(c, 4);
1099    }
1100
1101    #[test]
1102    fn test_js_for_loop() {
1103        let c = cc("function foo() { for (var i = 0; i < 10; i++) {} }", Language::JavaScript);
1104        // for_statement + binary_expression (i < 10) + binary_expression parent
1105        assert!(c >= 2, "for loop should add at least +1, got {c}");
1106    }
1107
1108    #[test]
1109    fn test_js_while_loop() {
1110        let c = cc("function foo(x) { while (x > 0) { x--; } }", Language::JavaScript);
1111        assert!(c >= 2, "while loop should add at least +1, got {c}");
1112    }
1113
1114    // ===============================================================
1115    //  8. TypeScript — mirrors JS behavior
1116    // ===============================================================
1117
1118    #[test]
1119    fn test_ts_empty_function() {
1120        assert_eq!(cc("function foo(): void {}", Language::TypeScript), 1);
1121    }
1122
1123    #[test]
1124    fn test_ts_single_if() {
1125        let c = cc(
1126            "function foo(x: number): number { if (x > 0) { return 1; } return 0; }",
1127            Language::TypeScript,
1128        );
1129        assert_eq!(c, 4);
1130    }
1131
1132    // ===============================================================
1133    //  9. Rust — basic constructs
1134    // ===============================================================
1135
1136    #[test]
1137    fn test_rust_empty_function() {
1138        assert_eq!(cc("fn foo() {}", Language::Rust), 1);
1139    }
1140
1141    #[test]
1142    fn test_rust_single_if() {
1143        // if_expression + binary_expression (comparison) + match on > operator
1144        let c = cc("fn foo(x: i32) -> i32 { if x > 0 { 1 } else { 0 } }", Language::Rust);
1145        assert_eq!(c, 4);
1146    }
1147
1148    #[test]
1149    fn test_rust_match_three_arms() {
1150        // match_expression + 3 match_arm nodes
1151        let c = cc("fn foo(x: i32) -> i32 { match x { 1 => 1, 2 => 2, _ => 0 } }", Language::Rust);
1152        assert_eq!(c, 5);
1153    }
1154
1155    #[test]
1156    fn test_rust_match_five_arms() {
1157        let c = cc(
1158            "fn foo(x: i32) -> &'static str { match x { 1 => \"a\", 2 => \"b\", 3 => \"c\", 4 => \"d\", _ => \"e\" } }",
1159            Language::Rust,
1160        );
1161        assert_eq!(c, 7);
1162    }
1163
1164    #[test]
1165    fn test_rust_if_let() {
1166        // if_let_expression counts as a decision point
1167        let c = cc(
1168            "fn foo(x: Option<i32>) { if let Some(v) = x { println!(\"{}\", v); } }",
1169            Language::Rust,
1170        );
1171        assert_eq!(c, 3);
1172    }
1173
1174    #[test]
1175    fn test_rust_while_let() {
1176        // while_let_expression counts as a decision point
1177        let c = cc(
1178            "fn foo(v: &mut Vec<i32>) { while let Some(x) = v.pop() { println!(\"{}\", x); } }",
1179            Language::Rust,
1180        );
1181        assert_eq!(c, 3);
1182    }
1183
1184    #[test]
1185    fn test_rust_for_loop() {
1186        // for_expression adds +1
1187        let c = cc("fn foo() { for i in 0..10 { println!(\"{}\", i); } }", Language::Rust);
1188        assert_eq!(c, 3);
1189    }
1190
1191    #[test]
1192    fn test_rust_while_loop() {
1193        // while_expression + binary_expression (comparison)
1194        let c = cc("fn foo() { let mut x = 10; while x > 0 { x -= 1; } }", Language::Rust);
1195        assert_eq!(c, 4);
1196    }
1197
1198    #[test]
1199    fn test_rust_logical_and_in_if() {
1200        let c =
1201            cc("fn foo(a: bool, b: bool) -> i32 { if a && b { 1 } else { 0 } }", Language::Rust);
1202        assert_eq!(c, 4);
1203    }
1204
1205    #[test]
1206    fn test_rust_logical_or_in_if() {
1207        let c =
1208            cc("fn foo(a: bool, b: bool) -> i32 { if a || b { 1 } else { 0 } }", Language::Rust);
1209        assert_eq!(c, 4);
1210    }
1211
1212    // ===============================================================
1213    // 10. Go — basic constructs
1214    // ===============================================================
1215
1216    #[test]
1217    fn test_go_empty_function() {
1218        assert_eq!(cc("package main\nfunc foo() {}", Language::Go), 1);
1219    }
1220
1221    #[test]
1222    fn test_go_single_if() {
1223        let c = cc(
1224            "package main\nfunc foo(x int) int { if x > 0 { return 1 }\n return 0 }",
1225            Language::Go,
1226        );
1227        assert_eq!(c, 4);
1228    }
1229
1230    #[test]
1231    fn test_go_for_loop() {
1232        let c = cc("package main\nfunc foo() { for i := 0; i < 10; i++ {} }", Language::Go);
1233        assert_eq!(c, 4);
1234    }
1235
1236    #[test]
1237    fn test_go_select_statement() {
1238        // select_statement is a Go-specific decision point
1239        let c = cc(
1240            "package main\nimport \"fmt\"\nfunc foo(ch chan int) { select { case v := <-ch: fmt.Println(v) } }",
1241            Language::Go,
1242        );
1243        assert_eq!(c, 3);
1244    }
1245
1246    // ===============================================================
1247    // 11. Java — basic constructs
1248    // ===============================================================
1249
1250    #[test]
1251    fn test_java_empty_method() {
1252        assert_eq!(cc("class Foo { void foo() {} }", Language::Java), 1);
1253    }
1254
1255    #[test]
1256    fn test_java_single_if() {
1257        let c = cc(
1258            "class Foo { int foo(int x) { if (x > 0) { return 1; } return 0; } }",
1259            Language::Java,
1260        );
1261        assert_eq!(c, 4);
1262    }
1263
1264    #[test]
1265    fn test_java_try_catch_multiple() {
1266        // Each catch_clause adds +1
1267        let c = cc(
1268            "class Foo { void foo() { try { doThing(); } catch (IOException e) { handle(e); } catch (Exception e) { handle2(e); } } }",
1269            Language::Java,
1270        );
1271        assert_eq!(c, 3);
1272    }
1273
1274    #[test]
1275    fn test_java_for_loop() {
1276        let c = cc(
1277            "class Foo { void foo() { for (int i = 0; i < 10; i++) { System.out.println(i); } } }",
1278            Language::Java,
1279        );
1280        assert!(c >= 2, "Java for loop should increase complexity, got {c}");
1281    }
1282
1283    #[test]
1284    fn test_java_while_loop() {
1285        let c = cc("class Foo { void foo(int x) { while (x > 0) { x--; } } }", Language::Java);
1286        assert!(c >= 2, "Java while loop should increase complexity, got {c}");
1287    }
1288
1289    // ===============================================================
1290    // 12. Cognitive complexity — cross-language
1291    // ===============================================================
1292
1293    #[test]
1294    fn test_cognitive_empty_python() {
1295        assert_eq!(cog("def foo():\n    pass", Language::Python), 0);
1296    }
1297
1298    #[test]
1299    fn test_cognitive_empty_rust() {
1300        assert_eq!(cog("fn foo() {}", Language::Rust), 0);
1301    }
1302
1303    #[test]
1304    fn test_cognitive_empty_js() {
1305        assert_eq!(cog("function foo() {}", Language::JavaScript), 0);
1306    }
1307
1308    #[test]
1309    fn test_cognitive_empty_go() {
1310        assert_eq!(cog("package main\nfunc foo() {}", Language::Go), 0);
1311    }
1312
1313    #[test]
1314    fn test_cognitive_empty_java() {
1315        assert_eq!(cog("class Foo { void foo() {} }", Language::Java), 0);
1316    }
1317
1318    // ===============================================================
1319    // 13. Full metrics (loc, return_count, etc.)
1320    // ===============================================================
1321
1322    #[test]
1323    fn test_full_metrics_python_empty() {
1324        let m = metrics("def foo():\n    pass", Language::Python);
1325        assert_eq!(m.cyclomatic, 1);
1326        assert_eq!(m.cognitive, 0);
1327        // return_count defaults to 1 for expression-body functions
1328        assert_eq!(m.return_count, 1);
1329        assert_eq!(m.loc.total, 2);
1330    }
1331
1332    #[test]
1333    fn test_full_metrics_rust_empty() {
1334        let m = metrics("fn foo() {}", Language::Rust);
1335        assert_eq!(m.cyclomatic, 1);
1336        assert_eq!(m.cognitive, 0);
1337        assert_eq!(m.return_count, 1);
1338    }
1339
1340    #[test]
1341    fn test_full_metrics_has_halstead() {
1342        // Code with operators and operands should produce Halstead metrics
1343        let m =
1344            metrics("fn foo(x: i32) -> i32 { if x > 0 { x + 1 } else { x - 1 } }", Language::Rust);
1345        assert!(m.halstead.is_some(), "Halstead metrics should be computed for non-trivial code");
1346        let h = m.halstead.unwrap();
1347        assert!(h.volume > 0.0);
1348        assert!(h.distinct_operators > 0);
1349        assert!(h.distinct_operands > 0);
1350    }
1351
1352    #[test]
1353    fn test_maintainability_index_range() {
1354        let m =
1355            metrics("fn foo(x: i32) -> i32 { if x > 0 { x + 1 } else { x - 1 } }", Language::Rust);
1356        if let Some(mi) = m.maintainability_index {
1357            assert!(
1358                (0.0..=100.0).contains(&mi),
1359                "Maintainability index {mi} should be in [0, 100]"
1360            );
1361        }
1362    }
1363
1364    // ===============================================================
1365    // 14. LOC metrics
1366    // ===============================================================
1367
1368    #[test]
1369    fn test_loc_multiline_python() {
1370        let source = "def foo():\n    # comment\n    x = 1\n\n    y = 2\n    return x + y";
1371        let m = metrics(source, Language::Python);
1372        assert_eq!(m.loc.total, 6);
1373        assert_eq!(m.loc.comments, 1);
1374        assert_eq!(m.loc.blank, 1);
1375        assert_eq!(m.loc.source, 4);
1376    }
1377
1378    #[test]
1379    fn test_loc_single_line() {
1380        let m = metrics("x = 1", Language::Python);
1381        assert_eq!(m.loc.total, 1);
1382        assert_eq!(m.loc.source, 1);
1383        assert_eq!(m.loc.blank, 0);
1384        assert_eq!(m.loc.comments, 0);
1385    }
1386
1387    // ===============================================================
1388    // 15. Comment line detection
1389    // ===============================================================
1390
1391    #[test]
1392    fn test_comment_line_detection_various() {
1393        let calc = ComplexityCalculator::new("");
1394        // Positive cases
1395        assert!(calc.is_comment_line("// C-style comment"));
1396        assert!(calc.is_comment_line("# Python/Ruby comment"));
1397        assert!(calc.is_comment_line("/* C block comment start"));
1398        assert!(calc.is_comment_line("* continuation of block comment"));
1399        assert!(calc.is_comment_line("*/ end of block comment"));
1400        assert!(calc.is_comment_line("-- SQL/Haskell comment"));
1401        assert!(calc.is_comment_line(";; Lisp comment"));
1402        assert!(calc.is_comment_line("\"\"\" Python docstring"));
1403        assert!(calc.is_comment_line("''' Python single-quote docstring"));
1404
1405        // Negative cases
1406        assert!(!calc.is_comment_line("let x = 1;"));
1407        assert!(!calc.is_comment_line("return 42"));
1408        assert!(!calc.is_comment_line("if x > 0:"));
1409    }
1410
1411    // ===============================================================
1412    // 16. Edge case: no branching in various languages
1413    // ===============================================================
1414
1415    #[test]
1416    fn test_no_branching_python() {
1417        assert_eq!(cc("x = 1\ny = 2\nz = x + y", Language::Python), 1);
1418    }
1419
1420    #[test]
1421    fn test_no_branching_js() {
1422        assert_eq!(cc("function foo() { var x = 1; var y = 2; }", Language::JavaScript), 1);
1423    }
1424
1425    #[test]
1426    fn test_no_branching_rust() {
1427        assert_eq!(cc("fn foo() { let x = 1; let y = 2; }", Language::Rust), 1);
1428    }
1429
1430    #[test]
1431    fn test_no_branching_go() {
1432        assert_eq!(cc("package main\nfunc foo() { x := 1; _ = x }", Language::Go), 1);
1433    }
1434
1435    #[test]
1436    fn test_no_branching_java() {
1437        assert_eq!(cc("class Foo { void foo() { int x = 1; } }", Language::Java), 1);
1438    }
1439
1440    // ===============================================================
1441    // 17. Complexity severity enum
1442    // ===============================================================
1443
1444    #[test]
1445    fn test_severity_equality() {
1446        assert_eq!(ComplexitySeverity::Ok, ComplexitySeverity::Ok);
1447        assert_eq!(ComplexitySeverity::Warning, ComplexitySeverity::Warning);
1448        assert_eq!(ComplexitySeverity::Error, ComplexitySeverity::Error);
1449        assert_ne!(ComplexitySeverity::Ok, ComplexitySeverity::Error);
1450    }
1451
1452    // ===============================================================
1453    // 18. Large/complex functions
1454    // ===============================================================
1455
1456    #[test]
1457    fn test_python_many_elif_branches() {
1458        let source = "\
1459def classify(x):
1460    if x == 1:
1461        return 'one'
1462    elif x == 2:
1463        return 'two'
1464    elif x == 3:
1465        return 'three'
1466    elif x == 4:
1467        return 'four'
1468    elif x == 5:
1469        return 'five'
1470    else:
1471        return 'other'";
1472        let c = cc(source, Language::Python);
1473        // Should be relatively high: base(1) + if + 4*elif + comparisons
1474        assert!(c >= 6, "Many elif branches should produce high complexity, got {c}");
1475    }
1476
1477    #[test]
1478    fn test_rust_complex_match_with_guards() {
1479        // match_expression + match_arm per arm
1480        let source = r#"
1481fn classify(x: i32) -> &'static str {
1482    match x {
1483        0 => "zero",
1484        1..=10 => "small",
1485        11..=100 => "medium",
1486        _ => "large",
1487    }
1488}"#;
1489        let c = cc(source, Language::Rust);
1490        // match_expression + 4 match_arm nodes = 1 + 5
1491        assert!(c >= 5, "Rust match with 4 arms should have complexity >= 5, got {c}");
1492    }
1493
1494    // ===============================================================
1495    // 19. Multiple catch clauses
1496    // ===============================================================
1497
1498    #[test]
1499    fn test_python_multiple_except() {
1500        let source = "\
1501def foo():
1502    try:
1503        do_thing()
1504    except ValueError:
1505        pass
1506    except TypeError:
1507        pass
1508    except Exception:
1509        pass";
1510        let c = cc(source, Language::Python);
1511        // 3 except_clause nodes → 1 + 3 = 4
1512        assert_eq!(c, 4);
1513    }
1514
1515    #[test]
1516    fn test_js_try_catch_finally() {
1517        // finally does not add to cyclomatic; catch does
1518        let c = cc(
1519            "function foo() { try { x(); } catch(e) { y(); } finally { z(); } }",
1520            Language::JavaScript,
1521        );
1522        assert_eq!(c, 2);
1523    }
1524
1525    // ===============================================================
1526    // 20. Nesting depth
1527    // ===============================================================
1528
1529    #[test]
1530    fn test_nesting_depth_flat_python() {
1531        let m = metrics("def foo():\n    pass", Language::Python);
1532        // The function body itself counts as a nesting structure ("block"),
1533        // so even a flat function has nesting depth 1.
1534        assert_eq!(m.max_nesting_depth, 1);
1535    }
1536
1537    #[test]
1538    fn test_nesting_depth_nested_python() {
1539        let m = metrics(
1540            "def foo(a, b, c):\n    if a:\n        if b:\n            if c:\n                return 1",
1541            Language::Python,
1542        );
1543        assert!(
1544            m.max_nesting_depth >= 3,
1545            "Three nested ifs should produce nesting >= 3, got {}",
1546            m.max_nesting_depth
1547        );
1548    }
1549
1550    // ===============================================================
1551    // 21. calculate_complexity (node-based API) via from_source
1552    // ===============================================================
1553
1554    #[test]
1555    fn test_calculate_complexity_from_source_returns_all_fields() {
1556        let m =
1557            metrics("fn foo(x: i32) -> i32 { if x > 0 { x + 1 } else { x - 1 } }", Language::Rust);
1558        // All fields should be populated
1559        assert!(m.cyclomatic >= 1);
1560        assert!(m.loc.total >= 1);
1561        assert!(m.return_count >= 1);
1562    }
1563
1564    // ===============================================================
1565    // 22. Python: match statement (Python 3.10+)
1566    // ===============================================================
1567
1568    #[test]
1569    fn test_python_match_statement() {
1570        // tree-sitter-python may or may not parse match as a keyword
1571        // depending on the grammar version. Test what we get.
1572        let source = "\
1573match command:
1574    case 'quit':
1575        quit()
1576    case 'hello':
1577        hello()
1578    case _:
1579        unknown()";
1580        let result = calculate_complexity_from_source(source, Language::Python);
1581        // If parsing succeeds, complexity should be > 1 due to case branches
1582        if let Ok(m) = result {
1583            assert!(m.cyclomatic >= 1);
1584        }
1585    }
1586
1587    // ===============================================================
1588    // 23. Rust: nested loops
1589    // ===============================================================
1590
1591    #[test]
1592    fn test_rust_nested_loops() {
1593        let source = "\
1594fn foo() {
1595    for i in 0..10 {
1596        for j in 0..10 {
1597            if i == j {
1598                println!(\"equal\");
1599            }
1600        }
1601    }
1602}";
1603        let c = cc(source, Language::Rust);
1604        // Two for_expression + if_expression + binary_expression for ==
1605        assert!(c >= 4, "Nested loops with if should have complexity >= 4, got {c}");
1606    }
1607
1608    // ===============================================================
1609    // 24. Go: type switch
1610    // ===============================================================
1611
1612    #[test]
1613    fn test_go_type_switch() {
1614        let source = "\
1615package main
1616func foo(i interface{}) {
1617    switch i.(type) {
1618    case int:
1619        println(\"int\")
1620    case string:
1621        println(\"string\")
1622    }
1623}";
1624        let c = cc(source, Language::Go);
1625        // type_switch_statement is a Go-specific decision point + case clauses
1626        assert!(c >= 2, "Go type switch should increase complexity, got {c}");
1627    }
1628
1629    // ===============================================================
1630    // 25. JavaScript: nested ternaries
1631    // ===============================================================
1632
1633    #[test]
1634    fn test_js_nested_ternary() {
1635        let c = cc(
1636            "function foo(x) { return x > 0 ? (x > 10 ? 'big' : 'small') : 'neg'; }",
1637            Language::JavaScript,
1638        );
1639        // Two ternary_expression nodes + comparisons
1640        assert!(c >= 3, "Nested ternaries should increase complexity, got {c}");
1641    }
1642
1643    // ===============================================================
1644    // 26. ComplexityCalculator constructor
1645    // ===============================================================
1646
1647    #[test]
1648    fn test_calculator_new_from_string() {
1649        let calc = ComplexityCalculator::new("some source code");
1650        assert_eq!(calc.source, "some source code");
1651    }
1652
1653    #[test]
1654    fn test_calculator_new_from_owned_string() {
1655        let calc = ComplexityCalculator::new(String::from("owned source"));
1656        assert_eq!(calc.source, "owned source");
1657    }
1658
1659    // ===============================================================
1660    // 27. Halstead metrics edge cases
1661    // ===============================================================
1662
1663    #[test]
1664    fn test_halstead_none_for_trivial_code() {
1665        // Code with no operators or operands produces None
1666        let m = metrics("", Language::Python);
1667        assert!(m.halstead.is_none());
1668    }
1669
1670    #[test]
1671    fn test_halstead_computed_for_arithmetic() {
1672        let m = metrics("fn foo() { let x = 1 + 2 * 3; }", Language::Rust);
1673        if let Some(h) = &m.halstead {
1674            assert!(h.length > 0, "Halstead length should be > 0");
1675            assert!(h.vocabulary > 0, "Halstead vocabulary should be > 0");
1676            assert!(h.bugs >= 0.0, "Estimated bugs should be non-negative");
1677            assert!(h.time >= 0.0, "Estimated time should be non-negative");
1678        }
1679    }
1680
1681    // ===============================================================
1682    // 28. Python: complex boolean expression
1683    // ===============================================================
1684
1685    #[test]
1686    fn test_python_complex_boolean() {
1687        let c = cc("def foo(a, b, c):\n    if a and b or c:\n        return 1", Language::Python);
1688        // Multiple boolean operators should each contribute
1689        assert!(c >= 3, "Complex boolean should increase complexity, got {c}");
1690    }
1691
1692    // ===============================================================
1693    // 29. Java: switch statement
1694    // ===============================================================
1695
1696    #[test]
1697    fn test_java_switch() {
1698        let source = "\
1699class Foo {
1700    String bar(int x) {
1701        switch (x) {
1702            case 1: return \"a\";
1703            case 2: return \"b\";
1704            case 3: return \"c\";
1705            default: return \"d\";
1706        }
1707    }
1708}";
1709        let c = cc(source, Language::Java);
1710        // switch_statement node + case nodes
1711        assert!(c >= 2, "Java switch should increase complexity, got {c}");
1712    }
1713
1714    // ===============================================================
1715    // 30. Return count
1716    // ===============================================================
1717
1718    #[test]
1719    fn test_return_count_multiple_returns() {
1720        let m =
1721            metrics("def foo(x):\n    if x > 0:\n        return 1\n    return 0", Language::Python);
1722        // The implementation walks the entire AST tree and counts all
1723        // "return_statement" nodes. In Python's tree-sitter grammar,
1724        // return statements may produce additional child nodes that
1725        // also match, resulting in a higher count than the literal
1726        // number of return statements in the source.
1727        assert_eq!(m.return_count, 4);
1728    }
1729
1730    #[test]
1731    fn test_return_count_no_explicit_return() {
1732        let m = metrics("def foo():\n    pass", Language::Python);
1733        // No explicit return → defaults to 1
1734        assert_eq!(m.return_count, 1);
1735    }
1736
1737    // ===============================================================
1738    // 31. Cyclomatic always >= 1
1739    // ===============================================================
1740
1741    #[test]
1742    fn test_cyclomatic_minimum_is_one() {
1743        // Even empty/trivial code has base complexity of 1
1744        for lang in [
1745            Language::Python,
1746            Language::JavaScript,
1747            Language::TypeScript,
1748            Language::Rust,
1749            Language::Go,
1750            Language::Java,
1751        ] {
1752            let source = match lang {
1753                Language::Go => "package main",
1754                _ => "",
1755            };
1756            let c = cc(source, lang);
1757            assert!(c >= 1, "Cyclomatic complexity should always be >= 1 for {lang:?}");
1758        }
1759    }
1760
1761    // ===============================================================
1762    // 32. Cognitive always >= 0
1763    // ===============================================================
1764
1765    #[test]
1766    fn test_cognitive_minimum_is_zero() {
1767        for lang in
1768            [Language::Python, Language::JavaScript, Language::Rust, Language::Go, Language::Java]
1769        {
1770            let source = match lang {
1771                Language::Go => "package main",
1772                _ => "",
1773            };
1774            let c = cog(source, lang);
1775            assert_eq!(c, 0, "Empty code cognitive complexity should be 0 for {lang:?}");
1776        }
1777    }
1778}