1use crate::mutation::{
2 mutant::{Mutant, OwnedLiteral},
3 mutators::{MutationContext, mutator_registry::MutatorRegistry},
4 type_analysis::{AssignmentReplacement, MutationExclusion, MutationExclusionSet},
5};
6use eyre::Report;
7use foundry_config::MutatorType;
8use solar::ast::{Expr, ItemContract, ItemFunction, VariableDefinition, visit::Visit, yul};
9use std::{ops::ControlFlow, path::PathBuf};
10
11#[cfg(test)]
12use crate::mutation::mutators::Mutator;
13
14#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
15pub enum AssignVarTypes {
16 Literal(OwnedLiteral),
17 Identifier(String),
18 NegatedIdentifier(String),
19}
20
21#[allow(clippy::type_complexity)]
23pub struct MutantVisitor<'src> {
24 pub mutation_to_conduct: Vec<Mutant>,
25 errors: Vec<Report>,
26 pub mutator_registry: MutatorRegistry,
27 pub path: PathBuf,
28 pub source: Option<&'src str>,
29 pub contract_filter: Option<Box<dyn Fn(&str) -> bool>>,
32 in_allowed_contract: bool,
37 mutation_exclusions: MutationExclusionSet,
38}
39
40impl<'src> MutantVisitor<'src> {
41 pub fn with_operators(path: PathBuf, operators: &[MutatorType]) -> Self {
43 Self {
44 mutation_to_conduct: Vec::new(),
45 errors: Vec::new(),
46 mutator_registry: MutatorRegistry::from_enabled(operators),
47 path,
48 source: None,
49 contract_filter: None,
50 in_allowed_contract: true,
51 mutation_exclusions: MutationExclusionSet::new(),
52 }
53 }
54
55 #[cfg(test)]
57 pub fn new_with_mutators(path: PathBuf, mutators: Vec<Box<dyn Mutator>>) -> Self {
58 Self {
59 mutator_registry: MutatorRegistry::new_with_mutators(mutators),
60 ..Self::with_operators(path, &[])
61 }
62 }
63
64 pub const fn with_source(mut self, source: &'src str) -> Self {
66 self.source = Some(source);
67 self
68 }
69
70 pub fn with_contract_filter<F>(mut self, filter: F) -> Self
73 where
74 F: Fn(&str) -> bool + 'static,
75 {
76 self.contract_filter = Some(Box::new(filter));
77 self
78 }
79
80 pub fn with_mutation_exclusions(mut self, mutations: MutationExclusionSet) -> Self {
82 self.mutation_exclusions = mutations;
83 self
84 }
85
86 pub fn take_errors(&mut self) -> Vec<Report> {
87 std::mem::take(&mut self.errors)
88 }
89
90 fn collect_mutations(&mut self, context: &MutationContext<'_>) {
91 let result = self.mutator_registry.generate_mutations(context);
92 self.mutation_to_conduct.extend(result.mutations.into_iter().filter(|mutant| {
93 let exclusion = match &mutant.mutation {
94 crate::mutation::mutant::MutationType::Assignment(kind) => {
95 let replacement = match kind {
96 AssignVarTypes::Literal(OwnedLiteral::Number(value)) if value.is_zero() => {
97 AssignmentReplacement::Zero
98 }
99 AssignVarTypes::Literal(OwnedLiteral::NegatedNumber(_))
100 | AssignVarTypes::NegatedIdentifier(_) => AssignmentReplacement::Negate,
101 _ => return true,
102 };
103 MutationExclusion::assignment(mutant.span, replacement)
104 }
105 crate::mutation::mutant::MutationType::BinaryOpExpr { new_op, .. } => {
106 MutationExclusion::binary(mutant.span, *new_op)
107 }
108 crate::mutation::mutant::MutationType::UnaryOperator(unary) => {
109 MutationExclusion::unary(mutant.span, unary.resulting_op_kind)
110 }
111 _ => return true,
112 };
113 !self.mutation_exclusions.contains(&exclusion)
114 }));
115
116 for err in result.errors {
117 self.errors.push(err.wrap_err(format!(
118 "failed to generate mutations for {}:{}:{}",
119 self.path.display(),
120 context.line_number(),
121 context.column_number()
122 )));
123 }
124 }
125}
126
127impl<'ast> Visit<'ast> for MutantVisitor<'ast> {
128 type BreakValue = ();
129
130 fn visit_item_contract(
131 &mut self,
132 contract: &'ast ItemContract<'ast>,
133 ) -> ControlFlow<Self::BreakValue> {
134 let prev = self.in_allowed_contract;
139 self.in_allowed_contract = match &self.contract_filter {
140 Some(filter) => filter(contract.name.as_str()),
141 None => true,
142 };
143 let res = self.walk_item_contract(contract);
144 self.in_allowed_contract = prev;
145 res
146 }
147
148 fn visit_item_function(
149 &mut self,
150 function: &'ast ItemFunction<'ast>,
151 ) -> ControlFlow<Self::BreakValue> {
152 if self.in_allowed_contract && function.body.is_some() {
153 let mut builder = MutationContext::builder()
154 .with_path(self.path.clone())
155 .with_span(function.body_span)
156 .with_function(function);
157 if let Some(src) = self.source {
158 builder = builder.with_source(src);
159 }
160 self.collect_mutations(&builder.build().expect("function body context"));
161 }
162 self.walk_item_function(function)
163 }
164
165 fn visit_variable_definition(
166 &mut self,
167 var: &'ast VariableDefinition<'ast>,
168 ) -> ControlFlow<Self::BreakValue> {
169 if !self.in_allowed_contract {
171 return self.walk_variable_definition(var);
172 }
173
174 let mut builder = MutationContext::builder()
175 .with_path(self.path.clone())
176 .with_span(var.span)
177 .with_var_definition(var);
178
179 if let Some(src) = self.source {
180 builder = builder.with_source(src);
181 }
182
183 let context = builder
184 .build()
185 .expect("MutationContext requires both path and span for variable definition");
186
187 self.collect_mutations(&context);
188 self.walk_variable_definition(var)
189 }
190
191 fn visit_expr(&mut self, expr: &'ast Expr<'ast>) -> ControlFlow<Self::BreakValue> {
192 if !self.in_allowed_contract {
194 return self.walk_expr(expr);
195 }
196
197 let mut builder = MutationContext::builder()
198 .with_path(self.path.clone())
199 .with_span(expr.span)
200 .with_expr(expr);
201
202 if let Some(src) = self.source {
203 builder = builder.with_source(src);
204 }
205
206 let context =
207 builder.build().expect("MutationContext requires both path and span for expression");
208
209 self.collect_mutations(&context);
210 self.walk_expr(expr)
211 }
212
213 fn visit_yul_expr(&mut self, expr: &'ast yul::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
214 if !self.in_allowed_contract {
216 return self.walk_yul_expr(expr);
217 }
218
219 let mut builder = MutationContext::builder()
220 .with_path(self.path.clone())
221 .with_span(expr.span)
222 .with_yul_expr(expr);
223
224 if let Some(src) = self.source {
225 builder = builder.with_source(src);
226 }
227
228 let context = builder
229 .build()
230 .expect("MutationContext requires both path and span for yul expression");
231
232 self.collect_mutations(&context);
233 self.walk_yul_expr(expr)
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use eyre::{Result, eyre};
240 use solar::{
241 ast::{Arena, interface::source_map::FileName},
242 parse::Parser,
243 };
244
245 use super::*;
246 use crate::mutation::{Session, mutant::MutationType};
247
248 struct FailingExprMutator;
249
250 impl Mutator for FailingExprMutator {
251 fn generate_mutants(&self, _ctxt: &MutationContext<'_>) -> Result<Vec<Mutant>> {
252 Err(eyre!("synthetic visitor failure"))
253 }
254
255 fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
256 ctxt.expr.is_some()
257 }
258 }
259
260 struct PassingExprMutator;
261
262 impl Mutator for PassingExprMutator {
263 fn generate_mutants(&self, ctxt: &MutationContext<'_>) -> Result<Vec<Mutant>> {
264 Ok(vec![Mutant {
265 path: ctxt.path.clone(),
266 span: ctxt.span,
267 mutation: MutationType::DeleteExpression,
268 original: ctxt.original_text(),
269 source_line: ctxt.source_line(),
270 line_number: ctxt.line_number(),
271 column_number: ctxt.column_number(),
272 }])
273 }
274
275 fn is_applicable(&self, ctxt: &MutationContext<'_>) -> bool {
276 ctxt.expr.is_some()
277 }
278 }
279
280 #[test]
281 fn visitor_collects_mutations_and_surfaces_mutator_errors() {
282 let source = "\
283// SPDX-License-Identifier: MIT
284pragma solidity ^0.8.0;
285contract Test {
286 function test() public {
287 uint256 x = 1 + 2;
288 }
289}
290";
291 let path = PathBuf::from("test.sol");
292 let sess = Session::builder().with_silent_emitter(None).build();
293
294 sess.enter(|| {
295 let arena = Arena::new();
296 let mut parser =
297 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
298 Ok(source.to_string())
299 })
300 .unwrap();
301 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
302 drop(parser);
303 let mut visitor = MutantVisitor::new_with_mutators(
304 path,
305 vec![Box::new(FailingExprMutator), Box::new(PassingExprMutator)],
306 )
307 .with_source(source);
308
309 let _ = visitor.visit_source_unit(&ast);
310 let errors = visitor.take_errors();
311
312 assert!(!visitor.mutation_to_conduct.is_empty());
313 assert!(!errors.is_empty());
314
315 let err = format!("{:?}", errors[0]);
316 assert!(err.contains("failed to generate mutations for test.sol:"));
317 assert!(err.contains("synthetic visitor failure"));
318 });
319 }
320
321 #[test]
322 fn visitor_excludes_binary_mutations_rejected_by_type_analysis() {
323 let source = "\
324pragma solidity ^0.8.0;
325contract Test {
326 function check(uint256 x) public pure returns (bool) {
327 return x == 0;
328 }
329}
330";
331 let path = PathBuf::from("test.sol");
332 let lo = source.find("x == 0").unwrap() as u32;
333 let span = solar::ast::Span::new(
334 solar::interface::BytePos(lo),
335 solar::interface::BytePos(lo + "x == 0".len() as u32),
336 );
337 let mutation_exclusions =
338 [MutationExclusion::binary(span, solar::ast::BinOpKind::Le)].into_iter().collect();
339 let sess = Session::builder().with_silent_emitter(None).build();
340
341 sess.enter(|| {
342 let arena = Arena::new();
343 let mut parser =
344 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
345 Ok(source.to_string())
346 })
347 .unwrap();
348 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
349 drop(parser);
350 let mut visitor = MutantVisitor::with_operators(path, &MutatorType::all())
351 .with_source(source)
352 .with_mutation_exclusions(mutation_exclusions);
353
354 let _ = visitor.visit_source_unit(&ast);
355 let comparison_mutations = visitor.mutation_to_conduct.iter().filter_map(|mutant| {
356 let MutationType::BinaryOpExpr { new_op, .. } = mutant.mutation else {
357 return None;
358 };
359 (mutant.span == span).then_some(new_op)
360 });
361 let comparison_mutations = comparison_mutations.collect::<Vec<_>>();
362
363 assert!(!comparison_mutations.contains(&solar::ast::BinOpKind::Le));
364 assert!(comparison_mutations.contains(&solar::ast::BinOpKind::Lt));
365 });
366 }
367
368 #[test]
369 fn visitor_excludes_assignment_mutations_rejected_by_type_analysis() {
370 let source = "\
371pragma solidity ^0.8.0;
372contract Test {
373 function check(address account) public pure {
374 address copy = account;
375 }
376}
377";
378 let path = PathBuf::from("test.sol");
379 let lo = source.rfind("account").unwrap() as u32;
380 let span = solar::ast::Span::new(
381 solar::interface::BytePos(lo),
382 solar::interface::BytePos(lo + "account".len() as u32),
383 );
384 let mutation_exclusions = [
385 MutationExclusion::assignment(span, AssignmentReplacement::Zero),
386 MutationExclusion::assignment(span, AssignmentReplacement::Negate),
387 ]
388 .into_iter()
389 .collect();
390 let sess = Session::builder().with_silent_emitter(None).build();
391
392 sess.enter(|| {
393 let arena = Arena::new();
394 let mut parser =
395 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
396 Ok(source.to_string())
397 })
398 .unwrap();
399 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
400 drop(parser);
401 let mut visitor = MutantVisitor::new_with_mutators(
402 path,
403 vec![Box::new(crate::mutation::mutators::assignment_mutator::AssignmentMutator)],
404 )
405 .with_source(source)
406 .with_mutation_exclusions(mutation_exclusions);
407
408 let _ = visitor.visit_source_unit(&ast);
409
410 assert!(visitor.mutation_to_conduct.iter().all(|mutant| mutant.span != span));
411 });
412 }
413
414 #[test]
415 fn visitor_excludes_negated_number_but_retains_zero_assignment() {
416 let source = "\
417pragma solidity ^0.8.0;
418contract Test {
419 function check() public pure {
420 uint8 copy = 1;
421 }
422}
423";
424 let path = PathBuf::from("test.sol");
425 let lo = source.rfind('1').unwrap() as u32;
426 let span =
427 solar::ast::Span::new(solar::interface::BytePos(lo), solar::interface::BytePos(lo + 1));
428 let mutation_exclusions =
429 [MutationExclusion::assignment(span, AssignmentReplacement::Negate)]
430 .into_iter()
431 .collect();
432 let sess = Session::builder().with_silent_emitter(None).build();
433
434 sess.enter(|| {
435 let arena = Arena::new();
436 let mut parser =
437 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
438 Ok(source.to_string())
439 })
440 .unwrap();
441 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
442 drop(parser);
443 let mut visitor = MutantVisitor::new_with_mutators(
444 path,
445 vec![Box::new(crate::mutation::mutators::assignment_mutator::AssignmentMutator)],
446 )
447 .with_source(source)
448 .with_mutation_exclusions(mutation_exclusions);
449
450 let _ = visitor.visit_source_unit(&ast);
451 let replacements = visitor
452 .mutation_to_conduct
453 .iter()
454 .filter(|mutant| mutant.span == span)
455 .map(|mutant| mutant.mutation.to_string())
456 .collect::<Vec<_>>();
457
458 assert_eq!(replacements, ["0"]);
459 });
460 }
461
462 #[test]
463 fn visitor_excludes_unsigned_negation_but_retains_unary_swaps() {
464 let source = "\
465pragma solidity ^0.8.0;
466contract Test {
467 function increment(uint256 x) public pure {
468 x++;
469 }
470}
471";
472 let path = PathBuf::from("test.sol");
473 let lo = source.find("x++").unwrap() as u32;
474 let span = solar::ast::Span::new(
475 solar::interface::BytePos(lo),
476 solar::interface::BytePos(lo + "x++".len() as u32),
477 );
478 let mutation_exclusions =
479 [MutationExclusion::unary(span, solar::ast::UnOpKind::Neg)].into_iter().collect();
480 let sess = Session::builder().with_silent_emitter(None).build();
481
482 sess.enter(|| {
483 let arena = Arena::new();
484 let mut parser =
485 Parser::from_lazy_source_code(&sess, &arena, FileName::Real(path.clone()), || {
486 Ok(source.to_string())
487 })
488 .unwrap();
489 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
490 drop(parser);
491 let mut visitor = MutantVisitor::new_with_mutators(
492 path,
493 vec![Box::new(crate::mutation::mutators::unary_op_mutator::UnaryOpMutator)],
494 )
495 .with_source(source)
496 .with_mutation_exclusions(mutation_exclusions);
497
498 let _ = visitor.visit_source_unit(&ast);
499 let unary_mutations = visitor
500 .mutation_to_conduct
501 .iter()
502 .filter_map(|mutant| {
503 let MutationType::UnaryOperator(unary) = &mutant.mutation else {
504 return None;
505 };
506 (mutant.span == span).then_some(unary.resulting_op_kind)
507 })
508 .collect::<Vec<_>>();
509
510 assert_eq!(unary_mutations.len(), 4);
511 assert!(!unary_mutations.contains(&solar::ast::UnOpKind::Neg));
512 assert!(unary_mutations.contains(&solar::ast::UnOpKind::PreInc));
513 assert!(unary_mutations.contains(&solar::ast::UnOpKind::PreDec));
514 assert!(unary_mutations.contains(&solar::ast::UnOpKind::BitNot));
515 assert!(unary_mutations.contains(&solar::ast::UnOpKind::PostDec));
516 });
517 }
518
519 #[test]
520 fn function_body_replacements_preserve_headers_and_skip_unsupported_functions() {
521 let source = r#"
522abstract contract Target {
523 constructor() { revert(); }
524 modifier guarded() { require(true); _; }
525 function effect() public guarded { revert(); }
526 function boolean() external pure returns (bool) { return 2 > 1; }
527 function unsigned() internal pure returns (uint8 result) { result = 7; }
528 function signed() private pure returns (int256) { return -7; }
529 function account() public view returns (address) { return msg.sender; }
530 function payableAccount() public view returns (address payable) { return payable(msg.sender); }
531 function tuple() public virtual returns (uint, bool);
532 function dynamic() public pure returns (bytes memory) { return hex"1234"; }
533 function multiple() public pure returns (uint, bool) { return (7, true); }
534 function testSomething() public { revert(); }
535 function invariantSomething() public { revert(); }
536 function setUp() public { revert(); }
537 function empty() public {}
538 function emptyCommented() public { /* No work. */ }
539 function implicitReturn() public pure returns (uint256 result) {}
540 receive() external payable { revert(); }
541 fallback() external { revert(); }
542}
543contract Other { function ignored() public { revert(); } }
544"#;
545 let mutants = function_body_mutants(source);
546 let first = &mutants[0];
547 let mut mutated = source.to_string();
548 mutated.replace_range(
549 first.span.lo().0 as usize..first.span.hi().0 as usize,
550 &first.mutation.to_string(),
551 );
552 assert_eq!(
553 mutated,
554 source.replacen(
555 "function effect() public guarded { revert(); }",
556 "function effect() public guarded {}",
557 1,
558 )
559 );
560 assert_eq!(
561 mutants.iter().map(|mutant| mutant.mutation.to_string()).collect::<Vec<_>>(),
562 [
563 "{}",
564 "{ return true; }",
565 "{ return false; }",
566 "{ return 0; }",
567 "{ return 1; }",
568 "{ return 0; }",
569 "{ return 1; }",
570 "{ return -1; }",
571 "{ return address(0); }",
572 "{ return payable(address(0)); }",
573 "{}",
574 "{}",
575 "{}",
576 "{ return 1; }",
577 ]
578 );
579 }
580
581 #[test]
582 fn function_body_replacements_skip_equivalent_constants() {
583 for (ty, expression, expected) in [
584 ("bool", "(true)", vec!["false"]),
585 ("bool", "(/* Always on. */ true)", vec!["false"]),
586 ("bool", "bool((false))", vec!["true"]),
587 ("uint256", "0x0", vec!["1"]),
588 ("uint8", "uint8(0)", vec!["1"]),
589 ("uint256", "1_0e-1", vec!["0"]),
590 ("int256", "-(int8((0x1)))", vec!["0", "1"]),
591 ("int256", "int256(-1)", vec!["0", "1"]),
592 ("int256", "-(-1)", vec!["0", "-1"]),
593 ("int256", "-0", vec!["1", "-1"]),
594 ("address", "address(uint160(0x0))", vec![]),
595 ("address payable", "payable(address((0)))", vec![]),
596 ("bool", "other()", vec!["true", "false"]),
598 ("uint8", "uint8(other())", vec!["0", "1"]),
599 ] {
600 let source = format!(
601 "contract Target {{ function target() public returns ({ty}) {{ return {expression}; }} }}"
602 );
603 let replacements = function_body_mutants(&source)
604 .iter()
605 .map(|mutant| mutant.mutation.to_string())
606 .collect::<Vec<_>>();
607 assert_eq!(
608 replacements,
609 expected.iter().map(|value| format!("{{ return {value}; }}")).collect::<Vec<_>>(),
610 "{source}"
611 );
612 }
613 }
614
615 fn function_body_mutants(source: &str) -> Vec<Mutant> {
616 let sess = Session::builder().with_silent_emitter(None).build();
617 sess.enter(|| {
618 let arena = Arena::new();
619 let mut parser = Parser::from_lazy_source_code(
620 &sess,
621 &arena,
622 FileName::Real("target.sol".into()),
623 || Ok(source.to_string()),
624 )
625 .unwrap();
626 let ast = parser.parse_file().map_err(|e| e.emit()).unwrap();
627 drop(parser);
628 let mut visitor =
629 MutantVisitor::with_operators("target.sol".into(), &[MutatorType::FunctionBody])
630 .with_source(source)
631 .with_contract_filter(|name| name == "Target");
632 let _ = visitor.visit_source_unit(&ast);
633 assert!(visitor.take_errors().is_empty());
634 for mutant in &visitor.mutation_to_conduct {
635 assert_eq!(
636 mutant.original,
637 &source[mutant.span.lo().0 as usize..mutant.span.hi().0 as usize]
638 );
639 }
640 visitor.mutation_to_conduct
641 })
642 }
643}