src/metrics/cognitive.rs (1,778 lines of code) (raw):

use std::collections::HashMap; use serde::Serialize; use serde::ser::{SerializeStruct, Serializer}; use std::fmt; use crate::checker::Checker; use crate::macros::implement_metric_trait; use crate::*; // TODO: Find a way to increment the cognitive complexity value // for recursive code. For some kind of languages, such as C++, it is pretty // hard to detect, just parsing the code, if a determined function is recursive // because the call graph of a function is solved at runtime. // So a possible solution could be searching for a crate which implements // a light language interpreter, computing the call graph, and then detecting // if there are cycles. At this point, it is possible to figure out if a // function is recursive or not. /// The `Cognitive Complexity` metric. #[derive(Debug, Clone)] pub struct Stats { structural: usize, structural_sum: usize, structural_min: usize, structural_max: usize, nesting: usize, total_space_functions: usize, boolean_seq: BoolSequence, } impl Default for Stats { fn default() -> Self { Self { structural: 0, structural_sum: 0, structural_min: usize::MAX, structural_max: 0, nesting: 0, total_space_functions: 1, boolean_seq: BoolSequence::default(), } } } impl Serialize for Stats { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { let mut st = serializer.serialize_struct("cognitive", 4)?; st.serialize_field("sum", &self.cognitive_sum())?; st.serialize_field("average", &self.cognitive_average())?; st.serialize_field("min", &self.cognitive_min())?; st.serialize_field("max", &self.cognitive_max())?; st.end() } } impl fmt::Display for Stats { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "sum: {}, average: {}, min:{}, max: {}", self.cognitive(), self.cognitive_average(), self.cognitive_min(), self.cognitive_max() ) } } impl Stats { /// Merges a second `Cognitive Complexity` metric into the first one pub fn merge(&mut self, other: &Stats) { self.structural_min = self.structural_min.min(other.structural_min); self.structural_max = self.structural_max.max(other.structural_max); self.structural_sum += other.structural_sum; } /// Returns the `Cognitive Complexity` metric value pub fn cognitive(&self) -> f64 { self.structural as f64 } /// Returns the `Cognitive Complexity` sum metric value pub fn cognitive_sum(&self) -> f64 { self.structural_sum as f64 } /// Returns the `Cognitive Complexity` minimum metric value pub fn cognitive_min(&self) -> f64 { self.structural_min as f64 } /// Returns the `Cognitive Complexity` maximum metric value pub fn cognitive_max(&self) -> f64 { self.structural_max as f64 } /// Returns the `Cognitive Complexity` metric average value /// /// This value is computed dividing the `Cognitive Complexity` value /// for the total number of functions/closures in a space. /// /// If there are no functions in a code, its value is `NAN`. pub fn cognitive_average(&self) -> f64 { self.cognitive_sum() / self.total_space_functions as f64 } #[inline(always)] pub(crate) fn compute_sum(&mut self) { self.structural_sum += self.structural; } #[inline(always)] pub(crate) fn compute_minmax(&mut self) { self.structural_min = self.structural_min.min(self.structural); self.structural_max = self.structural_max.max(self.structural); self.compute_sum(); } pub(crate) fn finalize(&mut self, total_space_functions: usize) { self.total_space_functions = total_space_functions; } } pub trait Cognitive where Self: Checker, { fn compute( node: &Node, stats: &mut Stats, nesting_map: &mut HashMap<usize, (usize, usize, usize)>, ); } fn compute_booleans<T: std::cmp::PartialEq + std::convert::From<u16>>( node: &Node, stats: &mut Stats, typs1: T, typs2: T, ) { for child in node.children() { if typs1 == child.kind_id().into() || typs2 == child.kind_id().into() { stats.structural = stats .boolean_seq .eval_based_on_prev(child.kind_id(), stats.structural) } } } #[derive(Debug, Default, Clone)] struct BoolSequence { boolean_op: Option<u16>, } impl BoolSequence { fn reset(&mut self) { self.boolean_op = None; } fn not_operator(&mut self, not_id: u16) { self.boolean_op = Some(not_id); } fn eval_based_on_prev(&mut self, bool_id: u16, structural: usize) -> usize { if let Some(prev) = self.boolean_op { if prev != bool_id { // The boolean operator is different from the previous one, so // the counter is incremented. structural + 1 } else { // The boolean operator is equal to the previous one, so // the counter is not incremented. structural } } else { // Save the first boolean operator in a sequence of // logical operators and increment the counter. self.boolean_op = Some(bool_id); structural + 1 } } } #[inline(always)] fn increment(stats: &mut Stats) { stats.structural += stats.nesting + 1; } #[inline(always)] fn increment_by_one(stats: &mut Stats) { stats.structural += 1; } fn get_nesting_from_map( node: &Node, nesting_map: &mut HashMap<usize, (usize, usize, usize)>, ) -> (usize, usize, usize) { if let Some(parent) = node.parent() { if let Some(n) = nesting_map.get(&parent.id()) { *n } else { (0, 0, 0) } } else { (0, 0, 0) } } fn increment_function_depth<T: std::cmp::PartialEq + std::convert::From<u16>>( depth: &mut usize, node: &Node, stop: T, ) { // Increase depth function nesting if needed let mut child = *node; while let Some(parent) = child.parent() { if stop == parent.kind_id().into() { *depth += 1; break; } child = parent; } } #[inline(always)] fn increase_nesting(stats: &mut Stats, nesting: &mut usize, depth: usize, lambda: usize) { stats.nesting = *nesting + depth + lambda; increment(stats); *nesting += 1; stats.boolean_seq.reset(); } impl Cognitive for PythonCode { fn compute( node: &Node, stats: &mut Stats, nesting_map: &mut HashMap<usize, (usize, usize, usize)>, ) { use Python::*; // Get nesting of the parent let (mut nesting, mut depth, mut lambda) = get_nesting_from_map(node, nesting_map); match node.kind_id().into() { IfStatement | ForStatement | WhileStatement | ConditionalExpression => { increase_nesting(stats, &mut nesting, depth, lambda); } ElifClause => { // No nesting increment for them because their cost has already // been paid by the if construct increment_by_one(stats); // Reset the boolean sequence stats.boolean_seq.reset(); } ElseClause | FinallyClause => { // No nesting increment for them because their cost has already // been paid by the if construct increment_by_one(stats); } ExceptClause => { nesting += 1; increment(stats); } ExpressionList | ExpressionStatement | Tuple => { stats.boolean_seq.reset(); } NotOperator => { stats.boolean_seq.not_operator(node.kind_id()); } BooleanOperator => { if node.count_specific_ancestors::<PythonParser>( |node| node.kind_id() == BooleanOperator, |node| node.kind_id() == Lambda, ) == 0 { stats.structural += node.count_specific_ancestors::<PythonParser>( |node| node.kind_id() == Lambda, |node| { matches!( node.kind_id().into(), ExpressionList | IfStatement | ForStatement | WhileStatement ) }, ); } compute_booleans::<language_python::Python>(node, stats, And, Or); } Lambda => { // Increase lambda nesting lambda += 1; } FunctionDefinition => { // Increase depth function nesting if needed increment_function_depth::<language_python::Python>( &mut depth, node, FunctionDefinition, ); } _ => {} } // Add node to nesting map nesting_map.insert(node.id(), (nesting, depth, lambda)); } } impl Cognitive for RustCode { fn compute( node: &Node, stats: &mut Stats, nesting_map: &mut HashMap<usize, (usize, usize, usize)>, ) { use Rust::*; //TODO: Implement macros let (mut nesting, mut depth, mut lambda) = get_nesting_from_map(node, nesting_map); match node.kind_id().into() { IfExpression => { // Check if a node is not an else-if if !Self::is_else_if(node) { increase_nesting(stats,&mut nesting, depth, lambda); } } ForExpression | WhileExpression | MatchExpression => { increase_nesting(stats,&mut nesting, depth, lambda); } Else /*else-if also */ => { increment_by_one(stats); } BreakExpression | ContinueExpression => { if let Some(label_child) = node.child(1) { if let Label = label_child.kind_id().into() { increment_by_one(stats); } } } UnaryExpression => { stats.boolean_seq.not_operator(node.kind_id()); } BinaryExpression => { compute_booleans::<language_rust::Rust>(node, stats, AMPAMP, PIPEPIPE); } FunctionItem => { nesting = 0; // Increase depth function nesting if needed increment_function_depth::<language_rust::Rust>(&mut depth, node, FunctionItem); } ClosureExpression => { lambda += 1; } _ => {} } nesting_map.insert(node.id(), (nesting, depth, lambda)); } } impl Cognitive for CppCode { fn compute( node: &Node, stats: &mut Stats, nesting_map: &mut HashMap<usize, (usize, usize, usize)>, ) { use Cpp::*; //TODO: Implement macros let (mut nesting, depth, mut lambda) = get_nesting_from_map(node, nesting_map); match node.kind_id().into() { IfStatement => { if !Self::is_else_if(node) { increase_nesting(stats,&mut nesting, depth, lambda); } } ForStatement | WhileStatement | DoStatement | SwitchStatement | CatchClause => { increase_nesting(stats,&mut nesting, depth, lambda); } GotoStatement | Else /* else-if also */ => { increment_by_one(stats); } UnaryExpression2 => { stats.boolean_seq.not_operator(node.kind_id()); } BinaryExpression2 => { compute_booleans::<language_cpp::Cpp>(node, stats, AMPAMP, PIPEPIPE); } LambdaExpression => { lambda += 1; } _ => {} } nesting_map.insert(node.id(), (nesting, depth, lambda)); } } macro_rules! js_cognitive { ($lang:ident) => { fn compute(node: &Node, stats: &mut Stats, nesting_map: &mut HashMap<usize, (usize, usize, usize)>) { use $lang::*; let (mut nesting, mut depth, mut lambda) = get_nesting_from_map(node, nesting_map); match node.kind_id().into() { IfStatement => { if !Self::is_else_if(&node) { increase_nesting(stats,&mut nesting, depth, lambda); } } ForStatement | ForInStatement | WhileStatement | DoStatement | SwitchStatement | CatchClause | TernaryExpression => { increase_nesting(stats,&mut nesting, depth, lambda); } Else /* else-if also */ => { increment_by_one(stats); } ExpressionStatement => { // Reset the boolean sequence stats.boolean_seq.reset(); } UnaryExpression => { stats.boolean_seq.not_operator(node.kind_id()); } BinaryExpression => { compute_booleans::<$lang>(node, stats, AMPAMP, PIPEPIPE); } FunctionDeclaration => { // Reset lambda nesting at function for JS nesting = 0; lambda = 0; // Increase depth function nesting if needed increment_function_depth::<$lang>(&mut depth, node, FunctionDeclaration); } ArrowFunction => { lambda += 1; } _ => {} } nesting_map.insert(node.id(), (nesting, depth, lambda)); } }; } impl Cognitive for MozjsCode { js_cognitive!(Mozjs); } impl Cognitive for JavascriptCode { js_cognitive!(Javascript); } impl Cognitive for TypescriptCode { js_cognitive!(Typescript); } impl Cognitive for TsxCode { js_cognitive!(Tsx); } impl Cognitive for JavaCode { fn compute( node: &Node, stats: &mut Stats, nesting_map: &mut HashMap<usize, (usize, usize, usize)>, ) { use Java::*; let (mut nesting, depth, mut lambda) = get_nesting_from_map(node, nesting_map); match node.kind_id().into() { IfStatement => { if !Self::is_else_if(node) { increase_nesting(stats,&mut nesting, depth, lambda); } } ForStatement | WhileStatement | DoStatement | SwitchBlock | CatchClause => { increase_nesting(stats,&mut nesting, depth, lambda); } Else /* else-if also */ => { increment_by_one(stats); } UnaryExpression => { stats.boolean_seq.not_operator(node.kind_id()); } BinaryExpression => { compute_booleans::<language_java::Java>(node, stats, AMPAMP, PIPEPIPE); } LambdaExpression => { lambda += 1; } _ => {} } nesting_map.insert(node.id(), (nesting, depth, lambda)); } } implement_metric_trait!(Cognitive, PreprocCode, CcommentCode, KotlinCode); #[cfg(test)] mod tests { use crate::tools::check_metrics; use super::*; #[test] fn python_no_cognitive() { check_metrics::<PythonParser>("a = 42", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 0.0, "average": null, "min": 0.0, "max": 0.0 }"### ); }); } #[test] fn rust_no_cognitive() { check_metrics::<RustParser>("let a = 42;", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 0.0, "average": null, "min": 0.0, "max": 0.0 }"### ); }); } #[test] fn c_no_cognitive() { check_metrics::<CppParser>("int a = 42;", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 0.0, "average": null, "min": 0.0, "max": 0.0 }"### ); }); } #[test] fn mozjs_no_cognitive() { check_metrics::<MozjsParser>("var a = 42;", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 0.0, "average": null, "min": 0.0, "max": 0.0 }"### ); }); } #[test] fn python_simple_function() { check_metrics::<PythonParser>( "def f(a, b): if a and b: # +2 (+1 and) return 1 if c and d: # +2 (+1 and) return 1", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn python_expression_statement() { // Boolean expressions containing `And` and `Or` operators were not // considered in assignments check_metrics::<PythonParser>( "def f(a, b): c = True and True", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 1.0, "average": 1.0, "min": 0.0, "max": 1.0 }"### ); }, ); } #[test] fn python_tuple() { // Boolean expressions containing `And` and `Or` operators were not // considered inside tuples check_metrics::<PythonParser>( "def f(a, b): return \"%s%s\" % (a and \"Get\" or \"Set\", b)", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 2.0, "average": 2.0, "min": 0.0, "max": 2.0 }"### ); }, ); } #[test] fn python_elif_function() { // Boolean expressions containing `And` and `Or` operators were not // considered in `elif` statements check_metrics::<PythonParser>( "def f(a, b): if a and b: # +2 (+1 and) return 1 elif c and d: # +2 (+1 and) return 1", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn python_more_elifs_function() { // Boolean expressions containing `And` and `Or` operators were not // considered when there were more `elif` statements check_metrics::<PythonParser>( "def f(a, b): if a and b: # +2 (+1 and) return 1 elif c and d: # +2 (+1 and) return 1 elif e and f: # +2 (+1 and) return 1", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 6.0, "average": 6.0, "min": 0.0, "max": 6.0 }"### ); }, ); } #[test] fn rust_simple_function() { check_metrics::<RustParser>( "fn f() { if a && b { // +2 (+1 &&) println!(\"test\"); } if c && d { // +2 (+1 &&) println!(\"test\"); } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn c_simple_function() { check_metrics::<CppParser>( "void f() { if (a && b) { // +2 (+1 &&) printf(\"test\"); } if (c && d) { // +2 (+1 &&) printf(\"test\"); } }", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn mozjs_simple_function() { check_metrics::<MozjsParser>( "function f() { if (a && b) { // +2 (+1 &&) window.print(\"test\"); } if (c && d) { // +2 (+1 &&) window.print(\"test\"); } }", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn python_sequence_same_booleans() { check_metrics::<PythonParser>( "def f(a, b): if a and b and True: # +2 (+1 sequence of and) return 1", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 2.0, "average": 2.0, "min": 0.0, "max": 2.0 }"### ); }, ); } #[test] fn rust_sequence_same_booleans() { check_metrics::<RustParser>( "fn f() { if a && b && true { // +2 (+1 sequence of &&) println!(\"test\"); } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 2.0, "average": 2.0, "min": 0.0, "max": 2.0 }"### ); }, ); check_metrics::<RustParser>( "fn f() { if a || b || c || d { // +2 (+1 sequence of ||) println!(\"test\"); } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 2.0, "average": 2.0, "min": 0.0, "max": 2.0 }"### ); }, ); } #[test] fn c_sequence_same_booleans() { check_metrics::<CppParser>( "void f() { if (a && b && 1 == 1) { // +2 (+1 sequence of &&) printf(\"test\"); } }", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 2.0, "average": 2.0, "min": 0.0, "max": 2.0 }"### ); }, ); check_metrics::<CppParser>( "void f() { if (a || b || c || d) { // +2 (+1 sequence of ||) printf(\"test\"); } }", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 2.0, "average": 2.0, "min": 0.0, "max": 2.0 }"### ); }, ); } #[test] fn mozjs_sequence_same_booleans() { check_metrics::<MozjsParser>( "function f() { if (a && b && 1 == 1) { // +2 (+1 sequence of &&) window.print(\"test\"); } }", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 2.0, "average": 2.0, "min": 0.0, "max": 2.0 }"### ); }, ); check_metrics::<MozjsParser>( "function f() { if (a || b || c || d) { // +2 (+1 sequence of ||) window.print(\"test\"); } }", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 2.0, "average": 2.0, "min": 0.0, "max": 2.0 }"### ); }, ); } #[test] fn rust_not_booleans() { check_metrics::<RustParser>( "fn f() { if !a && !b { // +2 (+1 &&) println!(\"test\"); } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 2.0, "average": 2.0, "min": 0.0, "max": 2.0 }"### ); }, ); check_metrics::<RustParser>( "fn f() { if a && !(b && c) { // +3 (+1 &&, +1 &&) println!(\"test\"); } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); check_metrics::<RustParser>( "fn f() { if !(a || b) && !(c || d) { // +4 (+1 ||, +1 &&, +1 ||) println!(\"test\"); } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn c_not_booleans() { check_metrics::<CppParser>( "void f() { if (a && !(b && c)) { // +3 (+1 &&, +1 &&) printf(\"test\"); } }", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); check_metrics::<CppParser>( "void f() { if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||) printf(\"test\"); } }", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn mozjs_not_booleans() { check_metrics::<MozjsParser>( "function f() { if (a && !(b && c)) { // +3 (+1 &&, +1 &&) window.print(\"test\"); } }", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); check_metrics::<MozjsParser>( "function f() { if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||) window.print(\"test\"); } }", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn python_sequence_different_booleans() { check_metrics::<PythonParser>( "def f(a, b): if a and b or True: # +3 (+1 and, +1 or) return 1", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn rust_sequence_different_booleans() { check_metrics::<RustParser>( "fn f() { if a && b || true { // +3 (+1 &&, +1 ||) println!(\"test\"); } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn c_sequence_different_booleans() { check_metrics::<CppParser>( "void f() { if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||) printf(\"test\"); } }", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn mozjs_sequence_different_booleans() { check_metrics::<MozjsParser>( "function f() { if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||) window.print(\"test\"); } }", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn python_formatted_sequence_different_booleans() { check_metrics::<PythonParser>( "def f(a, b): if ( # +1 a and b and # +1 (c or d) # +1 ): return 1", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn python_1_level_nesting() { check_metrics::<PythonParser>( "def f(a, b): if a: # +1 for i in range(b): # +2 return 1", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn rust_1_level_nesting() { check_metrics::<RustParser>( "fn f() { if true { // +1 if true { // +2 (nesting = 1) println!(\"test\"); } else if 1 == 1 { // +1 if true { // +3 (nesting = 2) println!(\"test\"); } } else { // +1 if true { // +3 (nesting = 2) println!(\"test\"); } } } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 11.0, "average": 11.0, "min": 0.0, "max": 11.0 }"### ); }, ); check_metrics::<RustParser>( "fn f() { if true { // +1 match true { // +2 (nesting = 1) true => println!(\"test\"), false => println!(\"test\"), } } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn c_1_level_nesting() { check_metrics::<CppParser>( "void f() { if (1 == 1) { // +1 if (1 == 1) { // +2 (nesting = 1) printf(\"test\"); } else if (1 == 1) { // +1 if (1 == 1) { // +3 (nesting = 2) printf(\"test\"); } } else { // +1 if (1 == 1) { // +3 (nesting = 2) printf(\"test\"); } } } }", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 11.0, "average": 11.0, "min": 0.0, "max": 11.0 }"### ); }, ); } #[test] fn mozjs_1_level_nesting() { check_metrics::<MozjsParser>( "function f() { if (1 == 1) { // +1 if (1 == 1) { // +2 (nesting = 1) window.print(\"test\"); } else if (1 == 1) { // +1 if (1 == 1) { // +3 (nesting = 2) window.print(\"test\"); } } else { // +1 if (1 == 1) { // +3 (nesting = 2) window.print(\"test\"); } } } }", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 11.0, "average": 11.0, "min": 0.0, "max": 11.0 }"### ); }, ); } #[test] fn python_2_level_nesting() { check_metrics::<PythonParser>( "def f(a, b): if a: # +1 for i in range(b): # +2 if b: # +3 return 1", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 6.0, "average": 6.0, "min": 0.0, "max": 6.0 }"### ); }, ); } #[test] fn rust_2_level_nesting() { check_metrics::<RustParser>( "fn f() { if true { // +1 for i in 0..4 { // +2 (nesting = 1) match true { // +3 (nesting = 2) true => println!(\"test\"), false => println!(\"test\"), } } } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 6.0, "average": 6.0, "min": 0.0, "max": 6.0 }"### ); }, ); } #[test] fn python_try_construct() { check_metrics::<PythonParser>( "def f(a, b): try: for foo in bar: # +1 return a except Exception: # +1 if a < 0: # +2 return a", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn mozjs_try_construct() { check_metrics::<MozjsParser>( "function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { for (const collector of this.collectors) { try { collector._onChannelRedirect(oldChannel, newChannel, flags); } catch (ex) { console.error( \"StackTraceCollector.onChannelRedirect threw an exception\", ex ); } } callback.onRedirectVerifyCallback(Cr.NS_OK); }", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn rust_break_continue() { // Only labeled break and continue statements are considered check_metrics::<RustParser>( "fn f() { 'tens: for ten in 0..3 { // +1 '_units: for unit in 0..=9 { // +2 (nesting = 1) if unit % 2 == 0 { // +3 (nesting = 2) continue; } else if unit == 5 { // +1 continue 'tens; // +1 } else if unit == 6 { // +1 break; } else { // +1 break 'tens; // +1 } } } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 11.0, "average": 11.0, "min": 0.0, "max": 11.0 }"### ); }, ); } #[test] fn c_goto() { check_metrics::<CppParser>( "void f() { OUT: for (int i = 1; i <= max; ++i) { // +1 for (int j = 2; j < i; ++j) { // +2 (nesting = 1) if (i % j == 0) { // +3 (nesting = 2) goto OUT; // +1 } } } }", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 7.0, "average": 7.0, "min": 0.0, "max": 7.0 }"### ); }, ); } #[test] fn c_switch() { check_metrics::<CppParser>( "void f() { switch (1) { // +1 case 1: printf(\"one\"); break; case 2: printf(\"two\"); break; case 3: printf(\"three\"); break; default: printf(\"all\"); break; } }", "foo.c", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 1.0, "average": 1.0, "min": 0.0, "max": 1.0 }"### ); }, ); } #[test] fn mozjs_switch() { check_metrics::<MozjsParser>( "function f() { switch (1) { // +1 case 1: window.print(\"one\"); break; case 2: window.print(\"two\"); break; case 3: window.print(\"three\"); break; default: window.print(\"all\"); break; } }", "foo.js", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 1.0, "average": 1.0, "min": 0.0, "max": 1.0 }"### ); }, ); } #[test] fn python_ternary_operator() { check_metrics::<PythonParser>( "def f(a, b): if a % 2: # +1 return 'c' if a else 'd' # +2 return 'a' if a else 'b' # +1", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn python_nested_functions_lambdas() { check_metrics::<PythonParser>( "def f(a, b): def foo(a): if a: # +2 (+1 nesting) return 1 # +3 (+1 for boolean sequence +2 for lambda nesting) bar = lambda a: lambda b: b or True or True return bar(foo(a))(a)", "foo.py", |metric| { // 2 functions + 2 lambdas = 4 insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 5.0, "average": 1.25, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn python_real_function() { check_metrics::<PythonParser>( "def process_raw_constant(constant, min_word_length): processed_words = [] raw_camelcase_words = [] for raw_word in re.findall(r'[a-z]+', constant): # +1 word = raw_word.strip() if ( # +2 (+1 if and +1 nesting) len(word) >= min_word_length and not (word.startswith('-') or word.endswith('-')) # +2 operators ): if is_camel_case_word(word): # +3 (+1 if and +2 nesting) raw_camelcase_words.append(word) else: # +1 else processed_words.append(word.lower()) return processed_words, raw_camelcase_words", "foo.py", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 9.0, "average": 9.0, "min": 0.0, "max": 9.0 }"### ); }, ); } #[test] fn rust_if_let_else_if_else() { check_metrics::<RustParser>( "pub fn create_usage_no_title(p: &Parser, used: &[&str]) -> String { debugln!(\"usage::create_usage_no_title;\"); if let Some(u) = p.meta.usage_str { // +1 String::from(&*u) } else if used.is_empty() { // +1 create_help_usage(p, true) } else { // +1 create_smart_usage(p, used) } }", "foo.rs", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } #[test] fn typescript_if_else_if_else() { check_metrics::<TypescriptParser>( "function foo() { if (this._closed) return Promise.resolve(); // +1 if (this._tempDirectory) { // +1 this.kill(); } else if (this.connection) { // +1 this.kill(); } else { // +1 throw new Error(`Error`); } helper.removeEventListeners(this._listeners); return this._processClosing; }", "foo.ts", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn java_no_cognitive() { check_metrics::<JavaParser>("int a = 42;", "foo.java", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 0.0, "average": null, "min": 0.0, "max": 0.0 } "### ); }); } #[test] fn java_single_branch_function() { check_metrics::<JavaParser>( "class X { public static void print(boolean a){ if(a){ // +1 System.out.println(\"test1\"); } } }", "foo.java", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 1.0, "average": 1.0, "min": 0.0, "max": 1.0 } "### ); }, ); } #[test] fn java_multiple_branch_function() { check_metrics::<JavaParser>( "class X { public static void print(boolean a, boolean b){ if(a){ // +1 System.out.println(\"test1\"); } if(b){ // +1 System.out.println(\"test2\"); } else { // +1 System.out.println(\"test3\"); } } }", "foo.java", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 } "### ); }, ); } #[test] fn java_compound_conditions() { check_metrics::<JavaParser>( "class X { public static void print(boolean a, boolean b, boolean c, boolean d){ if(a && b){ // +2 (+1 &&) System.out.println(\"test1\"); } if(c && d){ // +2 (+1 &&) System.out.println(\"test2\"); } } }", "foo.java", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 4.0, "average": 4.0, "min": 0.0, "max": 4.0 }"### ); }, ); } #[test] fn java_switch_statement() { check_metrics::<JavaParser>( "class X { public static void print(boolean a, boolean b, boolean c, boolean d){ switch(expr){ //+1 case 1: System.out.println(\"test1\"); break; case 2: System.out.println(\"test2\"); break; default: System.out.println(\"test\"); } } }", "foo.java", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 1.0, "average": 1.0, "min": 0.0, "max": 1.0 }"### ); }, ); } #[test] fn java_switch_expression() { check_metrics::<JavaParser>( "class X { public static void print(boolean a, boolean b, boolean c, boolean d){ switch(expr){ // +1 case 1 -> System.out.println(\"test1\"); case 2 -> System.out.println(\"test2\"); default -> System.out.println(\"test\"); } } }", "foo.java", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 1.0, "average": 1.0, "min": 0.0, "max": 1.0 }"### ); }, ); } #[test] fn java_not_booleans() { check_metrics::<JavaParser>( "class X { public static void print(boolean a, boolean b, boolean c, boolean d){ if (a && !(b && c)) { // +3 (+1 &&, +1 &&) printf(\"test\"); } } }", "foo.java", |metric| { insta::assert_json_snapshot!( metric.cognitive, @r###" { "sum": 3.0, "average": 3.0, "min": 0.0, "max": 3.0 }"### ); }, ); } }