use super::registry::{MutationRegistry, RegistryError};
use ryo_plugin_loader::{Capture, MatchResult, NodeKind, TextEdit, TransformContext, TransformDef};
use std::path::Path;
use syn::visit::Visit;
#[derive(Debug, thiserror::Error)]
pub enum PluginExecutorError {
#[error("Plugin not found: {0}")]
PluginNotFound(String),
#[error("Registry error: {0}")]
Registry(#[from] RegistryError),
#[error("Pattern match error: {0}")]
PatternMatch(String),
#[error("Transform error: {0}")]
Transform(String),
#[error("Parse error: {0}")]
Parse(String),
}
#[derive(Debug)]
pub struct PluginExecutionResult {
pub matches_found: usize,
pub edits: Vec<TextEdit>,
pub new_source: Option<String>,
}
pub struct PluginExecutor<'a> {
registry: &'a mut MutationRegistry,
}
impl<'a> PluginExecutor<'a> {
pub fn new(registry: &'a mut MutationRegistry) -> Self {
Self { registry }
}
pub fn execute(
&mut self,
plugin_name: &str,
file_path: &Path,
source: &str,
) -> Result<PluginExecutionResult, PluginExecutorError> {
let plugin = self
.registry
.get_plugin_mut(plugin_name)
.ok_or_else(|| PluginExecutorError::PluginNotFound(plugin_name.to_string()))?;
let syntax =
syn::parse_file(source).map_err(|e| PluginExecutorError::Parse(e.to_string()))?;
let pattern = &plugin.manifest.pattern;
let matches = match_pattern(pattern, &syntax, source)?;
if matches.is_empty() {
return Ok(PluginExecutionResult {
matches_found: 0,
edits: vec![],
new_source: None,
});
}
let fn_return_type = extract_function_return_type(&syntax);
let edits = match &plugin.manifest.transform {
TransformDef::Template(template) => {
expand_template(template, &matches)
}
TransformDef::WasmExecute => {
let context = TransformContext {
file_path: file_path.to_string_lossy().to_string(),
source_text: source.to_string(),
type_hints: vec![], fn_return_type,
};
plugin
.execute_transform(matches.clone(), context)
.map_err(|e| PluginExecutorError::Transform(e.to_string()))?
}
};
let new_source = apply_edits(source, &edits);
Ok(PluginExecutionResult {
matches_found: matches.len(),
edits,
new_source: Some(new_source),
})
}
}
fn extract_function_return_type(file: &syn::File) -> Option<String> {
use quote::ToTokens;
for item in &file.items {
if let syn::Item::Fn(func) = item {
if let syn::ReturnType::Type(_, ty) = &func.sig.output {
let type_str = ty.to_token_stream().to_string();
return Some(type_str);
}
}
}
None
}
struct PatternMatcher<'a> {
pattern: ParsedPattern,
source: &'a str,
matches: Vec<MatchResult>,
}
#[derive(Debug)]
struct ParsedPattern {
node_type: String,
conditions: Vec<(String, String)>,
}
fn parse_pattern(pattern: &str) -> Result<ParsedPattern, PluginExecutorError> {
let bracket_start = pattern.find('[');
let bracket_end = pattern.rfind(']');
let (node_type, conditions) = match (bracket_start, bracket_end) {
(Some(start), Some(end)) if end > start => {
let node_type = pattern[..start].to_string();
let conds_str = &pattern[start + 1..end];
let conditions = parse_conditions(conds_str)?;
(node_type, conditions)
}
_ => (pattern.to_string(), vec![]),
};
Ok(ParsedPattern {
node_type,
conditions,
})
}
fn parse_conditions(conds_str: &str) -> Result<Vec<(String, String)>, PluginExecutorError> {
let mut conditions = Vec::new();
for cond in conds_str.split(',') {
let cond = cond.trim();
if cond.is_empty() {
continue;
}
let parts: Vec<&str> = cond.splitn(2, "==").collect();
if parts.len() == 2 {
let key = parts[0].trim().to_string();
let value = parts[1].trim().trim_matches('"').to_string();
conditions.push((key, value));
}
}
Ok(conditions)
}
fn match_pattern(
pattern: &str,
file: &syn::File,
source: &str,
) -> Result<Vec<MatchResult>, PluginExecutorError> {
let parsed = parse_pattern(pattern)?;
let mut matcher = PatternMatcher {
pattern: parsed,
source,
matches: Vec::new(),
};
matcher.visit_file(file);
Ok(matcher.matches)
}
impl<'ast> Visit<'ast> for PatternMatcher<'ast> {
fn visit_expr(&mut self, expr: &'ast syn::Expr) {
if self.pattern.node_type == "binary_expr" {
if let syn::Expr::Binary(bin) = expr {
if self.matches_binary_expr(bin) {
self.add_binary_match(bin);
}
}
}
if self.pattern.node_type == "method_call" {
if let syn::Expr::MethodCall(call) = expr {
if self.matches_method_call(call) {
self.add_method_call_match(call);
}
}
}
syn::visit::visit_expr(self, expr);
}
}
impl<'a> PatternMatcher<'a> {
fn matches_binary_expr(&self, bin: &syn::ExprBinary) -> bool {
for (key, value) in &self.pattern.conditions {
match key.as_str() {
"op" => {
let op_str = op_to_string(&bin.op);
if op_str != *value {
return false;
}
}
"right" => {
let right_src = self.expr_to_string(&bin.right);
if right_src.trim() != *value {
return false;
}
}
"left" => {
let left_src = self.expr_to_string(&bin.left);
if left_src.trim() != *value {
return false;
}
}
_ => {}
}
}
true
}
fn add_binary_match(&mut self, bin: &syn::ExprBinary) {
let expr_str = self.expr_to_string_with_box(&syn::Expr::Binary(bin.clone()));
if let Some(start_byte) = self.source.find(&expr_str) {
let end_byte = start_byte + expr_str.len();
let left_text = self.expr_to_string(&bin.left);
let right_text = self.expr_to_string(&bin.right);
self.matches.push(MatchResult {
kind: NodeKind::BinaryExpr,
start_byte: start_byte as u64,
end_byte: end_byte as u64,
captures: vec![
Capture {
name: "left".to_string(),
start_byte: 0,
end_byte: 0,
text: left_text,
},
Capture {
name: "right".to_string(),
start_byte: 0,
end_byte: 0,
text: right_text,
},
],
});
}
}
fn matches_method_call(&self, call: &syn::ExprMethodCall) -> bool {
for (key, value) in &self.pattern.conditions {
if key.as_str() == "method" {
let method_name = call.method.to_string();
if method_name != *value {
return false;
}
}
}
true
}
fn add_method_call_match(&mut self, call: &syn::ExprMethodCall) {
let receiver_text = self.expr_to_string(&call.receiver);
let method_text = call.method.to_string();
let method_pattern = format!(".{}(", method_text);
let mut search_start = 0;
while let Some(method_pos) = self.source[search_start..].find(&method_pattern) {
let method_abs_pos = search_start + method_pos;
let args_start = method_abs_pos + method_pattern.len();
let after_method = &self.source[args_start..];
if let Some(paren_pos) = self.find_matching_paren(after_method) {
let end_pos = args_start + paren_pos + 1;
let start_pos = self.find_expr_start(method_abs_pos);
let actual_receiver = self.source[start_pos..method_abs_pos].to_string();
let actual_normalized: String = actual_receiver
.chars()
.filter(|c| !c.is_whitespace())
.collect();
let expected_normalized: String = receiver_text
.chars()
.filter(|c| !c.is_whitespace())
.collect();
if actual_normalized == expected_normalized {
self.matches.push(MatchResult {
kind: NodeKind::MethodCall,
start_byte: start_pos as u64,
end_byte: end_pos as u64,
captures: vec![
Capture {
name: "receiver".to_string(),
start_byte: start_pos as u64,
end_byte: method_abs_pos as u64,
text: actual_receiver,
},
Capture {
name: "method".to_string(),
start_byte: method_abs_pos as u64,
end_byte: end_pos as u64,
text: method_text.clone(),
},
],
});
return; }
}
search_start = method_abs_pos + 1;
}
}
fn find_expr_start(&self, from: usize) -> usize {
let bytes = self.source.as_bytes();
let mut pos = from;
let mut paren_depth = 0;
let mut bracket_depth = 0;
while pos > 0 {
pos -= 1;
let c = bytes[pos] as char;
match c {
')' => paren_depth += 1,
'(' => {
if paren_depth > 0 {
paren_depth -= 1;
} else {
return pos + 1;
}
}
']' => bracket_depth += 1,
'[' => {
if bracket_depth > 0 {
bracket_depth -= 1;
} else {
return pos + 1;
}
}
'=' | ';' | '{' | ',' | ':' if paren_depth == 0 && bracket_depth == 0 => {
let mut start = pos + 1;
while start < from && self.source.as_bytes()[start].is_ascii_whitespace() {
start += 1;
}
return start;
}
_ => {}
}
}
0
}
fn find_matching_paren(&self, s: &str) -> Option<usize> {
let mut depth = 1;
for (i, c) in s.chars().enumerate() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn expr_to_string(&self, expr: &syn::Expr) -> String {
use quote::ToTokens;
expr.to_token_stream().to_string()
}
fn expr_to_string_with_box(&self, expr: &syn::Expr) -> String {
use quote::ToTokens;
expr.to_token_stream().to_string()
}
}
fn op_to_string(op: &syn::BinOp) -> String {
match op {
syn::BinOp::Eq(_) => "==".to_string(),
syn::BinOp::Ne(_) => "!=".to_string(),
syn::BinOp::Lt(_) => "<".to_string(),
syn::BinOp::Le(_) => "<=".to_string(),
syn::BinOp::Gt(_) => ">".to_string(),
syn::BinOp::Ge(_) => ">=".to_string(),
syn::BinOp::And(_) => "&&".to_string(),
syn::BinOp::Or(_) => "||".to_string(),
syn::BinOp::Add(_) => "+".to_string(),
syn::BinOp::Sub(_) => "-".to_string(),
syn::BinOp::Mul(_) => "*".to_string(),
syn::BinOp::Div(_) => "/".to_string(),
syn::BinOp::Rem(_) => "%".to_string(),
syn::BinOp::BitAnd(_) => "&".to_string(),
syn::BinOp::BitOr(_) => "|".to_string(),
syn::BinOp::BitXor(_) => "^".to_string(),
syn::BinOp::Shl(_) => "<<".to_string(),
syn::BinOp::Shr(_) => ">>".to_string(),
syn::BinOp::AddAssign(_) => "+=".to_string(),
syn::BinOp::SubAssign(_) => "-=".to_string(),
syn::BinOp::MulAssign(_) => "*=".to_string(),
syn::BinOp::DivAssign(_) => "/=".to_string(),
syn::BinOp::RemAssign(_) => "%=".to_string(),
syn::BinOp::BitAndAssign(_) => "&=".to_string(),
syn::BinOp::BitOrAssign(_) => "|=".to_string(),
syn::BinOp::BitXorAssign(_) => "^=".to_string(),
syn::BinOp::ShlAssign(_) => "<<=".to_string(),
syn::BinOp::ShrAssign(_) => ">>=".to_string(),
_ => "?".to_string(),
}
}
fn expand_template(template: &str, matches: &[MatchResult]) -> Vec<TextEdit> {
let mut edits = Vec::new();
for m in matches {
let mut replacement = template.to_string();
for capture in &m.captures {
let placeholder = format!("{{{{{}}}}}", capture.name);
replacement = replacement.replace(&placeholder, &capture.text);
}
edits.push(TextEdit {
start_byte: m.start_byte,
end_byte: m.end_byte,
replacement,
});
}
edits
}
fn apply_edits(source: &str, edits: &[TextEdit]) -> String {
let mut result = source.to_string();
let mut sorted_edits: Vec<_> = edits.iter().collect();
sorted_edits.sort_by_key(|b| std::cmp::Reverse(b.start_byte));
for edit in sorted_edits {
let start = edit.start_byte as usize;
let end = edit.end_byte as usize;
if start <= end && end <= result.len() {
result.replace_range(start..end, &edit.replacement);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_pattern() {
let pattern = "binary_expr[op==\"==\", right==\"true\"]";
let parsed = parse_pattern(pattern).unwrap();
assert_eq!(parsed.node_type, "binary_expr");
assert_eq!(parsed.conditions.len(), 2);
}
#[test]
fn test_expand_template() {
let template = "{{left}}";
let matches = vec![MatchResult {
kind: NodeKind::BinaryExpr,
start_byte: 0,
end_byte: 10,
captures: vec![Capture {
name: "left".to_string(),
start_byte: 0,
end_byte: 5,
text: "is_ok".to_string(),
}],
}];
let edits = expand_template(template, &matches);
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].replacement, "is_ok");
}
#[test]
fn test_op_to_string() {
let eq_op = syn::BinOp::Eq(Default::default());
assert_eq!(op_to_string(&eq_op), "==");
}
}