1use solar::{
4 ast::{self, LitKind, UnOpKind},
5 interface::{Symbol, kw, sym},
6 sema::{
7 Gcx,
8 builtins::Builtin,
9 hir::{
10 self, CallArgs, ElementaryType, Expr, ExprKind, FunctionId, ItemId, Res, TypeKind,
11 VariableId,
12 },
13 ty::{CallableParamSource, TyKind},
14 },
15};
16use std::ops::ControlFlow;
17
18pub fn is_builtin(gcx: Gcx<'_>, expr: &Expr<'_>, name: Symbol) -> bool {
20 gcx.resolved_builtin(expr).is_some_and(|builtin| builtin.name() == name)
21}
22
23pub fn is_msg_sender(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
25 gcx.resolved_builtin(expr) == Some(Builtin::MsgSender)
26}
27
28pub fn is_sender_member(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
30 matches!(gcx.resolved_builtin(expr), Some(Builtin::MsgSender | Builtin::TxOrigin))
31}
32
33pub fn is_require_or_assert(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
35 matches!(gcx.resolved_builtin(callee), Some(Builtin::Require | Builtin::Assert))
36}
37
38pub fn is_revert_call(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
40 matches!(&expr.peel_parens().kind, ExprKind::Call(callee, ..) if is_builtin(gcx, callee, kw::Revert))
41}
42
43pub fn is_zero_value(expr: &Expr<'_>) -> bool {
45 match &expr.peel_parens().kind {
46 ExprKind::Lit(lit) => match &lit.kind {
47 LitKind::Number(value) => value.is_zero(),
48 LitKind::Address(value) => value.is_zero(),
49 LitKind::Bool(value) => !value,
50 _ => false,
51 },
52 ExprKind::Call(callee, args, _) if cast_type(callee).is_some() => {
53 let mut exprs = args.exprs();
54 exprs.len() == 1 && exprs.next().is_some_and(is_zero_value)
55 }
56 ExprKind::Payable(inner) => is_zero_value(inner),
57 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Neg => is_zero_value(inner),
58 _ => false,
59 }
60}
61
62pub fn is_exit_call(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
64 let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
65 match gcx.resolved_builtin(callee) {
66 Some(Builtin::Revert | Builtin::RevertMsg | Builtin::Selfdestruct) => true,
67 Some(Builtin::Require | Builtin::Assert) => {
68 args.exprs().next().is_some_and(is_literal_false)
69 }
70 _ => false,
71 }
72}
73
74pub fn is_literal_false(expr: &Expr<'_>) -> bool {
76 matches!(&expr.peel_parens().kind, ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Bool(false)))
77}
78
79pub fn is_literal_zero(expr: &Expr<'_>) -> bool {
81 matches!(&expr.peel_parens().kind, ExprKind::Lit(lit)
82 if matches!(&lit.kind, LitKind::Number(n) if n.is_zero()))
83}
84
85pub fn is_address_cast(callee: &Expr<'_>) -> bool {
87 matches!(
88 &callee.peel_parens().kind,
89 ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ElementaryType::Address(_)), .. })
90 )
91}
92
93pub fn is_contract_cast(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
95 gcx.type_of_expr(callee.peel_parens().id).is_some_and(
96 |ty| matches!(ty.kind, TyKind::Type(inner) if matches!(inner.kind, TyKind::Contract(_))),
97 )
98}
99
100pub fn is_address_like_cast(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
102 is_address_cast(callee) || is_contract_cast(gcx, callee)
103}
104
105pub fn is_address_self(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
107 let expr = expr.peel_parens();
108 match &expr.kind {
109 ExprKind::Payable(inner) => is_address_self(gcx, inner),
110 ExprKind::Call(callee, args, _) if is_address_like_cast(gcx, callee) => {
111 args.exprs().next().is_some_and(|expr| is_address_self(gcx, expr))
112 }
113 _ => is_builtin(gcx, expr, sym::this),
114 }
115}
116
117pub fn underlying_var(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<VariableId> {
120 match &expr.peel_parens().kind {
121 ExprKind::Ident(_) => gcx.resolved_variable(expr),
122 ExprKind::Call(callee, args, _) if is_address_like_cast(gcx, callee) => {
123 args.exprs().next().and_then(|arg| underlying_var(gcx, arg))
124 }
125 ExprKind::Payable(inner) => underlying_var(gcx, inner),
126 _ => None,
127 }
128}
129
130pub fn lhs_local_var(gcx: Gcx<'_>, lhs: &Expr<'_>) -> Option<VariableId> {
132 let ExprKind::Ident(_) = lhs.peel_parens().kind else { return None };
133 gcx.resolved_variable(lhs).filter(|&v| !gcx.hir.variable(v).kind.is_state())
134}
135
136pub fn state_lhs_vars(gcx: Gcx<'_>, lhs: &Expr<'_>) -> Vec<VariableId> {
139 let mut vars = Vec::new();
140 for_each_lhs_var(gcx, lhs, &mut |v| {
141 if gcx.hir.variable(v).kind.is_state() && !vars.contains(&v) {
142 vars.push(v);
143 }
144 });
145 vars
146}
147
148pub fn for_each_lhs_var(gcx: Gcx<'_>, expr: &Expr<'_>, f: &mut impl FnMut(VariableId)) {
151 match &expr.peel_parens().kind {
152 ExprKind::Ident(_) => {
153 if let Some(var) = gcx.resolved_variable(expr) {
154 f(var);
155 }
156 }
157 ExprKind::Index(base, _)
158 | ExprKind::Slice(base, ..)
159 | ExprKind::Member(base, _)
160 | ExprKind::Payable(base)
161 | ExprKind::Unary(_, base)
162 | ExprKind::Delete(base) => for_each_lhs_var(gcx, base, f),
163 ExprKind::Tuple(exprs) => exprs.iter().flatten().for_each(|e| for_each_lhs_var(gcx, e, f)),
164 _ => {}
165 }
166}
167
168pub fn tuple_elems<'gcx>(expr: &'gcx Expr<'gcx>) -> Option<&'gcx [Option<&'gcx Expr<'gcx>>]> {
170 match &expr.peel_parens().kind {
171 ExprKind::Tuple(elems) => Some(elems),
172 _ => None,
173 }
174}
175
176pub fn arg_for_param<'gcx>(
178 gcx: Gcx<'gcx>,
179 function_id: FunctionId,
180 param: VariableId,
181 args: &CallArgs<'gcx>,
182) -> Option<&'gcx Expr<'gcx>> {
183 let function = gcx.hir.function(function_id);
184 let idx = function.parameters.iter().position(|p| *p == param)?;
185 let names = gcx.callable_param_names(CallableParamSource::Function {
186 id: function_id,
187 skips_receiver: false,
188 });
189 args.argument_for_parameter(idx, Some(&names))
190}
191
192pub fn dispatched_function(
196 gcx: Gcx<'_>,
197 contract_id: hir::ContractId,
198 callee: &Expr<'_>,
199) -> Option<FunctionId> {
200 let callee = callee.peel_parens();
201 let function_id = gcx.resolved_callee(callee.id)?.res.as_function()?;
202 match &callee.kind {
203 ExprKind::Member(base, _) => {
204 let solar::sema::ty::TyKind::Type(ty) = gcx.type_of_expr(base.id)?.kind else {
205 return None;
206 };
207 match ty.kind {
208 solar::sema::ty::TyKind::Contract(_) => Some(function_id),
209 solar::sema::ty::TyKind::Super(defining) => {
210 Some(gcx.resolve_super_function(contract_id, defining, function_id))
211 }
212 _ => None,
213 }
214 }
215 ExprKind::Ident(_) => Some(gcx.resolve_virtual_function(contract_id, function_id)),
216 _ => None,
217 }
218}
219
220pub fn referenced_item(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<ItemId> {
222 let ExprKind::Ident(_) = expr.peel_parens().kind else { return None };
223 match gcx.resolved_expr(expr)? {
224 Res::Item(id) => Some(id),
225 _ => None,
226 }
227}
228
229pub fn address_call_receiver<'a>(callee: &'a Expr<'a>) -> Option<&'a Expr<'a>> {
232 let inner = match &callee.kind {
233 ExprKind::Call(inner, ..) if matches!(inner.kind, ExprKind::Member(..)) => inner,
234 _ => callee,
235 };
236 let ExprKind::Member(receiver, name) = &inner.kind else { return None };
237 matches!(name.name, kw::Call | kw::Delegatecall | sym::transfer | sym::send).then_some(receiver)
238}
239
240pub fn is_call_with_gas_limit(expr: &Expr<'_>) -> bool {
242 matches!(&expr.peel_parens().kind, ExprKind::Call(_, _, Some(opts))
243 if opts.args.iter().any(|opt| opt.name.name == kw::Gas))
244}
245
246pub const fn is_low_level_call(expr: &ast::Expr<'_>) -> bool {
249 if let ast::ExprKind::Call(call_expr, _) = &expr.kind {
250 let callee = match &call_expr.kind {
251 ast::ExprKind::CallOptions(inner, _) => inner,
252 _ => call_expr,
253 };
254 if let ast::ExprKind::Member(_, member) = &callee.kind {
255 return matches!(member.name, kw::Call | kw::Delegatecall | kw::Staticcall);
256 }
257 }
258 false
259}
260
261pub const fn write_target<'gcx>(expr: &'gcx Expr<'gcx>) -> Option<&'gcx Expr<'gcx>> {
263 match &expr.kind {
264 ExprKind::Assign(target, ..) | ExprKind::Delete(target) => Some(target),
265 ExprKind::Unary(op, target) if op.kind.has_side_effects() => Some(target),
266 _ => None,
267 }
268}
269
270pub fn runtime_entry_points(gcx: Gcx<'_>, contract_id: hir::ContractId) -> Vec<FunctionId> {
273 let bases = gcx.hir.contract(contract_id).linearized_bases;
274 let mut entries: Vec<_> =
275 gcx.interface_functions(contract_id).all().iter().map(|f| f.id).collect();
276 entries.extend(bases.iter().find_map(|&cid| gcx.hir.contract(cid).fallback));
277 entries.extend(bases.iter().find_map(|&cid| gcx.hir.contract(cid).receive));
278 entries
279}
280
281pub fn cast_type(callee: &Expr<'_>) -> Option<ElementaryType> {
283 match &callee.peel_parens().kind {
284 ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) => Some(*ty),
285 _ => None,
286 }
287}
288
289pub const fn var_is_address_like(var: &hir::Variable<'_>) -> bool {
291 matches!(
292 var.ty.kind,
293 TypeKind::Elementary(ElementaryType::Address(_)) | TypeKind::Custom(ItemId::Contract(_))
294 )
295}
296
297pub fn ast_bool_literal(expr: &ast::Expr<'_>) -> Option<bool> {
299 match &expr.peel_parens().kind {
300 ast::ExprKind::Lit(ast::Lit { kind: LitKind::Bool(value), .. }, _) => Some(*value),
301 _ => None,
302 }
303}
304
305pub fn for_each_child<'gcx>(expr: &'gcx Expr<'gcx>, f: &mut impl FnMut(&'gcx Expr<'gcx>)) {
307 match &expr.kind {
308 ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
309 f(lhs);
310 f(rhs);
311 }
312 ExprKind::Unary(_, inner)
313 | ExprKind::Delete(inner)
314 | ExprKind::Member(inner, _)
315 | ExprKind::Payable(inner) => f(inner),
316 ExprKind::Call(callee, args, opts) => {
317 f(callee);
318 opts.iter().flat_map(|opts| opts.args).for_each(|opt| f(&opt.value));
319 args.exprs().for_each(f);
320 }
321 ExprKind::Index(base, index) => {
322 f(base);
323 index.iter().copied().for_each(f);
324 }
325 ExprKind::Slice(base, start, end) => {
326 f(base);
327 [*start, *end].into_iter().flatten().for_each(f);
328 }
329 ExprKind::Ternary(cond, true_expr, false_expr) => {
330 f(cond);
331 f(true_expr);
332 f(false_expr);
333 }
334 ExprKind::Array(exprs) => exprs.iter().for_each(f),
335 ExprKind::Tuple(exprs) => exprs.iter().flatten().copied().for_each(f),
336 ExprKind::Ident(_)
337 | ExprKind::Lit(_)
338 | ExprKind::New(_)
339 | ExprKind::TypeCall(_)
340 | ExprKind::Type(_)
341 | ExprKind::YulMember(..)
342 | ExprKind::Err(_) => {}
343 }
344}
345
346pub fn any_subexpr(expr: &Expr<'_>, mut pred: impl FnMut(&Expr<'_>) -> bool) -> bool {
348 expr.visit(&mut |e| if pred(e) { ControlFlow::Break(()) } else { ControlFlow::Continue(()) })
349 .is_break()
350}
351
352pub fn has_side_effect(expr: &Expr<'_>) -> bool {
354 any_subexpr(expr, |e| match &e.kind {
355 ExprKind::Assign(..) | ExprKind::Delete(_) => true,
356 ExprKind::Unary(op, _) => op.kind.has_side_effects(),
357 _ => false,
358 })
359}
360
361pub fn callee_no_arg_returns<'gcx>(
364 gcx: Gcx<'gcx>,
365 callee: &'gcx Expr<'gcx>,
366 mut pred: impl FnMut(&'gcx Expr<'gcx>) -> bool,
367) -> bool {
368 gcx.type_of_expr(callee.peel_parens().id).is_some_and(
369 |ty| matches!(ty.kind, TyKind::Fn(f) if f.is_internal() || f.is_delegate_call()),
370 ) && gcx
371 .resolved_function(callee)
372 .is_some_and(|fid| function_no_arg_returns(gcx, fid, &mut pred))
373}
374
375pub fn function_no_arg_returns<'gcx>(
378 gcx: Gcx<'gcx>,
379 fid: FunctionId,
380 pred: &mut impl FnMut(&'gcx Expr<'gcx>) -> bool,
381) -> bool {
382 let f = gcx.hir.function(fid);
383 let Some(body) = f.body else { return false };
384 let stmts = match body.stmts {
385 [rest @ .., last] if matches!(last.kind, hir::StmtKind::Return(None)) => rest,
386 stmts => stmts,
387 };
388 let [stmt] = stmts else { return false };
389 f.parameters.is_empty()
390 && match &stmt.kind {
391 hir::StmtKind::Return(Some(e)) => pred(e),
392 hir::StmtKind::Expr(e) => {
393 matches!(&e.peel_parens().kind, ExprKind::Assign(lhs, None, rhs)
394 if f.returns.len() == 1 && underlying_var(gcx, lhs) == Some(f.returns[0]) && pred(rhs))
395 }
396 _ => false,
397 }
398}
399
400pub const OPENZEPPELIN_ROOTS: &[&str] =
402 &["@openzeppelin", "openzeppelin-contracts", "openzeppelin-contracts-upgradeable"];
403
404pub fn source_in_package(hir: &hir::Hir<'_>, source_id: hir::SourceId, roots: &[&str]) -> bool {
407 let solar::interface::source_map::FileName::Real(path) = &hir.source(source_id).file.name
408 else {
409 return false;
410 };
411 path.components().any(|component| {
412 matches!(component, std::path::Component::Normal(name)
413 if roots.iter().any(|root| name.eq_ignore_ascii_case(root)))
414 })
415}