Skip to main content

forge/mutation/mutators/
binary_op_mutator.rs

1use eyre::{OptionExt, Result};
2use solar::ast::{BinOp, BinOpKind, Expr, ExprKind, Span};
3
4use super::{MutationContext, Mutator};
5use crate::mutation::mutant::{Mutant, MutationType};
6
7pub struct BinaryOpMutator;
8
9impl Mutator for BinaryOpMutator {
10    fn generate_mutants(&self, context: &MutationContext<'_>) -> Result<Vec<Mutant>> {
11        let expr = context.expr.ok_or_eyre("BinaryOpMutator: no expression")?;
12        let (bin_op, _op_span, lhs, rhs, compound_assignment) = get_bin_op_parts(expr)?;
13        let op = bin_op.kind;
14
15        let operations_bools = vec![
16            BinOpKind::Lt,
17            BinOpKind::Le,
18            BinOpKind::Gt,
19            BinOpKind::Ge,
20            BinOpKind::Eq,
21            BinOpKind::Ne,
22            BinOpKind::Or,
23            BinOpKind::And,
24        ];
25
26        let operations_num_bitwise = vec![
27            BinOpKind::Shr,
28            BinOpKind::Shl,
29            BinOpKind::Sar,
30            BinOpKind::BitAnd,
31            BinOpKind::BitOr,
32            BinOpKind::BitXor,
33            BinOpKind::Add,
34            BinOpKind::Sub,
35            BinOpKind::Pow,
36            BinOpKind::Mul,
37            BinOpKind::Div,
38            BinOpKind::Rem,
39        ];
40
41        let operations =
42            if operations_bools.contains(&op) { operations_bools } else { operations_num_bitwise };
43
44        // Extract LHS and RHS text from source
45        let source = context.source.unwrap_or("");
46        let lhs_text = extract_span_text(source, lhs.span);
47        let rhs_text = extract_span_text(source, rhs.span);
48        let op_str = op.to_str();
49
50        let original_expr = if compound_assignment {
51            format!("{lhs_text} {op_str}= {rhs_text}")
52        } else {
53            format!("{lhs_text} {op_str} {rhs_text}")
54        };
55
56        // Use the full expression span for the mutation (not just the operator span)
57        let expr_span = context.span;
58
59        // Get line context
60        let source_line = context.source_line();
61        let line_number = context.line_number();
62        let column_number = context.column_number();
63
64        Ok(operations
65            .into_iter()
66            .filter(|&kind| kind != op)
67            .filter(|&kind| !compound_assignment || is_valid_compound_assignment_op(kind))
68            .map(|kind| {
69                let mutated_expr = if compound_assignment {
70                    format!("{} {}= {}", lhs_text, kind.to_str(), rhs_text)
71                } else {
72                    format!("{} {} {}", lhs_text, kind.to_str(), rhs_text)
73                };
74                Mutant {
75                    span: expr_span,
76                    mutation: MutationType::BinaryOpExpr { new_op: kind, mutated_expr },
77                    path: context.path.clone(),
78                    original: original_expr.clone(),
79                    source_line: source_line.clone(),
80                    line_number,
81                    column_number,
82                }
83            })
84            .collect())
85    }
86
87    fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
88        if ctxt.expr.is_none() {
89            return false;
90        }
91
92        matches!(
93            ctxt.expr.unwrap().kind,
94            ExprKind::Binary(_, _, _) | ExprKind::Assign(_, Some(_), _)
95        )
96    }
97}
98
99const fn is_valid_compound_assignment_op(kind: BinOpKind) -> bool {
100    matches!(
101        kind,
102        BinOpKind::BitOr
103            | BinOpKind::BitXor
104            | BinOpKind::BitAnd
105            | BinOpKind::Shl
106            | BinOpKind::Shr
107            | BinOpKind::Add
108            | BinOpKind::Sub
109            | BinOpKind::Mul
110            | BinOpKind::Div
111            | BinOpKind::Rem
112    )
113}
114
115/// Extract the binary operator, its span, and LHS/RHS expressions
116fn get_bin_op_parts<'a>(
117    expr: &'a Expr<'a>,
118) -> Result<(BinOp, Span, &'a Expr<'a>, &'a Expr<'a>, bool)> {
119    match &expr.kind {
120        ExprKind::Assign(lhs, Some(op), rhs) => Ok((*op, op.span, lhs, rhs, true)),
121        ExprKind::Binary(lhs, op, rhs) => Ok((*op, op.span, lhs, rhs, false)),
122        _ => eyre::bail!("BinaryOpMutator: unexpected expression kind"),
123    }
124}
125
126/// Extract text from source given a span
127fn extract_span_text(source: &str, span: Span) -> String {
128    let lo = span.lo().0 as usize;
129    let hi = span.hi().0 as usize;
130    source.get(lo..hi).map(|s| s.trim().to_string()).unwrap_or_default()
131}