use tree_sitter::Node as TsNode;
pub struct ComplexityConfig {
pub branch_types: &'static [&'static str],
pub loop_types: &'static [&'static str],
pub return_types: &'static [&'static str],
pub nesting_types: &'static [&'static str],
pub unsafe_types: &'static [&'static str],
pub unchecked_types: &'static [&'static str],
pub unchecked_methods: &'static [&'static str],
pub call_expression_types: &'static [&'static str],
pub call_method_field: &'static str,
pub assertion_names: &'static [&'static str],
pub macro_invocation_types: &'static [&'static str],
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ComplexityMetrics {
pub branches: u32,
pub loops: u32,
pub returns: u32,
pub max_nesting: u32,
pub unsafe_blocks: u32,
pub unchecked_calls: u32,
pub assertions: u32,
}
pub fn count_complexity(
node: TsNode<'_>,
config: &ComplexityConfig,
source: &[u8],
) -> ComplexityMetrics {
debug_assert!(
!config.branch_types.is_empty() || !config.loop_types.is_empty(),
"count_complexity called with config that has no branch or loop types"
);
debug_assert!(
node.child_count() > 0,
"count_complexity called on a node with no children"
);
let mut metrics = ComplexityMetrics::default();
let mut stack: Vec<(TsNode<'_>, u32)> = Vec::new();
let child_count = node.child_count();
let mut idx: u32 = 0;
while (idx as usize) < child_count {
if let Some(child) = node.child(idx) {
stack.push((child, 0));
}
idx += 1;
}
const MAX_ITERATIONS: usize = 500_000;
let mut iterations: usize = 0;
while let Some((current, depth)) = stack.pop() {
iterations += 1;
if iterations >= MAX_ITERATIONS {
break;
}
let kind = current.kind();
if config.branch_types.contains(&kind) {
metrics.branches += 1;
}
if config.loop_types.contains(&kind) {
metrics.loops += 1;
}
if config.return_types.contains(&kind) {
metrics.returns += 1;
}
if config.unsafe_types.contains(&kind) {
metrics.unsafe_blocks += 1;
}
if config.unchecked_types.contains(&kind) {
metrics.unchecked_calls += 1;
}
if !source.is_empty() && config.call_expression_types.contains(&kind) {
if let Some(name) = extract_call_name(current, config.call_method_field, source) {
if config.unchecked_methods.contains(&name.as_str()) {
metrics.unchecked_calls += 1;
}
if config.assertion_names.contains(&name.as_str()) {
metrics.assertions += 1;
}
}
}
if !source.is_empty() && config.macro_invocation_types.contains(&kind) {
if let Some(name) = extract_macro_name(current, source) {
if config.assertion_names.contains(&name.as_str()) {
metrics.assertions += 1;
}
if config.unchecked_methods.contains(&name.as_str()) {
metrics.unchecked_calls += 1;
}
}
}
let new_depth = if config.nesting_types.contains(&kind) {
let d = depth + 1;
if d > metrics.max_nesting {
metrics.max_nesting = d;
}
d
} else {
depth
};
let cc = current.child_count() as u32;
let mut ci = cc;
while ci > 0 {
ci -= 1;
if let Some(child) = current.child(ci) {
stack.push((child, new_depth));
}
}
}
debug_assert!(
metrics.max_nesting <= 500,
"max_nesting unexpectedly large, possible analysis error"
);
debug_assert!(
iterations <= MAX_ITERATIONS,
"iteration count invariant violated"
);
metrics
}
fn extract_call_name(node: TsNode<'_>, method_field: &str, source: &[u8]) -> Option<String> {
if !method_field.is_empty() {
if let Some(field_node) = node.child_by_field_name(method_field) {
let text = rightmost_identifier(field_node, source);
if !text.is_empty() {
return Some(text);
}
}
}
let child_count = node.child_count();
let mut i: u32 = 0;
while (i as usize) < child_count {
if let Some(child) = node.child(i) {
let ck = child.kind();
if ck == "identifier" || ck == "field_identifier" || ck == "property_identifier" {
if let Ok(text) = child.utf8_text(source) {
return Some(text.to_string());
}
}
if ck.contains("member_expression") || ck.contains("field_expression") {
let text = rightmost_identifier(child, source);
if !text.is_empty() {
return Some(text);
}
}
}
i += 1;
}
None
}
fn extract_macro_name(node: TsNode<'_>, source: &[u8]) -> Option<String> {
let child_count = node.child_count();
let mut i: u32 = 0;
while (i as usize) < child_count {
if let Some(child) = node.child(i) {
let ck = child.kind();
if ck == "identifier" || ck == "scoped_identifier" {
if let Ok(text) = child.utf8_text(source) {
return Some(text.trim_end_matches('!').to_string());
}
}
}
i += 1;
}
None
}
fn rightmost_identifier(node: TsNode<'_>, source: &[u8]) -> String {
let nk = node.kind();
if nk == "identifier" || nk == "field_identifier" || nk == "property_identifier" {
return node.utf8_text(source).unwrap_or("").to_string();
}
let cc = node.child_count();
let mut i = cc as u32;
while i > 0 {
i -= 1;
if let Some(child) = node.child(i) {
let ck = child.kind();
if ck == "identifier" || ck == "field_identifier" || ck == "property_identifier" {
return child.utf8_text(source).unwrap_or("").to_string();
}
}
}
String::new()
}
pub static RUST_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_expression", "match_arm", "else_clause"],
loop_types: &["for_expression", "while_expression", "loop_expression"],
return_types: &[
"return_expression",
"break_expression",
"continue_expression",
],
nesting_types: &["block"],
unsafe_types: &["unsafe_block"],
unchecked_types: &[],
unchecked_methods: &["unwrap", "expect"],
call_expression_types: &["call_expression"],
call_method_field: "function",
assertion_names: &[
"assert",
"assert_eq",
"assert_ne",
"debug_assert",
"debug_assert_eq",
"debug_assert_ne",
],
macro_invocation_types: &["macro_invocation"],
};
pub static JAVA_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"switch_block_statement_group",
"ternary_expression",
"catch_clause",
"else",
],
loop_types: &[
"for_statement",
"enhanced_for_statement",
"while_statement",
"do_statement",
],
return_types: &[
"return_statement",
"break_statement",
"continue_statement",
"throw_statement",
],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &["get"],
call_expression_types: &["method_invocation"],
call_method_field: "name",
assertion_names: &[
"assert",
"assertEquals",
"assertNotEquals",
"assertTrue",
"assertFalse",
"assertNull",
"assertNotNull",
"assertThrows",
"assertThat",
"assertArrayEquals",
],
macro_invocation_types: &[],
};
pub static GO_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"expression_case",
"type_case",
"default_case",
],
loop_types: &["for_statement"],
return_types: &["return_statement", "break_statement", "continue_statement"],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["call_expression"],
call_method_field: "function",
assertion_names: &[
"assert", "require", "Equal", "NotEqual", "True", "False", "Nil", "NotNil", "Error",
"NoError",
],
macro_invocation_types: &[],
};
pub static PYTHON_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"elif_clause",
"else_clause",
"conditional_expression",
"except_clause",
],
loop_types: &["for_statement", "while_statement"],
return_types: &[
"return_statement",
"break_statement",
"continue_statement",
"raise_statement",
],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["call"],
call_method_field: "function",
assertion_names: &[
"assert",
"assertEqual",
"assertNotEqual",
"assertTrue",
"assertFalse",
"assertIs",
"assertIsNone",
"assertIsNotNone",
"assertIn",
"assertRaises",
"assertAlmostEqual",
],
macro_invocation_types: &[],
};
pub static TYPESCRIPT_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"switch_case",
"ternary_expression",
"catch_clause",
"else_clause",
],
loop_types: &[
"for_statement",
"for_in_statement",
"while_statement",
"do_statement",
],
return_types: &[
"return_statement",
"break_statement",
"continue_statement",
"throw_statement",
],
nesting_types: &["statement_block"],
unsafe_types: &[],
unchecked_types: &["non_null_assertion_expression"],
unchecked_methods: &[],
call_expression_types: &["call_expression"],
call_method_field: "function",
assertion_names: &[
"assert",
"expect",
"assertEquals",
"assertStrictEquals",
"deepEqual",
"strictEqual",
"ok",
"notOk",
],
macro_invocation_types: &[],
};
pub static C_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"case_statement",
"conditional_expression",
"else_clause",
],
loop_types: &["for_statement", "while_statement", "do_statement"],
return_types: &["return_statement", "break_statement", "continue_statement"],
nesting_types: &["compound_statement"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["call_expression"],
call_method_field: "function",
assertion_names: &[
"assert",
"assert_true",
"assert_false",
"assert_int_equal",
"assert_string_equal",
"assert_null",
"assert_non_null",
"CU_ASSERT",
"CU_ASSERT_EQUAL",
],
macro_invocation_types: &[],
};
pub static CPP_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"case_statement",
"conditional_expression",
"catch_clause",
"else_clause",
],
loop_types: &[
"for_statement",
"while_statement",
"do_statement",
"for_range_loop",
],
return_types: &[
"return_statement",
"break_statement",
"continue_statement",
"throw_statement",
],
nesting_types: &["compound_statement"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["call_expression"],
call_method_field: "function",
assertion_names: &[
"assert",
"ASSERT_TRUE",
"ASSERT_FALSE",
"ASSERT_EQ",
"ASSERT_NE",
"ASSERT_LT",
"ASSERT_GT",
"EXPECT_TRUE",
"EXPECT_FALSE",
"EXPECT_EQ",
"EXPECT_NE",
"static_assert",
],
macro_invocation_types: &[],
};
pub static KOTLIN_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_expression", "when_entry", "catch_block", "else"],
loop_types: &["for_statement", "while_statement", "do_while_statement"],
return_types: &["jump_expression"],
nesting_types: &["statements"],
unsafe_types: &[],
unchecked_types: &["postfix_expression"],
unchecked_methods: &[],
call_expression_types: &["call_expression"],
call_method_field: "",
assertion_names: &[
"assert",
"assertEquals",
"assertNotEquals",
"assertTrue",
"assertFalse",
"assertNull",
"assertNotNull",
"assertIs",
"assertIsNot",
],
macro_invocation_types: &[],
};
pub static SCALA_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_expression", "case_clause", "catch_clause"],
loop_types: &["for_expression", "while_expression"],
return_types: &["return_expression"],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &["get"],
call_expression_types: &["call_expression"],
call_method_field: "function",
assertion_names: &["assert", "assertEquals", "assertResult", "assertThrows"],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-dart")]
pub static DART_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"switch_statement_case",
"catch_clause",
"conditional_expression",
],
loop_types: &["for_statement", "while_statement", "do_statement"],
return_types: &[
"return_statement",
"break_statement",
"continue_statement",
"throw_statement",
],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &["postfix_expression"],
unchecked_methods: &[],
call_expression_types: &["call_expression"],
call_method_field: "function",
assertion_names: &["assert", "expect", "expectLater", "expectAsync"],
macro_invocation_types: &[],
};
pub static CSHARP_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"switch_section",
"conditional_expression",
"catch_clause",
],
loop_types: &[
"for_statement",
"for_each_statement",
"while_statement",
"do_statement",
],
return_types: &[
"return_statement",
"break_statement",
"continue_statement",
"throw_statement",
],
nesting_types: &["block"],
unsafe_types: &["unsafe_statement"],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["invocation_expression"],
call_method_field: "function",
assertion_names: &[
"Assert",
"AreEqual",
"AreNotEqual",
"IsTrue",
"IsFalse",
"IsNull",
"IsNotNull",
"ThrowsException",
],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-pascal")]
pub static PASCAL_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_statement", "case_item", "else_clause"],
loop_types: &["for_statement", "while_statement", "repeat_statement"],
return_types: &["raise_statement"],
nesting_types: &["begin_end_block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["call_statement"],
call_method_field: "",
assertion_names: &["Assert", "CheckEquals", "CheckTrue", "CheckFalse"],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-php")]
pub static PHP_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"case_statement",
"catch_clause",
"else_clause",
"else_if_clause",
],
loop_types: &[
"for_statement",
"foreach_statement",
"while_statement",
"do_statement",
],
return_types: &[
"return_statement",
"break_statement",
"continue_statement",
"throw_expression",
],
nesting_types: &["compound_statement"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["function_call_expression", "member_call_expression"],
call_method_field: "name",
assertion_names: &[
"assert",
"assertEquals",
"assertNotEquals",
"assertTrue",
"assertFalse",
"assertNull",
"assertNotNull",
"assertSame",
"assertInstanceOf",
],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-ruby")]
pub static RUBY_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if", "elsif", "when", "rescue", "conditional"],
loop_types: &["for", "while", "until"],
return_types: &["return", "break", "next"],
nesting_types: &["body_statement", "do_block", "block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &["fetch"],
call_expression_types: &["call", "method_call"],
call_method_field: "method",
assertion_names: &[
"assert",
"assert_equal",
"assert_not_equal",
"assert_nil",
"assert_not_nil",
"assert_raises",
"assert_match",
"refute",
],
macro_invocation_types: &[],
};
pub static SWIFT_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"switch_entry",
"guard_statement",
"catch_keyword",
],
loop_types: &[
"for_in_statement",
"while_statement",
"repeat_while_statement",
],
return_types: &["control_transfer_statement"],
nesting_types: &["code_block"],
unsafe_types: &[],
unchecked_types: &["force_unwrap_expression"],
unchecked_methods: &[],
call_expression_types: &["call_expression"],
call_method_field: "",
assertion_names: &[
"assert",
"precondition",
"assertionFailure",
"XCTAssert",
"XCTAssertEqual",
"XCTAssertTrue",
"XCTAssertFalse",
"XCTAssertNil",
"XCTAssertNotNil",
],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-bash")]
pub static BASH_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_statement", "elif_clause", "else_clause", "case_item"],
loop_types: &["for_statement", "while_statement", "c_style_for_statement"],
return_types: &["return_statement"],
nesting_types: &["compound_statement", "subshell"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["command"],
call_method_field: "name",
assertion_names: &[],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-lua")]
pub static LUA_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_statement", "elseif_statement", "else_statement"],
loop_types: &[
"for_statement",
"for_in_statement",
"while_statement",
"repeat_statement",
],
return_types: &["return_statement", "break_statement"],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["function_call"],
call_method_field: "",
assertion_names: &["assert", "assert_equal", "assert_true", "assert_false"],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-zig")]
pub static ZIG_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_expression",
"switch_expression",
"else_expression",
"catch",
],
loop_types: &["for_expression", "while_expression"],
return_types: &[
"return_expression",
"break_expression",
"continue_expression",
],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &["orelse"],
call_expression_types: &["call_expression"],
call_method_field: "",
assertion_names: &["expect", "expectEqual", "expectEqualStrings", "expectError"],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-nix")]
pub static NIX_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_expression"],
loop_types: &[],
return_types: &[],
nesting_types: &["attrset_expression", "let_expression"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["apply_expression"],
call_method_field: "",
assertion_names: &[],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-vbnet")]
pub static VBNET_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"elseif_clause",
"else_clause",
"select_case_statement",
"catch_clause",
],
loop_types: &[
"for_statement",
"for_each_statement",
"while_statement",
"do_loop_statement",
],
return_types: &["return_statement", "exit_statement", "throw_statement"],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["invocation_expression"],
call_method_field: "",
assertion_names: &[
"Assert",
"AreEqual",
"AreNotEqual",
"IsTrue",
"IsFalse",
"IsNull",
"IsNotNull",
],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-powershell")]
pub static POWERSHELL_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"elseif_clause",
"else_clause",
"switch_statement",
"catch_clause",
],
loop_types: &[
"for_statement",
"foreach_statement",
"while_statement",
"do_while_statement",
],
return_types: &[
"return_statement",
"break_statement",
"continue_statement",
"throw_statement",
],
nesting_types: &["script_block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["command_expression"],
call_method_field: "",
assertion_names: &["Should", "Assert"],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-perl")]
pub static PERL_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"elsif_clause",
"else_clause",
"unless_statement",
"conditional_expression",
],
loop_types: &[
"for_statement",
"foreach_statement",
"while_statement",
"until_statement",
],
return_types: &["return_expression", "last_expression", "next_expression"],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["call_expression", "method_call_expression"],
call_method_field: "",
assertion_names: &["ok", "is", "isnt", "like", "unlike", "cmp_ok", "is_deeply"],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-objc")]
pub static OBJC_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"case_statement",
"conditional_expression",
"catch_clause",
"else_clause",
],
loop_types: &[
"for_statement",
"while_statement",
"do_statement",
"for_in_statement",
],
return_types: &["return_statement", "break_statement", "continue_statement"],
nesting_types: &["compound_statement"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["call_expression", "message_expression"],
call_method_field: "",
assertion_names: &[
"NSAssert",
"NSCAssert",
"XCTAssert",
"XCTAssertTrue",
"XCTAssertFalse",
"XCTAssertEqual",
"XCTAssertNil",
"XCTAssertNotNil",
],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-fortran")]
pub static FORTRAN_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &[
"if_statement",
"elseif_clause",
"else_clause",
"case_statement",
"where_statement",
],
loop_types: &["do_loop_statement", "forall_statement"],
return_types: &[
"return_statement",
"stop_statement",
"exit_statement",
"cycle_statement",
],
nesting_types: &["block"],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["call_expression"],
call_method_field: "",
assertion_names: &[],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-cobol")]
pub static COBOL_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_header", "evaluate_statement", "when_phrase"],
loop_types: &["perform_statement_loop"],
return_types: &["stop_statement", "goback_statement"],
nesting_types: &[],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["perform_statement_call_proc"],
call_method_field: "",
assertion_names: &[],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-msbasic2")]
pub static MSBASIC2_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_statement"],
loop_types: &["for_statement"],
return_types: &["return_statement"],
nesting_types: &[],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &[],
call_method_field: "",
assertion_names: &[],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-gwbasic")]
pub static GWBASIC_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["if_statement"],
loop_types: &["for_statement", "while_statement"],
return_types: &["return_statement"],
nesting_types: &[],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &[],
call_method_field: "",
assertion_names: &[],
macro_invocation_types: &[],
};
#[cfg(feature = "lang-qbasic")]
pub static QBASIC_COMPLEXITY: ComplexityConfig = ComplexityConfig {
branch_types: &["block_if_statement"],
loop_types: &["for_statement", "while_statement", "do_loop_statement"],
return_types: &["exit_statement"],
nesting_types: &[],
unsafe_types: &[],
unchecked_types: &[],
unchecked_methods: &[],
call_expression_types: &["call_statement"],
call_method_field: "",
assertion_names: &[],
macro_invocation_types: &[],
};