Skip to content

feat(lint): add internal-function-used-once detector#15586

Merged
mablr merged 7 commits into
foundry-rs:masterfrom
0xMars42:feat/lint-internal-function-used-once
Jul 9, 2026
Merged

feat(lint): add internal-function-used-once detector#15586
mablr merged 7 commits into
foundry-rs:masterfrom
0xMars42:feat/lint-internal-function-used-once

Conversation

@0xMars42

@0xMars42 0xMars42 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Adds the internal-function-used-once lint from #14381 (Info).

Reports an ordinary internal function, free functions included, that exactly one expression in the unit references, call or value alike. References resolve through the type checker (gcx.type_of_expr, TyKind::Fn.function_id), so overload selection, the qualified and using for forms and import aliases attribute each reference to the right declaration. References are counted across the whole unit, dependencies included, while only functions declared in the project's own sources report, mirroring the unused-error pass.

Exemptions on top of Aderyn's underscore rule: virtual functions and overrides, which exist for dynamic dispatch and cannot be inlined, and zero-reference functions, dead code being a different concern. The fixture pins each exemption separately, the function-pointer-value counting, recursion (the self-reference makes two), and a free function used from another contract.

Three existing fixtures had a helper referenced exactly once; each gains a second reference to keep fixtures mono-lint.

Reports an ordinary internal function, free functions included, that
exactly one expression in the unit references, call or value alike.
References resolve through the type checker, so overload selection,
the qualified and using-for forms and import aliases attribute each
reference to the right declaration; they are counted across the whole
unit, dependencies included, while only functions declared in the
project's own sources report, mirroring the unused-error pass.

Exemptions: names starting with an underscore (the hook convention,
also Aderyn's rule), plus virtual functions and overrides, which exist
for dynamic dispatch and cannot be inlined; zero references stay out,
dead code being a different concern.

Three existing fixtures had a helper referenced exactly once; each
gains a second reference to keep fixtures mono-lint.
Comment thread crates/lint/src/sol/info/internal_function_used_once.rs Outdated
Comment thread crates/lint/src/sol/info/internal_function_used_once.rs Outdated
Comment thread crates/lint/src/sol/info/internal_function_used_once.rs Outdated
…ed-once

A function bound as a user-defined operator through `using {f as +} for T`
is used by operator expressions that are not `Ident`/`Member` references, so
its reference count lies: an operator-only function looked unused, and one
operator use next to a single direct call was reported as used once.

Functions named by an operator entry of any file-level or contract-level
`using` directive are now out of scope entirely. Counting operator uses
instead would mean re-resolving user operators by hand, and the suggestion
would be wrong anyway: the binding requires a named function, so an operator
implementation cannot be inlined into its caller.

Covers the operator-only and the operator-plus-one-call cases in the
testdata.
Comment thread crates/lint/src/sol/info/internal_function_used_once.rs Outdated
A self-recursive helper with no external caller had one counted reference,
its own call, so the lint suggested inlining a function that cannot be
inlined anywhere; mutually recursive helpers whose only references form
the cycle were reported the same way.

The reference counter now tracks the referencing function: a
self-reference marks the function recursive instead of counting, which
rules it out entirely, and a single reference is followed through the
chain of single-reference sources, so a chain that loops back on itself
(a cycle with no external caller) does not report. Covers the
self-recursive, recursive-with-caller and mutual-recursion cases in the
testdata.
Comment thread crates/lint/src/sol/info/internal_function_used_once.rs
…function-used-once

The cycle walk returned true on any loop in the single-reference chain, so
a helper hanging off someone else's cycle was suppressed even though it is
not part of it and has a caller to inline into. The chain is linear, so the
cycle contains the candidate exactly when the loop closes on the candidate
itself: the walk now only suppresses in that case. The tail-off-cycle repro
is in the testdata and reports.
mattsse
mattsse previously requested changes Jul 8, 2026

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one blocker:

  • crates/lint/src/sol/info/internal_function_used_once.rs:196: the reference counter treats every Ident/Member typed as a function as an inlineable use, including value-position references. A helper assigned to a function pointer, returned, or passed as a callback can be reported even though there is no direct caller to inline into. Please track whether the single reference is a direct call callee, or conservatively exempt value-position function references unless escape/use can be ruled out.

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.
@0xMars42

0xMars42 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in a8c4658b3. A reference counts as an inlinable use only when it is the callee of a direct call. A single value-position reference (a function pointer assigned, returned, or passed as a callback) has no call site to inline into and no longer reports. Updated the pointer fixture to stay clean and added the returned and callback positions, alongside a function called once directly that still reports.

# Conflicts:
#	crates/lint/README.md
#	crates/lint/src/sol/info/mod.rs
mablr
mablr previously approved these changes Jul 9, 2026

@mablr mablr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mablr
mablr dismissed mattsse’s stale review July 9, 2026 08:47

addressed

@mablr
mablr requested review from mattsse and stevencartavia July 9, 2026 08:49
mattsse
mattsse previously approved these changes Jul 9, 2026

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prior value-position reference blocker is fixed: function references used as values are no longer treated as direct inlineable calls. No new actionable findings in this pass.

@mablr
mablr dismissed stale reviews from mattsse and themself via 4b6d67e July 9, 2026 19:35

@mablr mablr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed merge conflict, G2G

@mablr
mablr requested a review from mattsse July 9, 2026 19:36
@mablr
mablr enabled auto-merge (squash) July 9, 2026 19:44
@mablr
mablr merged commit ae0e93e into foundry-rs:master Jul 9, 2026
20 checks passed
@github-project-automation github-project-automation Bot moved this to Done in Foundry Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants