forge_lint/sol/med/
assert_state_change.rs1use super::AssertStateChange;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint, analysis::for_each_lhs_var},
5};
6use solar::{
7 ast::{DataLocation, StateMutability},
8 sema::{
9 Gcx,
10 builtins::Builtin,
11 hir::{Expr, ExprKind},
12 },
13};
14use std::ops::ControlFlow;
15
16declare_forge_lint!(
17 ASSERT_STATE_CHANGE,
18 Severity::Med,
19 "assert-state-change",
20 "`assert()` contains a state-modifying expression"
21);
22
23impl<'gcx> LateLintPass<'gcx> for AssertStateChange {
24 fn check_expr(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) {
25 let ExprKind::Call(callee, args, _) = &expr.kind else { return };
26 if gcx.resolved_builtin(callee) != Some(Builtin::Assert) {
27 return;
28 }
29 for arg in args.exprs() {
30 if let ControlFlow::Break(span) = arg.visit(&mut |e| {
32 if is_state_change(gcx, e) {
33 ControlFlow::Break(e.span)
34 } else {
35 ControlFlow::Continue(())
36 }
37 }) {
38 ctx.emit_with_msg(
39 &ASSERT_STATE_CHANGE,
40 span,
41 "`assert()` argument contains a state-modifying expression; \
42 `assert()` is for invariants, hoist the mutation before the `assert`, \
43 or use `require()` for validation",
44 );
45 }
46 }
47 }
48}
49
50fn is_state_change<'gcx>(gcx: Gcx<'gcx>, expr: &Expr<'gcx>) -> bool {
51 match &expr.kind {
52 ExprKind::Assign(lhs, ..) | ExprKind::Delete(lhs) => is_storage_lvalue(gcx, lhs),
53 ExprKind::Unary(op, lhs) => op.kind.has_side_effects() && is_storage_lvalue(gcx, lhs),
54 ExprKind::Call(callee, ..) => is_mutating_call(gcx, callee),
55 _ => false,
56 }
57}
58
59fn is_storage_lvalue(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
62 let mut found = false;
63 for_each_lhs_var(gcx, expr, &mut |v| {
64 let v = gcx.hir.variable(v);
65 found |= v.is_state_variable() || v.data_location == Some(DataLocation::Storage);
66 });
67 found
68}
69
70fn is_mutating_call<'gcx>(gcx: Gcx<'gcx>, callee: &Expr<'gcx>) -> bool {
71 if let ExprKind::Member(base, _) = &callee.kind {
72 if matches!(
75 gcx.resolved_builtin(callee),
76 Some(Builtin::ArrayPush0 | Builtin::ArrayPush | Builtin::ArrayPop)
77 ) && is_storage_lvalue(gcx, base)
78 {
79 return true;
80 }
81 if matches!(
84 gcx.resolved_builtin(callee),
85 Some(
86 Builtin::AddressCall
87 | Builtin::AddressDelegatecall
88 | Builtin::AddressPayableSend
89 | Builtin::AddressPayableTransfer
90 )
91 ) {
92 return true;
93 }
94 }
95 gcx.type_of_expr(callee.peel_parens().id)
96 .and_then(|ty| ty.state_mutability())
97 .is_some_and(|mutability| mutability > StateMutability::View)
98}