Skip to main content

ryo_executor/executor/registry/converters/
stmt.rs

1//! StmtConverter: Converts statement-level MutationSpec variants to Mutations
2//!
3//! Handles:
4//! - ReplaceExpr
5//! - RemoveStatement
6//! - InsertStatement
7//! - ReplaceStatement
8
9use crate::engine::ASTRegApply;
10use crate::executor::registry::converters::ResolveTargetSymbol;
11use crate::executor::registry::{ConvertError, MutationConverter};
12use crate::executor::spec::{MutationSpec, StmtInsertPosition};
13use ryo_analysis::AnalysisContext;
14use ryo_analysis::SymbolPath;
15use ryo_mutations::basic::stmt::{
16    InsertPosition, InsertStatementMutation, RemoveStatementMutation, ReplaceExprAtMutation,
17    ReplaceExprMutation, ReplaceStatementMutation,
18};
19use ryo_source::pure::{PureExpr, PureStmt, ToPure};
20
21/// Converter for statement-level mutations
22#[derive(Debug, Clone, Default)]
23pub struct StmtConverter;
24
25impl StmtConverter {
26    pub fn new() -> Self {
27        Self
28    }
29
30    /// Parse a Rust expression string into PureExpr
31    pub fn parse_expr(expr_str: &str) -> Result<PureExpr, ConvertError> {
32        let expr: syn::Expr = syn::parse_str(expr_str).map_err(|e| {
33            ConvertError::Parse(format!("Failed to parse expression '{}': {}", expr_str, e))
34        })?;
35        Ok(expr.to_pure())
36    }
37
38    /// Parse a Rust statement string into PureStmt
39    pub fn parse_stmt(stmt_str: &str) -> Result<PureStmt, ConvertError> {
40        // Try parsing as a statement first
41        let stmt_with_semi = if stmt_str.ends_with(';') {
42            stmt_str.to_string()
43        } else {
44            format!("{};", stmt_str)
45        };
46
47        let stmt: syn::Stmt = syn::parse_str(&stmt_with_semi).map_err(|e| {
48            ConvertError::Parse(format!("Failed to parse statement '{}': {}", stmt_str, e))
49        })?;
50
51        // Use ToPure trait implementation for syn::Stmt
52        Ok(stmt.to_pure())
53    }
54
55    /// Convert StmtInsertPosition to internal InsertPosition
56    fn convert_position(pos: &StmtInsertPosition) -> InsertPosition {
57        match pos {
58            StmtInsertPosition::Start => InsertPosition::Start,
59            StmtInsertPosition::End => InsertPosition::End,
60            StmtInsertPosition::BeforePattern => InsertPosition::BeforePattern,
61            StmtInsertPosition::AfterPattern => InsertPosition::AfterPattern,
62        }
63    }
64}
65
66// StmtConverter uses the default implementation of ResolveTargetSymbol
67impl ResolveTargetSymbol for StmtConverter {}
68
69impl MutationConverter for StmtConverter {
70    fn spec_kinds(&self) -> &'static [&'static str] {
71        &[
72            "ReplaceExpr",
73            "RemoveStatement",
74            "InsertStatement",
75            "ReplaceStatement",
76        ]
77    }
78
79    fn convert_v2(
80        &self,
81        spec: &MutationSpec,
82        ctx: &AnalysisContext,
83    ) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
84        match spec {
85            MutationSpec::ReplaceExpr {
86                fn_id,
87                old_expr,
88                new_expr,
89                replace_all,
90                symbol_path,
91                ..
92            } => {
93                let new = Self::parse_expr(new_expr)?;
94
95                // If symbol_path is provided, use position-based replacement
96                if let Some(path_str) = symbol_path {
97                    let path = SymbolPath::parse(path_str).map_err(|e| {
98                        ConvertError::Parse(format!("Invalid symbol_path '{}': {}", path_str, e))
99                    })?;
100
101                    if !path.has_body_segment() {
102                        return Err(ConvertError::Parse(format!(
103                            "symbol_path '{}' must contain $body segment for position-based replacement",
104                            path_str
105                        )));
106                    }
107
108                    let (fn_path, indices) = path.split_at_body().ok_or_else(|| {
109                        ConvertError::Parse(format!(
110                            "Failed to extract function path from symbol_path '{}'",
111                            path_str
112                        ))
113                    })?;
114
115                    // Lookup SymbolId from function path
116                    let fn_id = ctx.registry.lookup(&fn_path).ok_or_else(|| {
117                        ConvertError::TargetNotFound(format!(
118                            "Function '{}' not found in registry",
119                            fn_path
120                        ))
121                    })?;
122
123                    let mutation = ReplaceExprAtMutation::new(fn_id, indices, new);
124                    return Ok(vec![Box::new(mutation)]);
125                }
126
127                let old = Self::parse_expr(old_expr)?;
128
129                // If fn_id is None, generate mutations for all functions
130                let mutations: Vec<Box<dyn ASTRegApply>> = if let Some(id) = fn_id {
131                    let mut mutation = ReplaceExprMutation::new(old.clone(), new.clone(), *id);
132                    mutation.replace_all = *replace_all;
133                    vec![Box::new(mutation)]
134                } else {
135                    use ryo_analysis::SymbolKind;
136                    ctx.registry
137                        .iter()
138                        .filter(|(id, _)| {
139                            matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
140                        })
141                        .map(|(id, _)| {
142                            let mut mutation =
143                                ReplaceExprMutation::new(old.clone(), new.clone(), id);
144                            mutation.replace_all = *replace_all;
145                            Box::new(mutation) as Box<dyn ASTRegApply>
146                        })
147                        .collect()
148                };
149
150                Ok(mutations)
151            }
152
153            MutationSpec::RemoveStatement {
154                fn_id,
155                pattern,
156                remove_all,
157                ..
158            } => {
159                let target_stmt = Self::parse_stmt(pattern)?;
160
161                // If fn_id is None, generate mutations for all functions
162                let mutations: Vec<Box<dyn ASTRegApply>> = if let Some(id) = fn_id {
163                    let mut mutation =
164                        RemoveStatementMutation::new(target_stmt.clone(), pattern.clone(), *id);
165                    mutation.remove_all = *remove_all;
166                    vec![Box::new(mutation)]
167                } else {
168                    use ryo_analysis::SymbolKind;
169                    ctx.registry
170                        .iter()
171                        .filter(|(id, _)| {
172                            matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
173                        })
174                        .map(|(id, _)| {
175                            let mut mutation = RemoveStatementMutation::new(
176                                target_stmt.clone(),
177                                pattern.clone(),
178                                id,
179                            );
180                            mutation.remove_all = *remove_all;
181                            Box::new(mutation) as Box<dyn ASTRegApply>
182                        })
183                        .collect()
184                };
185
186                Ok(mutations)
187            }
188
189            MutationSpec::InsertStatement {
190                fn_id,
191                stmt,
192                position,
193                reference_pattern,
194                ..
195            } => {
196                let pure_stmt = Self::parse_stmt(stmt)?;
197                let reference_stmt = reference_pattern
198                    .as_ref()
199                    .map(|p| Self::parse_stmt(p))
200                    .transpose()?;
201
202                let mut mutation = InsertStatementMutation::new(pure_stmt, *fn_id);
203                mutation.position = Self::convert_position(position);
204                mutation.reference_stmt = reference_stmt;
205
206                Ok(vec![Box::new(mutation)])
207            }
208
209            MutationSpec::ReplaceStatement {
210                fn_id,
211                old_stmt,
212                new_stmt,
213                ..
214            } => {
215                let old = Self::parse_stmt(old_stmt)?;
216                let new = Self::parse_stmt(new_stmt)?;
217
218                // If fn_id is None, generate mutations for all functions
219                let mutations: Vec<Box<dyn ASTRegApply>> = if let Some(id) = fn_id {
220                    vec![Box::new(ReplaceStatementMutation::new(
221                        old.clone(),
222                        new.clone(),
223                        *id,
224                    ))]
225                } else {
226                    use ryo_analysis::SymbolKind;
227                    ctx.registry
228                        .iter()
229                        .filter(|(id, _)| {
230                            matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
231                        })
232                        .map(|(id, _)| {
233                            Box::new(ReplaceStatementMutation::new(old.clone(), new.clone(), id))
234                                as Box<dyn ASTRegApply>
235                        })
236                        .collect()
237                };
238
239                Ok(mutations)
240            }
241
242            _ => Err(ConvertError::TypeMismatch {
243                expected: "ReplaceExpr|RemoveStatement|InsertStatement|ReplaceStatement",
244                actual: spec.kind_name().to_string(),
245            }),
246        }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use ryo_symbol::SymbolId;
254
255    #[test]
256    fn test_stmt_converter_spec_kinds() {
257        let converter = StmtConverter::new();
258        let kinds = converter.spec_kinds();
259        assert!(kinds.contains(&"ReplaceExpr"));
260        assert!(kinds.contains(&"RemoveStatement"));
261        assert!(kinds.contains(&"InsertStatement"));
262        assert!(kinds.contains(&"ReplaceStatement"));
263    }
264
265    #[test]
266    fn test_stmt_converter_can_handle() {
267        let converter = StmtConverter::new();
268
269        let replace_expr_spec = MutationSpec::ReplaceExpr {
270            module_id: None,
271            fn_id: None,
272            old_expr: "a + b".into(),
273            new_expr: "c + d".into(),
274            replace_all: true,
275            symbol_path: None,
276        };
277        assert!(converter.can_handle(&replace_expr_spec));
278
279        let remove_stmt_spec = MutationSpec::RemoveStatement {
280            module_id: None,
281            fn_id: None,
282            pattern: "println!".into(),
283            remove_all: true,
284            symbol_path: None,
285        };
286        assert!(converter.can_handle(&remove_stmt_spec));
287
288        let insert_stmt_spec = MutationSpec::InsertStatement {
289            module_id: None,
290            fn_id: SymbolId::default(),
291            stmt: "let x = 1".into(),
292            position: StmtInsertPosition::Start,
293            reference_pattern: None,
294            symbol_path: None,
295        };
296        assert!(converter.can_handle(&insert_stmt_spec));
297
298        let replace_stmt_spec = MutationSpec::ReplaceStatement {
299            module_id: None,
300            fn_id: None,
301            old_stmt: "let x = 1".into(),
302            new_stmt: "let x = 2".into(),
303            symbol_path: None,
304        };
305        assert!(converter.can_handle(&replace_stmt_spec));
306    }
307}