impl TreeSitterMutationOperator for PythonBinaryOpMutation {
fn name(&self) -> &str {
"PythonBinaryOp"
}
fn can_mutate(&self, node: &Node, _source: &[u8]) -> bool {
node.kind() == "binary_operator"
}
fn mutate(&self, node: &Node, source: &[u8]) -> Vec<MutatedSource> {
let mut cursor = node.walk();
let mut operator_node = None;
for child in node.children(&mut cursor) {
let kind = child.kind();
if matches!(kind, "+" | "-" | "*" | "/" | "//" | "%" | "**") {
operator_node = Some(child);
break;
}
}
let operator_node = match operator_node {
Some(n) => n,
None => return vec![],
};
let op_bytes = &source[operator_node.byte_range()];
let op_text = std::str::from_utf8(op_bytes).unwrap_or("");
let replacements = python_binary_op_replacements(op_text);
if replacements.is_empty() {
return vec![];
}
build_operator_mutations(source, &operator_node, op_text, &replacements)
}
fn kill_probability(&self) -> f64 {
0.85 }
}
fn python_binary_op_replacements(op_text: &str) -> Vec<&'static str> {
match op_text {
"+" => vec!["-", "*", "/", "//", "%", "**"],
"-" => vec!["+", "*", "/", "//", "%", "**"],
"*" => vec!["+", "-", "/", "//", "%", "**"],
"/" => vec!["+", "-", "*", "//", "%", "**"],
"//" => vec!["+", "-", "*", "/", "%", "**"],
"%" => vec!["+", "-", "*", "/", "//", "**"],
"**" => vec!["+", "-", "*", "/", "//", "%"],
_ => vec![],
}
}
impl TreeSitterMutationOperator for PythonRelationalOpMutation {
fn name(&self) -> &str {
"PythonRelationalOp"
}
fn can_mutate(&self, node: &Node, _source: &[u8]) -> bool {
node.kind() == "comparison_operator"
}
fn mutate(&self, node: &Node, source: &[u8]) -> Vec<MutatedSource> {
let mut cursor = node.walk();
let mut operator_node = None;
for child in node.children(&mut cursor) {
let kind = child.kind();
if matches!(
kind,
"<" | ">" | "<=" | ">=" | "==" | "!=" | "is" | "is not" | "in" | "not in"
) {
if matches!(kind, "<" | ">" | "<=" | ">=" | "==" | "!=") {
operator_node = Some(child);
break;
}
}
}
let operator_node = match operator_node {
Some(n) => n,
None => return vec![],
};
let op_bytes = &source[operator_node.byte_range()];
let op_text = std::str::from_utf8(op_bytes).unwrap_or("");
let replacements = python_relational_op_replacements(op_text);
if replacements.is_empty() {
return vec![];
}
build_operator_mutations(source, &operator_node, op_text, &replacements)
}
fn kill_probability(&self) -> f64 {
0.75 }
}
fn python_relational_op_replacements(op_text: &str) -> Vec<&'static str> {
match op_text {
"<" => vec![">", "<=", ">=", "==", "!="],
">" => vec!["<", "<=", ">=", "==", "!="],
"<=" => vec!["<", ">", ">=", "==", "!="],
">=" => vec!["<", ">", "<=", "==", "!="],
"==" => vec!["!=", "<", ">", "<=", ">="],
"!=" => vec!["==", "<", ">", "<=", ">="],
_ => vec![],
}
}
fn build_operator_mutations(
source: &[u8],
operator_node: &Node,
op_text: &str,
replacements: &[&str],
) -> Vec<MutatedSource> {
replacements
.iter()
.map(|new_op| {
let mut mutated = source.to_vec();
mutated.splice(operator_node.byte_range(), new_op.bytes());
MutatedSource {
source: String::from_utf8(mutated).expect(
"mutated source is valid UTF-8 (original source + ASCII operators)",
),
description: format!("{} → {}", op_text, new_op),
location: SourceLocation {
line: operator_node.start_position().row + 1,
column: operator_node.start_position().column + 1,
end_line: operator_node.end_position().row + 1,
end_column: operator_node.end_position().column + 1,
},
}
})
.collect()
}