1use super::ExternalFunction;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint, analysis::is_builtin},
5};
6use solar::{
7 ast::{ContractKind, DataLocation, Visibility},
8 interface::{data_structures::Never, sym},
9 sema::{
10 Gcx,
11 hir::{
12 self, ContractId, Expr, ExprKind, FunctionId, Stmt, StmtKind, VariableId, Visit as _,
13 },
14 ty::TyKind,
15 },
16};
17use std::{
18 cell::RefCell,
19 collections::{HashMap, HashSet},
20 ops::ControlFlow,
21 rc::Rc,
22};
23
24declare_forge_lint!(
25 EXTERNAL_FUNCTION,
26 Severity::Gas,
27 "external-function",
28 "`public` function can be declared `external`"
29);
30
31#[derive(Default)]
32struct ProjectIndex {
33 referenced: HashSet<FunctionId>,
36 super_called: HashMap<FunctionId, HashSet<ContractId>>,
38}
39
40thread_local! {
41 static PROJECT_INDEX: RefCell<Option<(usize, Rc<ProjectIndex>)>> = const { RefCell::new(None) };
44}
45
46fn project_index(gcx: Gcx<'_>) -> Rc<ProjectIndex> {
47 let key = std::ptr::from_ref(&gcx.hir) as usize;
48 PROJECT_INDEX.with_borrow_mut(|slot| match slot {
49 Some((cached_key, index)) if *cached_key == key => index.clone(),
50 _ => slot.insert((key, Rc::new(build_project_index(gcx)))).1.clone(),
51 })
52}
53
54fn build_project_index(gcx: Gcx<'_>) -> ProjectIndex {
55 let hir = &gcx.hir;
56 let mut builder = IndexBuilder { gcx, index: ProjectIndex::default(), contract: None };
57 for func in hir.functions() {
58 builder.contract = func.contract;
59 let _ = builder.visit_function(func);
60 }
61 for var in hir.variables().filter(|var| var.is_state_variable()) {
63 builder.contract = var.contract;
64 let _ = builder.visit_var(var);
65 }
66 builder.index
67}
68
69struct IndexBuilder<'gcx> {
70 gcx: Gcx<'gcx>,
71 index: ProjectIndex,
72 contract: Option<ContractId>,
74}
75
76impl<'gcx> hir::Visit<'gcx> for IndexBuilder<'gcx> {
77 type BreakValue = Never;
78
79 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
80 &self.gcx.hir
81 }
82
83 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
84 match &expr.kind {
85 ExprKind::Ident(_) => self.index.referenced.extend(self.gcx.resolved_function(expr)),
86 ExprKind::Member(base, _) if is_builtin(self.gcx, base, sym::super_) => {
87 if let Some(cid) = self.contract
88 && let Some(fid) = self.gcx.resolved_function(expr)
89 {
90 self.index.super_called.entry(fid).or_default().insert(cid);
91 }
92 }
93 ExprKind::Member(base, _)
94 if matches!(self.gcx.type_of_expr(base.id).map(|ty| ty.kind),
95 Some(TyKind::Type(ty)) if matches!(ty.kind, TyKind::Contract(_))) =>
96 {
97 self.index.referenced.extend(self.gcx.resolved_function(expr));
98 }
99 _ => {}
100 }
101 self.walk_expr(expr)
102 }
103}
104
105impl<'gcx> LateLintPass<'gcx> for ExternalFunction {
106 fn check_nested_contract(
107 &mut self,
108 ctx: &LintContext,
109 gcx: Gcx<'gcx>,
110 contract_id: ContractId,
111 ) {
112 let contract = gcx.hir.contract(contract_id);
113 if !ctx.is_lint_enabled(EXTERNAL_FUNCTION.id)
116 || !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract)
117 || contract.linearization_failed()
118 {
119 return;
120 }
121 let index = project_index(gcx);
122
123 for fid in contract.functions() {
124 let func = gcx.hir.function(fid);
125 let Some(name) = func.name else { continue };
128 if func.visibility != Visibility::Public
129 || !func.is_ordinary()
130 || func.override_
131 || func.body.is_none()
132 {
133 continue;
134 }
135 if !func.parameters.iter().any(|&p| is_memory_reference(gcx, p)) {
137 continue;
138 }
139 let mut finder = ParamEscapeFinder { gcx, params: func.parameters };
140 if finder.visit_function(func).is_break() {
141 continue;
142 }
143 let super_called = index.super_called.iter().any(|(&target, callers)| {
144 callers.iter().any(|&caller| {
145 gcx.hir.contracts_enumerated().any(|(cid, contract)| {
146 contract.linearized_bases.contains(&caller)
147 && gcx.resolve_super_function(cid, caller, target) == fid
148 })
149 })
150 });
151 let override_referenced = gcx
153 .hir
154 .contracts_enumerated()
155 .filter(|(cid, c)| *cid == contract_id || c.linearized_bases.contains(&contract_id))
156 .any(|(cid, _)| index.referenced.contains(&gcx.resolve_virtual_function(cid, fid)));
157 if !super_called && !override_referenced {
158 ctx.emit(&EXTERNAL_FUNCTION, name.span);
159 }
160 }
161 }
162}
163
164struct ParamEscapeFinder<'a, 'gcx> {
167 gcx: Gcx<'gcx>,
168 params: &'a [VariableId],
169}
170
171impl ParamEscapeFinder<'_, '_> {
172 fn is_param(&self, expr: &Expr<'_>) -> bool {
173 root_var_is(self.gcx, expr, &|v| self.params.contains(&v))
174 }
175}
176
177impl<'gcx> hir::Visit<'gcx> for ParamEscapeFinder<'_, 'gcx> {
178 type BreakValue = ();
179
180 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
181 &self.gcx.hir
182 }
183
184 fn visit_modifier(&mut self, modifier: &'gcx hir::Modifier<'gcx>) -> ControlFlow<()> {
185 if modifier.args.exprs().any(|arg| self.is_param(arg)) {
186 return ControlFlow::Break(());
187 }
188 self.walk_modifier(modifier)
189 }
190
191 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<()> {
192 if let StmtKind::DeclSingle(vid) = &stmt.kind
193 && let var = self.gcx.hir.variable(*vid)
194 && is_memory_reference(self.gcx, *vid)
195 && var.initializer.is_some_and(|init| self.is_param(init))
196 {
197 return ControlFlow::Break(());
198 }
199 self.walk_stmt(stmt)
200 }
201
202 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
203 let escapes = match &expr.kind {
204 ExprKind::Assign(lhs, op, rhs) => {
205 self.is_param(lhs)
206 || (op.is_none()
207 && root_var_is(self.gcx, lhs, &|v| {
208 let var = self.gcx.hir.variable(v);
209 var.is_local_variable() && is_memory_reference(self.gcx, v)
210 })
211 && self.is_param(rhs))
212 }
213 ExprKind::Delete(inner) => self.is_param(inner),
214 ExprKind::Unary(op, inner) => op.kind.has_side_effects() && self.is_param(inner),
215 ExprKind::Call(callee, args, opts) => {
216 !self
217 .gcx
218 .type_of_expr(callee.id)
219 .is_some_and(|ty| matches!(ty.kind, TyKind::Type(_)))
220 && (args.exprs().any(|arg| self.is_param(arg))
221 || opts.is_some_and(|opts| {
222 opts.args.iter().any(|opt| self.is_param(&opt.value))
223 })
224 || matches!(&callee.peel_parens().kind, ExprKind::Member(receiver, _)
225 if self.is_param(receiver)))
226 }
227 _ => false,
228 };
229 if escapes {
230 return ControlFlow::Break(());
231 }
232 self.walk_expr(expr)
233 }
234}
235
236fn is_memory_reference(gcx: Gcx<'_>, var: VariableId) -> bool {
237 gcx.type_of_item(var.into()).loc() == Some(DataLocation::Memory)
238}
239
240fn root_var_is(gcx: Gcx<'_>, expr: &Expr<'_>, pred: &impl Fn(VariableId) -> bool) -> bool {
243 match &expr.peel_parens().kind {
244 ExprKind::Ident(_) => gcx.resolved_variable(expr).is_some_and(pred),
245 ExprKind::Member(base, _)
246 | ExprKind::Payable(base)
247 | ExprKind::Index(base, _)
248 | ExprKind::Slice(base, ..) => root_var_is(gcx, base, pred),
249 _ => false,
250 }
251}