Skip to main content

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}