#[allow(clippy::wildcard_imports, reason = "many AST types used")]
use oxc_ast::ast::*;
use oxc_ast_visit::Visit;
use oxc_ast_visit::walk;
use oxc_semantic::ScopeFlags;
use oxc_span::Span;
use fallow_types::extract::FunctionComplexity;
struct FunctionFrame {
name: String,
span: Span,
cyclomatic: u16,
cognitive: u16,
nesting_level: u16,
last_logical_operator: Option<LogicalOperator>,
param_count: u8,
}
pub struct ComplexityVisitor {
stack: Vec<FunctionFrame>,
pub results: Vec<FunctionComplexity>,
line_offsets: Vec<u32>,
pending_name: Option<String>,
}
impl ComplexityVisitor {
pub const fn new(line_offsets: Vec<u32>) -> Self {
Self {
stack: Vec::new(),
results: Vec::new(),
line_offsets,
pending_name: None,
}
}
fn push_function(&mut self, name: String, span: Span, param_count: u8) {
self.stack.push(FunctionFrame {
name,
span,
cyclomatic: 1, cognitive: 0,
nesting_level: 0,
last_logical_operator: None,
param_count,
});
}
fn pop_function(&mut self) {
if let Some(frame) = self.stack.pop() {
let (line, col) = fallow_types::extract::byte_offset_to_line_col(
&self.line_offsets,
frame.span.start,
);
let end_line =
fallow_types::extract::byte_offset_to_line_col(&self.line_offsets, frame.span.end)
.0;
self.results.push(FunctionComplexity {
name: frame.name,
line,
col,
cyclomatic: frame.cyclomatic,
cognitive: frame.cognitive,
line_count: end_line.saturating_sub(line) + 1,
param_count: frame.param_count,
});
}
}
fn inc_cyclomatic(&mut self) {
if let Some(frame) = self.stack.last_mut() {
frame.cyclomatic = frame.cyclomatic.saturating_add(1);
}
}
fn inc_cognitive_with_nesting(&mut self) {
if let Some(frame) = self.stack.last_mut() {
frame.cognitive = frame.cognitive.saturating_add(1 + frame.nesting_level);
}
}
fn inc_cognitive_flat(&mut self) {
if let Some(frame) = self.stack.last_mut() {
frame.cognitive = frame.cognitive.saturating_add(1);
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "functions with >255 params are unrealistic"
)]
fn count_params(params: &FormalParameters<'_>) -> u8 {
let mut count = params
.items
.iter()
.filter(|p| {
!matches!(&p.pattern, BindingPattern::BindingIdentifier(id) if id.name == "this")
})
.count();
if params.rest.is_some() {
count += 1;
}
count as u8
}
fn inc_nesting(&mut self) {
if let Some(frame) = self.stack.last_mut() {
frame.nesting_level = frame.nesting_level.saturating_add(1);
}
}
fn dec_nesting(&mut self) {
if let Some(frame) = self.stack.last_mut() {
frame.nesting_level = frame.nesting_level.saturating_sub(1);
}
}
fn handle_logical_operator(&mut self, op: LogicalOperator) {
if let Some(frame) = self.stack.last_mut() {
match frame.last_logical_operator {
None => {
frame.cognitive = frame.cognitive.saturating_add(1);
frame.last_logical_operator = Some(op);
}
Some(prev) if prev == op => {
}
Some(_) => {
frame.cognitive = frame.cognitive.saturating_add(1);
frame.last_logical_operator = Some(op);
}
}
}
}
fn reset_logical_operator(&mut self) {
if let Some(frame) = self.stack.last_mut() {
frame.last_logical_operator = None;
}
}
const fn is_nested_logical(expr: &Expression<'_>) -> bool {
matches!(expr, Expression::LogicalExpression(_))
}
}
impl<'a> Visit<'a> for ComplexityVisitor {
fn visit_function(&mut self, func: &Function<'a>, flags: ScopeFlags) {
let name = func
.id
.as_ref()
.map(|id| {
self.pending_name.take(); id.name.to_string()
})
.or_else(|| self.pending_name.take())
.unwrap_or_else(|| "<anonymous>".to_string());
let is_nested = !self.stack.is_empty();
if is_nested {
self.inc_nesting();
}
let param_count = Self::count_params(&func.params);
self.push_function(name, func.span, param_count);
walk::walk_function(self, func, flags);
self.pop_function();
if is_nested {
self.dec_nesting();
}
}
fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'a>) {
let name = self
.pending_name
.take()
.unwrap_or_else(|| "<arrow>".to_string());
let is_nested = !self.stack.is_empty();
if is_nested {
self.inc_nesting();
}
let param_count = Self::count_params(&arrow.params);
self.push_function(name, arrow.span, param_count);
walk::walk_arrow_function_expression(self, arrow);
self.pop_function();
if is_nested {
self.dec_nesting();
}
}
fn visit_method_definition(&mut self, method: &MethodDefinition<'a>) {
if let Some(name) = method.key.static_name() {
self.pending_name = Some(name.to_string());
}
walk::walk_method_definition(self, method);
self.pending_name = None;
}
fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
if let Some(id) = decl.id.get_binding_identifier() {
self.pending_name = Some(id.name.to_string());
}
walk::walk_variable_declarator(self, decl);
self.pending_name = None;
}
fn visit_property_definition(&mut self, prop: &PropertyDefinition<'a>) {
if let Some(name) = prop.key.static_name() {
self.pending_name = Some(name.to_string());
}
walk::walk_property_definition(self, prop);
self.pending_name = None;
}
fn visit_object_property(&mut self, prop: &ObjectProperty<'a>) {
if let Some(name) = prop.key.static_name() {
self.pending_name = Some(name.to_string());
}
walk::walk_object_property(self, prop);
self.pending_name = None;
}
fn visit_export_default_declaration(&mut self, decl: &ExportDefaultDeclaration<'a>) {
self.pending_name = Some("default".to_string());
walk::walk_export_default_declaration(self, decl);
self.pending_name = None;
}
fn visit_if_statement(&mut self, stmt: &IfStatement<'a>) {
self.inc_cyclomatic();
self.inc_cognitive_with_nesting();
self.visit_expression(&stmt.test);
self.inc_nesting();
self.visit_statement(&stmt.consequent);
self.dec_nesting();
if let Some(alternate) = &stmt.alternate {
match alternate {
Statement::IfStatement(else_if) => {
self.visit_if_statement(else_if);
if let Some(frame) = self.stack.last_mut() {
frame.cognitive = frame.cognitive.saturating_sub(frame.nesting_level);
}
}
_ => {
self.inc_cognitive_flat();
self.inc_nesting();
self.visit_statement(alternate);
self.dec_nesting();
}
}
}
}
fn visit_for_statement(&mut self, stmt: &ForStatement<'a>) {
self.inc_cyclomatic();
self.inc_cognitive_with_nesting();
if let Some(init) = &stmt.init {
self.visit_for_statement_init(init);
}
if let Some(test) = &stmt.test {
self.visit_expression(test);
}
if let Some(update) = &stmt.update {
self.visit_expression(update);
}
self.inc_nesting();
self.visit_statement(&stmt.body);
self.dec_nesting();
}
fn visit_for_in_statement(&mut self, stmt: &ForInStatement<'a>) {
self.inc_cyclomatic();
self.inc_cognitive_with_nesting();
self.visit_for_statement_left(&stmt.left);
self.visit_expression(&stmt.right);
self.inc_nesting();
self.visit_statement(&stmt.body);
self.dec_nesting();
}
fn visit_for_of_statement(&mut self, stmt: &ForOfStatement<'a>) {
self.inc_cyclomatic();
self.inc_cognitive_with_nesting();
self.visit_for_statement_left(&stmt.left);
self.visit_expression(&stmt.right);
self.inc_nesting();
self.visit_statement(&stmt.body);
self.dec_nesting();
}
fn visit_while_statement(&mut self, stmt: &WhileStatement<'a>) {
self.inc_cyclomatic();
self.inc_cognitive_with_nesting();
self.visit_expression(&stmt.test);
self.inc_nesting();
self.visit_statement(&stmt.body);
self.dec_nesting();
}
fn visit_do_while_statement(&mut self, stmt: &DoWhileStatement<'a>) {
self.inc_cyclomatic();
self.inc_cognitive_with_nesting();
self.inc_nesting();
self.visit_statement(&stmt.body);
self.dec_nesting();
self.visit_expression(&stmt.test);
}
fn visit_switch_statement(&mut self, stmt: &SwitchStatement<'a>) {
self.inc_cognitive_with_nesting();
self.visit_expression(&stmt.discriminant);
self.inc_nesting();
for case in &stmt.cases {
self.visit_switch_case(case);
}
self.dec_nesting();
}
fn visit_switch_case(&mut self, case: &SwitchCase<'a>) {
if case.test.is_some() {
self.inc_cyclomatic();
}
walk::walk_switch_case(self, case);
}
fn visit_catch_clause(&mut self, clause: &CatchClause<'a>) {
self.inc_cyclomatic();
self.inc_cognitive_with_nesting();
self.inc_nesting();
walk::walk_catch_clause(self, clause);
self.dec_nesting();
}
fn visit_conditional_expression(&mut self, expr: &ConditionalExpression<'a>) {
self.inc_cyclomatic();
self.inc_cognitive_with_nesting();
self.visit_expression(&expr.test);
self.inc_nesting();
self.visit_expression(&expr.consequent);
self.visit_expression(&expr.alternate);
self.dec_nesting();
}
fn visit_logical_expression(&mut self, expr: &LogicalExpression<'a>) {
self.inc_cyclomatic();
self.handle_logical_operator(expr.operator);
self.visit_expression(&expr.left);
self.visit_expression(&expr.right);
if !Self::is_nested_logical(&expr.right) {
if !Self::is_nested_logical(&expr.left) {
self.reset_logical_operator();
}
}
}
fn visit_assignment_expression(&mut self, expr: &AssignmentExpression<'a>) {
if matches!(
expr.operator,
AssignmentOperator::LogicalAnd
| AssignmentOperator::LogicalOr
| AssignmentOperator::LogicalNullish
) {
self.inc_cyclomatic();
}
walk::walk_assignment_expression(self, expr);
}
fn visit_chain_expression(&mut self, expr: &ChainExpression<'a>) {
match &expr.expression {
ChainElement::CallExpression(call) => {
if call.optional {
self.inc_cyclomatic();
}
}
ChainElement::StaticMemberExpression(member) => {
if member.optional {
self.inc_cyclomatic();
}
}
ChainElement::ComputedMemberExpression(member) => {
if member.optional {
self.inc_cyclomatic();
}
}
ChainElement::PrivateFieldExpression(field) => {
if field.optional {
self.inc_cyclomatic();
}
}
ChainElement::TSNonNullExpression(_) => {}
}
walk::walk_chain_expression(self, expr);
}
fn visit_break_statement(&mut self, stmt: &BreakStatement<'a>) {
if stmt.label.is_some() {
self.inc_cognitive_flat();
}
walk::walk_break_statement(self, stmt);
}
fn visit_continue_statement(&mut self, stmt: &ContinueStatement<'a>) {
if stmt.label.is_some() {
self.inc_cognitive_flat();
}
walk::walk_continue_statement(self, stmt);
}
}
pub fn compute_complexity(
program: &Program<'_>,
line_offsets: Vec<u32>,
) -> Vec<FunctionComplexity> {
let mut visitor = ComplexityVisitor::new(line_offsets);
visitor.visit_program(program);
visitor.results
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use fallow_types::extract::compute_line_offsets;
use oxc_allocator::Allocator;
use oxc_parser::Parser;
use oxc_span::SourceType;
fn analyze(source: &str) -> Vec<FunctionComplexity> {
let allocator = Allocator::default();
let source_type = SourceType::tsx();
let parser_return = Parser::new(&allocator, source, source_type).parse();
let line_offsets = compute_line_offsets(source);
compute_complexity(&parser_return.program, line_offsets)
}
fn find_fn<'a>(results: &'a [FunctionComplexity], name: &str) -> &'a FunctionComplexity {
results
.iter()
.find(|r| r.name == name)
.unwrap_or_else(|| panic!("function '{name}' not found in results: {results:?}"))
}
#[test]
fn empty_function_has_cyclomatic_1() {
let results = analyze("function foo() {}");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 1);
}
#[test]
fn if_statement_adds_1() {
let results = analyze("function foo(x) { if (x) { return 1; } return 0; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn if_else_if_else_adds_2() {
let results = analyze(
"function foo(x) { if (x > 0) { return 1; } else if (x < 0) { return -1; } else { return 0; } }",
);
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 3); }
#[test]
fn for_loop_adds_1() {
let results = analyze("function foo() { for (let i = 0; i < 10; i++) {} }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn while_loop_adds_1() {
let results = analyze("function foo() { while (true) { break; } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn switch_case_adds_per_case() {
let results = analyze(
"function foo(x) { switch (x) { case 1: break; case 2: break; default: break; } }",
);
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 3); }
#[test]
fn catch_adds_1() {
let results = analyze("function foo() { try { } catch (e) { } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn ternary_adds_1() {
let results = analyze("function foo(x) { return x ? 1 : 0; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn logical_and_adds_1() {
let results = analyze("function foo(a, b) { return a && b; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn logical_or_adds_1() {
let results = analyze("function foo(a, b) { return a || b; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn nullish_coalescing_adds_1() {
let results = analyze("function foo(a) { return a ?? 'default'; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn logical_assignment_adds_1() {
let results = analyze("function foo(a) { a &&= true; a ||= false; a ??= null; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 4); }
#[test]
fn do_while_adds_1() {
let results = analyze("function foo() { do { } while (true); }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn for_of_adds_1() {
let results = analyze("function foo(arr) { for (const x of arr) { } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn for_in_adds_1() {
let results = analyze("function foo(obj) { for (const k in obj) { } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn optional_chaining_adds_1() {
let results = analyze("function foo(obj) { return obj?.value; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2); }
#[test]
fn optional_chaining_computed_member_adds_1() {
let results = analyze("function foo(obj) { return obj?.[0]; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2); }
#[test]
fn optional_chaining_not_cognitive() {
let results = analyze("function foo(obj) { return obj?.a?.b?.c; }");
let f = find_fn(&results, "foo");
assert!(
f.cyclomatic > 1,
"optional chaining should increment cyclomatic"
);
assert_eq!(
f.cognitive, 0,
"optional chaining should NOT increment cognitive"
);
}
#[test]
fn complex_function_cyclomatic() {
let results = analyze(
r"function complex(x, y) {
if (x > 0) {
for (let i = 0; i < x; i++) {
if (y && i > 5) {
return true;
}
}
} else if (x < 0) {
while (y) {
y--;
}
}
return x ? true : false;
}",
);
let f = find_fn(&results, "complex");
assert_eq!(f.cyclomatic, 8);
}
#[test]
fn empty_function_has_cognitive_0() {
let results = analyze("function foo() {}");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 0);
}
#[test]
fn simple_if_cognitive_1() {
let results = analyze("function foo(x) { if (x) { return 1; } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 1); }
#[test]
fn nested_if_cognitive_with_nesting() {
let results = analyze("function foo(x, y) { if (x) { if (y) { return 1; } } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn if_else_cognitive() {
let results = analyze("function foo(x) { if (x) { return 1; } else { return 0; } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 2);
}
#[test]
fn if_else_if_else_cognitive() {
let results = analyze(
"function foo(x) { if (x > 0) { return 1; } else if (x < 0) { return -1; } else { return 0; } }",
);
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn boolean_sequence_same_operator() {
let results = analyze("function foo(a, b, c) { return a && b && c; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 1);
}
#[test]
fn boolean_sequence_mixed_operators() {
let results = analyze("function foo(a, b, c) { return a && b || c; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 2);
}
#[test]
fn for_loop_increases_nesting() {
let results =
analyze("function foo(arr) { for (const x of arr) { if (x) { return x; } } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn switch_cognitive_1() {
let results = analyze("function foo(x) { switch (x) { case 1: break; case 2: break; } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 1);
}
#[test]
fn nested_function_resets_nesting() {
let results = analyze(
r"function outer(x) {
if (x) {
const inner = () => {
if (x) { return 1; }
};
}
}",
);
let outer = find_fn(&results, "outer");
let inner = find_fn(&results, "inner");
assert_eq!(outer.cognitive, 1);
assert_eq!(inner.cognitive, 1);
}
#[test]
fn break_with_label_adds_1() {
let results = analyze("function foo() { outer: for (;;) { break outer; } }");
let f = find_fn(&results, "foo");
assert!(f.cognitive >= 2);
}
#[test]
fn arrow_function_tracked() {
let results = analyze("const foo = (x) => x > 0 ? 1 : 0;");
assert!(!results.is_empty());
let f = &results[0];
assert_eq!(f.name, "foo"); assert_eq!(f.cyclomatic, 2); }
#[test]
fn line_count_computed() {
let results =
analyze("function foo() {\n const a = 1;\n const b = 2;\n return a + b;\n}");
let f = find_fn(&results, "foo");
assert_eq!(f.line_count, 5);
}
#[test]
fn deeply_nested_cognitive() {
let results = analyze(
r"function deep(a, b, c, d) {
if (a) { // +1 (n=0) = 1
for (;;) { // +1+1 (n=1) = 3
if (b) { // +1+2 (n=2) = 6
while (c) { // +1+3 (n=3) = 10
if (d) {} // +1+4 (n=4) = 15
}
}
}
}
}",
);
let f = find_fn(&results, "deep");
assert_eq!(f.cognitive, 15);
}
#[test]
fn object_method_shorthand_named() {
let results = analyze("const obj = { foo(x) { if (x) {} } };");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn object_arrow_property_named() {
let results = analyze("const obj = { bar: (x) => x ? 1 : 0 };");
let f = find_fn(&results, "bar");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn class_method_named() {
let results = analyze("class Foo { parse(x) { if (x) {} } }");
let f = find_fn(&results, "parse");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn export_default_function_named() {
let results = analyze("export default function() { if (true) {} }");
let f = find_fn(&results, "default");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn export_default_named_function_keeps_name() {
let results = analyze("export default function myFn() { if (true) {} }");
let f = find_fn(&results, "myFn");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn catch_cognitive_with_nesting() {
let results = analyze("function foo() { if (true) { try { } catch (e) { } } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn do_while_cognitive_with_nesting() {
let results = analyze("function foo() { if (true) { do { } while (true); } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn while_cognitive_with_nesting() {
let results = analyze("function foo() { if (true) { while (true) { break; } } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn ternary_cognitive_with_nesting() {
let results = analyze("function foo(x) { if (x) { return x ? 1 : 0; } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn continue_with_label_cognitive() {
let results =
analyze("function foo() { outer: for (let i = 0; i < 10; i++) { continue outer; } }");
let f = find_fn(&results, "foo");
assert!(f.cognitive >= 2);
}
#[test]
fn class_property_arrow_named() {
let results = analyze("class Foo { bar = (x: number) => x > 0 ? 1 : 0; }");
let f = find_fn(&results, "bar");
assert_eq!(f.cyclomatic, 2); }
#[test]
fn nested_arrow_functions_independent_complexity() {
let results = analyze(
r"const outer = (x) => {
if (x) {
const inner = (y) => {
if (y) { return 1; }
return 0;
};
return inner(x);
}
return 0;
};",
);
let outer = find_fn(&results, "outer");
let inner = find_fn(&results, "inner");
assert_eq!(outer.cyclomatic, 2);
assert_eq!(inner.cyclomatic, 2);
}
#[test]
fn method_definition_named() {
let results = analyze("class Foo { doWork(x) { if (x) { return 1; } return 0; } }");
let f = find_fn(&results, "doWork");
assert_eq!(f.cyclomatic, 2);
}
#[test]
fn logical_nullish_cognitive() {
let results = analyze("function foo(a, b) { return a ?? b; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 1);
}
#[test]
fn mixed_logical_operators_cognitive() {
let results = analyze("function foo(a, b, c, d) { return a && b || c ?? d; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn saturating_add_prevents_overflow() {
let mut source = "function foo() {".to_string();
for _ in 0..20 {
source.push_str("if (true) {");
}
for _ in 0..20 {
source.push('}');
}
source.push('}');
let results = analyze(&source);
assert!(!results.is_empty());
}
#[test]
fn empty_source_no_functions() {
let results = analyze("");
assert!(results.is_empty());
}
#[test]
fn top_level_code_not_reported() {
let results = analyze("if (true) { console.log('hello'); }");
assert!(results.is_empty());
}
#[test]
fn for_in_cognitive_with_nesting() {
let results = analyze("function foo(obj) { for (const k in obj) { if (k) {} } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn for_of_cognitive_with_nesting() {
let results = analyze("function foo(arr) { for (const x of arr) { if (x) {} } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn optional_call_expression_cyclomatic() {
let results = analyze("function foo(obj) { return obj?.method(); }");
let f = find_fn(&results, "foo");
assert!(f.cyclomatic >= 1); assert_eq!(f.cognitive, 0); }
#[test]
fn logical_assignment_not_cognitive() {
let results = analyze("function foo(a) { a &&= true; }");
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 2); }
#[test]
fn multiple_switch_cases_cyclomatic() {
let results = analyze(
"function foo(x) { switch (x) { case 1: break; case 2: break; case 3: break; default: break; } }",
);
let f = find_fn(&results, "foo");
assert_eq!(f.cyclomatic, 4);
}
#[test]
fn switch_nested_in_if_cognitive() {
let results = analyze("function foo(x, y) { if (x) { switch (y) { case 1: break; } } }");
let f = find_fn(&results, "foo");
assert_eq!(f.cognitive, 3);
}
#[test]
fn line_and_col_computed_correctly() {
let results = analyze("\n\nfunction foo() {\n if (true) {}\n}\n");
let f = find_fn(&results, "foo");
assert_eq!(f.line, 3); }
#[test]
fn param_count_zero_for_no_params() {
let results = analyze("function foo() {}");
assert_eq!(find_fn(&results, "foo").param_count, 0);
}
#[test]
fn param_count_simple_params() {
let results = analyze("function foo(a, b, c) {}");
assert_eq!(find_fn(&results, "foo").param_count, 3);
}
#[test]
fn param_count_arrow_function() {
let results = analyze("const bar = (a, b, c, d, e) => {}");
assert_eq!(find_fn(&results, "bar").param_count, 5);
}
#[test]
fn param_count_excludes_ts_this_parameter() {
let results = analyze("function greet(this: Context, name: string) {}");
assert_eq!(find_fn(&results, "greet").param_count, 1);
}
#[test]
fn param_count_destructured_counts_as_one() {
let results = analyze("function foo({ a, b, c }: Options) {}");
assert_eq!(find_fn(&results, "foo").param_count, 1);
}
#[test]
fn param_count_rest_parameter() {
let results = analyze("function foo(a: number, ...rest: string[]) {}");
assert_eq!(find_fn(&results, "foo").param_count, 2);
}
#[test]
fn param_count_method_definition() {
let results = analyze("class Foo { bar(a: number, b: string) {} }");
assert_eq!(find_fn(&results, "bar").param_count, 2);
}
}