mod converter;
pub mod converters;
pub use converter::{
opt_resolve_file_path_from_symbol, resolve_file_path_from_symbol, ApplyResult, ConvertError,
MutationConverter, ResolvedMutation,
};
use crate::engine::ASTRegApply;
use crate::executor::spec::MutationSpec;
use ryo_analysis::{AnalysisContext, GraphChecker};
use std::collections::HashMap;
pub struct MutationRegistry {
converters: HashMap<&'static str, Box<dyn MutationConverter>>,
}
impl std::fmt::Debug for MutationRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MutationRegistry")
.field("registered_kinds", &self.registered_kinds())
.finish()
}
}
impl MutationRegistry {
pub fn new() -> Self {
let mut registry = Self {
converters: HashMap::new(),
};
registry.register("Rename", Box::new(converters::RenameConverter::new()));
registry.register(
"ChangeVisibility",
Box::new(converters::VisibilityConverter::new()),
);
registry.register_all::<converters::FieldConverter>(); registry.register_all::<converters::DeriveConverter>();
registry.register_all::<converters::EnumConverter>(); registry.register("RemoveItem", Box::new(converters::RemoveConverter::new()));
registry.register_all::<converters::MethodConverter>(); registry.register_all::<converters::ModuleConverter>(); registry.register("AddItem", Box::new(converters::AddItemConverter::new()));
registry.register_all::<converters::IdiomConverter>();
registry.register_all::<converters::TraitConverter>(); registry.register("MoveItem", Box::new(converters::MoveConverter::new()));
registry.register(
"PluginTransform",
Box::new(converters::PluginConverter::new()),
);
registry.register_all::<converters::StmtConverter>(); registry.register_all::<converters::MatchArmConverter>(); registry.register_all::<converters::StructLiteralFieldConverter>(); registry.register_all::<converters::DuplicateConverter>();
registry
}
pub fn register(&mut self, kind: &'static str, converter: Box<dyn MutationConverter>) {
self.converters.insert(kind, converter);
}
pub fn register_all<C: MutationConverter + Default + 'static>(&mut self) {
let temp = C::default();
for kind in temp.spec_kinds() {
self.converters.insert(*kind, Box::new(C::default()));
}
}
pub fn can_handle(&self, spec: &MutationSpec) -> bool {
self.converters.contains_key(spec.kind_name())
}
pub fn get(&self, spec: &MutationSpec) -> Option<&dyn MutationConverter> {
self.converters.get(spec.kind_name()).map(|c| c.as_ref())
}
#[deprecated(
since = "0.1.0",
note = "Returns Box<dyn Mutation> for legacy apply(&mut PureFile). Use convert_v2() for ASTRegApply."
)]
#[allow(deprecated)]
pub fn convert(
&self,
spec: &MutationSpec,
) -> Result<Box<dyn ryo_mutations::Mutation>, ConvertError> {
let converter = self
.converters
.get(spec.kind_name())
.ok_or_else(|| ConvertError::UnknownSpec(spec.kind_name().to_string()))?;
converter.convert(spec)
}
pub fn convert_v2(
&self,
spec: &MutationSpec,
ctx: &AnalysisContext,
) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
let converter = self
.converters
.get(spec.kind_name())
.ok_or_else(|| ConvertError::UnknownSpec(spec.kind_name().to_string()))?;
converter.convert_v2(spec, ctx)
}
pub fn pre_check(
&self,
spec: &MutationSpec,
ctx: &AnalysisContext,
) -> Result<(), ConvertError> {
let _checker = GraphChecker::new(ctx.code_graph(), ctx.typeflow_graph(), ctx.registry());
match spec {
MutationSpec::Rename { .. } => {}
MutationSpec::AddField { .. } | MutationSpec::RemoveField { .. } => {}
MutationSpec::AddDerive { .. } | MutationSpec::RemoveDerive { .. } => {}
MutationSpec::AddVariant { .. } => {}
MutationSpec::RemoveVariant { .. } => {}
MutationSpec::AddMethod {
target: target_symbol,
..
} => {
let _ = target_symbol; }
MutationSpec::RemoveMethod { .. } => {
}
MutationSpec::ChangeVisibility { .. } => {
}
MutationSpec::RemoveItem { .. } => {
}
MutationSpec::AddItem { .. }
| MutationSpec::RemoveMod { .. }
| MutationSpec::CreateMod { .. }
| MutationSpec::AddSpec { .. }
| MutationSpec::AddMatchArm { .. }
| MutationSpec::RemoveMatchArm { .. }
| MutationSpec::ReplaceMatchArm { .. }
| MutationSpec::AddStructLiteralField { .. }
| MutationSpec::RemoveStructLiteralField { .. } => {
}
MutationSpec::OrganizeImports { .. }
| MutationSpec::LoopToIterator { .. }
| MutationSpec::UnwrapToQuestion { .. }
| MutationSpec::AssignOp { .. }
| MutationSpec::BoolSimplify { .. }
| MutationSpec::CloneOnCopy { .. }
| MutationSpec::CollapsibleIf { .. }
| MutationSpec::ComparisonToMethod { .. }
| MutationSpec::RedundantClosure { .. }
| MutationSpec::IntroduceVariable { .. }
| MutationSpec::ManualMap { .. }
| MutationSpec::MatchToIfLet { .. }
| MutationSpec::FilterNext { .. }
| MutationSpec::MapUnwrapOr { .. } => {
}
MutationSpec::RemoveSpec { .. } => {
}
MutationSpec::ValidateSpec { .. } => {
}
MutationSpec::ExtractTrait { .. }
| MutationSpec::InlineTrait { .. }
| MutationSpec::ReplaceType { .. }
| MutationSpec::EnumToTrait { .. }
| MutationSpec::MoveItem { .. }
| MutationSpec::PluginTransform { .. }
| MutationSpec::ReplaceExpr { .. }
| MutationSpec::RemoveStatement { .. }
| MutationSpec::InsertStatement { .. }
| MutationSpec::ReplaceStatement { .. }
| MutationSpec::DuplicateFunction { .. }
| MutationSpec::DuplicateStruct { .. }
| MutationSpec::DuplicateEnum { .. }
| MutationSpec::DuplicateModTree { .. }
| MutationSpec::NoOpArmToTodo { .. } => {
}
}
Ok(())
}
pub fn len(&self) -> usize {
self.converters.len()
}
pub fn is_empty(&self) -> bool {
self.converters.is_empty()
}
pub fn registered_kinds(&self) -> Vec<&'static str> {
self.converters.keys().copied().collect()
}
}
impl Default for MutationRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_has_all_converters() {
let registry = MutationRegistry::new();
assert!(!registry.is_empty());
assert_eq!(registry.len(), 47);
}
#[test]
fn test_registry_can_handle_rename() {
use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
let registry = MutationRegistry::new();
let mut sym_registry = SymbolRegistry::new();
let path = SymbolPath::parse("test_crate::old").unwrap();
let symbol_id = sym_registry.register(path, SymbolKind::Function).unwrap();
let spec = MutationSpec::Rename {
target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
to: "new".into(),
scope: crate::executor::spec::Scope::Project,
};
assert!(registry.can_handle(&spec));
}
#[test]
fn test_registry_can_handle_field() {
use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
let registry = MutationRegistry::new();
let mut sym_registry = SymbolRegistry::new();
let path = SymbolPath::parse("test_crate::Config").unwrap();
let symbol_id = sym_registry.register(path, SymbolKind::Struct).unwrap();
let add_spec = MutationSpec::AddField {
target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
field_name: "timeout".into(),
field_type: "u64".into(),
visibility: crate::executor::spec::Visibility::Pub,
};
assert!(registry.can_handle(&add_spec));
let remove_spec = MutationSpec::RemoveField {
target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
field_name: "timeout".into(),
};
assert!(registry.can_handle(&remove_spec));
}
#[test]
fn test_registry_can_handle_add_item() {
let registry = MutationRegistry::new();
let spec = MutationSpec::AddItem {
target: crate::executor::spec::MutationTargetSymbol::ByPath(Box::new(
crate::executor::spec::SymbolPath::parse("test_crate::lib").unwrap(),
)),
content: "struct Foo {}".into(),
position: crate::executor::spec::InsertPosition::Top,
};
assert!(registry.can_handle(&spec));
}
#[test]
fn test_registry_registered_kinds() {
let registry = MutationRegistry::new();
let kinds = registry.registered_kinds();
assert!(kinds.contains(&"Rename"));
assert!(kinds.contains(&"AddField"));
assert!(kinds.contains(&"RemoveField"));
assert!(kinds.contains(&"ChangeVisibility"));
assert!(kinds.contains(&"AddDerive"));
assert!(kinds.contains(&"RemoveDerive"));
}
#[test]
fn test_all_converter_spec_kinds_are_registered() {
use std::collections::HashSet;
let registry = MutationRegistry::new();
let registered: HashSet<&str> = registry.registered_kinds().into_iter().collect();
let idiom = converters::IdiomConverter::new();
for kind in idiom.spec_kinds() {
assert!(
registered.contains(kind),
"IdiomConverter::spec_kinds() contains '{}' but NOT registered in MutationRegistry. \
Add: registry.register(\"{}\", Box::new(converters::IdiomConverter::new()));",
kind, kind
);
}
let rename = converters::RenameConverter::new();
for kind in rename.spec_kinds() {
assert!(
registered.contains(kind),
"RenameConverter::spec_kinds() contains '{}' but NOT registered",
kind
);
}
let field = converters::FieldConverter::new();
for kind in field.spec_kinds() {
assert!(
registered.contains(kind),
"FieldConverter::spec_kinds() contains '{}' but NOT registered",
kind
);
}
let enum_conv = converters::EnumConverter::new();
for kind in enum_conv.spec_kinds() {
assert!(
registered.contains(kind),
"EnumConverter::spec_kinds() contains '{}' but NOT registered",
kind
);
}
let module = converters::ModuleConverter::new();
for kind in module.spec_kinds() {
assert!(
registered.contains(kind),
"ModuleConverter::spec_kinds() contains '{}' but NOT registered",
kind
);
}
let stmt = converters::StmtConverter::new();
for kind in stmt.spec_kinds() {
assert!(
registered.contains(kind),
"StmtConverter::spec_kinds() contains '{}' but NOT registered",
kind
);
}
let dup = converters::DuplicateConverter::new();
for kind in dup.spec_kinds() {
assert!(
registered.contains(kind),
"DuplicateConverter::spec_kinds() contains '{}' but NOT registered",
kind
);
}
}
}
#[cfg(test)]
mod tests_pre_check {
use super::*;
use ryo_analysis::testing::ContextBuilder;
use ryo_source::pure::{
PureEnum, PureField, PureFields, PureFile, PureItem, PureStruct, PureType, PureVariant,
PureVis,
};
fn make_test_file_with_struct(struct_name: &str, fields: &[(&str, &str)]) -> PureFile {
let pure_fields = fields
.iter()
.map(|(name, ty)| PureField {
name: name.to_string(),
ty: PureType::Path(ty.to_string()),
attrs: vec![],
vis: PureVis::Public,
})
.collect();
PureFile {
attrs: vec![],
items: vec![PureItem::Struct(PureStruct {
name: struct_name.to_string(),
vis: PureVis::Public,
generics: Default::default(),
fields: PureFields::Named(pure_fields),
attrs: vec![],
})],
}
}
fn make_test_file_with_enum(enum_name: &str, variants: &[&str]) -> PureFile {
let pure_variants = variants
.iter()
.map(|name| PureVariant {
name: name.to_string(),
attrs: vec![],
fields: PureFields::Unit,
discriminant: None,
})
.collect();
PureFile {
attrs: vec![],
items: vec![PureItem::Enum(PureEnum {
name: enum_name.to_string(),
vis: PureVis::Public,
generics: Default::default(),
variants: pure_variants,
attrs: vec![],
})],
}
}
#[test]
fn test_pre_check_rename_with_symbol_id() {
use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
let registry = MutationRegistry::new();
let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
let ctx = ContextBuilder::new()
.with_pure_file("src/lib.rs", file)
.build();
let mut symbol_registry = SymbolRegistry::new();
let path = SymbolPath::parse("test_crate::Config").unwrap();
let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
let spec = MutationSpec::Rename {
target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
to: "Settings".into(),
scope: crate::executor::spec::Scope::Project,
};
let result = registry.pre_check(&spec, &ctx);
assert!(
result.is_ok(),
"Pre-check should always pass for Rename with symbol_id: {:?}",
result
);
}
#[test]
fn test_pre_check_add_field_with_symbol_id() {
use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
let mutation_registry = MutationRegistry::new();
let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
let mut symbol_registry = SymbolRegistry::new();
let path = SymbolPath::parse("test_crate::Config").unwrap();
let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
let ctx = ContextBuilder::new()
.with_pure_file("src/lib.rs", file)
.build();
let spec = MutationSpec::AddField {
target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
field_name: "name".into(),
field_type: "String".into(),
visibility: crate::executor::spec::Visibility::Pub,
};
let result = mutation_registry.pre_check(&spec, &ctx);
assert!(
result.is_ok(),
"Pre-check should always pass for AddField with required symbol_id: {:?}",
result
);
}
#[test]
fn test_pre_check_add_variant_always_passes() {
let registry = MutationRegistry::new();
let file = make_test_file_with_enum("Status", &["Active", "Inactive"]);
let ctx = ContextBuilder::new()
.with_pure_file("src/lib.rs", file)
.build();
let enum_id = ctx
.registry()
.iter()
.find(|(_, path)| path.name() == "Status")
.map(|(id, _)| id)
.expect("Status enum should exist");
let spec = MutationSpec::AddVariant {
target: crate::executor::spec::MutationTargetSymbol::ById(enum_id),
variant_name: "Pending".into(),
variant_kind: crate::executor::spec::VariantKind::Unit,
};
let result = registry.pre_check(&spec, &ctx);
assert!(
result.is_ok(),
"Pre-check should always pass for AddVariant with required symbol_id: {:?}",
result
);
}
#[test]
fn test_pre_check_add_derive_with_symbol_id() {
use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
let registry = MutationRegistry::new();
let file = make_test_file_with_struct("Config", &[("timeout", "u64")]);
let ctx = ContextBuilder::new()
.with_pure_file("src/lib.rs", file)
.build();
let mut symbol_registry = SymbolRegistry::new();
let path = SymbolPath::parse("test_crate::Config").unwrap();
let symbol_id = symbol_registry.register(path, SymbolKind::Struct).unwrap();
let spec = MutationSpec::AddDerive {
target: crate::executor::spec::MutationTargetSymbol::ById(symbol_id),
derives: vec!["Debug".into(), "Clone".into()],
};
let result = registry.pre_check(&spec, &ctx);
assert!(
result.is_ok(),
"Pre-check should always pass for AddDerive with symbol_id: {:?}",
result
);
}
#[test]
fn test_pre_check_add_item_no_check_needed() {
let registry = MutationRegistry::new();
let file = make_test_file_with_struct("Config", &[]);
let ctx = ContextBuilder::new()
.with_pure_file("src/lib.rs", file)
.build();
let spec = MutationSpec::AddItem {
target: crate::executor::spec::MutationTargetSymbol::ByPath(Box::new(
crate::executor::spec::SymbolPath::parse("test_crate").unwrap(),
)),
content: "struct NewStruct {}".into(),
position: crate::executor::spec::InsertPosition::Bottom,
};
let result = registry.pre_check(&spec, &ctx);
assert!(result.is_ok(), "AddItem should not require pre-check");
}
}