Skip to content

Commit a8c4658

Browse files
committed
fix(lint): only count a direct call as an inlinable use
The reference counter treated every name or member expression typed as a function as an inlinable use, so a helper referenced once as a value, whether assigned to a function pointer, returned, or passed as a callback, was reported even though there is no call site to inline into. Record whether a reference is the callee of a call and report only when the single reference is a direct call. Updates the pointer fixture to stay clean and adds the returned and callback value positions, alongside a function actually called once that still reports.
1 parent 03e724f commit a8c4658

3 files changed

Lines changed: 108 additions & 33 deletions

File tree

crates/lint/src/sol/info/internal_function_used_once.rs

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,17 @@ impl<'ast> ProjectLintPass<'ast> for InternalFunctionUsedOnce {
7878
if name.as_str().starts_with('_') {
7979
continue;
8080
}
81-
// Exactly one reference: zero references is dead code, a different concern.
82-
// Self-references do not count and mark the function recursive, which rules it
83-
// out entirely: a recursive function cannot be inlined into its caller. A single
84-
// reference that only enters through a reference cycle (mutually recursive
85-
// helpers with no external caller) has no caller to inline into either.
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.
8689
let Some(info) = counts.get(&function_id) else { continue };
8790
if info.count == 1
91+
&& info.value_count == 0
8892
&& !info.self_referencing
8993
&& !only_referenced_within_cycle(&counts, function_id)
9094
{
@@ -122,6 +126,7 @@ fn operator_bound_functions(hir: &hir::Hir<'_>) -> HashSet<hir::FunctionId> {
122126
#[derive(Default)]
123127
struct RefInfo {
124128
count: usize,
129+
value_count: usize,
125130
self_referencing: bool,
126131
first_from: Option<hir::FunctionId>,
127132
}
@@ -194,23 +199,58 @@ impl<'gcx> hir::Visit<'gcx> for ReferenceCounter<'gcx> {
194199
}
195200

196201
fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
197-
// A name or member expression typed as a function is one resolved reference. A
198-
// self-reference marks the function recursive instead of counting.
199-
if matches!(expr.kind, hir::ExprKind::Ident(..) | hir::ExprKind::Member(..))
200-
&& let Some(ty) = self.gcx.type_of_expr(expr.peel_parens().id)
201-
&& let TyKind::Fn(function_ty) = ty.kind
202-
&& let Some(function_id) = function_ty.function_id
203-
{
204-
let info = self.refs.entry(function_id).or_default();
205-
if self.current == Some(function_id) {
206-
info.self_referencing = true;
207-
} else {
208-
info.count += 1;
209-
if info.count == 1 {
210-
info.first_from = self.current;
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);
211222
}
212223
}
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);
213228
}
214229
self.walk_expr(expr)
215230
}
216231
}
232+
233+
impl<'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+
}

crates/lint/testdata/InternalFunctionUsedOnce.sol

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// SPDX-License-Identifier: MIT
33
pragma solidity ^0.8.18;
44

5-
// Tests for `internal-function-used-once`: an internal function referenced exactly once in the
6-
// whole unit can usually be inlined into its caller. References are resolved through the type
7-
// checker, calls and values alike. Out of scope: functions whose name starts with `_` (hook
8-
// convention), `virtual` functions and overrides (they exist for dynamic dispatch), functions
9-
// referenced zero times (dead code is another concern) or more than once, and functions bound
10-
// as user-defined operators (their operator uses are not name references, and the binding
11-
// requires a named function).
5+
// Tests for `internal-function-used-once`: an internal function called exactly once in the
6+
// whole unit can usually be inlined into its caller. The single reference must be a direct
7+
// call, resolved through the type checker; a reference used as a value (a function pointer
8+
// assigned, returned or passed as a callback) has no call site to inline into and is out of
9+
// scope. Also out of scope: functions whose name starts with `_` (hook convention), `virtual`
10+
// functions and overrides (they exist for dynamic dispatch), functions referenced zero times
11+
// (dead code is another concern) or more than once, and functions bound as user-defined
12+
// operators (their operator uses are not name references, and the binding requires a named
13+
// function).
1214

