forge/mutation/
visitor.rs1use 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#[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 pub contract_filter: Option<Box<dyn Fn(&str) -> bool>>,
31 in_allowed_contract: bool,
36}
37
38impl<'src> MutantVisitor<'src> {
39 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 #[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 #[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 pub const fn with_source(mut self, source: &'src str) -> Self {
82 self.source = Some(source);
83 self
84 }
85
86 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 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 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 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 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}