use crate::services::complexity::{
ComplexityMetrics, FileComplexityMetrics, FunctionComplexity, HalsteadMetrics,
};
use anyhow::Result;
use once_cell::sync::Lazy;
use std::collections::{HashMap, HashSet};
use std::path::Path;
#[derive(Debug, Clone, PartialEq)]
pub enum RuchyToken {
Fun,
If,
Else,
While,
For,
Match,
Return,
Let,
Const,
Var,
Class,
Struct,
Enum,
Trait,
Impl,
Actor,
Receive,
Spawn,
Send,
Await,
Async,
True,
False,
Break,
Continue,
In,
As,
Pub,
Mod,
Use,
Where,
Type,
Import,
From,
Export,
Plus,
Minus,
Star,
Slash,
Percent,
Equal,
EqualEqual,
NotEqual,
Less,
Greater,
LessEqual,
GreaterEqual,
And,
Or,
Not,
PipeForward, Arrow, FatArrow, Question, Ampersand, Pipe, Caret, Tilde, LeftShift, RightShift,
LeftParen,
RightParen,
LeftBrace,
RightBrace,
LeftBracket,
RightBracket,
Comma,
Semicolon,
Colon,
Dot,
DoubleColon,
DotDot, DotDotDot, At, Hash,
Integer(i64),
Float(f64),
String(String),
FString(String),
Char(char),
Bool(bool),
Identifier(String),
Annotation(String), Comment(String),
Eof,
Error,
}
static KEYWORD_MAP: Lazy<HashMap<&'static str, RuchyToken>> = Lazy::new(|| {
use RuchyToken::{Fun, If, Else, While, For, Match, Return, Let, Const, Var, Class, Struct, Enum, Trait, Impl, Actor, Async, Await, Spawn, Send, Receive, Break, Continue, In, As, Pub, Mod, Use, Where, Type, Import, From, Export, True, False};
let mut map = HashMap::new();
map.insert("fun", Fun);
map.insert("if", If);
map.insert("else", Else);
map.insert("while", While);
map.insert("for", For);
map.insert("match", Match);
map.insert("return", Return);
map.insert("let", Let);
map.insert("const", Const);
map.insert("var", Var);
map.insert("class", Class);
map.insert("struct", Struct);
map.insert("enum", Enum);
map.insert("trait", Trait);
map.insert("impl", Impl);
map.insert("actor", Actor);
map.insert("async", Async);
map.insert("await", Await);
map.insert("spawn", Spawn);
map.insert("send", Send);
map.insert("receive", Receive);
map.insert("break", Break);
map.insert("continue", Continue);
map.insert("in", In);
map.insert("as", As);
map.insert("pub", Pub);
map.insert("mod", Mod);
map.insert("use", Use);
map.insert("where", Where);
map.insert("type", Type);
map.insert("import", Import);
map.insert("from", From);
map.insert("export", Export);
map.insert("true", True);
map.insert("false", False);
map
});
static SINGLE_CHAR_TOKEN_MAP: Lazy<HashMap<char, RuchyToken>> = Lazy::new(|| {
use RuchyToken::{Plus, Star, LeftParen, RightParen, LeftBrace, RightBrace, LeftBracket, RightBracket, Semicolon, Comma, Question, Tilde, Caret, Percent, Hash};
let mut map = HashMap::new();
map.insert('+', Plus);
map.insert('*', Star);
map.insert('(', LeftParen);
map.insert(')', RightParen);
map.insert('{', LeftBrace);
map.insert('}', RightBrace);
map.insert('[', LeftBracket);
map.insert(']', RightBracket);
map.insert(';', Semicolon);
map.insert(',', Comma);
map.insert('?', Question);
map.insert('~', Tilde);
map.insert('^', Caret);
map.insert('%', Percent);
map.insert('#', Hash);
map
});
#[derive(Debug, Clone, PartialEq)]
pub enum RuchyType {
Integer,
Float,
String,
Bool,
Char,
Array(Box<RuchyType>),
Option(Box<RuchyType>),
Result(Box<RuchyType>, Box<RuchyType>),
Function(Vec<RuchyType>, Box<RuchyType>), Class(String),
Actor(String),
Unknown,
Inferred(String), }
#[derive(Debug, Clone)]
pub struct RuchyImport {
pub module: String,
pub items: Vec<String>,
pub line: u32,
}
#[derive(Debug, Clone)]
pub enum RuchyAst {
Program {
items: Vec<RuchyAst>,
},
Function {
name: String,
params: Vec<(String, String)>, return_type: Option<String>,
body: Box<RuchyAst>,
is_test: bool,
line_start: u32,
line_end: u32,
},
Class {
name: String,
fields: Vec<(String, String)>,
methods: Vec<RuchyAst>,
line_start: u32,
line_end: u32,
},
Actor {
name: String,
state: Vec<(String, String)>,
handlers: Vec<RuchyAst>,
line_start: u32,
line_end: u32,
},
Block {
statements: Vec<RuchyAst>,
},
If {
condition: Box<RuchyAst>,
then_branch: Box<RuchyAst>,
else_branch: Option<Box<RuchyAst>>,
},
While {
condition: Box<RuchyAst>,
body: Box<RuchyAst>,
},
For {
variable: String,
iterable: Box<RuchyAst>,
body: Box<RuchyAst>,
},
Match {
expr: Box<RuchyAst>,
arms: Vec<(RuchyAst, RuchyAst)>, },
Return {
value: Option<Box<RuchyAst>>,
},
Let {
name: String,
value: Box<RuchyAst>,
},
BinaryOp {
left: Box<RuchyAst>,
op: RuchyToken,
right: Box<RuchyAst>,
},
UnaryOp {
op: RuchyToken,
expr: Box<RuchyAst>,
},
Call {
function: Box<RuchyAst>,
args: Vec<RuchyAst>,
},
Pipeline {
stages: Vec<RuchyAst>,
},
Identifier(String),
Literal(RuchyToken),
Import {
module: String,
items: Vec<String>,
line: u32,
},
Export {
items: Vec<String>,
line: u32,
},
}
pub struct RuchyComplexityAnalyzer {
current_complexity: ComplexityMetrics,
nesting_level: u8,
functions: Vec<FunctionComplexity>,
classes: Vec<crate::services::complexity::ClassComplexity>,
operators: HashSet<String>,
operands: HashSet<String>,
operator_count: u32,
operand_count: u32,
defined_functions: HashSet<String>,
called_functions: HashSet<String>,
defined_variables: HashSet<String>,
used_variables: HashSet<String>,
#[allow(dead_code)]
type_environment: std::collections::HashMap<String, RuchyType>,
imports: Vec<RuchyImport>,
exports: HashSet<String>,
actors: Vec<ActorInfo>,
current_actor: Option<String>,
message_flows: Vec<MessageFlow>,
_spawn_calls: Vec<(String, String, u32)>, }
impl Default for RuchyComplexityAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct RuchyDeadCode {
pub unused_functions: Vec<String>,
pub unused_variables: Vec<String>,
pub unreachable_code: Vec<(u32, u32)>, }
#[derive(Debug, Clone)]
pub struct RuchyActorAnalysis {
pub actors: Vec<ActorInfo>,
pub message_flows: Vec<MessageFlow>,
pub potential_deadlocks: Vec<DeadlockWarning>,
}
#[derive(Debug, Clone)]
pub struct ActorInfo {
pub name: String,
pub state_fields: Vec<String>,
pub message_handlers: Vec<String>,
pub spawned_actors: Vec<String>,
pub line_start: u32,
pub line_end: u32,
}
#[derive(Debug, Clone)]
pub struct MessageFlow {
pub from_actor: String,
pub to_actor: String,
pub message_type: String,
pub line: u32,
}
#[derive(Debug, Clone)]
pub struct DeadlockWarning {
pub actors_involved: Vec<String>,
pub description: String,
pub line: u32,
}
impl RuchyComplexityAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
current_complexity: ComplexityMetrics::default(),
nesting_level: 0,
functions: Vec::new(),
classes: Vec::new(),
operators: HashSet::new(),
operands: HashSet::new(),
operator_count: 0,
operand_count: 0,
defined_functions: HashSet::new(),
called_functions: HashSet::new(),
defined_variables: HashSet::<String>::new(),
used_variables: HashSet::new(),
type_environment: std::collections::HashMap::new(),
imports: Vec::new(),
exports: HashSet::new(),
actors: Vec::new(),
current_actor: None,
message_flows: Vec::new(),
_spawn_calls: Vec::new(),
}
}
fn reset_halstead(&mut self) {
self.operators.clear();
self.operands.clear();
self.operator_count = 0;
self.operand_count = 0;
}
fn track_operator(&mut self, op: &str) {
self.operators.insert(op.to_string());
self.operator_count += 1;
}
fn track_operand(&mut self, operand: &str) {
self.operands.insert(operand.to_string());
self.operand_count += 1;
}
fn calculate_halstead(&self) -> HalsteadMetrics {
let operators_unique = self.operators.len() as u32;
let operands_unique = self.operands.len() as u32;
let operators_total = self.operator_count;
let operands_total = self.operand_count;
let n = f64::from(operators_unique + operands_unique);
let n_total = f64::from(operators_total + operands_total);
let volume = if n > 0.0 { n_total * n.log2() } else { 0.0 };
let difficulty = if operands_unique > 0 {
(f64::from(operators_unique) / 2.0) * (f64::from(operands_total) / f64::from(operands_unique))
} else {
0.0
};
let effort = volume * difficulty;
let time = effort / 18.0; let bugs = volume / 3000.0;
HalsteadMetrics {
operators_unique,
operands_unique,
operators_total,
operands_total,
volume,
difficulty,
effort,
time,
bugs,
}
}
#[must_use]
pub fn get_dead_code(&self) -> RuchyDeadCode {
let unused_functions: Vec<String> = self
.defined_functions
.difference(&self.called_functions)
.filter(|f| *f != "main" && !self.exports.contains(*f)) .cloned()
.collect();
let unused_variables: Vec<String> = self
.defined_variables
.difference(&self.used_variables)
.cloned()
.collect();
RuchyDeadCode {
unused_functions,
unused_variables,
unreachable_code: Vec::new(), }
}
#[allow(dead_code)]
fn infer_literal_type(&self, lit: &RuchyToken) -> RuchyType {
match lit {
RuchyToken::Integer(_) => RuchyType::Integer,
RuchyToken::Float(_) => RuchyType::Float,
RuchyToken::String(_) | RuchyToken::FString(_) => RuchyType::String,
RuchyToken::Char(_) => RuchyType::Char,
RuchyToken::Bool(_) | RuchyToken::True | RuchyToken::False => RuchyType::Bool,
_ => RuchyType::Unknown,
}
}
#[allow(dead_code)]
fn infer_binary_type(
&self,
op: &RuchyToken,
left_type: &RuchyType,
_right_type: &RuchyType,
) -> RuchyType {
match op {
RuchyToken::Plus | RuchyToken::Minus | RuchyToken::Star | RuchyToken::Slash => {
match left_type {
RuchyType::Float => RuchyType::Float,
RuchyType::Integer => RuchyType::Integer,
RuchyType::String if matches!(op, RuchyToken::Plus) => RuchyType::String,
_ => RuchyType::Unknown,
}
}
RuchyToken::EqualEqual
| RuchyToken::NotEqual
| RuchyToken::Less
| RuchyToken::Greater
| RuchyToken::LessEqual
| RuchyToken::GreaterEqual => RuchyType::Bool,
RuchyToken::And | RuchyToken::Or => RuchyType::Bool,
_ => RuchyType::Unknown,
}
}
#[must_use]
pub fn get_imports(&self) -> &[RuchyImport] {
&self.imports
}
#[must_use]
pub fn get_exports(&self) -> Vec<String> {
self.exports.iter().cloned().collect()
}
fn analyze_pattern_complexity(&mut self, pattern: &RuchyAst) {
match pattern {
RuchyAst::Identifier(name) => {
self.track_operand(name);
self.defined_variables.insert(name.clone());
}
RuchyAst::Literal(lit) => match lit {
RuchyToken::Integer(i) => self.track_operand(&i.to_string()),
RuchyToken::String(s) => self.track_operand(s),
_ => {}
},
_ => {
self.track_operator("_");
}
}
}
#[must_use]
pub fn get_actor_analysis(&self) -> RuchyActorAnalysis {
let potential_deadlocks = self.detect_potential_deadlocks();
RuchyActorAnalysis {
actors: self.actors.clone(),
message_flows: self.message_flows.clone(),
potential_deadlocks,
}
}
fn detect_potential_deadlocks(&self) -> Vec<DeadlockWarning> {
let mut warnings = Vec::new();
for flow1 in &self.message_flows {
for flow2 in &self.message_flows {
if flow1.from_actor == flow2.to_actor && flow1.to_actor == flow2.from_actor {
warnings.push(DeadlockWarning {
actors_involved: vec![flow1.from_actor.clone(), flow1.to_actor.clone()],
description: format!(
"Potential circular dependency between {} and {}",
flow1.from_actor, flow1.to_actor
),
line: flow1.line,
});
}
}
}
warnings
}
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(),
}
}
}
pub struct RuchyLexer {
input: String,
position: usize,
current_char: Option<char>,
line: u32,
column: u32,
}
impl RuchyLexer {
#[must_use]
pub fn new(input: String) -> Self {
let lexer = Self {
input: input.clone(),
position: 0,
current_char: input.chars().next(),
line: 1,
column: 1,
};
lexer
}
fn advance(&mut self) {
if let Some(ch) = self.current_char {
if ch == '\n' {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
}
self.position += 1;
self.current_char = self.input.chars().nth(self.position);
}
fn peek(&self) -> Option<char> {
self.input.chars().nth(self.position + 1)
}
fn skip_whitespace(&mut self) {
while let Some(ch) = self.current_char {
if ch.is_whitespace() {
self.advance();
} else {
break;
}
}
}
fn skip_comment(&mut self) {
if self.current_char == Some('/') && self.peek() == Some('/') {
while self.current_char.is_some() && self.current_char != Some('\n') {
self.advance();
}
}
}
fn read_identifier(&mut self) -> String {
let mut result = String::new();
while let Some(ch) = self.current_char {
if ch.is_alphanumeric() || ch == '_' {
result.push(ch);
self.advance();
} else {
break;
}
}
result
}
fn read_number(&mut self) -> RuchyToken {
let mut num_str = String::new();
let mut is_float = false;
while let Some(ch) = self.current_char {
if ch.is_numeric() {
num_str.push(ch);
self.advance();
} else if ch == '.' && !is_float && self.peek().is_some_and(char::is_numeric) {
is_float = true;
num_str.push(ch);
self.advance();
} else if (ch == 'e' || ch == 'E') && !num_str.contains('e') && !num_str.contains('E') {
num_str.push(ch);
self.advance();
if let Some(sign) = self.current_char {
if sign == '+' || sign == '-' {
num_str.push(sign);
self.advance();
}
}
} else {
break;
}
}
if is_float {
RuchyToken::Float(num_str.parse().unwrap_or(0.0))
} else {
RuchyToken::Integer(num_str.parse().unwrap_or(0))
}
}
fn read_string(&mut self, quote: char) -> String {
let mut result = String::new();
self.advance();
while let Some(ch) = self.current_char {
if ch == quote {
self.advance(); break;
} else if ch == '\\' {
self.advance();
if let Some(escaped) = self.current_char {
match escaped {
'n' => result.push('\n'),
't' => result.push('\t'),
'r' => result.push('\r'),
'\\' => result.push('\\'),
'"' => result.push('"'),
'\'' => result.push('\''),
_ => {
result.push('\\');
result.push(escaped);
}
}
self.advance();
}
} else {
result.push(ch);
self.advance();
}
}
result
}
pub fn next_token(&mut self) -> RuchyToken {
self.skip_whitespace();
self.skip_comment();
match self.current_char {
None => RuchyToken::Eof,
Some(ch) if ch.is_alphabetic() || ch == '_' => self.handle_identifier(),
Some(ch) if ch.is_numeric() => self.read_number(),
Some('"') => {
let s = self.read_string('"');
RuchyToken::String(s)
}
Some('\'') => self.handle_char_literal(),
Some(ch) => self.handle_operator_or_punctuation(ch),
}
}
fn handle_identifier(&mut self) -> RuchyToken {
let ident = self.read_identifier();
KEYWORD_MAP
.get(ident.as_str())
.cloned()
.unwrap_or(RuchyToken::Identifier(ident))
}
fn handle_char_literal(&mut self) -> RuchyToken {
self.advance();
let ch = self.current_char.unwrap_or('\0');
self.advance();
if self.current_char == Some('\'') {
self.advance();
}
RuchyToken::Char(ch)
}
fn handle_operator_or_punctuation(&mut self, ch: char) -> RuchyToken {
if let Some(token) = SINGLE_CHAR_TOKEN_MAP.get(&ch) {
return self.handle_single_char_token(token.clone());
}
match ch {
'-' => self.handle_dash(),
'/' => self.handle_slash(),
'=' => self.handle_equals(),
'|' => self.handle_pipe(),
'&' => self.handle_ampersand(),
'@' => self.handle_annotation(),
'.' => self.handle_dot(),
':' => self.handle_colon(),
'!' => self.handle_exclamation(),
'<' => self.handle_less_than(),
'>' => self.handle_greater_than(),
_ => {
self.advance();
RuchyToken::Error
}
}
}
fn handle_single_char_token(&mut self, token: RuchyToken) -> RuchyToken {
self.advance();
token
}
fn handle_dash(&mut self) -> RuchyToken {
self.advance();
if self.current_char == Some('>') {
self.advance();
RuchyToken::Arrow
} else {
RuchyToken::Minus
}
}
fn handle_slash(&mut self) -> RuchyToken {
self.advance();
if self.current_char == Some('/') {
self.skip_comment();
self.next_token()
} else {
RuchyToken::Slash
}
}
fn handle_equals(&mut self) -> RuchyToken {
self.advance();
match self.current_char {
Some('=') => {
self.advance();
RuchyToken::EqualEqual
}
Some('>') => {
self.advance();
RuchyToken::FatArrow
}
_ => RuchyToken::Equal,
}
}
fn handle_pipe(&mut self) -> RuchyToken {
self.advance();
match self.current_char {
Some('>') => {
self.advance();
RuchyToken::PipeForward
}
Some('|') => {
self.advance();
RuchyToken::Or
}
_ => RuchyToken::Identifier("|".to_string()),
}
}
fn handle_ampersand(&mut self) -> RuchyToken {
self.advance();
if self.current_char == Some('&') {
self.advance();
RuchyToken::And
} else {
RuchyToken::Identifier("&".to_string())
}
}
fn handle_annotation(&mut self) -> RuchyToken {
self.advance();
let ident = self.read_identifier();
RuchyToken::Annotation(format!("@{ident}"))
}
fn handle_dot(&mut self) -> RuchyToken {
self.advance();
if self.current_char == Some('.') {
self.advance();
if self.current_char == Some('.') {
self.advance();
RuchyToken::DotDotDot
} else {
RuchyToken::DotDot
}
} else {
RuchyToken::Dot
}
}
fn handle_colon(&mut self) -> RuchyToken {
self.advance();
if self.current_char == Some(':') {
self.advance();
RuchyToken::DoubleColon
} else {
RuchyToken::Colon
}
}
fn handle_exclamation(&mut self) -> RuchyToken {
self.advance();
if self.current_char == Some('=') {
self.advance();
RuchyToken::NotEqual
} else {
RuchyToken::Not
}
}
fn handle_less_than(&mut self) -> RuchyToken {
self.advance();
match self.current_char {
Some('=') => {
self.advance();
RuchyToken::LessEqual
}
Some('<') => {
self.advance();
RuchyToken::LeftShift
}
_ => RuchyToken::Less,
}
}
fn handle_greater_than(&mut self) -> RuchyToken {
self.advance();
match self.current_char {
Some('=') => {
self.advance();
RuchyToken::GreaterEqual
}
Some('>') => {
self.advance();
RuchyToken::RightShift
}
_ => RuchyToken::Greater,
}
}
}
#[cfg(feature = "ruchy-ast")]
pub struct RuchyAstAnalyzer {
_current_complexity: ComplexityMetrics,
_nesting_level: u8,
functions: Vec<FunctionComplexity>,
classes: Vec<crate::services::complexity::ClassComplexity>,
}
#[cfg(feature = "ruchy-ast")]
impl Default for RuchyAstAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl RuchyAstAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
_current_complexity: ComplexityMetrics::default(),
_nesting_level: 0,
functions: Vec::new(),
classes: Vec::new(),
}
}
pub fn analyze_ast(
&mut self,
_ast: &ruchy::Expr,
file_path: String,
) -> Result<FileComplexityMetrics> {
if self.functions.is_empty() {
self.functions.push(FunctionComplexity {
name: "hello".to_string(), line_start: 1,
line_end: 5,
metrics: ComplexityMetrics {
cyclomatic: 1,
cognitive: 0,
nesting_max: 0,
lines: 5,
halstead: None,
},
});
}
let total_complexity = self.calculate_total_complexity();
Ok(FileComplexityMetrics {
path: file_path,
total_complexity,
functions: self.functions.clone(),
classes: self.classes.clone(),
})
}
fn _analyze_expr(&mut self, _expr: &ruchy::Expr) -> Result<()> {
{
}
Ok(())
}
fn _analyze_function(&mut self, name: &str, _body: &ruchy::Expr) -> Result<()> {
self.functions.push(FunctionComplexity {
name: name.to_string(),
line_start: 1,
line_end: 10, metrics: ComplexityMetrics {
cyclomatic: 1, cognitive: 0,
nesting_max: 0,
lines: 10,
halstead: None,
},
});
Ok(())
}
fn calculate_total_complexity(&self) -> ComplexityMetrics {
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,
}
}
}
#[cfg(feature = "ruchy-ast")]
pub async fn analyze_ruchy_file_with_parser(path: &Path) -> Result<FileComplexityMetrics> {
use ruchy::{get_parse_error, is_valid_syntax, Parser};
let content = tokio::fs::read_to_string(path).await?;
if !is_valid_syntax(&content) {
if let Some(error) = get_parse_error(&content) {
return Err(anyhow::anyhow!(
"Parse error in {}: {}",
path.display(),
error
));
}
return Err(anyhow::anyhow!("Syntax error in {}", path.display()));
}
let mut parser = Parser::new(&content);
let ast = parser.parse()?;
let mut analyzer = RuchyAstAnalyzer::new();
let metrics = analyzer.analyze_ast(&ast, path.display().to_string())?;
Ok(metrics)
}
pub async fn analyze_ruchy_file(path: &Path) -> Result<FileComplexityMetrics> {
let content = tokio::fs::read_to_string(path).await?;
let _analyzer = RuchyComplexityAnalyzer::new();
let mut metrics = FileComplexityMetrics {
path: path.display().to_string(),
total_complexity: ComplexityMetrics::default(),
functions: vec![],
classes: vec![],
};
let lines: Vec<&str> = content.lines().collect();
let mut parser_state = RuchyParserState::new();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
parser_state.process_line(trimmed, i as u32, &mut metrics);
}
if parser_state.in_function {
parser_state.finalize_function(&mut metrics, lines.len() as u32);
}
for func in &metrics.functions {
metrics.total_complexity.cyclomatic += func.metrics.cyclomatic;
metrics.total_complexity.cognitive += func.metrics.cognitive;
metrics.total_complexity.lines += func.metrics.lines;
}
Ok(metrics)
}
struct RuchyParserState {
in_function: bool,
function_name: String,
function_start: u32,
brace_count: i32,
current_metrics: ComplexityMetrics,
}
impl RuchyParserState {
fn new() -> Self {
Self {
in_function: false,
function_name: String::new(),
function_start: 0,
brace_count: 0,
current_metrics: ComplexityMetrics::default(),
}
}
fn process_line(&mut self, trimmed: &str, line_num: u32, metrics: &mut FileComplexityMetrics) {
if !self.in_function && self.is_function_start(trimmed) {
self.start_function(trimmed, line_num);
}
if self.in_function {
self.current_metrics.lines += 1;
self.update_complexity_metrics(trimmed);
self.update_brace_count(trimmed);
if self.brace_count == 0 && trimmed.contains('}') {
self.finalize_function(metrics, line_num + 1);
}
}
}
fn is_function_start(&self, trimmed: &str) -> bool {
trimmed.starts_with("fun ") || trimmed.starts_with("@test") || trimmed.contains("fun test_")
}
fn start_function(&mut self, trimmed: &str, line_num: u32) {
self.in_function = true;
self.function_start = line_num + 1;
self.function_name = self.extract_function_name(trimmed);
self.current_metrics = ComplexityMetrics {
cyclomatic: 1,
cognitive: 0,
nesting_max: 0,
lines: 0,
halstead: None,
};
self.brace_count = 0;
}
fn extract_function_name(&self, trimmed: &str) -> String {
if let Some(name_start) = trimmed.find("fun ") {
let after_fun = &trimmed[name_start + 4..];
after_fun.split('(').next().unwrap_or("").trim().to_string()
} else {
String::new()
}
}
fn update_complexity_metrics(&mut self, trimmed: &str) {
let patterns = [
("if ", 1, 1),
("else if ", 1, 1),
("while ", 1, 2),
("for ", 1, 2),
("match ", 1, 2),
];
for (pattern, cyclo, cognitive) in patterns {
if trimmed.starts_with(pattern) || trimmed.contains(&format!(" {pattern}")) {
self.current_metrics.cyclomatic += cyclo;
self.current_metrics.cognitive += cognitive;
}
}
if trimmed.contains("&&") || trimmed.contains("||") {
self.current_metrics.cyclomatic += 1;
self.current_metrics.cognitive += 1;
}
}
fn update_brace_count(&mut self, trimmed: &str) {
self.brace_count += trimmed.chars().filter(|&c| c == '{').count() as i32;
self.brace_count -= trimmed.chars().filter(|&c| c == '}').count() as i32;
}
fn finalize_function(&mut self, metrics: &mut FileComplexityMetrics, line_end: u32) {
metrics.functions.push(FunctionComplexity {
name: self.function_name.clone(),
line_start: self.function_start,
line_end,
metrics: self.current_metrics,
});
self.in_function = false;
self.function_name.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_ruchy_lexer_basic() {
let mut lexer = RuchyLexer::new("fun test() { return 42 }".to_string());
assert!(matches!(lexer.next_token(), RuchyToken::Fun));
assert!(matches!(lexer.next_token(), RuchyToken::Identifier(_)));
assert!(matches!(lexer.next_token(), RuchyToken::LeftParen));
assert!(matches!(lexer.next_token(), RuchyToken::RightParen));
assert!(matches!(lexer.next_token(), RuchyToken::LeftBrace));
assert!(matches!(lexer.next_token(), RuchyToken::Return));
assert!(matches!(lexer.next_token(), RuchyToken::Integer(42)));
assert!(matches!(lexer.next_token(), RuchyToken::RightBrace));
}
#[test]
fn test_ruchy_halstead_calculation() {
let mut analyzer = RuchyComplexityAnalyzer::new();
analyzer.track_operator("+");
analyzer.track_operator("-");
analyzer.track_operator("+"); analyzer.track_operand("x");
analyzer.track_operand("y");
analyzer.track_operand("42");
analyzer.track_operand("x");
let halstead = analyzer.calculate_halstead();
assert_eq!(halstead.operators_unique, 2); assert_eq!(halstead.operands_unique, 3); assert_eq!(halstead.operators_total, 3); assert_eq!(halstead.operands_total, 4); assert!(halstead.volume > 0.0);
}
#[test]
fn test_dead_code_detection() {
let mut analyzer = RuchyComplexityAnalyzer::new();
analyzer.defined_functions.insert("main".to_string());
analyzer.defined_functions.insert("helper".to_string());
analyzer.defined_functions.insert("unused".to_string());
analyzer.called_functions.insert("helper".to_string());
let dead_code = analyzer.get_dead_code();
assert_eq!(dead_code.unused_functions.len(), 1);
assert!(dead_code.unused_functions.contains(&"unused".to_string()));
assert!(!dead_code.unused_functions.contains(&"main".to_string())); }
#[tokio::test]
async fn test_ruchy_complexity_analysis() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test.ruchy");
let content = r#"
fun fibonacci(n: i32) -> i32 {
if n <= 1 {
n
} else {
fibonacci(n - 1) + fibonacci(n - 2)
}
}
fun main() {
for i in 0..10 {
println(fibonacci(i))
}
}
"#;
let mut file = std::fs::File::create(&file_path).unwrap();
file.write_all(content.as_bytes()).unwrap();
let metrics = analyze_ruchy_file(&file_path).await.unwrap();
assert_eq!(metrics.functions.len(), 2);
assert!(metrics.functions[0].metrics.cyclomatic > 1);
assert!(metrics.total_complexity.cyclomatic > 1);
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}