Skip to main content

forge/mutation/
mutant.rs

1use std::{fmt::Display, path::PathBuf};
2
3use serde::{Deserialize, Serialize};
4use solar::{
5    interface::BytePos,
6    parse::ast::{BinOpKind, LitKind, Span, StrKind, UnOpKind},
7};
8
9use super::visitor::AssignVarTypes;
10
11/// Wraps an unary operator mutated, to easily store pre/post-fix op swaps
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct UnaryOpMutated {
14    /// String containing the whole new expression (operator and its target)
15    /// eg `a++`
16    new_expression: String,
17
18    /// The underlying operator used by this mutant
19    #[serde(serialize_with = "serialize_unop_kind", deserialize_with = "deserialize_unop_kind")]
20    pub resulting_op_kind: UnOpKind,
21}
22
23// Custom serialization for UnOpKind
24fn serialize_unop_kind<S>(value: &UnOpKind, serializer: S) -> Result<S::Ok, S::Error>
25where
26    S: serde::Serializer,
27{
28    let s = format!("{value:?}");
29    serializer.serialize_str(&s)
30}
31
32fn deserialize_unop_kind<'de, D>(deserializer: D) -> Result<UnOpKind, D::Error>
33where
34    D: serde::Deserializer<'de>,
35{
36    let s = String::deserialize(deserializer)?;
37    match s.as_str() {
38        "PreInc" => Ok(UnOpKind::PreInc),
39        "PostInc" => Ok(UnOpKind::PostInc),
40        "PreDec" => Ok(UnOpKind::PreDec),
41        "PostDec" => Ok(UnOpKind::PostDec),
42        "Not" => Ok(UnOpKind::Not),
43        "BitNot" => Ok(UnOpKind::BitNot),
44        "Neg" => Ok(UnOpKind::Neg),
45        other => Err(serde::de::Error::custom(format!("Unknown UnOpKind: {other}"))),
46    }
47}
48
49impl UnaryOpMutated {
50    pub const fn new(new_expression: String, resulting_op_kind: UnOpKind) -> Self {
51        Self { new_expression, resulting_op_kind }
52    }
53}
54
55impl Display for UnaryOpMutated {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "{}", self.new_expression)
58    }
59}
60
61// Custom serialization for BinOpKind
62fn serialize_binop<S>(value: &BinOpKind, serializer: S) -> Result<S::Ok, S::Error>
63where
64    S: serde::Serializer,
65{
66    let s = format!("{value:?}");
67    serializer.serialize_str(&s)
68}
69
70fn deserialize_binop<'de, D>(deserializer: D) -> Result<BinOpKind, D::Error>
71where
72    D: serde::Deserializer<'de>,
73{
74    let s = String::deserialize(deserializer)?;
75    match s.as_str() {
76        "Add" => Ok(BinOpKind::Add),
77        "Sub" => Ok(BinOpKind::Sub),
78        "Mul" => Ok(BinOpKind::Mul),
79        "Div" => Ok(BinOpKind::Div),
80        "And" => Ok(BinOpKind::And),
81        "Or" => Ok(BinOpKind::Or),
82        "Eq" => Ok(BinOpKind::Eq),
83        "Ne" => Ok(BinOpKind::Ne),
84        "Lt" => Ok(BinOpKind::Lt),
85        "Le" => Ok(BinOpKind::Le),
86        "Gt" => Ok(BinOpKind::Gt),
87        "Ge" => Ok(BinOpKind::Ge),
88        "BitAnd" => Ok(BinOpKind::BitAnd),
89        "BitOr" => Ok(BinOpKind::BitOr),
90        "BitXor" => Ok(BinOpKind::BitXor),
91        "Shl" => Ok(BinOpKind::Shl),
92        "Shr" => Ok(BinOpKind::Shr),
93        "Sar" => Ok(BinOpKind::Sar),
94        "Pow" => Ok(BinOpKind::Pow),
95        "Rem" => Ok(BinOpKind::Rem),
96        other => Err(serde::de::Error::custom(format!("Unknown BinOpKind: {other}"))),
97    }
98}
99
100// @todo add a mutation from universalmutator: line swap (swap two lines of code, as it
101// could theoretically uncover untested reentrancies
102#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
103pub enum OwnedStrKind {
104    Str,
105    Unicode,
106    Hex,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub enum OwnedLiteral {
111    Str {
112        kind: OwnedStrKind,
113        text: String,
114    },
115    Number(alloy_primitives::U256),
116    Rational(String),
117    Address(String),
118    Bool(bool),
119    Err(String),
120    /// Signed-negation of a numeric literal (e.g. `-123`). We cannot represent
121    /// negative values inside `Number(U256)` (the cast wraps via two's
122    /// complement and renders as a huge unsigned literal), so we carry the
123    /// negation textually and render it as `-{val}`.
124    NegatedNumber(alloy_primitives::U256),
125}
126
127impl From<&LitKind<'_>> for OwnedLiteral {
128    fn from(lit_kind: &LitKind<'_>) -> Self {
129        match lit_kind {
130            LitKind::Bool(b) => Self::Bool(*b),
131            LitKind::Number(n) => Self::Number(*n),
132            LitKind::Rational(r) => Self::Rational(r.to_string()),
133            LitKind::Address(addr) => Self::Address(addr.to_string()),
134            LitKind::Str(sk, bytesym, _extras) => {
135                let text = String::from_utf8_lossy(bytesym.as_byte_str()).into_owned();
136                let kind = match sk {
137                    StrKind::Str => OwnedStrKind::Str,
138                    StrKind::Unicode => OwnedStrKind::Unicode,
139                    StrKind::Hex => OwnedStrKind::Hex,
140                };
141                Self::Str { kind, text }
142            }
143            LitKind::Err(_) => Self::Err("parse_error".to_string()),
144        }
145    }
146}
147
148impl Display for OwnedLiteral {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        match self {
151            Self::Bool(val) => write!(f, "{val}"),
152            Self::Number(val) => write!(f, "{val}"),
153            Self::NegatedNumber(val) => write!(f, "-{val}"),
154            Self::Rational(s) => write!(f, "{s}"),
155            Self::Address(s) => write!(f, "{s}"),
156            Self::Str { kind, text } => match kind {
157                OwnedStrKind::Str => write!(f, "\"{text}\""),
158                OwnedStrKind::Unicode => write!(f, "unicode\"{text}\""),
159                OwnedStrKind::Hex => write!(f, "hex\"{text}\""),
160            },
161            Self::Err(s) => write!(f, "{s}"),
162        }
163    }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub enum MutationType {
168    // Note: Solar's LitKind::Number(U256) doesn't differentiate int vs uint - it only stores the
169    // numeric value without signedness info. For now we generate mutations for both and let solc
170    // filter out invalid ones (e.g., -x on uint). Future improvement: track variable types in a
171    // symbol table to avoid generating invalid mutants.
172    /// For an initializer x, of type
173    /// bool: replace x with !x
174    /// uint: replace x with 0
175    /// int: replace x with 0; replace x with -x (temp: this is mutated for uint as well)
176    ///
177    /// For a binary op y: apply BinaryOp(y)
178    Assignment(AssignVarTypes),
179
180    /// For a binary op y in BinOpKind ("+", "-", ">=", etc)
181    /// replace y with each non-y in op (legacy, kept for cache compatibility)
182    #[serde(serialize_with = "serialize_binop", deserialize_with = "deserialize_binop")]
183    BinaryOp(BinOpKind),
184
185    /// Binary operator mutation with full expression context
186    /// Stores both the new operator and the full mutated expression for display
187    BinaryOpExpr {
188        #[serde(serialize_with = "serialize_binop", deserialize_with = "deserialize_binop")]
189        new_op: BinOpKind,
190        mutated_expr: String,
191    },
192
193    /// For a delete expr x `delete foo`, replace x with `assert(true)`
194    DeleteExpression,
195
196    /// replace "delegatecall" with "call"
197    ElimDelegate,
198
199    /// Gambit doesn't implement nor define it?
200    FunctionCall,
201
202    // /// For a if(x) condition x:
203    // /// replace x with true; replace x with false
204    // This mutation is not used anymore, as we mutate the condition as an expression,
205    // which will creates true/false mutant as well as more complex conditions (eg if(foo++ >
206    // --bar) ) IfStatementMutation,
207    /// For a require(x) condition:
208    /// replace x with true; replace x with false
209    // Same as for IfStatementMutation, the expression inside the require is mutated as an
210    // expression to handle increment etc
211    Require,
212
213    /// For require(condition)/assert(condition), mutate the condition:
214    /// - require(x) -> require(true) (always passes - security critical!)
215    /// - require(x) -> require(false) (always fails)
216    /// - require(x) -> require(!x) (inverted condition)
217    RequireCondition {
218        /// The mutated full call expression
219        mutated_call: String,
220    },
221
222    // @todo review if needed -> this might creates *a lot* of combinations for super-polyadic fn
223    // tho       only swapping same type (to avoid obvious compilation failure), but should
224    // take into account       implicit casting too...
225    /// For 2 args of the same type x,y in a function args:
226    /// swap(x, y)
227    SwapArgumentsFunction,
228
229    // @todo same remark as above, might end up in a space too big to explore + filtering out
230    // based on type
231    /// For an expr taking 2 expression x, y (x+y, x-y, x = x + ...):
232    /// swap(x, y)
233    SwapArgumentsOperator,
234
235    /// For an unary operator x in UnOpKind (eg "++", "--", "~", "!"):
236    /// replace x with all other operator in op
237    /// Pre or post- are different UnOp
238    UnaryOperator(UnaryOpMutated),
239
240    YulOpcode {
241        original_opcode: String,
242        new_opcode: String,
243        mutated_expr: String,
244    },
245}
246
247impl Display for MutationType {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        match self {
250            Self::Assignment(kind) => match kind {
251                AssignVarTypes::Literal(lit) => write!(f, "{lit}"),
252                AssignVarTypes::Identifier(ident) => write!(f, "{ident}"),
253                AssignVarTypes::NegatedIdentifier(ident) => write!(f, "-{ident}"),
254            },
255            Self::BinaryOp(kind) => write!(f, "{}", kind.to_str()),
256            Self::BinaryOpExpr { mutated_expr, .. } => write!(f, "{mutated_expr}"),
257            Self::DeleteExpression => write!(f, "assert(true)"),
258            Self::ElimDelegate => write!(f, "call"),
259            Self::UnaryOperator(mutated) => write!(f, "{mutated}"),
260            Self::RequireCondition { mutated_call } => write!(f, "{mutated_call}"),
261
262            Self::YulOpcode { mutated_expr, .. } => write!(f, "{mutated_expr}"),
263
264            Self::FunctionCall
265            | Self::Require
266            | Self::SwapArgumentsFunction
267            | Self::SwapArgumentsOperator => write!(f, ""),
268        }
269    }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub enum MutationResult {
274    Dead,
275    Alive,
276    Invalid,
277    Skipped,
278    /// The mutant's compile-and-test run exceeded the configured timeout.
279    /// Treated as unresolved: not counted toward survived or killed.
280    TimedOut,
281}
282
283impl MutationResult {
284    /// Short uppercase label used in progress / reporter output.
285    pub const fn label(&self) -> &'static str {
286        match self {
287            Self::Dead => "KILLED",
288            Self::Alive => "SURVIVED",
289            Self::Invalid => "INVALID",
290            Self::Skipped => "SKIPPED",
291            Self::TimedOut => "TIMED OUT",
292        }
293    }
294}
295
296/// A given mutation
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct Mutant {
299    /// The path to the project root where this mutant (tries to) live
300    pub path: PathBuf,
301    #[serde(serialize_with = "serialize_span", deserialize_with = "deserialize_span")]
302    pub span: Span,
303    pub mutation: MutationType,
304    /// The original source text that will be replaced by this mutation (full expression)
305    #[serde(default)]
306    pub original: String,
307    /// The full source line for context (e.g., "uint256 x = a * b;")
308    #[serde(default)]
309    pub source_line: String,
310    /// Line number in the source file (1-indexed)
311    #[serde(default)]
312    pub line_number: usize,
313    /// Column number in the source file (1-indexed)
314    #[serde(default)]
315    pub column_number: usize,
316}
317
318// Custom serialization for Span (since solar::parse::ast::Span doesn't implement Serialize)
319fn serialize_span<S>(span: &Span, serializer: S) -> Result<S::Ok, S::Error>
320where
321    S: serde::Serializer,
322{
323    use serde::Serialize;
324    #[derive(Serialize)]
325    struct SpanHelper {
326        lo: u32,
327        hi: u32,
328    }
329    SpanHelper { lo: span.lo().0, hi: span.hi().0 }.serialize(serializer)
330}
331
332fn deserialize_span<'de, D>(deserializer: D) -> Result<Span, D::Error>
333where
334    D: serde::Deserializer<'de>,
335{
336    use serde::Deserialize;
337    #[derive(Deserialize)]
338    struct SpanHelper {
339        lo: u32,
340        hi: u32,
341    }
342    let helper = SpanHelper::deserialize(deserializer)?;
343    Ok(Span::new(BytePos(helper.lo), BytePos(helper.hi)))
344}
345
346impl Mutant {
347    /// Returns a relative path string.
348    ///
349    /// Walks ancestor components looking for a well-known directory root
350    /// (`src`, `test`, `lib`, `contracts`) so the output is cross-platform and
351    /// does not rely on OS-specific path separators.
352    pub fn relative_path(&self) -> String {
353        let components: Vec<_> = self.path.components().collect();
354        for (i, comp) in components.iter().enumerate() {
355            if let std::path::Component::Normal(name) = comp {
356                let s = name.to_string_lossy();
357                if matches!(s.as_ref(), "src" | "test" | "script" | "lib" | "contracts") {
358                    let parts: Vec<_> = components[i..]
359                        .iter()
360                        .filter_map(|c| match c {
361                            std::path::Component::Normal(s) => Some(s.to_string_lossy()),
362                            _ => None,
363                        })
364                        .collect();
365                    return parts.join("/");
366                }
367            }
368        }
369        self.path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown").to_string()
370    }
371
372    /// Returns a concise one-line description of the mutation (full original code)
373    pub fn short_description(&self) -> String {
374        let original = if self.original.is_empty() {
375            "<unknown>".to_string()
376        } else {
377            self.original.trim().to_string()
378        };
379        let mutated = self.mutation.to_string();
380
381        format!("`{}` → `{}`", original, mutated.trim())
382    }
383}
384
385impl Display for Mutant {
386    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387        if self.line_number > 0 {
388            write!(f, "{}:{}: {}", self.relative_path(), self.line_number, self.short_description())
389        } else {
390            write!(f, "{}: {}", self.relative_path(), self.short_description())
391        }
392    }
393}