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        // `>>>` is parsed but not implemented for integer types in Solidity, so a `Sar` mutant
27        // never compiles and only costs a build per mutation site.
28        let operations_num_bitwise = vec![
29            BinOpKind::Shr,
30            BinOpKind::Shl,
31            BinOpKind::BitAnd,
32            BinOpKind::BitOr,
33            BinOpKind::BitXor,
34            BinOpKind::Add,
35            BinOpKind::Sub,
36            BinOpKind::Pow,
37            BinOpKind::Mul,
38            BinOpKind::Div,
39            BinOpKind::Rem,
40        ];
41
42        let operations =
43            if operations_bools.contains(&op) { operations_bools } else { operations_num_bitwise };
44
45        // Extract LHS and RHS text from source
46        let source = context.source.unwrap_or("");
47        let lhs_text = extract_span_text(source, lhs.span);
48        let rhs_text = extract_span_text(source, rhs.span);
49        let op_str = op.to_str();
50
51        let original_expr = if compound_assignment {
52            format!("{lhs_text} {op_str}= {rhs_text}")
53        } else {
54            format!("{lhs_text} {op_str} {rhs_text}")
55        };
56
57        // Use the full expression span for the mutation (not just the operator span)
58        let expr_span = context.span;
59
60        // Get line context
61        let source_line = context.source_line();
62        let line_number = context.line_number();
63        let column_number = context.column_number();
64
65        Ok(operations
66            .into_iter()
67            .filter(|&kind| kind != op)
68            .filter(|&kind| !compound_assignment || is_valid_compound_assignment_op(kind))
69            .map(|kind| {
70                let mutated_expr = if compound_assignment {
71                    format!("{} {}= {}", lhs_text, kind.to_str(), rhs_text)
72                } else {
73                    format!("{} {} {}", lhs_text, kind.to_str(), rhs_text)
74                };
75                Mutant {
76                    span: expr_span,
77                    mutation: MutationType::BinaryOpExpr { new_op: kind, mutated_expr },
78                    path: context.path.clone(),
79                    original: original_expr.clone(),
80                    source_line: source_line.clone(),
81                    line_number,
82                    column_number,
83                }
84            })
85            .collect())
86    }
87
88    fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
89        if ctxt.expr.is_none() {
90            return false;
91        }
92
93        matches!(
94            ctxt.expr.unwrap().kind,
95            ExprKind::Binary(_, _, _) | ExprKind::Assign(_, Some(_), _)
96        )
97    }
98}
99
100const fn is_valid_compound_assignment_op(kind: BinOpKind) -> bool {
101    matches!(
102        kind,
103        BinOpKind::BitOr
104            | BinOpKind::BitXor
105            | BinOpKind::BitAnd
106            | BinOpKind::Shl
107            | BinOpKind::Shr
108            | BinOpKind::Add
109            | BinOpKind::Sub
110            | BinOpKind::Mul
111            | BinOpKind::Div
112            | BinOpKind::Rem
113    )
114}
115
116/// Extract the binary operator, its span, and LHS/RHS expressions
117fn get_bin_op_parts<'a>(
118    expr: &'a Expr<'a>,
119) -> Result<(BinOp, Span, &'a Expr<'a>, &'a Expr<'a>, bool)> {
120    match &expr.kind {
121        ExprKind::Assign(lhs, Some(op), rhs) => Ok((*op, op.span, lhs, rhs, true)),
122        ExprKind::Binary(lhs, op, rhs) => Ok((*op, op.span, lhs, rhs, false)),
123        _ => {
124            eyre::bail!("BinaryOpMutator: unexpected expression kind");
125        }
126    }
127}
128
129/// Extract text from source given a span
130fn extract_span_text(source: &str, span: Span) -> String {
131    let lo = span.lo().0 as usize;
132    let hi = span.hi().0 as usize;
133    source.get(lo..hi).map(|s| s.trim().to_string()).unwrap_or_default()
134}