1use crate::analysis::types::{ComplexityMetrics, HalsteadMetrics, LocMetrics};
7use crate::parser::Language;
8use std::collections::HashSet;
9use tree_sitter::Node;
10
11pub struct ComplexityCalculator {
13 source: String,
15}
16
17impl ComplexityCalculator {
18 pub fn new(source: impl Into<String>) -> Self {
20 Self { source: source.into() }
21 }
22
23 fn node_text(&self, node: &Node<'_>) -> &str {
25 node.utf8_text(self.source.as_bytes()).unwrap_or("")
26 }
27
28 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 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 (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 pub fn cyclomatic_complexity(&self, node: &Node<'_>, language: Language) -> u32 {
70 let mut complexity = 1; 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 fn is_decision_point(&self, node: &Node<'_>, language: Language) -> bool {
83 let kind = node.kind();
84
85 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_expression",
109 "binary_expression",
110 "logical_and",
111 "logical_or",
112 ];
113
114 if common_decisions.contains(&kind) {
115 return true;
116 }
117
118 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 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 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 let is_control_flow = self.is_control_flow(kind, language);
167 if is_control_flow {
168 *complexity += 1;
170 *complexity += nesting;
172 }
173
174 if self.is_flow_break(kind, language) {
176 *complexity += 1;
177 }
178
179 if self.is_recursion(node, language) {
181 *complexity += 1;
182 }
183
184 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 if node.kind() == "call_expression" || node.kind() == "function_call" {
251 }
254 false
255 }
256
257 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; let n2 = operands.len() as u32; let nn1 = total_operators; let nn2 = total_operands; if n1 == 0 || n2 == 0 {
283 return None;
284 }
285
286 let vocabulary = n1 + n2;
287 let length = nn1 + nn2;
288
289 let calculated_length = (n1 as f32) * (n1 as f32).log2() + (n2 as f32) * (n2 as f32).log2();
291
292 let volume = (length as f32) * (vocabulary as f32).log2();
294
295 let difficulty = ((n1 as f32) / 2.0) * ((nn2 as f32) / (n2 as f32).max(1.0));
297
298 let effort = difficulty * volume;
300
301 let time = effort / 18.0;
303
304 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 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 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 pub fn parameter_count(&self, node: &Node<'_>, _language: Language) -> u32 {
449 let mut count = 0;
450
451 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 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 count == 0 {
481 count = 1;
482 }
483
484 count
485 }
486
487 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
501pub 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
511pub fn calculate_complexity_from_source(
516 source: &str,
517 language: Language,
518) -> Result<ComplexityMetrics, String> {
519 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 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#[derive(Debug, Clone, Copy)]
574pub struct ComplexityThresholds {
575 pub cyclomatic_warn: u32,
577 pub cyclomatic_error: u32,
579 pub cognitive_warn: u32,
581 pub cognitive_error: u32,
583 pub nesting_warn: u32,
585 pub nesting_error: u32,
587 pub params_warn: u32,
589 pub params_error: u32,
591 pub maintainability_warn: f32,
593 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
616pub enum ComplexitySeverity {
617 Ok,
618 Warning,
619 Error,
620}
621
622pub fn check_complexity(
624 metrics: &ComplexityMetrics,
625 thresholds: &ComplexityThresholds,
626) -> Vec<(String, ComplexitySeverity)> {
627 let mut issues = Vec::new();
628
629 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 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 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 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 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 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 #[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 #[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 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 assert!(!issues
876 .iter()
877 .any(|(msg, _)| msg.contains("Maintainability")));
878 }
879
880 #[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 #[test]
905 fn test_python_empty_function() {
906 assert_eq!(cc("def foo():\n pass", Language::Python), 1);
908 }
909
910 #[test]
911 fn test_python_single_statement() {
912 assert_eq!(cc("x = 42", Language::Python), 1);
914 }
915
916 #[test]
917 fn test_python_single_if() {
918 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 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 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 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 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 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 #[test]
974 fn test_python_boolean_and() {
975 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 #[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 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 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 #[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 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 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 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 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 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 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 #[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 #[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 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 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 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 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 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 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 #[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 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 #[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 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 #[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 #[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 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 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 #[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 #[test]
1392 fn test_comment_line_detection_various() {
1393 let calc = ComplexityCalculator::new("");
1394 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 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 #[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 #[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 #[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 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 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 assert!(c >= 5, "Rust match with 4 arms should have complexity >= 5, got {c}");
1492 }
1493
1494 #[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 assert_eq!(c, 4);
1513 }
1514
1515 #[test]
1516 fn test_js_try_catch_finally() {
1517 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 #[test]
1530 fn test_nesting_depth_flat_python() {
1531 let m = metrics("def foo():\n pass", Language::Python);
1532 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 #[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 assert!(m.cyclomatic >= 1);
1560 assert!(m.loc.total >= 1);
1561 assert!(m.return_count >= 1);
1562 }
1563
1564 #[test]
1569 fn test_python_match_statement() {
1570 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 let Ok(m) = result {
1583 assert!(m.cyclomatic >= 1);
1584 }
1585 }
1586
1587 #[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 assert!(c >= 4, "Nested loops with if should have complexity >= 4, got {c}");
1606 }
1607
1608 #[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 assert!(c >= 2, "Go type switch should increase complexity, got {c}");
1627 }
1628
1629 #[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 assert!(c >= 3, "Nested ternaries should increase complexity, got {c}");
1641 }
1642
1643 #[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 #[test]
1664 fn test_halstead_none_for_trivial_code() {
1665 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 #[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 assert!(c >= 3, "Complex boolean should increase complexity, got {c}");
1690 }
1691
1692 #[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 assert!(c >= 2, "Java switch should increase complexity, got {c}");
1712 }
1713
1714 #[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 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 assert_eq!(m.return_count, 1);
1735 }
1736
1737 #[test]
1742 fn test_cyclomatic_minimum_is_one() {
1743 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 #[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}