use serde::{Deserialize, Serialize};
pub use ryo_analysis::{SymbolId, SymbolPath};
pub use ryo_mutations::{EnumToTraitStrategy, MatchHandling};
pub use ryo_source::ItemKind;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MutationTargetSymbol {
ById(SymbolId),
ByPath(Box<SymbolPath>),
ByKindAndName(ItemKind, String),
ByAffectedId {
parent_id: SymbolId,
kind: ItemKind,
name: Option<String>,
},
}
impl MutationTargetSymbol {
pub fn by_id(id: SymbolId) -> Self {
Self::ById(id)
}
pub fn by_path(path: SymbolPath) -> Self {
Self::ByPath(Box::new(path))
}
pub fn by_kind_and_name(kind: ItemKind, name: impl Into<String>) -> Self {
Self::ByKindAndName(kind, name.into())
}
pub fn by_affected_id(parent_id: SymbolId, kind: ItemKind, name: Option<String>) -> Self {
Self::ByAffectedId {
parent_id,
kind,
name,
}
}
pub fn is_resolved(&self) -> bool {
matches!(self, Self::ById(_))
}
pub fn to_path(&self, registry: &ryo_symbol::SymbolRegistry) -> Option<SymbolPath> {
match self {
Self::ById(id) => registry.resolve(*id).cloned(),
Self::ByPath(path) => Some(*path.clone()),
Self::ByKindAndName(_, name) => SymbolPath::parse(name).ok(),
Self::ByAffectedId { parent_id, .. } => registry.resolve(*parent_id).cloned(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TypeTransform {
BoxDyn {
trait_name: String,
},
ImplTrait {
trait_name: String,
},
Generic {
param_name: String,
bound: String,
},
Literal(String),
}
impl TypeTransform {
pub fn box_dyn(trait_name: impl Into<String>) -> Self {
Self::BoxDyn {
trait_name: trait_name.into(),
}
}
pub fn impl_trait(trait_name: impl Into<String>) -> Self {
Self::ImplTrait {
trait_name: trait_name.into(),
}
}
pub fn generic(param_name: impl Into<String>, bound: impl Into<String>) -> Self {
Self::Generic {
param_name: param_name.into(),
bound: bound.into(),
}
}
pub fn literal(type_str: impl Into<String>) -> Self {
Self::Literal(type_str.into())
}
pub fn to_type_string(&self) -> String {
match self {
Self::BoxDyn { trait_name } => format!("Box<dyn {}>", trait_name),
Self::ImplTrait { trait_name } => format!("impl {}", trait_name),
Self::Generic { param_name, .. } => param_name.clone(),
Self::Literal(s) => s.clone(),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TypeContext {
Parameter,
ReturnType,
Field,
LocalVar,
TraitBound,
ImplTarget,
GenericArg,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum MutationSpec {
Rename {
target: MutationTargetSymbol,
to: String,
#[serde(default)]
scope: Scope,
},
AddField {
target: MutationTargetSymbol,
field_name: String,
field_type: String,
#[serde(default)]
visibility: Visibility,
},
RemoveField {
target: MutationTargetSymbol,
field_name: String,
},
ChangeVisibility {
target: MutationTargetSymbol,
visibility: Visibility,
},
AddDerive {
target: MutationTargetSymbol,
derives: Vec<String>,
},
RemoveDerive {
target: MutationTargetSymbol,
derives: Vec<String>,
},
AddVariant {
target: MutationTargetSymbol,
variant_name: String,
#[serde(default)]
variant_kind: VariantKind,
},
RemoveVariant {
target: MutationTargetSymbol,
variant_name: String,
},
AddMatchArm {
target: MutationTargetSymbol,
enum_name: String,
pattern: String,
body: String,
},
RemoveMatchArm {
target: MutationTargetSymbol,
enum_name: String,
pattern: String,
},
ReplaceMatchArm {
target: MutationTargetSymbol,
enum_name: String,
old_pattern: String,
new_pattern: String,
new_body: String,
},
AddStructLiteralField {
target: MutationTargetSymbol,
field_name: String,
value: String,
},
RemoveStructLiteralField {
target: MutationTargetSymbol,
field_name: String,
},
AddItem {
target: MutationTargetSymbol,
content: String,
#[serde(default)]
position: InsertPosition,
},
RemoveItem {
target: MutationTargetSymbol,
item_kind: ItemKind,
},
AddSpec {
type_id: SymbolId,
module_id: SymbolId,
group: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
alias_name: Option<String>,
#[serde(default)]
relations: Vec<SpecRelation>,
},
RemoveSpec {
type_id: SymbolId,
module_id: SymbolId,
},
ValidateSpec {
type_ids: Vec<SymbolId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
expected_group: Option<String>,
#[serde(default = "default_true")]
validate_relations: bool,
},
AddMethod {
target: MutationTargetSymbol,
method_name: String,
#[serde(default)]
params: Vec<(String, String)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
return_type: Option<String>,
#[serde(default = "default_body")]
body: String,
#[serde(default)]
is_pub: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
self_param: Option<SelfParam>,
},
RemoveMethod {
target: MutationTargetSymbol,
method_name: String,
},
RemoveMod {
target: MutationTargetSymbol,
mod_name: String,
},
CreateMod {
target: MutationTargetSymbol,
mod_name: String,
#[serde(default)]
content: String,
#[serde(default)]
is_pub: bool,
},
OrganizeImports {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, #[serde(default = "default_true")]
deduplicate: bool,
#[serde(default = "default_true")]
merge_groups: bool,
},
LoopToIterator {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, target_var: Option<String>,
},
UnwrapToQuestion {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, #[serde(default)]
target_fn: Option<SymbolId>,
#[serde(default = "default_true")]
include_expect: bool,
},
AssignOp {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, #[serde(default)]
fn_id: Option<SymbolId>,
},
BoolSimplify {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, },
CloneOnCopy {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, },
CollapsibleIf {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, },
NoOpArmToTodo {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, #[serde(default = "default_noop_replacement")]
replacement: String,
},
ComparisonToMethod {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, },
RedundantClosure {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, },
IntroduceVariable {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, #[serde(default)]
fn_id: Option<SymbolId>,
expr: String,
var_name: String,
},
ManualMap {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, },
MatchToIfLet {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>,
},
FilterNext {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, #[serde(default, skip_serializing_if = "Option::is_none")]
fn_id: Option<SymbolId>,
},
MapUnwrapOr {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, #[serde(default, skip_serializing_if = "Option::is_none")]
fn_id: Option<SymbolId>,
},
ReplaceExpr {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, #[serde(default)]
fn_id: Option<SymbolId>,
old_expr: String,
new_expr: String,
#[serde(default = "default_true")]
replace_all: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
},
RemoveStatement {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>, #[serde(default)]
fn_id: Option<SymbolId>,
pattern: String,
#[serde(default = "default_true")]
remove_all: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
},
InsertStatement {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>,
#[serde(default)]
fn_id: SymbolId,
stmt: String,
#[serde(default)]
position: StmtInsertPosition,
#[serde(default, skip_serializing_if = "Option::is_none")]
reference_pattern: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
},
ReplaceStatement {
#[serde(default, skip_serializing_if = "Option::is_none")]
module_id: Option<SymbolId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
fn_id: Option<SymbolId>,
old_stmt: String,
new_stmt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
symbol_path: Option<String>,
},
ExtractTrait {
target: MutationTargetSymbol,
trait_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
methods: Option<Vec<String>>,
},
InlineTrait {
target: MutationTargetSymbol,
struct_name: String,
#[serde(default = "default_true")]
remove_trait: bool,
},
ReplaceType {
target: MutationTargetSymbol,
to_type: TypeTransform,
#[serde(default, skip_serializing_if = "Option::is_none")]
scope: Option<SymbolPath>,
#[serde(default, skip_serializing_if = "Option::is_none")]
contexts: Option<Vec<TypeContext>>,
},
EnumToTrait {
target: MutationTargetSymbol,
#[serde(default, skip_serializing_if = "Option::is_none")]
trait_name: Option<String>,
#[serde(default = "default_true")]
remove_enum: bool,
#[serde(default)]
strategy: EnumToTraitStrategy,
#[serde(default)]
match_handling: MatchHandling,
},
MoveItem {
source: MutationTargetSymbol,
target: MutationTargetSymbol,
item_name: String,
item_kind: ItemKind,
#[serde(default = "default_true")]
add_use: bool,
},
PluginTransform {
plugin_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
target_id: Option<SymbolId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
file_patterns: Vec<String>,
#[serde(default)]
config: serde_json::Value,
},
DuplicateFunction {
target: MutationTargetSymbol,
to: String,
},
DuplicateStruct {
target: MutationTargetSymbol,
to: String,
#[serde(default = "default_true")]
include_impls: bool,
},
DuplicateEnum {
target: MutationTargetSymbol,
to: String,
#[serde(default = "default_true")]
include_impls: bool,
},
DuplicateModTree {
target: MutationTargetSymbol,
to: String,
},
}
fn default_true() -> bool {
true
}
impl MutationSpec {
pub fn kind_name(&self) -> &'static str {
match self {
Self::Rename { .. } => "Rename",
Self::AddField { .. } => "AddField",
Self::RemoveField { .. } => "RemoveField",
Self::ChangeVisibility { .. } => "ChangeVisibility",
Self::AddDerive { .. } => "AddDerive",
Self::RemoveDerive { .. } => "RemoveDerive",
Self::AddVariant { .. } => "AddVariant",
Self::RemoveVariant { .. } => "RemoveVariant",
Self::AddMatchArm { .. } => "AddMatchArm",
Self::RemoveMatchArm { .. } => "RemoveMatchArm",
Self::ReplaceMatchArm { .. } => "ReplaceMatchArm",
Self::AddStructLiteralField { .. } => "AddStructLiteralField",
Self::RemoveStructLiteralField { .. } => "RemoveStructLiteralField",
Self::AddItem { .. } => "AddItem",
Self::RemoveItem { .. } => "RemoveItem",
Self::AddSpec { .. } => "AddSpec",
Self::RemoveSpec { .. } => "RemoveSpec",
Self::ValidateSpec { .. } => "ValidateSpec",
Self::AddMethod { .. } => "AddMethod",
Self::RemoveMethod { .. } => "RemoveMethod",
Self::RemoveMod { .. } => "RemoveMod",
Self::CreateMod { .. } => "CreateMod",
Self::OrganizeImports { .. } => "OrganizeImports",
Self::LoopToIterator { .. } => "LoopToIterator",
Self::UnwrapToQuestion { .. } => "UnwrapToQuestion",
Self::AssignOp { .. } => "AssignOp",
Self::BoolSimplify { .. } => "BoolSimplify",
Self::CloneOnCopy { .. } => "CloneOnCopy",
Self::CollapsibleIf { .. } => "CollapsibleIf",
Self::NoOpArmToTodo { .. } => "NoOpArmToTodo",
Self::ComparisonToMethod { .. } => "ComparisonToMethod",
Self::RedundantClosure { .. } => "RedundantClosure",
Self::IntroduceVariable { .. } => "IntroduceVariable",
Self::ManualMap { .. } => "ManualMap",
Self::MatchToIfLet { .. } => "MatchToIfLet",
Self::FilterNext { .. } => "FilterNext",
Self::MapUnwrapOr { .. } => "MapUnwrapOr",
Self::ReplaceExpr { .. } => "ReplaceExpr",
Self::RemoveStatement { .. } => "RemoveStatement",
Self::InsertStatement { .. } => "InsertStatement",
Self::ReplaceStatement { .. } => "ReplaceStatement",
Self::ExtractTrait { .. } => "ExtractTrait",
Self::InlineTrait { .. } => "InlineTrait",
Self::ReplaceType { .. } => "ReplaceType",
Self::EnumToTrait { .. } => "EnumToTrait",
Self::MoveItem { .. } => "MoveItem",
Self::PluginTransform { .. } => "PluginTransform",
Self::DuplicateFunction { .. } => "DuplicateFunction",
Self::DuplicateStruct { .. } => "DuplicateStruct",
Self::DuplicateEnum { .. } => "DuplicateEnum",
Self::DuplicateModTree { .. } => "DuplicateModTree",
}
}
pub fn is_rename(&self) -> bool {
matches!(self, Self::Rename { .. })
}
pub fn is_additive(&self) -> bool {
matches!(
self,
Self::AddItem { .. }
| Self::AddMethod { .. }
| Self::AddField { .. }
| Self::AddVariant { .. }
| Self::AddDerive { .. }
| Self::CreateMod { .. }
| Self::AddMatchArm { .. }
| Self::AddStructLiteralField { .. }
| Self::AddSpec { .. }
)
}
pub fn additive_identity(&self) -> Option<String> {
match self {
Self::AddItem { content, .. } => {
extract_item_name_from_content(content)
}
Self::AddMethod { method_name, .. } => Some(method_name.clone()),
Self::AddField { field_name, .. } => Some(field_name.clone()),
Self::AddVariant { variant_name, .. } => Some(variant_name.clone()),
Self::AddDerive { derives, .. } => Some(derives.join(",")),
Self::CreateMod { mod_name, .. } => Some(mod_name.clone()),
Self::AddMatchArm { pattern, .. } => Some(pattern.clone()),
Self::AddStructLiteralField { field_name, .. } => Some(field_name.clone()),
Self::AddSpec { type_id, .. } => Some(format!("{:?}", type_id)),
_ => None,
}
}
pub fn is_idiom(&self) -> bool {
matches!(
self,
Self::OrganizeImports { .. }
| Self::LoopToIterator { .. }
| Self::UnwrapToQuestion { .. }
| Self::AssignOp { .. }
| Self::BoolSimplify { .. }
| Self::CloneOnCopy { .. }
| Self::CollapsibleIf { .. }
| Self::NoOpArmToTodo { .. }
| Self::ComparisonToMethod { .. }
| Self::RedundantClosure { .. }
| Self::IntroduceVariable { .. }
| Self::ManualMap { .. }
| Self::MatchToIfLet { .. }
| Self::FilterNext { .. }
| Self::MapUnwrapOr { .. }
| Self::ReplaceExpr { .. }
| Self::RemoveStatement { .. }
| Self::InsertStatement { .. }
| Self::ReplaceStatement { .. }
| Self::PluginTransform { .. }
)
}
pub fn get_targets(&self) -> Vec<&MutationTargetSymbol> {
match self {
Self::Rename { target, .. }
| Self::AddField { target, .. }
| Self::RemoveField { target, .. }
| Self::ChangeVisibility { target, .. }
| Self::AddDerive { target, .. }
| Self::RemoveDerive { target, .. }
| Self::AddVariant { target, .. }
| Self::RemoveVariant { target, .. }
| Self::AddMatchArm { target, .. }
| Self::RemoveMatchArm { target, .. }
| Self::ReplaceMatchArm { target, .. }
| Self::AddStructLiteralField { target, .. }
| Self::RemoveStructLiteralField { target, .. }
| Self::AddItem { target, .. }
| Self::RemoveItem { target, .. }
| Self::AddMethod { target, .. }
| Self::RemoveMethod { target, .. }
| Self::RemoveMod { target, .. }
| Self::CreateMod { target, .. }
| Self::ExtractTrait { target, .. }
| Self::InlineTrait { target, .. }
| Self::ReplaceType { target, .. }
| Self::EnumToTrait { target, .. }
| Self::MoveItem { target, .. }
| Self::DuplicateFunction { target, .. }
| Self::DuplicateStruct { target, .. }
| Self::DuplicateEnum { target, .. }
| Self::DuplicateModTree { target, .. } => vec![target],
Self::AddSpec { .. } | Self::RemoveSpec { .. } | Self::ValidateSpec { .. } => vec![],
Self::OrganizeImports { .. }
| Self::LoopToIterator { .. }
| Self::UnwrapToQuestion { .. }
| Self::AssignOp { .. }
| Self::BoolSimplify { .. }
| Self::CloneOnCopy { .. }
| Self::CollapsibleIf { .. }
| Self::NoOpArmToTodo { .. }
| Self::ComparisonToMethod { .. }
| Self::RedundantClosure { .. }
| Self::IntroduceVariable { .. }
| Self::ManualMap { .. }
| Self::MatchToIfLet { .. }
| Self::FilterNext { .. }
| Self::MapUnwrapOr { .. }
| Self::ReplaceExpr { .. }
| Self::RemoveStatement { .. }
| Self::InsertStatement { .. }
| Self::ReplaceStatement { .. }
| Self::PluginTransform { .. } => vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(tag = "type")]
pub enum Scope {
#[default]
Project,
Mod { path: SymbolPath },
Item {
target: SymbolPath,
item_name: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
#[default]
Private,
Pub,
PubCrate,
PubSuper,
PubIn(String),
}
impl Visibility {
pub fn to_rust_syntax(&self) -> &str {
match self {
Self::Private => "",
Self::Pub => "pub ",
Self::PubCrate => "pub(crate) ",
Self::PubSuper => "pub(super) ",
Self::PubIn(_) => "pub(in ...) ", }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(tag = "type")]
pub enum VariantKind {
#[default]
Unit,
Tuple {
types: Vec<String>,
},
Struct {
fields: Vec<(String, String)>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(tag = "type")]
pub enum InsertPosition {
#[default]
Top,
Bottom,
AfterItem {
name: String,
},
BeforeItem {
name: String,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum StmtInsertPosition {
Start,
#[default]
End,
BeforePattern,
AfterPattern,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SelfParam {
Ref,
Mut,
Owned,
}
fn default_body() -> String {
"todo!()".to_string()
}
fn default_noop_replacement() -> String {
"todo".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpecRelation {
pub kind: SpecRelationKind,
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_id: Option<SymbolId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_path: Option<SymbolPath>,
}
impl SpecRelation {
pub fn new(kind: SpecRelationKind, target: impl Into<String>) -> Self {
Self {
kind,
target: target.into(),
symbol_id: None,
target_path: None,
}
}
pub fn with_path(kind: SpecRelationKind, target: impl Into<String>, path: SymbolPath) -> Self {
Self {
kind,
target: target.into(),
symbol_id: None,
target_path: Some(path),
}
}
pub fn with_symbol_id(
kind: SpecRelationKind,
target: impl Into<String>,
symbol_id: SymbolId,
) -> Self {
Self {
kind,
target: target.into(),
symbol_id: Some(symbol_id),
target_path: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub enum SpecRelationKind {
DependsOn,
RelatedTo,
PartOf,
}
impl SpecRelationKind {
pub fn as_type_name(&self) -> &'static str {
match self {
Self::DependsOn => "DependsOn",
Self::RelatedTo => "RelatedTo",
Self::PartOf => "PartOf",
}
}
}
fn extract_item_name_from_content(content: &str) -> Option<String> {
let trimmed = content.trim();
let mut lines = trimmed.lines();
let mut decl_line = "";
for line in lines.by_ref() {
let line = line.trim();
if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
decl_line = line;
break;
}
}
let tokens: Vec<&str> = decl_line.split_whitespace().collect();
if tokens.is_empty() {
return None;
}
let mut idx = 0;
if tokens.get(idx) == Some(&"pub") {
idx += 1;
if let Some(t) = tokens.get(idx) {
if t.starts_with('(') {
idx += 1;
}
}
}
let keyword = tokens.get(idx)?;
idx += 1;
match *keyword {
"struct" | "enum" | "fn" | "type" | "const" | "static" | "trait" | "mod" => {
let name = tokens.get(idx)?;
let name = name.split('<').next().unwrap_or(name);
let name = name.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
Some(name.to_string())
}
"impl" => {
let rest = tokens[idx..].join(" ");
let impl_sig = rest
.split(['{', '<'])
.next()
.map(|s| s.trim())
.filter(|s| !s.is_empty());
let methods: Vec<&str> = content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if trimmed.starts_with("pub fn ") || trimmed.starts_with("fn ") {
let after_fn = trimmed
.strip_prefix("pub fn ")
.or_else(|| trimmed.strip_prefix("fn "))?;
let method_name = after_fn.split('(').next()?.trim();
Some(method_name)
} else {
None
}
})
.collect();
match (impl_sig, methods.is_empty()) {
(Some(sig), false) => Some(format!(
"impl_{}::{}",
sig.replace(' ', "_"),
methods.join(",")
)),
(Some(sig), true) => Some(format!("impl_{}", sig.replace(' ', "_"))),
_ => None,
}
}
"use" => {
Some(tokens[idx..].join(" "))
}
_ => {
Some(keyword.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mutation_spec_serialize() {
let spec = MutationSpec::Rename {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::old_name").unwrap(),
)),
to: "new_name".to_string(),
scope: Scope::Project,
};
let json = serde_json::to_string_pretty(&spec).unwrap();
assert!(json.contains("Rename"), "JSON should contain Rename");
assert!(
json.contains("old_name"),
"JSON should contain old_name in ByPath"
);
assert!(json.contains("new_name"), "JSON should contain new_name");
let parsed: MutationSpec = serde_json::from_str(&json).unwrap();
assert_eq!(
spec, parsed,
"Round-trip serialization should preserve spec"
);
}
#[test]
fn test_idiom_detection() {
let spec = MutationSpec::OrganizeImports {
module_id: None,
deduplicate: true,
merge_groups: true,
};
assert!(spec.is_idiom());
assert!(!spec.is_rename());
}
}