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 type_analysis::{AssignmentReplacement, MutationExclusion, MutationExclusionSet},
12};
13use foundry_config::MutatorType;
14
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16pub enum AssignVarTypes {
17 Literal(OwnedLiteral),
18 Identifier(String),
19 NegatedIdentifier(String),
20}
21
22#[allow(clippy::type_complexity)]
24pub struct MutantVisitor<'src> {
25 pub mutation_to_conduct: Vec<Mutant>,
26 errors: Vec<Report>,
27 pub mutator_registry: MutatorRegistry,
28 pub path: PathBuf,
29 pub source: Option<&'src str>,
30 pub contract_filter: Option<Box<dyn Fn(&str) -> bool>>,
33 in_allowed_contract: bool,
38 mutation_exclusions: MutationExclusionSet,
39}
40
41impl<'src> MutantVisitor<'src> {
42 pub fn with_operators(path: PathBuf, operators: &[MutatorType]) -> Self {
44 Self {
45 mutation_to_conduct: Vec::new(),
46 errors: Vec::new(),
47 mutator_registry: MutatorRegistry::from_enabled(operators),
48 path,
49 source: None,
50 contract_filter: None,
51 in_allowed_contract: true,
52 mutation_exclusions: MutationExclusionSet::new(),
53 }
54 }
55
56 #[cfg(test)]
58 pub fn default(path: PathBuf) -> Self {
59 Self {
60 mutation_to_conduct: Vec::new(),
61 errors: Vec::new(),
62 mutator_registry: MutatorRegistry::default(),
63 path,
64 source: None,
65 contract_filter: None,
66 in_allowed_contract: true,
67 mutation_exclusions: MutationExclusionSet::new(),
68 }
69 }
70
71 #[cfg(test)]
73 pub fn new_with_mutators(path: PathBuf, mutators: Vec<Box<dyn Mutator>>) -> Self {
74 Self {
75 mutation_to_conduct: Vec::new(),
76 errors: Vec::new(),
77 mutator_registry: MutatorRegistry::new_with_mutators(mutators),
78 path,
79 source: None,
80 contract_filter: None,
81 in_allowed_contract: true,
82 mutation_exclusions: MutationExclusionSet::new(),
83 }
84 }
85
86 pub const fn with_source(mut self, source: &'src str) -> Self {
88 self.source = Some(source);
89 self
90 }
91
92 pub fn with_contract_filter<F>(mut self, filter: F) -> Self
95 where
96 F: Fn(&str) -> bool + 'static,
97 {
98 self.contract_filter = Some(Box::new(filter));
99 self
100 }
101
102 pub fn with_mutation_exclusions(mut self, mutations: MutationExclusionSet) -> Self {
104 self.mutation_exclusions = mutations;
105 self
106 }
107
108 pub fn take_errors(&mut self) -> Vec<Report> {
109 std::mem::take(&mut self.errors)
110 }
111
112 fn collect_mutations(&mut self, context: &MutationContext<'_>) {
113 let result = self.mutator_registry.generate_mutations(context);
114 self.mutation_to_conduct.extend(result.mutations.into_iter().filter(|mutant| {
115 let exclusion = match &mutant.mutation {
116 crate::mutation::mutant::MutationType::Assignment(kind) => {
117 let replacement = match kind {
118 AssignVarTypes::Literal(OwnedLiteral::Number(value)) if value.is_zero() => {
119 AssignmentReplacement::Zero
120 }
121 AssignVarTypes::Literal(OwnedLiteral::NegatedNumber(_))
122 | AssignVarTypes::NegatedIdentifier(_) => AssignmentReplacement::Negate,
123 _ => return true,
124 };
125 MutationExclusion::assignment(mutant.span, replacement)
126 }
127 crate::mutation::mutant::MutationType::BinaryOpExpr { new_op, .. } => {
128 MutationExclusion::binary(mutant.span, *new_op)
129 }
130 crate::mutation::mutant::MutationType::UnaryOperator(unary) => {
131 MutationExclusion::unary(mutant.span, unary.resulting_op_kind)
132 }
133 _ => return true,
134 };
135 !self.mutation_exclusions.contains(&exclusion)
136 }));
137
138 for err in result.errors {
139 self.errors.push(err.wrap_err(format!(
140 "failed to generate mutations for {}:{}:{}",
141 self.path.display(),
142 context.line_number(),
143 context.column_number()
144 )));
145 }
146 }
147}
148
149impl<'ast> Visit<'ast> for MutantVisitor<'ast> {
150 type BreakValue = ();
151
152 fn visit_item_contract(
153 &mut self,
154 contract: &'ast ItemContract<'ast>,
155 ) -> ControlFlow<Self::BreakValue> {
156 let prev = self.in_allowed_contract;
161 self.in_allowed_contract = match &self.contract_filter {
162 Some(filter) => filter(contract.name.as_str()),
163 None => true,
164 };
165 let res = self.walk_item_contract(contract);
166 self.in_allowed_contract = prev;
167 res
168 }
169
170 fn visit_variable_definition(
171 &mut self,
172 var: &'ast VariableDefinition<'ast>,
173 ) -> ControlFlow<Self::BreakValue> {
174 if !self.in_allowed_contract {
176 return self.walk_variable_definition(var);
177 }
178
179 let mut builder = MutationContext::builder()
180 .with_path(self.path.clone())
181 .with_span(var.span)
182 .with_var_definition(var);
183
184 if let Some(src) = self.source {
185 builder = builder.with_source(src);
186 }
187
188 let context = builder
189 .build()
190 .expect("MutationContext requires both path and span for variable definition");
191
192 self.collect_mutations(&context);
193 self.walk_variable_definition(var)
194 }
195
196 fn visit_expr(&mut self, expr: &'ast Expr<'ast>) -> ControlFlow<Self::BreakValue> {
197 if !self.in_allowed_contract {
199 return self.walk_expr(expr);
200 }
201
202 let mut builder = MutationContext::builder()
203 .with_path(self.path.clone())
204 .with_span(expr.span)
205 .with_expr(expr);
206
207 if let Some(src) = self.source {
208 builder = builder.with_source(src);
209 }
210
211 let context =
212 builder.build().expect("MutationContext requires both path and span for expression");
213
214 self.collect_mutations(&context);
215 self.walk_expr(expr)
216 }
217
218 fn visit_yul_expr(&mut self, expr: &'ast yul::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
219 if !self.in_allowed_contract {
221 return self.walk_yul_expr(expr);
222 }
223
224 let mut builder = MutationContext::builder()
225 .with_path(self.path.clone())
226 .with_span(expr.span)
227 .with_yul_expr(expr);
228
229 if let Some(src) = self.source {
230 builder = builder.with_source(src);
231 }
232
233 let context = builder
234 .build()
235 .expect("MutationContext requires both path and span for yul expression");
236
237 self.collect_mutations(&context);
238 self.walk_yul_expr(expr)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use eyre::{Result, eyre};
245 use solar::{
246 ast::{Arena, interface::source_map::FileName},
247 parse::Parser,
248 };
249
250 use super::*;
251 use crate::mutation::{Session, mutant::MutationType};
252
253 struct FailingExprMutator;
254
255 impl Mutator for FailingExprMutator {
256 fn generate_mutants(&self, _ctxt: &MutationContext<'_>) -> Result<Vec<Mutant>> {
257 Err(eyre!("synthetic visitor failure"))
258 }
259
260 fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
261 ctxt.expr.is_some()
262 }
263 }
264
265 struct PassingExprMutator;
266
267 impl Mutator for PassingExprMutator {
268 fn generate_mutants(&self, ctxt: &MutationContext<'_>) -> Result<Vec<Mutant>> {
269 Ok(vec![Mutant {
270 path: ctxt.path.clone(),
271 span: ctxt.span,
272 mutation: MutationType::DeleteExpression,
273 original: ctxt.original_text(),
274 source_line: ctxt.source_line(),
275 line_number: ctxt.line_number(),
276 column_number: ctxt.column_number(),
277 }])
278 }
279
280 fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
281 ctxt.expr.is_some()
282 }
283 }
284
285 #[test]
286 fn visitor_collects_mutations_and_surfaces_mutator_errors() {
287 let source = "\
288// SPDX-License-Identifier: MIT
289pragma solidity ^0.8.0;
290contract Test {
291 function test() public {
292 uint256 x = 1 + 2;
293 }
294}
295";
296 let path = PathBuf::from("test.sol");
297 let sess = Session::builder().with_silent_emitter(None).build();
298
299 sess.enter(|| {
300 let arena = Arena::new();
301 let mut parser =
302 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
303 Ok(source.to_string())
304 })
305 .unwrap();
306 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
307 drop(parser);
308 let mut visitor = MutantVisitor::new_with_mutators(
309 path,
310 vec![Box::new(FailingExprMutator), Box::new(PassingExprMutator)],
311 )
312 .with_source(source);
313
314 let _ = visitor.visit_source_unit(&ast);
315 let errors = visitor.take_errors();
316
317 assert!(!visitor.mutation_to_conduct.is_empty());
318 assert!(!errors.is_empty());
319
320 let err = format!("{:?}", errors[0]);
321 assert!(err.contains("failed to generate mutations for test.sol:"));
322 assert!(err.contains("synthetic visitor failure"));
323 });
324 }
325
326 #[test]
327 fn visitor_excludes_binary_mutations_rejected_by_type_analysis() {
328 let source = "\
329pragma solidity ^0.8.0;
330contract Test {
331 function check(uint256 x) public pure returns (bool) {
332 return x == 0;
333 }
334}
335";
336 let path = PathBuf::from("test.sol");
337 let lo = source.find("x == 0").unwrap() as u32;
338 let span = solar::ast::Span::new(
339 solar::interface::BytePos(lo),
340 solar::interface::BytePos(lo + "x == 0".len() as u32),
341 );
342 let mutation_exclusions =
343 [MutationExclusion::binary(span, solar::ast::BinOpKind::Le)].into_iter().collect();
344 let sess = Session::builder().with_silent_emitter(None).build();
345
346 sess.enter(|| {
347 let arena = Arena::new();
348 let mut parser =
349 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
350 Ok(source.to_string())
351 })
352 .unwrap();
353 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
354 drop(parser);
355 let mut visitor = MutantVisitor::default(path)
356 .with_source(source)
357 .with_mutation_exclusions(mutation_exclusions);
358
359 let _ = visitor.visit_source_unit(&ast);
360 let comparison_mutations = visitor.mutation_to_conduct.iter().filter_map(|mutant| {
361 let MutationType::BinaryOpExpr { new_op, .. } = mutant.mutation else {
362 return None;
363 };
364 (mutant.span == span).then_some(new_op)
365 });
366 let comparison_mutations = comparison_mutations.collect::<Vec<_>>();
367
368 assert!(!comparison_mutations.contains(&solar::ast::BinOpKind::Le));
369 assert!(comparison_mutations.contains(&solar::ast::BinOpKind::Lt));
370 });
371 }
372
373 #[test]
374 fn visitor_excludes_assignment_mutations_rejected_by_type_analysis() {
375 let source = "\
376pragma solidity ^0.8.0;
377contract Test {
378 function check(address account) public pure {
379 address copy = account;
380 }
381}
382";
383 let path = PathBuf::from("test.sol");
384 let lo = source.rfind("account").unwrap() as u32;
385 let span = solar::ast::Span::new(
386 solar::interface::BytePos(lo),
387 solar::interface::BytePos(lo + "account".len() as u32),
388 );
389 let mutation_exclusions = [
390 MutationExclusion::assignment(span, AssignmentReplacement::Zero),
391 MutationExclusion::assignment(span, AssignmentReplacement::Negate),
392 ]
393 .into_iter()
394 .collect();
395 let sess = Session::builder().with_silent_emitter(None).build();
396
397 sess.enter(|| {
398 let arena = Arena::new();
399 let mut parser =
400 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
401 Ok(source.to_string())
402 })
403 .unwrap();
404 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
405 drop(parser);
406 let mut visitor = MutantVisitor::new_with_mutators(
407 path,
408 vec![Box::new(crate::mutation::mutators::assignment_mutator::AssignmentMutator)],
409 )
410 .with_source(source)
411 .with_mutation_exclusions(mutation_exclusions);
412
413 let _ = visitor.visit_source_unit(&ast);
414
415 assert!(visitor.mutation_to_conduct.iter().all(|mutant| mutant.span != span));
416 });
417 }
418
419 #[test]
420 fn visitor_excludes_negated_number_but_retains_zero_assignment() {
421 let source = "\
422pragma solidity ^0.8.0;
423contract Test {
424 function check() public pure {
425 uint8 copy = 1;
426 }
427}
428";
429 let path = PathBuf::from("test.sol");
430 let lo = source.rfind('1').unwrap() as u32;
431 let span =
432 solar::ast::Span::new(solar::interface::BytePos(lo), solar::interface::BytePos(lo + 1));
433 let mutation_exclusions =
434 [MutationExclusion::assignment(span, AssignmentReplacement::Negate)]
435 .into_iter()
436 .collect();
437 let sess = Session::builder().with_silent_emitter(None).build();
438
439 sess.enter(|| {
440 let arena = Arena::new();
441 let mut parser =
442 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
443 Ok(source.to_string())
444 })
445 .unwrap();
446 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
447 drop(parser);
448 let mut visitor = MutantVisitor::new_with_mutators(
449 path,
450 vec![Box::new(crate::mutation::mutators::assignment_mutator::AssignmentMutator)],
451 )
452 .with_source(source)
453 .with_mutation_exclusions(mutation_exclusions);
454
455 let _ = visitor.visit_source_unit(&ast);
456 let replacements = visitor
457 .mutation_to_conduct
458 .iter()
459 .filter(|mutant| mutant.span == span)
460 .map(|mutant| mutant.mutation.to_string())
461 .collect::<Vec<_>>();
462
463 assert_eq!(replacements, ["0"]);
464 });
465 }
466
467 #[test]
468 fn visitor_excludes_unsigned_negation_but_retains_unary_swaps() {
469 let source = "\
470pragma solidity ^0.8.0;
471contract Test {
472 function increment(uint256 x) public pure {
473 x++;
474 }
475}
476";
477 let path = PathBuf::from("test.sol");
478 let lo = source.find("x++").unwrap() as u32;
479 let span = solar::ast::Span::new(
480 solar::interface::BytePos(lo),
481 solar::interface::BytePos(lo + "x++".len() as u32),
482 );
483 let mutation_exclusions =
484 [MutationExclusion::unary(span, solar::ast::UnOpKind::Neg)].into_iter().collect();
485 let sess = Session::builder().with_silent_emitter(None).build();
486
487 sess.enter(|| {
488 let arena = Arena::new();
489 let mut parser =
490 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
491 Ok(source.to_string())
492 })
493 .unwrap();
494 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
495 drop(parser);
496 let mut visitor = MutantVisitor::new_with_mutators(
497 path,
498 vec![Box::new(crate::mutation::mutators::unary_op_mutator::UnaryOpMutator)],
499 )
500 .with_source(source)
501 .with_mutation_exclusions(mutation_exclusions);
502
503 let _ = visitor.visit_source_unit(&ast);
504 let unary_mutations = visitor
505 .mutation_to_conduct
506 .iter()
507 .filter_map(|mutant| {
508 let MutationType::UnaryOperator(unary) = &mutant.mutation else {
509 return None;
510 };
511 (mutant.span == span).then_some(unary.resulting_op_kind)
512 })
513 .collect::<Vec<_>>();
514
515 assert_eq!(unary_mutations.len(), 4);
516 assert!(!unary_mutations.contains(&solar::ast::UnOpKind::Neg));
517 assert!(unary_mutations.contains(&solar::ast::UnOpKind::PreInc));
518 assert!(unary_mutations.contains(&solar::ast::UnOpKind::PreDec));
519 assert!(unary_mutations.contains(&solar::ast::UnOpKind::BitNot));
520 assert!(unary_mutations.contains(&solar::ast::UnOpKind::PostDec));
521 });
522 }
523}