1mod converter;
26pub mod converters;
27
28pub use converter::{
29 opt_resolve_file_path_from_symbol, resolve_file_path_from_symbol, ApplyResult, ConvertError,
30 MutationConverter, ResolvedMutation,
31};
32
33use crate::engine::ASTRegApply;
34use crate::executor::spec::MutationSpec;
35use ryo_analysis::{AnalysisContext, GraphChecker};
36use std::collections::HashMap;
37
38pub struct MutationRegistry {
42 converters: HashMap<&'static str, Box<dyn MutationConverter>>,
43}
44
45impl std::fmt::Debug for MutationRegistry {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("MutationRegistry")
48 .field("registered_kinds", &self.registered_kinds())
49 .finish()
50 }
51}
52
53impl MutationRegistry {
54 pub fn new() -> Self {
60 let mut registry = Self {
61 converters: HashMap::new(),
62 };
63
64 registry.register("Rename", Box::new(converters::RenameConverter::new()));
66 registry.register(
67 "ChangeVisibility",
68 Box::new(converters::VisibilityConverter::new()),
69 );
70 registry.register_all::<converters::FieldConverter>(); registry.register_all::<converters::DeriveConverter>(); registry.register_all::<converters::EnumConverter>(); registry.register_all::<converters::RemoveConverter>(); registry.register_all::<converters::MethodConverter>(); registry.register_all::<converters::ModuleConverter>(); registry.register("AddItem", Box::new(converters::AddItemConverter::new()));
79
80 registry.register_all::<converters::IdiomConverter>();
82
83 registry.register_all::<converters::TraitConverter>(); registry.register("MoveItem", Box::new(converters::MoveConverter::new()));
86 registry.register(
87 "PluginTransform",
88 Box::new(converters::PluginConverter::new()),
89 );
90 registry.register_all::<converters::StmtConverter>(); registry.register_all::<converters::MatchArmConverter>(); registry.register_all::<converters::StructLiteralFieldConverter>(); registry.register_all::<converters::DuplicateConverter>(); registry.register_all::<converters::SpecAliasConverter>(); registry
97 }
98
99 pub fn register(&mut self, kind: &'static str, converter: Box<dyn MutationConverter>) {
104 self.converters.insert(kind, converter);
105 }
106
107 pub fn register_all<C: MutationConverter + Default + 'static>(&mut self) {
124 let temp = C::default();
125 for kind in temp.spec_kinds() {
126 self.converters.insert(*kind, Box::new(C::default()));
127 }
128 }
129
130 pub fn can_handle(&self, spec: &MutationSpec) -> bool {
132 self.converters.contains_key(spec.kind_name())
133 }
134
135 pub fn get(&self, spec: &MutationSpec) -> Option<&dyn MutationConverter> {
137 self.converters.get(spec.kind_name()).map(|c| c.as_ref())
138 }
139
140 pub fn convert_v2(
151 &self,
152 spec: &MutationSpec,
153 ctx: &AnalysisContext,
154 ) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
155 let converter = self
156 .converters
157 .get(spec.kind_name())
158 .ok_or_else(|| ConvertError::UnknownSpec(spec.kind_name().to_string()))?;
159
160 converter.convert_v2(spec, ctx)
161 }
162
163 pub fn pre_check(
180 &self,
181 spec: &MutationSpec,
182 ctx: &AnalysisContext,
183 ) -> Result<(), ConvertError> {
184 let _checker = GraphChecker::new(ctx.code_graph(), ctx.typeflow_graph(), ctx.registry());
185
186 match spec {
187 MutationSpec::Rename { .. } => {}
191
192 MutationSpec::AddField { .. } | MutationSpec::RemoveField { .. } => {}
194
195 MutationSpec::AddDerive { .. } | MutationSpec::RemoveDerive { .. } => {}
197
198 MutationSpec::AddVariant { .. } => {}
200
201 MutationSpec::RemoveVariant { .. } => {}
203
204 MutationSpec::AddMethod {
205 target: target_symbol,
206 ..
207 } => {
208 let _ = target_symbol; }
212
213 MutationSpec::RemoveMethod { .. } => {
214 }
216
217 MutationSpec::ChangeVisibility { .. } => {
218 }
220
221 MutationSpec::RemoveItem { .. } => {
224 }
226
227 MutationSpec::ReplaceCode { .. } => {}
231
232 MutationSpec::AddItem { .. }
234 | MutationSpec::RemoveMod { .. }
235 | MutationSpec::CreateMod { .. }
236 | MutationSpec::AddSpec { .. }
237 | MutationSpec::AddMatchArm { .. }
238 | MutationSpec::RemoveMatchArm { .. }
239 | MutationSpec::ReplaceMatchArm { .. }
240 | MutationSpec::AddStructLiteralField { .. }
241 | MutationSpec::RemoveStructLiteralField { .. } => {
242 }
244
245 MutationSpec::OrganizeImports { .. }
248 | MutationSpec::LoopToIterator { .. }
249 | MutationSpec::UnwrapToQuestion { .. }
250 | MutationSpec::AssignOp { .. }
251 | MutationSpec::BoolSimplify { .. }
252 | MutationSpec::CloneOnCopy { .. }
253 | MutationSpec::CollapsibleIf { .. }
254 | MutationSpec::ComparisonToMethod { .. }
255 | MutationSpec::RedundantClosure { .. }
256 | MutationSpec::IntroduceVariable { .. }
257 | MutationSpec::ManualMap { .. }
258 | MutationSpec::MatchToIfLet { .. }
259 | MutationSpec::FilterNext { .. }
260 | MutationSpec::MapUnwrapOr { .. } => {
261 }
263
264 MutationSpec::RemoveSpec { .. } => {
266 }
268
269 MutationSpec::ValidateSpec { .. } => {
270 }
272
273 MutationSpec::ExtractTrait { .. }
275 | MutationSpec::InlineTrait { .. }
276 | MutationSpec::ReplaceType { .. }
277 | MutationSpec::EnumToTrait { .. }
278 | MutationSpec::MoveItem { .. }
279 | MutationSpec::PluginTransform { .. }
280 | MutationSpec::ReplaceExpr { .. }
281 | MutationSpec::RemoveStatement { .. }
282 | MutationSpec::InsertStatement { .. }
283 | MutationSpec::ReplaceStatement { .. }
284 | MutationSpec::DuplicateFunction { .. }
285 | MutationSpec::DuplicateStruct { .. }
286 | MutationSpec::DuplicateEnum { .. }
287 | MutationSpec::DuplicateModTree { .. }
288 | MutationSpec::NoOpArmToTodo { .. } => {
289 }
291 }
292
293 Ok(())
294 }
295
296 pub fn len(&self) -> usize {
298 self.converters.len()
299 }
300
301 pub fn is_empty(&self) -> bool {
303 self.converters.is_empty()
304 }
305
306 pub fn registered_kinds(&self) -> Vec<&'static str> {
308 self.converters.keys().copied().collect()
309 }
310}
311
312impl Default for MutationRegistry {
313 fn default() -> Self {
314 Self::new()
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_registry_has_all_converters() {
324 let registry = MutationRegistry::new();
325 assert!(!registry.is_empty());
327 assert_eq!(registry.len(), 51);
340 }
341
342 #[test]
343 fn test_registry_can_handle_rename() {
344 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
345
346 let registry = MutationRegistry::new();
347 let mut sym_registry = SymbolRegistry::new();
348 let path = SymbolPath::parse("test_crate::old").unwrap();
349 let symbol_id = sym_registry.register(path, SymbolKind::Function).unwrap();
350
351 let spec = MutationSpec::Rename {
352 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
353 to: "new".into(),
354 scope: crate::executor::spec::Scope::Project,
355 };
356
357 assert!(registry.can_handle(&spec));
358 }
359
360 #[test]
361 fn test_registry_can_handle_field() {
362 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
363
364 let registry = MutationRegistry::new();
365 let mut sym_registry = SymbolRegistry::new();
366 let path = SymbolPath::parse("test_crate::Config").unwrap();
367 let symbol_id = sym_registry.register(path, SymbolKind::Struct).unwrap();
368
369 let add_spec = MutationSpec::AddField {
370 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
371 field_name: "timeout".into(),
372 field_type: "u64".into(),
373 visibility: crate::executor::spec::Visibility::Pub,
374 };
375 assert!(registry.can_handle(&add_spec));
376
377 let remove_spec = MutationSpec::RemoveField {
378 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
379 field_name: "timeout".into(),
380 };
381 assert!(registry.can_handle(&remove_spec));
382 }
383
384 #[test]
385 fn test_registry_can_handle_add_item() {
386 let registry = MutationRegistry::new();
387
388 let spec = MutationSpec::AddItem {
390 target: crate::executor::spec::MutationTargetSymbol::ByPath(Box::new(
391 crate::executor::spec::SymbolPath::parse("test_crate::lib").unwrap(),
392 )),
393 content: "struct Foo {}".into(),
394 position: crate::executor::spec::InsertPosition::Top,
395 };
396 assert!(registry.can_handle(&spec));
397 }
398
399 #[test]
400 fn test_registry_registered_kinds() {
401 let registry = MutationRegistry::new();
402 let kinds = registry.registered_kinds();
403
404 assert!(kinds.contains(&"Rename"));
405 assert!(kinds.contains(&"AddField"));
406 assert!(kinds.contains(&"RemoveField"));
407 assert!(kinds.contains(&"ChangeVisibility"));
408 assert!(kinds.contains(&"AddDerive"));
409 assert!(kinds.contains(&"RemoveDerive"));
410 }
411
412 #[test]
416 fn test_all_converter_spec_kinds_are_registered() {
417 use std::collections::HashSet;
418
419 let registry = MutationRegistry::new();
420 let registered: HashSet<&str> = registry.registered_kinds().into_iter().collect();
421
422 let idiom = converters::IdiomConverter::new();
424 for kind in idiom.spec_kinds() {
425 assert!(
426 registered.contains(kind),
427 "IdiomConverter::spec_kinds() contains '{}' but NOT registered in MutationRegistry. \
428 Add: registry.register(\"{}\", Box::new(converters::IdiomConverter::new()));",
429 kind, kind
430 );
431 }
432
433 let rename = converters::RenameConverter::new();
435 for kind in rename.spec_kinds() {
436 assert!(
437 registered.contains(kind),
438 "RenameConverter::spec_kinds() contains '{}' but NOT registered",
439 kind
440 );
441 }
442
443 let field = converters::FieldConverter::new();
445 for kind in field.spec_kinds() {
446 assert!(
447 registered.contains(kind),
448 "FieldConverter::spec_kinds() contains '{}' but NOT registered",
449 kind
450 );
451 }
452
453 let enum_conv = converters::EnumConverter::new();
455 for kind in enum_conv.spec_kinds() {
456 assert!(
457 registered.contains(kind),
458 "EnumConverter::spec_kinds() contains '{}' but NOT registered",
459 kind
460 );
461 }
462
463 let module = converters::ModuleConverter::new();
465 for kind in module.spec_kinds() {
466 assert!(
467 registered.contains(kind),
468 "ModuleConverter::spec_kinds() contains '{}' but NOT registered",
469 kind
470 );
471 }
472
473 let stmt = converters::StmtConverter::new();
475 for kind in stmt.spec_kinds() {
476 assert!(
477 registered.contains(kind),
478 "StmtConverter::spec_kinds() contains '{}' but NOT registered",
479 kind
480 );
481 }
482
483 let dup = converters::DuplicateConverter::new();
485 for kind in dup.spec_kinds() {
486 assert!(
487 registered.contains(kind),
488 "DuplicateConverter::spec_kinds() contains '{}' but NOT registered",
489 kind
490 );
491 }
492 }
493}
494
495#[cfg(test)]
496mod tests_pre_check {
497 use super::*;
498 use ryo_analysis::testing::ContextBuilder;
499 use ryo_source::pure::{
500 PureEnum, PureField, PureFields, PureFile, PureItem, PureStruct, PureType, PureVariant,
501 PureVis,
502 };
503
504 fn make_test_file_with_struct(struct_name: &str, fields: &[(&str, &str)]) -> PureFile {
506 let pure_fields = fields
507 .iter()
508 .map(|(name, ty)| PureField {
509 name: name.to_string(),
510 ty: PureType::Path(ty.to_string()),
511 attrs: vec![],
512 vis: PureVis::Public,
513 })
514 .collect();
515
516 PureFile {
517 attrs: vec![],
518 items: vec![PureItem::Struct(PureStruct {
519 name: struct_name.to_string(),
520 vis: PureVis::Public,
521 generics: Default::default(),
522 fields: PureFields::Named(pure_fields),
523 attrs: vec![],
524 })],
525 }
526 }
527
528 fn make_test_file_with_enum(enum_name: &str, variants: &[&str]) -> PureFile {
530 let pure_variants = variants
531 .iter()
532 .map(|name| PureVariant {
533 name: name.to_string(),
534 attrs: vec![],
535 fields: PureFields::Unit,
536 discriminant: None,
537 })
538 .collect();
539
540 PureFile {
541 attrs: vec![],
542 items: vec![PureItem::Enum(PureEnum {
543 name: enum_name.to_string(),
544 vis: PureVis::Public,
545 generics: Default::default(),
546 variants: pure_variants,
547 attrs: vec![],
548 })],
549 }
550 }
551
552 #[test]
553 fn test_pre_check_rename_with_symbol_id() {
554 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
555
556 let registry = MutationRegistry::new();
557 let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
558 let ctx = ContextBuilder::new()
559 .with_pure_file("src/lib.rs", file)
560 .build();
561
562 let mut symbol_registry = SymbolRegistry::new();
564 let path = SymbolPath::parse("test_crate::Config").unwrap();
565 let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
566
567 let spec = MutationSpec::Rename {
569 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
570 to: "Settings".into(),
571 scope: crate::executor::spec::Scope::Project,
572 };
573
574 let result = registry.pre_check(&spec, &ctx);
576 assert!(
577 result.is_ok(),
578 "Pre-check should always pass for Rename with symbol_id: {:?}",
579 result
580 );
581 }
582
583 #[test]
584 fn test_pre_check_add_field_with_symbol_id() {
585 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
586
587 let mutation_registry = MutationRegistry::new();
588 let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
589
590 let mut symbol_registry = SymbolRegistry::new();
592 let path = SymbolPath::parse("test_crate::Config").unwrap();
593 let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
594
595 let ctx = ContextBuilder::new()
596 .with_pure_file("src/lib.rs", file)
597 .build();
598
599 let spec = MutationSpec::AddField {
600 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
601 field_name: "name".into(),
602 field_type: "String".into(),
603 visibility: crate::executor::spec::Visibility::Pub,
604 };
605
606 let result = mutation_registry.pre_check(&spec, &ctx);
608 assert!(
609 result.is_ok(),
610 "Pre-check should always pass for AddField with required symbol_id: {:?}",
611 result
612 );
613 }
614
615 #[test]
616 fn test_pre_check_add_variant_always_passes() {
617 let registry = MutationRegistry::new();
619 let file = make_test_file_with_enum("Status", &["Active", "Inactive"]);
620 let ctx = ContextBuilder::new()
621 .with_pure_file("src/lib.rs", file)
622 .build();
623
624 let enum_id = ctx
626 .registry()
627 .iter()
628 .find(|(_, path)| path.name() == "Status")
629 .map(|(id, _)| id)
630 .expect("Status enum should exist");
631
632 let spec = MutationSpec::AddVariant {
633 target: crate::executor::spec::MutationTargetSymbol::ById(enum_id),
634 variant_name: "Pending".into(),
635 variant_kind: crate::executor::spec::VariantKind::Unit,
636 };
637
638 let result = registry.pre_check(&spec, &ctx);
639 assert!(
640 result.is_ok(),
641 "Pre-check should always pass for AddVariant with required symbol_id: {:?}",
642 result
643 );
644 }
645
646 #[test]
647 fn test_pre_check_add_derive_with_symbol_id() {
648 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
649
650 let registry = MutationRegistry::new();
651 let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
652 let ctx = ContextBuilder::new()
653 .with_pure_file("src/lib.rs", file)
654 .build();
655
656 let mut symbol_registry = SymbolRegistry::new();
658 let path = SymbolPath::parse("test_crate::Config").unwrap();
659 let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
660
661 let spec = MutationSpec::AddDerive {
662 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
663 derives: vec!["Debug".into(), "Clone".into()],
664 };
665
666 let result = registry.pre_check(&spec, &ctx);
668 assert!(
669 result.is_ok(),
670 "Pre-check should always pass for AddDerive with symbol_id: {:?}",
671 result
672 );
673 }
674
675 #[test]
676 fn test_pre_check_add_item_no_check_needed() {
677 let registry = MutationRegistry::new();
678 let file = make_test_file_with_struct("Config", &[]);
679 let ctx = ContextBuilder::new()
680 .with_pure_file("src/lib.rs", file)
681 .build();
682
683 let spec = MutationSpec::AddItem {
685 target: crate::executor::spec::MutationTargetSymbol::ByPath(Box::new(
686 crate::executor::spec::SymbolPath::parse("test_crate").unwrap(),
687 )),
688 content: "struct NewStruct {}".into(),
689 position: crate::executor::spec::InsertPosition::Bottom,
690 };
691
692 let result = registry.pre_check(&spec, &ctx);
693 assert!(result.is_ok(), "AddItem should not require pre-check");
694 }
695}