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 is_mut: false,
169 pat: None,
170 });
171 }
172
173 let ret = self.return_type.as_ref().map(|ty| parse_type_simple(ty));
175
176 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 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 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 let type_name = type_path.name().to_string();
225
226 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 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 let impl_block_index = module_items.iter().position(|item| {
265 if let PureItem::Impl(impl_block) = item {
266 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 if let PureItem::Impl(impl_block) = &mut module_items[idx] {
281 impl_block.items.push(PureImplItem::Fn(method_fn));
282 }
283 } else {
284 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 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
311impl ASTRegApply for RemoveMethodMutation {
313 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
314 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 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 if let Some(type_path) = method_path.parent() {
340 let type_name = type_path.name().to_string();
341
342 let is_trait_impl = type_name.starts_with("<impl ");
344
345 let (impl_path, type_name_for_match) = if is_trait_impl {
346 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 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 let parent_id = type_path
371 .parent()
372 .and_then(|p| ctx.symbol_registry.lookup(&p));
373
374 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 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 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 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 #[test]
447 fn test_add_method_to_struct_new_design() {
448 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 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 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 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 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 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 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]
539 fn test_add_method_to_generic_struct() {
540 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 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 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 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 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]
602 fn test_add_method_to_generic_struct_via_add_item() {
603 use ryo_mutations::AddItemMutation;
604
605 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 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 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 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 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 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 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]
700 fn test_remove_method_from_struct_new_design() {
701 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 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 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 assert!(
753 ctx.registry.lookup(&get_name_path).is_none(),
754 "Method 'get_name' should be removed from SymbolRegistry"
755 );
756
757 assert!(
759 ctx.ast_registry.get(get_name_id).is_none(),
760 "Method AST should be removed from ASTRegistry"
761 );
762
763 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 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 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 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]
837 fn test_remove_multiple_methods() {
838 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 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 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 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 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]
948 fn test_remove_method_from_trait_impl() {
949 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 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 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 assert!(
996 ctx.registry.lookup(&color_path).is_none(),
997 "Method 'color' should be removed from SymbolRegistry"
998 );
999
1000 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 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 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 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 assert!(
1059 !shapes_content.contains("\"red\""),
1060 "Should NOT contain 'color' method implementation. Got:\n{}",
1061 shapes_content
1062 );
1063 }
1064}