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    /// Replace an item with raw source bytes, preserving trivia.
434    ///
435    /// Unlike `AddItem` / `ReplaceExpr` (whose `content` is re-parsed via
436    /// `syn::parse_str` and therefore loses `//` line comments, blank
437    /// lines, and DSL macro token spacing), `ReplaceCode` routes the raw
438    /// bytes through `PureItem::Verbatim` and is rendered back via the
439    /// stub + splice pipeline in `PureFile::to_source`. This is the β
440    /// route's structural escape hatch: when a target item contains a
441    /// proc-macro DSL, manually-formatted spans, or feature constructs
442    /// outside the RyoLang subset, the surrounding refactor still runs
443    /// while the target item itself is preserved exactly as authored.
444    ///
445    /// The `target` must resolve to a top-level item symbol (struct, enum,
446    /// fn, type alias, etc.) inside the file's root module. Nested
447    /// (inside `mod foo { ... }`) targets are not yet supported.
448    ReplaceCode {
449        /// Target item to replace (supports lazy resolution).
450        target: MutationTargetSymbol,
451        /// Raw replacement source bytes, splice-preserved verbatim.
452        raw: String,
453    },
454
455    // === Spec ===
456    /// Add a Spec TypeAlias (Spec<Group, T> or SpecWith<Group, R, T>)
457    AddSpec {
458        /// SymbolId of the target type (required, O(1) lookup)
459        type_id: SymbolId,
460        /// SymbolId of the module to add the type alias (required, O(1) lookup)
461        module_id: SymbolId,
462        /// Group name (e.g., "ConfigGroup", "DomainGroup")
463        group: String,
464        /// Optional alias name (default: "{target}Spec")
465        #[serde(default, skip_serializing_if = "Option::is_none")]
466        alias_name: Option<String>,
467        /// Relations (up to 3)
468        #[serde(default)]
469        relations: Vec<SpecRelation>,
470    },
471
472    /// Remove a Spec TypeAlias
473    RemoveSpec {
474        /// SymbolId of the spec alias to remove (required, O(1) lookup)
475        type_id: SymbolId,
476        /// SymbolId of the module containing the spec alias (required, O(1) lookup)
477        module_id: SymbolId,
478    },
479
480    /// Validate existing Spec definitions
481    ValidateSpec {
482        /// Target modules as SymbolIds
483        type_ids: Vec<SymbolId>,
484        /// Expected group name (None = any group)
485        #[serde(default, skip_serializing_if = "Option::is_none")]
486        expected_group: Option<String>,
487        /// Check that relations are valid (targets exist)
488        #[serde(default = "default_true")]
489        validate_relations: bool,
490    },
491
492    // === Method ===
493    /// Add a method to an impl block
494    AddMethod {
495        /// Target impl block (supports lazy resolution)
496        target: MutationTargetSymbol,
497        /// Method name
498        method_name: String,
499        /// Parameters as (name, type) pairs
500        #[serde(default)]
501        params: Vec<(String, String)>,
502        /// Return type (None for unit)
503        #[serde(default, skip_serializing_if = "Option::is_none")]
504        return_type: Option<String>,
505        /// Method body expression
506        #[serde(default = "default_body")]
507        body: String,
508        /// Whether the method is public
509        #[serde(default)]
510        is_pub: bool,
511        /// Self parameter: "ref" (&self), "mut" (&mut self), "owned" (self), or None
512        #[serde(default, skip_serializing_if = "Option::is_none")]
513        self_param: Option<SelfParam>,
514    },
515
516    /// Remove a method from an impl block
517    RemoveMethod {
518        /// Target impl block (supports lazy resolution)
519        target: MutationTargetSymbol,
520        /// Method name to remove
521        method_name: String,
522    },
523
524    // === Module ===
525    /// Remove a module declaration
526    RemoveMod {
527        /// Target parent module (supports lazy resolution)
528        target: MutationTargetSymbol,
529        /// Module name to remove
530        mod_name: String,
531    },
532
533    /// Create a new module (adds to module tree)
534    CreateMod {
535        /// Target parent module (supports lazy resolution)
536        target: MutationTargetSymbol,
537        /// New module name
538        mod_name: String,
539        /// Initial content (optional)
540        #[serde(default)]
541        content: String,
542        /// Whether the module is public
543        #[serde(default)]
544        is_pub: bool,
545    },
546
547    // === Idiom Transformations ===
548    /// Organize imports (sort, dedupe, merge)
549    OrganizeImports {
550        /// Target module (None = all modules)
551        #[serde(default, skip_serializing_if = "Option::is_none")]
552        module_id: Option<SymbolId>, // None = all
553        #[serde(default = "default_true")]
554        deduplicate: bool,
555        #[serde(default = "default_true")]
556        merge_groups: bool,
557    },
558
559    /// Convert loop to iterator
560    LoopToIterator {
561        /// Target module (None = all modules)
562        #[serde(default, skip_serializing_if = "Option::is_none")]
563        module_id: Option<SymbolId>, // None = all
564        target_var: Option<String>,
565    },
566
567    /// Convert unwrap/expect to ? operator
568    UnwrapToQuestion {
569        /// Target module (None = all modules)
570        #[serde(default, skip_serializing_if = "Option::is_none")]
571        module_id: Option<SymbolId>, // None = all
572        /// Only apply in specific function (None = all functions)
573        #[serde(default)]
574        target_fn: Option<SymbolId>,
575        #[serde(default = "default_true")]
576        include_expect: bool,
577    },
578
579    /// Simplify assign operations: `a = a + b` → `a += b`
580    AssignOp {
581        /// Target module (None = all modules)
582        #[serde(default, skip_serializing_if = "Option::is_none")]
583        module_id: Option<SymbolId>, // None = all
584        /// Only apply in specific function (None = all functions)
585        #[serde(default)]
586        fn_id: Option<SymbolId>,
587    },
588
589    /// Simplify boolean comparisons: `x == true` → `x`, `x == false` → `!x`
590    BoolSimplify {
591        /// Target module (None = all modules)
592        #[serde(default, skip_serializing_if = "Option::is_none")]
593        module_id: Option<SymbolId>, // None = all
594    },
595
596    /// Remove redundant .clone() on Copy types
597    CloneOnCopy {
598        /// Target module (None = all modules)
599        #[serde(default, skip_serializing_if = "Option::is_none")]
600        module_id: Option<SymbolId>, // None = all
601    },
602
603    /// Merge nested if statements into single if with &&
604    CollapsibleIf {
605        /// Target module (None = all modules)
606        #[serde(default, skip_serializing_if = "Option::is_none")]
607        module_id: Option<SymbolId>, // None = all
608    },
609
610    /// Replace empty/noop match arms with todo!/unimplemented!/unreachable!
611    /// `_ => {}` → `_ => todo!()` or `_ => unreachable!()`
612    NoOpArmToTodo {
613        /// Target module (None = all modules)
614        #[serde(default, skip_serializing_if = "Option::is_none")]
615        module_id: Option<SymbolId>, // None = all
616        /// Replacement macro: "todo", "unimplemented", or "unreachable" (default: "todo")
617        #[serde(default = "default_noop_replacement")]
618        replacement: String,
619    },
620
621    /// Convert comparisons to method calls: `s == ""` → `s.is_empty()`
622    ComparisonToMethod {
623        /// Target module (None = all modules)
624        #[serde(default, skip_serializing_if = "Option::is_none")]
625        module_id: Option<SymbolId>, // None = all
626    },
627
628    /// Remove redundant closures: `|x| f(x)` → `f`
629    RedundantClosure {
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    /// Introduce variable for repeated expressions
636    /// Expression is specified as string and parsed at runtime
637    IntroduceVariable {
638        /// Target module (None = all modules)
639        #[serde(default, skip_serializing_if = "Option::is_none")]
640        module_id: Option<SymbolId>, // None = all
641        /// Target function to apply (None = all functions)
642        #[serde(default)]
643        fn_id: Option<SymbolId>,
644        /// Expression to extract (as Rust code string, e.g. "a + b * c")
645        expr: String,
646        /// Name for the new variable
647        var_name: String,
648    },
649
650    /// Convert manual match on Option to .map(): `match opt { Some(x) => Some(f(x)), None => None }` → `opt.map(f)`
651    ManualMap {
652        /// Target module (None = all modules)
653        #[serde(default, skip_serializing_if = "Option::is_none")]
654        module_id: Option<SymbolId>, // None = all
655    },
656
657    /// Convert simple match to if let
658    MatchToIfLet {
659        /// Target module (None = all modules)
660        #[serde(default, skip_serializing_if = "Option::is_none")]
661        module_id: Option<SymbolId>,
662    },
663
664    /// Convert .filter().next() to .find()
665    FilterNext {
666        /// Target module (None = all modules)
667        #[serde(default, skip_serializing_if = "Option::is_none")]
668        module_id: Option<SymbolId>, // None = all
669        /// Target function (None = all functions)
670        #[serde(default, skip_serializing_if = "Option::is_none")]
671        fn_id: Option<SymbolId>,
672    },
673
674    /// Convert .map().unwrap_or() to .map_or()
675    MapUnwrapOr {
676        /// Target module (None = all modules)
677        #[serde(default, skip_serializing_if = "Option::is_none")]
678        module_id: Option<SymbolId>, // None = all
679        /// Target function (None = all functions)
680        #[serde(default, skip_serializing_if = "Option::is_none")]
681        fn_id: Option<SymbolId>,
682    },
683
684    // === PureStmt/PureExpr Operations ===
685    /// Replace an expression with another expression
686    ///
687    /// Target can be specified by:
688    /// - `old_expr`: Pattern matching (searches for matching expressions)
689    /// - `symbol_path`: Direct position (e.g., "crate::fn::$body::0::1")
690    ReplaceExpr {
691        /// Target module (None = all modules)
692        #[serde(default, skip_serializing_if = "Option::is_none")]
693        module_id: Option<SymbolId>, // None = all
694        #[serde(default)]
695        fn_id: Option<SymbolId>,
696        /// Expression to replace (as Rust code string) - pattern match mode
697        old_expr: String,
698        /// Replacement expression (as Rust code string)
699        new_expr: String,
700        /// Replace all occurrences (default: true)
701        #[serde(default = "default_true")]
702        replace_all: bool,
703        /// Direct position (e.g., "my_crate::my_fn::$body::0::1::2")
704        /// When specified, ignores old_expr and replaces at this exact position
705        #[serde(default, skip_serializing_if = "Option::is_none")]
706        symbol_path: Option<String>,
707    },
708
709    /// Remove statements matching a pattern
710    ///
711    /// Target can be specified by:
712    /// - `pattern`: Pattern matching (searches for matching statements)
713    /// - `symbol_path`: Direct position (e.g., "crate::fn::$body::2")
714    RemoveStatement {
715        /// Target module (None = all modules)
716        #[serde(default, skip_serializing_if = "Option::is_none")]
717        module_id: Option<SymbolId>, // None = all
718        #[serde(default)]
719        fn_id: Option<SymbolId>,
720        /// Statement pattern to remove (as Rust code string, e.g. "println!(..)") - pattern match mode
721        pattern: String,
722        /// Remove all occurrences (default: true)
723        #[serde(default = "default_true")]
724        remove_all: bool,
725        /// Direct position (e.g., "my_crate::my_fn::$body::2")
726        /// When specified, ignores pattern and removes at this exact position
727        #[serde(default, skip_serializing_if = "Option::is_none")]
728        symbol_path: Option<String>,
729    },
730
731    /// Insert a statement at a specific position
732    ///
733    /// Position can be specified by:
734    /// - `position` + `reference_pattern`: Traditional mode
735    /// - `symbol_path`: Direct position (inserts after $body::N)
736    InsertStatement {
737        /// Target module (None = all modules)
738        #[serde(default, skip_serializing_if = "Option::is_none")]
739        module_id: Option<SymbolId>,
740        /// Target function
741        #[serde(default)]
742        fn_id: SymbolId,
743        /// Statement to insert (as Rust code string)
744        stmt: String,
745        /// Insert position
746        #[serde(default)]
747        position: StmtInsertPosition,
748        /// Reference pattern for BeforePattern/AfterPattern positions
749        #[serde(default, skip_serializing_if = "Option::is_none")]
750        reference_pattern: Option<String>,
751        /// Direct position (e.g., "my_crate::my_fn::$body::2")
752        /// When specified, ignores position and inserts after this statement
753        #[serde(default, skip_serializing_if = "Option::is_none")]
754        symbol_path: Option<String>,
755    },
756
757    /// Replace a statement with another statement
758    ///
759    /// Target can be specified by:
760    /// - `old_stmt`: Pattern matching (searches for matching statements)
761    /// - `symbol_path`: Direct position (e.g., "crate::fn::$body::1")
762    ReplaceStatement {
763        /// Target module (None = all modules)
764        #[serde(default, skip_serializing_if = "Option::is_none")]
765        module_id: Option<SymbolId>,
766        #[serde(default, skip_serializing_if = "Option::is_none")]
767        fn_id: Option<SymbolId>,
768        /// Statement to replace (as Rust code string) - pattern match mode
769        old_stmt: String,
770        /// Replacement statement (as Rust code string)
771        new_stmt: String,
772        /// Direct position (e.g., "my_crate::my_fn::$body::1")
773        /// When specified, ignores old_stmt and replaces at this exact position
774        #[serde(default, skip_serializing_if = "Option::is_none")]
775        symbol_path: Option<String>,
776    },
777
778    // === Trait Abstraction ===
779    /// Extract a trait from an impl block
780    ExtractTrait {
781        /// Target impl block (supports lazy resolution)
782        target: MutationTargetSymbol,
783        /// Name for the new trait
784        trait_name: String,
785        /// Optional: specific methods to extract (None = all)
786        #[serde(default, skip_serializing_if = "Option::is_none")]
787        methods: Option<Vec<String>>,
788    },
789
790    /// Inline a trait back into inherent impl
791    InlineTrait {
792        /// Target trait (supports lazy resolution)
793        target: MutationTargetSymbol,
794        /// Struct that implements the trait
795        struct_name: String,
796        /// Whether to remove the trait definition
797        #[serde(default = "default_true")]
798        remove_trait: bool,
799    },
800
801    /// Replace all occurrences of a type with a transformed type
802    ///
803    /// # Examples
804    ///
805    /// ```ignore
806    /// // Replace Status with Box<dyn Status>
807    /// MutationSpec::ReplaceType {
808    ///     from_type: "Status".to_string(),
809    ///     to_type: TypeTransform::BoxDyn { trait_name: "Status".to_string() },
810    ///     scope: None,
811    ///     contexts: None,
812    /// }
813    /// ```
814    ReplaceType {
815        /// Target type to replace (supports lazy resolution)
816        target: MutationTargetSymbol,
817        /// How to transform the type
818        to_type: TypeTransform,
819        /// Scope to limit replacements (None = entire crate)
820        #[serde(default, skip_serializing_if = "Option::is_none")]
821        scope: Option<SymbolPath>,
822        /// Which contexts to replace in (None = all contexts)
823        #[serde(default, skip_serializing_if = "Option::is_none")]
824        contexts: Option<Vec<TypeContext>>,
825    },
826
827    /// Convert an enum to a trait with struct implementations
828    ///
829    /// Transforms:
830    /// - `enum Status { Running, Stopped }` into:
831    /// - `pub trait Status {}`
832    /// - `pub struct Running;`
833    /// - `pub struct Stopped;`
834    /// - `impl Status for Running {}`
835    /// - `impl Status for Stopped {}`
836    ///
837    /// Also updates all usage sites: `Status::Running` → `Running`
838    ///
839    /// ## Type Replacement
840    ///
841    /// With `strategy: dynamic` (default):
842    /// - `fn process(status: Status)` → `fn process(status: Box<dyn Status>)`
843    ///
844    /// With `strategy: static`:
845    /// - `fn process(status: Status)` → `fn process(status: impl Status)`
846    ///
847    /// With `strategy: marker_only`:
848    /// - No type replacement (manual migration required)
849    ///
850    EnumToTrait {
851        /// Target enum (supports lazy resolution)
852        target: MutationTargetSymbol,
853        /// Optional: custom trait name (default: same as enum name)
854        #[serde(default, skip_serializing_if = "Option::is_none")]
855        trait_name: Option<String>,
856        /// Whether to remove the original enum (default: true)
857        #[serde(default = "default_true")]
858        remove_enum: bool,
859        /// Type replacement strategy (default: dynamic = Box<dyn Trait>)
860        #[serde(default)]
861        strategy: EnumToTraitStrategy,
862        /// How to handle match expressions (default: warn_only)
863        #[serde(default)]
864        match_handling: MatchHandling,
865    },
866
867    // === Cross-file Operations ===
868    /// Move an item from one file to another
869    MoveItem {
870        /// Source module (supports lazy resolution)
871        source: MutationTargetSymbol,
872        /// Target module (supports lazy resolution)
873        target: MutationTargetSymbol,
874        /// Item name to move
875        item_name: String,
876        /// Item kind (Struct, Enum, Fn, etc.)
877        item_kind: ItemKind,
878        /// Whether to add use statement in source file
879        #[serde(default = "default_true")]
880        add_use: bool,
881    },
882
883    // === Plugin Transformations ===
884    /// Execute a WASM plugin transform
885    PluginTransform {
886        /// Plugin name (e.g., "map-unwrap-or", "custom-lint")
887        plugin_name: String,
888        /// Target module as SymbolId (None = all modules)
889        #[serde(default, skip_serializing_if = "Option::is_none")]
890        target_id: Option<SymbolId>,
891        /// File glob patterns (e.g., ["src/**/*.rs", "tests/*.rs"])
892        #[serde(default, skip_serializing_if = "Vec::is_empty")]
893        file_patterns: Vec<String>,
894        /// Plugin-specific configuration as JSON
895        #[serde(default)]
896        config: serde_json::Value,
897    },
898
899    // === Duplicate Operations ===
900    /// Duplicate a function with a new name
901    DuplicateFunction {
902        /// Target function (supports lazy resolution)
903        target: MutationTargetSymbol,
904        /// New function name
905        to: String,
906    },
907
908    /// Duplicate a struct with a new name (including impl blocks)
909    DuplicateStruct {
910        /// Target struct (supports lazy resolution)
911        target: MutationTargetSymbol,
912        /// New struct name
913        to: String,
914        /// Whether to also duplicate impl blocks
915        #[serde(default = "default_true")]
916        include_impls: bool,
917    },
918
919    /// Duplicate an enum with a new name (including impl blocks)
920    DuplicateEnum {
921        /// Target enum (supports lazy resolution)
922        target: MutationTargetSymbol,
923        /// New enum name
924        to: String,
925        /// Whether to also duplicate impl blocks
926        #[serde(default = "default_true")]
927        include_impls: bool,
928    },
929
930    /// Duplicate an inline module with a new name
931    DuplicateModTree {
932        /// Target module (supports lazy resolution)
933        target: MutationTargetSymbol,
934        /// New module name
935        to: String,
936    },
937}
938
939fn default_true() -> bool {
940    true
941}
942
943impl MutationSpec {
944    /// Get the kind name of this spec (e.g., "Rename", "AddField")
945    ///
946    /// Used by MutationRegistry to route specs to appropriate converters.
947    /// TODO: Consider typed approach (enum variant discriminant or macro-based)
948    pub fn kind_name(&self) -> &'static str {
949        match self {
950            Self::Rename { .. } => "Rename",
951            Self::AddField { .. } => "AddField",
952            Self::RemoveField { .. } => "RemoveField",
953            Self::ChangeVisibility { .. } => "ChangeVisibility",
954            Self::AddDerive { .. } => "AddDerive",
955            Self::RemoveDerive { .. } => "RemoveDerive",
956            Self::AddVariant { .. } => "AddVariant",
957            Self::RemoveVariant { .. } => "RemoveVariant",
958            Self::AddMatchArm { .. } => "AddMatchArm",
959            Self::RemoveMatchArm { .. } => "RemoveMatchArm",
960            Self::ReplaceMatchArm { .. } => "ReplaceMatchArm",
961            Self::AddStructLiteralField { .. } => "AddStructLiteralField",
962            Self::RemoveStructLiteralField { .. } => "RemoveStructLiteralField",
963            Self::AddItem { .. } => "AddItem",
964            Self::RemoveItem { .. } => "RemoveItem",
965            Self::ReplaceCode { .. } => "ReplaceCode",
966            Self::AddSpec { .. } => "AddSpec",
967            Self::RemoveSpec { .. } => "RemoveSpec",
968            Self::ValidateSpec { .. } => "ValidateSpec",
969            Self::AddMethod { .. } => "AddMethod",
970            Self::RemoveMethod { .. } => "RemoveMethod",
971            Self::RemoveMod { .. } => "RemoveMod",
972            Self::CreateMod { .. } => "CreateMod",
973            Self::OrganizeImports { .. } => "OrganizeImports",
974            Self::LoopToIterator { .. } => "LoopToIterator",
975            Self::UnwrapToQuestion { .. } => "UnwrapToQuestion",
976            Self::AssignOp { .. } => "AssignOp",
977            Self::BoolSimplify { .. } => "BoolSimplify",
978            Self::CloneOnCopy { .. } => "CloneOnCopy",
979            Self::CollapsibleIf { .. } => "CollapsibleIf",
980            Self::NoOpArmToTodo { .. } => "NoOpArmToTodo",
981            Self::ComparisonToMethod { .. } => "ComparisonToMethod",
982            Self::RedundantClosure { .. } => "RedundantClosure",
983            Self::IntroduceVariable { .. } => "IntroduceVariable",
984            Self::ManualMap { .. } => "ManualMap",
985            Self::MatchToIfLet { .. } => "MatchToIfLet",
986            Self::FilterNext { .. } => "FilterNext",
987            Self::MapUnwrapOr { .. } => "MapUnwrapOr",
988            Self::ReplaceExpr { .. } => "ReplaceExpr",
989            Self::RemoveStatement { .. } => "RemoveStatement",
990            Self::InsertStatement { .. } => "InsertStatement",
991            Self::ReplaceStatement { .. } => "ReplaceStatement",
992            Self::ExtractTrait { .. } => "ExtractTrait",
993            Self::InlineTrait { .. } => "InlineTrait",
994            Self::ReplaceType { .. } => "ReplaceType",
995            Self::EnumToTrait { .. } => "EnumToTrait",
996            Self::MoveItem { .. } => "MoveItem",
997            Self::PluginTransform { .. } => "PluginTransform",
998            Self::DuplicateFunction { .. } => "DuplicateFunction",
999            Self::DuplicateStruct { .. } => "DuplicateStruct",
1000            Self::DuplicateEnum { .. } => "DuplicateEnum",
1001            Self::DuplicateModTree { .. } => "DuplicateModTree",
1002        }
1003    }
1004
1005    /// Check if this mutation is a rename
1006    pub fn is_rename(&self) -> bool {
1007        matches!(self, Self::Rename { .. })
1008    }
1009
1010    /// Check if this is an additive operation (order-independent within same target)
1011    ///
1012    /// Additive operations like AddItem, AddMethod, AddField etc. can be applied
1013    /// in any order to the same target without conflict, unless they add the
1014    /// same named item (detected via `additive_identity`).
1015    pub fn is_additive(&self) -> bool {
1016        matches!(
1017            self,
1018            Self::AddItem { .. }
1019                | Self::AddMethod { .. }
1020                | Self::AddField { .. }
1021                | Self::AddVariant { .. }
1022                | Self::AddDerive { .. }
1023                | Self::CreateMod { .. }
1024                | Self::AddMatchArm { .. }
1025                | Self::AddStructLiteralField { .. }
1026                | Self::AddSpec { .. }
1027        )
1028    }
1029
1030    /// Get unique identity for additive operations (for duplicate detection)
1031    ///
1032    /// Returns a unique identifier for what this operation adds.
1033    /// Two additive operations with the same `additive_identity` targeting the
1034    /// same parent are true conflicts (trying to add the same thing twice).
1035    pub fn additive_identity(&self) -> Option<String> {
1036        match self {
1037            Self::AddItem { content, .. } => {
1038                // Extract item name from content (first identifier after pub/struct/fn/enum/etc.)
1039                extract_item_name_from_content(content)
1040            }
1041            Self::AddMethod { method_name, .. } => Some(method_name.clone()),
1042            Self::AddField { field_name, .. } => Some(field_name.clone()),
1043            Self::AddVariant { variant_name, .. } => Some(variant_name.clone()),
1044            Self::AddDerive { derives, .. } => Some(derives.join(",")),
1045            Self::CreateMod { mod_name, .. } => Some(mod_name.clone()),
1046            Self::AddMatchArm { pattern, .. } => Some(pattern.clone()),
1047            Self::AddStructLiteralField { field_name, .. } => Some(field_name.clone()),
1048            Self::AddSpec { type_id, .. } => Some(format!("{:?}", type_id)),
1049            _ => None,
1050        }
1051    }
1052
1053    /// Check if this is an idiom transformation
1054    pub fn is_idiom(&self) -> bool {
1055        matches!(
1056            self,
1057            Self::OrganizeImports { .. }
1058                | Self::LoopToIterator { .. }
1059                | Self::UnwrapToQuestion { .. }
1060                | Self::AssignOp { .. }
1061                | Self::BoolSimplify { .. }
1062                | Self::CloneOnCopy { .. }
1063                | Self::CollapsibleIf { .. }
1064                | Self::NoOpArmToTodo { .. }
1065                | Self::ComparisonToMethod { .. }
1066                | Self::RedundantClosure { .. }
1067                | Self::IntroduceVariable { .. }
1068                | Self::ManualMap { .. }
1069                | Self::MatchToIfLet { .. }
1070                | Self::FilterNext { .. }
1071                | Self::MapUnwrapOr { .. }
1072                | Self::ReplaceExpr { .. }
1073                | Self::RemoveStatement { .. }
1074                | Self::InsertStatement { .. }
1075                | Self::ReplaceStatement { .. }
1076                | Self::PluginTransform { .. }
1077        )
1078    }
1079
1080    /// Get target symbols from this spec.
1081    ///
1082    /// Returns references to all `MutationTargetSymbol` fields in this spec.
1083    /// Used for computing `affected_symbols` after mutation execution.
1084    pub fn get_targets(&self) -> Vec<&MutationTargetSymbol> {
1085        match self {
1086            // Specs with single target field
1087            Self::Rename { target, .. }
1088            | Self::AddField { target, .. }
1089            | Self::RemoveField { target, .. }
1090            | Self::ChangeVisibility { target, .. }
1091            | Self::AddDerive { target, .. }
1092            | Self::RemoveDerive { target, .. }
1093            | Self::AddVariant { target, .. }
1094            | Self::RemoveVariant { target, .. }
1095            | Self::AddMatchArm { target, .. }
1096            | Self::RemoveMatchArm { target, .. }
1097            | Self::ReplaceMatchArm { target, .. }
1098            | Self::AddStructLiteralField { target, .. }
1099            | Self::RemoveStructLiteralField { target, .. }
1100            | Self::AddItem { target, .. }
1101            | Self::RemoveItem { target, .. }
1102            | Self::ReplaceCode { target, .. }
1103            | Self::AddMethod { target, .. }
1104            | Self::RemoveMethod { target, .. }
1105            | Self::RemoveMod { target, .. }
1106            | Self::CreateMod { target, .. }
1107            | Self::ExtractTrait { target, .. }
1108            | Self::InlineTrait { target, .. }
1109            | Self::ReplaceType { target, .. }
1110            | Self::EnumToTrait { target, .. }
1111            | Self::MoveItem { target, .. }
1112            | Self::DuplicateFunction { target, .. }
1113            | Self::DuplicateStruct { target, .. }
1114            | Self::DuplicateEnum { target, .. }
1115            | Self::DuplicateModTree { target, .. } => vec![target],
1116
1117            // Specs with SymbolId fields (not MutationTargetSymbol)
1118            Self::AddSpec { .. } | Self::RemoveSpec { .. } | Self::ValidateSpec { .. } => vec![],
1119
1120            // Idiom transformations (module-scoped, no specific target)
1121            Self::OrganizeImports { .. }
1122            | Self::LoopToIterator { .. }
1123            | Self::UnwrapToQuestion { .. }
1124            | Self::AssignOp { .. }
1125            | Self::BoolSimplify { .. }
1126            | Self::CloneOnCopy { .. }
1127            | Self::CollapsibleIf { .. }
1128            | Self::NoOpArmToTodo { .. }
1129            | Self::ComparisonToMethod { .. }
1130            | Self::RedundantClosure { .. }
1131            | Self::IntroduceVariable { .. }
1132            | Self::ManualMap { .. }
1133            | Self::MatchToIfLet { .. }
1134            | Self::FilterNext { .. }
1135            | Self::MapUnwrapOr { .. }
1136            | Self::ReplaceExpr { .. }
1137            | Self::RemoveStatement { .. }
1138            | Self::InsertStatement { .. }
1139            | Self::ReplaceStatement { .. }
1140            | Self::PluginTransform { .. } => vec![],
1141        }
1142    }
1143}
1144
1145/// Scope for mutations
1146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1147#[serde(tag = "type")]
1148pub enum Scope {
1149    /// All modules in project
1150    #[default]
1151    Project,
1152
1153    /// Specific module by path
1154    Mod { path: SymbolPath },
1155
1156    /// Within a specific item
1157    Item {
1158        target: SymbolPath,
1159        item_name: String,
1160    },
1161}
1162
1163/// Visibility levels
1164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1165#[serde(rename_all = "snake_case")]
1166pub enum Visibility {
1167    #[default]
1168    Private,
1169    Pub,
1170    PubCrate,
1171    PubSuper,
1172    PubIn(String),
1173}
1174
1175impl Visibility {
1176    pub fn to_rust_syntax(&self) -> &str {
1177        match self {
1178            Self::Private => "",
1179            Self::Pub => "pub ",
1180            Self::PubCrate => "pub(crate) ",
1181            Self::PubSuper => "pub(super) ",
1182            Self::PubIn(_) => "pub(in ...) ", // Needs special handling
1183        }
1184    }
1185}
1186
1187/// Enum variant kinds
1188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1189#[serde(tag = "type")]
1190pub enum VariantKind {
1191    #[default]
1192    Unit,
1193    Tuple {
1194        types: Vec<String>,
1195    },
1196    Struct {
1197        fields: Vec<(String, String)>,
1198    },
1199}
1200
1201/// Position for inserting items
1202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1203#[serde(tag = "type")]
1204pub enum InsertPosition {
1205    #[default]
1206    Top,
1207    Bottom,
1208    AfterItem {
1209        name: String,
1210    },
1211    BeforeItem {
1212        name: String,
1213    },
1214}
1215
1216/// Position for inserting statements within a function
1217#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1218#[serde(rename_all = "snake_case")]
1219pub enum StmtInsertPosition {
1220    /// At the start of the function body
1221    Start,
1222    /// At the end of the function body (before return statement if any)
1223    #[default]
1224    End,
1225    /// Before a statement matching the reference pattern
1226    BeforePattern,
1227    /// After a statement matching the reference pattern
1228    AfterPattern,
1229}
1230
1231/// Self parameter for methods
1232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1233#[serde(rename_all = "lowercase")]
1234pub enum SelfParam {
1235    /// &self
1236    Ref,
1237    /// &mut self
1238    Mut,
1239    /// self (owned)
1240    Owned,
1241}
1242
1243fn default_body() -> String {
1244    "todo!()".to_string()
1245}
1246
1247fn default_noop_replacement() -> String {
1248    "todo".to_string()
1249}
1250
1251/// Relation for Spec TypeAlias
1252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1253pub struct SpecRelation {
1254    /// Relation kind
1255    pub kind: SpecRelationKind,
1256    /// Target type name (simple name, e.g., "User")
1257    pub target: String,
1258    /// Direct SymbolId for target (from Discover result)
1259    #[serde(default, skip_serializing_if = "Option::is_none")]
1260    pub symbol_id: Option<SymbolId>,
1261    /// Full SymbolPath for target (for disambiguation)
1262    #[serde(default, skip_serializing_if = "Option::is_none")]
1263    pub target_path: Option<SymbolPath>,
1264}
1265
1266impl SpecRelation {
1267    /// Create a new SpecRelation with just a name
1268    pub fn new(kind: SpecRelationKind, target: impl Into<String>) -> Self {
1269        Self {
1270            kind,
1271            target: target.into(),
1272            symbol_id: None,
1273            target_path: None,
1274        }
1275    }
1276
1277    /// Create a new SpecRelation with a SymbolPath
1278    pub fn with_path(kind: SpecRelationKind, target: impl Into<String>, path: SymbolPath) -> Self {
1279        Self {
1280            kind,
1281            target: target.into(),
1282            symbol_id: None,
1283            target_path: Some(path),
1284        }
1285    }
1286
1287    /// Create a new SpecRelation with a SymbolId
1288    pub fn with_symbol_id(
1289        kind: SpecRelationKind,
1290        target: impl Into<String>,
1291        symbol_id: SymbolId,
1292    ) -> Self {
1293        Self {
1294            kind,
1295            target: target.into(),
1296            symbol_id: Some(symbol_id),
1297            target_path: None,
1298        }
1299    }
1300}
1301
1302/// Relation kinds for Spec
1303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1304#[serde(rename_all = "PascalCase")]
1305pub enum SpecRelationKind {
1306    /// A depends on B (A needs B to function)
1307    DependsOn,
1308    /// A is related to B (semantic relationship)
1309    RelatedTo,
1310    /// A is part of B (aggregate membership)
1311    PartOf,
1312}
1313
1314impl SpecRelationKind {
1315    /// Get the Rust type name for this relation
1316    pub fn as_type_name(&self) -> &'static str {
1317        match self {
1318            Self::DependsOn => "DependsOn",
1319            Self::RelatedTo => "RelatedTo",
1320            Self::PartOf => "PartOf",
1321        }
1322    }
1323}
1324
1325// ItemKind is re-exported from ryo_source
1326
1327/// Extract item name from Rust code content
1328///
1329/// Parses common Rust item declarations to extract the name.
1330/// Used by `additive_identity()` for AddItem duplicate detection.
1331fn extract_item_name_from_content(content: &str) -> Option<String> {
1332    // Simple regex-free parsing for common patterns
1333    let trimmed = content.trim();
1334
1335    // Skip attributes and find the item declaration
1336    let mut lines = trimmed.lines();
1337    let mut decl_line = "";
1338    for line in lines.by_ref() {
1339        let line = line.trim();
1340        if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
1341            decl_line = line;
1342            break;
1343        }
1344    }
1345
1346    // Parse common patterns: pub? (struct|enum|fn|type|const|static|trait|impl|mod|use) NAME
1347    let tokens: Vec<&str> = decl_line.split_whitespace().collect();
1348    if tokens.is_empty() {
1349        return None;
1350    }
1351
1352    let mut idx = 0;
1353
1354    // Skip visibility
1355    if tokens.get(idx) == Some(&"pub") {
1356        idx += 1;
1357        // Skip pub(crate), pub(super), etc.
1358        if let Some(t) = tokens.get(idx) {
1359            if t.starts_with('(') {
1360                idx += 1;
1361            }
1362        }
1363    }
1364
1365    // Get keyword
1366    let keyword = tokens.get(idx)?;
1367    idx += 1;
1368
1369    match *keyword {
1370        "struct" | "enum" | "fn" | "type" | "const" | "static" | "trait" | "mod" => {
1371            // Next token is the name (may include generics)
1372            let name = tokens.get(idx)?;
1373            // Strip generics <...> and trailing punctuation
1374            let name = name.split('<').next().unwrap_or(name);
1375            let name = name.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
1376            Some(name.to_string())
1377        }
1378        "impl" => {
1379            // impl Trait for Type or impl Type
1380            // For impl blocks, include the full impl signature for identity
1381            let rest = tokens[idx..].join(" ");
1382
1383            // Extract impl signature (everything before '{' or '<')
1384            // This includes "Trait for Type" or just "Type"
1385            let impl_sig = rest
1386                .split(['{', '<'])
1387                .next()
1388                .map(|s| s.trim())
1389                .filter(|s| !s.is_empty());
1390
1391            // Also extract method names from the impl block to differentiate
1392            // multiple impl blocks for the same type
1393            let methods: Vec<&str> = content
1394                .lines()
1395                .filter_map(|line| {
1396                    let trimmed = line.trim();
1397                    if trimmed.starts_with("pub fn ") || trimmed.starts_with("fn ") {
1398                        let after_fn = trimmed
1399                            .strip_prefix("pub fn ")
1400                            .or_else(|| trimmed.strip_prefix("fn "))?;
1401                        let method_name = after_fn.split('(').next()?.trim();
1402                        Some(method_name)
1403                    } else {
1404                        None
1405                    }
1406                })
1407                .collect();
1408
1409            match (impl_sig, methods.is_empty()) {
1410                (Some(sig), false) => Some(format!(
1411                    "impl_{}::{}",
1412                    sig.replace(' ', "_"),
1413                    methods.join(",")
1414                )),
1415                // Use full signature for empty impl blocks (e.g., "impl_Status_for_Running")
1416                (Some(sig), true) => Some(format!("impl_{}", sig.replace(' ', "_"))),
1417                _ => None,
1418            }
1419        }
1420        "use" => {
1421            // use path::Name or use path::{A, B}
1422            // Return the full use statement as identity
1423            Some(tokens[idx..].join(" "))
1424        }
1425        _ => {
1426            // Unknown pattern, use first meaningful token
1427            Some(keyword.to_string())
1428        }
1429    }
1430}
1431
1432#[cfg(test)]
1433mod tests {
1434    use super::*;
1435
1436    #[test]
1437    fn test_mutation_spec_serialize() {
1438        // Test serialization with ByPath (which contains the symbol path)
1439        let spec = MutationSpec::Rename {
1440            target: MutationTargetSymbol::ByPath(Box::new(
1441                SymbolPath::parse("test_crate::old_name").unwrap(),
1442            )),
1443            to: "new_name".to_string(),
1444            scope: Scope::Project,
1445        };
1446
1447        let json = serde_json::to_string_pretty(&spec).unwrap();
1448        assert!(json.contains("Rename"), "JSON should contain Rename");
1449        assert!(
1450            json.contains("old_name"),
1451            "JSON should contain old_name in ByPath"
1452        );
1453        assert!(json.contains("new_name"), "JSON should contain new_name");
1454
1455        let parsed: MutationSpec = serde_json::from_str(&json).unwrap();
1456        assert_eq!(
1457            spec, parsed,
1458            "Round-trip serialization should preserve spec"
1459        );
1460    }
1461
1462    #[test]
1463    fn test_idiom_detection() {
1464        let spec = MutationSpec::OrganizeImports {
1465            module_id: None,
1466            deduplicate: true,
1467            merge_groups: true,
1468        };
1469
1470        assert!(spec.is_idiom());
1471        assert!(!spec.is_rename());
1472    }
1473}