use crate::ast::FunctionNode;
use crate::cfg::Cfg;
use swc_ecma_ast::*;
use swc_ecma_visit::{Visit, VisitWith};
#[derive(Debug, Clone)]
pub struct RawMetrics {
pub cc: usize,
pub nd: usize,
pub fo: usize,
pub ns: usize,
pub loc: usize,
pub callee_names: Vec<String>,
}
fn calculate_loc(source: &str) -> usize {
if source.is_empty() {
return 0;
}
source.lines().count()
}
fn calculate_loc_from_node(node: &tree_sitter::Node) -> usize {
let start_row = node.start_position().row;
let end_row = node.end_position().row;
end_row.saturating_sub(start_row) + 1
}
pub fn extract_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
use crate::language::FunctionBody;
match &function.body {
FunctionBody::ECMAScript(body) => {
let loc = function
.span
.end_line
.saturating_sub(function.span.start_line)
+ 1;
let callee_names = ecmascript_extract_callees(body);
RawMetrics {
cc: cyclomatic_complexity(cfg, body),
nd: nesting_depth(body),
fo: callee_names.len(),
ns: non_structured_exits(body),
loc: loc as usize,
callee_names,
}
}
FunctionBody::Go { .. } => {
extract_go_metrics(function, cfg)
}
FunctionBody::Java { .. } => {
extract_java_metrics(function, cfg)
}
FunctionBody::Python { .. } => {
extract_python_metrics(function, cfg)
}
FunctionBody::Rust { .. } => {
extract_rust_metrics(function, cfg)
}
FunctionBody::CSharp { .. } => extract_csharp_metrics(function, cfg),
}
}
fn calculate_cc_from_cfg(cfg: &Cfg) -> usize {
if cfg.edge_count() > 0 && cfg.node_count() > 2 {
let e = cfg.edge_count();
let n = cfg.node_count() - 2; if n > 0 {
e.saturating_sub(n).saturating_add(2)
} else {
1
}
} else {
1
}
}
fn cyclomatic_complexity(cfg: &Cfg, body: &BlockStmt) -> usize {
let base_cc = if cfg.edge_count() > 0 && cfg.node_count() > 2 {
let e = cfg.edge_count();
let n = cfg.node_count() - 2; if n > 0 {
e.saturating_sub(n).saturating_add(2)
} else {
1 }
} else {
1 };
let mut short_circuit_count = 0;
let mut visitor = ShortCircuitVisitor {
count: &mut short_circuit_count,
};
body.visit_with(&mut visitor);
let switch_case_count = count_switch_cases(body);
let catch_count = count_catch_clauses(body);
base_cc + short_circuit_count + switch_case_count + catch_count
}
struct ShortCircuitVisitor<'a> {
count: &'a mut usize,
}
impl Visit for ShortCircuitVisitor<'_> {
fn visit_bin_expr(&mut self, bin_expr: &BinExpr) {
match bin_expr.op {
BinaryOp::LogicalAnd | BinaryOp::LogicalOr => {
*self.count += 1;
}
_ => {}
}
bin_expr.visit_children_with(self);
}
}
fn count_switch_cases(body: &BlockStmt) -> usize {
let mut count = 0;
let mut visitor = SwitchCaseCounter { count: &mut count };
body.visit_with(&mut visitor);
count
}
struct SwitchCaseCounter<'a> {
count: &'a mut usize,
}
impl Visit for SwitchCaseCounter<'_> {
fn visit_switch_stmt(&mut self, switch_stmt: &SwitchStmt) {
*self.count += switch_stmt.cases.len();
switch_stmt.visit_children_with(self);
}
}
fn count_catch_clauses(body: &BlockStmt) -> usize {
let mut count = 0;
let mut visitor = CatchCounter { count: &mut count };
body.visit_with(&mut visitor);
count
}
struct CatchCounter<'a> {
count: &'a mut usize,
}
impl Visit for CatchCounter<'_> {
fn visit_try_stmt(&mut self, try_stmt: &TryStmt) {
if try_stmt.handler.is_some() {
*self.count += 1;
}
try_stmt.visit_children_with(self);
}
}
fn nesting_depth(body: &BlockStmt) -> usize {
let mut visitor = NestingDepthVisitor {
max_depth: 0,
current_depth: 0,
};
body.visit_with(&mut visitor);
visitor.max_depth
}
struct NestingDepthVisitor {
max_depth: usize,
current_depth: usize,
}
macro_rules! impl_nesting_visitor {
($($method:ident, $ty:ty, $node:ident);* $(;)?) => {
$(
fn $method(&mut self, $node: &$ty) {
self.current_depth += 1;
if self.current_depth > self.max_depth {
self.max_depth = self.current_depth;
}
$node.visit_children_with(self);
self.current_depth -= 1;
}
)*
};
}
impl Visit for NestingDepthVisitor {
impl_nesting_visitor!(
visit_if_stmt, IfStmt, if_stmt;
visit_while_stmt, WhileStmt, while_stmt;
visit_do_while_stmt, DoWhileStmt, do_while_stmt;
visit_for_stmt, ForStmt, for_stmt;
visit_for_in_stmt, ForInStmt, for_in_stmt;
visit_for_of_stmt, ForOfStmt, for_of_stmt;
visit_switch_stmt, SwitchStmt, switch_stmt;
visit_try_stmt, TryStmt, try_stmt;
);
}
fn ecmascript_extract_callees(body: &BlockStmt) -> Vec<String> {
let mut visitor = FanOutVisitor {
calls: std::collections::HashSet::new(),
};
body.visit_with(&mut visitor);
let mut names: Vec<String> = visitor.calls.into_iter().collect();
names.sort();
names
}
struct FanOutVisitor {
calls: std::collections::HashSet<String>,
}
impl Visit for FanOutVisitor {
fn visit_call_expr(&mut self, call_expr: &CallExpr) {
let callee_str = callee_to_string(&call_expr.callee);
if !callee_str.is_empty() && callee_str != "<computed>" {
self.calls.insert(callee_str);
}
call_expr.visit_children_with(self);
}
}
fn callee_to_string(callee: &Callee) -> String {
match callee {
Callee::Expr(expr) => expr_to_callee_string(expr),
Callee::Super(_) => "super".to_string(),
Callee::Import(_) => "<computed>".to_string(),
}
}
fn expr_to_callee_string(expr: &Expr) -> String {
match expr {
Expr::Ident(ident) => ident.sym.to_string(),
Expr::Member(member) => {
let obj_str = match &*member.obj {
Expr::Ident(id) => id.sym.to_string(),
Expr::Call(call) => {
match &call.callee {
Callee::Expr(callee_expr) => expr_to_callee_string(callee_expr),
_ => "<computed>".to_string(),
}
}
Expr::Member(member_obj) => {
expr_to_callee_string(&member_obj.obj)
}
_ => "<computed>".to_string(),
};
let prop_str = match &member.prop {
MemberProp::Ident(id) => id.sym.to_string(),
MemberProp::PrivateName(name) => name.name.to_string(),
MemberProp::Computed(_) => "<computed>".to_string(),
};
if obj_str == "<computed>" || prop_str == "<computed>" {
"<computed>".to_string()
} else {
format!("{}.{}", obj_str, prop_str)
}
}
Expr::Call(_) => "<computed>".to_string(), _ => "<computed>".to_string(),
}
}
fn non_structured_exits(body: &BlockStmt) -> usize {
let mut visitor = NonStructuredExitVisitor {
count: 0,
return_count: 0,
};
body.visit_with(&mut visitor);
let has_final_return = body
.stmts
.last()
.map(|s| matches!(s, Stmt::Return(_)))
.unwrap_or(false);
if has_final_return && visitor.return_count > 0 {
visitor.count -= 1;
}
visitor.count
}
struct NonStructuredExitVisitor {
count: usize,
return_count: usize,
}
impl Visit for NonStructuredExitVisitor {
fn visit_return_stmt(&mut self, _return_stmt: &ReturnStmt) {
self.count += 1;
self.return_count += 1;
}
fn visit_break_stmt(&mut self, _break_stmt: &BreakStmt) {
self.count += 1;
}
fn visit_continue_stmt(&mut self, _continue_stmt: &ContinueStmt) {
self.count += 1;
}
fn visit_throw_stmt(&mut self, _throw_stmt: &ThrowStmt) {
self.count += 1;
}
}
fn ts_find_child_by_kind<'a>(
node: tree_sitter::Node<'a>,
kind: &str,
) -> Option<tree_sitter::Node<'a>> {
let mut cursor = node.walk();
let result = node
.children(&mut cursor)
.find(|child| child.kind() == kind);
result
}
fn ts_find_function_by_start<'a>(
root: tree_sitter::Node<'a>,
start_byte: usize,
func_kinds: &[&str],
) -> Option<tree_sitter::Node<'a>> {
if func_kinds.contains(&root.kind()) && root.start_byte() == start_byte {
return Some(root);
}
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if let Some(found) = ts_find_function_by_start(child, start_byte, func_kinds) {
return Some(found);
}
}
None
}
fn ts_nesting_depth(body_node: &tree_sitter::Node, nesting_kinds: &[&str]) -> usize {
fn recurse(node: tree_sitter::Node, kinds: &[&str], current: usize, max: &mut usize) {
let next = if kinds.contains(&node.kind()) {
let d = current + 1;
if d > *max {
*max = d;
}
d
} else {
current
};
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
recurse(child, kinds, next, max);
}
}
let mut max_depth = 0;
recurse(*body_node, nesting_kinds, 0, &mut max_depth);
max_depth
}
fn ts_non_structured_exits(body_node: &tree_sitter::Node, exit_kinds: &[&str]) -> usize {
fn recurse(node: tree_sitter::Node, kinds: &[&str], count: &mut usize) {
if kinds.contains(&node.kind()) {
*count += 1;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
recurse(child, kinds, count);
}
}
let mut count = 0;
recurse(*body_node, exit_kinds, &mut count);
count
}
fn ts_with_function_body<R>(
source: &str,
language: tree_sitter::Language,
start_byte: usize,
func_kinds: &[&str],
body_kinds: &[&str],
f: impl FnOnce(tree_sitter::Node, tree_sitter::Node) -> R,
) -> Option<R> {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&language).ok()?;
let tree = parser.parse(source, None)?;
let root = tree.root_node();
let func_node = ts_find_function_by_start(root, start_byte, func_kinds)?;
for kind in body_kinds {
if let Some(body_node) = ts_find_child_by_kind(func_node, kind) {
return Some(f(func_node, body_node));
}
}
None
}
fn extract_go_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
let (_body_node_id, source) = function.body.as_go();
ts_with_function_body(
source,
tree_sitter_go::LANGUAGE.into(),
function.span.start,
&["function_declaration", "method_declaration"],
&["block"],
|func_node, body_node| {
let callee_names = go_extract_callees(&body_node, source);
RawMetrics {
cc: calculate_cc_from_cfg(cfg) + go_count_cc_extras(&body_node, source),
nd: ts_nesting_depth(
&body_node,
&[
"if_statement",
"for_statement",
"switch_statement",
"expression_switch_statement",
"type_switch_statement",
"select_statement",
],
),
fo: callee_names.len(),
ns: go_non_structured_exits(&body_node, source),
loc: calculate_loc_from_node(&func_node),
callee_names,
}
},
)
.unwrap_or(RawMetrics {
cc: 1,
nd: 0,
fo: 0,
ns: 0,
loc: 0,
callee_names: vec![],
})
}
fn go_extract_callees(body_node: &tree_sitter::Node, source: &str) -> Vec<String> {
use std::collections::HashSet;
fn collect(node: tree_sitter::Node, source: &str, calls: &mut HashSet<String>) {
match node.kind() {
"call_expression" => {
if let Some(func_node) = ts_find_child_by_kind(node, "identifier")
.or_else(|| ts_find_child_by_kind(node, "selector_expression"))
{
let func_text = &source[func_node.start_byte()..func_node.end_byte()];
calls.insert(func_text.to_string());
}
}
"go_statement" => {
calls.insert(format!("<go@{}>", node.start_byte()));
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect(child, source, calls);
}
}
let mut calls = HashSet::new();
collect(*body_node, source, &mut calls);
let mut result: Vec<String> = calls.into_iter().collect();
result.sort();
result
}
fn go_non_structured_exits(body_node: &tree_sitter::Node, source: &str) -> usize {
fn count_exits(node: tree_sitter::Node, source: &str, count: &mut usize) {
match node.kind() {
"return_statement" => *count += 1,
"defer_statement" => *count += 1,
"expression_statement" => {
if let Some(call) = ts_find_child_by_kind(node, "call_expression") {
if let Some(ident) = ts_find_child_by_kind(call, "identifier") {
let name = &source[ident.start_byte()..ident.end_byte()];
if name == "panic" {
*count += 1;
}
}
else if let Some(sel) = ts_find_child_by_kind(call, "selector_expression") {
if let Some(field) = ts_find_child_by_kind(sel, "field_identifier") {
let field_name = &source[field.start_byte()..field.end_byte()];
if matches!(field_name, "Exit" | "Fatal" | "Fatalf" | "Fatalln") {
*count += 1;
}
}
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
count_exits(child, source, count);
}
}
let mut count = 0;
count_exits(*body_node, source, &mut count);
if count > 0 {
let mut cursor = body_node.walk();
if let Some(last_child) = body_node.children(&mut cursor).last() {
if last_child.kind() == "return_statement" {
count = count.saturating_sub(1);
}
}
}
count
}
fn go_count_cc_extras(body_node: &tree_sitter::Node, _source: &str) -> usize {
fn count_extras(node: tree_sitter::Node, count: &mut usize) {
match node.kind() {
"expression_case" | "default_case" | "communication_case" | "type_case" => {
*count += 1;
}
"binary_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "&&" || child.kind() == "||" {
*count += 1;
break;
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
count_extras(child, count);
}
}
let mut count = 0;
count_extras(*body_node, &mut count);
count
}
fn extract_java_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
let (_body_node_id, source) = function.body.as_java();
ts_with_function_body(
source,
tree_sitter_java::LANGUAGE.into(),
function.span.start,
&["method_declaration", "constructor_declaration"],
&["block", "constructor_body"],
|func_node, body_node| {
let callee_names = java_extract_callees(&body_node, source);
RawMetrics {
cc: calculate_cc_from_cfg(cfg) + java_count_cc_extras(&body_node, source),
nd: ts_nesting_depth(
&body_node,
&[
"if_statement",
"while_statement",
"do_statement",
"for_statement",
"enhanced_for_statement",
"switch_statement",
"switch_expression",
"try_statement",
"synchronized_statement",
],
),
fo: callee_names.len(),
ns: ts_non_structured_exits(
&body_node,
&[
"return_statement",
"throw_statement",
"break_statement",
"continue_statement",
],
),
loc: calculate_loc_from_node(&func_node),
callee_names,
}
},
)
.unwrap_or(RawMetrics {
cc: 1,
nd: 0,
fo: 0,
ns: 0,
loc: 0,
callee_names: vec![],
})
}
fn java_extract_callees(body_node: &tree_sitter::Node, source: &str) -> Vec<String> {
fn collect(
node: tree_sitter::Node,
source: &str,
calls: &mut std::collections::HashSet<String>,
) {
if node.kind() == "method_invocation" {
let method_text = &source[node.start_byte()..node.end_byte()];
calls.insert(method_text.to_string());
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect(child, source, calls);
}
}
let mut calls = std::collections::HashSet::new();
collect(*body_node, source, &mut calls);
let mut result: Vec<String> = calls.into_iter().collect();
result.sort();
result
}
fn java_count_cc_extras(body_node: &tree_sitter::Node, _source: &str) -> usize {
fn count_extras(node: tree_sitter::Node, count: &mut usize) {
match node.kind() {
"ternary_expression" => {
*count += 1;
}
"binary_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "&&" || child.kind() == "||" {
*count += 1;
break;
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
count_extras(child, count);
}
}
let mut count = 0;
count_extras(*body_node, &mut count);
count
}
fn extract_python_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
let (_body_node_id, source) = function.body.as_python();
ts_with_function_body(
source,
tree_sitter_python::LANGUAGE.into(),
function.span.start,
&["function_definition", "async_function_definition"],
&["block"],
|func_node, body_node| {
let callee_names = python_extract_callees(&body_node, source);
RawMetrics {
cc: calculate_cc_from_cfg(cfg) + python_count_cc_extras(&body_node, source),
nd: ts_nesting_depth(
&body_node,
&[
"if_statement",
"while_statement",
"for_statement",
"try_statement",
"with_statement",
"match_statement",
],
),
fo: callee_names.len(),
ns: ts_non_structured_exits(
&body_node,
&[
"return_statement",
"raise_statement",
"break_statement",
"continue_statement",
],
),
loc: calculate_loc_from_node(&func_node),
callee_names,
}
},
)
.unwrap_or(RawMetrics {
cc: 1,
nd: 0,
fo: 0,
ns: 0,
loc: 0,
callee_names: vec![],
})
}
fn python_extract_callees(body_node: &tree_sitter::Node, source: &str) -> Vec<String> {
fn collect(
node: tree_sitter::Node,
source: &str,
calls: &mut std::collections::HashSet<String>,
) {
if node.kind() == "call" {
let mut cursor = node.walk();
if let Some(func_node) = node.children(&mut cursor).next() {
let func_text = &source[func_node.start_byte()..func_node.end_byte()];
calls.insert(func_text.to_string());
};
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect(child, source, calls);
}
}
let mut calls = std::collections::HashSet::new();
collect(*body_node, source, &mut calls);
let mut result: Vec<String> = calls.into_iter().collect();
result.sort();
result
}
fn python_count_cc_extras(body_node: &tree_sitter::Node, _source: &str) -> usize {
fn count_extras(node: tree_sitter::Node, count: &mut usize) {
match node.kind() {
"boolean_operator" => {
*count += 1;
}
"conditional_expression" => {
*count += 1;
}
"list_comprehension"
| "dictionary_comprehension"
| "set_comprehension"
| "generator_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "if_clause" {
*count += 1;
break;
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
count_extras(child, count);
}
}
let mut count = 0;
count_extras(*body_node, &mut count);
count
}
fn extract_csharp_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
let (_body_node_id, source) = function.body.as_csharp();
ts_with_function_body(
source,
tree_sitter_c_sharp::LANGUAGE.into(),
function.span.start,
&[
"method_declaration",
"constructor_declaration",
"local_function_statement",
"operator_declaration",
"conversion_operator_declaration",
],
&["block"],
|func_node, body_node| {
let callee_names = csharp_extract_callees(&body_node, source);
RawMetrics {
cc: calculate_cc_from_cfg(cfg) + csharp_count_cc_extras(&body_node, source),
nd: ts_nesting_depth(
&body_node,
&[
"if_statement",
"while_statement",
"do_statement",
"for_statement",
"foreach_statement",
"switch_statement",
"try_statement",
],
),
fo: callee_names.len(),
ns: ts_non_structured_exits(
&body_node,
&[
"return_statement",
"throw_statement",
"break_statement",
"continue_statement",
],
),
loc: calculate_loc_from_node(&func_node),
callee_names,
}
},
)
.unwrap_or(RawMetrics {
cc: 1,
nd: 0,
fo: 0,
ns: 0,
loc: 0,
callee_names: vec![],
})
}
fn csharp_extract_callees(body_node: &tree_sitter::Node, source: &str) -> Vec<String> {
fn collect(
node: tree_sitter::Node,
source: &str,
calls: &mut std::collections::HashSet<String>,
) {
if node.kind() == "invocation_expression" {
let call_text = &source[node.start_byte()..node.end_byte()];
calls.insert(call_text.to_string());
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect(child, source, calls);
}
}
let mut calls = std::collections::HashSet::new();
collect(*body_node, source, &mut calls);
let mut result: Vec<String> = calls.into_iter().collect();
result.sort();
result
}
fn csharp_count_cc_extras(body_node: &tree_sitter::Node, _source: &str) -> usize {
fn count_extras(node: tree_sitter::Node, count: &mut usize) {
match node.kind() {
"conditional_expression" => {
*count += 1;
}
"binary_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "&&" || child.kind() == "||" || child.kind() == "??" {
*count += 1;
break;
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
count_extras(child, count);
}
}
let mut count = 0;
count_extras(*body_node, &mut count);
count
}
fn extract_rust_metrics(function: &FunctionNode, cfg: &Cfg) -> RawMetrics {
let source = function.body.as_rust();
let item_fn: syn::ItemFn = match syn::parse_str(source) {
Ok(func) => func,
Err(_) => {
return RawMetrics {
cc: calculate_cc_from_cfg(cfg),
nd: 0,
fo: 0,
ns: 0,
loc: 0,
callee_names: vec![],
};
}
};
let base_cc = calculate_cc_from_cfg(cfg);
let extra_cc = rust_count_cc_extras(&item_fn.block);
let nd = rust_nesting_depth(&item_fn.block);
let callee_names = rust_extract_callees(&item_fn.block);
let ns = rust_non_structured_exits(&item_fn.block);
RawMetrics {
cc: base_cc + extra_cc,
nd,
fo: callee_names.len(),
ns,
loc: calculate_loc(source),
callee_names,
}
}
fn rust_nesting_depth(block: &syn::Block) -> usize {
use syn::{Expr, Stmt};
fn calculate_depth(stmts: &[Stmt], current_depth: usize, max_depth: &mut usize) {
for stmt in stmts {
match stmt {
Stmt::Expr(expr, _) => expr_depth(expr, current_depth, max_depth),
Stmt::Local(local) => {
if let Some(init) = &local.init {
expr_depth(&init.expr, current_depth, max_depth);
}
}
_ => {}
}
}
}
fn expr_depth(expr: &Expr, current_depth: usize, max_depth: &mut usize) {
let new_depth = match expr {
Expr::If(_) | Expr::Match(_) | Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => {
let depth = current_depth + 1;
if depth > *max_depth {
*max_depth = depth;
}
depth
}
_ => current_depth,
};
match expr {
Expr::If(expr_if) => {
calculate_depth(&expr_if.then_branch.stmts, new_depth, max_depth);
if let Some((_, else_expr)) = &expr_if.else_branch {
expr_depth(else_expr, new_depth, max_depth);
}
}
Expr::Match(expr_match) => {
for arm in &expr_match.arms {
expr_depth(&arm.body, new_depth, max_depth);
}
}
Expr::Loop(expr_loop) => {
calculate_depth(&expr_loop.body.stmts, new_depth, max_depth);
}
Expr::While(expr_while) => {
calculate_depth(&expr_while.body.stmts, new_depth, max_depth);
}
Expr::ForLoop(expr_for) => {
calculate_depth(&expr_for.body.stmts, new_depth, max_depth);
}
Expr::Block(expr_block) => {
calculate_depth(&expr_block.block.stmts, new_depth, max_depth);
}
_ => {}
}
}
let mut max_depth = 0;
calculate_depth(&block.stmts, 0, &mut max_depth);
max_depth
}
fn rust_extract_callees(block: &syn::Block) -> Vec<String> {
use std::collections::HashSet;
use syn::{Expr, ExprCall, ExprMethodCall, Stmt};
fn count_calls(stmts: &[Stmt], calls: &mut HashSet<String>) {
for stmt in stmts {
match stmt {
Stmt::Expr(expr, _) => expr_calls(expr, calls),
Stmt::Local(local) => {
if let Some(init) = &local.init {
expr_calls(&init.expr, calls);
}
}
Stmt::Macro(stmt_macro) => {
let macro_name = stmt_macro
.mac
.path
.segments
.last()
.map(|seg| seg.ident.to_string())
.unwrap_or_else(|| "macro".to_string());
calls.insert(macro_name);
}
_ => {}
}
}
}
fn expr_calls(expr: &Expr, calls: &mut HashSet<String>) {
match expr {
Expr::Call(ExprCall { func, .. }) => {
if let Expr::Path(expr_path) = &**func {
let func_name = expr_path
.path
.segments
.last()
.map(|seg| seg.ident.to_string())
.unwrap_or_else(|| "fn".to_string());
calls.insert(func_name);
}
}
Expr::MethodCall(ExprMethodCall { method, .. }) => {
calls.insert(method.to_string());
}
Expr::Macro(expr_macro) => {
let macro_name = expr_macro
.mac
.path
.segments
.last()
.map(|seg| seg.ident.to_string())
.unwrap_or_else(|| "macro".to_string());
calls.insert(macro_name);
}
Expr::If(expr_if) => {
expr_calls(&expr_if.cond, calls);
count_calls(&expr_if.then_branch.stmts, calls);
if let Some((_, else_expr)) = &expr_if.else_branch {
expr_calls(else_expr, calls);
}
}
Expr::Match(expr_match) => {
expr_calls(&expr_match.expr, calls);
for arm in &expr_match.arms {
expr_calls(&arm.body, calls);
}
}
Expr::Loop(expr_loop) => {
count_calls(&expr_loop.body.stmts, calls);
}
Expr::While(expr_while) => {
expr_calls(&expr_while.cond, calls);
count_calls(&expr_while.body.stmts, calls);
}
Expr::ForLoop(expr_for) => {
expr_calls(&expr_for.expr, calls);
count_calls(&expr_for.body.stmts, calls);
}
Expr::Block(expr_block) => {
count_calls(&expr_block.block.stmts, calls);
}
_ => {}
}
}
let mut calls = HashSet::new();
count_calls(&block.stmts, &mut calls);
let mut names: Vec<String> = calls.into_iter().collect();
names.sort();
names
}
fn rust_non_structured_exits(block: &syn::Block) -> usize {
use syn::{Expr, ExprMethodCall, Stmt};
fn count_exits(stmts: &[Stmt], count: &mut usize, is_tail: bool) {
for (i, stmt) in stmts.iter().enumerate() {
let is_last = i == stmts.len() - 1;
match stmt {
Stmt::Expr(expr, _) => {
expr_exits(expr, count, is_tail && is_last);
}
Stmt::Local(local) => {
if let Some(init) = &local.init {
expr_exits(&init.expr, count, false);
}
}
_ => {}
}
}
}
fn expr_exits(expr: &Expr, count: &mut usize, is_tail: bool) {
match expr {
Expr::Return(_) if !is_tail => {
*count += 1;
}
Expr::Try(_) => {
*count += 1;
}
Expr::MethodCall(ExprMethodCall { method, .. }) => {
let method_name = method.to_string();
if matches!(
method_name.as_str(),
"unwrap" | "expect" | "unwrap_or_else" | "unwrap_or"
) {
*count += 1;
}
}
Expr::Macro(expr_macro) => {
if let Some(segment) = expr_macro.mac.path.segments.last() {
let macro_name = segment.ident.to_string();
if matches!(
macro_name.as_str(),
"panic" | "unreachable" | "unimplemented" | "todo"
) {
*count += 1;
}
}
}
Expr::If(expr_if) => {
expr_exits(&expr_if.cond, count, false);
count_exits(&expr_if.then_branch.stmts, count, false);
if let Some((_, else_expr)) = &expr_if.else_branch {
expr_exits(else_expr, count, false);
}
}
Expr::Match(expr_match) => {
expr_exits(&expr_match.expr, count, false);
for arm in &expr_match.arms {
expr_exits(&arm.body, count, false);
}
}
Expr::Loop(expr_loop) => {
count_exits(&expr_loop.body.stmts, count, false);
}
Expr::While(expr_while) => {
expr_exits(&expr_while.cond, count, false);
count_exits(&expr_while.body.stmts, count, false);
}
Expr::ForLoop(expr_for) => {
expr_exits(&expr_for.expr, count, false);
count_exits(&expr_for.body.stmts, count, false);
}
Expr::Block(expr_block) => {
count_exits(&expr_block.block.stmts, count, is_tail);
}
_ => {}
}
}
let mut count = 0;
count_exits(&block.stmts, &mut count, true);
count
}
fn rust_count_cc_extras(block: &syn::Block) -> usize {
use syn::{BinOp, Expr, Stmt};
fn count_extras(stmts: &[Stmt], count: &mut usize) {
for stmt in stmts {
match stmt {
Stmt::Expr(expr, _) => expr_extras(expr, count),
Stmt::Local(local) => {
if let Some(init) = &local.init {
expr_extras(&init.expr, count);
}
}
_ => {}
}
}
}
fn expr_extras(expr: &Expr, count: &mut usize) {
match expr {
Expr::Match(expr_match) => {
*count += expr_match.arms.len();
expr_extras(&expr_match.expr, count);
for arm in &expr_match.arms {
expr_extras(&arm.body, count);
}
}
Expr::Binary(expr_binary) => {
if matches!(expr_binary.op, BinOp::And(_) | BinOp::Or(_)) {
*count += 1;
}
expr_extras(&expr_binary.left, count);
expr_extras(&expr_binary.right, count);
}
Expr::If(expr_if) => {
expr_extras(&expr_if.cond, count);
count_extras(&expr_if.then_branch.stmts, count);
if let Some((_, else_expr)) = &expr_if.else_branch {
expr_extras(else_expr, count);
}
}
Expr::Loop(expr_loop) => {
count_extras(&expr_loop.body.stmts, count);
}
Expr::While(expr_while) => {
expr_extras(&expr_while.cond, count);
count_extras(&expr_while.body.stmts, count);
}
Expr::ForLoop(expr_for) => {
expr_extras(&expr_for.expr, count);
count_extras(&expr_for.body.stmts, count);
}
Expr::Block(expr_block) => {
count_extras(&expr_block.block.stmts, count);
}
_ => {}
}
}
let mut count = 0;
count_extras(&block.stmts, &mut count);
count
}
#[cfg(test)]
mod tests {
use super::*;
use crate::language::{
CfgBuilder, ECMAScriptCfgBuilder, ECMAScriptParser, GoCfgBuilder, GoParser, JavaCfgBuilder,
JavaParser, LanguageParser, PythonCfgBuilder, PythonParser, RustCfgBuilder,
};
fn go_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
let parser = GoParser::new().unwrap();
let module = parser.parse(source, "test.go").unwrap();
let functions = module.discover_functions(0, source);
assert!(!functions.is_empty(), "expected at least one Go function");
let func = functions.into_iter().next().unwrap();
let cfg = GoCfgBuilder.build(&func);
(func, cfg)
}
fn java_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
let parser = JavaParser::new().unwrap();
let module = parser.parse(source, "Test.java").unwrap();
let functions = module.discover_functions(0, source);
assert!(!functions.is_empty(), "expected at least one Java function");
let func = functions.into_iter().next().unwrap();
let cfg = JavaCfgBuilder.build(&func);
(func, cfg)
}
fn python_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
let parser = PythonParser::new().unwrap();
let module = parser.parse(source, "test.py").unwrap();
let functions = module.discover_functions(0, source);
assert!(
!functions.is_empty(),
"expected at least one Python function"
);
let func = functions.into_iter().next().unwrap();
let cfg = PythonCfgBuilder.build(&func);
(func, cfg)
}
fn ecmascript_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
use swc_common::{sync::Lrc, SourceMap};
let source_map: Lrc<SourceMap> = Default::default();
let parser = ECMAScriptParser::new(source_map);
let module = parser.parse(source, "test.ts").unwrap();
let functions = module.discover_functions(0, source);
assert!(
!functions.is_empty(),
"expected at least one ECMAScript function"
);
let func = functions.into_iter().next().unwrap();
let cfg = ECMAScriptCfgBuilder.build(&func);
(func, cfg)
}
fn rust_function_and_cfg(source: &str) -> (crate::ast::FunctionNode, crate::cfg::Cfg) {
use crate::ast::FunctionId;
use crate::language::{FunctionBody, SourceSpan};
let func = crate::ast::FunctionNode {
id: FunctionId {
file_index: 0,
local_index: 0,
},
name: Some("test".to_string()),
span: SourceSpan::new(0, source.len(), 1, 1, 0),
body: FunctionBody::Rust {
source: source.to_string(),
},
suppression_reason: None,
};
let cfg = RustCfgBuilder.build(&func);
(func, cfg)
}
#[test]
fn test_extract_go_simple_function() {
let source = r#"package main
func hello() {
println("hi")
}
"#;
let (func, cfg) = go_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(m.cc >= 1, "CC must be at least 1, got {}", m.cc);
assert_eq!(m.nd, 0, "no nesting");
assert_eq!(m.ns, 0, "no non-structured exits");
assert!(m.loc >= 3, "at least 3 lines");
assert!(m.callee_names.contains(&"println".to_string()));
assert_eq!(m.fo, m.callee_names.len());
}
#[test]
fn test_extract_go_if_increments_cc_and_nd() {
let source = r#"package main
func check(x int) string {
if x > 0 {
if x > 100 {
return "big"
}
return "positive"
}
return "non-positive"
}
"#;
let (func, cfg) = go_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(m.cc >= 2, "two branches → CC >= 2, got {}", m.cc);
assert!(m.nd >= 2, "nested ifs → ND >= 2, got {}", m.nd);
assert!(m.ns >= 2, "multiple returns → NS >= 2, got {}", m.ns);
}
#[test]
fn test_extract_go_callee_names_and_fanout() {
let source = r#"package main
import "fmt"
func doWork() {
fmt.Println("a")
fmt.Println("b")
len("x")
}
"#;
let (func, cfg) = go_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(
m.callee_names.contains(&"fmt.Println".to_string())
|| m.callee_names.iter().any(|n| n.contains("Println")),
"expected Println in callees: {:?}",
m.callee_names
);
assert_eq!(m.fo, m.callee_names.len(), "fo == callee_names.len()");
}
#[test]
fn test_extract_go_defer_counts_as_ns() {
let source = r#"package main
func withDefer() {
defer cleanup()
doWork()
}
"#;
let (func, cfg) = go_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(
m.ns >= 1,
"defer should count as non-structured exit, got {}",
m.ns
);
}
#[test]
fn test_extract_go_fallback_on_bad_source() {
use crate::ast::FunctionId;
use crate::language::{FunctionBody, SourceSpan};
let func = crate::ast::FunctionNode {
id: FunctionId {
file_index: 0,
local_index: 0,
},
name: Some("bad".to_string()),
span: SourceSpan::new(0, 0, 1, 1, 0),
body: FunctionBody::Go {
body_node: 0,
source: String::new(),
},
suppression_reason: None,
};
let cfg = crate::cfg::Cfg::new();
let m = extract_metrics(&func, &cfg);
assert_eq!(m.cc, 1, "fallback cc");
assert_eq!(m.nd, 0);
assert_eq!(m.fo, 0);
assert_eq!(m.ns, 0);
assert_eq!(m.loc, 0);
assert!(m.callee_names.is_empty());
}
#[test]
fn test_extract_java_simple_method() {
let source = r#"class Foo {
void greet() {
System.out.println("hello");
}
}
"#;
let (func, cfg) = java_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(m.cc >= 1, "CC must be at least 1, got {}", m.cc);
assert_eq!(m.nd, 0, "no nesting");
assert!(m.loc >= 3);
assert!(m.fo >= 1, "at least one callee");
}
#[test]
fn test_extract_java_if_increments_cc_and_nd() {
let source = r#"class Foo {
String classify(int x) {
if (x > 0) {
if (x > 100) {
return "big";
}
return "positive";
}
return "non-positive";
}
}
"#;
let (func, cfg) = java_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(m.cc >= 2, "two branches → CC >= 2, got {}", m.cc);
assert!(m.nd >= 2, "nested ifs → ND >= 2, got {}", m.nd);
assert!(m.ns >= 2, "multiple returns → NS >= 2, got {}", m.ns);
}
#[test]
fn test_extract_java_ternary_increments_cc() {
let source = r#"class Foo {
int sign(int x) {
return x > 0 ? 1 : -1;
}
}
"#;
let (func, cfg) = java_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(m.cc >= 2, "ternary → CC >= 2, got {}", m.cc);
}
#[test]
fn test_extract_java_fallback_on_bad_source() {
use crate::ast::FunctionId;
use crate::language::{FunctionBody, SourceSpan};
let func = crate::ast::FunctionNode {
id: FunctionId {
file_index: 0,
local_index: 0,
},
name: Some("bad".to_string()),
span: SourceSpan::new(0, 0, 1, 1, 0),
body: FunctionBody::Java {
body_node: 0,
source: String::new(),
},
suppression_reason: None,
};
let cfg = crate::cfg::Cfg::new();
let m = extract_metrics(&func, &cfg);
assert_eq!(m.cc, 1);
assert_eq!(m.fo, 0);
assert!(m.callee_names.is_empty());
}
#[test]
fn test_extract_python_simple_function() {
let source = r#"def greet():
print("hello")
"#;
let (func, cfg) = python_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(m.cc >= 1, "CC must be at least 1, got {}", m.cc);
assert_eq!(m.nd, 0, "no nesting");
assert!(m.loc >= 2);
assert!(m.callee_names.contains(&"print".to_string()));
assert_eq!(m.fo, m.callee_names.len());
}
#[test]
fn test_extract_python_if_increments_cc_and_nd() {
let source = r#"def classify(x):
if x > 0:
if x > 100:
return "big"
return "positive"
return "non-positive"
"#;
let (func, cfg) = python_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(m.cc >= 2, "two branches → CC >= 2, got {}", m.cc);
assert!(m.nd >= 2, "nested ifs → ND >= 2, got {}", m.nd);
assert!(m.ns >= 2, "multiple returns → NS >= 2, got {}", m.ns);
}
#[test]
fn test_extract_python_callee_names_and_fanout() {
let source = r#"def do_work():
foo()
bar()
foo()
"#;
let (func, cfg) = python_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(
m.callee_names.contains(&"foo".to_string()),
"callee_names: {:?}",
m.callee_names
);
assert!(
m.callee_names.contains(&"bar".to_string()),
"callee_names: {:?}",
m.callee_names
);
assert_eq!(m.fo, m.callee_names.len(), "fo == callee_names.len()");
}
#[test]
fn test_extract_python_fallback_on_bad_source() {
use crate::ast::FunctionId;
use crate::language::{FunctionBody, SourceSpan};
let func = crate::ast::FunctionNode {
id: FunctionId {
file_index: 0,
local_index: 0,
},
name: Some("bad".to_string()),
span: SourceSpan::new(0, 0, 1, 1, 0),
body: FunctionBody::Python {
body_node: 0,
source: String::new(),
},
suppression_reason: None,
};
let cfg = crate::cfg::Cfg::new();
let m = extract_metrics(&func, &cfg);
assert_eq!(m.cc, 1);
assert_eq!(m.fo, 0);
assert!(m.callee_names.is_empty());
}
#[test]
fn test_extract_ecmascript_callee_names_direct_call() {
let source = r#"function doWork() { foo(); bar(); foo(); }"#;
let (func, cfg) = ecmascript_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(
m.callee_names.contains(&"foo".to_string()),
"callee_names: {:?}",
m.callee_names
);
assert!(
m.callee_names.contains(&"bar".to_string()),
"callee_names: {:?}",
m.callee_names
);
assert_eq!(m.fo, m.callee_names.len(), "fo == callee_names.len()");
}
#[test]
fn test_extract_ecmascript_callee_names_method_call() {
let source = r#"function doWork() { obj.method(); console.log("x"); }"#;
let (func, cfg) = ecmascript_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(m.fo >= 1, "method calls counted in fo");
assert_eq!(m.fo, m.callee_names.len());
}
#[test]
fn test_extract_ecmascript_no_calls() {
let source = r#"function pure(x: number) { return x + 1; }"#;
let (func, cfg) = ecmascript_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert_eq!(m.fo, 0, "no calls → fo=0");
assert!(m.callee_names.is_empty(), "no calls → empty callee_names");
}
#[test]
fn test_extract_ecmascript_computed_callee_filtered() {
let source = r#"function dyn(arr: any[]) { arr[0](); }"#;
let (func, cfg) = ecmascript_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(
!m.callee_names.iter().any(|n| n == "<computed>"),
"computed callees must be filtered: {:?}",
m.callee_names
);
}
#[test]
fn test_extract_rust_callee_names_function_call() {
let source = r#"fn do_work() { foo(); bar(); foo(); }"#;
let (func, cfg) = rust_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(
m.callee_names.contains(&"foo".to_string()),
"callee_names: {:?}",
m.callee_names
);
assert!(
m.callee_names.contains(&"bar".to_string()),
"callee_names: {:?}",
m.callee_names
);
assert_eq!(m.fo, m.callee_names.len(), "fo == callee_names.len()");
}
#[test]
fn test_extract_rust_callee_names_method_call() {
let source = r#"fn do_work(v: Vec<i32>) -> usize { v.len() }"#;
let (func, cfg) = rust_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(
m.callee_names.contains(&"len".to_string()),
"method call in callee_names: {:?}",
m.callee_names
);
assert_eq!(m.fo, m.callee_names.len());
}
#[test]
fn test_extract_rust_callee_names_macro_call() {
let source = r#"fn greet() { println!("hello"); }"#;
let (func, cfg) = rust_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert!(
m.callee_names.contains(&"println".to_string()),
"macro in callee_names: {:?}",
m.callee_names
);
assert_eq!(m.fo, m.callee_names.len());
}
#[test]
fn test_extract_rust_no_calls() {
let source = r#"fn pure(x: i32) -> i32 { x + 1 }"#;
let (func, cfg) = rust_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert_eq!(m.fo, 0, "no calls → fo=0");
assert!(m.callee_names.is_empty(), "no calls → empty callee_names");
}
#[test]
fn test_extract_rust_deduplication() {
let source = r#"fn work() { foo(); foo(); foo(); bar(); }"#;
let (func, cfg) = rust_function_and_cfg(source);
let m = extract_metrics(&func, &cfg);
assert_eq!(m.fo, 2, "deduplicated: foo+bar = 2");
assert_eq!(m.callee_names, vec!["bar", "foo"], "sorted callee_names");
}
}