Skip to main content

ryo_executor/engine/impls/
method.rs

1//! ASTRegApply implementations for AddMethodMutation and RemoveMethodMutation
2//!
3//! New design: Methods are registered directly on types (Struct/Enum) as Type::method.
4//! Impl blocks are file-level constructs added to module_items for code generation.
5
6use ryo_mutations::{AddMethodMutation, MutationResult, RemoveMethodMutation};
7use ryo_source::pure::{
8    PureBlock, PureExpr, PureFn, PureGenerics, PureImpl, PureImplItem, PureItem, PureParam,
9    PureStmt, PureType, PureVis,
10};
11use ryo_symbol::SymbolKind;
12
13use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
14
15// ============================================================================
16// Helper functions
17// ============================================================================
18
19fn parse_type_simple(s: &str) -> PureType {
20    let s = s.trim();
21
22    // Handle reference types: &str, &mut T, &'a T, &[T]
23    if let Some(rest) = s.strip_prefix('&') {
24        let rest = rest.trim_start();
25
26        // Check for lifetime: &'a T
27        if rest.starts_with('\'') {
28            // Find end of lifetime
29            let lifetime_end = rest.find(|c: char| c.is_whitespace()).unwrap_or(rest.len());
30            let lifetime = rest[..lifetime_end].to_string();
31            let rest = rest[lifetime_end..].trim_start();
32
33            // Check for mut after lifetime
34            if let Some(inner) = rest.strip_prefix("mut ") {
35                return PureType::Ref {
36                    lifetime: Some(lifetime),
37                    is_mut: true,
38                    ty: Box::new(parse_type_simple(inner)),
39                };
40            } else {
41                return PureType::Ref {
42                    lifetime: Some(lifetime),
43                    is_mut: false,
44                    ty: Box::new(parse_type_simple(rest)),
45                };
46            }
47        }
48
49        // Check for mut: &mut T
50        if let Some(inner) = rest.strip_prefix("mut ") {
51            return PureType::Ref {
52                lifetime: None,
53                is_mut: true,
54                ty: Box::new(parse_type_simple(inner)),
55            };
56        }
57
58        // Simple reference: &T (including &[T])
59        return PureType::Ref {
60            lifetime: None,
61            is_mut: false,
62            ty: Box::new(parse_type_simple(rest)),
63        };
64    }
65
66    // Handle slice types: [T]
67    if s.starts_with('[') && s.ends_with(']') {
68        let inner = &s[1..s.len() - 1];
69        return PureType::Slice(Box::new(parse_type_simple(inner)));
70    }
71
72    // Handle tuple types: (), (A,), (A, B), etc.
73    if s.starts_with('(') && s.ends_with(')') {
74        let inner = s[1..s.len() - 1].trim();
75        if inner.is_empty() {
76            // Unit type ()
77            return PureType::Tuple(vec![]);
78        }
79        // Parse tuple elements (simple split by comma - may not handle nested types correctly)
80        let elements: Vec<PureType> = inner
81            .split(',')
82            .map(|e| parse_type_simple(e.trim()))
83            .collect();
84        return PureType::Tuple(elements);
85    }
86
87    // Handle Option<T>, Result<T, E>, Vec<T> etc. - keep as path for now
88    // (These are valid path types)
89
90    // Default: treat as path
91    PureType::Path(s.to_string())
92}
93
94// ============================================================================
95// ASTRegApply implementations
96// ============================================================================
97
98/// New design: Add method directly to Struct/Enum
99///
100/// Flow:
101/// 1. Register method as Type::method in SymbolRegistry
102/// 2. Store method AST in ASTRegistry
103/// 3. Add/update plain impl block in parent module's module_items
104impl ASTRegApply for AddMethodMutation {
105    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
106        // Verify type exists and is Struct/Enum
107        let type_kind = ctx.symbol_registry.kind(self.type_id);
108        if !matches!(type_kind, Some(SymbolKind::Struct | SymbolKind::Enum)) {
109            return MutationResult {
110                mutation_type: "AddMethod".to_string(),
111                changes: 0,
112                description: format!(
113                    "Symbol {} is not a struct or enum (kind: {:?})",
114                    self.type_id, type_kind
115                ),
116            };
117        }
118
119        // Get type path
120        let type_path = match ctx.symbol_registry.path(self.type_id) {
121            Some(path) => path.clone(),
122            None => {
123                return MutationResult {
124                    mutation_type: "AddMethod".to_string(),
125                    changes: 0,
126                    description: format!("Type {} not found in registry", self.type_id),
127                };
128            }
129        };
130
131        // Build method path: Type::method
132        let method_path = match type_path.child(&self.name) {
133            Ok(path) => path,
134            Err(_) => {
135                return MutationResult {
136                    mutation_type: "AddMethod".to_string(),
137                    changes: 0,
138                    description: format!("Failed to create method path for '{}'", self.name),
139                };
140            }
141        };
142
143        // Check if method already exists
144        if ctx.symbol_registry.lookup(&method_path).is_some() {
145            return MutationResult {
146                mutation_type: "AddMethod".to_string(),
147                changes: 0,
148                description: format!(
149                    "Method '{}' already exists on type {}",
150                    self.name, type_path
151                ),
152            };
153        }
154
155        // Build parameters
156        let mut fn_params = Vec::new();
157
158        // Add self parameter if specified
159        if let Some((is_ref, is_mut)) = self.takes_self {
160            fn_params.push(PureParam::SelfValue { is_ref, is_mut });
161        }
162
163        // Add typed parameters
164        for (name, ty) in &self.params {
165            fn_params.push(PureParam::Typed {
166                name: name.clone(),
167                ty: parse_type_simple(ty),
168            });
169        }
170
171        // Build return type
172        let ret = self.return_type.as_ref().map(|ty| parse_type_simple(ty));
173
174        // Create body block
175        let body_wrapped = if self.body.trim().starts_with('{') {
176            self.body.clone()
177        } else {
178            format!("{{ {} }}", self.body)
179        };
180
181        let body_block = PureBlock {
182            stmts: vec![PureStmt::Expr(PureExpr::Other(body_wrapped))],
183        };
184
185        // Create the method
186        let method_fn = PureFn {
187            attrs: Vec::new(),
188            vis: if self.is_pub {
189                PureVis::Public
190            } else {
191                PureVis::Private
192            },
193            is_async: false,
194            is_async_inferred: false,
195            is_const: false,
196            is_unsafe: false,
197            name: self.name.clone(),
198            generics: PureGenerics::default(),
199            params: fn_params,
200            ret,
201            body: body_block,
202            abi: None,
203        };
204
205        // Register method in SymbolRegistry + ASTRegistry
206        let method_id = match ctx.register_with_ast(
207            method_path.clone(),
208            SymbolKind::Method,
209            PureItem::Fn(method_fn.clone()),
210        ) {
211            Some(id) => id,
212            None => {
213                return MutationResult {
214                    mutation_type: "AddMethod".to_string(),
215                    changes: 0,
216                    description: format!("Failed to register method '{}'", method_path),
217                };
218            }
219        };
220
221        // Add/update plain impl block in parent module's module_items
222        let type_name = type_path.name().to_string();
223
224        // Get type's generics from its definition
225        let type_generics = ctx
226            .ast_registry
227            .get(self.type_id)
228            .and_then(|item| match item {
229                PureItem::Struct(s) => Some(s.generics.clone()),
230                PureItem::Enum(e) => Some(e.generics.clone()),
231                _ => None,
232            })
233            .unwrap_or_default();
234
235        // Build self_ty with generics for the impl block (e.g., "Foo<T>" instead of "Foo")
236        let self_ty_with_generics = if type_generics.params.is_empty() {
237            type_name.clone()
238        } else {
239            use ryo_source::pure::PureGenericParam;
240            let param_names: Vec<String> = type_generics
241                .params
242                .iter()
243                .map(|p| match p {
244                    PureGenericParam::Type { name, .. } => name.clone(),
245                    PureGenericParam::Lifetime { name, .. } => name.clone(),
246                    PureGenericParam::Const { name, .. } => name.clone(),
247                })
248                .collect();
249            format!("{}<{}>", type_name, param_names.join(", "))
250        };
251
252        if let Some(parent_path) = type_path.parent() {
253            if let Some(parent_id) = ctx.symbol_registry.lookup(&parent_path) {
254                let mut module_items = ctx
255                    .ast_registry
256                    .get_module_items(parent_id)
257                    .cloned()
258                    .unwrap_or_default();
259
260                // Find or create plain impl block for this type
261                // Match by base type name (without generics) for existing impl blocks
262                let impl_block_index = module_items.iter().position(|item| {
263                    if let PureItem::Impl(impl_block) = item {
264                        // Match base type name (e.g., "Foo" matches "Foo<T>")
265                        let base_self_ty = impl_block
266                            .self_ty
267                            .split('<')
268                            .next()
269                            .unwrap_or(&impl_block.self_ty);
270                        base_self_ty == type_name && impl_block.trait_.is_none()
271                    } else {
272                        false
273                    }
274                });
275
276                if let Some(idx) = impl_block_index {
277                    // Add method to existing impl block
278                    if let PureItem::Impl(impl_block) = &mut module_items[idx] {
279                        impl_block.items.push(PureImplItem::Fn(method_fn));
280                    }
281                } else {
282                    // Create new plain impl block with type's generics
283                    let new_impl = PureImpl {
284                        attrs: vec![],
285                        generics: type_generics,
286                        is_unsafe: false,
287                        trait_: None,
288                        self_ty: self_ty_with_generics,
289                        items: vec![PureImplItem::Fn(method_fn)],
290                    };
291                    module_items.push(PureItem::Impl(new_impl));
292                }
293
294                ctx.ast_registry.set_module_items(parent_id, module_items);
295            }
296        }
297
298        // Emit event
299        ctx.emit_modified(method_id, ModificationType::MethodAdded(self.name.clone()));
300
301        MutationResult {
302            mutation_type: "AddMethod".to_string(),
303            changes: 1,
304            description: format!("Added method '{}' to type {}", self.name, type_path),
305        }
306    }
307}
308
309/// New design: Remove method directly from Struct/Enum
310impl ASTRegApply for RemoveMethodMutation {
311    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
312        // Verify method exists
313        if ctx.symbol_registry.kind(self.method_id) != Some(SymbolKind::Method) {
314            return MutationResult {
315                mutation_type: "RemoveMethod".to_string(),
316                changes: 0,
317                description: format!("Symbol {} is not a method", self.method_id),
318            };
319        }
320
321        // Get method path
322        let method_path = match ctx.symbol_registry.path(self.method_id) {
323            Some(path) => path.clone(),
324            None => {
325                return MutationResult {
326                    mutation_type: "RemoveMethod".to_string(),
327                    changes: 0,
328                    description: format!("Method {} not found", self.method_id),
329                };
330            }
331        };
332
333        let method_name = method_path.name().to_string();
334
335        // Remove from impl block in module_items AND ASTRegistry
336        // Handles both plain impl (Type::method) and trait impl (<impl Trait for Type>::method)
337        if let Some(type_path) = method_path.parent() {
338            let type_name = type_path.name().to_string();
339
340            // Determine if this is a trait impl or plain impl
341            let is_trait_impl = type_name.starts_with("<impl ");
342
343            let (impl_path, type_name_for_match) = if is_trait_impl {
344                // Trait impl: type_path IS the impl path (<impl Trait for Type>)
345                // Extract type name from "<impl Trait for Type>"
346                let type_name_extracted = if let Some(for_pos) = type_name.find(" for ") {
347                    let after_for = &type_name[for_pos + 5..];
348                    after_for.trim_end_matches('>').trim().to_string()
349                } else {
350                    type_name.clone()
351                };
352                (Some(type_path.clone()), type_name_extracted)
353            } else {
354                // Plain impl: construct impl path <impl Type>
355                if let Some(parent_path) = type_path.parent() {
356                    let impl_name = format!("<impl {}>", type_name);
357                    let impl_path = parent_path.child(&impl_name).ok();
358                    (impl_path, type_name)
359                } else {
360                    (None, type_name)
361                }
362            };
363
364            // Get parent module
365            // Both plain impl and trait impl: parent is type_path.parent()
366            // For plain impl: Type::method -> Type -> module
367            // For trait impl: <impl Trait for Type>::method -> <impl Trait for Type> -> module
368            let parent_id = type_path
369                .parent()
370                .and_then(|p| ctx.symbol_registry.lookup(&p));
371
372            // Update module_items
373            if let Some(parent_id) = parent_id {
374                if let Some(module_items) = ctx.ast_registry.get_module_items_mut(parent_id) {
375                    for item in module_items.iter_mut() {
376                        if let PureItem::Impl(impl_block) = item {
377                            // Match by self_ty and trait status
378                            let matches = if is_trait_impl {
379                                impl_block.trait_.is_some()
380                                    && impl_block.self_ty == type_name_for_match
381                            } else {
382                                impl_block.trait_.is_none()
383                                    && impl_block.self_ty == type_name_for_match
384                            };
385
386                            if matches {
387                                impl_block.items.retain(|impl_item| {
388                                    if let PureImplItem::Fn(f) = impl_item {
389                                        f.name != method_name
390                                    } else {
391                                        true
392                                    }
393                                });
394                            }
395                        }
396                    }
397                }
398            }
399
400            // Update ASTRegistry impl block
401            if let Some(impl_path) = impl_path {
402                if let Some(impl_id) = ctx.symbol_registry.lookup(&impl_path) {
403                    if let Some(PureItem::Impl(impl_block)) = ctx.ast_registry.get_mut(impl_id) {
404                        impl_block.items.retain(|impl_item| {
405                            if let PureImplItem::Fn(f) = impl_item {
406                                f.name != method_name
407                            } else {
408                                true
409                            }
410                        });
411                    }
412                }
413            }
414        }
415
416        // Remove from SymbolRegistry and ASTRegistry manually
417        // (can't use ctx.remove_symbol because it won't remove methods from impl blocks)
418        ctx.symbol_registry.remove(self.method_id);
419        ctx.ast_registry.remove(self.method_id);
420        ctx.emit_removed(method_path.clone());
421
422        MutationResult {
423            mutation_type: "RemoveMethod".to_string(),
424            changes: 1,
425            description: format!("Removed method '{}'", method_path),
426        }
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use crate::engine::{multi_file_dumper, ASTMutationEngine};
434    use ryo_analysis::testing::ContextBuilder;
435    use ryo_symbol::WorkspaceFilePath;
436
437    // =========================================================================
438    // TDD: New design tests - Methods registered directly on type
439    // =========================================================================
440
441    /// Test AddMethodMutation with new design:
442    /// - Methods are registered as Type::method
443    /// - Impl block is added to module_items for file generation
444    #[test]
445    fn test_add_method_to_struct_new_design() {
446        // Setup: Struct without impl block
447        let mut ctx = ContextBuilder::new()
448            .with_file(
449                "src/lib.rs",
450                r#"pub mod user;
451"#,
452            )
453            .with_file(
454                "src/user.rs",
455                r#"pub struct User {
456    pub name: String,
457}
458"#,
459            )
460            .build();
461
462        // Find the User struct
463        let user_path = ryo_symbol::SymbolPath::parse("test_crate::user::User").unwrap();
464        let user_id = ctx.registry.lookup(&user_path).expect("User not found");
465
466        // Add method "new" to User
467        let mutation = AddMethodMutation::new(user_id, "new")
468            .public()
469            .with_params(vec![("name".to_string(), "String".to_string())])
470            .with_return_type("Self")
471            .with_body("Self { name }");
472
473        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
474        assert!(result.has_changes(), "Expected changes");
475
476        // Verify 1: Method registered as Type::method
477        let method_path = ryo_symbol::SymbolPath::parse("test_crate::user::User::new").unwrap();
478        let method_id = ctx
479            .registry
480            .lookup(&method_path)
481            .expect("Method should be registered");
482        assert_eq!(
483            ctx.registry.kind(method_id),
484            Some(SymbolKind::Method),
485            "Method should have SymbolKind::Method"
486        );
487
488        // Verify 2: Method AST exists
489        let method_ast = ctx.ast_registry.get(method_id);
490        assert!(method_ast.is_some(), "Method AST should exist");
491        assert!(
492            matches!(method_ast, Some(PureItem::Fn(_))),
493            "Method AST should be PureItem::Fn"
494        );
495
496        // Verify 3: Impl block added to module_items
497        let user_module_path = ryo_symbol::SymbolPath::parse("test_crate::user").unwrap();
498        let user_module_id = ctx
499            .registry
500            .lookup(&user_module_path)
501            .expect("Module not found");
502        let module_items = ctx
503            .ast_registry
504            .get_module_items(user_module_id)
505            .expect("Module should have items");
506
507        let has_impl_block = module_items.iter().any(|item| {
508            if let PureItem::Impl(impl_block) = item {
509                impl_block.self_ty == "User"
510            } else {
511                false
512            }
513        });
514        assert!(has_impl_block, "Module should contain impl block for User");
515
516        // Verify 4: File generation includes impl block
517        let files = multi_file_dumper().dump_all(&ctx).unwrap();
518        let user_file_path =
519            WorkspaceFilePath::new_for_test("src/user.rs", ctx.workspace_root(), "test_crate");
520        let user_content = files.get(&user_file_path).expect("user.rs should exist");
521
522        assert!(
523            user_content.contains("impl User"),
524            "File should contain impl block. Got:\n{}",
525            user_content
526        );
527        assert!(
528            user_content.contains("pub fn new(name: String) -> Self"),
529            "File should contain method signature. Got:\n{}",
530            user_content
531        );
532    }
533
534    /// Test AddMethodMutation for a generic struct.
535    /// Verifies that impl block is generated with correct generics.
536    #[test]
537    fn test_add_method_to_generic_struct() {
538        // Setup: Generic struct without impl block
539        let mut ctx = ContextBuilder::new()
540            .with_file(
541                "src/lib.rs",
542                r#"pub mod service;
543"#,
544            )
545            .with_file(
546                "src/service.rs",
547                r#"pub trait Repository {
548    fn find(&self, id: u64) -> Option<String>;
549}
550
551pub struct Service<R: Repository> {
552    repository: R,
553}
554"#,
555            )
556            .build();
557
558        // Find the Service struct
559        let service_path = ryo_symbol::SymbolPath::parse("test_crate::service::Service").unwrap();
560        let service_id = ctx
561            .registry
562            .lookup(&service_path)
563            .expect("Service not found");
564
565        // Add method "new" to Service
566        let mutation = AddMethodMutation::new(service_id, "new")
567            .public()
568            .with_params(vec![("repository".to_string(), "R".to_string())])
569            .with_return_type("Self")
570            .with_body("Self { repository }");
571
572        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
573        assert!(result.has_changes(), "Expected changes");
574
575        // Verify: File generation includes impl block with generics
576        let files = multi_file_dumper().dump_all(&ctx).unwrap();
577        let service_file_path =
578            WorkspaceFilePath::new_for_test("src/service.rs", ctx.workspace_root(), "test_crate");
579        let service_content = files
580            .get(&service_file_path)
581            .expect("service.rs should exist");
582
583        // The impl block should have the generic parameter
584        assert!(
585            service_content.contains("impl<R: Repository> Service<R>")
586                || service_content.contains("impl<R : Repository> Service<R>"),
587            "File should contain impl block with generics. Got:\n{}",
588            service_content
589        );
590        assert!(
591            service_content.contains("pub fn new(repository: R) -> Self"),
592            "File should contain method signature. Got:\n{}",
593            service_content
594        );
595    }
596
597    /// Test adding method to a generic struct that was created via AddItem.
598    /// This simulates the E2E scenario where struct and method are added in same execution.
599    #[test]
600    fn test_add_method_to_generic_struct_via_add_item() {
601        use ryo_mutations::AddItemMutation;
602
603        // Setup: Empty module
604        let mut ctx = ContextBuilder::new()
605            .with_file(
606                "src/lib.rs",
607                r#"pub mod repository;
608pub mod service;
609"#,
610            )
611            .with_file(
612                "src/repository.rs",
613                r#"pub trait InventoryRepository {
614    fn find(&self, id: u64) -> Option<String>;
615}
616"#,
617            )
618            .with_file("src/service.rs", "//! Service module\n")
619            .build();
620
621        // Step 1: Add generic struct via AddItem
622        let service_mod_path = ryo_symbol::SymbolPath::parse("test_crate::service").unwrap();
623        let service_mod_id = ctx
624            .registry
625            .lookup(&service_mod_path)
626            .expect("service module not found");
627
628        let add_struct_mutation = AddItemMutation::new(
629            service_mod_id,
630            r#"pub struct InventoryService<R: crate::repository::InventoryRepository> {
631    repository: R,
632}"#
633            .to_string(),
634        );
635
636        let result = ASTMutationEngine::execute_ast_reg(&add_struct_mutation, &mut ctx);
637        assert!(result.has_changes(), "AddItem should add struct");
638
639        // Step 2: Find the newly added struct
640        let struct_path =
641            ryo_symbol::SymbolPath::parse("test_crate::service::InventoryService").unwrap();
642        let struct_id = ctx
643            .registry
644            .lookup(&struct_path)
645            .expect("InventoryService not found after AddItem");
646
647        // Debug: Verify struct generics are stored
648        let struct_ast = ctx.ast_registry.get(struct_id);
649        assert!(struct_ast.is_some(), "Struct AST should exist");
650        if let Some(PureItem::Struct(s)) = struct_ast {
651            assert!(
652                !s.generics.params.is_empty(),
653                "Struct should have generic params. Got: {:?}",
654                s.generics
655            );
656        } else {
657            panic!("Expected PureItem::Struct");
658        }
659
660        // Step 3: Add method via AddMethod
661        let add_method_mutation = AddMethodMutation::new(struct_id, "new")
662            .public()
663            .with_params(vec![("repository".to_string(), "R".to_string())])
664            .with_return_type("Self")
665            .with_body("Self { repository }");
666
667        let result = ASTMutationEngine::execute_ast_reg(&add_method_mutation, &mut ctx);
668        assert!(result.has_changes(), "AddMethod should add method");
669
670        // Verify: File generation includes impl block with generics
671        let files = multi_file_dumper().dump_all(&ctx).unwrap();
672        let service_file_path =
673            WorkspaceFilePath::new_for_test("src/service.rs", ctx.workspace_root(), "test_crate");
674        let service_content = files
675            .get(&service_file_path)
676            .expect("service.rs should exist");
677
678        // The impl block should have the generic parameter with trait bound
679        assert!(
680            service_content
681                .contains("impl<R: crate::repository::InventoryRepository> InventoryService<R>")
682                || service_content.contains(
683                    "impl<R : crate :: repository :: InventoryRepository> InventoryService<R>"
684                )
685                || service_content.contains(
686                    "impl<R: crate :: repository :: InventoryRepository> InventoryService < R >"
687                ),
688            "File should contain impl block with generics. Got:\n{}",
689            service_content
690        );
691    }
692
693    /// Test RemoveMethodMutation with new design:
694    /// - Method is removed from SymbolRegistry (Type::method)
695    /// - Method AST is removed from ASTRegistry
696    /// - Method is removed from impl block in module_items
697    #[test]
698    fn test_remove_method_from_struct_new_design() {
699        // Setup: Struct with a method
700        let mut ctx = ContextBuilder::new()
701            .with_file(
702                "src/lib.rs",
703                r#"pub mod user;
704"#,
705            )
706            .with_file(
707                "src/user.rs",
708                r#"pub struct User {
709    pub name: String,
710}
711
712impl User {
713    pub fn new(name: String) -> Self {
714        Self { name }
715    }
716
717    pub fn get_name(&self) -> &str {
718        &self.name
719    }
720}
721"#,
722            )
723            .build();
724
725        // Verify initial state: both methods exist
726        let new_method_path = ryo_symbol::SymbolPath::parse("test_crate::user::User::new").unwrap();
727        let get_name_path =
728            ryo_symbol::SymbolPath::parse("test_crate::user::User::get_name").unwrap();
729
730        assert!(
731            ctx.registry.lookup(&new_method_path).is_some(),
732            "Method 'new' should exist initially"
733        );
734        assert!(
735            ctx.registry.lookup(&get_name_path).is_some(),
736            "Method 'get_name' should exist initially"
737        );
738
739        // Remove method "get_name"
740        let get_name_id = ctx
741            .registry
742            .lookup(&get_name_path)
743            .expect("Method not found");
744        let mutation = RemoveMethodMutation::new(get_name_id);
745
746        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
747        assert!(result.has_changes(), "Expected changes");
748
749        // Verify 1: Method removed from SymbolRegistry
750        assert!(
751            ctx.registry.lookup(&get_name_path).is_none(),
752            "Method 'get_name' should be removed from SymbolRegistry"
753        );
754
755        // Verify 2: Method AST removed from ASTRegistry
756        assert!(
757            ctx.ast_registry.get(get_name_id).is_none(),
758            "Method AST should be removed from ASTRegistry"
759        );
760
761        // Verify 3: Method removed from impl block in module_items
762        let user_module_path = ryo_symbol::SymbolPath::parse("test_crate::user").unwrap();
763        let user_module_id = ctx
764            .registry
765            .lookup(&user_module_path)
766            .expect("Module not found");
767        let module_items = ctx
768            .ast_registry
769            .get_module_items(user_module_id)
770            .expect("Module should have items");
771
772        // Check impl block still exists but without get_name
773        let impl_block = module_items.iter().find_map(|item| {
774            if let PureItem::Impl(impl_block) = item {
775                if impl_block.self_ty == "User" {
776                    Some(impl_block)
777                } else {
778                    None
779                }
780            } else {
781                None
782            }
783        });
784
785        assert!(impl_block.is_some(), "Impl block should still exist");
786        let impl_block = impl_block.unwrap();
787
788        // Should have only "new" method, not "get_name"
789        let method_names: Vec<String> = impl_block
790            .items
791            .iter()
792            .filter_map(|item| {
793                if let PureImplItem::Fn(f) = item {
794                    Some(f.name.clone())
795                } else {
796                    None
797                }
798            })
799            .collect();
800
801        assert!(
802            method_names.contains(&"new".to_string()),
803            "Method 'new' should still exist"
804        );
805        assert!(
806            !method_names.contains(&"get_name".to_string()),
807            "Method 'get_name' should be removed from impl block"
808        );
809
810        // Verify 4: File generation doesn't include removed method
811        let files = multi_file_dumper().dump_all(&ctx).unwrap();
812        let user_file_path =
813            WorkspaceFilePath::new_for_test("src/user.rs", ctx.workspace_root(), "test_crate");
814        let user_content = files.get(&user_file_path).expect("user.rs should exist");
815
816        assert!(
817            user_content.contains("impl User"),
818            "File should contain impl block. Got:\n{}",
819            user_content
820        );
821        assert!(
822            user_content.contains("fn new("),
823            "File should contain 'new' method. Got:\n{}",
824            user_content
825        );
826        assert!(
827            !user_content.contains("fn get_name("),
828            "File should NOT contain 'get_name' method. Got:\n{}",
829            user_content
830        );
831    }
832
833    /// Test removing multiple methods sequentially
834    #[test]
835    fn test_remove_multiple_methods() {
836        // Setup: Struct with 3 methods
837        let mut ctx = ContextBuilder::new()
838            .with_file(
839                "src/lib.rs",
840                r#"pub mod calc;
841"#,
842            )
843            .with_file(
844                "src/calc.rs",
845                r#"pub struct Calculator {
846    value: i32,
847}
848
849impl Calculator {
850    pub fn new(value: i32) -> Self {
851        Self { value }
852    }
853
854    pub fn add(&mut self, x: i32) {
855        self.value += x;
856    }
857
858    pub fn multiply(&mut self, x: i32) {
859        self.value *= x;
860    }
861
862    pub fn get_value(&self) -> i32 {
863        self.value
864    }
865}
866"#,
867            )
868            .build();
869
870        // Remove "add" method
871        let add_path = ryo_symbol::SymbolPath::parse("test_crate::calc::Calculator::add").unwrap();
872        let add_id = ctx
873            .registry
874            .lookup(&add_path)
875            .expect("Method 'add' not found");
876        let mutation1 = RemoveMethodMutation::new(add_id);
877        let result1 = ASTMutationEngine::execute_ast_reg(&mutation1, &mut ctx);
878        assert!(result1.has_changes(), "First removal should succeed");
879
880        // Remove "multiply" method
881        let multiply_path =
882            ryo_symbol::SymbolPath::parse("test_crate::calc::Calculator::multiply").unwrap();
883        let multiply_id = ctx
884            .registry
885            .lookup(&multiply_path)
886            .expect("Method 'multiply' not found");
887        let mutation2 = RemoveMethodMutation::new(multiply_id);
888        let result2 = ASTMutationEngine::execute_ast_reg(&mutation2, &mut ctx);
889        assert!(result2.has_changes(), "Second removal should succeed");
890
891        // Verify: Only "new" and "get_value" remain
892        assert!(
893            ctx.registry
894                .lookup(
895                    &ryo_symbol::SymbolPath::parse("test_crate::calc::Calculator::new").unwrap()
896                )
897                .is_some(),
898            "Method 'new' should still exist"
899        );
900        assert!(
901            ctx.registry
902                .lookup(
903                    &ryo_symbol::SymbolPath::parse("test_crate::calc::Calculator::get_value")
904                        .unwrap()
905                )
906                .is_some(),
907            "Method 'get_value' should still exist"
908        );
909        assert!(
910            ctx.registry.lookup(&add_path).is_none(),
911            "Method 'add' should be removed"
912        );
913        assert!(
914            ctx.registry.lookup(&multiply_path).is_none(),
915            "Method 'multiply' should be removed"
916        );
917
918        // Verify file output
919        let files = multi_file_dumper().dump_all(&ctx).unwrap();
920        let calc_path =
921            WorkspaceFilePath::new_for_test("src/calc.rs", ctx.workspace_root(), "test_crate");
922        let calc_content = files.get(&calc_path).expect("calc.rs should exist");
923
924        assert!(
925            calc_content.contains("fn new("),
926            "Should contain 'new' method"
927        );
928        assert!(
929            calc_content.contains("fn get_value("),
930            "Should contain 'get_value' method"
931        );
932        assert!(
933            !calc_content.contains("fn add("),
934            "Should NOT contain 'add' method. Got:\n{}",
935            calc_content
936        );
937        assert!(
938            !calc_content.contains("fn multiply("),
939            "Should NOT contain 'multiply' method. Got:\n{}",
940            calc_content
941        );
942    }
943
944    /// Test removing method from trait impl
945    #[test]
946    fn test_remove_method_from_trait_impl() {
947        // Setup: Struct with trait impl (using simple trait name without ::)
948        let mut ctx = ContextBuilder::new()
949            .with_file(
950                "src/lib.rs",
951                r#"pub mod shapes;
952"#,
953            )
954            .with_file(
955                "src/shapes.rs",
956                r#"pub trait Drawable {
957    fn draw(&self) -> String;
958    fn color(&self) -> String;
959}
960
961pub struct Circle {
962    pub radius: f64,
963}
964
965impl Drawable for Circle {
966    fn draw(&self) -> String {
967        format!("Circle with radius {}", self.radius)
968    }
969
970    fn color(&self) -> String {
971        "red".to_string()
972    }
973}
974"#,
975            )
976            .build();
977
978        // Find trait impl method: <impl Drawable for Circle>::color
979        let color_path =
980            ryo_symbol::SymbolPath::parse("test_crate::shapes::<impl Drawable for Circle>::color")
981                .unwrap();
982        let color_id = ctx
983            .registry
984            .lookup(&color_path)
985            .expect("Method 'color' not found in trait impl");
986
987        // Remove method "color"
988        let mutation = RemoveMethodMutation::new(color_id);
989        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
990        assert!(result.has_changes(), "Removal should succeed");
991
992        // Verify: Method removed from SymbolRegistry
993        assert!(
994            ctx.registry.lookup(&color_path).is_none(),
995            "Method 'color' should be removed from SymbolRegistry"
996        );
997
998        // Verify: "draw" method still exists
999        let draw_path =
1000            ryo_symbol::SymbolPath::parse("test_crate::shapes::<impl Drawable for Circle>::draw")
1001                .unwrap();
1002        assert!(
1003            ctx.registry.lookup(&draw_path).is_some(),
1004            "Method 'draw' should still exist"
1005        );
1006
1007        // Verify: trait impl block still exists with only "draw"
1008        let shapes_module_path = ryo_symbol::SymbolPath::parse("test_crate::shapes").unwrap();
1009        let shapes_module_id = ctx
1010            .registry
1011            .lookup(&shapes_module_path)
1012            .expect("Module not found");
1013        let module_items = ctx
1014            .ast_registry
1015            .get_module_items(shapes_module_id)
1016            .expect("Module should have items");
1017
1018        // Check trait impl block exists with only "draw"
1019        let trait_impl = module_items.iter().find_map(|item| {
1020            if let PureItem::Impl(impl_block) = item {
1021                if impl_block.trait_.is_some() && impl_block.self_ty == "Circle" {
1022                    Some(impl_block)
1023                } else {
1024                    None
1025                }
1026            } else {
1027                None
1028            }
1029        });
1030
1031        assert!(trait_impl.is_some(), "Trait impl block should exist");
1032        let trait_impl = trait_impl.unwrap();
1033        assert_eq!(
1034            trait_impl.items.len(),
1035            1,
1036            "Trait impl should have 1 method after removal"
1037        );
1038
1039        // Verify file output
1040        let files = multi_file_dumper().dump_all(&ctx).unwrap();
1041        let shapes_path =
1042            WorkspaceFilePath::new_for_test("src/shapes.rs", ctx.workspace_root(), "test_crate");
1043        let shapes_content = files.get(&shapes_path).expect("shapes.rs should exist");
1044
1045        assert!(
1046            shapes_content.contains("impl Drawable for Circle"),
1047            "Should contain trait impl block. Got:\n{}",
1048            shapes_content
1049        );
1050        assert!(
1051            shapes_content.contains("fn draw("),
1052            "Should contain 'draw' method. Got:\n{}",
1053            shapes_content
1054        );
1055        // Check that color method implementation is removed (trait definition should still have it)
1056        assert!(
1057            !shapes_content.contains("\"red\""),
1058            "Should NOT contain 'color' method implementation. Got:\n{}",
1059            shapes_content
1060        );
1061    }
1062}