1use super::{
5 branch_always_exits, is_require_or_assert, is_sender_member, lhs_local_var, loop_stmts,
6 stmt_expr, underlying_var, visit_stmts,
7};
8use solar::sema::{
9 Gcx,
10 hir::{self, BinOpKind, Expr, ExprKind, FunctionId, Stmt, StmtKind, UnOpKind, VariableId},
11};
12use std::{collections::HashSet, iter, ops::ControlFlow};
13
14pub fn is_protected<'gcx>(gcx: Gcx<'gcx>, func_id: FunctionId) -> bool {
16 modifiers_and_self(gcx, func_id).any(|id| has_access_guard(gcx, id, &mut HashSet::new()))
17}
18
19pub fn modifiers_and_self<'gcx>(
21 gcx: Gcx<'gcx>,
22 func_id: FunctionId,
23) -> impl Iterator<Item = FunctionId> + 'gcx {
24 gcx.hir
25 .function(func_id)
26 .modifiers
27 .iter()
28 .filter_map(move |modifier| {
29 gcx.hir.function(func_id).contract.map_or_else(
30 || modifier.id.as_function(),
31 |contract| gcx.resolve_modifier_target(contract, modifier),
32 )
33 })
34 .chain(iter::once(func_id))
35}
36
37pub fn has_access_guard<'gcx>(
41 gcx: Gcx<'gcx>,
42 func_id: FunctionId,
43 seen: &mut HashSet<FunctionId>,
44) -> bool {
45 if !seen.insert(func_id) {
46 return false;
47 }
48 let func = gcx.hir.function(func_id);
49 match func.body {
50 Some(body) => for_each_guard(gcx, body, seen, &mut |_| ControlFlow::Break(())).is_break(),
51 None => looks_like_access_control(func),
52 }
53}
54
55pub fn guard_vars<'gcx>(gcx: Gcx<'gcx>, func_id: FunctionId) -> HashSet<VariableId> {
57 let mut out = HashSet::new();
58 for id in modifiers_and_self(gcx, func_id) {
59 let Some(body) = gcx.hir.function(id).body else { continue };
60 let mut seen = HashSet::from([id]);
61 let _ = for_each_guard(gcx, body, &mut HashSet::from([id]), &mut |guard| {
62 match guard {
63 Guard::Check(cond) => expr_state_vars(gcx, cond, &mut seen, &mut out),
64 Guard::Call(callee_id) => function_state_vars(gcx, callee_id, &mut seen, &mut out),
65 }
66 ControlFlow::Continue(())
67 });
68 }
69 out
70}
71
72pub fn looks_like_access_control(func: &hir::Function<'_>) -> bool {
75 let Some(name) = func.name else { return false };
76 if !func.returns.is_empty() {
77 return false;
78 }
79 let lower = name.as_str().to_ascii_lowercase();
80 matches!(lower.as_str(), "auth" | "requiresauth" | "restricted")
81 || ["only", "check", "_check"].iter().any(|prefix| {
82 ["admin", "guardian", "manager", "owner", "role"]
83 .iter()
84 .any(|role| lower.starts_with(&format!("{prefix}{role}")))
85 })
86}
87
88pub fn access_check_polarity<'gcx>(
93 gcx: Gcx<'gcx>,
94 expr: &Expr<'_>,
95 aliases: &HashSet<VariableId>,
96) -> Option<bool> {
97 let is_check = |sender: &Expr<'_>, authority: &Expr<'_>| {
98 expr_reads_sender(gcx, sender, &mut HashSet::new(), aliases)
99 && expr_reads_state(gcx, authority)
100 };
101 match &expr.peel_parens().kind {
102 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
103 access_check_polarity(gcx, inner, aliases).map(|polarity| !polarity)
104 }
105 ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
106 let dominant = op.kind == BinOpKind::And;
109 let lhs = access_check_polarity(gcx, lhs, aliases);
110 let rhs = access_check_polarity(gcx, rhs, aliases);
111 if lhs == Some(dominant) || rhs == Some(dominant) {
112 Some(dominant)
113 } else if lhs == Some(!dominant) && rhs == Some(!dominant) {
114 Some(!dominant)
115 } else {
116 None
117 }
118 }
119 ExprKind::Binary(lhs, op, rhs)
120 if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne)
121 && (is_check(lhs, rhs) || is_check(rhs, lhs)) =>
122 {
123 Some(op.kind == BinOpKind::Eq)
124 }
125 _ => is_check(expr, expr).then_some(true),
126 }
127}
128
129fn update_sender_aliases<'gcx>(
133 gcx: Gcx<'gcx>,
134 stmt: &Stmt<'gcx>,
135 aliases: &mut HashSet<VariableId>,
136) {
137 let (var_id, value) = match stmt.kind {
138 StmtKind::DeclSingle(var_id) => (Some(var_id), gcx.hir.variable(var_id).initializer),
139 StmtKind::Expr(expr) => match &expr.peel_parens().kind {
140 ExprKind::Assign(lhs, _, rhs) => (lhs_local_var(gcx, lhs), Some(*rhs)),
141 _ => (None, None),
142 },
143 _ => (None, None),
144 };
145 if let Some(var_id) = var_id
146 && let Some(value) = value
147 {
148 if expr_reads_sender(gcx, value, &mut HashSet::new(), aliases) {
149 aliases.insert(var_id);
150 } else {
151 aliases.remove(&var_id);
152 }
153 }
154}
155
156pub fn expr_reads_sender<'gcx>(
159 gcx: Gcx<'gcx>,
160 expr: &Expr<'_>,
161 seen: &mut HashSet<FunctionId>,
162 aliases: &HashSet<VariableId>,
163) -> bool {
164 expr.visit(&mut |e| {
165 let reads = is_sender_member(gcx, e)
166 || underlying_var(gcx, e).is_some_and(|v| aliases.contains(&v))
167 || matches!(&e.kind, ExprKind::Call(callee, ..)
168 if matches!(callee.peel_parens().kind, ExprKind::Ident(_))
169 && gcx.resolved_function(callee).is_some_and(|id| function_reads_sender(gcx, id, seen)));
170 if reads { ControlFlow::Break(()) } else { ControlFlow::Continue(()) }
171 })
172 .is_break()
173}
174
175pub fn function_reads_sender<'gcx>(
177 gcx: Gcx<'gcx>,
178 func_id: FunctionId,
179 seen: &mut HashSet<FunctionId>,
180) -> bool {
181 seen.insert(func_id)
182 && gcx.hir.function(func_id).body.is_some_and(|body| {
183 visit_stmts(&gcx.hir, body.stmts, |stmt| {
184 let reads = stmt_expr(&gcx.hir, stmt)
185 .is_some_and(|expr| expr_reads_sender(gcx, expr, seen, &HashSet::new()));
186 if reads { ControlFlow::Break(()) } else { ControlFlow::Continue(()) }
187 })
188 .is_break()
189 })
190}
191
192pub fn expr_state_vars<'gcx>(
194 gcx: Gcx<'gcx>,
195 expr: &Expr<'_>,
196 seen: &mut HashSet<FunctionId>,
197 out: &mut HashSet<VariableId>,
198) {
199 let _ = expr.visit(&mut |e| {
200 if let Some(var_id) = underlying_var(gcx, e)
201 && gcx.hir.variable(var_id).kind.is_state()
202 {
203 out.insert(var_id);
204 }
205 if let ExprKind::Call(callee, ..) = &e.kind
206 && matches!(callee.peel_parens().kind, ExprKind::Ident(_))
207 && let Some(callee_id) = gcx.resolved_function(callee)
208 {
209 function_state_vars(gcx, callee_id, seen, out);
210 }
211 ControlFlow::<()>::Continue(())
212 });
213}
214
215pub fn function_state_vars<'gcx>(
217 gcx: Gcx<'gcx>,
218 func_id: FunctionId,
219 seen: &mut HashSet<FunctionId>,
220 out: &mut HashSet<VariableId>,
221) {
222 if seen.insert(func_id)
223 && let Some(body) = gcx.hir.function(func_id).body
224 {
225 let _ = visit_stmts(&gcx.hir, body.stmts, |stmt| {
226 if let Some(expr) = stmt_expr(&gcx.hir, stmt) {
227 expr_state_vars(gcx, expr, seen, out);
228 }
229 ControlFlow::Continue(())
230 });
231 }
232}
233
234fn expr_reads_state<'gcx>(gcx: Gcx<'gcx>, expr: &Expr<'_>) -> bool {
235 let mut vars = HashSet::new();
236 expr_state_vars(gcx, expr, &mut HashSet::new(), &mut vars);
237 !vars.is_empty()
238}
239
240enum Guard<'a> {
242 Check(&'a Expr<'a>),
244 Call(FunctionId),
246}
247
248fn for_each_guard<'gcx>(
251 gcx: Gcx<'gcx>,
252 body: hir::Block<'gcx>,
253 seen: &mut HashSet<FunctionId>,
254 f: &mut impl FnMut(Guard<'_>) -> ControlFlow<()>,
255) -> ControlFlow<()> {
256 let mut stmts = Vec::new();
257 let _ = dominating_stmts(body.stmts, &mut stmts);
258 let mut aliases = HashSet::new();
262 for stmt in stmts {
263 if let StmtKind::If(cond, then_stmt, else_stmt) = stmt.kind {
264 let exits = match access_check_polarity(gcx, cond, &aliases) {
265 Some(false) => branch_always_exits(gcx, then_stmt),
266 Some(true) => else_stmt.is_some_and(|expr| branch_always_exits(gcx, expr)),
267 None => false,
268 };
269 if exits {
270 f(Guard::Check(cond))?;
271 }
272 continue;
273 }
274 update_sender_aliases(gcx, stmt, &mut aliases);
275 let Some(expr) = stmt_expr(&gcx.hir, stmt) else { continue };
276 expr.visit(&mut |e| {
277 match &e.kind {
278 ExprKind::Call(callee, args, _) if is_require_or_assert(gcx, callee) => {
279 if let Some(cond) = args.exprs().next()
280 && access_check_polarity(gcx, cond, &aliases) == Some(true)
281 {
282 f(Guard::Check(cond))?;
283 }
284 }
285 ExprKind::Call(callee, ..)
286 if matches!(callee.peel_parens().kind, ExprKind::Ident(_)) =>
287 {
288 if let Some(callee_id) = gcx.resolved_function(callee)
289 && (looks_like_access_control(gcx.hir.function(callee_id))
290 || has_access_guard(gcx, callee_id, seen))
291 {
292 f(Guard::Call(callee_id))?;
293 }
294 }
295 _ => {}
296 }
297 ControlFlow::Continue(())
298 })?;
299 }
300 ControlFlow::Continue(())
301}
302
303fn dominating_stmts<'gcx>(
306 stmts: impl IntoIterator<Item = &'gcx Stmt<'gcx>>,
307 out: &mut Vec<&'gcx Stmt<'gcx>>,
308) -> ControlFlow<()> {
309 for stmt in stmts {
310 match stmt.kind {
311 StmtKind::Placeholder => return ControlFlow::Break(()),
312 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
313 dominating_stmts(block.stmts, out)?;
314 }
315 StmtKind::Loop(block, source) => dominating_stmts(loop_stmts(block, source), out)?,
316 _ => out.push(stmt),
317 }
318 }
319 ControlFlow::Continue(())
320}