use ryo_mutations::basic::{AddMatchArmMutation, RemoveMatchArmMutation, ReplaceMatchArmMutation};
use ryo_mutations::MutationResult;
use ryo_source::pure::{
MacroDelimiter, PureBlock, PureExpr, PureItem, PureMatchArm, PurePattern, PureStmt,
};
use ryo_symbol::SymbolKind;
use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
impl ASTRegApply for AddMatchArmMutation {
fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
let fn_id = self.function_id;
let kind = ctx.symbol_registry.kind(fn_id);
if !matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method)) {
return MutationResult {
mutation_type: "AddMatchArm".to_string(),
changes: 0,
description: format!("Symbol {} is not a function or method", fn_id),
};
}
let ast = match ctx.get_ast_mut(fn_id) {
Some(ast) => ast,
None => {
return MutationResult {
mutation_type: "AddMatchArm".to_string(),
changes: 0,
description: format!("AST not found for function {}", fn_id),
};
}
};
if let PureItem::Fn(f) = ast {
if walk_and_add_arm(&mut f.body, &self.enum_name, &self.pattern, &self.body) {
ctx.emit_modified(fn_id, ModificationType::Other("MatchArmAdded".into()));
return MutationResult {
mutation_type: "AddMatchArm".to_string(),
changes: 1,
description: format!(
"Added match arm '{}' in function {}",
self.pattern, fn_id
),
};
}
}
MutationResult {
mutation_type: "AddMatchArm".to_string(),
changes: 0,
description: format!(
"No match expression for '{}' found in function {}",
self.enum_name, fn_id
),
}
}
}
impl ASTRegApply for RemoveMatchArmMutation {
fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
let fn_id = self.function_id;
let kind = ctx.symbol_registry.kind(fn_id);
if !matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method)) {
return MutationResult {
mutation_type: "RemoveMatchArm".to_string(),
changes: 0,
description: format!("Symbol {} is not a function or method", fn_id),
};
}
let ast = match ctx.get_ast_mut(fn_id) {
Some(ast) => ast,
None => {
return MutationResult {
mutation_type: "RemoveMatchArm".to_string(),
changes: 0,
description: format!("AST not found for function {}", fn_id),
};
}
};
if let PureItem::Fn(f) = ast {
if walk_and_remove_arm(&mut f.body, &self.enum_name, &self.pattern) {
ctx.emit_modified(fn_id, ModificationType::Other("MatchArmRemoved".into()));
return MutationResult {
mutation_type: "RemoveMatchArm".to_string(),
changes: 1,
description: format!(
"Removed match arm '{}' from function {}",
self.pattern, fn_id
),
};
}
}
MutationResult {
mutation_type: "RemoveMatchArm".to_string(),
changes: 0,
description: format!(
"No match arm '{}' found in function {}",
self.pattern, fn_id
),
}
}
}
fn walk_and_add_arm(block: &mut PureBlock, enum_name: &str, pattern: &str, body: &str) -> bool {
for stmt in &mut block.stmts {
if walk_stmt_and_add_arm(stmt, enum_name, pattern, body) {
return true;
}
}
false
}
fn walk_and_remove_arm(block: &mut PureBlock, enum_name: &str, pattern: &str) -> bool {
for stmt in &mut block.stmts {
if walk_stmt_and_remove_arm(stmt, enum_name, pattern) {
return true;
}
}
false
}
fn walk_stmt_and_add_arm(stmt: &mut PureStmt, enum_name: &str, pattern: &str, body: &str) -> bool {
match stmt {
PureStmt::Local {
init: Some(expr), ..
} => {
return walk_expr_and_add_arm(expr, enum_name, pattern, body);
}
PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
return walk_expr_and_add_arm(expr, enum_name, pattern, body);
}
_ => {}
}
false
}
fn walk_stmt_and_remove_arm(stmt: &mut PureStmt, enum_name: &str, pattern: &str) -> bool {
match stmt {
PureStmt::Local {
init: Some(expr), ..
} => {
return walk_expr_and_remove_arm(expr, enum_name, pattern);
}
PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
return walk_expr_and_remove_arm(expr, enum_name, pattern);
}
_ => {}
}
false
}
fn walk_expr_and_add_arm(expr: &mut PureExpr, enum_name: &str, pattern: &str, body: &str) -> bool {
if let PureExpr::Match {
expr: match_expr,
arms,
} = expr
{
if is_target_match(match_expr, arms, enum_name) {
if arms.iter().any(|a| pattern_matches(&a.pattern, pattern)) {
return false;
}
let insert_pos = arms
.iter()
.position(|a| matches!(a.pattern, PurePattern::Wild | PurePattern::Rest))
.unwrap_or(arms.len());
arms.insert(insert_pos, create_arm(pattern, body));
return true;
}
}
walk_expr_children_and_add_arm(expr, enum_name, pattern, body)
}
fn walk_expr_and_remove_arm(expr: &mut PureExpr, enum_name: &str, pattern: &str) -> bool {
if let PureExpr::Match {
expr: match_expr,
arms,
} = expr
{
if is_target_match(match_expr, arms, enum_name) {
let original_len = arms.len();
arms.retain(|a| !pattern_matches(&a.pattern, pattern));
if arms.len() < original_len {
return true;
}
}
}
walk_expr_children_and_remove_arm(expr, enum_name, pattern)
}
fn is_target_match(match_expr: &PureExpr, arms: &[PureMatchArm], enum_name: &str) -> bool {
for arm in arms {
if pattern_contains_enum(&arm.pattern, enum_name) {
return true;
}
}
if let PureExpr::Path(path) = match_expr {
if path.contains(enum_name) {
return true;
}
}
false
}
fn pattern_contains_enum(pattern: &PurePattern, enum_name: &str) -> bool {
match pattern {
PurePattern::Path(path) => path_has_enum_segment(path, enum_name),
PurePattern::Struct { path, .. } => path_has_enum_segment(path, enum_name),
PurePattern::Or(patterns) => patterns.iter().any(|p| pattern_contains_enum(p, enum_name)),
PurePattern::Other(s) => {
let path_part = s.split(&['(', '{', ' '][..]).next().unwrap_or(s);
path_has_enum_segment(path_part, enum_name)
}
_ => false,
}
}
fn path_has_enum_segment(path: &str, enum_name: &str) -> bool {
path.split("::").any(|segment| segment == enum_name)
}
fn create_arm(pattern: &str, body: &str) -> PureMatchArm {
let pat = if pattern.contains('(') || pattern.contains('{') || pattern.contains("..") {
PurePattern::Other(pattern.to_string())
} else {
PurePattern::Path(pattern.to_string())
};
let body_expr = if let Some(bang_pos) = body.find('!') {
let body_str = body.trim();
let name = body_str[..bang_pos].trim();
let rest = body_str[bang_pos + 1..].trim();
let is_valid_ident = !name.is_empty()
&& name.chars().all(|c| c.is_alphanumeric() || c == '_')
&& name
.chars()
.next()
.map(|c| c.is_alphabetic() || c == '_')
.unwrap_or(false);
let has_delimiter = rest.starts_with('(') || rest.starts_with('{') || rest.starts_with('[');
if is_valid_ident && has_delimiter {
let (delimiter, tokens) = if rest.starts_with('(') && rest.ends_with(')') {
(MacroDelimiter::Paren, rest[1..rest.len() - 1].to_string())
} else if rest.starts_with('{') && rest.ends_with('}') {
(MacroDelimiter::Brace, rest[1..rest.len() - 1].to_string())
} else if rest.starts_with('[') && rest.ends_with(']') {
(MacroDelimiter::Bracket, rest[1..rest.len() - 1].to_string())
} else {
(MacroDelimiter::Paren, String::new())
};
PureExpr::Macro {
name: name.to_string(),
delimiter,
tokens,
}
} else {
PureExpr::Other(normalize_body_as_expr(body))
}
} else {
PureExpr::Other(normalize_body_as_expr(body))
};
PureMatchArm {
pattern: pat,
guard: None,
body: body_expr,
}
}
fn normalize_body_as_expr(body: &str) -> String {
let trimmed = body.trim();
if trimmed.contains(';') && !trimmed.starts_with('{') {
format!("{{ {} }}", trimmed)
} else {
trimmed.to_string()
}
}
fn walk_expr_children_and_add_arm(
expr: &mut PureExpr,
enum_name: &str,
pattern: &str,
body: &str,
) -> bool {
match expr {
PureExpr::Block { block, .. } => walk_and_add_arm(block, enum_name, pattern, body),
PureExpr::If {
cond,
then_branch,
else_branch,
} => {
if walk_expr_and_add_arm(cond, enum_name, pattern, body) {
return true;
}
if walk_and_add_arm(then_branch, enum_name, pattern, body) {
return true;
}
if let Some(else_expr) = else_branch {
if walk_expr_and_add_arm(else_expr, enum_name, pattern, body) {
return true;
}
}
false
}
PureExpr::Match { expr: e, arms } => {
if walk_expr_and_add_arm(e, enum_name, pattern, body) {
return true;
}
for arm in arms {
if walk_expr_and_add_arm(&mut arm.body, enum_name, pattern, body) {
return true;
}
}
false
}
PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
walk_and_add_arm(block, enum_name, pattern, body)
}
PureExpr::While { cond, body: b, .. } => {
walk_expr_and_add_arm(cond, enum_name, pattern, body)
|| walk_and_add_arm(b, enum_name, pattern, body)
}
PureExpr::For {
expr: e, body: b, ..
} => {
walk_expr_and_add_arm(e, enum_name, pattern, body)
|| walk_and_add_arm(b, enum_name, pattern, body)
}
PureExpr::Async { body: b, .. } => walk_and_add_arm(b, enum_name, pattern, body),
PureExpr::Closure { body: b, .. } => walk_expr_and_add_arm(b, enum_name, pattern, body),
PureExpr::Call { func, args } => {
if walk_expr_and_add_arm(func, enum_name, pattern, body) {
return true;
}
for arg in args {
if walk_expr_and_add_arm(arg, enum_name, pattern, body) {
return true;
}
}
false
}
PureExpr::MethodCall { receiver, args, .. } => {
if walk_expr_and_add_arm(receiver, enum_name, pattern, body) {
return true;
}
for arg in args {
if walk_expr_and_add_arm(arg, enum_name, pattern, body) {
return true;
}
}
false
}
PureExpr::Binary { left, right, .. } => {
walk_expr_and_add_arm(left, enum_name, pattern, body)
|| walk_expr_and_add_arm(right, enum_name, pattern, body)
}
PureExpr::Unary { expr: e, .. }
| PureExpr::Field { expr: e, .. }
| PureExpr::Await(e)
| PureExpr::Try(e) => walk_expr_and_add_arm(e, enum_name, pattern, body),
PureExpr::Index { expr: e, index } => {
walk_expr_and_add_arm(e, enum_name, pattern, body)
|| walk_expr_and_add_arm(index, enum_name, pattern, body)
}
PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
for e in exprs {
if walk_expr_and_add_arm(e, enum_name, pattern, body) {
return true;
}
}
false
}
PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
walk_expr_and_add_arm(e, enum_name, pattern, body)
}
PureExpr::Let { expr: e, .. }
| PureExpr::Cast { expr: e, .. }
| PureExpr::Ref { expr: e, .. } => walk_expr_and_add_arm(e, enum_name, pattern, body),
PureExpr::Range { start, end, .. } => {
if let Some(s) = start {
if walk_expr_and_add_arm(s, enum_name, pattern, body) {
return true;
}
}
if let Some(e) = end {
if walk_expr_and_add_arm(e, enum_name, pattern, body) {
return true;
}
}
false
}
PureExpr::Struct { fields, .. } => {
for (_, e) in fields {
if walk_expr_and_add_arm(e, enum_name, pattern, body) {
return true;
}
}
false
}
PureExpr::Repeat { expr: e, len } => {
walk_expr_and_add_arm(e, enum_name, pattern, body)
|| walk_expr_and_add_arm(len, enum_name, pattern, body)
}
_ => false,
}
}
fn walk_expr_children_and_remove_arm(expr: &mut PureExpr, enum_name: &str, pattern: &str) -> bool {
match expr {
PureExpr::Block { block, .. } => walk_and_remove_arm(block, enum_name, pattern),
PureExpr::If {
cond,
then_branch,
else_branch,
} => {
if walk_expr_and_remove_arm(cond, enum_name, pattern) {
return true;
}
if walk_and_remove_arm(then_branch, enum_name, pattern) {
return true;
}
if let Some(else_expr) = else_branch {
if walk_expr_and_remove_arm(else_expr, enum_name, pattern) {
return true;
}
}
false
}
PureExpr::Match { expr: e, arms } => {
if walk_expr_and_remove_arm(e, enum_name, pattern) {
return true;
}
for arm in arms {
if walk_expr_and_remove_arm(&mut arm.body, enum_name, pattern) {
return true;
}
}
false
}
PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
walk_and_remove_arm(block, enum_name, pattern)
}
PureExpr::While { cond, body, .. } => {
walk_expr_and_remove_arm(cond, enum_name, pattern)
|| walk_and_remove_arm(body, enum_name, pattern)
}
PureExpr::For { expr: e, body, .. } => {
walk_expr_and_remove_arm(e, enum_name, pattern)
|| walk_and_remove_arm(body, enum_name, pattern)
}
PureExpr::Async { body, .. } => walk_and_remove_arm(body, enum_name, pattern),
PureExpr::Closure { body, .. } => walk_expr_and_remove_arm(body, enum_name, pattern),
PureExpr::Call { func, args } => {
if walk_expr_and_remove_arm(func, enum_name, pattern) {
return true;
}
for arg in args {
if walk_expr_and_remove_arm(arg, enum_name, pattern) {
return true;
}
}
false
}
PureExpr::MethodCall { receiver, args, .. } => {
if walk_expr_and_remove_arm(receiver, enum_name, pattern) {
return true;
}
for arg in args {
if walk_expr_and_remove_arm(arg, enum_name, pattern) {
return true;
}
}
false
}
PureExpr::Binary { left, right, .. } => {
walk_expr_and_remove_arm(left, enum_name, pattern)
|| walk_expr_and_remove_arm(right, enum_name, pattern)
}
PureExpr::Unary { expr: e, .. }
| PureExpr::Field { expr: e, .. }
| PureExpr::Await(e)
| PureExpr::Try(e) => walk_expr_and_remove_arm(e, enum_name, pattern),
PureExpr::Index { expr: e, index } => {
walk_expr_and_remove_arm(e, enum_name, pattern)
|| walk_expr_and_remove_arm(index, enum_name, pattern)
}
PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
for e in exprs {
if walk_expr_and_remove_arm(e, enum_name, pattern) {
return true;
}
}
false
}
PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
walk_expr_and_remove_arm(e, enum_name, pattern)
}
PureExpr::Let { expr: e, .. }
| PureExpr::Cast { expr: e, .. }
| PureExpr::Ref { expr: e, .. } => walk_expr_and_remove_arm(e, enum_name, pattern),
PureExpr::Range { start, end, .. } => {
if let Some(s) = start {
if walk_expr_and_remove_arm(s, enum_name, pattern) {
return true;
}
}
if let Some(e) = end {
if walk_expr_and_remove_arm(e, enum_name, pattern) {
return true;
}
}
false
}
PureExpr::Struct { fields, .. } => {
for (_, e) in fields {
if walk_expr_and_remove_arm(e, enum_name, pattern) {
return true;
}
}
false
}
PureExpr::Repeat { expr: e, len } => {
walk_expr_and_remove_arm(e, enum_name, pattern)
|| walk_expr_and_remove_arm(len, enum_name, pattern)
}
_ => false,
}
}
impl ASTRegApply for ReplaceMatchArmMutation {
fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
let fn_id = self.function_id;
let kind = ctx.symbol_registry.kind(fn_id);
if !matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method)) {
return MutationResult {
mutation_type: "ReplaceMatchArm".to_string(),
changes: 0,
description: format!("Symbol {} is not a function or method", fn_id),
};
}
let ast = match ctx.get_ast_mut(fn_id) {
Some(ast) => ast,
None => {
return MutationResult {
mutation_type: "ReplaceMatchArm".to_string(),
changes: 0,
description: format!("AST not found for function {}", fn_id),
};
}
};
if let PureItem::Fn(f) = ast {
if walk_and_replace_arm(
&mut f.body,
&self.enum_name,
&self.old_pattern,
&self.new_pattern,
&self.new_body,
) {
ctx.emit_modified(fn_id, ModificationType::Other("MatchArmReplaced".into()));
return MutationResult {
mutation_type: "ReplaceMatchArm".to_string(),
changes: 1,
description: format!(
"Replaced match arm '{}' with '{}' in function {}",
self.old_pattern, self.new_pattern, fn_id
),
};
}
}
MutationResult {
mutation_type: "ReplaceMatchArm".to_string(),
changes: 0,
description: format!(
"No match arm '{}' for '{}' found in function {}",
self.old_pattern, self.enum_name, fn_id
),
}
}
}
fn walk_and_replace_arm(
block: &mut PureBlock,
enum_name: &str,
old_pattern: &str,
new_pattern: &str,
new_body: &str,
) -> bool {
for stmt in &mut block.stmts {
if walk_stmt_and_replace_arm(stmt, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
}
false
}
fn walk_stmt_and_replace_arm(
stmt: &mut PureStmt,
enum_name: &str,
old_pattern: &str,
new_pattern: &str,
new_body: &str,
) -> bool {
match stmt {
PureStmt::Local {
init: Some(expr), ..
} => {
return walk_expr_and_replace_arm(expr, enum_name, old_pattern, new_pattern, new_body);
}
PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
return walk_expr_and_replace_arm(expr, enum_name, old_pattern, new_pattern, new_body);
}
_ => {}
}
false
}
fn walk_expr_and_replace_arm(
expr: &mut PureExpr,
enum_name: &str,
old_pattern: &str,
new_pattern: &str,
new_body: &str,
) -> bool {
if let PureExpr::Match {
expr: match_expr,
arms,
} = expr
{
if is_target_match(match_expr, arms, enum_name) {
for arm in arms.iter_mut() {
if pattern_matches(&arm.pattern, old_pattern) {
arm.pattern = parse_pattern(new_pattern);
arm.body = parse_body(new_body);
return true;
}
}
}
}
walk_expr_children_and_replace_arm(expr, enum_name, old_pattern, new_pattern, new_body)
}
fn pattern_matches(pattern: &PurePattern, target: &str) -> bool {
match pattern {
PurePattern::Path(path) => path == target,
PurePattern::Struct { path, fields, rest } => {
let is_tuple_struct = !*rest
&& !fields.is_empty()
&& fields.iter().all(|(name, _)| name.parse::<u32>().is_ok());
let pattern_str = if is_tuple_struct {
let field_strs: Vec<_> = fields
.iter()
.map(|(_, pat)| format_pattern_for_display(pat))
.collect();
format!("{}({})", path, field_strs.join(", "))
} else {
let mut s = path.clone();
s.push_str(" { ");
let field_strs: Vec<_> = fields
.iter()
.map(|(name, pat)| {
if matches!(pat, PurePattern::Ident { name: ident, .. } if ident == name) {
name.clone()
} else if matches!(pat, PurePattern::Wild) {
format!("{}: _", name)
} else {
format!("{}: {}", name, format_pattern_for_display(pat))
}
})
.collect();
s.push_str(&field_strs.join(", "));
if *rest {
if !fields.is_empty() {
s.push_str(", ");
}
s.push_str("..");
}
s.push_str(" }");
s
};
normalize_pattern(&pattern_str) == normalize_pattern(target)
}
PurePattern::Tuple(patterns) => {
let patterns_str: Vec<_> = patterns.iter().map(format_pattern_for_display).collect();
let pattern_str = format!("({})", patterns_str.join(", "));
normalize_pattern(&pattern_str) == normalize_pattern(target)
}
PurePattern::Wild => target == "_",
PurePattern::Ident { name, .. } => name == target,
PurePattern::Other(s) => normalize_pattern(s) == normalize_pattern(target),
_ => false,
}
}
fn format_pattern_for_display(pattern: &PurePattern) -> String {
match pattern {
PurePattern::Ident { name, .. } => name.clone(),
PurePattern::Wild => "_".to_string(),
PurePattern::Path(path) => path.clone(),
PurePattern::Rest => "..".to_string(),
PurePattern::Tuple(pats) => {
let inner: Vec<_> = pats.iter().map(format_pattern_for_display).collect();
format!("({})", inner.join(", "))
}
PurePattern::Or(pats) => {
let inner: Vec<_> = pats.iter().map(format_pattern_for_display).collect();
inner.join(" | ")
}
PurePattern::Struct { path, fields, rest } => {
let is_tuple_struct = !*rest
&& !fields.is_empty()
&& fields.iter().all(|(name, _)| name.parse::<u32>().is_ok());
if is_tuple_struct {
let inner: Vec<_> = fields
.iter()
.map(|(_, pat)| format_pattern_for_display(pat))
.collect();
format!("{}({})", path, inner.join(", "))
} else {
let inner: Vec<_> = fields
.iter()
.map(|(name, pat)| {
if matches!(pat, PurePattern::Ident { name: ident, .. } if ident == name) {
name.clone()
} else {
format!("{}: {}", name, format_pattern_for_display(pat))
}
})
.collect();
let rest_str = if *rest {
if inner.is_empty() {
".."
} else {
", .."
}
} else {
""
};
format!("{} {{ {}{} }}", path, inner.join(", "), rest_str)
}
}
other => format!("{:?}", other),
}
}
fn normalize_pattern(s: &str) -> String {
let s = s.split_whitespace().collect::<Vec<_>>().join(" ");
normalize_field_shorthand(&s)
}
fn normalize_field_shorthand(s: &str) -> String {
let Some(brace_start) = s.find('{') else {
return s.to_string();
};
let Some(brace_end) = s.rfind('}') else {
return s.to_string();
};
let before = &s[..=brace_start];
let fields_str = &s[brace_start + 1..brace_end];
let after = &s[brace_end..];
let normalized_fields: Vec<String> = fields_str
.split(',')
.map(|field| {
let field = field.trim();
if field.is_empty() || field == ".." {
return field.to_string();
}
if let Some(colon_pos) = field.find(':') {
let name = field[..colon_pos].trim();
let value = field[colon_pos + 1..].trim();
if name == value {
name.to_string()
} else {
field.to_string()
}
} else {
field.to_string()
}
})
.collect();
format!(
"{} {} {}",
before.trim(),
normalized_fields.join(", "),
after.trim()
)
}
fn parse_pattern(pattern: &str) -> PurePattern {
let pattern = pattern.trim();
if let Some(brace_start) = pattern.find('{') {
if let Some(brace_end) = pattern.rfind('}') {
let path = pattern[..brace_start].trim().to_string();
let fields_str = &pattern[brace_start + 1..brace_end];
let mut fields = Vec::new();
let mut rest = false;
for field_part in fields_str.split(',') {
let field_part = field_part.trim();
if field_part.is_empty() {
continue;
}
if field_part == ".." {
rest = true;
continue;
}
if let Some(colon_pos) = field_part.find(':') {
let name = field_part[..colon_pos].trim().to_string();
let pat_str = field_part[colon_pos + 1..].trim();
let pat = if pat_str == "_" {
PurePattern::Wild
} else {
PurePattern::Ident {
name: pat_str.to_string(),
is_mut: false,
}
};
fields.push((name, pat));
} else {
let name = field_part.to_string();
fields.push((
name.clone(),
PurePattern::Ident {
name,
is_mut: false,
},
));
}
}
return PurePattern::Struct { path, fields, rest };
}
}
if let Some(paren_start) = pattern.find('(') {
if paren_start > 0 && pattern.ends_with(')') {
let path = pattern[..paren_start].trim().to_string();
let inner = &pattern[paren_start + 1..pattern.len() - 1];
let fields: Vec<(String, PurePattern)> = inner
.split(',')
.enumerate()
.filter_map(|(i, s)| {
let s = s.trim();
if s.is_empty() {
return None;
}
let pat = if s == "_" {
PurePattern::Wild
} else {
PurePattern::Ident {
name: s.to_string(),
is_mut: false,
}
};
Some((i.to_string(), pat))
})
.collect();
return PurePattern::Struct {
path,
fields,
rest: false,
};
}
}
if pattern.starts_with('(') && pattern.ends_with(')') {
let inner = &pattern[1..pattern.len() - 1];
let parts: Vec<_> = inner.split(',').map(|s| s.trim()).collect();
if parts.len() > 1 || !inner.is_empty() {
let patterns: Vec<_> = parts
.iter()
.map(|&s| {
if s == "_" {
PurePattern::Wild
} else {
PurePattern::Ident {
name: s.to_string(),
is_mut: false,
}
}
})
.collect();
return PurePattern::Tuple(patterns);
}
}
if pattern == "_" {
return PurePattern::Wild;
}
PurePattern::Path(pattern.to_string())
}
fn parse_body(body: &str) -> PureExpr {
let body_str = body.trim();
if let Some(bang_pos) = body_str.find('!') {
let name = body_str[..bang_pos].trim();
let rest = body_str[bang_pos + 1..].trim();
let is_valid_ident = !name.is_empty()
&& name.chars().all(|c| c.is_alphanumeric() || c == '_')
&& name
.chars()
.next()
.map(|c| c.is_alphabetic() || c == '_')
.unwrap_or(false);
let has_delimiter = rest.starts_with('(') || rest.starts_with('{') || rest.starts_with('[');
if is_valid_ident && has_delimiter {
let (delimiter, tokens) = if rest.starts_with('(') && rest.ends_with(')') {
(MacroDelimiter::Paren, rest[1..rest.len() - 1].to_string())
} else if rest.starts_with('{') && rest.ends_with('}') {
(MacroDelimiter::Brace, rest[1..rest.len() - 1].to_string())
} else if rest.starts_with('[') && rest.ends_with(']') {
(MacroDelimiter::Bracket, rest[1..rest.len() - 1].to_string())
} else {
(MacroDelimiter::Paren, String::new())
};
return PureExpr::Macro {
name: name.to_string(),
delimiter,
tokens,
};
}
}
PureExpr::Other(body.to_string())
}
fn walk_expr_children_and_replace_arm(
expr: &mut PureExpr,
enum_name: &str,
old_pattern: &str,
new_pattern: &str,
new_body: &str,
) -> bool {
match expr {
PureExpr::Block { block, .. } => {
walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::If {
cond,
then_branch,
else_branch,
} => {
if walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
if walk_and_replace_arm(then_branch, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
if let Some(else_expr) = else_branch {
if walk_expr_and_replace_arm(
else_expr,
enum_name,
old_pattern,
new_pattern,
new_body,
) {
return true;
}
}
false
}
PureExpr::Match { expr: e, arms } => {
if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
for arm in arms {
if walk_expr_and_replace_arm(
&mut arm.body,
enum_name,
old_pattern,
new_pattern,
new_body,
) {
return true;
}
}
false
}
PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::While { cond, body, .. } => {
walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body)
|| walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::For { expr: e, body, .. } => {
walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
|| walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::Async { body, .. } => {
walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::Closure { body, .. } => {
walk_expr_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::Call { func, args } => {
if walk_expr_and_replace_arm(func, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
for arg in args {
if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
}
false
}
PureExpr::MethodCall { receiver, args, .. } => {
if walk_expr_and_replace_arm(receiver, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
for arg in args {
if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
}
false
}
PureExpr::Binary { left, right, .. } => {
walk_expr_and_replace_arm(left, enum_name, old_pattern, new_pattern, new_body)
|| walk_expr_and_replace_arm(right, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::Unary { expr: e, .. }
| PureExpr::Field { expr: e, .. }
| PureExpr::Await(e)
| PureExpr::Try(e) => {
walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::Index { expr: e, index } => {
walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
|| walk_expr_and_replace_arm(index, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
for e in exprs {
if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
}
false
}
PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::Let { expr: e, .. }
| PureExpr::Cast { expr: e, .. }
| PureExpr::Ref { expr: e, .. } => {
walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
}
PureExpr::Range { start, end, .. } => {
if let Some(s) = start {
if walk_expr_and_replace_arm(s, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
}
if let Some(e) = end {
if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
}
false
}
PureExpr::Struct { fields, .. } => {
for (_, e) in fields {
if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
return true;
}
}
false
}
PureExpr::Repeat { expr: e, len } => {
walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
|| walk_expr_and_replace_arm(len, enum_name, old_pattern, new_pattern, new_body)
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_simple_expr_unchanged() {
assert_eq!(normalize_body_as_expr("Ok(42)"), "Ok(42)");
}
#[test]
fn normalize_block_expr_unchanged() {
assert_eq!(
normalize_body_as_expr("{ let v = 1; v + 1 }"),
"{ let v = 1; v + 1 }"
);
}
#[test]
fn normalize_stmt_sequence_wrapped() {
assert_eq!(
normalize_body_as_expr("let v = x; if v { a } else { b }"),
"{ let v = x; if v { a } else { b } }"
);
}
#[test]
fn normalize_let_match_wrapped() {
assert_eq!(
normalize_body_as_expr("let v = get(); match v { A => 1, _ => 2 }"),
"{ let v = get(); match v { A => 1, _ => 2 } }"
);
}
#[test]
fn normalize_whitespace_trimmed() {
assert_eq!(normalize_body_as_expr(" Ok(1) "), "Ok(1)");
}
#[test]
fn pattern_matches_path() {
let pat = PurePattern::Path("Status::Active".to_string());
assert!(pattern_matches(&pat, "Status::Active"));
assert!(!pattern_matches(&pat, "Status::Inactive"));
}
#[test]
fn pattern_matches_tuple_struct() {
let pat = PurePattern::Struct {
path: "Message::Text".to_string(),
fields: vec![(
"0".to_string(),
PurePattern::Ident {
name: "s".to_string(),
is_mut: false,
},
)],
rest: false,
};
assert!(pattern_matches(&pat, "Message::Text(s)"));
assert!(!pattern_matches(&pat, "Message::Text { s }"));
assert!(!pattern_matches(&pat, "Message::Number(n)"));
}
#[test]
fn pattern_matches_tuple_struct_multi() {
let pat = PurePattern::Struct {
path: "Foo::Bar".to_string(),
fields: vec![
(
"0".to_string(),
PurePattern::Ident {
name: "a".to_string(),
is_mut: false,
},
),
(
"1".to_string(),
PurePattern::Ident {
name: "b".to_string(),
is_mut: false,
},
),
],
rest: false,
};
assert!(pattern_matches(&pat, "Foo::Bar(a, b)"));
}
#[test]
fn pattern_matches_tuple_struct_wildcard() {
let pat = PurePattern::Struct {
path: "Some".to_string(),
fields: vec![("0".to_string(), PurePattern::Wild)],
rest: false,
};
assert!(pattern_matches(&pat, "Some(_)"));
}
#[test]
fn pattern_matches_named_struct() {
let pat = PurePattern::Struct {
path: "Point".to_string(),
fields: vec![
(
"x".to_string(),
PurePattern::Ident {
name: "x".to_string(),
is_mut: false,
},
),
(
"y".to_string(),
PurePattern::Ident {
name: "y".to_string(),
is_mut: false,
},
),
],
rest: false,
};
assert!(pattern_matches(&pat, "Point { x, y }"));
assert!(
pattern_matches(&pat, "Point { x: x, y: y }"),
"Explicit field binding should match shorthand equivalent"
);
}
#[test]
fn pattern_matches_wild() {
assert!(pattern_matches(&PurePattern::Wild, "_"));
assert!(!pattern_matches(&PurePattern::Wild, "x"));
}
#[test]
fn parse_pattern_path() {
let pat = parse_pattern("Status::Active");
assert!(matches!(pat, PurePattern::Path(p) if p == "Status::Active"));
}
#[test]
fn parse_pattern_tuple_struct() {
let pat = parse_pattern("Message::Text(s)");
if let PurePattern::Struct { path, fields, rest } = &pat {
assert_eq!(path, "Message::Text");
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].0, "0");
assert!(matches!(&fields[0].1, PurePattern::Ident { name, .. } if name == "s"));
assert!(!rest);
} else {
panic!("Expected Struct (TupleStruct), got {:?}", pat);
}
}
#[test]
fn parse_pattern_tuple_struct_wild() {
let pat = parse_pattern("Some(_)");
if let PurePattern::Struct { path, fields, rest } = &pat {
assert_eq!(path, "Some");
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].0, "0");
assert!(matches!(&fields[0].1, PurePattern::Wild));
assert!(!rest);
} else {
panic!("Expected Struct (TupleStruct), got {:?}", pat);
}
}
#[test]
fn parse_pattern_wildcard() {
assert!(matches!(parse_pattern("_"), PurePattern::Wild));
}
#[test]
fn parse_pattern_struct() {
let pat = parse_pattern("Point { x, y }");
if let PurePattern::Struct { path, fields, rest } = &pat {
assert_eq!(path, "Point");
assert_eq!(fields.len(), 2);
assert!(!rest);
} else {
panic!("Expected Struct, got {:?}", pat);
}
}
#[test]
fn pattern_matches_other_variant() {
let pat = PurePattern::Other("Filter::Map(_)".to_string());
assert!(pattern_matches(&pat, "Filter::Map(_)"));
assert!(!pattern_matches(&pat, "Filter::Exclude(_)"));
}
#[test]
fn path_segment_exact_match() {
assert!(path_has_enum_segment("Filter::Recurse", "Filter"));
assert!(path_has_enum_segment("Filter", "Filter"));
}
#[test]
fn path_segment_no_substring_match() {
assert!(!path_has_enum_segment("FilterKind::Inclusive", "Filter"));
assert!(!path_has_enum_segment("MyFilter::Recurse", "Filter"));
}
#[test]
fn path_segment_middle_match() {
assert!(path_has_enum_segment("module::Filter::Recurse", "Filter"));
}
#[test]
fn pattern_contains_enum_path() {
let pat = PurePattern::Path("Filter::Recurse".to_string());
assert!(pattern_contains_enum(&pat, "Filter"));
assert!(!pattern_contains_enum(&pat, "FilterKind"));
}
#[test]
fn pattern_contains_enum_struct() {
let pat = PurePattern::Struct {
path: "FilterKind::Inclusive".to_string(),
fields: vec![],
rest: false,
};
assert!(pattern_contains_enum(&pat, "FilterKind"));
assert!(!pattern_contains_enum(&pat, "Filter"));
}
#[test]
fn pattern_contains_enum_other() {
let pat = PurePattern::Other("Filter::Map(_)".to_string());
assert!(pattern_contains_enum(&pat, "Filter"));
assert!(!pattern_contains_enum(&pat, "FilterKind"));
}
#[test]
fn normalize_shorthand_explicit_to_shorthand() {
assert_eq!(
normalize_pattern("Filter::Slice { start: start, end: end }"),
"Filter::Slice { start, end }"
);
}
#[test]
fn normalize_shorthand_already_short() {
assert_eq!(
normalize_pattern("Filter::Slice { start, end }"),
"Filter::Slice { start, end }"
);
}
#[test]
fn normalize_shorthand_different_binding_unchanged() {
assert_eq!(
normalize_pattern("Filter::Slice { start: s, end: e }"),
"Filter::Slice { start: s, end: e }"
);
}
#[test]
fn normalize_shorthand_mixed() {
assert_eq!(
normalize_pattern("Foo { a: a, b: other, c }"),
"Foo { a, b: other, c }"
);
}
#[test]
fn normalize_shorthand_with_rest() {
assert_eq!(normalize_pattern("Foo { x: x, .. }"), "Foo { x, .. }");
}
#[test]
fn normalize_shorthand_no_braces() {
assert_eq!(normalize_pattern("Some(x)"), "Some(x)");
}
#[test]
fn replace_match_arm_struct_pattern() {
use crate::engine::ASTMutationEngine;
use ryo_analysis::testing::ContextBuilder;
use ryo_mutations::basic::ReplaceMatchArmMutation;
let mut ctx = ContextBuilder::new()
.with_file(
"src/lib.rs",
r#"
enum Shape {
Circle { radius: f64 },
Rect { width: f64, height: f64 },
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rect { width, height } => width * height,
}
}
"#,
)
.build();
let area_id = ctx
.registry
.iter()
.find(|(id, path)| {
matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
&& path.name() == "area"
})
.map(|(id, _)| id)
.expect("area function not found");
let mutation = ReplaceMatchArmMutation {
function_id: area_id,
enum_name: "Shape".to_string(),
old_pattern: "Shape::Rect { width: width, height: height }".to_string(),
new_pattern: "Shape::Rect { width, height }".to_string(),
new_body: "width * height * 2.0".to_string(),
};
let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
assert_eq!(
result.result.changes, 1,
"Struct pattern with explicit binding should match: {}",
result.result.description
);
}
}