1use 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
15fn parse_type_simple(s: &str) -> PureType {
20 let s = s.trim();
21
22 if let Some(rest) = s.strip_prefix('&') {
24 let rest = rest.trim_start();
25
26 if rest.starts_with('\'') {
28 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 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 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 return PureType::Ref {
60 lifetime: None,
61 is_mut: false,
62 ty: Box::new(parse_type_simple(rest)),
63 };
64 }
65
66 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 if s.starts_with('(') && s.ends_with(')') {
74 let inner = s[1..s.len() - 1].trim();
75 if inner.is_empty() {
76 return PureType::Tuple(vec![]);
78 }
79 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 PureType::Path(s.to_string())
92}
93
94impl ASTRegApply for AddMethodMutation {
105 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
106 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 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 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 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 let mut fn_params = Vec::new();
157
158 if let Some((is_ref, is_mut)) = self.takes_self {
160 fn_params.push(PureParam::SelfValue { is_ref, is_mut });
161 }
162
163 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 let ret = self.return_type.as_ref().map(|ty| parse_type_simple(ty));
173
174 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 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 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 let type_name = type_path.name().to_string();
223
224 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 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 let impl_block_index = module_items.iter().position(|item| {
263 if let PureItem::Impl(impl_block) = item {
264 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 if let PureItem::Impl(impl_block) = &mut module_items[idx] {
279 impl_block.items.push(PureImplItem::Fn(method_fn));
280 }
281 } else {
282 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 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
309impl ASTRegApply for RemoveMethodMutation {
311 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
312 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 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 if let Some(type_path) = method_path.parent() {
338 let type_name = type_path.name().to_string();
339
340 let is_trait_impl = type_name.starts_with("<impl ");
342
343 let (impl_path, type_name_for_match) = if is_trait_impl {
344 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 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 let parent_id = type_path
369 .parent()
370 .and_then(|p| ctx.symbol_registry.lookup(&p));
371
372 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 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 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 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 #[test]
445 fn test_add_method_to_struct_new_design() {
446 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 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 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 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 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 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 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]
537 fn test_add_method_to_generic_struct() {
538 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 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 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 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 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]
600 fn test_add_method_to_generic_struct_via_add_item() {
601 use ryo_mutations::AddItemMutation;
602
603 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 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 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 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 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 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 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]
698 fn test_remove_method_from_struct_new_design() {
699 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 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 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 assert!(
751 ctx.registry.lookup(&get_name_path).is_none(),
752 "Method 'get_name' should be removed from SymbolRegistry"
753 );
754
755 assert!(
757 ctx.ast_registry.get(get_name_id).is_none(),
758 "Method AST should be removed from ASTRegistry"
759 );
760
761 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 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 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 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]
835 fn test_remove_multiple_methods() {
836 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 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 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 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 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]
946 fn test_remove_method_from_trait_impl() {
947 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 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 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 assert!(
994 ctx.registry.lookup(&color_path).is_none(),
995 "Method 'color' should be removed from SymbolRegistry"
996 );
997
998 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 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 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 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 assert!(
1057 !shapes_content.contains("\"red\""),
1058 "Should NOT contain 'color' method implementation. Got:\n{}",
1059 shapes_content
1060 );
1061 }
1062}