1315
library MathLib {
1416
function onceQualified(uint256 v) internal pure returns (uint256) { //~NOTE: this internal function is used only once
@@ -46,7 +48,9 @@ contract Counter {
4648
return v == 0 ? 0 : recurse(v - 1);
4749
}
4850

49-
function pointed(uint256 v) internal pure returns (uint256) { //~NOTE: this internal function is used only once
51+
// Referenced exactly once, but as a value assigned to a function pointer rather than a
52+
// direct call: there is no call site to inline into, so it stays out of scope.
53+
function pointed(uint256 v) internal pure returns (uint256) {
5054
return v + 6;
5155
}
5256

@@ -56,7 +60,6 @@ contract Counter {
5660
}
5761

5862
function runAgain(uint256 v) internal {
59-
// a reference used as a value is a reference like any other
6063
function(uint256) internal pure returns (uint256) f = pointed;
6164
count = twice(f(v));
6265
}
@@ -178,3 +181,35 @@ contract TailOffCycle {
178181
return cycleA(v);
179182
}
180183
}
184+
185+
// The remaining value positions: a helper returned as a function pointer and one passed as a
186+
// callback argument. Neither is a call site to inline into, so both stay out of scope, while
187+
// the function actually called once (`invoke`) is reported.
188+
contract ValuePositions {
189+
function returned(uint256 v) internal pure returns (uint256) {
190+
return v + 1;
191+
}
192+
193+
function passed(uint256 v) internal pure returns (uint256) {
194+
return v + 2;
195+
}
196+
197+
function invoke( //~NOTE: this internal function is used only once
198+
function(uint256) internal pure returns (uint256) cb,
199+
uint256 v
200+
) internal pure returns (uint256) {
201+
return cb(v);
202+
}
203+
204+
function pickReturned()
205+
internal
206+
pure
207+
returns (function(uint256) internal pure returns (uint256))
208+
{
209+
return returned;
210+
}
211+
212+
function usePassed(uint256 v) internal pure returns (uint256) {
213+
return invoke(passed, v);
214+
}
215+
}

crates/lint/testdata/InternalFunctionUsedOnce.stderr

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,23 @@ LL │ function helper(uint256 v) internal pure returns (uint256) {
2525
note[internal-function-used-once]: this internal function is used only once; consider inlining it into its caller
2626
╭▸ ROOT/testdata/InternalFunctionUsedOnce.sol:LL:CC
2727
28-
LL │ function pointed(uint256 v) internal pure returns (uint256) {
29-
━━━━━━━━
28+
LL │ function freeOnce(uint256 v) pure returns (uint256) {
29+
│ ━━━━━━━━
3030
3131
╰ help: https://getfoundry.sh/forge/linting/internal-function-used-once
3232

3333
note[internal-function-used-once]: this internal function is used only once; consider inlining it into its caller
3434
╭▸ ROOT/testdata/InternalFunctionUsedOnce.sol:LL:CC
3535
36-
LL │ function freeOnce(uint256 v) pure returns (uint256) {
37-
│ ━━━━━━━━
36+
LL │ function tail(uint256 v) internal pure returns (uint256) {
37+
━━━━━━━━
3838
3939
╰ help: https://getfoundry.sh/forge/linting/internal-function-used-once
4040

4141
note[internal-function-used-once]: this internal function is used only once; consider inlining it into its caller
4242
╭▸ ROOT/testdata/InternalFunctionUsedOnce.sol:LL:CC
4343
44-
LL │ function tail(uint256 v) internal pure returns (uint256) {
44+
LL │ function invoke(
4545
│ ━━━━━━━━
4646
4747
╰ help: https://getfoundry.sh/forge/linting/internal-function-used-once

0 commit comments

Comments
 (0)