impl RuchyComplexityAnalyzer {
fn analyze_node(&mut self, node: &RuchyAst) {
match node {
RuchyAst::Function {
name,
body,
line_start,
line_end,
..
} => {
self.analyze_function(name, body, *line_start, *line_end);
}
RuchyAst::If {
condition,
then_branch,
else_branch,
} => {
self.analyze_if(condition, then_branch, else_branch.as_deref());
}
RuchyAst::While { condition, body } => {
self.analyze_while(condition, body);
}
RuchyAst::For { body, .. } => {
self.analyze_for(body);
}
RuchyAst::Match { expr, arms } => {
self.analyze_match(expr, arms);
}
RuchyAst::BinaryOp { left, op, right } => {
self.analyze_binary_op(left, op, right);
}
RuchyAst::Block { statements } => {
self.analyze_block(statements);
}
RuchyAst::Import {
module,
items,
line,
} => {
self.analyze_import(module, items, *line);
}
RuchyAst::Export { items, .. } => {
self.analyze_export(items);
}
RuchyAst::Actor {
name,
state,
handlers,
line_start,
line_end,
} => {
self.analyze_actor(name, state, handlers, *line_start, *line_end);
}
_ => {
}
}
}
fn analyze_function(&mut self, name: &str, body: &RuchyAst, line_start: u32, line_end: u32) {
self.defined_functions.insert(name.to_string());
self.track_operator("fun");
self.track_operand(name);
let prev_complexity = self.current_complexity;
let prev_nesting = self.nesting_level;
self.current_complexity = ComplexityMetrics {
cyclomatic: 1,
cognitive: 0,
nesting_max: 0,
lines: (line_end - line_start) as u16,
halstead: None,
};
self.nesting_level = 0;
self.reset_halstead();
self.analyze_node(body);
let halstead = self.calculate_halstead();
self.current_complexity.halstead = Some(halstead);
self.functions.push(FunctionComplexity {
name: name.to_string(),
line_start,
line_end,
metrics: self.current_complexity,
});
self.current_complexity = prev_complexity;
self.nesting_level = prev_nesting;
}
fn analyze_if(
&mut self,
condition: &RuchyAst,
then_branch: &RuchyAst,
else_branch: Option<&RuchyAst>,
) {
self.current_complexity.cyclomatic += 1;
self.current_complexity.cognitive += 1 + u16::from(self.nesting_level);
self.track_operator("if");
self.nesting_level += 1;
self.current_complexity.nesting_max =
self.current_complexity.nesting_max.max(self.nesting_level);
self.analyze_node(condition);
self.analyze_node(then_branch);
if let Some(else_br) = else_branch {
self.current_complexity.cyclomatic += 1;
self.track_operator("else");
self.analyze_node(else_br);
}
self.nesting_level -= 1;
}
fn analyze_while(&mut self, condition: &RuchyAst, body: &RuchyAst) {
self.current_complexity.cyclomatic += 1;
self.current_complexity.cognitive += 1 + u16::from(self.nesting_level);
self.nesting_level += 1;
self.current_complexity.nesting_max =
self.current_complexity.nesting_max.max(self.nesting_level);
self.analyze_node(condition);
self.analyze_node(body);
self.nesting_level -= 1;
}
fn analyze_for(&mut self, body: &RuchyAst) {
self.current_complexity.cyclomatic += 1;
self.current_complexity.cognitive += 1 + u16::from(self.nesting_level);
self.nesting_level += 1;
self.current_complexity.nesting_max =
self.current_complexity.nesting_max.max(self.nesting_level);
self.analyze_node(body);
self.nesting_level -= 1;
}
fn analyze_match(&mut self, expr: &RuchyAst, arms: &[(RuchyAst, RuchyAst)]) {
let arm_count = arms.len() as u16;
self.current_complexity.cyclomatic += arm_count;
self.current_complexity.cognitive += (arm_count * 2) + u16::from(self.nesting_level);
self.track_operator("match");
self.nesting_level += 1;
self.current_complexity.nesting_max =
self.current_complexity.nesting_max.max(self.nesting_level);
self.analyze_node(expr);
for (pattern, body) in arms {
self.analyze_pattern_complexity(pattern);
self.analyze_node(body);
}
self.nesting_level -= 1;
}
fn analyze_binary_op(&mut self, left: &RuchyAst, op: &RuchyToken, right: &RuchyAst) {
let op_str = Self::get_operator_string(op);
self.track_operator(op_str);
self.handle_logical_operator_complexity(op);
self.analyze_node(left);
self.analyze_node(right);
}
fn get_operator_string(op: &RuchyToken) -> &'static str {
match op {
RuchyToken::Plus => "+",
RuchyToken::Minus => "-",
RuchyToken::Star => "*",
RuchyToken::Slash => "/",
RuchyToken::Percent => "%",
RuchyToken::EqualEqual => "==",
RuchyToken::NotEqual => "!=",
RuchyToken::Less => "<",
RuchyToken::Greater => ">",
RuchyToken::LessEqual => "<=",
RuchyToken::GreaterEqual => ">=",
RuchyToken::And => "&&",
RuchyToken::Or => "||",
RuchyToken::PipeForward => "|>",
_ => "op",
}
}
fn handle_logical_operator_complexity(&mut self, op: &RuchyToken) {
if matches!(op, RuchyToken::And | RuchyToken::Or) {
self.current_complexity.cyclomatic += 1;
self.current_complexity.cognitive += 1;
}
}
fn analyze_block(&mut self, statements: &[RuchyAst]) {
for stmt in statements {
self.analyze_node(stmt);
}
}
fn analyze_import(&mut self, module: &str, items: &[String], line: u32) {
self.imports.push(RuchyImport {
module: module.to_string(),
items: items.to_vec(),
line,
});
self.track_operator("import");
self.track_operand(module);
}
fn analyze_export(&mut self, items: &[String]) {
for item in items {
self.exports.insert(item.clone());
}
self.track_operator("export");
}
fn analyze_actor(
&mut self,
name: &str,
state: &[(String, String)],
handlers: &[RuchyAst],
line_start: u32,
line_end: u32,
) {
self.track_operator("actor");
self.track_operand(name);
let prev_actor = self.current_actor.clone();
self.current_actor = Some(name.to_string());
let mut actor_info = ActorInfo {
name: name.to_string(),
state_fields: state.iter().map(|(field, _)| field.clone()).collect(),
message_handlers: Vec::new(),
spawned_actors: Vec::new(),
line_start,
line_end,
};
let mut class_complexity = ComplexityMetrics::default();
for handler in handlers {
if let RuchyAst::Function {
name: handler_name, ..
} = handler
{
actor_info.message_handlers.push(handler_name.clone());
}
self.analyze_node(handler);
if let RuchyAst::Function { .. } = handler {
if let Some(func) = self.functions.last() {
class_complexity.cyclomatic += func.metrics.cyclomatic;
class_complexity.cognitive += func.metrics.cognitive;
class_complexity.nesting_max =
class_complexity.nesting_max.max(func.metrics.nesting_max);
}
}
}
self.actors.push(actor_info);
self.classes
.push(crate::services::complexity::ClassComplexity {
name: name.to_string(),
line_start,
line_end,
metrics: class_complexity,
methods: vec![],
});
self.current_actor = prev_actor;
}
pub fn analyze_program(&mut self, ast: &RuchyAst) -> FileComplexityMetrics {
if let RuchyAst::Program { items } = ast {
for item in items {
self.analyze_node(item);
}
} else {
self.analyze_node(ast);
}
let total_complexity = ComplexityMetrics {
cyclomatic: self
.functions
.iter()
.map(|f| f.metrics.cyclomatic)
.sum::<u16>()
.max(1),
cognitive: self
.functions
.iter()
.map(|f| f.metrics.cognitive)
.sum::<u16>()
.max(1),
nesting_max: self
.functions
.iter()
.map(|f| f.metrics.nesting_max)
.max()
.unwrap_or(0),
lines: self.functions.iter().map(|f| f.metrics.lines).sum::<u16>(),
halstead: None,
};
FileComplexityMetrics {
path: String::new(), total_complexity,
functions: self.functions.clone(),
classes: self.classes.clone(),
}
}
}