1use super::registry::{MutationRegistry, RegistryError};
4use ryo_plugin_loader::{Capture, MatchResult, NodeKind, TextEdit, TransformContext, TransformDef};
5use std::path::Path;
6use syn::visit::Visit;
7
8#[derive(Debug, thiserror::Error)]
10pub enum PluginExecutorError {
11 #[error("Plugin not found: {0}")]
12 PluginNotFound(String),
13
14 #[error("Registry error: {0}")]
15 Registry(#[from] RegistryError),
16
17 #[error("Pattern match error: {0}")]
18 PatternMatch(String),
19
20 #[error("Transform error: {0}")]
21 Transform(String),
22
23 #[error("Parse error: {0}")]
24 Parse(String),
25}
26
27#[derive(Debug)]
29pub struct PluginExecutionResult {
30 pub matches_found: usize,
32 pub edits: Vec<TextEdit>,
34 pub new_source: Option<String>,
36}
37
38pub struct PluginExecutor<'a> {
40 registry: &'a mut MutationRegistry,
41}
42
43impl<'a> PluginExecutor<'a> {
44 pub fn new(registry: &'a mut MutationRegistry) -> Self {
46 Self { registry }
47 }
48
49 pub fn execute(
51 &mut self,
52 plugin_name: &str,
53 file_path: &Path,
54 source: &str,
55 ) -> Result<PluginExecutionResult, PluginExecutorError> {
56 let plugin = self
58 .registry
59 .get_plugin_mut(plugin_name)
60 .ok_or_else(|| PluginExecutorError::PluginNotFound(plugin_name.to_string()))?;
61
62 let syntax =
64 syn::parse_file(source).map_err(|e| PluginExecutorError::Parse(e.to_string()))?;
65
66 let pattern = &plugin.manifest.pattern;
68 let matches = match_pattern(pattern, &syntax, source)?;
69
70 if matches.is_empty() {
71 return Ok(PluginExecutionResult {
72 matches_found: 0,
73 edits: vec![],
74 new_source: None,
75 });
76 }
77
78 let fn_return_type = extract_function_return_type(&syntax);
80
81 let edits = match &plugin.manifest.transform {
83 TransformDef::Template(template) => {
84 expand_template(template, &matches)
86 }
87 TransformDef::WasmExecute => {
88 let context = TransformContext {
90 file_path: file_path.to_string_lossy().to_string(),
91 source_text: source.to_string(),
92 type_hints: vec![], fn_return_type,
94 };
95 plugin
96 .execute_transform(matches.clone(), context)
97 .map_err(|e| PluginExecutorError::Transform(e.to_string()))?
98 }
99 };
100
101 let new_source = apply_edits(source, &edits);
103
104 Ok(PluginExecutionResult {
105 matches_found: matches.len(),
106 edits,
107 new_source: Some(new_source),
108 })
109 }
110}
111
112fn extract_function_return_type(file: &syn::File) -> Option<String> {
119 use quote::ToTokens;
120
121 for item in &file.items {
122 if let syn::Item::Fn(func) = item {
123 if let syn::ReturnType::Type(_, ty) = &func.sig.output {
124 let type_str = ty.to_token_stream().to_string();
125 return Some(type_str);
126 }
127 }
128 }
129 None
130}
131
132struct PatternMatcher<'a> {
138 pattern: ParsedPattern,
139 source: &'a str,
140 matches: Vec<MatchResult>,
141}
142
143#[derive(Debug)]
145struct ParsedPattern {
146 node_type: String,
147 conditions: Vec<(String, String)>,
148}
149
150fn parse_pattern(pattern: &str) -> Result<ParsedPattern, PluginExecutorError> {
152 let bracket_start = pattern.find('[');
156 let bracket_end = pattern.rfind(']');
157
158 let (node_type, conditions) = match (bracket_start, bracket_end) {
159 (Some(start), Some(end)) if end > start => {
160 let node_type = pattern[..start].to_string();
161 let conds_str = &pattern[start + 1..end];
162 let conditions = parse_conditions(conds_str)?;
163 (node_type, conditions)
164 }
165 _ => (pattern.to_string(), vec![]),
166 };
167
168 Ok(ParsedPattern {
169 node_type,
170 conditions,
171 })
172}
173
174fn parse_conditions(conds_str: &str) -> Result<Vec<(String, String)>, PluginExecutorError> {
175 let mut conditions = Vec::new();
176
177 for cond in conds_str.split(',') {
178 let cond = cond.trim();
179 if cond.is_empty() {
180 continue;
181 }
182
183 let parts: Vec<&str> = cond.splitn(2, "==").collect();
185 if parts.len() == 2 {
186 let key = parts[0].trim().to_string();
187 let value = parts[1].trim().trim_matches('"').to_string();
188 conditions.push((key, value));
189 }
190 }
191
192 Ok(conditions)
193}
194
195fn match_pattern(
197 pattern: &str,
198 file: &syn::File,
199 source: &str,
200) -> Result<Vec<MatchResult>, PluginExecutorError> {
201 let parsed = parse_pattern(pattern)?;
202
203 let mut matcher = PatternMatcher {
204 pattern: parsed,
205 source,
206 matches: Vec::new(),
207 };
208
209 matcher.visit_file(file);
210
211 Ok(matcher.matches)
212}
213
214impl<'ast> Visit<'ast> for PatternMatcher<'ast> {
215 fn visit_expr(&mut self, expr: &'ast syn::Expr) {
216 if self.pattern.node_type == "binary_expr" {
218 if let syn::Expr::Binary(bin) = expr {
219 if self.matches_binary_expr(bin) {
220 self.add_binary_match(bin);
221 }
222 }
223 }
224
225 if self.pattern.node_type == "method_call" {
227 if let syn::Expr::MethodCall(call) = expr {
228 if self.matches_method_call(call) {
229 self.add_method_call_match(call);
230 }
231 }
232 }
233
234 syn::visit::visit_expr(self, expr);
236 }
237}
238
239impl<'a> PatternMatcher<'a> {
240 fn matches_binary_expr(&self, bin: &syn::ExprBinary) -> bool {
241 for (key, value) in &self.pattern.conditions {
242 match key.as_str() {
243 "op" => {
244 let op_str = op_to_string(&bin.op);
245 if op_str != *value {
246 return false;
247 }
248 }
249 "right" => {
250 let right_src = self.expr_to_string(&bin.right);
251 if right_src.trim() != *value {
252 return false;
253 }
254 }
255 "left" => {
256 let left_src = self.expr_to_string(&bin.left);
257 if left_src.trim() != *value {
258 return false;
259 }
260 }
261 _ => {}
262 }
263 }
264 true
265 }
266
267 fn add_binary_match(&mut self, bin: &syn::ExprBinary) {
268 let expr_str = self.expr_to_string_with_box(&syn::Expr::Binary(bin.clone()));
270
271 if let Some(start_byte) = self.source.find(&expr_str) {
273 let end_byte = start_byte + expr_str.len();
274
275 let left_text = self.expr_to_string(&bin.left);
276 let right_text = self.expr_to_string(&bin.right);
277
278 self.matches.push(MatchResult {
279 kind: NodeKind::BinaryExpr,
280 start_byte: start_byte as u64,
281 end_byte: end_byte as u64,
282 captures: vec![
283 Capture {
284 name: "left".to_string(),
285 start_byte: 0,
286 end_byte: 0,
287 text: left_text,
288 },
289 Capture {
290 name: "right".to_string(),
291 start_byte: 0,
292 end_byte: 0,
293 text: right_text,
294 },
295 ],
296 });
297 }
298 }
299
300 fn matches_method_call(&self, call: &syn::ExprMethodCall) -> bool {
301 for (key, value) in &self.pattern.conditions {
302 if key.as_str() == "method" {
303 let method_name = call.method.to_string();
304 if method_name != *value {
305 return false;
306 }
307 }
308 }
309 true
310 }
311
312 fn add_method_call_match(&mut self, call: &syn::ExprMethodCall) {
313 let receiver_text = self.expr_to_string(&call.receiver);
314 let method_text = call.method.to_string();
315
316 let method_pattern = format!(".{}(", method_text);
318
319 let mut search_start = 0;
321 while let Some(method_pos) = self.source[search_start..].find(&method_pattern) {
322 let method_abs_pos = search_start + method_pos;
323
324 let args_start = method_abs_pos + method_pattern.len();
326 let after_method = &self.source[args_start..];
327
328 if let Some(paren_pos) = self.find_matching_paren(after_method) {
329 let end_pos = args_start + paren_pos + 1;
330
331 let start_pos = self.find_expr_start(method_abs_pos);
334
335 let actual_receiver = self.source[start_pos..method_abs_pos].to_string();
337
338 let actual_normalized: String = actual_receiver
341 .chars()
342 .filter(|c| !c.is_whitespace())
343 .collect();
344 let expected_normalized: String = receiver_text
345 .chars()
346 .filter(|c| !c.is_whitespace())
347 .collect();
348
349 if actual_normalized == expected_normalized {
350 self.matches.push(MatchResult {
351 kind: NodeKind::MethodCall,
352 start_byte: start_pos as u64,
353 end_byte: end_pos as u64,
354 captures: vec![
355 Capture {
356 name: "receiver".to_string(),
357 start_byte: start_pos as u64,
358 end_byte: method_abs_pos as u64,
359 text: actual_receiver,
360 },
361 Capture {
362 name: "method".to_string(),
363 start_byte: method_abs_pos as u64,
364 end_byte: end_pos as u64,
365 text: method_text.clone(),
366 },
367 ],
368 });
369 return; }
371 }
372
373 search_start = method_abs_pos + 1;
374 }
375 }
376
377 fn find_expr_start(&self, from: usize) -> usize {
379 let bytes = self.source.as_bytes();
380 let mut pos = from;
381 let mut paren_depth = 0;
382 let mut bracket_depth = 0;
383
384 while pos > 0 {
385 pos -= 1;
386 let c = bytes[pos] as char;
387
388 match c {
389 ')' => paren_depth += 1,
390 '(' => {
391 if paren_depth > 0 {
392 paren_depth -= 1;
393 } else {
394 return pos + 1;
396 }
397 }
398 ']' => bracket_depth += 1,
399 '[' => {
400 if bracket_depth > 0 {
401 bracket_depth -= 1;
402 } else {
403 return pos + 1;
404 }
405 }
406 '=' | ';' | '{' | ',' | ':' if paren_depth == 0 && bracket_depth == 0 => {
408 let mut start = pos + 1;
410 while start < from && self.source.as_bytes()[start].is_ascii_whitespace() {
411 start += 1;
412 }
413 return start;
414 }
415 _ => {}
416 }
417 }
418
419 0
420 }
421
422 fn find_matching_paren(&self, s: &str) -> Option<usize> {
423 let mut depth = 1;
424 for (i, c) in s.chars().enumerate() {
425 match c {
426 '(' => depth += 1,
427 ')' => {
428 depth -= 1;
429 if depth == 0 {
430 return Some(i);
431 }
432 }
433 _ => {}
434 }
435 }
436 None
437 }
438
439 fn expr_to_string(&self, expr: &syn::Expr) -> String {
440 use quote::ToTokens;
441 expr.to_token_stream().to_string()
442 }
443
444 fn expr_to_string_with_box(&self, expr: &syn::Expr) -> String {
445 use quote::ToTokens;
446 expr.to_token_stream().to_string()
447 }
448}
449
450fn op_to_string(op: &syn::BinOp) -> String {
452 match op {
453 syn::BinOp::Eq(_) => "==".to_string(),
454 syn::BinOp::Ne(_) => "!=".to_string(),
455 syn::BinOp::Lt(_) => "<".to_string(),
456 syn::BinOp::Le(_) => "<=".to_string(),
457 syn::BinOp::Gt(_) => ">".to_string(),
458 syn::BinOp::Ge(_) => ">=".to_string(),
459 syn::BinOp::And(_) => "&&".to_string(),
460 syn::BinOp::Or(_) => "||".to_string(),
461 syn::BinOp::Add(_) => "+".to_string(),
462 syn::BinOp::Sub(_) => "-".to_string(),
463 syn::BinOp::Mul(_) => "*".to_string(),
464 syn::BinOp::Div(_) => "/".to_string(),
465 syn::BinOp::Rem(_) => "%".to_string(),
466 syn::BinOp::BitAnd(_) => "&".to_string(),
467 syn::BinOp::BitOr(_) => "|".to_string(),
468 syn::BinOp::BitXor(_) => "^".to_string(),
469 syn::BinOp::Shl(_) => "<<".to_string(),
470 syn::BinOp::Shr(_) => ">>".to_string(),
471 syn::BinOp::AddAssign(_) => "+=".to_string(),
472 syn::BinOp::SubAssign(_) => "-=".to_string(),
473 syn::BinOp::MulAssign(_) => "*=".to_string(),
474 syn::BinOp::DivAssign(_) => "/=".to_string(),
475 syn::BinOp::RemAssign(_) => "%=".to_string(),
476 syn::BinOp::BitAndAssign(_) => "&=".to_string(),
477 syn::BinOp::BitOrAssign(_) => "|=".to_string(),
478 syn::BinOp::BitXorAssign(_) => "^=".to_string(),
479 syn::BinOp::ShlAssign(_) => "<<=".to_string(),
480 syn::BinOp::ShrAssign(_) => ">>=".to_string(),
481 _ => "?".to_string(),
482 }
483}
484
485fn expand_template(template: &str, matches: &[MatchResult]) -> Vec<TextEdit> {
491 let mut edits = Vec::new();
492
493 for m in matches {
494 let mut replacement = template.to_string();
495
496 for capture in &m.captures {
498 let placeholder = format!("{{{{{}}}}}", capture.name);
499 replacement = replacement.replace(&placeholder, &capture.text);
500 }
501
502 edits.push(TextEdit {
503 start_byte: m.start_byte,
504 end_byte: m.end_byte,
505 replacement,
506 });
507 }
508
509 edits
510}
511
512fn apply_edits(source: &str, edits: &[TextEdit]) -> String {
518 let mut result = source.to_string();
519
520 let mut sorted_edits: Vec<_> = edits.iter().collect();
522 sorted_edits.sort_by_key(|b| std::cmp::Reverse(b.start_byte));
523
524 for edit in sorted_edits {
525 let start = edit.start_byte as usize;
526 let end = edit.end_byte as usize;
527
528 if start <= end && end <= result.len() {
529 result.replace_range(start..end, &edit.replacement);
530 }
531 }
532
533 result
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn test_parse_pattern() {
542 let pattern = "binary_expr[op==\"==\", right==\"true\"]";
543 let parsed = parse_pattern(pattern).unwrap();
544
545 assert_eq!(parsed.node_type, "binary_expr");
546 assert_eq!(parsed.conditions.len(), 2);
547 }
548
549 #[test]
550 fn test_expand_template() {
551 let template = "{{left}}";
552 let matches = vec![MatchResult {
553 kind: NodeKind::BinaryExpr,
554 start_byte: 0,
555 end_byte: 10,
556 captures: vec![Capture {
557 name: "left".to_string(),
558 start_byte: 0,
559 end_byte: 5,
560 text: "is_ok".to_string(),
561 }],
562 }];
563
564 let edits = expand_template(template, &matches);
565 assert_eq!(edits.len(), 1);
566 assert_eq!(edits[0].replacement, "is_ok");
567 }
568
569 #[test]
570 fn test_op_to_string() {
571 let eq_op = syn::BinOp::Eq(Default::default());
572 assert_eq!(op_to_string(&eq_op), "==");
573 }
574}