forge_lint/sol/low/
incorrect_modifier.rs1use super::IncorrectModifier;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint},
5};
6use solar::{
7 ast,
8 sema::{
9 Gcx, Hir,
10 builtins::Builtin,
11 hir::{Block, Expr, ExprKind, Function, LoopSource, Res, Stmt, StmtKind},
12 },
13};
14
15declare_forge_lint!(
16 INCORRECT_MODIFIER,
17 Severity::Low,
18 "incorrect-modifier",
19 "modifier can finish without executing the modified function"
20);
21
22impl<'hir> LateLintPass<'hir> for IncorrectModifier {
23 fn check_function(
24 &mut self,
25 ctx: &LintContext,
26 _gcx: Gcx<'hir>,
27 _hir: &'hir Hir<'hir>,
28 func: &'hir Function<'hir>,
29 ) {
30 let (ast::FunctionKind::Modifier, Some(body)) = (func.kind, func.body) else {
31 return;
32 };
33
34 if block_outcome(body).can_skip_placeholder() {
35 ctx.emit(&INCORRECT_MODIFIER, func.span);
36 }
37 }
38}
39
40#[derive(Clone, Copy)]
46pub(crate) struct Outcome {
47 falls_through: bool,
49 returns: bool,
51 breaks: bool,
53 continues: bool,
55}
56
57impl Outcome {
58 const COVERED: Self =
60 Self { falls_through: false, returns: false, breaks: false, continues: false };
61 const FALLTHROUGH: Self = Self { falls_through: true, ..Self::COVERED };
62 const RETURNS: Self = Self { returns: true, ..Self::COVERED };
63 const BREAKS: Self = Self { breaks: true, ..Self::COVERED };
64 const CONTINUES: Self = Self { continues: true, ..Self::COVERED };
65
66 pub(crate) const fn can_skip_placeholder(self) -> bool {
69 self.falls_through || self.returns
70 }
71
72 const fn merge(self, other: Self) -> Self {
73 Self {
74 falls_through: self.falls_through || other.falls_through,
75 returns: self.returns || other.returns,
76 breaks: self.breaks || other.breaks,
77 continues: self.continues || other.continues,
78 }
79 }
80}
81
82pub(crate) fn block_outcome(block: Block<'_>) -> Outcome {
83 let mut outcome = Outcome::FALLTHROUGH;
84 for stmt in block.stmts {
85 if !outcome.falls_through {
87 return outcome;
88 }
89 let stmt_outcome = stmt_outcome(stmt);
90 outcome = Outcome {
91 falls_through: stmt_outcome.falls_through,
92 returns: outcome.returns || stmt_outcome.returns,
93 breaks: outcome.breaks || stmt_outcome.breaks,
94 continues: outcome.continues || stmt_outcome.continues,
95 };
96 }
97 outcome
98}
99
100fn stmt_outcome(stmt: &Stmt<'_>) -> Outcome {
101 match &stmt.kind {
102 StmtKind::Placeholder => Outcome::COVERED,
103 StmtKind::Return(_) => Outcome::RETURNS,
104 StmtKind::Break => Outcome::BREAKS,
105 StmtKind::Continue => Outcome::CONTINUES,
106 StmtKind::Expr(expr) => call_outcome(expr).unwrap_or(Outcome::FALLTHROUGH),
107 StmtKind::Revert(_) => Outcome::COVERED,
108 StmtKind::Block(block)
109 | StmtKind::UncheckedBlock(block)
110 | StmtKind::AssemblyBlock(block) => block_outcome(*block),
111 StmtKind::If(_, then_stmt, else_stmt) => {
112 let then_outcome = stmt_outcome(then_stmt);
113 let else_outcome = else_stmt.map_or(Outcome::FALLTHROUGH, stmt_outcome);
114 then_outcome.merge(else_outcome)
115 }
116 StmtKind::Loop(block, source) => {
117 let body = block_outcome(*block);
125 let falls_through =
126 body.breaks || (matches!(source, LoopSource::DoWhile) && body.continues);
127 Outcome { falls_through, returns: body.returns, ..Outcome::COVERED }
128 }
129 StmtKind::Try(try_stmt) => {
130 let mut outcome = Outcome::COVERED;
134 for clause in try_stmt.clauses {
135 outcome = outcome.merge(block_outcome(clause.block));
136 }
137 outcome
138 }
139 StmtKind::Switch(switch) => {
140 let has_default = switch.cases.last().is_some_and(|case| case.constant.is_none());
143 let mut outcome = if has_default { Outcome::COVERED } else { Outcome::FALLTHROUGH };
144 for case in switch.cases {
145 outcome = outcome.merge(block_outcome(case.body));
146 }
147 outcome
148 }
149 StmtKind::DeclSingle(_)
150 | StmtKind::DeclMulti(_, _)
151 | StmtKind::Emit(_)
152 | StmtKind::Err(_) => Outcome::FALLTHROUGH,
153 }
154}
155
156fn call_outcome(expr: &Expr<'_>) -> Option<Outcome> {
166 let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return None };
167 let ExprKind::Ident(resolutions) = &callee.peel_parens().kind else { return None };
168 resolutions.iter().find_map(|res| match res {
169 Res::Builtin(
170 Builtin::Revert | Builtin::RevertMsg | Builtin::YulRevert | Builtin::YulInvalid,
171 ) => Some(Outcome::COVERED),
172 Res::Builtin(Builtin::Require | Builtin::Assert)
173 if args.exprs().next().is_some_and(is_literal_false) =>
174 {
175 Some(Outcome::COVERED)
176 }
177 Res::Builtin(
178 Builtin::YulReturn
179 | Builtin::YulStop
180 | Builtin::YulSelfdestruct
181 | Builtin::Selfdestruct,
182 ) => Some(Outcome::RETURNS),
183 _ => None,
184 })
185}
186
187fn is_literal_false(expr: &Expr<'_>) -> bool {
188 matches!(
189 &expr.peel_parens().kind,
190 ExprKind::Lit(lit) if matches!(lit.kind, ast::LitKind::Bool(false))
191 )
192}