forge_lint/sol/info/internal_function_used_once.rs
1use super::InternalFunctionUsedOnce;
2use crate::{
3 linter::{Lint, ProjectLintEmitter, ProjectLintPass, ProjectSource},
4 sol::{Severity, SolLint},
5};
6use solar::{
7 interface::{data_structures::Never, source_map::FileName},
8 sema::{
9 Gcx,
10 hir::{self, Visit},
11 ty::TyKind,
12 },
13};
14use std::{
15 collections::{HashMap, HashSet},
16 ops::ControlFlow,
17};
18
19declare_forge_lint!(
20 INTERNAL_FUNCTION_USED_ONCE,
21 Severity::Info,
22 "internal-function-used-once",
23 "this internal function is used only once; consider inlining it into its caller"
24);
25
26impl<'ast> ProjectLintPass<'ast> for InternalFunctionUsedOnce {
27 fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) {
28 if !ctx.is_lint_enabled(INTERNAL_FUNCTION_USED_ONCE.id()) {
29 return;
30 }
31 let gcx = ctx.gcx();
32 let hir = &gcx.hir;
33
34 // Map every input source's HIR `SourceId` to the corresponding `ProjectSource` index:
35 // only functions declared in user-provided files are reported, while references are
36 // counted across the whole unit, dependencies included.
37 let input_source_idx: HashMap<hir::SourceId, usize> = hir
38 .sources_enumerated()
39 .filter_map(|(sid, src)| {
40 let path = match &src.file.name {
41 FileName::Real(p) => p,
42 _ => return None,
43 };
44 let idx = sources.iter().position(|s| &s.path == path)?;
45 Some((sid, idx))
46 })
47 .collect();
48
49 if input_source_idx.is_empty() {
50 return;
51 }
52
53 let counts = count_function_references(gcx);
54 let operator_bound = operator_bound_functions(hir);
55
56 for function_id in hir.function_ids() {
57 let function = hir.function(function_id);
58 let Some(&src_idx) = input_source_idx.get(&function.source) else { continue };
59 // A function bound as a user-defined operator is out of scope: its operator uses
60 // are not `Ident`/`Member` references, so its count would lie, and inlining it is
61 // not an option anyway, the `using {f as +}` binding requires a named function.
62 if operator_bound.contains(&function_id) {
63 continue;
64 }
65 // Only ordinary internal functions with a body qualify. A name starting with `_`
66 // follows the hook convention (OpenZeppelin style) and stays out, and so do
67 // `virtual` functions and overrides: they exist for dynamic dispatch, so inlining
68 // them is not an option and their reference count does not tell the story.
69 if function.visibility != hir::Visibility::Internal
70 || !function.is_ordinary()
71 || function.body.is_none()
72 || function.virtual_
73 || function.override_
74 {
75 continue;
76 }
77 let Some(name) = function.name else { continue };
78 if name.as_str().starts_with('_') {
79 continue;
80 }
81 // Exactly one reference, and it must be a direct call: zero references is dead
82 // code, a different concern, and a reference used as a value (assigned to a
83 // function pointer, returned, or passed as a callback) has no call site to inline
84 // into, so it is not an inlining candidate. Self-references do not count and mark
85 // the function recursive, which rules it out entirely: a recursive function cannot
86 // be inlined into its caller. A single reference that only enters through a
87 // reference cycle (mutually recursive helpers with no external caller) has no
88 // caller to inline into either.
89 let Some(info) = counts.get(&function_id) else { continue };
90 if info.count == 1
91 && info.value_count == 0
92 && !info.self_referencing
93 && !only_referenced_within_cycle(&counts, function_id)
94 {
95 ctx.emit(&sources[src_idx], &INTERNAL_FUNCTION_USED_ONCE, function.keyword_span());
96 }
97 }
98 }
99}
100
101/// Collects every function the unit binds as a user-defined operator, through a
102/// `using {f as +} for T` entry of a file-level or contract-level directive. The HIR
103/// already resolved those entries to function ids.
104fn operator_bound_functions(hir: &hir::Hir<'_>) -> HashSet<hir::FunctionId> {
105 let mut bound = HashSet::new();
106 // File-level directives, then contract-level ones: an operator entry can sit in either.
107 let source_usings = hir.source_ids().flat_map(|id| hir.source(id).usings.iter());
108 let contract_usings = hir.contract_ids().flat_map(|id| hir.contract(id).usings.iter());
109 for directive in source_usings.chain(contract_usings) {
110 for entry in directive.entries {
111 // Only braced function entries can carry an operator binding.
112 if entry.operator.is_some()
113 && let hir::UsingEntryKind::Functions(ids) = entry.kind
114 {
115 bound.extend(ids.iter().copied());
116 }
117 }
118 }
119 bound
120}
121
122/// The references resolving to one function: how many, whether the function references
123/// itself, and which function the first reference came from, `None` when it came from
124/// outside any function body (a variable initializer). Self-references are recorded apart
125/// rather than counted.
126#[derive(Default)]
127struct RefInfo {
128 count: usize,
129 value_count: usize,
130 self_referencing: bool,
131 first_from: Option<hir::FunctionId>,
132}
133
134/// Counts, for every function of the unit, the expressions that resolve to it, calls and
135/// references used as values alike. `type_of_expr` gives the single declaration the type
136/// checker selected, so overload selection, the qualified and `using for` forms and import
137/// aliases are all attributed to the right function.
138fn count_function_references(gcx: Gcx<'_>) -> HashMap<hir::FunctionId, RefInfo> {
139 let hir = &gcx.hir;
140 let mut counter = ReferenceCounter { gcx, hir, current: None, refs: HashMap::new() };
141 // Walk every source of the unit: functions, modifiers, and variable initializers.
142 for source_id in hir.source_ids() {
143 let _ = counter.visit_nested_source(source_id);
144 }
145 counter.refs
146}
147
148/// Whether a function's single reference only enters it through a reference cycle: the
149/// chain of single-reference sources loops back on itself, so there is no non-recursive
150/// caller to inline into (mutually recursive helpers with no external caller).
151fn only_referenced_within_cycle(
152 refs: &HashMap<hir::FunctionId, RefInfo>,
153 start: hir::FunctionId,
154) -> bool {
155 let mut visited = vec![start];
156 let Some(mut current) = refs.get(&start).and_then(|info| info.first_from) else {
157 return false;
158 };
159 // Each hop follows the unique referencing function. The chain is linear, so when it
160 // loops, the cycle contains `start` exactly when the loop closes on `start` itself: a
161 // loop closing on a later node is someone else's cycle, and `start` hangs off it as an
162 // inlineable tail.
163 loop {
164 if visited.contains(¤t) {
165 return current == start;
166 }
167 visited.push(current);
168 let Some(info) = refs.get(¤t) else { return false };
169 // A fork (several references) or a reference from outside a function ends the
170 // chain: the start is reachable from a non-cyclic context.
171 if info.count != 1 {
172 return false;
173 }
174 let Some(next) = info.first_from else { return false };
175 current = next;
176 }
177}
178
179struct ReferenceCounter<'gcx> {
180 gcx: Gcx<'gcx>,
181 hir: &'gcx hir::Hir<'gcx>,
182 current: Option<hir::FunctionId>,
183 refs: HashMap<hir::FunctionId, RefInfo>,
184}
185
186impl<'gcx> hir::Visit<'gcx> for ReferenceCounter<'gcx> {
187 type BreakValue = Never;
188
189 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
190 self.hir
191 }
192
193 fn visit_nested_function(&mut self, id: hir::FunctionId) -> ControlFlow<Self::BreakValue> {
194 // The enclosing function is tracked so each reference knows its source.
195 let previous = self.current.replace(id);
196 let result = self.visit_function(self.hir.function(id));
197 self.current = previous;
198 result
199 }
200
201 fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
202 // The callee of a call is a direct, inlinable use: record it as a call and descend into
203 // the callee's own sub-expressions (a member receiver, a nested call) without also
204 // counting the callee node as a value reference. Every other name or member expression
205 // typed as a function is a value-position reference, such as a function pointer that is
206 // assigned, returned or passed as a callback, which has no call site to inline into.
207 if let hir::ExprKind::Call(callee, args, opts) = &expr.kind {
208 let peeled = callee.peel_parens();
209 match peeled.kind {
210 hir::ExprKind::Ident(_) => self.record_reference(peeled, true),
211 hir::ExprKind::Member(receiver, _) => {
212 self.record_reference(peeled, true);
213 let _ = self.visit_expr(receiver);
214 }
215 _ => {
216 let _ = self.visit_expr(peeled);
217 }
218 }
219 if let Some(opts) = opts {
220 for arg in opts.args {
221 let _ = self.visit_expr(&arg.value);
222 }
223 }
224 return self.visit_call_args(args);
225 }
226 if matches!(expr.kind, hir::ExprKind::Ident(..) | hir::ExprKind::Member(..)) {
227 self.record_reference(expr, false);
228 }
229 self.walk_expr(expr)
230 }
231}
232
233impl<'gcx> ReferenceCounter<'gcx> {
234 /// Records one resolved function reference from `expr`, marking whether it is a direct call
235 /// or a value-position use. `type_of_expr` gives the single declaration the type checker
236 /// selected, so overloads, the qualified and `using for` forms and import aliases are all
237 /// attributed to the right function. A self-reference marks the function recursive rather
238 /// than counting.
239 fn record_reference(&mut self, expr: &hir::Expr<'_>, is_call: bool) {
240 let Some(ty) = self.gcx.type_of_expr(expr.peel_parens().id) else { return };
241 let TyKind::Fn(function_ty) = ty.kind else { return };
242 let Some(function_id) = function_ty.function_id else { return };
243 let info = self.refs.entry(function_id).or_default();
244 if self.current == Some(function_id) {
245 info.self_referencing = true;
246 } else {
247 info.count += 1;
248 if info.count == 1 {
249 info.first_from = self.current;
250 }
251 if !is_call {
252 info.value_count += 1;
253 }
254 }
255 }
256}