Skip to main content

forge/mutation/
visitor.rs

1use std::{ops::ControlFlow, path::PathBuf};
2
3use eyre::Report;
4use solar::ast::{Expr, ItemContract, VariableDefinition, visit::Visit, yul};
5
6#[cfg(test)]
7use crate::mutation::mutators::Mutator;
8use crate::mutation::{
9    mutant::{Mutant, OwnedLiteral},
10    mutators::{MutationContext, mutator_registry::MutatorRegistry},
11};
12use foundry_config::MutatorType;
13
14#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
15pub enum AssignVarTypes {
16    Literal(OwnedLiteral),
17    Identifier(String),
18}
19
20/// A visitor which collect all expression to mutate as well as the mutation types
21#[allow(clippy::type_complexity)]
22pub struct MutantVisitor<'src> {
23    pub mutation_to_conduct: Vec<Mutant>,
24    errors: Vec<Report>,
25    pub mutator_registry: MutatorRegistry,
26    pub path: PathBuf,
27    pub source: Option<&'src str>,
28    /// Optional per-contract name filter. When `Some`, mutations are only collected
29    /// from contracts whose name matches the predicate.
30    pub contract_filter: Option<Box<dyn Fn(&str) -> bool>>,
31    /// Whether the currently-visited contract is allowed by `contract_filter`.
32    /// `true` when no filter is set or when we are visiting a contract whose name
33    /// matched the filter. Top-level items (outside any contract) are always
34    /// considered "allowed".
35    in_allowed_contract: bool,
36}
37
38impl<'src> MutantVisitor<'src> {
39    /// Create a visitor with the specified mutator operators enabled
40    pub fn with_operators(path: PathBuf, operators: &[MutatorType]) -> Self {
41        Self {
42            mutation_to_conduct: Vec::new(),
43            errors: Vec::new(),
44            mutator_registry: MutatorRegistry::from_enabled(operators),
45            path,
46            source: None,
47            contract_filter: None,
48            in_allowed_contract: true,
49        }
50    }
51
52    /// Use all mutators from registry (all operators enabled)
53    #[cfg(test)]
54    pub fn default(path: PathBuf) -> Self {
55        Self {
56            mutation_to_conduct: Vec::new(),
57            errors: Vec::new(),
58            mutator_registry: MutatorRegistry::default(),
59            path,
60            source: None,
61            contract_filter: None,
62            in_allowed_contract: true,
63        }
64    }
65
66    /// Use only a set of mutators
67    #[cfg(test)]
68    pub fn new_with_mutators(path: PathBuf, mutators: Vec<Box<dyn Mutator>>) -> Self {
69        Self {
70            mutation_to_conduct: Vec::new(),
71            errors: Vec::new(),
72            mutator_registry: MutatorRegistry::new_with_mutators(mutators),
73            path,
74            source: None,
75            contract_filter: None,
76            in_allowed_contract: true,
77        }
78    }
79
80    /// Set the source code for extracting original text
81    pub const fn with_source(mut self, source: &'src str) -> Self {
82        self.source = Some(source);
83        self
84    }
85
86    /// Set a contract-name filter; only contracts whose name matches the
87    /// predicate will have their bodies mutated.
88    pub fn with_contract_filter<F>(mut self, filter: F) -> Self
89    where
90        F: Fn(&str) -> bool + 'static,
91    {
92        self.contract_filter = Some(Box::new(filter));
93        self
94    }
95
96    pub fn take_errors(&mut self) -> Vec<Report> {
97        std::mem::take(&mut self.errors)
98    }
99
100    fn collect_mutations(&mut self, context: &MutationContext<'_>) {
101        let result = self.mutator_registry.generate_mutations(context);
102        self.mutation_to_conduct.extend(result.mutations);
103
104        for err in result.errors {
105            self.errors.push(err.wrap_err(format!(
106                "failed to generate mutations for {}:{}:{}",
107                self.path.display(),
108                context.line_number(),
109                context.column_number()
110            )));
111        }
112    }
113}
114
115impl<'ast> Visit<'ast> for MutantVisitor<'ast> {
116    type BreakValue = ();
117
118    fn visit_item_contract(
119        &mut self,
120        contract: &'ast ItemContract<'ast>,
121    ) -> ControlFlow<Self::BreakValue> {
122        // When a contract name filter is configured, only descend into matching
123        // contracts. We toggle `in_allowed_contract` for the duration of the
124        // walk so nested visit_expr / visit_variable_definition calls can gate
125        // mutant collection accordingly.
126        let prev = self.in_allowed_contract;
127        self.in_allowed_contract = match &self.contract_filter {
128            Some(filter) => filter(contract.name.as_str()),
129            None => true,
130        };
131        let res = self.walk_item_contract(contract);
132        self.in_allowed_contract = prev;
133        res
134    }
135
136    fn visit_variable_definition(
137        &mut self,
138        var: &'ast VariableDefinition<'ast>,
139    ) -> ControlFlow<Self::BreakValue> {
140        // Skip entirely when the surrounding contract is filtered out.
141        if !self.in_allowed_contract {
142            return self.walk_variable_definition(var);
143        }
144
145        let mut builder = MutationContext::builder()
146            .with_path(self.path.clone())
147            .with_span(var.span)
148            .with_var_definition(var);
149
150        if let Some(src) = self.source {
151            builder = builder.with_source(src);
152        }
153
154        let context = builder
155            .build()
156            .expect("MutationContext requires both path and span for variable definition");
157
158        self.collect_mutations(&context);
159        self.walk_variable_definition(var)
160    }
161
162    fn visit_expr(&mut self, expr: &'ast Expr<'ast>) -> ControlFlow<Self::BreakValue> {
163        // Skip entirely when the surrounding contract is filtered out.
164        if !self.in_allowed_contract {
165            return self.walk_expr(expr);
166        }
167
168        let mut builder = MutationContext::builder()
169            .with_path(self.path.clone())
170            .with_span(expr.span)
171            .with_expr(expr);
172
173        if let Some(src) = self.source {
174            builder = builder.with_source(src);
175        }
176
177        let context =
178            builder.build().expect("MutationContext requires both path and span for expression");
179
180        self.collect_mutations(&context);
181        self.walk_expr(expr)
182    }
183
184    fn visit_yul_expr(&mut self, expr: &'ast yul::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
185        // Skip entirely when the surrounding contract is filtered out.
186        if !self.in_allowed_contract {
187            return self.walk_yul_expr(expr);
188        }
189
190        let mut builder = MutationContext::builder()
191            .with_path(self.path.clone())
192            .with_span(expr.span)
193            .with_yul_expr(expr);
194
195        if let Some(src) = self.source {
196            builder = builder.with_source(src);
197        }
198
199        let context = builder
200            .build()
201            .expect("MutationContext requires both path and span for yul expression");
202
203        self.collect_mutations(&context);
204        self.walk_yul_expr(expr)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use eyre::{Result, eyre};
211    use solar::{
212        ast::{Arena, interface::source_map::FileName},
213        parse::Parser,
214    };
215
216    use super::*;
217    use crate::mutation::{Session, mutant::MutationType};
218
219    struct FailingExprMutator;
220
221    impl Mutator for FailingExprMutator {
222        fn generate_mutants(&self, _ctxt: &MutationContext<'_>) -> Result<Vec<Mutant>> {
223            Err(eyre!("synthetic visitor failure"))
224        }
225
226        fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
227            ctxt.expr.is_some()
228        }
229    }
230
231    struct PassingExprMutator;
232
233    impl Mutator for PassingExprMutator {
234        fn generate_mutants(&self, ctxt: &MutationContext<'_>) -> Result<Vec<Mutant>> {
235            Ok(vec![Mutant {
236                path: ctxt.path.clone(),
237                span: ctxt.span,
238                mutation: MutationType::DeleteExpression,
239                original: ctxt.original_text(),
240                source_line: ctxt.source_line(),
241                line_number: ctxt.line_number(),
242                column_number: ctxt.column_number(),
243            }])
244        }
245
246        fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
247            ctxt.expr.is_some()
248        }
249    }
250
251    #[test]
252    fn visitor_collects_mutations_and_surfaces_mutator_errors() {
253        let source = "\
254// SPDX-License-Identifier: MIT
255pragma solidity ^0.8.0;
256contract Test {
257    function test() public {
258        uint256 x = 1 + 2;
259    }
260}
261";
262        let path = PathBuf::from("test.sol");
263        let sess = Session::builder().with_silent_emitter(None).build();
264
265        sess.enter(|| {
266            let arena = Arena::new();
267            let mut parser =
268                Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
269                    Ok(source.to_string())
270                })
271                .unwrap();
272            let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
273            drop(parser);
274            let mut visitor = MutantVisitor::new_with_mutators(
275                path,
276                vec![Box::new(FailingExprMutator), Box::new(PassingExprMutator)],
277            )
278            .with_source(source);
279
280            let _ = visitor.visit_source_unit(&ast);
281            let errors = visitor.take_errors();
282
283            assert!(!visitor.mutation_to_conduct.is_empty());
284            assert!(!errors.is_empty());
285
286            let err = format!("{:?}", errors[0]);
287            assert!(err.contains("failed to generate mutations for test.sol:"));
288            assert!(err.contains("synthetic visitor failure"));
289        });
290    }
291}