use crate::ir::Node;
use crate::report::{FileReport, FunctionReport};
struct Frame {
name: String,
kind: String,
line: u32,
cognitive: u32,
cyclomatic: u32,
nesting: u32,
children: Vec<FunctionReport>,
}
struct Engine {
stack: Vec<Frame>,
}
impl Engine {
fn new() -> Self {
let module = Frame {
name: "<module>".to_string(),
kind: "module".to_string(),
line: 1,
cognitive: 0,
cyclomatic: 0,
nesting: 0,
children: Vec::new(),
};
Self {
stack: vec![module],
}
}
fn top(&mut self) -> &mut Frame {
self.stack.last_mut().expect("stack never empty")
}
fn top_nesting(&self) -> u32 {
self.stack.last().expect("stack never empty").nesting
}
fn add_cognitive(&mut self, amount: u32) {
self.top().cognitive += amount;
}
fn add_cyclomatic(&mut self) {
self.top().cyclomatic += 1;
}
fn enter_nesting(&mut self) {
self.top().nesting += 1;
}
fn leave_nesting(&mut self) {
self.top().nesting -= 1;
}
fn walk(&mut self, nodes: &[Node]) {
for node in nodes {
self.visit(node);
}
}
fn nested(&mut self, add_cyclomatic: bool, body: &[Node]) {
let n = self.top_nesting();
self.add_cognitive(1 + n);
if add_cyclomatic {
self.add_cyclomatic();
}
self.enter_nesting();
self.walk(body);
self.leave_nesting();
}
fn visit(&mut self, node: &Node) {
match node {
Node::Function {
name,
kind,
line,
body,
} => self.visit_function(name, kind, *line, body),
Node::Branch {
test,
then,
alternate,
} => self.visit_branch(test, then, alternate),
Node::Conditional {
test,
then,
alternate,
} => {
let n = self.top_nesting();
self.add_cognitive(1 + n);
self.add_cyclomatic();
self.walk(test);
self.enter_nesting();
self.walk(then);
self.walk(alternate);
self.leave_nesting();
}
Node::Loop { body } => self.nested(true, body),
Node::Catch { body } => self.nested(true, body),
Node::Switch { cases } => {
let n = self.top_nesting();
self.add_cognitive(1 + n);
self.enter_nesting();
for case in cases {
if !case.is_default {
self.add_cyclomatic(); }
self.walk(&case.body);
}
self.leave_nesting();
}
Node::Jump { labeled } => {
if *labeled {
self.add_cognitive(1);
}
}
Node::Logical { operands, .. } => self.visit_logical(operands),
Node::Call { callee } => self.visit_call(callee.as_deref()),
Node::Group(children) => self.walk(children),
}
}
fn visit_function(&mut self, name: &str, kind: &str, line: u32, body: &[Node]) {
self.stack.push(Frame {
name: name.to_string(),
kind: kind.to_string(),
line,
cognitive: 0,
cyclomatic: 1, nesting: 0,
children: Vec::new(),
});
self.walk(body);
let frame = self.stack.pop().expect("function frame");
let report = FunctionReport {
name: frame.name,
kind: frame.kind,
line: frame.line,
cognitive: frame.cognitive,
cyclomatic: frame.cyclomatic,
children: frame.children,
};
self.top().children.push(report);
}
fn visit_branch(&mut self, test: &[Node], then: &[Node], alternate: &Option<Box<Node>>) {
let n = self.top_nesting();
self.add_cognitive(1 + n);
self.add_cyclomatic();
self.walk(test);
self.enter_nesting();
self.walk(then);
self.leave_nesting();
self.visit_alternate(alternate);
}
fn visit_alternate(&mut self, alternate: &Option<Box<Node>>) {
let Some(node) = alternate else { return };
match node.as_ref() {
Node::Branch {
test,
then,
alternate,
} => {
self.add_cognitive(1);
self.add_cyclomatic();
self.walk(test);
self.enter_nesting();
self.walk(then);
self.leave_nesting();
self.visit_alternate(alternate);
}
other => {
self.add_cognitive(1);
self.enter_nesting();
self.visit(other);
self.leave_nesting();
}
}
}
fn visit_logical(&mut self, operands: &[Node]) {
self.add_cognitive(1);
for _ in 1..operands.len() {
self.add_cyclomatic();
}
self.walk(operands);
}
fn visit_call(&mut self, callee: Option<&str>) {
if let Some(name) = callee
&& let Some(top) = self.stack.last()
&& top.kind != "module"
&& top.name == name
{
self.add_cognitive(1); }
}
}
fn sum_tree(fns: &[FunctionReport], cog: &mut u32, cyc: &mut u32) {
for f in fns {
*cog += f.cognitive;
*cyc += f.cyclomatic;
sum_tree(&f.children, cog, cyc);
}
}
pub fn analyze(path: &str, nodes: &[Node], parse_errors: Vec<String>) -> FileReport {
let mut engine = Engine::new();
engine.walk(nodes);
let module = engine.stack.pop().expect("module frame");
let functions = module.children;
let mut cognitive = module.cognitive;
let mut cyclomatic = module.cyclomatic;
sum_tree(&functions, &mut cognitive, &mut cyclomatic);
FileReport {
path: path.to_string(),
cognitive,
cyclomatic,
functions,
parse_errors,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{LogicalOp, Node, SwitchCase};
fn func(name: &str, kind: &str, body: Vec<Node>) -> Node {
Node::Function {
name: name.into(),
kind: kind.into(),
line: 1,
body,
}
}
fn find<'a>(fns: &'a [FunctionReport], name: &str) -> Option<&'a FunctionReport> {
for f in fns {
if f.name == name {
return Some(f);
}
if let Some(found) = find(&f.children, name) {
return Some(found);
}
}
None
}
fn cog(report: &FileReport, name: &str) -> u32 {
find(&report.functions, name).expect("function").cognitive
}
fn cyc(report: &FileReport, name: &str) -> u32 {
find(&report.functions, name).expect("function").cyclomatic
}
#[test]
fn sonar_sum_of_primes_is_7() {
let inner_if = Node::Branch {
test: vec![],
then: vec![Node::Jump { labeled: true }],
alternate: None,
};
let inner_for = Node::Loop {
body: vec![inner_if],
};
let outer_for = Node::Loop {
body: vec![inner_for],
};
let f = func("sumOfPrimes", "function", vec![outer_for]);
let r = analyze("t", &[f], vec![]);
assert_eq!(cog(&r, "sumOfPrimes"), 7);
}
#[test]
fn sonar_get_words_is_1() {
let sw = Node::Switch {
cases: vec![
SwitchCase {
is_default: false,
body: vec![],
},
SwitchCase {
is_default: false,
body: vec![],
},
SwitchCase {
is_default: true,
body: vec![],
},
],
};
let r = analyze("t", &[func("getWords", "function", vec![sw])], vec![]);
assert_eq!(cog(&r, "getWords"), 1);
assert_eq!(cyc(&r, "getWords"), 3);
}
#[test]
fn nested_if_adds_nesting() {
let deep = Node::Branch {
test: vec![],
then: vec![Node::Branch {
test: vec![],
then: vec![Node::Branch {
test: vec![],
then: vec![],
alternate: None,
}],
alternate: None,
}],
alternate: None,
};
let r = analyze("t", &[func("f", "function", vec![deep])], vec![]);
assert_eq!(cog(&r, "f"), 6); }
#[test]
fn else_if_else_are_flat() {
let chain = Node::Branch {
test: vec![],
then: vec![],
alternate: Some(Box::new(Node::Branch {
test: vec![],
then: vec![],
alternate: Some(Box::new(Node::Group(vec![]))),
})),
};
let r = analyze("t", &[func("f", "function", vec![chain])], vec![]);
assert_eq!(cog(&r, "f"), 3); }
#[test]
fn logical_sequences() {
let inner_and = Node::Logical {
op: LogicalOp::And,
operands: vec![
Node::Group(vec![]),
Node::Group(vec![]),
Node::Group(vec![]),
],
};
let outer_or = Node::Logical {
op: LogicalOp::Or,
operands: vec![inner_and, Node::Group(vec![])],
};
let branch = Node::Branch {
test: vec![outer_or],
then: vec![],
alternate: None,
};
let r = analyze("t", &[func("f", "function", vec![branch])], vec![]);
assert_eq!(cog(&r, "f"), 3);
assert_eq!(cyc(&r, "f"), 5);
}
#[test]
fn recursion_adds_one() {
let body = vec![
Node::Branch {
test: vec![],
then: vec![],
alternate: None,
},
Node::Call {
callee: Some("fib".into()),
},
];
let r = analyze("t", &[func("fib", "function", body)], vec![]);
assert_eq!(cog(&r, "fib"), 2);
}
#[test]
fn nested_function_is_independent_unit() {
let inner = func(
"inner",
"function",
vec![Node::Branch {
test: vec![],
then: vec![],
alternate: None,
}],
);
let outer = func("outer", "function", vec![inner]);
let r = analyze("t", &[outer], vec![]);
assert_eq!(cog(&r, "outer"), 0); assert_eq!(cog(&r, "inner"), 1);
}
#[test]
fn file_total_sums_all_functions() {
let a = func(
"a",
"function",
vec![Node::Branch {
test: vec![],
then: vec![],
alternate: None,
}],
);
let b = func(
"b",
"function",
vec![Node::Branch {
test: vec![],
then: vec![],
alternate: None,
}],
);
let r = analyze("t", &[a, b], vec![]);
assert_eq!(r.cognitive, 2);
}
}