Skip to main content

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(&current) {
165            return current == start;
166        }
167        visited.push(current);
168        let Some(info) = refs.get(&current) 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}