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("RemoveItem", Box::new(converters::RemoveConverter::new()));
76 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
96 }
97
98 pub fn register(&mut self, kind: &'static str, converter: Box<dyn MutationConverter>) {
103 self.converters.insert(kind, converter);
104 }
105
106 pub fn register_all<C: MutationConverter + Default + 'static>(&mut self) {
123 let temp = C::default();
124 for kind in temp.spec_kinds() {
125 self.converters.insert(*kind, Box::new(C::default()));
126 }
127 }
128
129 pub fn can_handle(&self, spec: &MutationSpec) -> bool {
131 self.converters.contains_key(spec.kind_name())
132 }
133
134 pub fn get(&self, spec: &MutationSpec) -> Option<&dyn MutationConverter> {
136 self.converters.get(spec.kind_name()).map(|c| c.as_ref())
137 }
138
139 #[deprecated(
141 since = "0.1.0",
142 note = "Returns Box<dyn Mutation> for legacy apply(&mut PureFile). Use convert_v2() for ASTRegApply."
143 )]
144 #[allow(deprecated)]
145 pub fn convert(
146 &self,
147 spec: &MutationSpec,
148 ) -> Result<Box<dyn ryo_mutations::Mutation>, ConvertError> {
149 let converter = self
150 .converters
151 .get(spec.kind_name())
152 .ok_or_else(|| ConvertError::UnknownSpec(spec.kind_name().to_string()))?;
153
154 converter.convert(spec)
155 }
156
157 pub fn convert_v2(
168 &self,
169 spec: &MutationSpec,
170 ctx: &AnalysisContext,
171 ) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
172 let converter = self
173 .converters
174 .get(spec.kind_name())
175 .ok_or_else(|| ConvertError::UnknownSpec(spec.kind_name().to_string()))?;
176
177 converter.convert_v2(spec, ctx)
178 }
179
180 pub fn pre_check(
197 &self,
198 spec: &MutationSpec,
199 ctx: &AnalysisContext,
200 ) -> Result<(), ConvertError> {
201 let _checker = GraphChecker::new(ctx.code_graph(), ctx.typeflow_graph(), ctx.registry());
202
203 match spec {
204 MutationSpec::Rename { .. } => {}
208
209 MutationSpec::AddField { .. } | MutationSpec::RemoveField { .. } => {}
211
212 MutationSpec::AddDerive { .. } | MutationSpec::RemoveDerive { .. } => {}
214
215 MutationSpec::AddVariant { .. } => {}
217
218 MutationSpec::RemoveVariant { .. } => {}
220
221 MutationSpec::AddMethod {
222 target: target_symbol,
223 ..
224 } => {
225 let _ = target_symbol; }
229
230 MutationSpec::RemoveMethod { .. } => {
231 }
233
234 MutationSpec::ChangeVisibility { .. } => {
235 }
237
238 MutationSpec::RemoveItem { .. } => {
241 }
243
244 MutationSpec::AddItem { .. }
246 | MutationSpec::RemoveMod { .. }
247 | MutationSpec::CreateMod { .. }
248 | MutationSpec::AddSpec { .. }
249 | MutationSpec::AddMatchArm { .. }
250 | MutationSpec::RemoveMatchArm { .. }
251 | MutationSpec::ReplaceMatchArm { .. }
252 | MutationSpec::AddStructLiteralField { .. }
253 | MutationSpec::RemoveStructLiteralField { .. } => {
254 }
256
257 MutationSpec::OrganizeImports { .. }
260 | MutationSpec::LoopToIterator { .. }
261 | MutationSpec::UnwrapToQuestion { .. }
262 | MutationSpec::AssignOp { .. }
263 | MutationSpec::BoolSimplify { .. }
264 | MutationSpec::CloneOnCopy { .. }
265 | MutationSpec::CollapsibleIf { .. }
266 | MutationSpec::ComparisonToMethod { .. }
267 | MutationSpec::RedundantClosure { .. }
268 | MutationSpec::IntroduceVariable { .. }
269 | MutationSpec::ManualMap { .. }
270 | MutationSpec::MatchToIfLet { .. }
271 | MutationSpec::FilterNext { .. }
272 | MutationSpec::MapUnwrapOr { .. } => {
273 }
275
276 MutationSpec::RemoveSpec { .. } => {
278 }
280
281 MutationSpec::ValidateSpec { .. } => {
282 }
284
285 MutationSpec::ExtractTrait { .. }
287 | MutationSpec::InlineTrait { .. }
288 | MutationSpec::ReplaceType { .. }
289 | MutationSpec::EnumToTrait { .. }
290 | MutationSpec::MoveItem { .. }
291 | MutationSpec::PluginTransform { .. }
292 | MutationSpec::ReplaceExpr { .. }
293 | MutationSpec::RemoveStatement { .. }
294 | MutationSpec::InsertStatement { .. }
295 | MutationSpec::ReplaceStatement { .. }
296 | MutationSpec::DuplicateFunction { .. }
297 | MutationSpec::DuplicateStruct { .. }
298 | MutationSpec::DuplicateEnum { .. }
299 | MutationSpec::DuplicateModTree { .. }
300 | MutationSpec::NoOpArmToTodo { .. } => {
301 }
303 }
304
305 Ok(())
306 }
307
308 pub fn len(&self) -> usize {
310 self.converters.len()
311 }
312
313 pub fn is_empty(&self) -> bool {
315 self.converters.is_empty()
316 }
317
318 pub fn registered_kinds(&self) -> Vec<&'static str> {
320 self.converters.keys().copied().collect()
321 }
322}
323
324impl Default for MutationRegistry {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_registry_has_all_converters() {
336 let registry = MutationRegistry::new();
337 assert!(!registry.is_empty());
339 assert_eq!(registry.len(), 47);
348 }
349
350 #[test]
351 fn test_registry_can_handle_rename() {
352 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
353
354 let registry = MutationRegistry::new();
355 let mut sym_registry = SymbolRegistry::new();
356 let path = SymbolPath::parse("test_crate::old").unwrap();
357 let symbol_id = sym_registry.register(path, SymbolKind::Function).unwrap();
358
359 let spec = MutationSpec::Rename {
360 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
361 to: "new".into(),
362 scope: crate::executor::spec::Scope::Project,
363 };
364
365 assert!(registry.can_handle(&spec));
366 }
367
368 #[test]
369 fn test_registry_can_handle_field() {
370 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
371
372 let registry = MutationRegistry::new();
373 let mut sym_registry = SymbolRegistry::new();
374 let path = SymbolPath::parse("test_crate::Config").unwrap();
375 let symbol_id = sym_registry.register(path, SymbolKind::Struct).unwrap();
376
377 let add_spec = MutationSpec::AddField {
378 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
379 field_name: "timeout".into(),
380 field_type: "u64".into(),
381 visibility: crate::executor::spec::Visibility::Pub,
382 };
383 assert!(registry.can_handle(&add_spec));
384
385 let remove_spec = MutationSpec::RemoveField {
386 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
387 field_name: "timeout".into(),
388 };
389 assert!(registry.can_handle(&remove_spec));
390 }
391
392 #[test]
393 fn test_registry_can_handle_add_item() {
394 let registry = MutationRegistry::new();
395
396 let spec = MutationSpec::AddItem {
398 target: crate::executor::spec::MutationTargetSymbol::ByPath(Box::new(
399 crate::executor::spec::SymbolPath::parse("test_crate::lib").unwrap(),
400 )),
401 content: "struct Foo {}".into(),
402 position: crate::executor::spec::InsertPosition::Top,
403 };
404 assert!(registry.can_handle(&spec));
405 }
406
407 #[test]
408 fn test_registry_registered_kinds() {
409 let registry = MutationRegistry::new();
410 let kinds = registry.registered_kinds();
411
412 assert!(kinds.contains(&"Rename"));
413 assert!(kinds.contains(&"AddField"));
414 assert!(kinds.contains(&"RemoveField"));
415 assert!(kinds.contains(&"ChangeVisibility"));
416 assert!(kinds.contains(&"AddDerive"));
417 assert!(kinds.contains(&"RemoveDerive"));
418 }
419
420 #[test]
424 fn test_all_converter_spec_kinds_are_registered() {
425 use std::collections::HashSet;
426
427 let registry = MutationRegistry::new();
428 let registered: HashSet<&str> = registry.registered_kinds().into_iter().collect();
429
430 let idiom = converters::IdiomConverter::new();
432 for kind in idiom.spec_kinds() {
433 assert!(
434 registered.contains(kind),
435 "IdiomConverter::spec_kinds() contains '{}' but NOT registered in MutationRegistry. \
436 Add: registry.register(\"{}\", Box::new(converters::IdiomConverter::new()));",
437 kind, kind
438 );
439 }
440
441 let rename = converters::RenameConverter::new();
443 for kind in rename.spec_kinds() {
444 assert!(
445 registered.contains(kind),
446 "RenameConverter::spec_kinds() contains '{}' but NOT registered",
447 kind
448 );
449 }
450
451 let field = converters::FieldConverter::new();
453 for kind in field.spec_kinds() {
454 assert!(
455 registered.contains(kind),
456 "FieldConverter::spec_kinds() contains '{}' but NOT registered",
457 kind
458 );
459 }
460
461 let enum_conv = converters::EnumConverter::new();
463 for kind in enum_conv.spec_kinds() {
464 assert!(
465 registered.contains(kind),
466 "EnumConverter::spec_kinds() contains '{}' but NOT registered",
467 kind
468 );
469 }
470
471 let module = converters::ModuleConverter::new();
473 for kind in module.spec_kinds() {
474 assert!(
475 registered.contains(kind),
476 "ModuleConverter::spec_kinds() contains '{}' but NOT registered",
477 kind
478 );
479 }
480
481 let stmt = converters::StmtConverter::new();
483 for kind in stmt.spec_kinds() {
484 assert!(
485 registered.contains(kind),
486 "StmtConverter::spec_kinds() contains '{}' but NOT registered",
487 kind
488 );
489 }
490
491 let dup = converters::DuplicateConverter::new();
493 for kind in dup.spec_kinds() {
494 assert!(
495 registered.contains(kind),
496 "DuplicateConverter::spec_kinds() contains '{}' but NOT registered",
497 kind
498 );
499 }
500 }
501}
502
503#[cfg(test)]
504mod tests_pre_check {
505 use super::*;
506 use ryo_analysis::testing::ContextBuilder;
507 use ryo_source::pure::{
508 PureEnum, PureField, PureFields, PureFile, PureItem, PureStruct, PureType, PureVariant,
509 PureVis,
510 };
511
512 fn make_test_file_with_struct(struct_name: &str, fields: &[(&str, &str)]) -> PureFile {
514 let pure_fields = fields
515 .iter()
516 .map(|(name, ty)| PureField {
517 name: name.to_string(),
518 ty: PureType::Path(ty.to_string()),
519 attrs: vec![],
520 vis: PureVis::Public,
521 })
522 .collect();
523
524 PureFile {
525 attrs: vec![],
526 items: vec![PureItem::Struct(PureStruct {
527 name: struct_name.to_string(),
528 vis: PureVis::Public,
529 generics: Default::default(),
530 fields: PureFields::Named(pure_fields),
531 attrs: vec![],
532 })],
533 }
534 }
535
536 fn make_test_file_with_enum(enum_name: &str, variants: &[&str]) -> PureFile {
538 let pure_variants = variants
539 .iter()
540 .map(|name| PureVariant {
541 name: name.to_string(),
542 attrs: vec![],
543 fields: PureFields::Unit,
544 discriminant: None,
545 })
546 .collect();
547
548 PureFile {
549 attrs: vec![],
550 items: vec![PureItem::Enum(PureEnum {
551 name: enum_name.to_string(),
552 vis: PureVis::Public,
553 generics: Default::default(),
554 variants: pure_variants,
555 attrs: vec![],
556 })],
557 }
558 }
559
560 #[test]
561 fn test_pre_check_rename_with_symbol_id() {
562 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
563
564 let registry = MutationRegistry::new();
565 let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
566 let ctx = ContextBuilder::new()
567 .with_pure_file("src/lib.rs", file)
568 .build();
569
570 let mut symbol_registry = SymbolRegistry::new();
572 let path = SymbolPath::parse("test_crate::Config").unwrap();
573 let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
574
575 let spec = MutationSpec::Rename {
577 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
578 to: "Settings".into(),
579 scope: crate::executor::spec::Scope::Project,
580 };
581
582 let result = registry.pre_check(&spec, &ctx);
584 assert!(
585 result.is_ok(),
586 "Pre-check should always pass for Rename with symbol_id: {:?}",
587 result
588 );
589 }
590
591 #[test]
592 fn test_pre_check_add_field_with_symbol_id() {
593 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
594
595 let mutation_registry = MutationRegistry::new();
596 let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
597
598 let mut symbol_registry = SymbolRegistry::new();
600 let path = SymbolPath::parse("test_crate::Config").unwrap();
601 let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
602
603 let ctx = ContextBuilder::new()
604 .with_pure_file("src/lib.rs", file)
605 .build();
606
607 let spec = MutationSpec::AddField {
608 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
609 field_name: "name".into(),
610 field_type: "String".into(),
611 visibility: crate::executor::spec::Visibility::Pub,
612 };
613
614 let result = mutation_registry.pre_check(&spec, &ctx);
616 assert!(
617 result.is_ok(),
618 "Pre-check should always pass for AddField with required symbol_id: {:?}",
619 result
620 );
621 }
622
623 #[test]
624 fn test_pre_check_add_variant_always_passes() {
625 let registry = MutationRegistry::new();
627 let file = make_test_file_with_enum("Status", &["Active", "Inactive"]);
628 let ctx = ContextBuilder::new()
629 .with_pure_file("src/lib.rs", file)
630 .build();
631
632 let enum_id = ctx
634 .registry()
635 .iter()
636 .find(|(_, path)| path.name() == "Status")
637 .map(|(id, _)| id)
638 .expect("Status enum should exist");
639
640 let spec = MutationSpec::AddVariant {
641 target: crate::executor::spec::MutationTargetSymbol::ById(enum_id),
642 variant_name: "Pending".into(),
643 variant_kind: crate::executor::spec::VariantKind::Unit,
644 };
645
646 let result = registry.pre_check(&spec, &ctx);
647 assert!(
648 result.is_ok(),
649 "Pre-check should always pass for AddVariant with required symbol_id: {:?}",
650 result
651 );
652 }
653
654 #[test]
655 fn test_pre_check_add_derive_with_symbol_id() {
656 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
657
658 let registry = MutationRegistry::new();
659 let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
660 let ctx = ContextBuilder::new()
661 .with_pure_file("src/lib.rs", file)
662 .build();
663
664 let mut symbol_registry = SymbolRegistry::new();
666 let path = SymbolPath::parse("test_crate::Config").unwrap();
667 let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
668
669 let spec = MutationSpec::AddDerive {
670 target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
671 derives: vec!["Debug".into(), "Clone".into()],
672 };
673
674 let result = registry.pre_check(&spec, &ctx);
676 assert!(
677 result.is_ok(),
678 "Pre-check should always pass for AddDerive with symbol_id: {:?}",
679 result
680 );
681 }
682
683 #[test]
684 fn test_pre_check_add_item_no_check_needed() {
685 let registry = MutationRegistry::new();
686 let file = make_test_file_with_struct("Config", &[]);
687 let ctx = ContextBuilder::new()
688 .with_pure_file("src/lib.rs", file)
689 .build();
690
691 let spec = MutationSpec::AddItem {
693 target: crate::executor::spec::MutationTargetSymbol::ByPath(Box::new(
694 crate::executor::spec::SymbolPath::parse("test_crate").unwrap(),
695 )),
696 content: "struct NewStruct {}".into(),
697 position: crate::executor::spec::InsertPosition::Bottom,
698 };
699
700 let result = registry.pre_check(&spec, &ctx);
701 assert!(result.is_ok(), "AddItem should not require pre-check");
702 }
703}