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(_)) => Some(AssignmentDestinationKind::SignedNumber),
276 TyKind::Elementary(ElementaryType::UInt(_)) => {
277 Some(AssignmentDestinationKind::UnsignedNumber)
278 }
279 TyKind::Elementary(ElementaryType::FixedBytes(_)) => {
280 Some(AssignmentDestinationKind::FixedBytes)
281 }
282 TyKind::Udvt(..) | TyKind::Err(_) => None,
283 _ => Some(AssignmentDestinationKind::Other),
284 }
285}
286
287fn assignment_value_accepts_negation(gcx: Gcx<'_>, value: &hir::Expr<'_>) -> Option<bool> {
288 if matches!(&value.peel_parens().kind, ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Number(_)))
289 {
290 return Some(true);
291 }
292
293 let ty = gcx.type_of_expr(value.peel_parens().id)?;
294 match ty.peel_refs().kind {
295 TyKind::Elementary(ElementaryType::Int(_)) => Some(true),
296 TyKind::Elementary(ElementaryType::UInt(_) | ElementaryType::FixedBytes(_)) => Some(false),
297 TyKind::Udvt(..) | TyKind::Err(_) => None,
298 _ => Some(false),
299 }
300}
301
302fn is_type_invalid_replacement(
303 gcx: Gcx<'_>,
304 left: &hir::Expr<'_>,
305 original: BinOpKind,
306 right: &hir::Expr<'_>,
307 candidate: BinOpKind,
308) -> bool {
309 match candidate {
310 BinOpKind::And | BinOpKind::Or => matches!(
311 comparison_operand_kind(gcx, left, right),
312 Some(ComparisonOperandKind::Function | ComparisonOperandKind::Other)
313 ),
314 BinOpKind::Lt | BinOpKind::Le | BinOpKind::Gt | BinOpKind::Ge => {
315 matches!(original, BinOpKind::And | BinOpKind::Or)
316 || matches!(
317 comparison_operand_kind(gcx, left, right),
318 Some(ComparisonOperandKind::Bool | ComparisonOperandKind::Function)
319 )
320 }
321 _ => false,
322 }
323}
324
325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
326enum ComparisonOperandKind {
327 Bool,
328 Function,
329 Other,
330}
331
332fn comparison_operand_kind(
333 gcx: Gcx<'_>,
334 left: &hir::Expr<'_>,
335 right: &hir::Expr<'_>,
336) -> Option<ComparisonOperandKind> {
337 let left = gcx.type_of_expr(left.id)?;
338 let right = gcx.type_of_expr(right.id)?;
339 match (left.peel_refs().kind, right.peel_refs().kind) {
340 (TyKind::Elementary(ElementaryType::Bool), TyKind::Elementary(ElementaryType::Bool)) => {
341 Some(ComparisonOperandKind::Bool)
342 }
343 (TyKind::Fn(_), TyKind::Fn(_)) => Some(ComparisonOperandKind::Function),
344 (TyKind::Err(_), _) | (_, TyKind::Err(_)) => None,
345 _ => Some(ComparisonOperandKind::Other),
346 }
347}
348
349#[derive(Clone, Copy)]
350enum UnaryOperandKind {
351 SignedInteger,
352 UnsignedInteger,
353 FixedBytes,
354}
355
356fn unary_operand_kind(gcx: Gcx<'_>, expr: &hir::Expr<'_>) -> Option<UnaryOperandKind> {
357 let ty = gcx.type_of_expr(expr.peel_parens().id)?;
358 match ty.peel_refs().kind {
359 TyKind::Elementary(ElementaryType::Int(_)) => Some(UnaryOperandKind::SignedInteger),
360 TyKind::Elementary(ElementaryType::UInt(_)) => Some(UnaryOperandKind::UnsignedInteger),
361 TyKind::Elementary(ElementaryType::FixedBytes(_)) => Some(UnaryOperandKind::FixedBytes),
362 _ => None,
363 }
364}
365
366fn is_non_storage_push_call(gcx: Gcx<'_>, expr: &hir::Expr<'_>) -> bool {
367 let ExprKind::Call(callee, args) = &expr.peel_parens().kind else { return false };
368 let ExprKind::Member(receiver, member) = &callee.peel_parens().kind else { return false };
369 if member.as_str() != "push" || !args.is_empty() {
370 return false;
371 }
372
373 !gcx.type_of_expr(receiver.id).is_some_and(|ty| {
374 matches!(
375 ty.kind,
376 TyKind::Ref(inner, location)
377 if location.is_storage()
378 && matches!(
379 inner.kind,
380 TyKind::DynArray(_) | TyKind::Elementary(ElementaryType::Bytes)
381 )
382 )
383 })
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use solar::interface::BytePos;
390
391 fn collect(source: &str) -> MutationExclusionSet {
392 let path = PathBuf::from("Test.sol");
393 let mut compiler = Compiler::new(Session::builder().with_silent_emitter(None).build());
394 compiler.enter_mut(|compiler| {
395 let mut pcx = compiler.parse();
396 let file =
397 pcx.sess.source_map().new_source_file(path.clone(), source).expect("source file");
398 pcx.add_file(file);
399 pcx.parse();
400 assert!(matches!(compiler.lower_asts(), Ok(ControlFlow::Continue(()))));
401 analyze_and_collect(compiler).remove(&path).unwrap_or_default()
402 })
403 }
404
405 fn mutation(source: &str, expression: &str, new_op: BinOpKind) -> MutationExclusion {
406 let lo = source.find(expression).expect("expression") as u32;
407 MutationExclusion::binary(
408 solar::ast::Span::new(BytePos(lo), BytePos(lo + expression.len() as u32)),
409 new_op,
410 )
411 }
412
413 fn unary_mutation(source: &str, expression: &str, new_op: UnOpKind) -> MutationExclusion {
414 let lo = source.find(expression).expect("expression") as u32;
415 MutationExclusion::unary(
416 solar::ast::Span::new(BytePos(lo), BytePos(lo + expression.len() as u32)),
417 new_op,
418 )
419 }
420
421 fn assignment_mutation(
422 source: &str,
423 value: &str,
424 replacement: AssignmentReplacement,
425 ) -> MutationExclusion {
426 let lo = source.rfind(value).expect("value") as u32;
427 MutationExclusion::assignment(
428 solar::ast::Span::new(BytePos(lo), BytePos(lo + value.len() as u32)),
429 replacement,
430 )
431 }
432
433 #[test]
434 fn excludes_assignments_invalid_for_destination_type() {
435 let source = r#"
436enum Choice { A, B }
437
438contract Test {
439 function check(address account, bool flag, uint256 unsigned, Choice choice) external pure {
440 address accountCopy = account;
441 bool flagCopy = flag;
442 uint256 unsignedCopy = unsigned;
443 Choice choiceCopy = choice;
444 }
445}
446"#;
447 let mutations = collect(source);
448
449 for value in ["account", "flag", "choice"] {
450 assert!(mutations.contains(&assignment_mutation(
451 source,
452 value,
453 AssignmentReplacement::Zero,
454 )));
455 assert!(mutations.contains(&assignment_mutation(
456 source,
457 value,
458 AssignmentReplacement::Negate,
459 )));
460 }
461 assert!(!mutations.contains(&assignment_mutation(
462 source,
463 "unsigned",
464 AssignmentReplacement::Zero,
465 )));
466 assert!(mutations.contains(&assignment_mutation(
467 source,
468 "unsigned",
469 AssignmentReplacement::Negate,
470 )));
471 }
472
473 #[test]
474 fn uses_lhs_type_for_assignments() {
475 let source = r#"
476contract Test {
477 function check(address account, bool flag) external pure {
478 account = account;
479 flag = flag;
480 }
481}
482"#;
483 let mutations = collect(source);
484
485 for value in ["account", "flag"] {
486 assert!(mutations.contains(&assignment_mutation(
487 source,
488 value,
489 AssignmentReplacement::Zero,
490 )));
491 assert!(mutations.contains(&assignment_mutation(
492 source,
493 value,
494 AssignmentReplacement::Negate,
495 )));
496 }
497 }
498
499 #[test]
500 fn excludes_invalid_compound_assignment_negation() {
501 let source = r#"
502contract Test {
503 uint256 unsignedTotal;
504 int256 signedTotal;
505 bytes32 word;
506
507 function check(
508 uint256 unsignedAmount,
509 int256 signedAmount,
510 bytes32 mask,
511 uint8 shift
512 ) external {
513 unsignedTotal += unsignedAmount;
514 signedTotal += signedAmount;
515 word &= mask;
516 signedTotal <<= shift;
517 signedTotal += 11;
518 signedTotal &= 12;
519 signedTotal <<= 13;
520 signedTotal >>= 14;
521 }
522}
523"#;
524 let mutations = collect(source);
525
526 for value in ["unsignedAmount", "mask", "shift"] {
527 assert!(mutations.contains(&assignment_mutation(
528 source,
529 value,
530 AssignmentReplacement::Negate,
531 )));
532 }
533 assert!(!mutations.contains(&assignment_mutation(
534 source,
535 "signedAmount",
536 AssignmentReplacement::Negate,
537 )));
538 for value in ["11", "12"] {
539 assert!(!mutations.contains(&assignment_mutation(
540 source,
541 value,
542 AssignmentReplacement::Negate,
543 )));
544 }
545 for value in ["13", "14"] {
546 assert!(mutations.contains(&assignment_mutation(
547 source,
548 value,
549 AssignmentReplacement::Negate,
550 )));
551 }
552 for value in ["unsignedAmount", "signedAmount", "mask", "shift"] {
553 assert!(!mutations.contains(&assignment_mutation(
554 source,
555 value,
556 AssignmentReplacement::Zero,
557 )));
558 }
559 }
560
561 #[test]
562 fn preserves_valid_signed_and_fixed_bytes_assignments() {
563 let source = r#"
564contract Test {
565 function check(int256 signed, bytes32 word) external pure {
566 int256 signedCopy = signed;
567 bytes32 wordCopy = word;
568 }
569}
570"#;
571 let mutations = collect(source);
572
573 for replacement in [AssignmentReplacement::Zero, AssignmentReplacement::Negate] {
574 assert!(!mutations.contains(&assignment_mutation(source, "signed", replacement)));
575 }
576 assert!(!mutations.contains(&assignment_mutation(
577 source,
578 "word",
579 AssignmentReplacement::Zero,
580 )));
581 assert!(mutations.contains(&assignment_mutation(
582 source,
583 "word",
584 AssignmentReplacement::Negate,
585 )));
586 }
587
588 #[test]
589 fn preserves_udvt_assignments() {
590 let source = r#"
591type Amount is uint256;
592
593contract Test {
594 function check(Amount amount) external pure {
595 Amount copy = amount;
596 }
597}
598"#;
599 let mutations = collect(source);
600
601 for replacement in [AssignmentReplacement::Zero, AssignmentReplacement::Negate] {
602 assert!(!mutations.contains(&assignment_mutation(source, "amount", replacement)));
603 }
604 }
605
606 #[test]
607 fn preserves_all_mutations_when_analysis_has_errors() {
608 let source = r#"
609contract Test {
610 function check(address account) external pure {
611 address copy = account;
612 unresolved = unresolved;
613 }
614}
615"#;
616
617 assert!(collect(source).is_empty());
618 }
619
620 #[test]
621 fn preserves_gas_distinct_unsigned_boundary_mutations() {
622 let source = r#"
623contract Test {
624 function check(uint256 x) external pure returns (bool) {
625 return x == 0;
626 }
627}
628"#;
629 let mutations = collect(source);
630
631 assert!(!mutations.contains(&mutation(source, "x == 0", BinOpKind::Le)));
632 assert!(!mutations.contains(&mutation(source, "x == 0", BinOpKind::Lt)));
633 }
634
635 #[test]
636 fn excludes_negation_only_for_unsigned_unary_operands() {
637 let source = r#"
638contract Test {
639 function check(uint256 unsigned, int256 signed) external pure {
640 unsigned++;
641 ++unsigned;
642 signed++;
643 }
644}
645"#;
646 let mutations = collect(source);
647
648 assert!(mutations.contains(&unary_mutation(source, "unsigned++", UnOpKind::Neg)));
649 assert!(mutations.contains(&unary_mutation(source, "++unsigned", UnOpKind::Neg)));
650 assert!(!mutations.contains(&unary_mutation(source, "signed++", UnOpKind::Neg)));
651 }
652
653 #[test]
654 fn excludes_lvalue_mutations_for_user_defined_push() {
655 let source = r#"
656contract Test {
657 function push() external pure returns (int256) {
658 return 1;
659 }
660
661 function check() external view returns (int256) {
662 return -this.push();
663 }
664}
665"#;
666 let mutations = collect(source);
667
668 for op in [UnOpKind::PreInc, UnOpKind::PreDec, UnOpKind::PostInc, UnOpKind::PostDec] {
669 assert!(mutations.contains(&unary_mutation(source, "-this.push()", op)));
670 }
671 }
672
673 #[test]
674 fn preserves_lvalue_mutations_for_storage_push() {
675 let source = r#"
676contract Test {
677 int256[] values;
678
679 function check() external returns (int256) {
680 return -values.push();
681 }
682}
683"#;
684 let mutations = collect(source);
685
686 for op in [UnOpKind::PreInc, UnOpKind::PreDec, UnOpKind::PostInc, UnOpKind::PostDec] {
687 assert!(!mutations.contains(&unary_mutation(source, "-values.push()", op)));
688 }
689 }
690
691 #[test]
692 fn excludes_invalid_fixed_bytes_unary_mutations() {
693 let source = r#"
694contract Test {
695 function check(bytes32 word) external pure returns (bytes32) {
696 return ~word;
697 }
698
699 function checkNumber(uint256 number) external pure returns (uint256) {
700 return ~number;
701 }
702}
703"#;
704 let mutations = collect(source);
705
706 for op in [
707 UnOpKind::PreInc,
708 UnOpKind::PreDec,
709 UnOpKind::PostInc,
710 UnOpKind::PostDec,
711 UnOpKind::Neg,
712 ] {
713 assert!(mutations.contains(&unary_mutation(source, "~word", op)));
714 }
715 assert!(!mutations.contains(&unary_mutation(source, "~word", UnOpKind::BitNot)));
716
717 for op in [UnOpKind::PreInc, UnOpKind::PreDec, UnOpKind::PostInc, UnOpKind::PostDec] {
718 assert!(!mutations.contains(&unary_mutation(source, "~number", op)));
719 }
720 assert!(mutations.contains(&unary_mutation(source, "~number", UnOpKind::Neg)));
721 }
722
723 #[test]
724 fn excludes_invalid_fixed_bytes_storage_push_mutations() {
725 let source = r#"
726contract Test {
727 bytes32[] values;
728
729 function check() external returns (bytes32) {
730 return ~values.push();
731 }
732}
733"#;
734 let mutations = collect(source);
735
736 for op in [
737 UnOpKind::PreInc,
738 UnOpKind::PreDec,
739 UnOpKind::PostInc,
740 UnOpKind::PostDec,
741 UnOpKind::Neg,
742 ] {
743 assert!(mutations.contains(&unary_mutation(source, "~values.push()", op)));
744 }
745 }
746
747 #[test]
748 fn preserves_overloaded_negation_for_unsigned_udvt() {
749 let source = r#"
750type Amount is uint256;
751
752function negate(Amount amount) pure returns (Amount) {
753 return Amount.wrap(type(uint256).max - Amount.unwrap(amount));
754}
755
756function complement(Amount amount) pure returns (Amount) {
757 return Amount.wrap(~Amount.unwrap(amount));
758}
759
760using {negate as -, complement as ~} for Amount global;
761
762contract Test {
763 function check(Amount amount) external pure returns (Amount) {
764 return ~amount;
765 }
766}
767"#;
768 let mutations = collect(source);
769
770 assert!(!mutations.contains(&unary_mutation(source, "~amount", UnOpKind::Neg)));
771 }
772
773 #[test]
774 fn preserves_negation_for_unresolved_operand() {
775 let source = r#"
776contract Test {
777 function check() external pure {
778 unresolved++;
779 }
780}
781"#;
782 let mutations = collect(source);
783
784 assert!(!mutations.contains(&unary_mutation(source, "unresolved++", UnOpKind::Neg)));
785 }
786
787 #[test]
788 fn excludes_logical_replacements_for_numeric_comparisons() {
789 let source = r#"
790contract Test {
791 function check(uint256 left, uint256 right) external pure returns (bool) {
792 return left == right;
793 }
794}
795"#;
796 let mutations = collect(source);
797
798 assert!(mutations.contains(&mutation(source, "left == right", BinOpKind::And)));
799 assert!(mutations.contains(&mutation(source, "left == right", BinOpKind::Or)));
800 assert!(!mutations.contains(&mutation(source, "left == right", BinOpKind::Lt)));
801 assert!(!mutations.contains(&mutation(source, "left == right", BinOpKind::Ne)));
802 }
803
804 #[test]
805 fn excludes_ordered_replacements_for_boolean_equality() {
806 let source = r#"
807contract Test {
808 function check(bool left, bool right) external pure returns (bool) {
809 return left == right;
810 }
811}
812"#;
813 let mutations = collect(source);
814
815 for candidate in [BinOpKind::Lt, BinOpKind::Le, BinOpKind::Gt, BinOpKind::Ge] {
816 assert!(mutations.contains(&mutation(source, "left == right", candidate)));
817 }
818 for candidate in [BinOpKind::Ne, BinOpKind::And, BinOpKind::Or] {
819 assert!(!mutations.contains(&mutation(source, "left == right", candidate)));
820 }
821 }
822
823 #[test]
824 fn excludes_ordered_replacements_for_logical_operations() {
825 let source = r#"
826contract Test {
827 function check(bool left, bool right) external pure returns (bool) {
828 return left && right;
829 }
830}
831"#;
832 let mutations = collect(source);
833
834 for candidate in [BinOpKind::Lt, BinOpKind::Le, BinOpKind::Gt, BinOpKind::Ge] {
835 assert!(mutations.contains(&mutation(source, "left && right", candidate)));
836 }
837 for candidate in [BinOpKind::Eq, BinOpKind::Ne, BinOpKind::Or] {
838 assert!(!mutations.contains(&mutation(source, "left && right", candidate)));
839 }
840 }
841
842 #[test]
843 fn excludes_non_equality_replacements_for_function_comparisons() {
844 let source = r#"
845contract Test {
846 function check(
847 function() external left,
848 function() external right
849 ) external pure returns (bool) {
850 return left == right;
851 }
852}
853"#;
854 let mutations = collect(source);
855
856 for candidate in [
857 BinOpKind::Lt,
858 BinOpKind::Le,
859 BinOpKind::Gt,
860 BinOpKind::Ge,
861 BinOpKind::And,
862 BinOpKind::Or,
863 ] {
864 assert!(mutations.contains(&mutation(source, "left == right", candidate)));
865 }
866 assert!(!mutations.contains(&mutation(source, "left == right", BinOpKind::Ne)));
867 }
868
869 #[test]
870 fn preserves_reversed_and_upper_boundary_mutations() {
871 let source = r#"
872contract Test {
873 function lower(uint256 x) external pure returns (bool) {
874 return 0 == x;
875 }
876
877 function upper(uint8 x) external pure returns (bool) {
878 return x == type(uint8).max;
879 }
880}
881"#;
882 let mutations = collect(source);
883
884 assert!(!mutations.contains(&mutation(source, "0 == x", BinOpKind::Ge)));
885 assert!(!mutations.contains(&mutation(source, "0 == x", BinOpKind::Gt)));
886 assert!(!mutations.contains(&mutation(source, "x == type(uint8).max", BinOpKind::Ge)));
887 }
888
889 #[test]
890 fn preserves_member_and_typed_constant_boundary_mutations() {
891 let source = r#"
892contract Test {
893 function empty(bytes memory value) external pure returns (bool) {
894 return value.length == 0;
895 }
896
897 function zero(address value) external pure returns (bool) {
898 return value == address(0);
899 }
900}
901"#;
902 let mutations = collect(source);
903
904 assert!(!mutations.contains(&mutation(source, "value.length == 0", BinOpKind::Le)));
905 assert!(!mutations.contains(&mutation(source, "value == address(0)", BinOpKind::Le)));
906 }
907}