Skip to main content

ryo_executor/engine/impls/
match_arm.rs

1//! ASTRegApply implementation for match arm mutations
2//!
3//! V2 implementation that operates directly on ASTRegistry without
4//! creating temporary PureFile instances.
5//!
6//! # Design Notes
7//!
8//! ## Current Approach
9//!
10//! Match expressions are found by walking the function body AST and
11//! identifying matches by enum name in arm patterns.
12//!
13//! ## Future Improvements (TODO)
14//!
15//! - **TypeSystem Integration**: Match expressions are tightly coupled with
16//!   the type system. Currently we identify target match by enum name string
17//!   matching, but ideally we'd use type information to precisely identify
18//!   the correct match expression.
19//!
20//! - **PureAST Enhancement**: Consider whether Match should be treated
21//!   specially in PureAST rather than as generic expression content.
22//!   This would enable better exhaustiveness analysis and type-aware
23//!   transformations. However, this requires careful consideration of
24//!   DFN (Data Flow Normal form) principles.
25//!
26//! - **Nested Match Handling**: Current implementation finds first matching
27//!   match expression. May need more precise targeting for deeply nested
28//!   or multiple match expressions on same enum.
29
30use ryo_mutations::basic::{AddMatchArmMutation, RemoveMatchArmMutation, ReplaceMatchArmMutation};
31use ryo_mutations::MutationResult;
32use ryo_source::pure::{
33    MacroDelimiter, PureBlock, PureExpr, PureItem, PureMatchArm, PurePattern, PureStmt,
34};
35use ryo_symbol::SymbolKind;
36
37use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
38
39// ============================================================================
40// ASTRegApply for AddMatchArmMutation
41// ============================================================================
42
43impl ASTRegApply for AddMatchArmMutation {
44    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
45        // Use the provided symbol_id directly
46        let fn_id = self.function_id;
47
48        // Verify the target is a function or method
49        let kind = ctx.symbol_registry.kind(fn_id);
50        if !matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method)) {
51            return MutationResult {
52                mutation_type: "AddMatchArm".to_string(),
53                changes: 0,
54                description: format!("Symbol {} is not a function or method", fn_id),
55            };
56        }
57
58        // Get the function/method AST
59        let ast = match ctx.get_ast_mut(fn_id) {
60            Some(ast) => ast,
61            None => {
62                return MutationResult {
63                    mutation_type: "AddMatchArm".to_string(),
64                    changes: 0,
65                    description: format!("AST not found for function {}", fn_id),
66                };
67            }
68        };
69
70        // Apply to function body
71        if let PureItem::Fn(f) = ast {
72            if walk_and_add_arm(&mut f.body, &self.enum_name, &self.pattern, &self.body) {
73                ctx.emit_modified(fn_id, ModificationType::Other("MatchArmAdded".into()));
74                return MutationResult {
75                    mutation_type: "AddMatchArm".to_string(),
76                    changes: 1,
77                    description: format!(
78                        "Added match arm '{}' in function {}",
79                        self.pattern, fn_id
80                    ),
81                };
82            }
83        }
84
85        MutationResult {
86            mutation_type: "AddMatchArm".to_string(),
87            changes: 0,
88            description: format!(
89                "No match expression for '{}' found in function {}",
90                self.enum_name, fn_id
91            ),
92        }
93    }
94}
95
96// ============================================================================
97// ASTRegApply for RemoveMatchArmMutation
98// ============================================================================
99
100impl ASTRegApply for RemoveMatchArmMutation {
101    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
102        // Use the provided symbol_id directly
103        let fn_id = self.function_id;
104
105        // Verify the target is a function or method
106        let kind = ctx.symbol_registry.kind(fn_id);
107        if !matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method)) {
108            return MutationResult {
109                mutation_type: "RemoveMatchArm".to_string(),
110                changes: 0,
111                description: format!("Symbol {} is not a function or method", fn_id),
112            };
113        }
114
115        // Get the function AST
116        let ast = match ctx.get_ast_mut(fn_id) {
117            Some(ast) => ast,
118            None => {
119                return MutationResult {
120                    mutation_type: "RemoveMatchArm".to_string(),
121                    changes: 0,
122                    description: format!("AST not found for function {}", fn_id),
123                };
124            }
125        };
126
127        // Apply to function body
128        if let PureItem::Fn(f) = ast {
129            if walk_and_remove_arm(&mut f.body, &self.enum_name, &self.pattern) {
130                ctx.emit_modified(fn_id, ModificationType::Other("MatchArmRemoved".into()));
131                return MutationResult {
132                    mutation_type: "RemoveMatchArm".to_string(),
133                    changes: 1,
134                    description: format!(
135                        "Removed match arm '{}' from function {}",
136                        self.pattern, fn_id
137                    ),
138                };
139            }
140        }
141
142        MutationResult {
143            mutation_type: "RemoveMatchArm".to_string(),
144            changes: 0,
145            description: format!(
146                "No match arm '{}' found in function {}",
147                self.pattern, fn_id
148            ),
149        }
150    }
151}
152
153// ============================================================================
154// Helper functions for walking AST and modifying match expressions
155// ============================================================================
156
157/// Walk block and add arm to matching match expression
158fn walk_and_add_arm(block: &mut PureBlock, enum_name: &str, pattern: &str, body: &str) -> bool {
159    for stmt in &mut block.stmts {
160        if walk_stmt_and_add_arm(stmt, enum_name, pattern, body) {
161            return true;
162        }
163    }
164    false
165}
166
167/// Walk block and remove arm from matching match expression
168fn walk_and_remove_arm(block: &mut PureBlock, enum_name: &str, pattern: &str) -> bool {
169    for stmt in &mut block.stmts {
170        if walk_stmt_and_remove_arm(stmt, enum_name, pattern) {
171            return true;
172        }
173    }
174    false
175}
176
177fn walk_stmt_and_add_arm(stmt: &mut PureStmt, enum_name: &str, pattern: &str, body: &str) -> bool {
178    match stmt {
179        PureStmt::Local {
180            init: Some(expr), ..
181        } => {
182            return walk_expr_and_add_arm(expr, enum_name, pattern, body);
183        }
184        PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
185            return walk_expr_and_add_arm(expr, enum_name, pattern, body);
186        }
187        _ => {}
188    }
189    false
190}
191
192fn walk_stmt_and_remove_arm(stmt: &mut PureStmt, enum_name: &str, pattern: &str) -> bool {
193    match stmt {
194        PureStmt::Local {
195            init: Some(expr), ..
196        } => {
197            return walk_expr_and_remove_arm(expr, enum_name, pattern);
198        }
199        PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
200            return walk_expr_and_remove_arm(expr, enum_name, pattern);
201        }
202        _ => {}
203    }
204    false
205}
206
207fn walk_expr_and_add_arm(expr: &mut PureExpr, enum_name: &str, pattern: &str, body: &str) -> bool {
208    // Check if this is the target match expression
209    if let PureExpr::Match {
210        expr: match_expr,
211        arms,
212    } = expr
213    {
214        if is_target_match(match_expr, arms, enum_name) {
215            // Check if arm already exists
216            if arms.iter().any(|a| pattern_matches(&a.pattern, pattern)) {
217                return false;
218            }
219
220            // Find insert position (before wildcard/rest patterns)
221            let insert_pos = arms
222                .iter()
223                .position(|a| matches!(a.pattern, PurePattern::Wild | PurePattern::Rest))
224                .unwrap_or(arms.len());
225
226            // Create and insert new arm
227            arms.insert(insert_pos, create_arm(pattern, body));
228            return true;
229        }
230    }
231
232    // Recursively walk nested expressions
233    walk_expr_children_and_add_arm(expr, enum_name, pattern, body)
234}
235
236fn walk_expr_and_remove_arm(expr: &mut PureExpr, enum_name: &str, pattern: &str) -> bool {
237    // Check if this is the target match expression
238    if let PureExpr::Match {
239        expr: match_expr,
240        arms,
241    } = expr
242    {
243        if is_target_match(match_expr, arms, enum_name) {
244            let original_len = arms.len();
245            arms.retain(|a| !pattern_matches(&a.pattern, pattern));
246            if arms.len() < original_len {
247                return true;
248            }
249        }
250    }
251
252    // Recursively walk nested expressions
253    walk_expr_children_and_remove_arm(expr, enum_name, pattern)
254}
255
256/// Check if match expression targets the specified enum
257fn is_target_match(match_expr: &PureExpr, arms: &[PureMatchArm], enum_name: &str) -> bool {
258    // Check if any arm's pattern contains the enum name
259    for arm in arms {
260        if pattern_contains_enum(&arm.pattern, enum_name) {
261            return true;
262        }
263    }
264
265    // Check if matched expression is a path containing enum name
266    if let PureExpr::Path(path) = match_expr {
267        if path.contains(enum_name) {
268            return true;
269        }
270    }
271
272    false
273}
274
275fn pattern_contains_enum(pattern: &PurePattern, enum_name: &str) -> bool {
276    match pattern {
277        PurePattern::Path(path) => path_has_enum_segment(path, enum_name),
278        PurePattern::Struct { path, .. } => path_has_enum_segment(path, enum_name),
279        PurePattern::Or(patterns) => patterns.iter().any(|p| pattern_contains_enum(p, enum_name)),
280        PurePattern::Other(s) => {
281            // Extract path prefix from verbatim pattern (e.g., "Filter::Map(_)" → "Filter::Map")
282            let path_part = s.split(&['(', '{', ' '][..]).next().unwrap_or(s);
283            path_has_enum_segment(path_part, enum_name)
284        }
285        _ => false,
286    }
287}
288
289/// Check if a `::` separated path contains the enum name as an exact segment.
290///
291/// `"Filter::Recurse"` with enum_name `"Filter"` → true
292/// `"FilterKind::Inclusive"` with enum_name `"Filter"` → false (substring, not segment)
293fn path_has_enum_segment(path: &str, enum_name: &str) -> bool {
294    path.split("::").any(|segment| segment == enum_name)
295}
296
297/// Create a new match arm from pattern and body strings
298fn create_arm(pattern: &str, body: &str) -> PureMatchArm {
299    // Detect if pattern is a simple path or a complex pattern
300    // Complex patterns contain: (, ), {, }, .. (rest pattern), or other pattern syntax
301    // These need to be preserved verbatim to avoid mangling by parse_path
302    let pat = if pattern.contains('(') || pattern.contains('{') || pattern.contains("..") {
303        // Use Other to preserve the pattern verbatim
304        PurePattern::Other(pattern.to_string())
305    } else {
306        PurePattern::Path(pattern.to_string())
307    };
308
309    // Parse body - detect macro calls like "todo!()"
310    let body_expr = if let Some(bang_pos) = body.find('!') {
311        let body_str = body.trim();
312        let name = body_str[..bang_pos].trim();
313        let rest = body_str[bang_pos + 1..].trim();
314
315        // Validate macro pattern
316        let is_valid_ident = !name.is_empty()
317            && name.chars().all(|c| c.is_alphanumeric() || c == '_')
318            && name
319                .chars()
320                .next()
321                .map(|c| c.is_alphabetic() || c == '_')
322                .unwrap_or(false);
323        let has_delimiter = rest.starts_with('(') || rest.starts_with('{') || rest.starts_with('[');
324
325        if is_valid_ident && has_delimiter {
326            let (delimiter, tokens) = if rest.starts_with('(') && rest.ends_with(')') {
327                (MacroDelimiter::Paren, rest[1..rest.len() - 1].to_string())
328            } else if rest.starts_with('{') && rest.ends_with('}') {
329                (MacroDelimiter::Brace, rest[1..rest.len() - 1].to_string())
330            } else if rest.starts_with('[') && rest.ends_with(']') {
331                (MacroDelimiter::Bracket, rest[1..rest.len() - 1].to_string())
332            } else {
333                (MacroDelimiter::Paren, String::new())
334            };
335            PureExpr::Macro {
336                name: name.to_string(),
337                delimiter,
338                tokens,
339            }
340        } else {
341            PureExpr::Other(normalize_body_as_expr(body))
342        }
343    } else {
344        PureExpr::Other(normalize_body_as_expr(body))
345    };
346
347    PureMatchArm {
348        pattern: pat,
349        guard: None,
350        body: body_expr,
351    }
352}
353
354/// Normalize a body string so it is a valid Rust expression for syn::parse_str.
355///
356/// Statement sequences like `let v = x; if v { a } else { b }` are NOT valid
357/// expressions — they are multiple statements. Passing them to syn::parse_str
358/// causes exponential backtracking and hangs. Wrapping in `{ ... }` makes them
359/// a valid block expression.
360fn normalize_body_as_expr(body: &str) -> String {
361    let trimmed = body.trim();
362    if trimmed.contains(';') && !trimmed.starts_with('{') {
363        format!("{{ {} }}", trimmed)
364    } else {
365        trimmed.to_string()
366    }
367}
368
369// ============================================================================
370// Recursive expression walkers
371// ============================================================================
372
373fn walk_expr_children_and_add_arm(
374    expr: &mut PureExpr,
375    enum_name: &str,
376    pattern: &str,
377    body: &str,
378) -> bool {
379    match expr {
380        PureExpr::Block { block, .. } => walk_and_add_arm(block, enum_name, pattern, body),
381        PureExpr::If {
382            cond,
383            then_branch,
384            else_branch,
385        } => {
386            if walk_expr_and_add_arm(cond, enum_name, pattern, body) {
387                return true;
388            }
389            if walk_and_add_arm(then_branch, enum_name, pattern, body) {
390                return true;
391            }
392            if let Some(else_expr) = else_branch {
393                if walk_expr_and_add_arm(else_expr, enum_name, pattern, body) {
394                    return true;
395                }
396            }
397            false
398        }
399        PureExpr::Match { expr: e, arms } => {
400            if walk_expr_and_add_arm(e, enum_name, pattern, body) {
401                return true;
402            }
403            for arm in arms {
404                if walk_expr_and_add_arm(&mut arm.body, enum_name, pattern, body) {
405                    return true;
406                }
407            }
408            false
409        }
410        PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
411            walk_and_add_arm(block, enum_name, pattern, body)
412        }
413        PureExpr::While { cond, body: b, .. } => {
414            walk_expr_and_add_arm(cond, enum_name, pattern, body)
415                || walk_and_add_arm(b, enum_name, pattern, body)
416        }
417        PureExpr::For {
418            expr: e, body: b, ..
419        } => {
420            walk_expr_and_add_arm(e, enum_name, pattern, body)
421                || walk_and_add_arm(b, enum_name, pattern, body)
422        }
423        PureExpr::Async { body: b, .. } => walk_and_add_arm(b, enum_name, pattern, body),
424        PureExpr::Closure { body: b, .. } => walk_expr_and_add_arm(b, enum_name, pattern, body),
425        PureExpr::Call { func, args } => {
426            if walk_expr_and_add_arm(func, enum_name, pattern, body) {
427                return true;
428            }
429            for arg in args {
430                if walk_expr_and_add_arm(arg, enum_name, pattern, body) {
431                    return true;
432                }
433            }
434            false
435        }
436        PureExpr::MethodCall { receiver, args, .. } => {
437            if walk_expr_and_add_arm(receiver, enum_name, pattern, body) {
438                return true;
439            }
440            for arg in args {
441                if walk_expr_and_add_arm(arg, enum_name, pattern, body) {
442                    return true;
443                }
444            }
445            false
446        }
447        PureExpr::Binary { left, right, .. } => {
448            walk_expr_and_add_arm(left, enum_name, pattern, body)
449                || walk_expr_and_add_arm(right, enum_name, pattern, body)
450        }
451        PureExpr::Unary { expr: e, .. }
452        | PureExpr::Field { expr: e, .. }
453        | PureExpr::Await(e)
454        | PureExpr::Try(e) => walk_expr_and_add_arm(e, enum_name, pattern, body),
455        PureExpr::Index { expr: e, index } => {
456            walk_expr_and_add_arm(e, enum_name, pattern, body)
457                || walk_expr_and_add_arm(index, enum_name, pattern, body)
458        }
459        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
460            for e in exprs {
461                if walk_expr_and_add_arm(e, enum_name, pattern, body) {
462                    return true;
463                }
464            }
465            false
466        }
467        PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
468            walk_expr_and_add_arm(e, enum_name, pattern, body)
469        }
470        PureExpr::Let { expr: e, .. }
471        | PureExpr::Cast { expr: e, .. }
472        | PureExpr::Ref { expr: e, .. } => walk_expr_and_add_arm(e, enum_name, pattern, body),
473        PureExpr::Range { start, end, .. } => {
474            if let Some(s) = start {
475                if walk_expr_and_add_arm(s, enum_name, pattern, body) {
476                    return true;
477                }
478            }
479            if let Some(e) = end {
480                if walk_expr_and_add_arm(e, enum_name, pattern, body) {
481                    return true;
482                }
483            }
484            false
485        }
486        PureExpr::Struct { fields, .. } => {
487            for (_, e) in fields {
488                if walk_expr_and_add_arm(e, enum_name, pattern, body) {
489                    return true;
490                }
491            }
492            false
493        }
494        PureExpr::Repeat { expr: e, len } => {
495            walk_expr_and_add_arm(e, enum_name, pattern, body)
496                || walk_expr_and_add_arm(len, enum_name, pattern, body)
497        }
498        _ => false,
499    }
500}
501
502fn walk_expr_children_and_remove_arm(expr: &mut PureExpr, enum_name: &str, pattern: &str) -> bool {
503    match expr {
504        PureExpr::Block { block, .. } => walk_and_remove_arm(block, enum_name, pattern),
505        PureExpr::If {
506            cond,
507            then_branch,
508            else_branch,
509        } => {
510            if walk_expr_and_remove_arm(cond, enum_name, pattern) {
511                return true;
512            }
513            if walk_and_remove_arm(then_branch, enum_name, pattern) {
514                return true;
515            }
516            if let Some(else_expr) = else_branch {
517                if walk_expr_and_remove_arm(else_expr, enum_name, pattern) {
518                    return true;
519                }
520            }
521            false
522        }
523        PureExpr::Match { expr: e, arms } => {
524            if walk_expr_and_remove_arm(e, enum_name, pattern) {
525                return true;
526            }
527            for arm in arms {
528                if walk_expr_and_remove_arm(&mut arm.body, enum_name, pattern) {
529                    return true;
530                }
531            }
532            false
533        }
534        PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
535            walk_and_remove_arm(block, enum_name, pattern)
536        }
537        PureExpr::While { cond, body, .. } => {
538            walk_expr_and_remove_arm(cond, enum_name, pattern)
539                || walk_and_remove_arm(body, enum_name, pattern)
540        }
541        PureExpr::For { expr: e, body, .. } => {
542            walk_expr_and_remove_arm(e, enum_name, pattern)
543                || walk_and_remove_arm(body, enum_name, pattern)
544        }
545        PureExpr::Async { body, .. } => walk_and_remove_arm(body, enum_name, pattern),
546        PureExpr::Closure { body, .. } => walk_expr_and_remove_arm(body, enum_name, pattern),
547        PureExpr::Call { func, args } => {
548            if walk_expr_and_remove_arm(func, enum_name, pattern) {
549                return true;
550            }
551            for arg in args {
552                if walk_expr_and_remove_arm(arg, enum_name, pattern) {
553                    return true;
554                }
555            }
556            false
557        }
558        PureExpr::MethodCall { receiver, args, .. } => {
559            if walk_expr_and_remove_arm(receiver, enum_name, pattern) {
560                return true;
561            }
562            for arg in args {
563                if walk_expr_and_remove_arm(arg, enum_name, pattern) {
564                    return true;
565                }
566            }
567            false
568        }
569        PureExpr::Binary { left, right, .. } => {
570            walk_expr_and_remove_arm(left, enum_name, pattern)
571                || walk_expr_and_remove_arm(right, enum_name, pattern)
572        }
573        PureExpr::Unary { expr: e, .. }
574        | PureExpr::Field { expr: e, .. }
575        | PureExpr::Await(e)
576        | PureExpr::Try(e) => walk_expr_and_remove_arm(e, enum_name, pattern),
577        PureExpr::Index { expr: e, index } => {
578            walk_expr_and_remove_arm(e, enum_name, pattern)
579                || walk_expr_and_remove_arm(index, enum_name, pattern)
580        }
581        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
582            for e in exprs {
583                if walk_expr_and_remove_arm(e, enum_name, pattern) {
584                    return true;
585                }
586            }
587            false
588        }
589        PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
590            walk_expr_and_remove_arm(e, enum_name, pattern)
591        }
592        PureExpr::Let { expr: e, .. }
593        | PureExpr::Cast { expr: e, .. }
594        | PureExpr::Ref { expr: e, .. } => walk_expr_and_remove_arm(e, enum_name, pattern),
595        PureExpr::Range { start, end, .. } => {
596            if let Some(s) = start {
597                if walk_expr_and_remove_arm(s, enum_name, pattern) {
598                    return true;
599                }
600            }
601            if let Some(e) = end {
602                if walk_expr_and_remove_arm(e, enum_name, pattern) {
603                    return true;
604                }
605            }
606            false
607        }
608        PureExpr::Struct { fields, .. } => {
609            for (_, e) in fields {
610                if walk_expr_and_remove_arm(e, enum_name, pattern) {
611                    return true;
612                }
613            }
614            false
615        }
616        PureExpr::Repeat { expr: e, len } => {
617            walk_expr_and_remove_arm(e, enum_name, pattern)
618                || walk_expr_and_remove_arm(len, enum_name, pattern)
619        }
620        _ => false,
621    }
622}
623
624// ============================================================================
625// ASTRegApply for ReplaceMatchArmMutation
626// ============================================================================
627
628impl ASTRegApply for ReplaceMatchArmMutation {
629    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
630        // Use the provided symbol_id directly
631        let fn_id = self.function_id;
632
633        // Verify the target is a function or method
634        let kind = ctx.symbol_registry.kind(fn_id);
635        if !matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method)) {
636            return MutationResult {
637                mutation_type: "ReplaceMatchArm".to_string(),
638                changes: 0,
639                description: format!("Symbol {} is not a function or method", fn_id),
640            };
641        }
642
643        // Get the function AST
644        let ast = match ctx.get_ast_mut(fn_id) {
645            Some(ast) => ast,
646            None => {
647                return MutationResult {
648                    mutation_type: "ReplaceMatchArm".to_string(),
649                    changes: 0,
650                    description: format!("AST not found for function {}", fn_id),
651                };
652            }
653        };
654
655        // Apply to function body
656        if let PureItem::Fn(f) = ast {
657            if walk_and_replace_arm(
658                &mut f.body,
659                &self.enum_name,
660                &self.old_pattern,
661                &self.new_pattern,
662                &self.new_body,
663            ) {
664                ctx.emit_modified(fn_id, ModificationType::Other("MatchArmReplaced".into()));
665                return MutationResult {
666                    mutation_type: "ReplaceMatchArm".to_string(),
667                    changes: 1,
668                    description: format!(
669                        "Replaced match arm '{}' with '{}' in function {}",
670                        self.old_pattern, self.new_pattern, fn_id
671                    ),
672                };
673            }
674        }
675
676        MutationResult {
677            mutation_type: "ReplaceMatchArm".to_string(),
678            changes: 0,
679            description: format!(
680                "No match arm '{}' for '{}' found in function {}",
681                self.old_pattern, self.enum_name, fn_id
682            ),
683        }
684    }
685}
686
687// ============================================================================
688// Helper functions for replace operation
689// ============================================================================
690
691/// Walk block and replace arm in matching match expression
692fn walk_and_replace_arm(
693    block: &mut PureBlock,
694    enum_name: &str,
695    old_pattern: &str,
696    new_pattern: &str,
697    new_body: &str,
698) -> bool {
699    for stmt in &mut block.stmts {
700        if walk_stmt_and_replace_arm(stmt, enum_name, old_pattern, new_pattern, new_body) {
701            return true;
702        }
703    }
704    false
705}
706
707fn walk_stmt_and_replace_arm(
708    stmt: &mut PureStmt,
709    enum_name: &str,
710    old_pattern: &str,
711    new_pattern: &str,
712    new_body: &str,
713) -> bool {
714    match stmt {
715        PureStmt::Local {
716            init: Some(expr), ..
717        } => {
718            return walk_expr_and_replace_arm(expr, enum_name, old_pattern, new_pattern, new_body);
719        }
720        PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
721            return walk_expr_and_replace_arm(expr, enum_name, old_pattern, new_pattern, new_body);
722        }
723        _ => {}
724    }
725    false
726}
727
728fn walk_expr_and_replace_arm(
729    expr: &mut PureExpr,
730    enum_name: &str,
731    old_pattern: &str,
732    new_pattern: &str,
733    new_body: &str,
734) -> bool {
735    // Check if this is the target match expression
736    if let PureExpr::Match {
737        expr: match_expr,
738        arms,
739    } = expr
740    {
741        if is_target_match(match_expr, arms, enum_name) {
742            // Find and replace the matching arm
743            for arm in arms.iter_mut() {
744                if pattern_matches(&arm.pattern, old_pattern) {
745                    // Replace pattern and body
746                    arm.pattern = parse_pattern(new_pattern);
747                    arm.body = parse_body(new_body);
748                    return true;
749                }
750            }
751        }
752    }
753
754    // Recursively walk nested expressions
755    walk_expr_children_and_replace_arm(expr, enum_name, old_pattern, new_pattern, new_body)
756}
757
758/// Check if a pattern matches the target pattern string
759fn pattern_matches(pattern: &PurePattern, target: &str) -> bool {
760    match pattern {
761        PurePattern::Path(path) => path == target,
762        PurePattern::Struct { path, fields, rest } => {
763            // Detect TupleStruct: numeric field names, not rest, non-empty
764            // (same heuristic as to_syn conversion)
765            let is_tuple_struct = !*rest
766                && !fields.is_empty()
767                && fields.iter().all(|(name, _)| name.parse::<u32>().is_ok());
768
769            let pattern_str = if is_tuple_struct {
770                // Build parenthesized form: "Path::Variant(a, b)"
771                let field_strs: Vec<_> = fields
772                    .iter()
773                    .map(|(_, pat)| format_pattern_for_display(pat))
774                    .collect();
775                format!("{}({})", path, field_strs.join(", "))
776            } else {
777                // Build brace form: "Path::Variant { field1, field2 }"
778                let mut s = path.clone();
779                s.push_str(" { ");
780                let field_strs: Vec<_> = fields
781                    .iter()
782                    .map(|(name, pat)| {
783                        if matches!(pat, PurePattern::Ident { name: ident, .. } if ident == name) {
784                            name.clone()
785                        } else if matches!(pat, PurePattern::Wild) {
786                            format!("{}: _", name)
787                        } else {
788                            format!("{}: {}", name, format_pattern_for_display(pat))
789                        }
790                    })
791                    .collect();
792                s.push_str(&field_strs.join(", "));
793                if *rest {
794                    if !fields.is_empty() {
795                        s.push_str(", ");
796                    }
797                    s.push_str("..");
798                }
799                s.push_str(" }");
800                s
801            };
802
803            normalize_pattern(&pattern_str) == normalize_pattern(target)
804        }
805        PurePattern::Tuple(patterns) => {
806            let patterns_str: Vec<_> = patterns.iter().map(format_pattern_for_display).collect();
807            let pattern_str = format!("({})", patterns_str.join(", "));
808            normalize_pattern(&pattern_str) == normalize_pattern(target)
809        }
810        PurePattern::Wild => target == "_",
811        PurePattern::Ident { name, .. } => name == target,
812        PurePattern::Other(s) => normalize_pattern(s) == normalize_pattern(target),
813        _ => false,
814    }
815}
816
817/// Format a PurePattern for human-readable display (used in pattern_matches comparisons)
818fn format_pattern_for_display(pattern: &PurePattern) -> String {
819    match pattern {
820        PurePattern::Ident { name, .. } => name.clone(),
821        PurePattern::Wild => "_".to_string(),
822        PurePattern::Path(path) => path.clone(),
823        PurePattern::Rest => "..".to_string(),
824        PurePattern::Tuple(pats) => {
825            let inner: Vec<_> = pats.iter().map(format_pattern_for_display).collect();
826            format!("({})", inner.join(", "))
827        }
828        PurePattern::Or(pats) => {
829            let inner: Vec<_> = pats.iter().map(format_pattern_for_display).collect();
830            inner.join(" | ")
831        }
832        PurePattern::Struct { path, fields, rest } => {
833            let is_tuple_struct = !*rest
834                && !fields.is_empty()
835                && fields.iter().all(|(name, _)| name.parse::<u32>().is_ok());
836            if is_tuple_struct {
837                let inner: Vec<_> = fields
838                    .iter()
839                    .map(|(_, pat)| format_pattern_for_display(pat))
840                    .collect();
841                format!("{}({})", path, inner.join(", "))
842            } else {
843                let inner: Vec<_> = fields
844                    .iter()
845                    .map(|(name, pat)| {
846                        if matches!(pat, PurePattern::Ident { name: ident, .. } if ident == name) {
847                            name.clone()
848                        } else {
849                            format!("{}: {}", name, format_pattern_for_display(pat))
850                        }
851                    })
852                    .collect();
853                let rest_str = if *rest {
854                    if inner.is_empty() {
855                        ".."
856                    } else {
857                        ", .."
858                    }
859                } else {
860                    ""
861                };
862                format!("{} {{ {}{} }}", path, inner.join(", "), rest_str)
863            }
864        }
865        other => format!("{:?}", other),
866    }
867}
868
869/// Normalize pattern string for comparison (remove extra whitespace)
870fn normalize_pattern(s: &str) -> String {
871    let s = s.split_whitespace().collect::<Vec<_>>().join(" ");
872    // Normalize struct field shorthand: "{ field: field }" → "{ field }"
873    // In Rust, `Foo { x: x }` and `Foo { x }` are semantically identical.
874    normalize_field_shorthand(&s)
875}
876
877/// Normalize `name: name` → `name` inside `{ ... }` sections.
878///
879/// `Foo { start: start, end: end }` → `Foo { start, end }`
880/// `Foo { start: s, end: e }` → unchanged (name ≠ binding)
881fn normalize_field_shorthand(s: &str) -> String {
882    let Some(brace_start) = s.find('{') else {
883        return s.to_string();
884    };
885    let Some(brace_end) = s.rfind('}') else {
886        return s.to_string();
887    };
888
889    let before = &s[..=brace_start];
890    let fields_str = &s[brace_start + 1..brace_end];
891    let after = &s[brace_end..];
892
893    let normalized_fields: Vec<String> = fields_str
894        .split(',')
895        .map(|field| {
896            let field = field.trim();
897            if field.is_empty() || field == ".." {
898                return field.to_string();
899            }
900            if let Some(colon_pos) = field.find(':') {
901                let name = field[..colon_pos].trim();
902                let value = field[colon_pos + 1..].trim();
903                if name == value {
904                    name.to_string()
905                } else {
906                    field.to_string()
907                }
908            } else {
909                field.to_string()
910            }
911        })
912        .collect();
913
914    format!(
915        "{} {} {}",
916        before.trim(),
917        normalized_fields.join(", "),
918        after.trim()
919    )
920}
921
922/// Parse a pattern string into PurePattern
923fn parse_pattern(pattern: &str) -> PurePattern {
924    let pattern = pattern.trim();
925
926    // Check for struct pattern: "Type::Variant { field1, field2: _ }"
927    if let Some(brace_start) = pattern.find('{') {
928        if let Some(brace_end) = pattern.rfind('}') {
929            let path = pattern[..brace_start].trim().to_string();
930            let fields_str = &pattern[brace_start + 1..brace_end];
931
932            let mut fields = Vec::new();
933            let mut rest = false;
934
935            for field_part in fields_str.split(',') {
936                let field_part = field_part.trim();
937                if field_part.is_empty() {
938                    continue;
939                }
940                if field_part == ".." {
941                    rest = true;
942                    continue;
943                }
944
945                if let Some(colon_pos) = field_part.find(':') {
946                    let name = field_part[..colon_pos].trim().to_string();
947                    let pat_str = field_part[colon_pos + 1..].trim();
948                    let pat = if pat_str == "_" {
949                        PurePattern::Wild
950                    } else {
951                        PurePattern::Ident {
952                            name: pat_str.to_string(),
953                            is_mut: false,
954                            by_ref: false,
955                        }
956                    };
957                    fields.push((name, pat));
958                } else {
959                    // Shorthand: `field` means `field: field`
960                    let name = field_part.to_string();
961                    fields.push((
962                        name.clone(),
963                        PurePattern::Ident {
964                            name,
965                            is_mut: false,
966                            by_ref: false,
967                        },
968                    ));
969                }
970            }
971
972            return PurePattern::Struct { path, fields, rest };
973        }
974    }
975
976    // Check for TupleStruct pattern: "Path::Variant(a, b)" or "Some(x)"
977    // Must have '(' NOT at position 0 (that's a bare tuple) and ')' at end
978    if let Some(paren_start) = pattern.find('(') {
979        if paren_start > 0 && pattern.ends_with(')') {
980            let path = pattern[..paren_start].trim().to_string();
981            let inner = &pattern[paren_start + 1..pattern.len() - 1];
982            let fields: Vec<(String, PurePattern)> = inner
983                .split(',')
984                .enumerate()
985                .filter_map(|(i, s)| {
986                    let s = s.trim();
987                    if s.is_empty() {
988                        return None;
989                    }
990                    let pat = if s == "_" {
991                        PurePattern::Wild
992                    } else {
993                        PurePattern::Ident {
994                            name: s.to_string(),
995                            is_mut: false,
996                            by_ref: false,
997                        }
998                    };
999                    Some((i.to_string(), pat))
1000                })
1001                .collect();
1002            return PurePattern::Struct {
1003                path,
1004                fields,
1005                rest: false,
1006            };
1007        }
1008    }
1009
1010    // Check for tuple pattern: "(a, b)"
1011    if pattern.starts_with('(') && pattern.ends_with(')') {
1012        let inner = &pattern[1..pattern.len() - 1];
1013        let parts: Vec<_> = inner.split(',').map(|s| s.trim()).collect();
1014        if parts.len() > 1 || !inner.is_empty() {
1015            let patterns: Vec<_> = parts
1016                .iter()
1017                .map(|&s| {
1018                    if s == "_" {
1019                        PurePattern::Wild
1020                    } else {
1021                        PurePattern::Ident {
1022                            name: s.to_string(),
1023                            is_mut: false,
1024                            by_ref: false,
1025                        }
1026                    }
1027                })
1028                .collect();
1029            return PurePattern::Tuple(patterns);
1030        }
1031    }
1032
1033    // Check for wildcard
1034    if pattern == "_" {
1035        return PurePattern::Wild;
1036    }
1037
1038    // Default to path pattern
1039    PurePattern::Path(pattern.to_string())
1040}
1041
1042/// Parse a body string into PureExpr
1043fn parse_body(body: &str) -> PureExpr {
1044    let body_str = body.trim();
1045
1046    // Parse macro calls like "todo!()"
1047    if let Some(bang_pos) = body_str.find('!') {
1048        let name = body_str[..bang_pos].trim();
1049        let rest = body_str[bang_pos + 1..].trim();
1050
1051        // Validate macro pattern
1052        let is_valid_ident = !name.is_empty()
1053            && name.chars().all(|c| c.is_alphanumeric() || c == '_')
1054            && name
1055                .chars()
1056                .next()
1057                .map(|c| c.is_alphabetic() || c == '_')
1058                .unwrap_or(false);
1059        let has_delimiter = rest.starts_with('(') || rest.starts_with('{') || rest.starts_with('[');
1060
1061        if is_valid_ident && has_delimiter {
1062            let (delimiter, tokens) = if rest.starts_with('(') && rest.ends_with(')') {
1063                (MacroDelimiter::Paren, rest[1..rest.len() - 1].to_string())
1064            } else if rest.starts_with('{') && rest.ends_with('}') {
1065                (MacroDelimiter::Brace, rest[1..rest.len() - 1].to_string())
1066            } else if rest.starts_with('[') && rest.ends_with(']') {
1067                (MacroDelimiter::Bracket, rest[1..rest.len() - 1].to_string())
1068            } else {
1069                (MacroDelimiter::Paren, String::new())
1070            };
1071            return PureExpr::Macro {
1072                name: name.to_string(),
1073                delimiter,
1074                tokens,
1075            };
1076        }
1077    }
1078
1079    // Default to Other
1080    PureExpr::Other(body.to_string())
1081}
1082
1083fn walk_expr_children_and_replace_arm(
1084    expr: &mut PureExpr,
1085    enum_name: &str,
1086    old_pattern: &str,
1087    new_pattern: &str,
1088    new_body: &str,
1089) -> bool {
1090    match expr {
1091        PureExpr::Block { block, .. } => {
1092            walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
1093        }
1094        PureExpr::If {
1095            cond,
1096            then_branch,
1097            else_branch,
1098        } => {
1099            if walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body) {
1100                return true;
1101            }
1102            if walk_and_replace_arm(then_branch, enum_name, old_pattern, new_pattern, new_body) {
1103                return true;
1104            }
1105            if let Some(else_expr) = else_branch {
1106                if walk_expr_and_replace_arm(
1107                    else_expr,
1108                    enum_name,
1109                    old_pattern,
1110                    new_pattern,
1111                    new_body,
1112                ) {
1113                    return true;
1114                }
1115            }
1116            false
1117        }
1118        PureExpr::Match { expr: e, arms } => {
1119            if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1120                return true;
1121            }
1122            for arm in arms {
1123                if walk_expr_and_replace_arm(
1124                    &mut arm.body,
1125                    enum_name,
1126                    old_pattern,
1127                    new_pattern,
1128                    new_body,
1129                ) {
1130                    return true;
1131                }
1132            }
1133            false
1134        }
1135        PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
1136            walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
1137        }
1138        PureExpr::While { cond, body, .. } => {
1139            walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body)
1140                || walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1141        }
1142        PureExpr::For { expr: e, body, .. } => {
1143            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1144                || walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1145        }
1146        PureExpr::Async { body, .. } => {
1147            walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1148        }
1149        PureExpr::Closure { body, .. } => {
1150            walk_expr_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1151        }
1152        PureExpr::Call { func, args } => {
1153            if walk_expr_and_replace_arm(func, enum_name, old_pattern, new_pattern, new_body) {
1154                return true;
1155            }
1156            for arg in args {
1157                if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
1158                    return true;
1159                }
1160            }
1161            false
1162        }
1163        PureExpr::MethodCall { receiver, args, .. } => {
1164            if walk_expr_and_replace_arm(receiver, enum_name, old_pattern, new_pattern, new_body) {
1165                return true;
1166            }
1167            for arg in args {
1168                if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
1169                    return true;
1170                }
1171            }
1172            false
1173        }
1174        PureExpr::Binary { left, right, .. } => {
1175            walk_expr_and_replace_arm(left, enum_name, old_pattern, new_pattern, new_body)
1176                || walk_expr_and_replace_arm(right, enum_name, old_pattern, new_pattern, new_body)
1177        }
1178        PureExpr::Unary { expr: e, .. }
1179        | PureExpr::Field { expr: e, .. }
1180        | PureExpr::Await(e)
1181        | PureExpr::Try(e) => {
1182            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1183        }
1184        PureExpr::Index { expr: e, index } => {
1185            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1186                || walk_expr_and_replace_arm(index, enum_name, old_pattern, new_pattern, new_body)
1187        }
1188        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1189            for e in exprs {
1190                if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1191                    return true;
1192                }
1193            }
1194            false
1195        }
1196        PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
1197            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1198        }
1199        PureExpr::Let { expr: e, .. }
1200        | PureExpr::Cast { expr: e, .. }
1201        | PureExpr::Ref { expr: e, .. } => {
1202            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1203        }
1204        PureExpr::Range { start, end, .. } => {
1205            if let Some(s) = start {
1206                if walk_expr_and_replace_arm(s, enum_name, old_pattern, new_pattern, new_body) {
1207                    return true;
1208                }
1209            }
1210            if let Some(e) = end {
1211                if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1212                    return true;
1213                }
1214            }
1215            false
1216        }
1217        PureExpr::Struct { fields, .. } => {
1218            for (_, e) in fields {
1219                if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1220                    return true;
1221                }
1222            }
1223            false
1224        }
1225        PureExpr::Repeat { expr: e, len } => {
1226            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1227                || walk_expr_and_replace_arm(len, enum_name, old_pattern, new_pattern, new_body)
1228        }
1229        _ => false,
1230    }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235    use super::*;
1236
1237    #[test]
1238    fn normalize_simple_expr_unchanged() {
1239        assert_eq!(normalize_body_as_expr("Ok(42)"), "Ok(42)");
1240    }
1241
1242    #[test]
1243    fn normalize_block_expr_unchanged() {
1244        assert_eq!(
1245            normalize_body_as_expr("{ let v = 1; v + 1 }"),
1246            "{ let v = 1; v + 1 }"
1247        );
1248    }
1249
1250    #[test]
1251    fn normalize_stmt_sequence_wrapped() {
1252        assert_eq!(
1253            normalize_body_as_expr("let v = x; if v { a } else { b }"),
1254            "{ let v = x; if v { a } else { b } }"
1255        );
1256    }
1257
1258    #[test]
1259    fn normalize_let_match_wrapped() {
1260        assert_eq!(
1261            normalize_body_as_expr("let v = get(); match v { A => 1, _ => 2 }"),
1262            "{ let v = get(); match v { A => 1, _ => 2 } }"
1263        );
1264    }
1265
1266    #[test]
1267    fn normalize_whitespace_trimmed() {
1268        assert_eq!(normalize_body_as_expr("  Ok(1)  "), "Ok(1)");
1269    }
1270
1271    // ----------------------------------------------------------------
1272    // pattern_matches tests
1273    // ----------------------------------------------------------------
1274
1275    #[test]
1276    fn pattern_matches_path() {
1277        let pat = PurePattern::Path("Status::Active".to_string());
1278        assert!(pattern_matches(&pat, "Status::Active"));
1279        assert!(!pattern_matches(&pat, "Status::Inactive"));
1280    }
1281
1282    #[test]
1283    fn pattern_matches_tuple_struct() {
1284        // TupleStruct stored as Struct with numeric field names
1285        let pat = PurePattern::Struct {
1286            path: "Message::Text".to_string(),
1287            fields: vec![(
1288                "0".to_string(),
1289                PurePattern::Ident {
1290                    name: "s".to_string(),
1291                    is_mut: false,
1292                    by_ref: false,
1293                },
1294            )],
1295            rest: false,
1296        };
1297        assert!(pattern_matches(&pat, "Message::Text(s)"));
1298        assert!(!pattern_matches(&pat, "Message::Text { s }"));
1299        assert!(!pattern_matches(&pat, "Message::Number(n)"));
1300    }
1301
1302    #[test]
1303    fn pattern_matches_tuple_struct_multi() {
1304        let pat = PurePattern::Struct {
1305            path: "Foo::Bar".to_string(),
1306            fields: vec![
1307                (
1308                    "0".to_string(),
1309                    PurePattern::Ident {
1310                        name: "a".to_string(),
1311                        is_mut: false,
1312                        by_ref: false,
1313                    },
1314                ),
1315                (
1316                    "1".to_string(),
1317                    PurePattern::Ident {
1318                        name: "b".to_string(),
1319                        is_mut: false,
1320                        by_ref: false,
1321                    },
1322                ),
1323            ],
1324            rest: false,
1325        };
1326        assert!(pattern_matches(&pat, "Foo::Bar(a, b)"));
1327    }
1328
1329    #[test]
1330    fn pattern_matches_tuple_struct_wildcard() {
1331        let pat = PurePattern::Struct {
1332            path: "Some".to_string(),
1333            fields: vec![("0".to_string(), PurePattern::Wild)],
1334            rest: false,
1335        };
1336        assert!(pattern_matches(&pat, "Some(_)"));
1337    }
1338
1339    #[test]
1340    fn pattern_matches_named_struct() {
1341        let pat = PurePattern::Struct {
1342            path: "Point".to_string(),
1343            fields: vec![
1344                (
1345                    "x".to_string(),
1346                    PurePattern::Ident {
1347                        name: "x".to_string(),
1348                        is_mut: false,
1349                        by_ref: false,
1350                    },
1351                ),
1352                (
1353                    "y".to_string(),
1354                    PurePattern::Ident {
1355                        name: "y".to_string(),
1356                        is_mut: false,
1357                        by_ref: false,
1358                    },
1359                ),
1360            ],
1361            rest: false,
1362        };
1363        assert!(pattern_matches(&pat, "Point { x, y }"));
1364        // NG-3: explicit field binding "x: x" should also match shorthand "x"
1365        assert!(
1366            pattern_matches(&pat, "Point { x: x, y: y }"),
1367            "Explicit field binding should match shorthand equivalent"
1368        );
1369    }
1370
1371    #[test]
1372    fn pattern_matches_wild() {
1373        assert!(pattern_matches(&PurePattern::Wild, "_"));
1374        assert!(!pattern_matches(&PurePattern::Wild, "x"));
1375    }
1376
1377    // ----------------------------------------------------------------
1378    // parse_pattern tests
1379    // ----------------------------------------------------------------
1380
1381    #[test]
1382    fn parse_pattern_path() {
1383        let pat = parse_pattern("Status::Active");
1384        assert!(matches!(pat, PurePattern::Path(p) if p == "Status::Active"));
1385    }
1386
1387    #[test]
1388    fn parse_pattern_tuple_struct() {
1389        let pat = parse_pattern("Message::Text(s)");
1390        if let PurePattern::Struct { path, fields, rest } = &pat {
1391            assert_eq!(path, "Message::Text");
1392            assert_eq!(fields.len(), 1);
1393            assert_eq!(fields[0].0, "0");
1394            assert!(matches!(&fields[0].1, PurePattern::Ident { name, .. } if name == "s"));
1395            assert!(!rest);
1396        } else {
1397            panic!("Expected Struct (TupleStruct), got {:?}", pat);
1398        }
1399    }
1400
1401    #[test]
1402    fn parse_pattern_tuple_struct_wild() {
1403        let pat = parse_pattern("Some(_)");
1404        if let PurePattern::Struct { path, fields, rest } = &pat {
1405            assert_eq!(path, "Some");
1406            assert_eq!(fields.len(), 1);
1407            assert_eq!(fields[0].0, "0");
1408            assert!(matches!(&fields[0].1, PurePattern::Wild));
1409            assert!(!rest);
1410        } else {
1411            panic!("Expected Struct (TupleStruct), got {:?}", pat);
1412        }
1413    }
1414
1415    #[test]
1416    fn parse_pattern_wildcard() {
1417        assert!(matches!(parse_pattern("_"), PurePattern::Wild));
1418    }
1419
1420    #[test]
1421    fn parse_pattern_struct() {
1422        let pat = parse_pattern("Point { x, y }");
1423        if let PurePattern::Struct { path, fields, rest } = &pat {
1424            assert_eq!(path, "Point");
1425            assert_eq!(fields.len(), 2);
1426            assert!(!rest);
1427        } else {
1428            panic!("Expected Struct, got {:?}", pat);
1429        }
1430    }
1431
1432    // ----------------------------------------------------------------
1433    // pattern_matches for PurePattern::Other
1434    // ----------------------------------------------------------------
1435
1436    #[test]
1437    fn pattern_matches_other_variant() {
1438        let pat = PurePattern::Other("Filter::Map(_)".to_string());
1439        assert!(pattern_matches(&pat, "Filter::Map(_)"));
1440        assert!(!pattern_matches(&pat, "Filter::Exclude(_)"));
1441    }
1442
1443    // ----------------------------------------------------------------
1444    // path_has_enum_segment tests
1445    // ----------------------------------------------------------------
1446
1447    #[test]
1448    fn path_segment_exact_match() {
1449        assert!(path_has_enum_segment("Filter::Recurse", "Filter"));
1450        assert!(path_has_enum_segment("Filter", "Filter"));
1451    }
1452
1453    #[test]
1454    fn path_segment_no_substring_match() {
1455        // "FilterKind" contains "Filter" as substring but is NOT an exact segment
1456        assert!(!path_has_enum_segment("FilterKind::Inclusive", "Filter"));
1457        assert!(!path_has_enum_segment("MyFilter::Recurse", "Filter"));
1458    }
1459
1460    #[test]
1461    fn path_segment_middle_match() {
1462        assert!(path_has_enum_segment("module::Filter::Recurse", "Filter"));
1463    }
1464
1465    // ----------------------------------------------------------------
1466    // pattern_contains_enum tests (with segment matching)
1467    // ----------------------------------------------------------------
1468
1469    #[test]
1470    fn pattern_contains_enum_path() {
1471        let pat = PurePattern::Path("Filter::Recurse".to_string());
1472        assert!(pattern_contains_enum(&pat, "Filter"));
1473        assert!(!pattern_contains_enum(&pat, "FilterKind"));
1474    }
1475
1476    #[test]
1477    fn pattern_contains_enum_struct() {
1478        let pat = PurePattern::Struct {
1479            path: "FilterKind::Inclusive".to_string(),
1480            fields: vec![],
1481            rest: false,
1482        };
1483        assert!(pattern_contains_enum(&pat, "FilterKind"));
1484        // "Filter" is NOT a segment of "FilterKind::Inclusive"
1485        assert!(!pattern_contains_enum(&pat, "Filter"));
1486    }
1487
1488    #[test]
1489    fn pattern_contains_enum_other() {
1490        let pat = PurePattern::Other("Filter::Map(_)".to_string());
1491        assert!(pattern_contains_enum(&pat, "Filter"));
1492        assert!(!pattern_contains_enum(&pat, "FilterKind"));
1493    }
1494
1495    // ----------------------------------------------------------------
1496    // normalize_field_shorthand tests
1497    // ----------------------------------------------------------------
1498
1499    #[test]
1500    fn normalize_shorthand_explicit_to_shorthand() {
1501        assert_eq!(
1502            normalize_pattern("Filter::Slice { start: start, end: end }"),
1503            "Filter::Slice { start, end }"
1504        );
1505    }
1506
1507    #[test]
1508    fn normalize_shorthand_already_short() {
1509        assert_eq!(
1510            normalize_pattern("Filter::Slice { start, end }"),
1511            "Filter::Slice { start, end }"
1512        );
1513    }
1514
1515    #[test]
1516    fn normalize_shorthand_different_binding_unchanged() {
1517        assert_eq!(
1518            normalize_pattern("Filter::Slice { start: s, end: e }"),
1519            "Filter::Slice { start: s, end: e }"
1520        );
1521    }
1522
1523    #[test]
1524    fn normalize_shorthand_mixed() {
1525        assert_eq!(
1526            normalize_pattern("Foo { a: a, b: other, c }"),
1527            "Foo { a, b: other, c }"
1528        );
1529    }
1530
1531    #[test]
1532    fn normalize_shorthand_with_rest() {
1533        assert_eq!(normalize_pattern("Foo { x: x, .. }"), "Foo { x, .. }");
1534    }
1535
1536    #[test]
1537    fn normalize_shorthand_no_braces() {
1538        assert_eq!(normalize_pattern("Some(x)"), "Some(x)");
1539    }
1540
1541    // ----------------------------------------------------------------
1542    // ReplaceMatchArm with struct pattern (NG-3)
1543    // ----------------------------------------------------------------
1544
1545    #[test]
1546    fn replace_match_arm_struct_pattern() {
1547        use crate::engine::ASTMutationEngine;
1548        use ryo_analysis::testing::ContextBuilder;
1549        use ryo_mutations::basic::ReplaceMatchArmMutation;
1550
1551        let mut ctx = ContextBuilder::new()
1552            .with_file(
1553                "src/lib.rs",
1554                r#"
1555enum Shape {
1556    Circle { radius: f64 },
1557    Rect { width: f64, height: f64 },
1558}
1559
1560fn area(s: &Shape) -> f64 {
1561    match s {
1562        Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
1563        Shape::Rect { width, height } => width * height,
1564    }
1565}
1566"#,
1567            )
1568            .build();
1569
1570        let area_id = ctx
1571            .registry
1572            .iter()
1573            .find(|(id, path)| {
1574                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1575                    && path.name() == "area"
1576            })
1577            .map(|(id, _)| id)
1578            .expect("area function not found");
1579
1580        // User specifies explicit field binding (NG-3 scenario)
1581        let mutation = ReplaceMatchArmMutation {
1582            function_id: area_id,
1583            enum_name: "Shape".to_string(),
1584            old_pattern: "Shape::Rect { width: width, height: height }".to_string(),
1585            new_pattern: "Shape::Rect { width, height }".to_string(),
1586            new_body: "width * height * 2.0".to_string(),
1587        };
1588
1589        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1590        assert_eq!(
1591            result.result.changes, 1,
1592            "Struct pattern with explicit binding should match: {}",
1593            result.result.description
1594        );
1595    }
1596}