use serde::ser::Serializer;
use serde::Serialize;
use std::fmt;
use crate::checker::Checker;
use crate::*;
#[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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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 {
pub fn merge(&mut self, other: &Stats) {
self.structural += other.structural;
}
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 )|*]) => {
$stats.nesting = count_specific_ancestors!($node, $nest_func, $nest_func_stop).max(1) - 1;
let lambda_depth = count_specific_ancestors!($node, $lambdas, $( $lambdas_stop )|*);
$stats.nesting += lambda_depth
+ count_specific_ancestors!(
$node,
$( $nest_level )|*,
$( $nest_level_stop)|*
);
$stats.boolean_seq.reset();
increment($stats);
};
($node: ident, $stats: ident,
[$lambdas: pat => $( $lambdas_stop: pat )|*],
[$( $nest_level: pat )|* => $( $nest_level_stop: pat )|*]) => {
let lambda_depth = count_specific_ancestors!($node, $lambdas, $( $lambdas_stop )|*);
$stats.nesting = lambda_depth
+ count_specific_ancestors!(
$node,
$( $nest_level )|*,
$( $nest_level_stop)|*
);
$stats.boolean_seq.reset();
increment($stats);
};
}
#[derive(Debug, Default, Clone)]
struct BoolSequence {
boolean_op: Option<u16>,
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 {
structural + 1
} else {
structural
}
} else {
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 => {
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::*;
match node.object().kind_id().into() {
IfExpression => {
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 => {
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::*;
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 => {
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 => {
increment_by_one(stats);
}
ExpressionStatement => {
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() {
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)]
);
}
}