ryo_executor/executor/registry/converters/
stmt.rs1use 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#[derive(Debug, Clone, Default)]
23pub struct StmtConverter;
24
25impl StmtConverter {
26 pub fn new() -> Self {
27 Self
28 }
29
30 pub fn parse_expr(expr_str: &str) -> Result<PureExpr, ConvertError> {
36 let expr: syn::Expr = syn::parse_str(expr_str).map_err(|e| {
37 ConvertError::Parse(format!("Failed to parse expression '{}': {}", expr_str, e))
38 })?;
39 Ok(expr.to_pure())
40 }
41
42 pub fn parse_stmt(stmt_str: &str) -> Result<PureStmt, ConvertError> {
46 let stmt_with_semi = if stmt_str.ends_with(';') {
48 stmt_str.to_string()
49 } else {
50 format!("{};", stmt_str)
51 };
52
53 let stmt: syn::Stmt = syn::parse_str(&stmt_with_semi).map_err(|e| {
54 ConvertError::Parse(format!("Failed to parse statement '{}': {}", stmt_str, e))
55 })?;
56
57 Ok(stmt.to_pure())
59 }
60
61 pub fn build_verbatim_expr(expr_str: &str) -> Result<PureExpr, ConvertError> {
72 let _: syn::Expr = syn::parse_str(expr_str).map_err(|e| {
74 ConvertError::Parse(format!("Failed to parse expression '{}': {}", expr_str, e))
75 })?;
76 Ok(PureExpr::Verbatim(expr_str.to_string()))
77 }
78
79 pub fn build_verbatim_stmt(stmt_str: &str) -> Result<PureStmt, ConvertError> {
85 let stmt_with_semi = if stmt_str.ends_with(';') {
86 stmt_str.to_string()
87 } else {
88 format!("{};", stmt_str)
89 };
90 let _: syn::Stmt = syn::parse_str(&stmt_with_semi).map_err(|e| {
91 ConvertError::Parse(format!("Failed to parse statement '{}': {}", stmt_str, e))
92 })?;
93 Ok(PureStmt::Verbatim(stmt_with_semi))
101 }
102
103 pub fn build_verbatim_replacement_stmt(stmt_str: &str) -> Result<PureStmt, ConvertError> {
121 let trimmed = stmt_str.trim_end();
122 if !trimmed.ends_with(';') {
123 match syn::parse_str::<syn::Expr>(trimmed) {
124 Ok(expr) if !matches!(expr, syn::Expr::Let(_)) => {
128 return Ok(PureStmt::Verbatim(trimmed.to_string()));
129 }
130 _ => {}
131 }
132 }
133 Self::build_verbatim_stmt(stmt_str)
134 }
135
136 fn convert_position(pos: &StmtInsertPosition) -> InsertPosition {
138 match pos {
139 StmtInsertPosition::Start => InsertPosition::Start,
140 StmtInsertPosition::End => InsertPosition::End,
141 StmtInsertPosition::BeforePattern => InsertPosition::BeforePattern,
142 StmtInsertPosition::AfterPattern => InsertPosition::AfterPattern,
143 }
144 }
145}
146
147impl ResolveTargetSymbol for StmtConverter {}
149
150impl MutationConverter for StmtConverter {
151 fn spec_kinds(&self) -> &'static [&'static str] {
152 &[
153 "ReplaceExpr",
154 "RemoveStatement",
155 "InsertStatement",
156 "ReplaceStatement",
157 ]
158 }
159
160 fn convert_v2(
161 &self,
162 spec: &MutationSpec,
163 ctx: &AnalysisContext,
164 ) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
165 match spec {
166 MutationSpec::ReplaceExpr {
167 fn_id,
168 old_expr,
169 new_expr,
170 replace_all,
171 symbol_path,
172 ..
173 } => {
174 let new = Self::build_verbatim_expr(new_expr)?;
178
179 if let Some(path_str) = symbol_path {
181 let path = SymbolPath::parse(path_str).map_err(|e| {
182 ConvertError::Parse(format!("Invalid symbol_path '{}': {}", path_str, e))
183 })?;
184
185 if !path.has_body_segment() {
186 return Err(ConvertError::Parse(format!(
187 "symbol_path '{}' must contain $body segment for position-based replacement",
188 path_str
189 )));
190 }
191
192 let (fn_path, indices) = path.split_at_body().ok_or_else(|| {
193 ConvertError::Parse(format!(
194 "Failed to extract function path from symbol_path '{}'",
195 path_str
196 ))
197 })?;
198
199 let fn_id = ctx.registry.lookup(&fn_path).ok_or_else(|| {
201 ConvertError::TargetNotFound(format!(
202 "Function '{}' not found in registry",
203 fn_path
204 ))
205 })?;
206
207 let mutation = ReplaceExprAtMutation::new(fn_id, indices, new);
208 return Ok(vec![Box::new(mutation)]);
209 }
210
211 let old = Self::parse_expr(old_expr)?;
212
213 let mutations: Vec<Box<dyn ASTRegApply>> = if let Some(id) = fn_id {
215 let mut mutation = ReplaceExprMutation::new(old.clone(), new.clone(), *id);
216 mutation.replace_all = *replace_all;
217 vec![Box::new(mutation)]
218 } else {
219 use ryo_analysis::SymbolKind;
220 ctx.registry
221 .iter()
222 .filter(|(id, _)| {
223 matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
224 })
225 .map(|(id, _)| {
226 let mut mutation =
227 ReplaceExprMutation::new(old.clone(), new.clone(), id);
228 mutation.replace_all = *replace_all;
229 Box::new(mutation) as Box<dyn ASTRegApply>
230 })
231 .collect()
232 };
233
234 Ok(mutations)
235 }
236
237 MutationSpec::RemoveStatement {
238 fn_id,
239 pattern,
240 remove_all,
241 ..
242 } => {
243 let target_stmt = Self::parse_stmt(pattern)?;
244
245 let mutations: Vec<Box<dyn ASTRegApply>> = if let Some(id) = fn_id {
247 let mut mutation =
248 RemoveStatementMutation::new(target_stmt.clone(), pattern.clone(), *id);
249 mutation.remove_all = *remove_all;
250 vec![Box::new(mutation)]
251 } else {
252 use ryo_analysis::SymbolKind;
253 ctx.registry
254 .iter()
255 .filter(|(id, _)| {
256 matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
257 })
258 .map(|(id, _)| {
259 let mut mutation = RemoveStatementMutation::new(
260 target_stmt.clone(),
261 pattern.clone(),
262 id,
263 );
264 mutation.remove_all = *remove_all;
265 Box::new(mutation) as Box<dyn ASTRegApply>
266 })
267 .collect()
268 };
269
270 Ok(mutations)
271 }
272
273 MutationSpec::InsertStatement {
274 fn_id,
275 stmt,
276 position,
277 reference_pattern,
278 ..
279 } => {
280 let pure_stmt = Self::build_verbatim_stmt(stmt)?;
284 let reference_stmt = reference_pattern
285 .as_ref()
286 .map(|p| Self::parse_stmt(p))
287 .transpose()?;
288
289 let mut mutation = InsertStatementMutation::new(pure_stmt, *fn_id);
290 mutation.position = Self::convert_position(position);
291 mutation.reference_stmt = reference_stmt;
292
293 Ok(vec![Box::new(mutation)])
294 }
295
296 MutationSpec::ReplaceStatement {
297 fn_id,
298 old_stmt,
299 new_stmt,
300 ..
301 } => {
302 let old = Self::parse_stmt(old_stmt)?;
303 let new = Self::build_verbatim_replacement_stmt(new_stmt)?;
307
308 let mutations: Vec<Box<dyn ASTRegApply>> = if let Some(id) = fn_id {
310 vec![Box::new(ReplaceStatementMutation::new(
311 old.clone(),
312 new.clone(),
313 *id,
314 ))]
315 } else {
316 use ryo_analysis::SymbolKind;
317 ctx.registry
318 .iter()
319 .filter(|(id, _)| {
320 matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
321 })
322 .map(|(id, _)| {
323 Box::new(ReplaceStatementMutation::new(old.clone(), new.clone(), id))
324 as Box<dyn ASTRegApply>
325 })
326 .collect()
327 };
328
329 Ok(mutations)
330 }
331
332 _ => Err(ConvertError::TypeMismatch {
333 expected: "ReplaceExpr|RemoveStatement|InsertStatement|ReplaceStatement",
334 actual: spec.kind_name().to_string(),
335 }),
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use ryo_symbol::SymbolId;
344
345 #[test]
346 fn test_stmt_converter_spec_kinds() {
347 let converter = StmtConverter::new();
348 let kinds = converter.spec_kinds();
349 assert!(kinds.contains(&"ReplaceExpr"));
350 assert!(kinds.contains(&"RemoveStatement"));
351 assert!(kinds.contains(&"InsertStatement"));
352 assert!(kinds.contains(&"ReplaceStatement"));
353 }
354
355 #[test]
356 fn test_stmt_converter_can_handle() {
357 let converter = StmtConverter::new();
358
359 let replace_expr_spec = MutationSpec::ReplaceExpr {
360 module_id: None,
361 fn_id: None,
362 old_expr: "a + b".into(),
363 new_expr: "c + d".into(),
364 replace_all: true,
365 symbol_path: None,
366 };
367 assert!(converter.can_handle(&replace_expr_spec));
368
369 let remove_stmt_spec = MutationSpec::RemoveStatement {
370 module_id: None,
371 fn_id: None,
372 pattern: "println!".into(),
373 remove_all: true,
374 symbol_path: None,
375 };
376 assert!(converter.can_handle(&remove_stmt_spec));
377
378 let insert_stmt_spec = MutationSpec::InsertStatement {
379 module_id: None,
380 fn_id: SymbolId::default(),
381 stmt: "let x = 1".into(),
382 position: StmtInsertPosition::Start,
383 reference_pattern: None,
384 symbol_path: None,
385 };
386 assert!(converter.can_handle(&insert_stmt_spec));
387
388 let replace_stmt_spec = MutationSpec::ReplaceStatement {
389 module_id: None,
390 fn_id: None,
391 old_stmt: "let x = 1".into(),
392 new_stmt: "let x = 2".into(),
393 symbol_path: None,
394 };
395 assert!(converter.can_handle(&replace_stmt_spec));
396 }
397
398 #[test]
405 fn build_verbatim_expr_preserves_raw_bytes_with_unusual_spacing() {
406 let raw = "html ! { < div id = \"x\" > { value } < / div > }";
409 match StmtConverter::build_verbatim_expr(raw).unwrap() {
410 PureExpr::Verbatim(s) => assert_eq!(s, raw),
411 other => panic!("expected PureExpr::Verbatim, got {:?}", other),
412 }
413 }
414
415 #[test]
416 fn build_verbatim_stmt_appends_trailing_semicolon_if_missing() {
417 match StmtConverter::build_verbatim_stmt("let x = 1").unwrap() {
420 PureStmt::Verbatim(s) => assert_eq!(s, "let x = 1;"),
421 other => panic!("expected PureStmt::Verbatim, got {:?}", other),
422 }
423 }
424
425 #[test]
426 fn build_verbatim_stmt_preserves_user_terminator_and_interior_spacing() {
427 let raw = "let x = foo ! { a , b } ;";
429 match StmtConverter::build_verbatim_stmt(raw).unwrap() {
430 PureStmt::Verbatim(s) => assert_eq!(s, raw),
431 other => panic!("expected PureStmt::Verbatim, got {:?}", other),
432 }
433 }
434
435 #[test]
436 fn build_verbatim_expr_rejects_syntactically_invalid_input() {
437 assert!(StmtConverter::build_verbatim_expr("let x = ;").is_err());
440 }
441
442 #[test]
443 fn build_verbatim_stmt_rejects_syntactically_invalid_input() {
444 assert!(StmtConverter::build_verbatim_stmt("@@@ nonsense @@@").is_err());
445 }
446
447 #[test]
452 fn build_verbatim_replacement_stmt_honors_tail_expr_intent() {
453 match StmtConverter::build_verbatim_replacement_stmt("sum").unwrap() {
459 PureStmt::Verbatim(s) => assert_eq!(s, "sum"),
460 other => panic!("expected PureStmt::Verbatim, got {:?}", other),
461 }
462 }
463
464 #[test]
465 fn build_verbatim_replacement_stmt_keeps_semi_completion_for_let() {
466 match StmtConverter::build_verbatim_replacement_stmt("let new_value = 20").unwrap() {
471 PureStmt::Verbatim(s) => assert_eq!(s, "let new_value = 20;"),
472 other => panic!("expected PureStmt::Verbatim, got {:?}", other),
473 }
474 }
475
476 #[test]
477 fn build_verbatim_replacement_stmt_passes_explicit_terminator_through() {
478 match StmtConverter::build_verbatim_replacement_stmt("foo();").unwrap() {
480 PureStmt::Verbatim(s) => assert_eq!(s, "foo();"),
481 other => panic!("expected PureStmt::Verbatim, got {:?}", other),
482 }
483 }
484}