use crate::analyzers::state_field_detector::{
ConfidenceClass, StateDetectionConfig, StateFieldDetector,
};
use crate::priority::complexity_patterns::{CoordinatorSignals, StateMachineSignals};
use syn::{
visit::Visit, Arm, Block, Expr, ExprBinary, ExprField, ExprMatch, ExprMethodCall, Pat,
PatTupleStruct, Stmt,
};
pub struct StateMachinePatternDetector {
state_detector: StateFieldDetector,
}
const STATE_FIELD_KEYWORDS: &[&str] = &[
"state", "mode", "status", "phase", "stage", "desired", "current", "target", "actual",
];
const STATE_PATH_KEYWORDS: &[&str] = &["state", "mode", "status", "phase"];
const ERROR_ACCUMULATION_KEYWORDS: &[&str] = &[
"error",
"err",
"issue",
"warning",
"warn",
"validation",
"invalid",
"problem",
];
const ACTION_TYPE_PATTERNS: &[&str] = &[
"Action::",
"Command::",
"Operation::",
"Task::",
"Event::",
"Message::",
];
impl StateMachinePatternDetector {
pub fn new() -> Self {
Self {
state_detector: StateFieldDetector::new(StateDetectionConfig::default()),
}
}
pub fn with_config(config: StateDetectionConfig) -> Self {
Self {
state_detector: StateFieldDetector::new(config),
}
}
pub fn detect_state_machine(&self, block: &Block) -> Option<StateMachineSignals> {
let mut visitor = StateMachineVisitor::new();
visitor.visit_block(block);
let state_fields: Vec<_> = visitor
.field_accesses
.iter()
.map(|field| self.state_detector.detect_state_field(field))
.filter(|detection| detection.classification != ConfidenceClass::Low)
.collect();
if !visitor.has_enum_match && state_fields.is_empty() {
return None;
}
let field_confidence: f64 = state_fields
.iter()
.map(|d| d.confidence)
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0);
let confidence = calculate_enhanced_state_machine_confidence(
visitor.enum_match_count,
visitor.tuple_match_count,
state_fields.len() as u32,
visitor.action_dispatch_count,
field_confidence,
);
if confidence < 0.5 {
return None;
}
let mut primary = 0;
let mut nested = 0;
let mut delegated = 0;
let mut trivial = 0;
let mut complex = 0;
let mut total_lines = 0;
const TRIVIAL_THRESHOLD: u32 = 10;
for arm in &visitor.arm_metrics {
if arm.is_primary_match {
primary += 1;
} else {
nested += 1;
}
if arm.is_delegated {
delegated += 1;
} else if arm.inline_lines < TRIVIAL_THRESHOLD {
trivial += 1;
} else {
complex += 1;
total_lines += arm.inline_lines;
}
}
let avg_complexity = if !visitor.arm_metrics.is_empty() {
visitor
.arm_metrics
.iter()
.map(|a| a.inline_lines)
.sum::<u32>() as f32
/ visitor.arm_metrics.len() as f32
} else {
0.0
};
Some(StateMachineSignals {
transition_count: visitor.enum_match_count + visitor.tuple_match_count,
match_expression_count: visitor.match_expression_count,
has_enum_match: visitor.has_enum_match,
has_state_comparison: !state_fields.is_empty(),
action_dispatch_count: visitor.action_dispatch_count,
confidence,
primary_match_arms: primary,
nested_match_arms: nested,
delegated_arms: delegated,
trivial_arms: trivial,
complex_inline_arms: complex,
total_inline_lines: total_lines,
avg_arm_complexity: avg_complexity,
})
}
pub fn detect_coordinator(&self, block: &Block) -> Option<CoordinatorSignals> {
let mut visitor = CoordinatorVisitor::new();
visitor.visit_block(block);
let action_count = if visitor.state_comparison_count >= 2 {
visitor.action_push_count } else {
visitor.state_aware_push_count };
if action_count < 3 || visitor.state_comparison_count < 2 {
return None;
}
let total_pushes = action_count + visitor.error_accumulation_count;
if total_pushes > 0 {
let error_ratio = visitor.error_accumulation_count as f64 / total_pushes as f64;
if error_ratio > 0.5 {
return None; }
}
let has_action_types = visitor.explicit_action_type_count > 0;
let confidence = calculate_enhanced_coordinator_confidence(
action_count,
visitor.state_comparison_count,
visitor.has_helper_calls,
has_action_types,
visitor.has_final_dispatch,
);
if confidence < 0.7 {
return None;
}
Some(CoordinatorSignals {
actions: action_count,
comparisons: visitor.state_comparison_count,
has_action_accumulation: true,
has_helper_calls: visitor.has_helper_calls,
confidence,
})
}
}
impl Default for StateMachinePatternDetector {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
fn calculate_state_machine_confidence(
enum_match_count: u32,
tuple_match_count: u32,
state_comparison_count: u32,
action_dispatch_count: u32,
) -> f64 {
let mut confidence = 0.0;
if enum_match_count > 0 || tuple_match_count > 0 {
confidence += 0.5;
}
confidence += (state_comparison_count as f64 / 10.0).min(0.3);
confidence += (action_dispatch_count as f64 / 10.0).min(0.2);
confidence.min(1.0)
}
fn calculate_enhanced_state_machine_confidence(
enum_match_count: u32,
tuple_match_count: u32,
state_field_count: u32,
action_dispatch_count: u32,
max_field_confidence: f64,
) -> f64 {
let mut confidence = 0.0;
if enum_match_count > 0 || tuple_match_count >= 2 {
confidence += 0.5;
}
if state_field_count > 0 {
confidence += max_field_confidence * 0.4; }
if action_dispatch_count >= 2 {
confidence += 0.2;
}
confidence.min(1.0)
}
fn calculate_enhanced_coordinator_confidence(
state_aware_pushes: u32,
state_comparisons: u32,
has_helper_calls: bool,
has_action_types: bool,
has_final_dispatch: bool,
) -> f64 {
let mut confidence = 0.0;
confidence += (state_aware_pushes as f64 / 10.0).min(0.4);
confidence += (state_comparisons as f64 / 10.0).min(0.3);
if has_helper_calls {
confidence += 0.1;
}
if has_action_types {
confidence += 0.15;
}
if has_final_dispatch {
confidence += 0.1;
}
confidence.min(1.0)
}
#[derive(Debug, Clone)]
#[allow(dead_code)] struct ArmMetrics {
is_delegated: bool, inline_lines: u32, has_nested_match: bool, arm_index: usize, is_primary_match: bool, }
struct StateMachineVisitor {
enum_match_count: u32,
tuple_match_count: u32,
state_comparison_count: u32,
action_dispatch_count: u32,
match_expression_count: u32,
has_enum_match: bool,
field_accesses: Vec<ExprField>,
arm_metrics: Vec<ArmMetrics>,
match_nesting_depth: u32,
in_primary_match: bool,
}
impl StateMachineVisitor {
fn new() -> Self {
Self {
enum_match_count: 0,
tuple_match_count: 0,
state_comparison_count: 0,
action_dispatch_count: 0,
match_expression_count: 0,
has_enum_match: false,
field_accesses: Vec::new(),
arm_metrics: Vec::new(),
match_nesting_depth: 0,
in_primary_match: false,
}
}
fn analyze_arm(&self, arm: &Arm, index: usize) -> ArmMetrics {
let is_delegated = is_delegated_to_handler(&arm.body);
let inline_lines = estimate_arm_lines(&arm.body);
let has_nested_match = contains_nested_match(&arm.body);
ArmMetrics {
is_delegated,
inline_lines,
has_nested_match,
arm_index: index,
is_primary_match: self.in_primary_match,
}
}
fn is_enum_or_tuple_pattern(&self, pat: &Pat) -> bool {
matches!(
pat,
Pat::TupleStruct(_) | Pat::Struct(_) | Pat::Ident(_) | Pat::Tuple(_) | Pat::Path(_)
)
}
fn has_state_field_access(&self, expr: &Expr) -> bool {
match expr {
Expr::Field(field_expr) => {
let field_name = match &field_expr.member {
syn::Member::Named(ident) => ident.to_string(),
syn::Member::Unnamed(_) => String::new(),
};
let field_name = field_name.to_lowercase();
field_name.contains("state")
|| field_name.contains("mode")
|| field_name.contains("status")
}
Expr::Path(path) => {
path.path.segments.iter().any(|seg| {
let name = seg.ident.to_string().to_lowercase();
name.contains("state") || name.contains("mode") || name.contains("status")
})
}
_ => false,
}
}
}
impl<'ast> Visit<'ast> for StateMachineVisitor {
fn visit_expr_match(&mut self, match_expr: &'ast ExprMatch) {
let was_in_primary = self.in_primary_match;
self.in_primary_match = self.match_nesting_depth == 0;
self.match_nesting_depth += 1;
self.match_expression_count += 1;
if let Expr::Field(field_expr) = match_expr.expr.as_ref() {
self.field_accesses.push(field_expr.clone());
}
if self.has_state_field_access(&match_expr.expr) {
self.has_enum_match = true;
self.state_comparison_count += 1;
}
for (idx, arm) in match_expr.arms.iter().enumerate() {
let metrics = self.analyze_arm(arm, idx);
self.arm_metrics.push(metrics);
match &arm.pat {
Pat::TupleStruct(tuple) => {
self.tuple_match_count += 1;
self.has_enum_match = true;
if is_nested_tuple_pattern(tuple) {
self.enum_match_count += 1;
}
}
Pat::Tuple(_) => {
self.tuple_match_count += 1;
self.has_enum_match = true;
}
Pat::Struct(_) => {
self.enum_match_count += 1;
self.has_enum_match = true;
}
_ => {
if self.is_enum_or_tuple_pattern(&arm.pat) {
self.enum_match_count += 1;
self.has_enum_match = true;
}
}
}
if has_vec_push_or_call(&arm.body) {
self.action_dispatch_count += 1;
}
}
syn::visit::visit_expr_match(self, match_expr);
self.match_nesting_depth -= 1;
self.in_primary_match = was_in_primary;
}
fn visit_expr(&mut self, expr: &'ast Expr) {
if let Expr::If(if_expr) = expr {
if self.has_state_field_access(&if_expr.cond) {
self.state_comparison_count += 1;
}
}
if let Expr::Field(field_expr) = expr {
self.field_accesses.push(field_expr.clone());
}
syn::visit::visit_expr(self, expr);
}
}
fn is_nested_tuple_pattern(tuple: &PatTupleStruct) -> bool {
tuple.elems.len() >= 2
}
fn has_vec_push_or_call(expr: &Expr) -> bool {
match expr {
Expr::MethodCall(method) => method.method == "push" || method.method == "extend",
Expr::Call(_) => true,
Expr::Block(block) => block.block.stmts.iter().any(|stmt| match stmt {
Stmt::Expr(expr, _) => has_vec_push_or_call(expr),
Stmt::Local(local) => {
if let Some(init) = &local.init {
has_vec_push_or_call(&init.expr)
} else {
false
}
}
_ => false,
}),
_ => false,
}
}
fn is_delegated_to_handler(body: &Expr) -> bool {
match body {
Expr::Try(try_expr) => matches!(*try_expr.expr, Expr::Call(_) | Expr::MethodCall(_)),
Expr::Call(_) | Expr::MethodCall(_) => true,
Expr::Block(block) if block.block.stmts.len() == 1 => {
matches!(
&block.block.stmts[0],
Stmt::Expr(Expr::Call(_), _)
| Stmt::Expr(Expr::Try(_), _)
| Stmt::Expr(Expr::MethodCall(_), _)
)
}
_ => false,
}
}
fn estimate_arm_lines(body: &Expr) -> u32 {
let mut counter = LineCounter::new();
counter.visit_expr(body);
counter.estimated_lines
}
struct LineCounter {
estimated_lines: u32,
}
impl LineCounter {
fn new() -> Self {
Self { estimated_lines: 0 }
}
}
impl<'ast> Visit<'ast> for LineCounter {
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
self.estimated_lines += 1;
syn::visit::visit_stmt(self, stmt);
}
fn visit_expr(&mut self, expr: &'ast Expr) {
match expr {
Expr::Match(_) | Expr::If(_) | Expr::ForLoop(_) | Expr::While(_) => {
self.estimated_lines += 1;
}
Expr::Struct(struct_expr) => {
self.estimated_lines += struct_expr.fields.len() as u32;
}
_ => {}
}
syn::visit::visit_expr(self, expr);
}
}
fn contains_nested_match(body: &Expr) -> bool {
struct MatchFinder {
found: bool,
}
impl<'ast> Visit<'ast> for MatchFinder {
fn visit_expr_match(&mut self, _: &'ast ExprMatch) {
self.found = true;
}
}
let mut finder = MatchFinder { found: false };
finder.visit_expr(body);
finder.found
}
fn is_state_comparison(binary: &ExprBinary) -> bool {
contains_state_identifier(&binary.left) || contains_state_identifier(&binary.right)
}
fn contains_state_identifier(expr: &Expr) -> bool {
match expr {
Expr::Field(field) => {
let field_name = match &field.member {
syn::Member::Named(ident) => ident.to_string(),
syn::Member::Unnamed(_) => String::new(),
};
let field_name = field_name.to_lowercase();
STATE_FIELD_KEYWORDS
.iter()
.any(|kw| field_name.contains(kw))
}
Expr::Path(path) => path.path.segments.iter().any(|seg| {
let name = seg.ident.to_string().to_lowercase();
STATE_PATH_KEYWORDS.iter().any(|kw| name.contains(kw))
}),
_ => false,
}
}
fn is_error_accumulation(method: &ExprMethodCall) -> bool {
if method.method != "push" {
return false;
}
let receiver_name = get_receiver_name(&method.receiver).to_lowercase();
ERROR_ACCUMULATION_KEYWORDS
.iter()
.any(|kw| receiver_name.contains(kw))
}
fn is_action_type(args: &syn::punctuated::Punctuated<Expr, syn::token::Comma>) -> bool {
args.iter().any(|arg| {
if let Expr::Path(path) = arg {
let path_str = path_to_string(&path.path);
ACTION_TYPE_PATTERNS
.iter()
.any(|pat| path_str.contains(pat))
} else {
false
}
})
}
fn get_receiver_name(receiver: &Expr) -> String {
match receiver {
Expr::Path(path) => path_to_string(&path.path),
Expr::Field(field) => match &field.member {
syn::Member::Named(ident) => ident.to_string(),
syn::Member::Unnamed(_) => String::new(),
},
_ => String::new(),
}
}
fn path_to_string(path: &syn::Path) -> String {
path.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}
struct CoordinatorVisitor {
vec_push_count: u32,
comparison_count: u32,
has_helper_calls: bool,
action_push_count: u32, state_aware_push_count: u32, state_comparison_count: u32, error_accumulation_count: u32, explicit_action_type_count: u32, has_final_dispatch: bool,
current_conditional_is_state_related: bool,
}
impl CoordinatorVisitor {
fn new() -> Self {
Self {
vec_push_count: 0,
comparison_count: 0,
has_helper_calls: false,
action_push_count: 0,
state_aware_push_count: 0,
state_comparison_count: 0,
error_accumulation_count: 0,
explicit_action_type_count: 0,
has_final_dispatch: false,
current_conditional_is_state_related: false,
}
}
}
impl<'ast> Visit<'ast> for CoordinatorVisitor {
fn visit_expr(&mut self, expr: &'ast Expr) {
match expr {
Expr::MethodCall(method) if method.method == "push" => {
self.vec_push_count += 1;
if is_error_accumulation(method) {
self.error_accumulation_count += 1;
} else {
self.action_push_count += 1;
if self.current_conditional_is_state_related {
self.state_aware_push_count += 1;
}
if is_action_type(&method.args) {
self.explicit_action_type_count += 1;
}
}
}
Expr::Binary(binary) => {
use syn::BinOp;
if matches!(
binary.op,
BinOp::Eq(_)
| BinOp::Ne(_)
| BinOp::Lt(_)
| BinOp::Le(_)
| BinOp::Gt(_)
| BinOp::Ge(_)
) {
self.comparison_count += 1;
if is_state_comparison(binary) {
self.state_comparison_count += 1;
self.current_conditional_is_state_related = true;
}
}
}
Expr::Call(_) => {
self.has_helper_calls = true;
}
_ => {}
}
syn::visit::visit_expr(self, expr);
if matches!(expr, Expr::If(_)) {
self.current_conditional_is_state_related = false;
}
}
fn visit_stmt(&mut self, stmt: &'ast Stmt) {
if let Stmt::Expr(Expr::Path(path), None) = stmt {
let path_str = path_to_string(&path.path);
if path_str.contains("action") || path_str.contains("command") {
self.has_final_dispatch = true;
}
}
syn::visit::visit_stmt(self, stmt);
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn detect_state_machine_with_match() {
let block: Block = parse_quote! {
{
let mut actions = vec![];
match (current.mode, desired.mode) {
(Mode::Active, Mode::Standby) => {
if current.has_active_connections() {
actions.push(Action::DrainConnections);
}
actions.push(Action::TransitionToStandby);
}
(Mode::Standby, Mode::Active) => {
actions.push(Action::TransitionToActive);
}
_ => {}
}
actions
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_state_machine(&block);
assert!(signals.is_some());
let signals = signals.unwrap();
assert!(signals.has_enum_match);
assert_eq!(signals.transition_count, 2); assert_eq!(signals.match_expression_count, 1); assert!(signals.confidence >= 0.6);
}
#[test]
fn detect_coordinator_with_action_accumulation() {
let block: Block = parse_quote! {
{
let mut actions = vec![];
if current.status != desired.status {
actions.push(Action::UpdateStatus);
}
if current.phase != desired.phase {
actions.push(Action::UpdatePhase);
}
if current.mode != desired.mode {
actions.push(Action::Apply);
}
actions
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_coordinator(&block);
assert!(signals.is_some(), "Should detect coordinator pattern");
let signals = signals.unwrap();
assert_eq!(signals.actions, 3);
assert_eq!(signals.comparisons, 3);
assert!(signals.has_action_accumulation);
assert!(signals.confidence >= 0.7);
}
#[test]
fn rejects_validation_code() {
let block: Block = parse_quote! {
{
let mut errors = vec![];
if email.is_empty() {
errors.push("Email is required");
}
if !email.contains('@') {
errors.push("Invalid email format");
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_coordinator(&block);
assert!(
signals.is_none(),
"Validation code should not trigger coordinator pattern"
);
}
#[test]
fn distinguishes_error_from_action_accumulation() {
let error_push: ExprMethodCall = parse_quote! {
errors.push("validation failed")
};
assert!(is_error_accumulation(&error_push));
let action_push: ExprMethodCall = parse_quote! {
actions.push(Action::DoSomething)
};
assert!(!is_error_accumulation(&action_push));
let warning_push: ExprMethodCall = parse_quote! {
warnings.push("deprecated")
};
assert!(is_error_accumulation(&warning_push));
}
#[test]
fn recognizes_state_comparisons() {
let state_comp: ExprBinary = parse_quote! {
current.state != desired.state
};
assert!(is_state_comparison(&state_comp));
let mode_comp: ExprBinary = parse_quote! {
current.mode == Mode::Active
};
assert!(is_state_comparison(&mode_comp));
let status_comp: ExprBinary = parse_quote! {
obj.status != target.status
};
assert!(is_state_comparison(&status_comp));
let value_comp: ExprBinary = parse_quote! {
email.is_empty() == true
};
assert!(!is_state_comparison(&value_comp));
}
#[test]
fn confidence_scoring_works() {
let high = calculate_enhanced_coordinator_confidence(
5, 4, true, true, true, );
assert!(
high >= 0.8,
"High confidence should be >= 0.8, got {}",
high
);
let medium = calculate_enhanced_coordinator_confidence(
3, 2, false, true, false, );
assert!(
(0.6..0.8).contains(&medium),
"Medium confidence should be 0.6-0.8, got {}",
medium
);
let low = calculate_enhanced_coordinator_confidence(
2, 1, false, false, false, );
assert!(low < 0.7, "Low confidence should be < 0.7, got {}", low);
}
#[test]
fn detects_true_coordinator_pattern() {
let block: Block = parse_quote! {
{
let mut actions = vec![];
if current.state != desired.state {
actions.push(Action::TransitionState);
}
if current.mode != desired.mode {
actions.push(Action::ChangeMode);
}
if current.status != desired.status {
actions.push(Action::UpdateStatus);
}
actions
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_coordinator(&block);
assert!(signals.is_some(), "Should detect true coordinator");
let signals = signals.unwrap();
assert_eq!(signals.actions, 3);
assert_eq!(signals.comparisons, 3);
assert!(signals.confidence >= 0.7);
}
#[test]
fn action_type_detection_works() {
use syn::punctuated::Punctuated;
use syn::token::Comma;
let action_arg: Punctuated<Expr, Comma> = parse_quote! {
Action::DoSomething
};
assert!(is_action_type(&action_arg));
let command_arg: Punctuated<Expr, Comma> = parse_quote! {
Command::Execute
};
assert!(is_action_type(&command_arg));
let string_arg: Punctuated<Expr, Comma> = parse_quote! {
"error message"
};
assert!(!is_action_type(&string_arg));
}
#[test]
fn rejects_mixed_validation_and_action_code() {
let block: Block = parse_quote! {
{
let mut errors = vec![];
let mut actions = vec![];
if email.is_empty() {
errors.push("required");
}
if !email.contains('@') {
errors.push("invalid");
}
if current.state != desired.state {
actions.push(Action::Sync);
}
(errors, actions)
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_coordinator(&block);
assert!(
signals.is_none(),
"Mixed code with dominant error accumulation should be rejected"
);
}
#[test]
fn final_dispatch_detection() {
let block_with_dispatch: Block = parse_quote! {
{
let mut actions = vec![];
if current.state != desired.state {
actions.push(Action::Transition);
}
if current.mode != desired.mode {
actions.push(Action::Change);
}
if current.status != desired.status {
actions.push(Action::Update);
}
actions
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_coordinator(&block_with_dispatch);
assert!(signals.is_some());
let signals = signals.unwrap();
assert!(signals.confidence >= 0.7);
}
#[test]
fn no_detection_for_simple_function() {
let block: Block = parse_quote! {
{
let x = compute_value();
x + 10
}
};
let detector = StateMachinePatternDetector::new();
let state_signals = detector.detect_state_machine(&block);
let coord_signals = detector.detect_coordinator(&block);
assert!(state_signals.is_none());
assert!(coord_signals.is_none());
}
#[test]
fn state_machine_requires_enum_match() {
let block: Block = parse_quote! {
{
if x > 10 {
do_something();
}
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_state_machine(&block);
assert!(signals.is_none());
}
#[test]
fn enhanced_detection_with_fsm_state_field() {
let block: Block = parse_quote! {
{
match self.fsm_state {
FsmState::Idle => self.handle_idle(),
FsmState::Processing => self.handle_processing(),
FsmState::Complete => self.handle_complete(),
}
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_state_machine(&block);
assert!(
signals.is_some(),
"Should detect state machine with fsm_state field"
);
let signals = signals.unwrap();
assert!(signals.confidence >= 0.5, "Confidence should be >= 0.5");
}
#[test]
fn enhanced_detection_with_prefix_pattern() {
let block: Block = parse_quote! {
{
match self.current_operation {
Operation::Read => { }
Operation::Write => { }
Operation::Delete => { }
}
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_state_machine(&block);
assert!(
signals.is_some(),
"Should detect state machine with current_* prefix"
);
}
#[test]
fn enhanced_detection_with_suffix_pattern() {
let block: Block = parse_quote! {
{
match self.connection_state {
ConnectionState::Idle => { }
ConnectionState::Connecting => { }
ConnectionState::Connected => { }
}
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_state_machine(&block);
assert!(
signals.is_some(),
"Should detect state machine with *_state suffix"
);
}
#[test]
fn test_delegation_detection() {
use syn::parse_quote;
let arm1: Expr = parse_quote! { handle_a()? };
assert!(is_delegated_to_handler(&arm1));
let arm2: Expr = parse_quote! { handle_b(x, y)? };
assert!(is_delegated_to_handler(&arm2));
let arm3: Expr = parse_quote! { self.handle_c()? };
assert!(is_delegated_to_handler(&arm3));
let arm4: Expr = parse_quote! {
{
handle_d()
}
};
assert!(is_delegated_to_handler(&arm4));
let arm5: Expr = parse_quote! {
{
let cfg = build_config();
process(cfg)?;
Ok(())
}
};
assert!(!is_delegated_to_handler(&arm5));
}
#[test]
fn test_arm_complexity_estimation() {
use syn::parse_quote;
let simple: Expr = parse_quote! { do_thing() };
let lines = estimate_arm_lines(&simple);
assert!(lines < 10, "Simple arm should be < 10 lines, got {}", lines);
let complex: Expr = parse_quote! {
{
let validate_config = ValidateConfig {
path: path,
config: config,
format: match format {
Fmt::Json => Format::Json,
Fmt::Markdown => Format::Markdown,
Fmt::Text => Format::Text,
},
output: None,
top: 10,
verbose: false,
};
debtmap::commands::validate::validate_project(validate_config)?;
Ok(())
}
};
let lines = estimate_arm_lines(&complex);
assert!(
lines >= 10,
"Complex arm should be >= 10 lines, got {}",
lines
);
}
#[test]
fn test_primary_vs_nested_match_tracking() {
let block: Block = parse_quote! {
{
match cmd {
Cmd::A => handle_a()?,
Cmd::B => {
let fmt = match format {
Fmt::Json => json(),
Fmt::Text => text(),
};
handle_b(fmt)?
}
Cmd::C => handle_c()?,
}
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_state_machine(&block).unwrap();
assert_eq!(signals.primary_match_arms, 3, "Should have 3 primary arms");
assert_eq!(signals.nested_match_arms, 2, "Should have 2 nested arms");
}
#[test]
fn test_arm_classification() {
let block: Block = parse_quote! {
{
match command {
Commands::Analyze { .. } => handle_analyze_command(command)?,
Commands::Compare { .. } => handle_compare_command(before, after)?,
Commands::Init { force } => {
debtmap::commands::init::init_config(force)?;
Ok(())
}
Commands::Validate { path, config } => {
let validate_config = ValidateConfig {
path: path,
config: config,
format: match format {
Fmt::Json => Format::Json,
Fmt::Markdown => Format::Markdown,
Fmt::Text => Format::Text,
},
output: None,
top: 10,
verbose: false,
filter: None,
};
debtmap::commands::validate::validate_project(validate_config)?;
Ok(())
}
}
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_state_machine(&block).unwrap();
assert_eq!(signals.primary_match_arms, 4);
assert_eq!(signals.delegated_arms, 2, "Analyze and Compare");
assert!(signals.trivial_arms >= 1, "Init is trivial");
assert!(signals.complex_inline_arms >= 1, "Validate is complex");
assert_eq!(signals.nested_match_arms, 3, "Format conversion match");
}
#[test]
fn test_clean_state_machine_no_complex_arms() {
let block: Block = parse_quote! {
{
match cmd {
Command::Start => handle_start()?,
Command::Stop => handle_stop()?,
Command::Restart => handle_restart()?,
}
}
};
let detector = StateMachinePatternDetector::new();
let signals = detector.detect_state_machine(&block).unwrap();
assert_eq!(signals.complex_inline_arms, 0, "All arms delegated");
assert_eq!(signals.delegated_arms, 3, "Should have 3 delegated");
}
#[test]
fn test_contains_nested_match() {
use syn::parse_quote;
let with_match: Expr = parse_quote! {
{
let fmt = match format {
Fmt::Json => json(),
Fmt::Text => text(),
};
process(fmt)
}
};
assert!(contains_nested_match(&with_match));
let without_match: Expr = parse_quote! {
{
let cfg = build_config();
process(cfg)
}
};
assert!(!contains_nested_match(&without_match));
}
}