diff --git a/rust-code-analysis-cli/src/web/server.rs b/rust-code-analysis-cli/src/web/server.rs index 85682ffc6..70a244f10 100644 --- a/rust-code-analysis-cli/src/web/server.rs +++ b/rust-code-analysis-cli/src/web/server.rs @@ -632,6 +632,7 @@ mod tests { "start_line": 1, "end_line": 4, "metrics": {"cyclomatic": {"sum": 2.0, "average": 1.0}, + "cognitive": 0.0, "nargs": 0., "nexits": 0., "halstead": {"bugs": 0.000_942_552_557_372_941_4, @@ -658,6 +659,7 @@ mod tests { "start_line": 3, "end_line": 4, "metrics": {"cyclomatic": {"sum": 1.0, "average": 1.0}, + "cognitive": 0.0, "nargs": 0., "nexits": 0., "halstead": {"bugs": 0.000_942_552_557_372_941_4, @@ -710,6 +712,7 @@ mod tests { "start_line": 1, "end_line": 2, "metrics": {"cyclomatic": {"sum": 2.0, "average": 1.0}, + "cognitive": 0.0, "nargs": 0., "nexits": 0., "halstead": {"bugs": 0.000_942_552_557_372_941_4, @@ -758,6 +761,7 @@ mod tests { "start_line": 1, "end_line": 2, "metrics": {"cyclomatic": {"sum": 2.0, "average": 1.0}, + "cognitive": 0.0, "nargs": 0., "nexits": 0., "halstead": {"bugs": 0.000_942_552_557_372_941_4, @@ -784,6 +788,7 @@ mod tests { "start_line": 1, "end_line": 2, "metrics": {"cyclomatic": {"sum": 1.0, "average": 1.0}, + "cognitive": 0.0, "nargs": 0., "nexits": 0., "halstead": {"bugs": 0.000_942_552_557_372_941_4, diff --git a/src/asttools.rs b/src/asttools.rs index 56703f810..76538f06a 100644 --- a/src/asttools.rs +++ b/src/asttools.rs @@ -17,7 +17,7 @@ pub fn get_parent<'a>(node: &'a Node<'a>, level: usize) -> Option> { } #[doc(hidden)] -#[macro_export] +#[macro_use] macro_rules! has_ancestors { ($node:expr, $( $typs:pat )|*, $( $typ:pat ),+) => {{ let mut res = false; @@ -54,3 +54,25 @@ macro_rules! has_ancestors { res }}; } + +#[doc(hidden)] +#[macro_use] +macro_rules! count_specific_ancestors { + ($node:expr, $( $typs:pat )|*, $( $stops:pat )|*) => {{ + let mut count = 0; + let mut node = *$node; + while let Some(parent) = node.object().parent() { + match parent.kind_id().into() { + $( $typs )|* => { + if !Self::is_else_if(&Node::new(parent)) { + count += 1; + } + }, + $( $stops )|* => break, + _ => {} + } + node = Node::new(parent); + } + count + }}; +} diff --git a/src/checker.rs b/src/checker.rs index 701a66584..21ccfb8fe 100644 --- a/src/checker.rs +++ b/src/checker.rs @@ -11,6 +11,11 @@ pub trait Checker { false } + #[inline(always)] + fn is_else_if(_: &Node) -> bool { + false + } + fn is_string(node: &Node) -> bool; fn is_call(node: &Node) -> bool; fn is_func(node: &Node) -> bool; @@ -81,6 +86,8 @@ impl Checker for CppCode { let code = &code[node.object().start_byte()..node.object().end_byte()]; AC.is_match(code) } + + mk_else_if!(IfStatement); mk_checker!(is_non_arg, LPAREN, LPAREN2, COMMA, RPAREN); } @@ -161,6 +168,8 @@ impl Checker for MozjsCode { ClassDeclaration, ArrowFunction ); + + mk_else_if!(IfStatement); mk_checker!(is_non_arg, LPAREN, COMMA, RPAREN); } @@ -189,6 +198,7 @@ impl Checker for JavascriptCode { ClassDeclaration, ArrowFunction ); + mk_else_if!(IfStatement); mk_checker!(is_non_arg, LPAREN, COMMA, RPAREN); } @@ -217,6 +227,7 @@ impl Checker for TypescriptCode { ClassDeclaration, ArrowFunction ); + mk_else_if!(IfStatement); mk_checker!(is_non_arg, LPAREN, COMMA, RPAREN); } @@ -246,6 +257,7 @@ impl Checker for TsxCode { ClassDeclaration, ArrowFunction ); + mk_else_if!(IfStatement); mk_checker!(is_non_arg, LPAREN, COMMA, RPAREN); } @@ -296,6 +308,7 @@ impl Checker for RustCode { code.starts_with(b"/// cbindgen:") } + mk_else_if!(IfExpression); mk_checker!(is_string, StringLiteral, RawStringLiteral); mk_checker!(is_call, CallExpression); mk_checker!(is_func, FunctionItem, ClosureExpression); diff --git a/src/macros.rs b/src/macros.rs index e66871043..3ea90be79 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -14,6 +14,24 @@ macro_rules! mk_checker { }; } +#[doc(hidden)] +#[macro_export] +macro_rules! mk_else_if { + ($if_type:ident) => { + #[inline(always)] + fn is_else_if(node: &Node) -> bool { + if node.object().kind_id() != ::BaseLang::$if_type { + return false; + } + if let Some(parent) = node.object().parent() { + return node.object().kind_id() == ::BaseLang::$if_type + && parent.kind_id() == ::BaseLang::$if_type; + } + false + } + }; +} + #[doc(hidden)] #[macro_export] macro_rules! mk_extern { diff --git a/src/metrics/cognitive.rs b/src/metrics/cognitive.rs new file mode 100644 index 000000000..d648933c2 --- /dev/null +++ b/src/metrics/cognitive.rs @@ -0,0 +1,1006 @@ +use serde::ser::Serializer; +use serde::Serialize; +use std::fmt; + +use crate::checker::Checker; +use crate::*; + +// TODO: Find a way to increment the cognitive complexity value +// for recursive code. For some kind of langauges, 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, + nesting: usize, + boolean_seq: BoolSequence, +} + +impl Default for Stats { + fn default() -> Self { + Self { + structural: 0, + nesting: 0, + boolean_seq: BoolSequence::default(), + } + } +} + +impl Serialize for Stats { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_f64(self.cognitive()) + } +} + +impl fmt::Display for Stats { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.cognitive()) + } +} + +impl Stats { + /// Merges a second `Cognitive Complexity` metric into the first one + pub fn merge(&mut self, other: &Stats) { + self.structural += other.structural; + } + + /// Returns the `Cognitive Complexity` metric value + pub fn cognitive(&self) -> f64 { + self.structural as f64 + } +} + +#[doc(hidden)] +pub trait Cognitive +where + Self: Checker, +{ + fn compute(_node: &Node, _stats: &mut Stats) {} +} + +macro_rules! compute_booleans { + ($node: ident, $stats: ident, $( $typs: pat )|*) => { + let mut cursor = $node.object().walk(); + for child in $node.object().children(&mut cursor) { + if let $( $typs )|* = child.kind_id().into() { + $stats.structural = $stats + .boolean_seq + .eval_based_on_prev(child.kind_id(), $stats.structural); + } + } + }; +} + +macro_rules! nesting_levels { + ($node: ident, $stats: ident, [$nest_func: pat => $nest_func_stop: pat], + [$lambdas: pat => $( $lambdas_stop: pat )|*], + [$( $nest_level: pat )|* => $( $nest_level_stop: pat )|*]) => { + // Find the depth of a function (the most external function is + // not considered) + $stats.nesting = count_specific_ancestors!($node, $nest_func, $nest_func_stop).max(1) - 1; + + // Find the depth of a lambda + let lambda_depth = count_specific_ancestors!($node, $lambdas, $( $lambdas_stop )|*); + + // Find the nesting operator level + $stats.nesting += lambda_depth + + count_specific_ancestors!( + $node, + $( $nest_level )|*, + $( $nest_level_stop)|* + ); + + // Reset the boolean sequence + $stats.boolean_seq.reset(); + + increment($stats); + }; + ($node: ident, $stats: ident, + [$lambdas: pat => $( $lambdas_stop: pat )|*], + [$( $nest_level: pat )|* => $( $nest_level_stop: pat )|*]) => { + // Find the depth of a lambda + let lambda_depth = count_specific_ancestors!($node, $lambdas, $( $lambdas_stop )|*); + + // Find the nesting operator level + $stats.nesting = lambda_depth + + count_specific_ancestors!( + $node, + $( $nest_level )|*, + $( $nest_level_stop)|* + ); + + // Reset the boolean sequence + $stats.boolean_seq.reset(); + + increment($stats); + }; +} + +#[derive(Debug, Default, Clone)] +struct BoolSequence { + boolean_op: Option, + first_boolean: bool, +} + +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 { + let new_structural = 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 + }; + new_structural + } +} + +#[inline(always)] +fn increment(stats: &mut Stats) { + stats.structural += stats.nesting + 1; +} + +#[inline(always)] +fn increment_by_one(stats: &mut Stats) { + stats.structural += 1; +} + +impl Cognitive for PythonCode { + fn compute(node: &Node, stats: &mut Stats) { + use Python::*; + + match node.object().kind_id().into() { + IfStatement | ForStatement | WhileStatement | ConditionalExpression => { + nesting_levels!( + node, stats, + [FunctionDefinition => Module], + [Lambda => FunctionDefinition | Module], + [IfStatement | ForStatement | WhileStatement | ExceptClause => FunctionDefinition] + ); + } + ElifClause | ElseClause | FinallyClause => { + // No nesting increment for them because their cost has already + // been paid by the if construct + increment_by_one(stats); + } + ExceptClause => { + increment(stats); + } + ExpressionList => { + stats.boolean_seq.reset(); + } + NotOperator => { + stats.boolean_seq.not_operator(node.object().kind_id()); + } + BooleanOperator => { + if count_specific_ancestors!(node, BooleanOperator, Lambda) == 0 { + stats.structural += count_specific_ancestors!( + node, + Lambda, + ExpressionList | IfStatement | ForStatement | WhileStatement + ); + } + compute_booleans!(node, stats, And | Or); + } + _ => {} + } + } +} + +impl Cognitive for RustCode { + fn compute(node: &Node, stats: &mut Stats) { + use Rust::*; + + //TODO: Implement macros + + match node.object().kind_id().into() { + IfExpression => { + // Check if a node is not an else-if + if !Self::is_else_if(&node) { + nesting_levels!( + node, stats, + [FunctionItem => SourceFile], + [ClosureExpression => SourceFile], + [IfExpression | ForExpression | WhileExpression | MatchExpression => FunctionItem] + ); + } + } + ForExpression | WhileExpression | MatchExpression => { + nesting_levels!( + node, stats, + [FunctionItem => SourceFile], + [ClosureExpression => SourceFile], + [IfExpression | ForExpression | WhileExpression | MatchExpression => FunctionItem] + ); + } + Else /*else-if also */ => { + increment_by_one(stats); + } + BreakExpression | ContinueExpression => { + if let Some(label_child) = node.object().child(1) { + if let LoopLabel = label_child.kind_id().into() { + increment_by_one(stats); + } + } + } + UnaryExpression => { + stats.boolean_seq.not_operator(node.object().kind_id()); + } + BinaryExpression => { + compute_booleans!(node, stats, AMPAMP | PIPEPIPE); + } + _ => {} + } + } +} + +impl Cognitive for CppCode { + fn compute(node: &Node, stats: &mut Stats) { + use Cpp::*; + + //TODO: Implement macros + + match node.object().kind_id().into() { + IfStatement => { + if !Self::is_else_if(&node) { + nesting_levels!( + node, stats, + [LambdaExpression => TranslationUnit], + [IfStatement + | ForStatement + | WhileStatement + | DoStatement + | SwitchStatement + | CatchClause => FunctionDefinition] + ); + } + } + ForStatement | WhileStatement | DoStatement | SwitchStatement | CatchClause => { + nesting_levels!( + node, stats, + [LambdaExpression => TranslationUnit], + [IfStatement + | ForStatement + | WhileStatement + | DoStatement + | SwitchStatement + | CatchClause => FunctionDefinition] + ); + } + GotoStatement | Else /* else-if also */ => { + increment_by_one(stats); + } + UnaryExpression2 => { + stats.boolean_seq.not_operator(node.object().kind_id()); + } + BinaryExpression2 => { + compute_booleans!(node, stats, AMPAMP | PIPEPIPE); + } + _ => {} + } + } +} + +macro_rules! js_cognitive { + ($lang:ident) => { + fn compute(node: &Node, stats: &mut Stats) { + use $lang::*; + + match node.object().kind_id().into() { + IfStatement => { + if !Self::is_else_if(&node) { + nesting_levels!( + node, stats, + [FunctionDeclaration => Program], + [ArrowFunction => FunctionDeclaration | Program], + [IfStatement + | ForStatement + | ForInStatement + | WhileStatement + | DoStatement + | SwitchStatement + | CatchClause + | TernaryExpression => FunctionDeclaration] + ); + } + } + ForStatement | ForInStatement | WhileStatement | DoStatement | SwitchStatement | CatchClause | TernaryExpression => { + nesting_levels!( + node, stats, + [FunctionDeclaration => Program], + [ArrowFunction => FunctionDeclaration | Program], + [IfStatement + | ForStatement + | ForInStatement + | WhileStatement + | DoStatement + | SwitchStatement + | CatchClause + | TernaryExpression => FunctionDeclaration] + ); + } + Else /* else-if also */ => { + increment_by_one(stats); + } + ExpressionStatement => { + // Reset the boolean sequence + stats.boolean_seq.reset(); + } + UnaryExpression => { + stats.boolean_seq.not_operator(node.object().kind_id()); + } + BinaryExpression => { + compute_booleans!(node, stats, AMPAMP | PIPEPIPE); + } + _ => {} + } + } + }; +} + +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 PreprocCode {} +impl Cognitive for CcommentCode {} +impl Cognitive for CSharpCode {} +impl Cognitive for JavaCode {} +impl Cognitive for GoCode {} +impl Cognitive for CssCode {} +impl Cognitive for HtmlCode {} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + #[test] + fn test_simple_cognitive() { + check_metrics!( + "def f(a, b): + if a and b: # +2 (+1 and) + return 1 + if c and d: # +2 (+1 and) + return 1", + "foo.py", + PythonParser, + cognitive, + [(cognitive, 4, usize)] + ); + + check_metrics!( + "fn f() { + if a && b { // +2 (+1 &&) + println!(\"test\"); + } + if c && d { // +2 (+1 &&) + println!(\"test\"); + } + }", + "foo.rs", + RustParser, + cognitive, + [(cognitive, 4, usize)] + ); + + check_metrics!( + "void f() { + if (a && b) { // +2 (+1 &&) + printf(\"test\"); + } + if (c && d) { // +2 (+1 &&) + printf(\"test\"); + } + }", + "foo.c", + CppParser, + cognitive, + [(cognitive, 4, usize)] + ); + + check_metrics!( + "function f() { + if (a && b) { // +2 (+1 &&) + window.print(\"test\"); + } + if (c && d) { // +2 (+1 &&) + window.print(\"test\"); + } + }", + "foo.js", + MozjsParser, + cognitive, + [(cognitive, 4, usize)] + ); + } + + #[test] + fn test_sequence_same_booleans_cognitive() { + check_metrics!( + "def f(a, b): + if a and b and True: # +2 (+1 sequence of and) + return 1", + "foo.py", + PythonParser, + cognitive, + [(cognitive, 2, usize)] + ); + + check_metrics!( + "fn f() { + if a && b && true { // +2 (+1 sequence of &&) + println!(\"test\"); + } + }", + "foo.rs", + RustParser, + cognitive, + [(cognitive, 2, usize)] + ); + + check_metrics!( + "void f() { + if (a && b && 1 == 1) { // +2 (+1 sequence of &&) + printf(\"test\"); + } + }", + "foo.c", + CppParser, + cognitive, + [(cognitive, 2, usize)] + ); + + check_metrics!( + "function f() { + if (a && b && 1 == 1) { // +2 (+1 sequence of &&) + window.print(\"test\"); + } + }", + "foo.js", + MozjsParser, + cognitive, + [(cognitive, 2, usize)] + ); + + check_metrics!( + "fn f() { + if a || b || c || d { // +2 (+1 sequence of ||) + println!(\"test\"); + } + }", + "foo.rs", + RustParser, + cognitive, + [(cognitive, 2, usize)] + ); + + check_metrics!( + "void f() { + if (a || b || c || d) { // +2 (+1 sequence of ||) + printf(\"test\"); + } + }", + "foo.c", + CppParser, + cognitive, + [(cognitive, 2, usize)] + ); + + check_metrics!( + "function f() { + if (a || b || c || d) { // +2 (+1 sequence of ||) + window.print(\"test\"); + } + }", + "foo.js", + MozjsParser, + cognitive, + [(cognitive, 2, usize)] + ); + } + + #[test] + fn test_not_booleans_cognitive() { + check_metrics!( + "fn f() { + if !a && !b { // +2 (+1 &&) + println!(\"test\"); + } + }", + "foo.rs", + RustParser, + cognitive, + [(cognitive, 2, usize)] + ); + + check_metrics!( + "fn f() { + if a && !(b && c) { // +3 (+1 &&, +1 &&) + println!(\"test\"); + } + }", + "foo.rs", + RustParser, + cognitive, + [(cognitive, 3, usize)] + ); + + check_metrics!( + "void f() { + if (a && !(b && c)) { // +3 (+1 &&, +1 &&) + printf(\"test\"); + } + }", + "foo.c", + CppParser, + cognitive, + [(cognitive, 3, usize)] + ); + + check_metrics!( + "function f() { + if (a && !(b && c)) { // +3 (+1 &&, +1 &&) + window.print(\"test\"); + } + }", + "foo.js", + MozjsParser, + cognitive, + [(cognitive, 3, usize)] + ); + + check_metrics!( + "fn f() { + if !(a || b) && !(c || d) { // +4 (+1 ||, +1 &&, +1 ||) + println!(\"test\"); + } + }", + "foo.rs", + RustParser, + cognitive, + [(cognitive, 4, usize)] + ); + + check_metrics!( + "void f() { + if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||) + printf(\"test\"); + } + }", + "foo.c", + CppParser, + cognitive, + [(cognitive, 4, usize)] + ); + + check_metrics!( + "function f() { + if (!(a || b) && !(c || d)) { // +4 (+1 ||, +1 &&, +1 ||) + window.print(\"test\"); + } + }", + "foo.js", + MozjsParser, + cognitive, + [(cognitive, 4, usize)] + ); + } + + #[test] + fn test_sequence_different_booleans_cognitive() { + check_metrics!( + "def f(a, b): + if a and b or True: # +3 (+1 and, +1 or) + return 1", + "foo.py", + PythonParser, + cognitive, + [(cognitive, 3, usize)] + ); + + check_metrics!( + "fn f() { + if a && b || true { // +3 (+1 &&, +1 ||) + println!(\"test\"); + } + }", + "foo.rs", + RustParser, + cognitive, + [(cognitive, 3, usize)] + ); + + check_metrics!( + "void f() { + if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||) + printf(\"test\"); + } + }", + "foo.c", + CppParser, + cognitive, + [(cognitive, 3, usize)] + ); + + check_metrics!( + "function f() { + if (a && b || 1 == 1) { // +3 (+1 &&, +1 ||) + window.print(\"test\"); + } + }", + "foo.js", + MozjsParser, + cognitive, + [(cognitive, 3, usize)] + ); + } + + #[test] + fn test_formatted_sequence_different_booleans_cognitive() { + check_metrics!( + "def f(a, b): + if ( # +1 + a and b and # +1 + (c or d) # +1 + ): + return 1", + "foo.py", + PythonParser, + cognitive, + [(cognitive, 3, usize)] + ); + } + + #[test] + fn test_1_level_nesting_cognitive() { + check_metrics!( + "def f(a, b): + if a: # +1 + for i in range(b): # +2 + return 1", + "foo.py", + PythonParser, + cognitive, + [(cognitive, 3, usize)] + ); + + check_metrics!( + "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", + RustParser, + cognitive, + [(cognitive, 11, usize)] + ); + + check_metrics!( + "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", + CppParser, + cognitive, + [(cognitive, 11, usize)] + ); + + check_metrics!( + "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", + MozjsParser, + cognitive, + [(cognitive, 11, usize)] + ); + + check_metrics!( + "fn f() { + if true { // +1 + match true { // +2 (nesting = 1) + true => println!(\"test\"), + false => println!(\"test\"), + } + } + }", + "foo.rs", + RustParser, + cognitive, + [(cognitive, 3, usize)] + ); + } + + #[test] + fn test_2_level_nesting_cognitive() { + check_metrics!( + "def f(a, b): + if a: # +1 + for i in range(b): # +2 + if b: # +3 + return 1", + "foo.py", + PythonParser, + cognitive, + [(cognitive, 6, usize)] + ); + + check_metrics!( + "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", + RustParser, + cognitive, + [(cognitive, 6, usize)] + ); + } + + #[test] + fn test_try_construct_cognitive() { + check_metrics!( + "def f(a, b): + try: + for foo in bar: # +1 + return a + except Exception: # +1 + if a < 0: # +2 + return a", + "foo.py", + PythonParser, + cognitive, + [(cognitive, 4, usize)] + ); + + check_metrics!( + "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", + JavascriptParser, + cognitive, + [(cognitive, 3, usize)] + ); + } + + #[test] + fn test_break_continue_cognitive() { + // Only labeled break and continue statements are considered + check_metrics!( + "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", + RustParser, + cognitive, + [(cognitive, 11, usize)] + ); + } + + #[test] + fn test_goto_cognitive() { + check_metrics!( + "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", + CppParser, + cognitive, + [(cognitive, 7, usize)] + ); + } + + #[test] + fn test_switch_cognitive() { + check_metrics!( + "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", + CppParser, + cognitive, + [(cognitive, 1, usize)] + ); + + check_metrics!( + "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", + MozjsParser, + cognitive, + [(cognitive, 1, usize)] + ); + } + + #[test] + fn test_ternary_operator_cognitive() { + check_metrics!( + "def f(a, b): + if a % 2: # +1 + return 'c' if a else 'd' # +2 + return 'a' if a else 'b' # +1", + "foo.py", + PythonParser, + cognitive, + [(cognitive, 4, usize)] + ); + } + + #[test] + fn test_nested_functions_cognitive() { + check_metrics!( + "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", + PythonParser, + cognitive, + [(cognitive, 5, usize)] + ); + } + + #[test] + fn test_real_function_cognitive() { + check_metrics!( + "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", + PythonParser, + cognitive, + [(cognitive, 9, usize)] + ); + } +} diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs index 275f6557b..d16f0322a 100644 --- a/src/metrics/mod.rs +++ b/src/metrics/mod.rs @@ -1,3 +1,4 @@ +pub mod cognitive; pub mod cyclomatic; pub mod exit; pub mod fn_args; diff --git a/src/output/dump_metrics.rs b/src/output/dump_metrics.rs index cf44ebf09..f9d58f978 100644 --- a/src/output/dump_metrics.rs +++ b/src/output/dump_metrics.rs @@ -1,6 +1,7 @@ use std::io::Write; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor}; +use crate::cognitive; use crate::cyclomatic; use crate::exit; use crate::fn_args; @@ -98,6 +99,7 @@ fn dump_metrics( writeln!(stdout, "metrics")?; let prefix = format!("{}{}", prefix, pref_child); + dump_cognitive(&metrics.cognitive, &prefix, false, stdout)?; dump_cyclomatic(&metrics.cyclomatic, &prefix, false, stdout)?; dump_nargs(&metrics.nargs, &prefix, false, stdout)?; dump_nexits(&metrics.nexits, &prefix, false, stdout)?; @@ -107,6 +109,24 @@ fn dump_metrics( dump_mi(&metrics.mi, &prefix, true, stdout) } +fn dump_cognitive( + stats: &cognitive::Stats, + prefix: &str, + last: bool, + stdout: &mut StandardStreamLock, +) -> std::io::Result<()> { + let pref = if last { "`- " } else { "|- " }; + + color!(stdout, Blue); + write!(stdout, "{}{}", prefix, pref)?; + + color!(stdout, Green, true); + write!(stdout, "cognitive: ")?; + + color!(stdout, White); + writeln!(stdout, "{}", stats.cognitive()) +} + fn dump_cyclomatic( stats: &cyclomatic::Stats, prefix: &str, diff --git a/src/parser.rs b/src/parser.rs index a73198942..5bb3f7c07 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -68,6 +68,7 @@ impl Pars { type Checker = T; type Getter = T; + type Cognitive = T; type Cyclomatic = T; type Halstead = T; type Loc = T; diff --git a/src/spaces.rs b/src/spaces.rs index fb93fc672..8875c49c8 100644 --- a/src/spaces.rs +++ b/src/spaces.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use crate::checker::Checker; use crate::node::Node; +use crate::cognitive::{self, Cognitive}; use crate::cyclomatic::{self, Cyclomatic}; use crate::exit::{self, Exit}; use crate::fn_args::{self, NArgs}; @@ -62,6 +63,7 @@ pub struct CodeMetrics { pub nargs: fn_args::Stats, /// `NExits` data pub nexits: exit::Stats, + pub cognitive: cognitive::Stats, /// `Cyclomatic` data pub cyclomatic: cyclomatic::Stats, /// `Halstead` data @@ -77,6 +79,7 @@ pub struct CodeMetrics { impl Default for CodeMetrics { fn default() -> Self { Self { + cognitive: cognitive::Stats::default(), cyclomatic: cyclomatic::Stats::default(), halstead: halstead::Stats::default(), loc: loc::Stats::default(), @@ -92,6 +95,7 @@ impl fmt::Display for CodeMetrics { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}", self.nargs)?; writeln!(f, "{}", self.nexits)?; + writeln!(f, "{}", self.cognitive)?; writeln!(f, "{}", self.cyclomatic)?; writeln!(f, "{}", self.halstead)?; writeln!(f, "{}", self.loc)?; @@ -102,6 +106,7 @@ impl fmt::Display for CodeMetrics { impl CodeMetrics { pub fn merge(&mut self, other: &CodeMetrics) { + self.cognitive.merge(&other.cognitive); self.cyclomatic.merge(&other.cyclomatic); self.halstead.merge(&other.halstead); self.loc.merge(&other.loc); @@ -261,6 +266,7 @@ pub fn metrics<'a, T: ParserTrait>(parser: &'a T, path: &'a PathBuf) -> Option