ryo_executor/executor/spec.rs
1//! MutationSpec: Atomic, serializable mutation specifications
2//!
3//! # Architecture: Intent vs MutationSpec
4//!
5//! Ryo has a two-layer mutation system:
6//!
7//! ```text
8//! ┌─────────────────────────────────────────────────────────────────┐
9//! │ Intent (ryo-app::intent) │
10//! │ - Public DSL for CLI users │
11//! │ - High-level, uses Pattern matching │
12//! │ - One Intent may expand to multiple MutationSpecs │
13//! │ - Example: AddField { target: Pattern::Glob("*Config"), ... } │
14//! └───────────────────────────┬─────────────────────────────────────┘
15//! ↓ Resolution & Expansion
16//! ┌─────────────────────────────────────────────────────────────────┐
17//! │ MutationSpec (this module) │
18//! │ - Execution-level specification │
19//! │ - Concrete targets (SymbolId, exact names) │
20//! │ - Atomic: one spec = one mutation │
21//! │ - Example: AddField { struct_name: "AppConfig", ... } │
22//! └───────────────────────────┬─────────────────────────────────────┘
23//! ↓ Execution
24//! ┌─────────────────────────────────────────────────────────────────┐
25//! │ AST Mutation │
26//! │ - Actual code transformation │
27//! └─────────────────────────────────────────────────────────────────┘
28//! ```
29//!
30//! ## Why Two Layers?
31//!
32//! - **Intent**: User-friendly, pattern-based, requires symbol resolution
33//! - **MutationSpec**: Machine-friendly, direct targets, ready for execution
34//!
35//! This separation allows:
36//! 1. CLI users to use high-level patterns (`*Config`)
37//! 2. `Suggest` system to generate specs directly (bypassing Intent)
38//! 3. Clear conflict detection at the MutationSpec level
39//!
40//! ## Usage by Suggest
41//!
42//! The `Suggest` trait (in `ryo-suggest`) generates `MutationSpec` directly:
43//! - Detects opportunities from analyzed code
44//! - Converts opportunities to `MutationSpec` via `to_mutation_specs()`
45//! - Bypasses Intent layer for efficiency (no pattern resolution needed)
46//!
47//! See `ryo_suggest::suggest::Suggest` for the trait definition.
48//!
49//! # Design Goals
50//!
51//! Designed for:
52//! - LLM-friendly: Can be generated/selected by lightweight LLMs
53//! - Declarative: Pure data, no behavior
54//! - Composable: Multiple specs form a ParallelBlueprint
55//!
56//! ## Scope: Single Crate + Multi-Module
57//!
58//! MutationSpec operates within a **single crate** (MonoCrate model).
59//! Multi-crate workspace operations are **NOT SUPPORTED**:
60//! - No cross-crate MoveItem
61//! - No Cargo.toml manipulation (requires TOML parser, not AST)
62//! - Use external tools for workspace-level refactoring
63//!
64//! ## Target Resolution
65//!
66//! All targeting uses `SymbolPath` (e.g., "crate::config::Settings").
67//! SymbolPath provides:
68//! - AST-based resolution
69//! - Fine-grained conflict detection
70//! - Type-safe path operations
71
72use serde::{Deserialize, Serialize};
73
74pub use ryo_analysis::{SymbolId, SymbolPath};
75pub use ryo_mutations::{EnumToTraitStrategy, MatchHandling};
76pub use ryo_source::ItemKind;
77
78/// Target symbol specification for MutationSpec.
79///
80/// Supports flexible target resolution:
81/// - Eager: Already resolved to SymbolId
82/// - Lazy: Resolved during Wave execution (DetectConflict phase)
83/// - Derived: Resolved from parent mutations (e.g., newly added struct)
84///
85/// # Design
86///
87/// Replaces the scattered `request_*` fields with a unified approach.
88/// Enables batch processing of Add & Update operations within a Wave.
89///
90/// # Examples
91///
92/// ```text
93/// ById(symbol_id) // Direct reference (already resolved)
94/// ByPath("crate::config::Settings") // Lazy resolution by path
95/// ByKindAndName(Struct, "User") // Lazy resolution by kind + name
96/// ByAffectedId(parent_id, Field, "id") // Derived from parent (e.g., field in newly added struct)
97/// ```
98#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
99pub enum MutationTargetSymbol {
100 /// Direct reference by SymbolId (already resolved)
101 ById(SymbolId),
102 /// Lazy resolution by SymbolPath (parsed path)
103 ByPath(Box<SymbolPath>),
104 /// Lazy resolution by kind and name
105 ByKindAndName(ItemKind, String),
106 /// Derived from affected parent symbol
107 /// Example: Field in a struct that was just added in the same Wave
108 ByAffectedId {
109 /// Parent symbol ID
110 parent_id: SymbolId,
111 /// Kind of the child item
112 kind: ItemKind,
113 /// Optional name (None for anonymous items)
114 name: Option<String>,
115 },
116}
117
118impl MutationTargetSymbol {
119 /// Create a direct SymbolId reference
120 pub fn by_id(id: SymbolId) -> Self {
121 Self::ById(id)
122 }
123
124 /// Create a lazy SymbolPath reference
125 pub fn by_path(path: SymbolPath) -> Self {
126 Self::ByPath(Box::new(path))
127 }
128
129 /// Create a lazy kind+name reference
130 pub fn by_kind_and_name(kind: ItemKind, name: impl Into<String>) -> Self {
131 Self::ByKindAndName(kind, name.into())
132 }
133
134 /// Create a derived reference from parent
135 pub fn by_affected_id(parent_id: SymbolId, kind: ItemKind, name: Option<String>) -> Self {
136 Self::ByAffectedId {
137 parent_id,
138 kind,
139 name,
140 }
141 }
142
143 /// Check if this is already resolved to a SymbolId
144 pub fn is_resolved(&self) -> bool {
145 matches!(self, Self::ById(_))
146 }
147
148 /// Resolve to SymbolPath using the registry.
149 ///
150 /// Returns `Some(SymbolPath)` if resolution succeeds, `None` otherwise.
151 pub fn to_path(&self, registry: &ryo_symbol::SymbolRegistry) -> Option<SymbolPath> {
152 match self {
153 Self::ById(id) => registry.resolve(*id).cloned(),
154 Self::ByPath(path) => Some(*path.clone()),
155 Self::ByKindAndName(_, name) => SymbolPath::parse(name).ok(),
156 Self::ByAffectedId { parent_id, .. } => registry.resolve(*parent_id).cloned(),
157 }
158 }
159}
160
161// ============================================================================
162// Type Transformation Types (for ReplaceType and EnumToTrait)
163// ============================================================================
164
165/// Type transformation pattern for ReplaceType.
166///
167/// Specifies how to transform a type reference.
168///
169/// # Examples
170///
171/// ```ignore
172/// // Box<dyn Trait>
173/// TypeTransform::BoxDyn { trait_name: "Status".to_string() }
174///
175/// // impl Trait
176/// TypeTransform::ImplTrait { trait_name: "Status".to_string() }
177///
178/// // Generic: <T: Trait>
179/// TypeTransform::Generic { param_name: "S".to_string(), bound: "Status".to_string() }
180/// ```
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182pub enum TypeTransform {
183 /// Transform to `Box<dyn Trait>`
184 ///
185 /// Example: `Status` → `Box<dyn Status>`
186 BoxDyn {
187 /// The trait name to use
188 trait_name: String,
189 },
190
191 /// Transform to `impl Trait` (argument position) or `-> impl Trait` (return position)
192 ///
193 /// Example: `Status` → `impl Status`
194 ImplTrait {
195 /// The trait name to use
196 trait_name: String,
197 },
198
199 /// Transform to generic parameter with trait bound
200 ///
201 /// Example: `fn foo(s: Status)` → `fn foo<S: Status>(s: S)`
202 Generic {
203 /// Name of the generic parameter (e.g., "S", "T")
204 param_name: String,
205 /// Trait bound (e.g., "Status", "Status + Send")
206 bound: String,
207 },
208
209 /// Transform to a literal type string (escape hatch)
210 ///
211 /// Example: `Status` → `Arc<dyn Status + Send + Sync>`
212 Literal(String),
213}
214
215impl TypeTransform {
216 /// Create a BoxDyn transform
217 pub fn box_dyn(trait_name: impl Into<String>) -> Self {
218 Self::BoxDyn {
219 trait_name: trait_name.into(),
220 }
221 }
222
223 /// Create an ImplTrait transform
224 pub fn impl_trait(trait_name: impl Into<String>) -> Self {
225 Self::ImplTrait {
226 trait_name: trait_name.into(),
227 }
228 }
229
230 /// Create a Generic transform
231 pub fn generic(param_name: impl Into<String>, bound: impl Into<String>) -> Self {
232 Self::Generic {
233 param_name: param_name.into(),
234 bound: bound.into(),
235 }
236 }
237
238 /// Create a Literal transform
239 pub fn literal(type_str: impl Into<String>) -> Self {
240 Self::Literal(type_str.into())
241 }
242
243 /// Get the resulting type as a string representation
244 pub fn to_type_string(&self) -> String {
245 match self {
246 Self::BoxDyn { trait_name } => format!("Box<dyn {}>", trait_name),
247 Self::ImplTrait { trait_name } => format!("impl {}", trait_name),
248 Self::Generic { param_name, .. } => param_name.clone(),
249 Self::Literal(s) => s.clone(),
250 }
251 }
252}
253
254/// Context where a type is used.
255///
256/// Used to filter which type usages should be replaced.
257#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
258pub enum TypeContext {
259 /// Function parameter type
260 Parameter,
261 /// Function return type
262 ReturnType,
263 /// Struct/enum field type
264 Field,
265 /// Local variable type annotation
266 LocalVar,
267 /// Trait bound (e.g., `T: Status`)
268 TraitBound,
269 /// Impl target type (e.g., `impl Foo for Bar`)
270 ImplTarget,
271 /// Generic type argument (e.g., `Vec<Status>`)
272 GenericArg,
273}
274
275/// Atomic mutation specification
276#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
277#[serde(tag = "type")]
278pub enum MutationSpec {
279 // === Rename ===
280 /// Rename an identifier across scope
281 Rename {
282 /// Target symbol (supports lazy resolution)
283 target: MutationTargetSymbol,
284 /// New name to rename to
285 to: String,
286 #[serde(default)]
287 scope: Scope,
288 },
289
290 // === Struct/Field ===
291 /// Add a field to a struct
292 AddField {
293 /// Target struct (supports lazy resolution)
294 target: MutationTargetSymbol,
295 field_name: String,
296 field_type: String,
297 #[serde(default)]
298 visibility: Visibility,
299 },
300
301 /// Remove a field from a struct
302 RemoveField {
303 /// Target struct (supports lazy resolution)
304 target: MutationTargetSymbol,
305 field_name: String,
306 },
307
308 // === Visibility ===
309 /// Change visibility of an item or struct field
310 ChangeVisibility {
311 /// Target symbol (supports lazy resolution)
312 target: MutationTargetSymbol,
313 visibility: Visibility,
314 },
315
316 // === Derive ===
317 /// Add derive macros to a type
318 AddDerive {
319 /// Target type (supports lazy resolution)
320 target: MutationTargetSymbol,
321 derives: Vec<String>,
322 },
323
324 /// Remove derive macros from a type
325 RemoveDerive {
326 /// Target type (supports lazy resolution)
327 target: MutationTargetSymbol,
328 derives: Vec<String>,
329 },
330
331 // === Enum ===
332 /// Add a variant to an enum
333 AddVariant {
334 /// Target enum (supports lazy resolution)
335 target: MutationTargetSymbol,
336 variant_name: String,
337 #[serde(default)]
338 variant_kind: VariantKind,
339 },
340
341 /// Remove a variant from an enum
342 RemoveVariant {
343 /// Target enum (supports lazy resolution)
344 target: MutationTargetSymbol,
345 variant_name: String,
346 },
347
348 /// Add a match arm to a match expression
349 ///
350 /// Used to fix exhaustiveness errors when adding enum variants.
351 AddMatchArm {
352 /// Target function/method containing the match expression
353 target: MutationTargetSymbol,
354 /// Enum type being matched (for validation)
355 enum_name: String,
356 /// Pattern for the new arm (e.g., "Status::Cancelled")
357 pattern: String,
358 /// Body of the new arm (e.g., "todo!()")
359 body: String,
360 },
361
362 /// Remove a match arm from a match expression
363 ///
364 /// Used to remove arms when deleting enum variants.
365 RemoveMatchArm {
366 /// Target function/method containing the match expression
367 target: MutationTargetSymbol,
368 /// Enum type being matched (for validation)
369 enum_name: String,
370 /// Pattern to remove (e.g., "Status::Completed")
371 pattern: String,
372 },
373
374 /// Replace a match arm (pattern + body) in a match expression
375 ///
376 /// Unlike ReplaceExpr which only replaces the body, this replaces both
377 /// the pattern and body atomically. Useful when pattern bindings need
378 /// to change along with the body.
379 ReplaceMatchArm {
380 /// Target function/method containing the match expression
381 target: MutationTargetSymbol,
382 /// Enum type being matched (for validation)
383 enum_name: String,
384 /// Pattern to find and replace (e.g., "PathSegment::Slice { start: _, end: _ }")
385 old_pattern: String,
386 /// New pattern (e.g., "PathSegment::Slice { start, end }")
387 new_pattern: String,
388 /// New body expression
389 new_body: String,
390 },
391
392 /// Add a field to all struct literals of a given type
393 ///
394 /// Used to fix missing field errors when adding struct fields.
395 AddStructLiteralField {
396 /// Target struct (supports lazy resolution)
397 target: MutationTargetSymbol,
398 /// Field name to add
399 field_name: String,
400 /// Value expression (e.g., "None", "Default::default()")
401 value: String,
402 },
403
404 /// Remove a field from all struct literals of a given type
405 ///
406 /// Used to update struct literals when removing struct fields.
407 RemoveStructLiteralField {
408 /// Target struct (supports lazy resolution)
409 target: MutationTargetSymbol,
410 /// Field name to remove
411 field_name: String,
412 },
413
414 // === Items ===
415 /// Add an item (struct, fn, impl, etc.)
416 AddItem {
417 /// Target module (supports lazy resolution)
418 target: MutationTargetSymbol,
419 /// Item content (Rust code)
420 content: String,
421 /// Insert position within the module
422 #[serde(default)]
423 position: InsertPosition,
424 },
425
426 /// Remove an item
427 RemoveItem {
428 /// Target item (supports lazy resolution)
429 target: MutationTargetSymbol,
430 item_kind: ItemKind,
431 },
432
433 // === Spec ===
434 /// Add a Spec TypeAlias (Spec<Group, T> or SpecWith<Group, R, T>)
435 AddSpec {
436 /// SymbolId of the target type (required, O(1) lookup)
437 type_id: SymbolId,
438 /// SymbolId of the module to add the type alias (required, O(1) lookup)
439 module_id: SymbolId,
440 /// Group name (e.g., "ConfigGroup", "DomainGroup")
441 group: String,
442 /// Optional alias name (default: "{target}Spec")
443 #[serde(default, skip_serializing_if = "Option::is_none")]
444 alias_name: Option<String>,
445 /// Relations (up to 3)
446 #[serde(default)]
447 relations: Vec<SpecRelation>,
448 },
449
450 /// Remove a Spec TypeAlias
451 RemoveSpec {
452 /// SymbolId of the spec alias to remove (required, O(1) lookup)
453 type_id: SymbolId,
454 /// SymbolId of the module containing the spec alias (required, O(1) lookup)
455 module_id: SymbolId,
456 },
457
458 /// Validate existing Spec definitions
459 ValidateSpec {
460 /// Target modules as SymbolIds
461 type_ids: Vec<SymbolId>,
462 /// Expected group name (None = any group)
463 #[serde(default, skip_serializing_if = "Option::is_none")]
464 expected_group: Option<String>,
465 /// Check that relations are valid (targets exist)
466 #[serde(default = "default_true")]
467 validate_relations: bool,
468 },
469
470 // === Method ===
471 /// Add a method to an impl block
472 AddMethod {
473 /// Target impl block (supports lazy resolution)
474 target: MutationTargetSymbol,
475 /// Method name
476 method_name: String,
477 /// Parameters as (name, type) pairs
478 #[serde(default)]
479 params: Vec<(String, String)>,
480 /// Return type (None for unit)
481 #[serde(default, skip_serializing_if = "Option::is_none")]
482 return_type: Option<String>,
483 /// Method body expression
484 #[serde(default = "default_body")]
485 body: String,
486 /// Whether the method is public
487 #[serde(default)]
488 is_pub: bool,
489 /// Self parameter: "ref" (&self), "mut" (&mut self), "owned" (self), or None
490 #[serde(default, skip_serializing_if = "Option::is_none")]
491 self_param: Option<SelfParam>,
492 },
493
494 /// Remove a method from an impl block
495 RemoveMethod {
496 /// Target impl block (supports lazy resolution)
497 target: MutationTargetSymbol,
498 /// Method name to remove
499 method_name: String,
500 },
501
502 // === Module ===
503 /// Remove a module declaration
504 RemoveMod {
505 /// Target parent module (supports lazy resolution)
506 target: MutationTargetSymbol,
507 /// Module name to remove
508 mod_name: String,
509 },
510
511 /// Create a new module (adds to module tree)
512 CreateMod {
513 /// Target parent module (supports lazy resolution)
514 target: MutationTargetSymbol,
515 /// New module name
516 mod_name: String,
517 /// Initial content (optional)
518 #[serde(default)]
519 content: String,
520 /// Whether the module is public
521 #[serde(default)]
522 is_pub: bool,
523 },
524
525 // === Idiom Transformations ===
526 /// Organize imports (sort, dedupe, merge)
527 OrganizeImports {
528 /// Target module (None = all modules)
529 #[serde(default, skip_serializing_if = "Option::is_none")]
530 module_id: Option<SymbolId>, // None = all
531 #[serde(default = "default_true")]
532 deduplicate: bool,
533 #[serde(default = "default_true")]
534 merge_groups: bool,
535 },
536
537 /// Convert loop to iterator
538 LoopToIterator {
539 /// Target module (None = all modules)
540 #[serde(default, skip_serializing_if = "Option::is_none")]
541 module_id: Option<SymbolId>, // None = all
542 target_var: Option<String>,
543 },
544
545 /// Convert unwrap/expect to ? operator
546 UnwrapToQuestion {
547 /// Target module (None = all modules)
548 #[serde(default, skip_serializing_if = "Option::is_none")]
549 module_id: Option<SymbolId>, // None = all
550 /// Only apply in specific function (None = all functions)
551 #[serde(default)]
552 target_fn: Option<SymbolId>,
553 #[serde(default = "default_true")]
554 include_expect: bool,
555 },
556
557 /// Simplify assign operations: `a = a + b` → `a += b`
558 AssignOp {
559 /// Target module (None = all modules)
560 #[serde(default, skip_serializing_if = "Option::is_none")]
561 module_id: Option<SymbolId>, // None = all
562 /// Only apply in specific function (None = all functions)
563 #[serde(default)]
564 fn_id: Option<SymbolId>,
565 },
566
567 /// Simplify boolean comparisons: `x == true` → `x`, `x == false` → `!x`
568 BoolSimplify {
569 /// Target module (None = all modules)
570 #[serde(default, skip_serializing_if = "Option::is_none")]
571 module_id: Option<SymbolId>, // None = all
572 },
573
574 /// Remove redundant .clone() on Copy types
575 CloneOnCopy {
576 /// Target module (None = all modules)
577 #[serde(default, skip_serializing_if = "Option::is_none")]
578 module_id: Option<SymbolId>, // None = all
579 },
580
581 /// Merge nested if statements into single if with &&
582 CollapsibleIf {
583 /// Target module (None = all modules)
584 #[serde(default, skip_serializing_if = "Option::is_none")]
585 module_id: Option<SymbolId>, // None = all
586 },
587
588 /// Replace empty/noop match arms with todo!/unimplemented!/unreachable!
589 /// `_ => {}` → `_ => todo!()` or `_ => unreachable!()`
590 NoOpArmToTodo {
591 /// Target module (None = all modules)
592 #[serde(default, skip_serializing_if = "Option::is_none")]
593 module_id: Option<SymbolId>, // None = all
594 /// Replacement macro: "todo", "unimplemented", or "unreachable" (default: "todo")
595 #[serde(default = "default_noop_replacement")]
596 replacement: String,
597 },
598
599 /// Convert comparisons to method calls: `s == ""` → `s.is_empty()`
600 ComparisonToMethod {
601 /// Target module (None = all modules)
602 #[serde(default, skip_serializing_if = "Option::is_none")]
603 module_id: Option<SymbolId>, // None = all
604 },
605
606 /// Remove redundant closures: `|x| f(x)` → `f`
607 RedundantClosure {
608 /// Target module (None = all modules)
609 #[serde(default, skip_serializing_if = "Option::is_none")]
610 module_id: Option<SymbolId>, // None = all
611 },
612
613 /// Introduce variable for repeated expressions
614 /// Expression is specified as string and parsed at runtime
615 IntroduceVariable {
616 /// Target module (None = all modules)
617 #[serde(default, skip_serializing_if = "Option::is_none")]
618 module_id: Option<SymbolId>, // None = all
619 /// Target function to apply (None = all functions)
620 #[serde(default)]
621 fn_id: Option<SymbolId>,
622 /// Expression to extract (as Rust code string, e.g. "a + b * c")
623 expr: String,
624 /// Name for the new variable
625 var_name: String,
626 },
627
628 /// Convert manual match on Option to .map(): `match opt { Some(x) => Some(f(x)), None => None }` → `opt.map(f)`
629 ManualMap {
630 /// Target module (None = all modules)
631 #[serde(default, skip_serializing_if = "Option::is_none")]
632 module_id: Option<SymbolId>, // None = all
633 },
634
635 /// Convert simple match to if let
636 MatchToIfLet {
637 /// Target module (None = all modules)
638 #[serde(default, skip_serializing_if = "Option::is_none")]
639 module_id: Option<SymbolId>,
640 },
641
642 /// Convert .filter().next() to .find()
643 FilterNext {
644 /// Target module (None = all modules)
645 #[serde(default, skip_serializing_if = "Option::is_none")]
646 module_id: Option<SymbolId>, // None = all
647 /// Target function (None = all functions)
648 #[serde(default, skip_serializing_if = "Option::is_none")]
649 fn_id: Option<SymbolId>,
650 },
651
652 /// Convert .map().unwrap_or() to .map_or()
653 MapUnwrapOr {
654 /// Target module (None = all modules)
655 #[serde(default, skip_serializing_if = "Option::is_none")]
656 module_id: Option<SymbolId>, // None = all
657 /// Target function (None = all functions)
658 #[serde(default, skip_serializing_if = "Option::is_none")]
659 fn_id: Option<SymbolId>,
660 },
661
662 // === PureStmt/PureExpr Operations ===
663 /// Replace an expression with another expression
664 ///
665 /// Target can be specified by:
666 /// - `old_expr`: Pattern matching (searches for matching expressions)
667 /// - `symbol_path`: Direct position (e.g., "crate::fn::$body::0::1")
668 ReplaceExpr {
669 /// Target module (None = all modules)
670 #[serde(default, skip_serializing_if = "Option::is_none")]
671 module_id: Option<SymbolId>, // None = all
672 #[serde(default)]
673 fn_id: Option<SymbolId>,
674 /// Expression to replace (as Rust code string) - pattern match mode
675 old_expr: String,
676 /// Replacement expression (as Rust code string)
677 new_expr: String,
678 /// Replace all occurrences (default: true)
679 #[serde(default = "default_true")]
680 replace_all: bool,
681 /// Direct position (e.g., "my_crate::my_fn::$body::0::1::2")
682 /// When specified, ignores old_expr and replaces at this exact position
683 #[serde(default, skip_serializing_if = "Option::is_none")]
684 symbol_path: Option<String>,
685 },
686
687 /// Remove statements matching a pattern
688 ///
689 /// Target can be specified by:
690 /// - `pattern`: Pattern matching (searches for matching statements)
691 /// - `symbol_path`: Direct position (e.g., "crate::fn::$body::2")
692 RemoveStatement {
693 /// Target module (None = all modules)
694 #[serde(default, skip_serializing_if = "Option::is_none")]
695 module_id: Option<SymbolId>, // None = all
696 #[serde(default)]
697 fn_id: Option<SymbolId>,
698 /// Statement pattern to remove (as Rust code string, e.g. "println!(..)") - pattern match mode
699 pattern: String,
700 /// Remove all occurrences (default: true)
701 #[serde(default = "default_true")]
702 remove_all: bool,
703 /// Direct position (e.g., "my_crate::my_fn::$body::2")
704 /// When specified, ignores pattern and removes at this exact position
705 #[serde(default, skip_serializing_if = "Option::is_none")]
706 symbol_path: Option<String>,
707 },
708
709 /// Insert a statement at a specific position
710 ///
711 /// Position can be specified by:
712 /// - `position` + `reference_pattern`: Traditional mode
713 /// - `symbol_path`: Direct position (inserts after $body::N)
714 InsertStatement {
715 /// Target module (None = all modules)
716 #[serde(default, skip_serializing_if = "Option::is_none")]
717 module_id: Option<SymbolId>,
718 /// Target function
719 #[serde(default)]
720 fn_id: SymbolId,
721 /// Statement to insert (as Rust code string)
722 stmt: String,
723 /// Insert position
724 #[serde(default)]
725 position: StmtInsertPosition,
726 /// Reference pattern for BeforePattern/AfterPattern positions
727 #[serde(default, skip_serializing_if = "Option::is_none")]
728 reference_pattern: Option<String>,
729 /// Direct position (e.g., "my_crate::my_fn::$body::2")
730 /// When specified, ignores position and inserts after this statement
731 #[serde(default, skip_serializing_if = "Option::is_none")]
732 symbol_path: Option<String>,
733 },
734
735 /// Replace a statement with another statement
736 ///
737 /// Target can be specified by:
738 /// - `old_stmt`: Pattern matching (searches for matching statements)
739 /// - `symbol_path`: Direct position (e.g., "crate::fn::$body::1")
740 ReplaceStatement {
741 /// Target module (None = all modules)
742 #[serde(default, skip_serializing_if = "Option::is_none")]
743 module_id: Option<SymbolId>,
744 #[serde(default, skip_serializing_if = "Option::is_none")]
745 fn_id: Option<SymbolId>,
746 /// Statement to replace (as Rust code string) - pattern match mode
747 old_stmt: String,
748 /// Replacement statement (as Rust code string)
749 new_stmt: String,
750 /// Direct position (e.g., "my_crate::my_fn::$body::1")
751 /// When specified, ignores old_stmt and replaces at this exact position
752 #[serde(default, skip_serializing_if = "Option::is_none")]
753 symbol_path: Option<String>,
754 },
755
756 // === Trait Abstraction ===
757 /// Extract a trait from an impl block
758 ExtractTrait {
759 /// Target impl block (supports lazy resolution)
760 target: MutationTargetSymbol,
761 /// Name for the new trait
762 trait_name: String,
763 /// Optional: specific methods to extract (None = all)
764 #[serde(default, skip_serializing_if = "Option::is_none")]
765 methods: Option<Vec<String>>,
766 },
767
768 /// Inline a trait back into inherent impl
769 InlineTrait {
770 /// Target trait (supports lazy resolution)
771 target: MutationTargetSymbol,
772 /// Struct that implements the trait
773 struct_name: String,
774 /// Whether to remove the trait definition
775 #[serde(default = "default_true")]
776 remove_trait: bool,
777 },
778
779 /// Replace all occurrences of a type with a transformed type
780 ///
781 /// # Examples
782 ///
783 /// ```ignore
784 /// // Replace Status with Box<dyn Status>
785 /// MutationSpec::ReplaceType {
786 /// from_type: "Status".to_string(),
787 /// to_type: TypeTransform::BoxDyn { trait_name: "Status".to_string() },
788 /// scope: None,
789 /// contexts: None,
790 /// }
791 /// ```
792 ReplaceType {
793 /// Target type to replace (supports lazy resolution)
794 target: MutationTargetSymbol,
795 /// How to transform the type
796 to_type: TypeTransform,
797 /// Scope to limit replacements (None = entire crate)
798 #[serde(default, skip_serializing_if = "Option::is_none")]
799 scope: Option<SymbolPath>,
800 /// Which contexts to replace in (None = all contexts)
801 #[serde(default, skip_serializing_if = "Option::is_none")]
802 contexts: Option<Vec<TypeContext>>,
803 },
804
805 /// Convert an enum to a trait with struct implementations
806 ///
807 /// Transforms:
808 /// - `enum Status { Running, Stopped }` into:
809 /// - `pub trait Status {}`
810 /// - `pub struct Running;`
811 /// - `pub struct Stopped;`
812 /// - `impl Status for Running {}`
813 /// - `impl Status for Stopped {}`
814 ///
815 /// Also updates all usage sites: `Status::Running` → `Running`
816 ///
817 /// ## Type Replacement
818 ///
819 /// With `strategy: dynamic` (default):
820 /// - `fn process(status: Status)` → `fn process(status: Box<dyn Status>)`
821 ///
822 /// With `strategy: static`:
823 /// - `fn process(status: Status)` → `fn process(status: impl Status)`
824 ///
825 /// With `strategy: marker_only`:
826 /// - No type replacement (manual migration required)
827 ///
828 EnumToTrait {
829 /// Target enum (supports lazy resolution)
830 target: MutationTargetSymbol,
831 /// Optional: custom trait name (default: same as enum name)
832 #[serde(default, skip_serializing_if = "Option::is_none")]
833 trait_name: Option<String>,
834 /// Whether to remove the original enum (default: true)
835 #[serde(default = "default_true")]
836 remove_enum: bool,
837 /// Type replacement strategy (default: dynamic = Box<dyn Trait>)
838 #[serde(default)]
839 strategy: EnumToTraitStrategy,
840 /// How to handle match expressions (default: warn_only)
841 #[serde(default)]
842 match_handling: MatchHandling,
843 },
844
845 // === Cross-file Operations ===
846 /// Move an item from one file to another
847 MoveItem {
848 /// Source module (supports lazy resolution)
849 source: MutationTargetSymbol,
850 /// Target module (supports lazy resolution)
851 target: MutationTargetSymbol,
852 /// Item name to move
853 item_name: String,
854 /// Item kind (Struct, Enum, Fn, etc.)
855 item_kind: ItemKind,
856 /// Whether to add use statement in source file
857 #[serde(default = "default_true")]
858 add_use: bool,
859 },
860
861 // === Plugin Transformations ===
862 /// Execute a WASM plugin transform
863 PluginTransform {
864 /// Plugin name (e.g., "map-unwrap-or", "custom-lint")
865 plugin_name: String,
866 /// Target module as SymbolId (None = all modules)
867 #[serde(default, skip_serializing_if = "Option::is_none")]
868 target_id: Option<SymbolId>,
869 /// File glob patterns (e.g., ["src/**/*.rs", "tests/*.rs"])
870 #[serde(default, skip_serializing_if = "Vec::is_empty")]
871 file_patterns: Vec<String>,
872 /// Plugin-specific configuration as JSON
873 #[serde(default)]
874 config: serde_json::Value,
875 },
876
877 // === Duplicate Operations ===
878 /// Duplicate a function with a new name
879 DuplicateFunction {
880 /// Target function (supports lazy resolution)
881 target: MutationTargetSymbol,
882 /// New function name
883 to: String,
884 },
885
886 /// Duplicate a struct with a new name (including impl blocks)
887 DuplicateStruct {
888 /// Target struct (supports lazy resolution)
889 target: MutationTargetSymbol,
890 /// New struct name
891 to: String,
892 /// Whether to also duplicate impl blocks
893 #[serde(default = "default_true")]
894 include_impls: bool,
895 },
896
897 /// Duplicate an enum with a new name (including impl blocks)
898 DuplicateEnum {
899 /// Target enum (supports lazy resolution)
900 target: MutationTargetSymbol,
901 /// New enum name
902 to: String,
903 /// Whether to also duplicate impl blocks
904 #[serde(default = "default_true")]
905 include_impls: bool,
906 },
907
908 /// Duplicate an inline module with a new name
909 DuplicateModTree {
910 /// Target module (supports lazy resolution)
911 target: MutationTargetSymbol,
912 /// New module name
913 to: String,
914 },
915}
916
917fn default_true() -> bool {
918 true
919}
920
921impl MutationSpec {
922 /// Get the kind name of this spec (e.g., "Rename", "AddField")
923 ///
924 /// Used by MutationRegistry to route specs to appropriate converters.
925 /// TODO: Consider typed approach (enum variant discriminant or macro-based)
926 pub fn kind_name(&self) -> &'static str {
927 match self {
928 Self::Rename { .. } => "Rename",
929 Self::AddField { .. } => "AddField",
930 Self::RemoveField { .. } => "RemoveField",
931 Self::ChangeVisibility { .. } => "ChangeVisibility",
932 Self::AddDerive { .. } => "AddDerive",
933 Self::RemoveDerive { .. } => "RemoveDerive",
934 Self::AddVariant { .. } => "AddVariant",
935 Self::RemoveVariant { .. } => "RemoveVariant",
936 Self::AddMatchArm { .. } => "AddMatchArm",
937 Self::RemoveMatchArm { .. } => "RemoveMatchArm",
938 Self::ReplaceMatchArm { .. } => "ReplaceMatchArm",
939 Self::AddStructLiteralField { .. } => "AddStructLiteralField",
940 Self::RemoveStructLiteralField { .. } => "RemoveStructLiteralField",
941 Self::AddItem { .. } => "AddItem",
942 Self::RemoveItem { .. } => "RemoveItem",
943 Self::AddSpec { .. } => "AddSpec",
944 Self::RemoveSpec { .. } => "RemoveSpec",
945 Self::ValidateSpec { .. } => "ValidateSpec",
946 Self::AddMethod { .. } => "AddMethod",
947 Self::RemoveMethod { .. } => "RemoveMethod",
948 Self::RemoveMod { .. } => "RemoveMod",
949 Self::CreateMod { .. } => "CreateMod",
950 Self::OrganizeImports { .. } => "OrganizeImports",
951 Self::LoopToIterator { .. } => "LoopToIterator",
952 Self::UnwrapToQuestion { .. } => "UnwrapToQuestion",
953 Self::AssignOp { .. } => "AssignOp",
954 Self::BoolSimplify { .. } => "BoolSimplify",
955 Self::CloneOnCopy { .. } => "CloneOnCopy",
956 Self::CollapsibleIf { .. } => "CollapsibleIf",
957 Self::NoOpArmToTodo { .. } => "NoOpArmToTodo",
958 Self::ComparisonToMethod { .. } => "ComparisonToMethod",
959 Self::RedundantClosure { .. } => "RedundantClosure",
960 Self::IntroduceVariable { .. } => "IntroduceVariable",
961 Self::ManualMap { .. } => "ManualMap",
962 Self::MatchToIfLet { .. } => "MatchToIfLet",
963 Self::FilterNext { .. } => "FilterNext",
964 Self::MapUnwrapOr { .. } => "MapUnwrapOr",
965 Self::ReplaceExpr { .. } => "ReplaceExpr",
966 Self::RemoveStatement { .. } => "RemoveStatement",
967 Self::InsertStatement { .. } => "InsertStatement",
968 Self::ReplaceStatement { .. } => "ReplaceStatement",
969 Self::ExtractTrait { .. } => "ExtractTrait",
970 Self::InlineTrait { .. } => "InlineTrait",
971 Self::ReplaceType { .. } => "ReplaceType",
972 Self::EnumToTrait { .. } => "EnumToTrait",
973 Self::MoveItem { .. } => "MoveItem",
974 Self::PluginTransform { .. } => "PluginTransform",
975 Self::DuplicateFunction { .. } => "DuplicateFunction",
976 Self::DuplicateStruct { .. } => "DuplicateStruct",
977 Self::DuplicateEnum { .. } => "DuplicateEnum",
978 Self::DuplicateModTree { .. } => "DuplicateModTree",
979 }
980 }
981
982 /// Check if this mutation is a rename
983 pub fn is_rename(&self) -> bool {
984 matches!(self, Self::Rename { .. })
985 }
986
987 /// Check if this is an additive operation (order-independent within same target)
988 ///
989 /// Additive operations like AddItem, AddMethod, AddField etc. can be applied
990 /// in any order to the same target without conflict, unless they add the
991 /// same named item (detected via `additive_identity`).
992 pub fn is_additive(&self) -> bool {
993 matches!(
994 self,
995 Self::AddItem { .. }
996 | Self::AddMethod { .. }
997 | Self::AddField { .. }
998 | Self::AddVariant { .. }
999 | Self::AddDerive { .. }
1000 | Self::CreateMod { .. }
1001 | Self::AddMatchArm { .. }
1002 | Self::AddStructLiteralField { .. }
1003 | Self::AddSpec { .. }
1004 )
1005 }
1006
1007 /// Get unique identity for additive operations (for duplicate detection)
1008 ///
1009 /// Returns a unique identifier for what this operation adds.
1010 /// Two additive operations with the same `additive_identity` targeting the
1011 /// same parent are true conflicts (trying to add the same thing twice).
1012 pub fn additive_identity(&self) -> Option<String> {
1013 match self {
1014 Self::AddItem { content, .. } => {
1015 // Extract item name from content (first identifier after pub/struct/fn/enum/etc.)
1016 extract_item_name_from_content(content)
1017 }
1018 Self::AddMethod { method_name, .. } => Some(method_name.clone()),
1019 Self::AddField { field_name, .. } => Some(field_name.clone()),
1020 Self::AddVariant { variant_name, .. } => Some(variant_name.clone()),
1021 Self::AddDerive { derives, .. } => Some(derives.join(",")),
1022 Self::CreateMod { mod_name, .. } => Some(mod_name.clone()),
1023 Self::AddMatchArm { pattern, .. } => Some(pattern.clone()),
1024 Self::AddStructLiteralField { field_name, .. } => Some(field_name.clone()),
1025 Self::AddSpec { type_id, .. } => Some(format!("{:?}", type_id)),
1026 _ => None,
1027 }
1028 }
1029
1030 /// Check if this is an idiom transformation
1031 pub fn is_idiom(&self) -> bool {
1032 matches!(
1033 self,
1034 Self::OrganizeImports { .. }
1035 | Self::LoopToIterator { .. }
1036 | Self::UnwrapToQuestion { .. }
1037 | Self::AssignOp { .. }
1038 | Self::BoolSimplify { .. }
1039 | Self::CloneOnCopy { .. }
1040 | Self::CollapsibleIf { .. }
1041 | Self::NoOpArmToTodo { .. }
1042 | Self::ComparisonToMethod { .. }
1043 | Self::RedundantClosure { .. }
1044 | Self::IntroduceVariable { .. }
1045 | Self::ManualMap { .. }
1046 | Self::MatchToIfLet { .. }
1047 | Self::FilterNext { .. }
1048 | Self::MapUnwrapOr { .. }
1049 | Self::ReplaceExpr { .. }
1050 | Self::RemoveStatement { .. }
1051 | Self::InsertStatement { .. }
1052 | Self::ReplaceStatement { .. }
1053 | Self::PluginTransform { .. }
1054 )
1055 }
1056
1057 /// Get target symbols from this spec.
1058 ///
1059 /// Returns references to all `MutationTargetSymbol` fields in this spec.
1060 /// Used for computing `affected_symbols` after mutation execution.
1061 pub fn get_targets(&self) -> Vec<&MutationTargetSymbol> {
1062 match self {
1063 // Specs with single target field
1064 Self::Rename { target, .. }
1065 | Self::AddField { target, .. }
1066 | Self::RemoveField { target, .. }
1067 | Self::ChangeVisibility { target, .. }
1068 | Self::AddDerive { target, .. }
1069 | Self::RemoveDerive { target, .. }
1070 | Self::AddVariant { target, .. }
1071 | Self::RemoveVariant { target, .. }
1072 | Self::AddMatchArm { target, .. }
1073 | Self::RemoveMatchArm { target, .. }
1074 | Self::ReplaceMatchArm { target, .. }
1075 | Self::AddStructLiteralField { target, .. }
1076 | Self::RemoveStructLiteralField { target, .. }
1077 | Self::AddItem { target, .. }
1078 | Self::RemoveItem { target, .. }
1079 | Self::AddMethod { target, .. }
1080 | Self::RemoveMethod { target, .. }
1081 | Self::RemoveMod { target, .. }
1082 | Self::CreateMod { target, .. }
1083 | Self::ExtractTrait { target, .. }
1084 | Self::InlineTrait { target, .. }
1085 | Self::ReplaceType { target, .. }
1086 | Self::EnumToTrait { target, .. }
1087 | Self::MoveItem { target, .. }
1088 | Self::DuplicateFunction { target, .. }
1089 | Self::DuplicateStruct { target, .. }
1090 | Self::DuplicateEnum { target, .. }
1091 | Self::DuplicateModTree { target, .. } => vec![target],
1092
1093 // Specs with SymbolId fields (not MutationTargetSymbol)
1094 Self::AddSpec { .. } | Self::RemoveSpec { .. } | Self::ValidateSpec { .. } => vec![],
1095
1096 // Idiom transformations (module-scoped, no specific target)
1097 Self::OrganizeImports { .. }
1098 | Self::LoopToIterator { .. }
1099 | Self::UnwrapToQuestion { .. }
1100 | Self::AssignOp { .. }
1101 | Self::BoolSimplify { .. }
1102 | Self::CloneOnCopy { .. }
1103 | Self::CollapsibleIf { .. }
1104 | Self::NoOpArmToTodo { .. }
1105 | Self::ComparisonToMethod { .. }
1106 | Self::RedundantClosure { .. }
1107 | Self::IntroduceVariable { .. }
1108 | Self::ManualMap { .. }
1109 | Self::MatchToIfLet { .. }
1110 | Self::FilterNext { .. }
1111 | Self::MapUnwrapOr { .. }
1112 | Self::ReplaceExpr { .. }
1113 | Self::RemoveStatement { .. }
1114 | Self::InsertStatement { .. }
1115 | Self::ReplaceStatement { .. }
1116 | Self::PluginTransform { .. } => vec![],
1117 }
1118 }
1119}
1120
1121/// Scope for mutations
1122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1123#[serde(tag = "type")]
1124pub enum Scope {
1125 /// All modules in project
1126 #[default]
1127 Project,
1128
1129 /// Specific module by path
1130 Mod { path: SymbolPath },
1131
1132 /// Within a specific item
1133 Item {
1134 target: SymbolPath,
1135 item_name: String,
1136 },
1137}
1138
1139/// Visibility levels
1140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1141#[serde(rename_all = "snake_case")]
1142pub enum Visibility {
1143 #[default]
1144 Private,
1145 Pub,
1146 PubCrate,
1147 PubSuper,
1148 PubIn(String),
1149}
1150
1151impl Visibility {
1152 pub fn to_rust_syntax(&self) -> &str {
1153 match self {
1154 Self::Private => "",
1155 Self::Pub => "pub ",
1156 Self::PubCrate => "pub(crate) ",
1157 Self::PubSuper => "pub(super) ",
1158 Self::PubIn(_) => "pub(in ...) ", // Needs special handling
1159 }
1160 }
1161}
1162
1163/// Enum variant kinds
1164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1165#[serde(tag = "type")]
1166pub enum VariantKind {
1167 #[default]
1168 Unit,
1169 Tuple {
1170 types: Vec<String>,
1171 },
1172 Struct {
1173 fields: Vec<(String, String)>,
1174 },
1175}
1176
1177/// Position for inserting items
1178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1179#[serde(tag = "type")]
1180pub enum InsertPosition {
1181 #[default]
1182 Top,
1183 Bottom,
1184 AfterItem {
1185 name: String,
1186 },
1187 BeforeItem {
1188 name: String,
1189 },
1190}
1191
1192/// Position for inserting statements within a function
1193#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1194#[serde(rename_all = "snake_case")]
1195pub enum StmtInsertPosition {
1196 /// At the start of the function body
1197 Start,
1198 /// At the end of the function body (before return statement if any)
1199 #[default]
1200 End,
1201 /// Before a statement matching the reference pattern
1202 BeforePattern,
1203 /// After a statement matching the reference pattern
1204 AfterPattern,
1205}
1206
1207/// Self parameter for methods
1208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1209#[serde(rename_all = "lowercase")]
1210pub enum SelfParam {
1211 /// &self
1212 Ref,
1213 /// &mut self
1214 Mut,
1215 /// self (owned)
1216 Owned,
1217}
1218
1219fn default_body() -> String {
1220 "todo!()".to_string()
1221}
1222
1223fn default_noop_replacement() -> String {
1224 "todo".to_string()
1225}
1226
1227/// Relation for Spec TypeAlias
1228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1229pub struct SpecRelation {
1230 /// Relation kind
1231 pub kind: SpecRelationKind,
1232 /// Target type name (simple name, e.g., "User")
1233 pub target: String,
1234 /// Direct SymbolId for target (from Discover result)
1235 #[serde(default, skip_serializing_if = "Option::is_none")]
1236 pub symbol_id: Option<SymbolId>,
1237 /// Full SymbolPath for target (for disambiguation)
1238 #[serde(default, skip_serializing_if = "Option::is_none")]
1239 pub target_path: Option<SymbolPath>,
1240}
1241
1242impl SpecRelation {
1243 /// Create a new SpecRelation with just a name
1244 pub fn new(kind: SpecRelationKind, target: impl Into<String>) -> Self {
1245 Self {
1246 kind,
1247 target: target.into(),
1248 symbol_id: None,
1249 target_path: None,
1250 }
1251 }
1252
1253 /// Create a new SpecRelation with a SymbolPath
1254 pub fn with_path(kind: SpecRelationKind, target: impl Into<String>, path: SymbolPath) -> Self {
1255 Self {
1256 kind,
1257 target: target.into(),
1258 symbol_id: None,
1259 target_path: Some(path),
1260 }
1261 }
1262
1263 /// Create a new SpecRelation with a SymbolId
1264 pub fn with_symbol_id(
1265 kind: SpecRelationKind,
1266 target: impl Into<String>,
1267 symbol_id: SymbolId,
1268 ) -> Self {
1269 Self {
1270 kind,
1271 target: target.into(),
1272 symbol_id: Some(symbol_id),
1273 target_path: None,
1274 }
1275 }
1276}
1277
1278/// Relation kinds for Spec
1279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1280#[serde(rename_all = "PascalCase")]
1281pub enum SpecRelationKind {
1282 /// A depends on B (A needs B to function)
1283 DependsOn,
1284 /// A is related to B (semantic relationship)
1285 RelatedTo,
1286 /// A is part of B (aggregate membership)
1287 PartOf,
1288}
1289
1290impl SpecRelationKind {
1291 /// Get the Rust type name for this relation
1292 pub fn as_type_name(&self) -> &'static str {
1293 match self {
1294 Self::DependsOn => "DependsOn",
1295 Self::RelatedTo => "RelatedTo",
1296 Self::PartOf => "PartOf",
1297 }
1298 }
1299}
1300
1301// ItemKind is re-exported from ryo_source
1302
1303/// Extract item name from Rust code content
1304///
1305/// Parses common Rust item declarations to extract the name.
1306/// Used by `additive_identity()` for AddItem duplicate detection.
1307fn extract_item_name_from_content(content: &str) -> Option<String> {
1308 // Simple regex-free parsing for common patterns
1309 let trimmed = content.trim();
1310
1311 // Skip attributes and find the item declaration
1312 let mut lines = trimmed.lines();
1313 let mut decl_line = "";
1314 for line in lines.by_ref() {
1315 let line = line.trim();
1316 if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
1317 decl_line = line;
1318 break;
1319 }
1320 }
1321
1322 // Parse common patterns: pub? (struct|enum|fn|type|const|static|trait|impl|mod|use) NAME
1323 let tokens: Vec<&str> = decl_line.split_whitespace().collect();
1324 if tokens.is_empty() {
1325 return None;
1326 }
1327
1328 let mut idx = 0;
1329
1330 // Skip visibility
1331 if tokens.get(idx) == Some(&"pub") {
1332 idx += 1;
1333 // Skip pub(crate), pub(super), etc.
1334 if let Some(t) = tokens.get(idx) {
1335 if t.starts_with('(') {
1336 idx += 1;
1337 }
1338 }
1339 }
1340
1341 // Get keyword
1342 let keyword = tokens.get(idx)?;
1343 idx += 1;
1344
1345 match *keyword {
1346 "struct" | "enum" | "fn" | "type" | "const" | "static" | "trait" | "mod" => {
1347 // Next token is the name (may include generics)
1348 let name = tokens.get(idx)?;
1349 // Strip generics <...> and trailing punctuation
1350 let name = name.split('<').next().unwrap_or(name);
1351 let name = name.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
1352 Some(name.to_string())
1353 }
1354 "impl" => {
1355 // impl Trait for Type or impl Type
1356 // For impl blocks, include the full impl signature for identity
1357 let rest = tokens[idx..].join(" ");
1358
1359 // Extract impl signature (everything before '{' or '<')
1360 // This includes "Trait for Type" or just "Type"
1361 let impl_sig = rest
1362 .split(['{', '<'])
1363 .next()
1364 .map(|s| s.trim())
1365 .filter(|s| !s.is_empty());
1366
1367 // Also extract method names from the impl block to differentiate
1368 // multiple impl blocks for the same type
1369 let methods: Vec<&str> = content
1370 .lines()
1371 .filter_map(|line| {
1372 let trimmed = line.trim();
1373 if trimmed.starts_with("pub fn ") || trimmed.starts_with("fn ") {
1374 let after_fn = trimmed
1375 .strip_prefix("pub fn ")
1376 .or_else(|| trimmed.strip_prefix("fn "))?;
1377 let method_name = after_fn.split('(').next()?.trim();
1378 Some(method_name)
1379 } else {
1380 None
1381 }
1382 })
1383 .collect();
1384
1385 match (impl_sig, methods.is_empty()) {
1386 (Some(sig), false) => Some(format!(
1387 "impl_{}::{}",
1388 sig.replace(' ', "_"),
1389 methods.join(",")
1390 )),
1391 // Use full signature for empty impl blocks (e.g., "impl_Status_for_Running")
1392 (Some(sig), true) => Some(format!("impl_{}", sig.replace(' ', "_"))),
1393 _ => None,
1394 }
1395 }
1396 "use" => {
1397 // use path::Name or use path::{A, B}
1398 // Return the full use statement as identity
1399 Some(tokens[idx..].join(" "))
1400 }
1401 _ => {
1402 // Unknown pattern, use first meaningful token
1403 Some(keyword.to_string())
1404 }
1405 }
1406}
1407
1408#[cfg(test)]
1409mod tests {
1410 use super::*;
1411
1412 #[test]
1413 fn test_mutation_spec_serialize() {
1414 // Test serialization with ByPath (which contains the symbol path)
1415 let spec = MutationSpec::Rename {
1416 target: MutationTargetSymbol::ByPath(Box::new(
1417 SymbolPath::parse("test_crate::old_name").unwrap(),
1418 )),
1419 to: "new_name".to_string(),
1420 scope: Scope::Project,
1421 };
1422
1423 let json = serde_json::to_string_pretty(&spec).unwrap();
1424 assert!(json.contains("Rename"), "JSON should contain Rename");
1425 assert!(
1426 json.contains("old_name"),
1427 "JSON should contain old_name in ByPath"
1428 );
1429 assert!(json.contains("new_name"), "JSON should contain new_name");
1430
1431 let parsed: MutationSpec = serde_json::from_str(&json).unwrap();
1432 assert_eq!(
1433 spec, parsed,
1434 "Round-trip serialization should preserve spec"
1435 );
1436 }
1437
1438 #[test]
1439 fn test_idiom_detection() {
1440 let spec = MutationSpec::OrganizeImports {
1441 module_id: None,
1442 deduplicate: true,
1443 merge_groups: true,
1444 };
1445
1446 assert!(spec.is_idiom());
1447 assert!(!spec.is_rename());
1448 }
1449}