forge_lint/sol/info/
internal_function_used_once.rs1use 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 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 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 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 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#[derive(Default)]
105struct RefInfo {
106 count: usize,
107 used_as_value: bool,
108 self_referencing: bool,
109 first_from: Option<hir::FunctionId>,
110}
111
112fn 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(¤t) else { return false };
123 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
133struct ReferenceCounter<'gcx> {
136 gcx: Gcx<'gcx>,
137 current: Option<hir::FunctionId>,
139 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}