Skip to main content

ryo_mutations/basic/
item.rs

1//! AddItemMutation, RemoveItemMutation, and MoveItemMutation
2//!
3//! Add, remove, or move arbitrary items from source code.
4
5use crate::Mutation;
6use ryo_source::pure::PureItem;
7use ryo_source::ItemKind;
8use ryo_symbol::{SymbolId, SymbolPath};
9
10/// Add an item from raw source code content
11#[derive(Debug, Clone)]
12pub struct AddItemMutation {
13    /// Parent module SymbolId where the item should be added
14    pub parent: SymbolId,
15    /// Raw source code content to parse and add
16    pub content: String,
17}
18
19impl AddItemMutation {
20    pub fn new(parent: SymbolId, content: impl Into<String>) -> Self {
21        Self {
22            parent,
23            content: content.into(),
24        }
25    }
26}
27
28impl Mutation for AddItemMutation {
29    fn mutation_type(&self) -> &'static str {
30        "AddItem"
31    }
32
33    fn describe(&self) -> String {
34        format!("Add item to {}", self.parent)
35    }
36
37    fn box_clone(&self) -> Box<dyn Mutation> {
38        Box::new(self.clone())
39    }
40}
41
42/// Add multiple items directly from AST (no parsing overhead)
43///
44/// Unlike AddItemMutation which parses string content, this mutation
45/// takes pre-built PureItem AST nodes. Used by Duplicate operations
46/// to avoid AST → String → AST round-trips.
47#[derive(Debug, Clone)]
48pub struct AddPureItemsMutation {
49    /// Parent module SymbolId where items should be added
50    pub parent: SymbolId,
51    /// Pre-built AST items to add
52    pub items: Vec<PureItem>,
53}
54
55impl AddPureItemsMutation {
56    pub fn new(parent: SymbolId, items: Vec<PureItem>) -> Self {
57        Self { parent, items }
58    }
59}
60
61impl Mutation for AddPureItemsMutation {
62    fn mutation_type(&self) -> &'static str {
63        "AddPureItems"
64    }
65
66    fn describe(&self) -> String {
67        format!("Add {} items to {}", self.items.len(), self.parent)
68    }
69
70    fn box_clone(&self) -> Box<dyn Mutation> {
71        Box::new(self.clone())
72    }
73}
74
75/// Replace an item by SymbolId with raw source bytes, routed through
76/// `PureItem::Verbatim` so trivia (line comments, blank lines, DSL macro
77/// spacing) survives the codegen round-trip.
78///
79/// This is the producer side of the β-route ReplaceCode escape hatch:
80/// the executor finds the parent module of `symbol_id`, locates the item
81/// position, and substitutes it with a `PureItem::Verbatim(raw)` which
82/// `PureFile::to_source` lays out via stub + splice.
83#[derive(Debug, Clone)]
84pub struct ReplaceCodeMutation {
85    /// SymbolId of the item to replace.
86    pub symbol_id: SymbolId,
87    /// Raw replacement bytes, preserved verbatim.
88    pub raw: String,
89}
90
91impl ReplaceCodeMutation {
92    pub fn new(symbol_id: SymbolId, raw: impl Into<String>) -> Self {
93        Self {
94            symbol_id,
95            raw: raw.into(),
96        }
97    }
98}
99
100impl Mutation for ReplaceCodeMutation {
101    fn mutation_type(&self) -> &'static str {
102        "ReplaceCode"
103    }
104
105    fn describe(&self) -> String {
106        format!(
107            "Replace {} verbatim ({} bytes)",
108            self.symbol_id,
109            self.raw.len()
110        )
111    }
112
113    fn box_clone(&self) -> Box<dyn Mutation> {
114        Box::new(self.clone())
115    }
116}
117
118/// Remove an item by SymbolId
119#[derive(Debug, Clone)]
120pub struct RemoveItemMutation {
121    /// SymbolId of the item to remove (required, O(1) lookup)
122    pub symbol_id: SymbolId,
123    /// Kind of item to remove
124    pub item_kind: ItemKind,
125}
126
127impl RemoveItemMutation {
128    pub fn new(symbol_id: SymbolId, item_kind: ItemKind) -> Self {
129        Self {
130            symbol_id,
131            item_kind,
132        }
133    }
134}
135
136impl Mutation for RemoveItemMutation {
137    fn mutation_type(&self) -> &'static str {
138        "RemoveItem"
139    }
140
141    fn describe(&self) -> String {
142        format!("Remove {:?} ({})", self.item_kind, self.symbol_id)
143    }
144
145    fn box_clone(&self) -> Box<dyn Mutation> {
146        Box::new(self.clone())
147    }
148}
149
150/// Move an item from one module to another
151#[derive(Debug, Clone)]
152pub struct MoveItemMutation {
153    /// Source module as SymbolPath
154    pub source: SymbolPath,
155    /// Target module as SymbolPath
156    pub target: SymbolPath,
157    /// Item name to move
158    pub item_name: String,
159    /// Kind of item to move
160    pub item_kind: ItemKind,
161    /// Whether to add use statement in source file
162    pub add_use: bool,
163}
164
165impl MoveItemMutation {
166    pub fn new(
167        source: SymbolPath,
168        target: SymbolPath,
169        item_name: impl Into<String>,
170        item_kind: ItemKind,
171        add_use: bool,
172    ) -> Self {
173        Self {
174            source,
175            target,
176            item_name: item_name.into(),
177            item_kind,
178            add_use,
179        }
180    }
181}
182
183impl Mutation for MoveItemMutation {
184    fn mutation_type(&self) -> &'static str {
185        "MoveItem"
186    }
187
188    fn describe(&self) -> String {
189        format!(
190            "Move {:?} '{}' from {} to {}",
191            self.item_kind, self.item_name, self.source, self.target
192        )
193    }
194
195    fn box_clone(&self) -> Box<dyn Mutation> {
196        Box::new(self.clone())
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_add_item_mutation_describe() {
206        let mutation = AddItemMutation::new(SymbolId::default(), "pub struct Foo {}");
207        assert!(mutation.describe().contains("Add item"));
208    }
209
210    #[test]
211    fn test_remove_item_mutation_describe() {
212        let mutation = RemoveItemMutation::new(SymbolId::default(), ItemKind::Struct);
213        assert!(mutation.describe().contains("Struct"));
214    }
215}