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