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                        }
955                    };
956                    fields.push((name, pat));
957                } else {
958                    // Shorthand: `field` means `field: field`
959                    let name = field_part.to_string();
960                    fields.push((
961                        name.clone(),
962                        PurePattern::Ident {
963                            name,
964                            is_mut: false,
965                        },
966                    ));
967                }
968            }
969
970            return PurePattern::Struct { path, fields, rest };
971        }
972    }
973
974    // Check for TupleStruct pattern: "Path::Variant(a, b)" or "Some(x)"
975    // Must have '(' NOT at position 0 (that's a bare tuple) and ')' at end
976    if let Some(paren_start) = pattern.find('(') {
977        if paren_start > 0 && pattern.ends_with(')') {
978            let path = pattern[..paren_start].trim().to_string();
979            let inner = &pattern[paren_start + 1..pattern.len() - 1];
980            let fields: Vec<(String, PurePattern)> = inner
981                .split(',')
982                .enumerate()
983                .filter_map(|(i, s)| {
984                    let s = s.trim();
985                    if s.is_empty() {
986                        return None;
987                    }
988                    let pat = if s == "_" {
989                        PurePattern::Wild
990                    } else {
991                        PurePattern::Ident {
992                            name: s.to_string(),
993                            is_mut: false,
994                        }
995                    };
996                    Some((i.to_string(), pat))
997                })
998                .collect();
999            return PurePattern::Struct {
1000                path,
1001                fields,
1002                rest: false,
1003            };
1004        }
1005    }
1006
1007    // Check for tuple pattern: "(a, b)"
1008    if pattern.starts_with('(') && pattern.ends_with(')') {
1009        let inner = &pattern[1..pattern.len() - 1];
1010        let parts: Vec<_> = inner.split(',').map(|s| s.trim()).collect();
1011        if parts.len() > 1 || !inner.is_empty() {
1012            let patterns: Vec<_> = parts
1013                .iter()
1014                .map(|&s| {
1015                    if s == "_" {
1016                        PurePattern::Wild
1017                    } else {
1018                        PurePattern::Ident {
1019                            name: s.to_string(),
1020                            is_mut: false,
1021                        }
1022                    }
1023                })
1024                .collect();
1025            return PurePattern::Tuple(patterns);
1026        }
1027    }
1028
1029    // Check for wildcard
1030    if pattern == "_" {
1031        return PurePattern::Wild;
1032    }
1033
1034    // Default to path pattern
1035    PurePattern::Path(pattern.to_string())
1036}
1037
1038/// Parse a body string into PureExpr
1039fn parse_body(body: &str) -> PureExpr {
1040    let body_str = body.trim();
1041
1042    // Parse macro calls like "todo!()"
1043    if let Some(bang_pos) = body_str.find('!') {
1044        let name = body_str[..bang_pos].trim();
1045        let rest = body_str[bang_pos + 1..].trim();
1046
1047        // Validate macro pattern
1048        let is_valid_ident = !name.is_empty()
1049            && name.chars().all(|c| c.is_alphanumeric() || c == '_')
1050            && name
1051                .chars()
1052                .next()
1053                .map(|c| c.is_alphabetic() || c == '_')
1054                .unwrap_or(false);
1055        let has_delimiter = rest.starts_with('(') || rest.starts_with('{') || rest.starts_with('[');
1056
1057        if is_valid_ident && has_delimiter {
1058            let (delimiter, tokens) = if rest.starts_with('(') && rest.ends_with(')') {
1059                (MacroDelimiter::Paren, rest[1..rest.len() - 1].to_string())
1060            } else if rest.starts_with('{') && rest.ends_with('}') {
1061                (MacroDelimiter::Brace, rest[1..rest.len() - 1].to_string())
1062            } else if rest.starts_with('[') && rest.ends_with(']') {
1063                (MacroDelimiter::Bracket, rest[1..rest.len() - 1].to_string())
1064            } else {
1065                (MacroDelimiter::Paren, String::new())
1066            };
1067            return PureExpr::Macro {
1068                name: name.to_string(),
1069                delimiter,
1070                tokens,
1071            };
1072        }
1073    }
1074
1075    // Default to Other
1076    PureExpr::Other(body.to_string())
1077}
1078
1079fn walk_expr_children_and_replace_arm(
1080    expr: &mut PureExpr,
1081    enum_name: &str,
1082    old_pattern: &str,
1083    new_pattern: &str,
1084    new_body: &str,
1085) -> bool {
1086    match expr {
1087        PureExpr::Block { block, .. } => {
1088            walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
1089        }
1090        PureExpr::If {
1091            cond,
1092            then_branch,
1093            else_branch,
1094        } => {
1095            if walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body) {
1096                return true;
1097            }
1098            if walk_and_replace_arm(then_branch, enum_name, old_pattern, new_pattern, new_body) {
1099                return true;
1100            }
1101            if let Some(else_expr) = else_branch {
1102                if walk_expr_and_replace_arm(
1103                    else_expr,
1104                    enum_name,
1105                    old_pattern,
1106                    new_pattern,
1107                    new_body,
1108                ) {
1109                    return true;
1110                }
1111            }
1112            false
1113        }
1114        PureExpr::Match { expr: e, arms } => {
1115            if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1116                return true;
1117            }
1118            for arm in arms {
1119                if walk_expr_and_replace_arm(
1120                    &mut arm.body,
1121                    enum_name,
1122                    old_pattern,
1123                    new_pattern,
1124                    new_body,
1125                ) {
1126                    return true;
1127                }
1128            }
1129            false
1130        }
1131        PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
1132            walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
1133        }
1134        PureExpr::While { cond, body, .. } => {
1135            walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body)
1136                || walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1137        }
1138        PureExpr::For { expr: e, body, .. } => {
1139            walk_expr_and_replace_arm(e, 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::Async { body, .. } => {
1143            walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1144        }
1145        PureExpr::Closure { body, .. } => {
1146            walk_expr_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1147        }
1148        PureExpr::Call { func, args } => {
1149            if walk_expr_and_replace_arm(func, enum_name, old_pattern, new_pattern, new_body) {
1150                return true;
1151            }
1152            for arg in args {
1153                if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
1154                    return true;
1155                }
1156            }
1157            false
1158        }
1159        PureExpr::MethodCall { receiver, args, .. } => {
1160            if walk_expr_and_replace_arm(receiver, enum_name, old_pattern, new_pattern, new_body) {
1161                return true;
1162            }
1163            for arg in args {
1164                if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
1165                    return true;
1166                }
1167            }
1168            false
1169        }
1170        PureExpr::Binary { left, right, .. } => {
1171            walk_expr_and_replace_arm(left, enum_name, old_pattern, new_pattern, new_body)
1172                || walk_expr_and_replace_arm(right, enum_name, old_pattern, new_pattern, new_body)
1173        }
1174        PureExpr::Unary { expr: e, .. }
1175        | PureExpr::Field { expr: e, .. }
1176        | PureExpr::Await(e)
1177        | PureExpr::Try(e) => {
1178            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1179        }
1180        PureExpr::Index { expr: e, index } => {
1181            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1182                || walk_expr_and_replace_arm(index, enum_name, old_pattern, new_pattern, new_body)
1183        }
1184        PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1185            for e in exprs {
1186                if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1187                    return true;
1188                }
1189            }
1190            false
1191        }
1192        PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
1193            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1194        }
1195        PureExpr::Let { expr: e, .. }
1196        | PureExpr::Cast { expr: e, .. }
1197        | PureExpr::Ref { expr: e, .. } => {
1198            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1199        }
1200        PureExpr::Range { start, end, .. } => {
1201            if let Some(s) = start {
1202                if walk_expr_and_replace_arm(s, enum_name, old_pattern, new_pattern, new_body) {
1203                    return true;
1204                }
1205            }
1206            if let Some(e) = end {
1207                if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1208                    return true;
1209                }
1210            }
1211            false
1212        }
1213        PureExpr::Struct { fields, .. } => {
1214            for (_, e) in fields {
1215                if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1216                    return true;
1217                }
1218            }
1219            false
1220        }
1221        PureExpr::Repeat { expr: e, len } => {
1222            walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1223                || walk_expr_and_replace_arm(len, enum_name, old_pattern, new_pattern, new_body)
1224        }
1225        _ => false,
1226    }
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231    use super::*;
1232
1233    #[test]
1234    fn normalize_simple_expr_unchanged() {
1235        assert_eq!(normalize_body_as_expr("Ok(42)"), "Ok(42)");
1236    }
1237
1238    #[test]
1239    fn normalize_block_expr_unchanged() {
1240        assert_eq!(
1241            normalize_body_as_expr("{ let v = 1; v + 1 }"),
1242            "{ let v = 1; v + 1 }"
1243        );
1244    }
1245
1246    #[test]
1247    fn normalize_stmt_sequence_wrapped() {
1248        assert_eq!(
1249            normalize_body_as_expr("let v = x; if v { a } else { b }"),
1250            "{ let v = x; if v { a } else { b } }"
1251        );
1252    }
1253
1254    #[test]
1255    fn normalize_let_match_wrapped() {
1256        assert_eq!(
1257            normalize_body_as_expr("let v = get(); match v { A => 1, _ => 2 }"),
1258            "{ let v = get(); match v { A => 1, _ => 2 } }"
1259        );
1260    }
1261
1262    #[test]
1263    fn normalize_whitespace_trimmed() {
1264        assert_eq!(normalize_body_as_expr("  Ok(1)  "), "Ok(1)");
1265    }
1266
1267    // ----------------------------------------------------------------
1268    // pattern_matches tests
1269    // ----------------------------------------------------------------
1270
1271    #[test]
1272    fn pattern_matches_path() {
1273        let pat = PurePattern::Path("Status::Active".to_string());
1274        assert!(pattern_matches(&pat, "Status::Active"));
1275        assert!(!pattern_matches(&pat, "Status::Inactive"));
1276    }
1277
1278    #[test]
1279    fn pattern_matches_tuple_struct() {
1280        // TupleStruct stored as Struct with numeric field names
1281        let pat = PurePattern::Struct {
1282            path: "Message::Text".to_string(),
1283            fields: vec![(
1284                "0".to_string(),
1285                PurePattern::Ident {
1286                    name: "s".to_string(),
1287                    is_mut: false,
1288                },
1289            )],
1290            rest: false,
1291        };
1292        assert!(pattern_matches(&pat, "Message::Text(s)"));
1293        assert!(!pattern_matches(&pat, "Message::Text { s }"));
1294        assert!(!pattern_matches(&pat, "Message::Number(n)"));
1295    }
1296
1297    #[test]
1298    fn pattern_matches_tuple_struct_multi() {
1299        let pat = PurePattern::Struct {
1300            path: "Foo::Bar".to_string(),
1301            fields: vec![
1302                (
1303                    "0".to_string(),
1304                    PurePattern::Ident {
1305                        name: "a".to_string(),
1306                        is_mut: false,
1307                    },
1308                ),
1309                (
1310                    "1".to_string(),
1311                    PurePattern::Ident {
1312                        name: "b".to_string(),
1313                        is_mut: false,
1314                    },
1315                ),
1316            ],
1317            rest: false,
1318        };
1319        assert!(pattern_matches(&pat, "Foo::Bar(a, b)"));
1320    }
1321
1322    #[test]
1323    fn pattern_matches_tuple_struct_wildcard() {
1324        let pat = PurePattern::Struct {
1325            path: "Some".to_string(),
1326            fields: vec![("0".to_string(), PurePattern::Wild)],
1327            rest: false,
1328        };
1329        assert!(pattern_matches(&pat, "Some(_)"));
1330    }
1331
1332    #[test]
1333    fn pattern_matches_named_struct() {
1334        let pat = PurePattern::Struct {
1335            path: "Point".to_string(),
1336            fields: vec![
1337                (
1338                    "x".to_string(),
1339                    PurePattern::Ident {
1340                        name: "x".to_string(),
1341                        is_mut: false,
1342                    },
1343                ),
1344                (
1345                    "y".to_string(),
1346                    PurePattern::Ident {
1347                        name: "y".to_string(),
1348                        is_mut: false,
1349                    },
1350                ),
1351            ],
1352            rest: false,
1353        };
1354        assert!(pattern_matches(&pat, "Point { x, y }"));
1355        // NG-3: explicit field binding "x: x" should also match shorthand "x"
1356        assert!(
1357            pattern_matches(&pat, "Point { x: x, y: y }"),
1358            "Explicit field binding should match shorthand equivalent"
1359        );
1360    }
1361
1362    #[test]
1363    fn pattern_matches_wild() {
1364        assert!(pattern_matches(&PurePattern::Wild, "_"));
1365        assert!(!pattern_matches(&PurePattern::Wild, "x"));
1366    }
1367
1368    // ----------------------------------------------------------------
1369    // parse_pattern tests
1370    // ----------------------------------------------------------------
1371
1372    #[test]
1373    fn parse_pattern_path() {
1374        let pat = parse_pattern("Status::Active");
1375        assert!(matches!(pat, PurePattern::Path(p) if p == "Status::Active"));
1376    }
1377
1378    #[test]
1379    fn parse_pattern_tuple_struct() {
1380        let pat = parse_pattern("Message::Text(s)");
1381        if let PurePattern::Struct { path, fields, rest } = &pat {
1382            assert_eq!(path, "Message::Text");
1383            assert_eq!(fields.len(), 1);
1384            assert_eq!(fields[0].0, "0");
1385            assert!(matches!(&fields[0].1, PurePattern::Ident { name, .. } if name == "s"));
1386            assert!(!rest);
1387        } else {
1388            panic!("Expected Struct (TupleStruct), got {:?}", pat);
1389        }
1390    }
1391
1392    #[test]
1393    fn parse_pattern_tuple_struct_wild() {
1394        let pat = parse_pattern("Some(_)");
1395        if let PurePattern::Struct { path, fields, rest } = &pat {
1396            assert_eq!(path, "Some");
1397            assert_eq!(fields.len(), 1);
1398            assert_eq!(fields[0].0, "0");
1399            assert!(matches!(&fields[0].1, PurePattern::Wild));
1400            assert!(!rest);
1401        } else {
1402            panic!("Expected Struct (TupleStruct), got {:?}", pat);
1403        }
1404    }
1405
1406    #[test]
1407    fn parse_pattern_wildcard() {
1408        assert!(matches!(parse_pattern("_"), PurePattern::Wild));
1409    }
1410
1411    #[test]
1412    fn parse_pattern_struct() {
1413        let pat = parse_pattern("Point { x, y }");
1414        if let PurePattern::Struct { path, fields, rest } = &pat {
1415            assert_eq!(path, "Point");
1416            assert_eq!(fields.len(), 2);
1417            assert!(!rest);
1418        } else {
1419            panic!("Expected Struct, got {:?}", pat);
1420        }
1421    }
1422
1423    // ----------------------------------------------------------------
1424    // pattern_matches for PurePattern::Other
1425    // ----------------------------------------------------------------
1426
1427    #[test]
1428    fn pattern_matches_other_variant() {
1429        let pat = PurePattern::Other("Filter::Map(_)".to_string());
1430        assert!(pattern_matches(&pat, "Filter::Map(_)"));
1431        assert!(!pattern_matches(&pat, "Filter::Exclude(_)"));
1432    }
1433
1434    // ----------------------------------------------------------------
1435    // path_has_enum_segment tests
1436    // ----------------------------------------------------------------
1437
1438    #[test]
1439    fn path_segment_exact_match() {
1440        assert!(path_has_enum_segment("Filter::Recurse", "Filter"));
1441        assert!(path_has_enum_segment("Filter", "Filter"));
1442    }
1443
1444    #[test]
1445    fn path_segment_no_substring_match() {
1446        // "FilterKind" contains "Filter" as substring but is NOT an exact segment
1447        assert!(!path_has_enum_segment("FilterKind::Inclusive", "Filter"));
1448        assert!(!path_has_enum_segment("MyFilter::Recurse", "Filter"));
1449    }
1450
1451    #[test]
1452    fn path_segment_middle_match() {
1453        assert!(path_has_enum_segment("module::Filter::Recurse", "Filter"));
1454    }
1455
1456    // ----------------------------------------------------------------
1457    // pattern_contains_enum tests (with segment matching)
1458    // ----------------------------------------------------------------
1459
1460    #[test]
1461    fn pattern_contains_enum_path() {
1462        let pat = PurePattern::Path("Filter::Recurse".to_string());
1463        assert!(pattern_contains_enum(&pat, "Filter"));
1464        assert!(!pattern_contains_enum(&pat, "FilterKind"));
1465    }
1466
1467    #[test]
1468    fn pattern_contains_enum_struct() {
1469        let pat = PurePattern::Struct {
1470            path: "FilterKind::Inclusive".to_string(),
1471            fields: vec![],
1472            rest: false,
1473        };
1474        assert!(pattern_contains_enum(&pat, "FilterKind"));
1475        // "Filter" is NOT a segment of "FilterKind::Inclusive"
1476        assert!(!pattern_contains_enum(&pat, "Filter"));
1477    }
1478
1479    #[test]
1480    fn pattern_contains_enum_other() {
1481        let pat = PurePattern::Other("Filter::Map(_)".to_string());
1482        assert!(pattern_contains_enum(&pat, "Filter"));
1483        assert!(!pattern_contains_enum(&pat, "FilterKind"));
1484    }
1485
1486    // ----------------------------------------------------------------
1487    // normalize_field_shorthand tests
1488    // ----------------------------------------------------------------
1489
1490    #[test]
1491    fn normalize_shorthand_explicit_to_shorthand() {
1492        assert_eq!(
1493            normalize_pattern("Filter::Slice { start: start, end: end }"),
1494            "Filter::Slice { start, end }"
1495        );
1496    }
1497
1498    #[test]
1499    fn normalize_shorthand_already_short() {
1500        assert_eq!(
1501            normalize_pattern("Filter::Slice { start, end }"),
1502            "Filter::Slice { start, end }"
1503        );
1504    }
1505
1506    #[test]
1507    fn normalize_shorthand_different_binding_unchanged() {
1508        assert_eq!(
1509            normalize_pattern("Filter::Slice { start: s, end: e }"),
1510            "Filter::Slice { start: s, end: e }"
1511        );
1512    }
1513
1514    #[test]
1515    fn normalize_shorthand_mixed() {
1516        assert_eq!(
1517            normalize_pattern("Foo { a: a, b: other, c }"),
1518            "Foo { a, b: other, c }"
1519        );
1520    }
1521
1522    #[test]
1523    fn normalize_shorthand_with_rest() {
1524        assert_eq!(normalize_pattern("Foo { x: x, .. }"), "Foo { x, .. }");
1525    }
1526
1527    #[test]
1528    fn normalize_shorthand_no_braces() {
1529        assert_eq!(normalize_pattern("Some(x)"), "Some(x)");
1530    }
1531
1532    // ----------------------------------------------------------------
1533    // ReplaceMatchArm with struct pattern (NG-3)
1534    // ----------------------------------------------------------------
1535
1536    #[test]
1537    fn replace_match_arm_struct_pattern() {
1538        use crate::engine::ASTMutationEngine;
1539        use ryo_analysis::testing::ContextBuilder;
1540        use ryo_mutations::basic::ReplaceMatchArmMutation;
1541
1542        let mut ctx = ContextBuilder::new()
1543            .with_file(
1544                "src/lib.rs",
1545                r#"
1546enum Shape {
1547    Circle { radius: f64 },
1548    Rect { width: f64, height: f64 },
1549}
1550
1551fn area(s: &Shape) -> f64 {
1552    match s {
1553        Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
1554        Shape::Rect { width, height } => width * height,
1555    }
1556}
1557"#,
1558            )
1559            .build();
1560
1561        let area_id = ctx
1562            .registry
1563            .iter()
1564            .find(|(id, path)| {
1565                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1566                    && path.name() == "area"
1567            })
1568            .map(|(id, _)| id)
1569            .expect("area function not found");
1570
1571        // User specifies explicit field binding (NG-3 scenario)
1572        let mutation = ReplaceMatchArmMutation {
1573            function_id: area_id,
1574            enum_name: "Shape".to_string(),
1575            old_pattern: "Shape::Rect { width: width, height: height }".to_string(),
1576            new_pattern: "Shape::Rect { width, height }".to_string(),
1577            new_body: "width * height * 2.0".to_string(),
1578        };
1579
1580        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1581        assert_eq!(
1582            result.result.changes, 1,
1583            "Struct pattern with explicit binding should match: {}",
1584            result.result.description
1585        );
1586    }
1587}