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}