1use std::{
8 collections::{HashMap, HashSet},
9 ops::ControlFlow,
10 path::{Path, PathBuf},
11};
12
13use eyre::Result;
14use foundry_cli::opts::configure_pcx_from_compile_output;
15use foundry_compilers::{ProjectCompileOutput, compilers::multi::MultiCompiler};
16use foundry_config::Config;
17use solar::{
18 ast::{BinOpKind, LitKind, UnOpKind},
19 interface::{Session, source_map::FileName},
20 sema::{
21 Compiler, CompilerRef, Gcx,
22 hir::{self, ElementaryType, ExprKind, Visit},
23 ty::TyKind,
24 },
25};
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28enum ReplacementOperator {
29 Assignment(AssignmentReplacement),
30 Binary(BinOpKind),
31 Unary(UnOpKind),
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
35pub enum AssignmentReplacement {
36 Zero,
37 Negate,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41pub struct MutationExclusion {
42 lo: u32,
43 hi: u32,
44 new_op: ReplacementOperator,
45}
46
47impl MutationExclusion {
48 pub fn assignment(span: solar::ast::Span, replacement: AssignmentReplacement) -> Self {
49 Self {
50 lo: span.lo().0,
51 hi: span.hi().0,
52 new_op: ReplacementOperator::Assignment(replacement),
53 }
54 }
55
56 pub fn binary(span: solar::ast::Span, new_op: BinOpKind) -> Self {
57 Self { lo: span.lo().0, hi: span.hi().0, new_op: ReplacementOperator::Binary(new_op) }
58 }
59
60 pub fn unary(span: solar::ast::Span, new_op: UnOpKind) -> Self {
61 Self { lo: span.lo().0, hi: span.hi().0, new_op: ReplacementOperator::Unary(new_op) }
62 }
63}
64
65pub type MutationExclusionSet = HashSet<MutationExclusion>;
66pub type MutationExclusionsByPath = HashMap<PathBuf, MutationExclusionSet>;
67
68pub fn collect_mutation_exclusions(
69 config: &Config,
70 output: &ProjectCompileOutput<MultiCompiler>,
71) -> Result<MutationExclusionsByPath> {
72 let mut compiler = Compiler::new(Session::builder().with_silent_emitter(None).build());
73 compiler.enter_mut(|compiler| {
74 let mut pcx = compiler.parse();
75 configure_pcx_from_compile_output(&mut pcx, config, output, None)?;
76 pcx.parse();
77
78 let Ok(ControlFlow::Continue(())) = compiler.lower_asts() else {
79 return Ok(MutationExclusionsByPath::new());
80 };
81 Ok(analyze_and_collect(compiler))
82 })
83}
84
85fn analyze_and_collect(compiler: &CompilerRef<'_>) -> MutationExclusionsByPath {
86 let Ok(ControlFlow::Continue(())) = compiler.analysis() else {
87 return MutationExclusionsByPath::new();
88 };
89 if compiler.dcx().has_errors().is_err() {
90 return MutationExclusionsByPath::new();
91 }
92 collect_from_gcx(compiler.gcx())
93}
94
95fn collect_from_gcx<'gcx>(gcx: Gcx<'gcx>) -> MutationExclusionsByPath {
96 let mut by_path = MutationExclusionsByPath::new();
97 for source_id in gcx.hir.source_ids() {
98 let source = gcx.hir.source(source_id);
99 let FileName::Real(path) = &source.file.name else { continue };
100 let mut collector = MutationExclusionCollector {
101 gcx,
102 source_start: source.file.start_pos.0,
105 mutations: MutationExclusionSet::new(),
106 };
107 let _ = collector.visit_nested_source(source_id);
108 if !collector.mutations.is_empty() {
109 by_path.insert(normalize_path(path), collector.mutations);
110 }
111 }
112 by_path
113}
114
115pub fn normalize_path(path: &Path) -> PathBuf {
116 path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
117}
118
119struct MutationExclusionCollector<'hir> {
120 gcx: Gcx<'hir>,
121 source_start: u32,
122 mutations: MutationExclusionSet,
123}
124
125impl<'hir> Visit<'hir> for MutationExclusionCollector<'hir> {
126 type BreakValue = ();
127
128 fn hir(&self) -> &'hir hir::Hir<'hir> {
129 &self.gcx.hir
130 }
131
132 fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
133 if let ExprKind::Assign(destination, op, value) = &expr.kind
134 && let Some(ty) = self.gcx.type_of_expr(destination.id)
135 {
136 if let Some(op) = op {
137 self.collect_compound_assignment(value, ty, op.kind);
138 } else {
139 self.collect_assignment(value, ty);
140 }
141 }
142 if let ExprKind::Binary(left, op, right) = &expr.kind
143 && (op.kind.is_cmp() || matches!(op.kind, BinOpKind::And | BinOpKind::Or))
144 {
145 self.collect_comparison(expr, left, op.kind, right);
146 }
147 if let ExprKind::Unary(_, operand) = &expr.kind
148 && let Some(kind) = unary_operand_kind(self.gcx, operand)
149 && let Some(span) = self.local_span(expr.span)
150 {
151 match kind {
152 UnaryOperandKind::SignedInteger => {}
153 UnaryOperandKind::UnsignedInteger => {
154 self.mutations.insert(MutationExclusion::unary(span, UnOpKind::Neg));
155 }
156 UnaryOperandKind::FixedBytes => {
157 for op in [
158 UnOpKind::PreInc,
159 UnOpKind::PreDec,
160 UnOpKind::PostInc,
161 UnOpKind::PostDec,
162 UnOpKind::Neg,
163 ] {
164 self.mutations.insert(MutationExclusion::unary(span, op));
165 }
166 }
167 }
168 }
169 if let ExprKind::Unary(_, operand) = &expr.kind
170 && is_non_storage_push_call(self.gcx, operand)
171 && let Some(span) = self.local_span(expr.span)
172 {
173 for op in [UnOpKind::PreInc, UnOpKind::PreDec, UnOpKind::PostInc, UnOpKind::PostDec] {
174 self.mutations.insert(MutationExclusion::unary(span, op));
175 }
176 }
177 self.walk_expr(expr)
178 }
179
180 fn visit_var(&mut self, var: &'hir hir::Variable<'hir>) -> ControlFlow<Self::BreakValue> {
181 if let Some(value) = var.initializer {
182 self.collect_assignment(value, self.gcx.type_of_hir_ty(&var.ty));
183 }
184 self.walk_var(var)
185 }
186}
187
188impl MutationExclusionCollector<'_> {
189 fn collect_assignment(&mut self, value: &hir::Expr<'_>, destination: solar::sema::ty::Ty<'_>) {
190 let Some(span) = self.local_span(value.span) else { return };
191 let Some(kind) = assignment_destination_kind(destination) else { return };
192
193 if !kind.accepts_zero() {
194 self.mutations.insert(MutationExclusion::assignment(span, AssignmentReplacement::Zero));
195 }
196 if !kind.accepts_negation() {
197 self.mutations
198 .insert(MutationExclusion::assignment(span, AssignmentReplacement::Negate));
199 }
200 }
201
202 fn collect_compound_assignment(
203 &mut self,
204 value: &hir::Expr<'_>,
205 destination: solar::sema::ty::Ty<'_>,
206 op: BinOpKind,
207 ) {
208 let Some(span) = self.local_span(value.span) else { return };
209 let Some(destination) = assignment_destination_kind(destination) else { return };
210 let Some(value_accepts_negation) = assignment_value_accepts_negation(self.gcx, value)
211 else {
212 return;
213 };
214
215 if op.is_shift() || !destination.accepts_negation() || !value_accepts_negation {
216 self.mutations
217 .insert(MutationExclusion::assignment(span, AssignmentReplacement::Negate));
218 }
219 }
220
221 fn collect_comparison(
222 &mut self,
223 expr: &hir::Expr<'_>,
224 left: &hir::Expr<'_>,
225 original: BinOpKind,
226 right: &hir::Expr<'_>,
227 ) {
228 let Some(span) = self.local_span(expr.span) else { return };
229
230 for candidate in [
231 BinOpKind::Lt,
232 BinOpKind::Le,
233 BinOpKind::Gt,
234 BinOpKind::Ge,
235 BinOpKind::Eq,
236 BinOpKind::Ne,
237 BinOpKind::Or,
238 BinOpKind::And,
239 ] {
240 if candidate != original
241 && is_type_invalid_replacement(self.gcx, left, original, right, candidate)
242 {
243 self.mutations.insert(MutationExclusion::binary(span, candidate));
244 }
245 }
246 }
247
248 fn local_span(&self, span: solar::ast::Span) -> Option<solar::ast::Span> {
249 let lo = span.lo().0.checked_sub(self.source_start)?;
250 let hi = span.hi().0.checked_sub(self.source_start)?;
251 Some(solar::ast::Span::new(solar::interface::BytePos(lo), solar::interface::BytePos(hi)))
252 }
253}
254
255#[derive(Clone, Copy)]
256enum AssignmentDestinationKind {
257 SignedNumber,
258 UnsignedNumber,
259 FixedBytes,
260 Other,
261}
262
263impl AssignmentDestinationKind {
264 const fn accepts_zero(self) -> bool {
265 !matches!(self, Self::Other)
266 }
267
268 const fn accepts_negation(self) -> bool {
269 matches!(self, Self::SignedNumber)
270 }
271}
272
273fn assignment_destination_kind(ty: solar::sema::ty::Ty<'_>) -> Option<AssignmentDestinationKind> {
274 match ty.peel_refs().kind {
275 TyKind::Elementary(ElementaryType::Int(_) | ElementaryType::Fixed(..)) => {
276 Some(AssignmentDestinationKind::SignedNumber)
277 }
278 TyKind::Elementary(ElementaryType::UInt(_) | ElementaryType::UFixed(..)) => {
279 Some(AssignmentDestinationKind::UnsignedNumber)
280 }
281 TyKind::Elementary(ElementaryType::FixedBytes(_)) => {
282 Some(AssignmentDestinationKind::FixedBytes)
283 }
284 TyKind::Udvt(..) | TyKind::Err(_) => None,
285 _ => Some(AssignmentDestinationKind::Other),
286 }
287}
288
289fn assignment_value_accepts_negation(gcx: Gcx<'_>, value: &hir::Expr<'_>) -> Option<bool> {
290 if matches!(&value.peel_parens().kind, ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Number(_)))
291 {
292 return Some(true);
293 }
294
295 let ty = gcx.type_of_expr(value.peel_parens().id)?;
296 match ty.peel_refs().kind {
297 TyKind::Elementary(ElementaryType::Int(_) | ElementaryType::Fixed(..)) => Some(true),
298 TyKind::Elementary(
299 ElementaryType::UInt(_) | ElementaryType::UFixed(..) | ElementaryType::FixedBytes(_),
300 ) => Some(false),
301 TyKind::Udvt(..) | TyKind::Err(_) => None,
302 _ => Some(false),
303 }
304}
305
306fn is_type_invalid_replacement(
307 gcx: Gcx<'_>,
308 left: &hir::Expr<'_>,
309 original: BinOpKind,
310 right: &hir::Expr<'_>,
311 candidate: BinOpKind,
312) -> bool {
313 match candidate {
314 BinOpKind::And | BinOpKind::Or => matches!(
315 comparison_operand_kind(gcx, left, right),
316 Some(ComparisonOperandKind::Function | ComparisonOperandKind::Other)
317 ),
318 BinOpKind::Lt | BinOpKind::Le | BinOpKind::Gt | BinOpKind::Ge => {
319 matches!(original, BinOpKind::And | BinOpKind::Or)
320 || matches!(
321 comparison_operand_kind(gcx, left, right),
322 Some(ComparisonOperandKind::Bool | ComparisonOperandKind::Function)
323 )
324 }
325 _ => false,
326 }
327}
328
329#[derive(Clone, Copy, Debug, PartialEq, Eq)]
330enum ComparisonOperandKind {
331 Bool,
332 Function,
333 Other,
334}
335
336fn comparison_operand_kind(
337 gcx: Gcx<'_>,
338 left: &hir::Expr<'_>,
339 right: &hir::Expr<'_>,
340) -> Option<ComparisonOperandKind> {
341 let left = gcx.type_of_expr(left.id)?;
342 let right = gcx.type_of_expr(right.id)?;
343 match (left.peel_refs().kind, right.peel_refs().kind) {
344 (TyKind::Elementary(ElementaryType::Bool), TyKind::Elementary(ElementaryType::Bool)) => {
345 Some(ComparisonOperandKind::Bool)
346 }
347 (TyKind::Fn(_), TyKind::Fn(_)) => Some(ComparisonOperandKind::Function),
348 (TyKind::Err(_), _) | (_, TyKind::Err(_)) => None,
349 _ => Some(ComparisonOperandKind::Other),
350 }
351}
352
353#[derive(Clone, Copy)]
354enum UnaryOperandKind {
355 SignedInteger,
356 UnsignedInteger,
357 FixedBytes,
358}
359
360fn unary_operand_kind(gcx: Gcx<'_>, expr: &hir::Expr<'_>) -> Option<UnaryOperandKind> {
361 let ty = gcx.type_of_expr(expr.peel_parens().id)?;
362 match ty.peel_refs().kind {
363 TyKind::Elementary(ElementaryType::Int(_)) => Some(UnaryOperandKind::SignedInteger),
364 TyKind::Elementary(ElementaryType::UInt(_)) => Some(UnaryOperandKind::UnsignedInteger),
365 TyKind::Elementary(ElementaryType::FixedBytes(_)) => Some(UnaryOperandKind::FixedBytes),
366 _ => None,
367 }
368}
369
370fn is_non_storage_push_call(gcx: Gcx<'_>, expr: &hir::Expr<'_>) -> bool {
371 let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
372 let ExprKind::Member(receiver, member) = &callee.peel_parens().kind else { return false };
373 if member.as_str() != "push" || !args.is_empty() {
374 return false;
375 }
376
377 !gcx.type_of_expr(receiver.id).is_some_and(|ty| {
378 matches!(
379 ty.kind,
380 TyKind::Ref(inner, location)
381 if location.is_storage()
382 && matches!(
383 inner.kind,
384 TyKind::DynArray(_) | TyKind::Elementary(ElementaryType::Bytes)
385 )
386 )
387 })
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use solar::interface::BytePos;
394
395 fn collect(source: &str) -> MutationExclusionSet {
396 let path = PathBuf::from("Test.sol");
397 let mut compiler = Compiler::new(Session::builder().with_silent_emitter(None).build());
398 compiler.enter_mut(|compiler| {
399 let mut pcx = compiler.parse();
400 let file =
401 pcx.sess.source_map().new_source_file(path.clone(), source).expect("source file");
402 pcx.add_file(file);
403 pcx.parse();
404 assert!(matches!(compiler.lower_asts(), Ok(ControlFlow::Continue(()))));
405 analyze_and_collect(compiler).remove(&path).unwrap_or_default()
406 })
407 }
408
409 fn mutation(source: &str, expression: &str, new_op: BinOpKind) -> MutationExclusion {
410 let lo = source.find(expression).expect("expression") as u32;
411 MutationExclusion::binary(
412 solar::ast::Span::new(BytePos(lo), BytePos(lo + expression.len() as u32)),
413 new_op,
414 )
415 }
416
417 fn unary_mutation(source: &str, expression: &str, new_op: UnOpKind) -> MutationExclusion {
418 let lo = source.find(expression).expect("expression") as u32;
419 MutationExclusion::unary(
420 solar::ast::Span::new(BytePos(lo), BytePos(lo + expression.len() as u32)),
421 new_op,
422 )
423 }
424
425 fn assignment_mutation(
426 source: &str,
427 value: &str,
428 replacement: AssignmentReplacement,
429 ) -> MutationExclusion {
430 let lo = source.rfind(value).expect("value") as u32;
431 MutationExclusion::assignment(
432 solar::ast::Span::new(BytePos(lo), BytePos(lo + value.len() as u32)),
433 replacement,
434 )
435 }
436
437 #[test]
438 fn excludes_assignments_invalid_for_destination_type() {
439 let source = r#"
440enum Choice { A, B }
441
442contract Test {
443 function check(address account, bool flag, uint256 unsigned, Choice choice) external pure {
444 address accountCopy = account;
445 bool flagCopy = flag;
446 uint256 unsignedCopy = unsigned;
447 Choice choiceCopy = choice;
448 }
449}
450"#;
451 let mutations = collect(source);
452
453 for value in ["account", "flag", "choice"] {
454 assert!(mutations.contains(&assignment_mutation(
455 source,
456 value,
457 AssignmentReplacement::Zero,
458 )));
459 assert!(mutations.contains(&assignment_mutation(
460 source,
461 value,
462 AssignmentReplacement::Negate,
463 )));
464 }
465 assert!(!mutations.contains(&assignment_mutation(
466 source,
467 "unsigned",
468 AssignmentReplacement::Zero,
469 )));
470 assert!(mutations.contains(&assignment_mutation(
471 source,
472 "unsigned",
473 AssignmentReplacement::Negate,
474 )));
475 }
476
477 #[test]
478 fn uses_lhs_type_for_assignments() {
479 let source = r#"
480contract Test {
481 function check(address account, bool flag) external pure {
482 account = account;
483 flag = flag;
484 }
485}
486"#;
487 let mutations = collect(source);
488
489 for value in ["account", "flag"] {
490 assert!(mutations.contains(&assignment_mutation(
491 source,
492 value,
493 AssignmentReplacement::Zero,
494 )));
495 assert!(mutations.contains(&assignment_mutation(
496 source,
497 value,
498 AssignmentReplacement::Negate,
499 )));
500 }
501 }
502
503 #[test]
504 fn excludes_invalid_compound_assignment_negation() {
505 let source = r#"
506contract Test {
507 uint256 unsignedTotal;
508 int256 signedTotal;
509 bytes32 word;
510
511 function check(
512 uint256 unsignedAmount,
513 int256 signedAmount,
514 bytes32 mask,
515 uint8 shift
516 ) external {
517 unsignedTotal += unsignedAmount;
518 signedTotal += signedAmount;
519 word &= mask;
520 signedTotal <<= shift;
521 signedTotal += 11;
522 signedTotal &= 12;
523 signedTotal <<= 13;
524 signedTotal >>= 14;
525 }
526}
527"#;
528 let mutations = collect(source);
529
530 for value in ["unsignedAmount", "mask", "shift"] {
531 assert!(mutations.contains(&assignment_mutation(
532 source,
533 value,
534 AssignmentReplacement::Negate,
535 )));
536 }
537 assert!(!mutations.contains(&assignment_mutation(
538 source,
539 "signedAmount",
540 AssignmentReplacement::Negate,
541 )));
542 for value in ["11", "12"] {
543 assert!(!mutations.contains(&assignment_mutation(
544 source,
545 value,
546 AssignmentReplacement::Negate,
547 )));
548 }
549 for value in ["13", "14"] {
550 assert!(mutations.contains(&assignment_mutation(
551 source,
552 value,
553 AssignmentReplacement::Negate,
554 )));
555 }
556 for value in ["unsignedAmount", "signedAmount", "mask", "shift"] {
557 assert!(!mutations.contains(&assignment_mutation(
558 source,
559 value,
560 AssignmentReplacement::Zero,
561 )));
562 }
563 }
564
565 #[test]
566 fn preserves_valid_signed_and_fixed_bytes_assignments() {
567 let source = r#"
568contract Test {
569 function check(int256 signed, bytes32 word) external pure {
570 int256 signedCopy = signed;
571 bytes32 wordCopy = word;
572 }
573}
574"#;
575 let mutations = collect(source);
576
577 for replacement in [AssignmentReplacement::Zero, AssignmentReplacement::Negate] {
578 assert!(!mutations.contains(&assignment_mutation(source, "signed", replacement)));
579 }
580 assert!(!mutations.contains(&assignment_mutation(
581 source,
582 "word",
583 AssignmentReplacement::Zero,
584 )));
585 assert!(mutations.contains(&assignment_mutation(
586 source,
587 "word",
588 AssignmentReplacement::Negate,
589 )));
590 }
591
592 #[test]
593 fn preserves_udvt_assignments() {
594 let source = r#"
595type Amount is uint256;
596
597contract Test {
598 function check(Amount amount) external pure {
599 Amount copy = amount;
600 }
601}
602"#;
603 let mutations = collect(source);
604
605 for replacement in [AssignmentReplacement::Zero, AssignmentReplacement::Negate] {
606 assert!(!mutations.contains(&assignment_mutation(source, "amount", replacement)));
607 }
608 }
609
610 #[test]
611 fn preserves_all_mutations_when_analysis_has_errors() {
612 let source = r#"
613contract Test {
614 function check(address account) external pure {
615 address copy = account;
616 unresolved = unresolved;
617 }
618}
619"#;
620
621 assert!(collect(source).is_empty());
622 }
623
624 #[test]
625 fn preserves_gas_distinct_unsigned_boundary_mutations() {
626 let source = r#"
627contract Test {
628 function check(uint256 x) external pure returns (bool) {
629 return x == 0;
630 }
631}
632"#;
633 let mutations = collect(source);
634
635 assert!(!mutations.contains(&mutation(source, "x == 0", BinOpKind::Le)));
636 assert!(!mutations.contains(&mutation(source, "x == 0", BinOpKind::Lt)));
637 }
638
639 #[test]
640 fn excludes_negation_only_for_unsigned_unary_operands() {
641 let source = r#"
642contract Test {
643 function check(uint256 unsigned, int256 signed) external pure {
644 unsigned++;
645 ++unsigned;
646 signed++;
647 }
648}
649"#;
650 let mutations = collect(source);
651
652 assert!(mutations.contains(&unary_mutation(source, "unsigned++", UnOpKind::Neg)));
653 assert!(mutations.contains(&unary_mutation(source, "++unsigned", UnOpKind::Neg)));
654 assert!(!mutations.contains(&unary_mutation(source, "signed++", UnOpKind::Neg)));
655 }
656
657 #[test]
658 fn excludes_lvalue_mutations_for_user_defined_push() {
659 let source = r#"
660contract Test {
661 function push() external pure returns (int256) {
662 return 1;
663 }
664
665 function check() external view returns (int256) {
666 return -this.push();
667 }
668}
669"#;
670 let mutations = collect(source);
671
672 for op in [UnOpKind::PreInc, UnOpKind::PreDec, UnOpKind::PostInc, UnOpKind::PostDec] {
673 assert!(mutations.contains(&unary_mutation(source, "-this.push()", op)));
674 }
675 }
676
677 #[test]
678 fn preserves_lvalue_mutations_for_storage_push() {
679 let source = r#"
680contract Test {
681 int256[] values;
682
683 function check() external returns (int256) {
684 return -values.push();
685 }
686}
687"#;
688 let mutations = collect(source);
689
690 for op in [UnOpKind::PreInc, UnOpKind::PreDec, UnOpKind::PostInc, UnOpKind::PostDec] {
691 assert!(!mutations.contains(&unary_mutation(source, "-values.push()", op)));
692 }
693 }
694
695 #[test]
696 fn excludes_invalid_fixed_bytes_unary_mutations() {
697 let source = r#"
698contract Test {
699 function check(bytes32 word) external pure returns (bytes32) {
700 return ~word;
701 }
702
703 function checkNumber(uint256 number) external pure returns (uint256) {
704 return ~number;
705 }
706}
707"#;
708 let mutations = collect(source);
709
710 for op in [
711 UnOpKind::PreInc,
712 UnOpKind::PreDec,
713 UnOpKind::PostInc,
714 UnOpKind::PostDec,
715 UnOpKind::Neg,
716 ] {
717 assert!(mutations.contains(&unary_mutation(source, "~word", op)));
718 }
719 assert!(!mutations.contains(&unary_mutation(source, "~word", UnOpKind::BitNot)));
720
721 for op in [UnOpKind::PreInc, UnOpKind::PreDec, UnOpKind::PostInc, UnOpKind::PostDec] {
722 assert!(!mutations.contains(&unary_mutation(source, "~number", op)));
723 }
724 assert!(mutations.contains(&unary_mutation(source, "~number", UnOpKind::Neg)));
725 }
726
727 #[test]
728 fn excludes_invalid_fixed_bytes_storage_push_mutations() {
729 let source = r#"
730contract Test {
731 bytes32[] values;
732
733 function check() external returns (bytes32) {
734 return ~values.push();
735 }
736}
737"#;
738 let mutations = collect(source);
739
740 for op in [
741 UnOpKind::PreInc,
742 UnOpKind::PreDec,
743 UnOpKind::PostInc,
744 UnOpKind::PostDec,
745 UnOpKind::Neg,
746 ] {
747 assert!(mutations.contains(&unary_mutation(source, "~values.push()", op)));
748 }
749 }
750
751 #[test]
752 fn preserves_overloaded_negation_for_unsigned_udvt() {
753 let source = r#"
754type Amount is uint256;
755
756function negate(Amount amount) pure returns (Amount) {
757 return Amount.wrap(type(uint256).max - Amount.unwrap(amount));
758}
759
760function complement(Amount amount) pure returns (Amount) {
761 return Amount.wrap(~Amount.unwrap(amount));
762}
763
764using {negate as -, complement as ~} for Amount global;
765
766contract Test {
767 function check(Amount amount) external pure returns (Amount) {
768 return ~amount;
769 }
770}
771"#;
772 let mutations = collect(source);
773
774 assert!(!mutations.contains(&unary_mutation(source, "~amount", UnOpKind::Neg)));
775 }
776
777 #[test]
778 fn preserves_negation_for_unresolved_operand() {
779 let source = r#"
780contract Test {
781 function check() external pure {
782 unresolved++;
783 }
784}
785"#;
786 let mutations = collect(source);
787
788 assert!(!mutations.contains(&unary_mutation(source, "unresolved++", UnOpKind::Neg)));
789 }
790
791 #[test]
792 fn excludes_logical_replacements_for_numeric_comparisons() {
793 let source = r#"
794contract Test {
795 function check(uint256 left, uint256 right) external pure returns (bool) {
796 return left == right;
797 }
798}
799"#;
800 let mutations = collect(source);
801
802 assert!(mutations.contains(&mutation(source, "left == right", BinOpKind::And)));
803 assert!(mutations.contains(&mutation(source, "left == right", BinOpKind::Or)));
804 assert!(!mutations.contains(&mutation(source, "left == right", BinOpKind::Lt)));
805 assert!(!mutations.contains(&mutation(source, "left == right", BinOpKind::Ne)));
806 }
807
808 #[test]
809 fn excludes_ordered_replacements_for_boolean_equality() {
810 let source = r#"
811contract Test {
812 function check(bool left, bool right) external pure returns (bool) {
813 return left == right;
814 }
815}
816"#;
817 let mutations = collect(source);
818
819 for candidate in [BinOpKind::Lt, BinOpKind::Le, BinOpKind::Gt, BinOpKind::Ge] {
820 assert!(mutations.contains(&mutation(source, "left == right", candidate)));
821 }
822 for candidate in [BinOpKind::Ne, BinOpKind::And, BinOpKind::Or] {
823 assert!(!mutations.contains(&mutation(source, "left == right", candidate)));
824 }
825 }
826
827 #[test]
828 fn excludes_ordered_replacements_for_logical_operations() {
829 let source = r#"
830contract Test {
831 function check(bool left, bool right) external pure returns (bool) {
832 return left && right;
833 }
834}
835"#;
836 let mutations = collect(source);
837
838 for candidate in [BinOpKind::Lt, BinOpKind::Le, BinOpKind::Gt, BinOpKind::Ge] {
839 assert!(mutations.contains(&mutation(source, "left && right", candidate)));
840 }
841 for candidate in [BinOpKind::Eq, BinOpKind::Ne, BinOpKind::Or] {
842 assert!(!mutations.contains(&mutation(source, "left && right", candidate)));
843 }
844 }
845
846 #[test]
847 fn excludes_non_equality_replacements_for_function_comparisons() {
848 let source = r#"
849contract Test {
850 function check(
851 function() external left,
852 function() external right
853 ) external pure returns (bool) {
854 return left == right;
855 }
856}
857"#;
858 let mutations = collect(source);
859
860 for candidate in [
861 BinOpKind::Lt,
862 BinOpKind::Le,
863 BinOpKind::Gt,
864 BinOpKind::Ge,
865 BinOpKind::And,
866 BinOpKind::Or,
867 ] {
868 assert!(mutations.contains(&mutation(source, "left == right", candidate)));
869 }
870 assert!(!mutations.contains(&mutation(source, "left == right", BinOpKind::Ne)));
871 }
872
873 #[test]
874 fn preserves_reversed_and_upper_boundary_mutations() {
875 let source = r#"
876contract Test {
877 function lower(uint256 x) external pure returns (bool) {
878 return 0 == x;
879 }
880
881 function upper(uint8 x) external pure returns (bool) {
882 return x == type(uint8).max;
883 }
884}
885"#;
886 let mutations = collect(source);
887
888 assert!(!mutations.contains(&mutation(source, "0 == x", BinOpKind::Ge)));
889 assert!(!mutations.contains(&mutation(source, "0 == x", BinOpKind::Gt)));
890 assert!(!mutations.contains(&mutation(source, "x == type(uint8).max", BinOpKind::Ge)));
891 }
892
893 #[test]
894 fn preserves_member_and_typed_constant_boundary_mutations() {
895 let source = r#"
896contract Test {
897 function empty(bytes memory value) external pure returns (bool) {
898 return value.length == 0;
899 }
900
901 function zero(address value) external pure returns (bool) {
902 return value == address(0);
903 }
904}
905"#;
906 let mutations = collect(source);
907
908 assert!(!mutations.contains(&mutation(source, "value.length == 0", BinOpKind::Le)));
909 assert!(!mutations.contains(&mutation(source, "value == address(0)", BinOpKind::Le)));
910 }
911}