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    },
12};
13use std::{
14    collections::{HashMap, HashSet},
15    ops::ControlFlow,
16};
17
18declare_forge_lint!(
19    INTERNAL_FUNCTION_USED_ONCE,
20    Severity::Info,
21    "internal-function-used-once",
22    "this internal function is used only once"
23);
24
25impl<'ast> ProjectLintPass<'ast> for InternalFunctionUsedOnce {
26    fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) {
27        if !ctx.is_lint_enabled(INTERNAL_FUNCTION_USED_ONCE.id()) {
28            return;
29        }
30        let gcx = ctx.gcx();
31
32        // Only functions declared in user-provided files are reported, while references are
33        // counted across the whole unit, dependencies included.
34        let input_source_idx: HashMap<_, _> = gcx
35            .hir
36            .sources_enumerated()
37            .filter_map(|(sid, src)| {
38                let FileName::Real(path) = &src.file.name else { return None };
39                Some((sid, sources.iter().position(|s| &s.path == path)?))
40            })
41            .collect();
42        if input_source_idx.is_empty() {
43            return;
44        }
45
46        // A function bound as a user-defined operator (`using {f as +} for T`) is out of scope:
47        // its operator uses are not `Ident`/`Member` references, and the binding requires a
48        // named function anyway.
49        let source_usings = gcx.hir.source_ids().flat_map(|id| gcx.hir.source(id).usings);
50        let contract_usings = gcx.hir.contract_ids().flat_map(|id| gcx.hir.contract(id).usings);
51        let operator_bound: HashSet<_> = source_usings
52            .chain(contract_usings)
53            .flat_map(|directive| directive.entries)
54            .filter(|entry| entry.operator.is_some())
55            .filter_map(|entry| match entry.kind {
56                hir::UsingEntryKind::Functions(ids) => Some(ids),
57                _ => None,
58            })
59            .flatten()
60            .copied()
61            .collect();
62
63        let mut counter =
64            ReferenceCounter { gcx, current: None, callee: None, refs: HashMap::new() };
65        for source_id in gcx.hir.source_ids() {
66            let _ = counter.visit_nested_source(source_id);
67        }
68        let refs = counter.refs;
69
70        for function_id in gcx.hir.function_ids() {
71            let function = gcx.hir.function(function_id);
72            let Some(&src_idx) = input_source_idx.get(&function.source) else { continue };
73            // Only ordinary internal functions with a body qualify. `virtual` functions and
74            // overrides exist for dynamic dispatch, and a `_`-prefixed name follows the hook
75            // convention (OpenZeppelin style).
76            if function.visibility != hir::Visibility::Internal
77                || !function.is_ordinary()
78                || function.body.is_none()
79                || function.virtual_
80                || function.override_
81                || operator_bound.contains(&function_id)
82                || function.name.is_none_or(|name| name.as_str().starts_with('_'))
83            {
84                continue;
85            }
86            // Exactly one reference, and it must be a direct call: zero references is dead
87            // code, a value-position reference (function pointer, callback) has no call site
88            // to inline into, a recursive function cannot be inlined, and a reference that
89            // only enters through a reference cycle has no caller to inline into either.
90            let Some(info) = refs.get(&function_id) else { continue };
91            if info.count == 1
92                && !info.used_as_value
93                && !info.self_referencing
94                && !only_referenced_within_cycle(&refs, function_id)
95            {
96                ctx.emit(&sources[src_idx], &INTERNAL_FUNCTION_USED_ONCE, function.keyword_span());
97            }
98        }
99    }
100}
101
102/// The references resolving to one function. Self-references are recorded apart rather than
103/// counted; `first_from` is `None` for a reference outside any function body.
104#[derive(Default)]
105struct RefInfo {
106    count: usize,
107    used_as_value: bool,
108    self_referencing: bool,
109    first_from: Option<hir::FunctionId>,
110}
111
112/// Whether a function's single reference only enters it through a reference cycle: the chain
113/// of single-reference sources loops back on `start` itself. A loop closing on a later node is
114/// someone else's cycle, and `start` hangs off it as an inlineable tail.
115fn only_referenced_within_cycle(
116    refs: &HashMap<hir::FunctionId, RefInfo>,
117    start: hir::FunctionId,
118) -> bool {
119    let mut visited = vec![start];
120    let mut current = start;
121    loop {
122        let Some(info) = refs.get(&current) else { return false };
123        // A fork (several references) or a reference from outside a function ends the chain.
124        let Some(next) = info.first_from.filter(|_| info.count == 1) else { return false };
125        if visited.contains(&next) {
126            return next == start;
127        }
128        visited.push(next);
129        current = next;
130    }
131}
132
133/// Counts, for every function of the unit, the expressions the type checker resolved to it,
134/// direct calls and value-position references alike.
135struct ReferenceCounter<'gcx> {
136    gcx: Gcx<'gcx>,
137    /// The enclosing function, so each reference knows its source.
138    current: Option<hir::FunctionId>,
139    /// The callee of the call being walked: that reference is a direct call.
140    callee: Option<hir::ExprId>,
141    refs: HashMap<hir::FunctionId, RefInfo>,
142}
143
144impl<'gcx> hir::Visit<'gcx> for ReferenceCounter<'gcx> {
145    type BreakValue = Never;
146
147    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
148        &self.gcx.hir
149    }
150
151    fn visit_nested_function(&mut self, id: hir::FunctionId) -> ControlFlow<Self::BreakValue> {
152        let previous = self.current.replace(id);
153        let result = self.visit_function(self.gcx.hir.function(id));
154        self.current = previous;
155        result
156    }
157
158    fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
159        match &expr.kind {
160            hir::ExprKind::Call(callee, ..) => self.callee = Some(callee.peel_parens().id),
161            hir::ExprKind::Ident(..) | hir::ExprKind::Member(..) => {
162                if let Some(function_id) = self.gcx.resolved_function(expr) {
163                    let is_call = self.callee == Some(expr.id);
164                    let info = self.refs.entry(function_id).or_default();
165                    if self.current == Some(function_id) {
166                        info.self_referencing = true;
167                    } else {
168                        info.count += 1;
169                        info.used_as_value |= !is_call;
170                        if info.count == 1 {
171                            info.first_from = self.current;
172                        }
173                    }
174                }
175            }
176            _ => {}
177        }
178        self.walk_expr(expr)
179    }
180}