Skip to main content

ryo_plugin_runtime/
executor.rs

1//! Plugin Executor - Execute WASM plugins against source code
2
3use super::registry::{MutationRegistry, RegistryError};
4use ryo_plugin_loader::{Capture, MatchResult, NodeKind, TextEdit, TransformContext, TransformDef};
5use std::path::Path;
6use syn::visit::Visit;
7
8/// Error type for plugin execution
9#[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/// Result of executing a plugin on a source file
28#[derive(Debug)]
29pub struct PluginExecutionResult {
30    /// Number of matches found
31    pub matches_found: usize,
32    /// Text edits to apply
33    pub edits: Vec<TextEdit>,
34    /// New source after applying edits
35    pub new_source: Option<String>,
36}
37
38/// Execute WASM plugins against source code
39pub struct PluginExecutor<'a> {
40    registry: &'a mut MutationRegistry,
41}
42
43impl<'a> PluginExecutor<'a> {
44    /// Create a new plugin executor with a registry reference
45    pub fn new(registry: &'a mut MutationRegistry) -> Self {
46        Self { registry }
47    }
48
49    /// Execute a plugin by name on source code
50    pub fn execute(
51        &mut self,
52        plugin_name: &str,
53        file_path: &Path,
54        source: &str,
55    ) -> Result<PluginExecutionResult, PluginExecutorError> {
56        // Get plugin from registry
57        let plugin = self
58            .registry
59            .get_plugin_mut(plugin_name)
60            .ok_or_else(|| PluginExecutorError::PluginNotFound(plugin_name.to_string()))?;
61
62        // Parse the source code
63        let syntax =
64            syn::parse_file(source).map_err(|e| PluginExecutorError::Parse(e.to_string()))?;
65
66        // Match patterns
67        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        // Extract function return types from the AST
79        let fn_return_type = extract_function_return_type(&syntax);
80
81        // Apply transform
82        let edits = match &plugin.manifest.transform {
83            TransformDef::Template(template) => {
84                // Host-side template expansion
85                expand_template(template, &matches)
86            }
87            TransformDef::WasmExecute => {
88                // Call WASM for complex transform
89                let context = TransformContext {
90                    file_path: file_path.to_string_lossy().to_string(),
91                    source_text: source.to_string(),
92                    type_hints: vec![], // TODO: implement full type analysis
93                    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        // Apply edits to source
102        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
112// =============================================================================
113// Type Analysis
114// =============================================================================
115
116/// Extract function return types from the parsed file
117/// Returns the first function's return type found (simplified for now)
118fn 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
132// =============================================================================
133// Pattern Matching (syn-based)
134// =============================================================================
135
136/// Pattern matcher visitor for syn AST
137struct PatternMatcher<'a> {
138    pattern: ParsedPattern,
139    source: &'a str,
140    matches: Vec<MatchResult>,
141}
142
143/// Parsed pattern from DSL
144#[derive(Debug)]
145struct ParsedPattern {
146    node_type: String,
147    conditions: Vec<(String, String)>,
148}
149
150/// Parse pattern DSL like `binary_expr[op=="==", right=="true"]`
151fn parse_pattern(pattern: &str) -> Result<ParsedPattern, PluginExecutorError> {
152    // Very simple pattern parser for now
153    // Format: node_type[cond1="val1", cond2="val2"]
154
155    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        // Parse: key=="value" or key="value"
184        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
195/// Match pattern against parsed AST
196fn 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        // Match binary expressions
217        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        // Match method calls (e.g., x.unwrap(), x.expect("msg"))
226        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        // Continue visiting children
235        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        // Find byte offsets using simple source text search
269        let expr_str = self.expr_to_string_with_box(&syn::Expr::Binary(bin.clone()));
270
271        // Search for the expression in source
272        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        // Search for .method( pattern in source
317        let method_pattern = format!(".{}(", method_text);
318
319        // Find all occurrences and pick the one that makes sense
320        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            // Find the end of the method call (matching closing paren)
325            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                // Walk backwards to find the start of the expression
332                // Look for a character that can't be part of an expression
333                let start_pos = self.find_expr_start(method_abs_pos);
334
335                // Extract the actual receiver from source
336                let actual_receiver = self.source[start_pos..method_abs_pos].to_string();
337
338                // Verify this looks like the right match by checking receiver similarity
339                // (normalized comparison - remove all spaces)
340                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; // Found the match
370                }
371            }
372
373            search_start = method_abs_pos + 1;
374        }
375    }
376
377    /// Find the start of an expression by walking backwards from a position
378    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                        // Unmatched open paren - expression starts after this
395                        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                // These characters indicate the start of the expression
407                '=' | ';' | '{' | ',' | ':' if paren_depth == 0 && bracket_depth == 0 => {
408                    // Skip any whitespace after the delimiter
409                    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
450/// Convert binary operator to string
451fn 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
485// =============================================================================
486// Template Expansion
487// =============================================================================
488
489/// Expand template with captured values
490fn 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        // Replace {{capture_name}} with captured text
497        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
512// =============================================================================
513// Edit Application
514// =============================================================================
515
516/// Apply text edits to source (in reverse order to preserve offsets)
517fn apply_edits(source: &str, edits: &[TextEdit]) -> String {
518    let mut result = source.to_string();
519
520    // Sort edits in reverse order by start position
521    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}