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