feat(lint): add internal-function-used-once detector#15586
Merged
mablr merged 7 commits intoJul 9, 2026
Conversation
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.
0xMars42
requested review from
0xrusowsky,
DaniPopes,
figtracer,
grandizzy,
mablr,
mattsse and
stevencartavia
as code owners
July 5, 2026 15:06
mablr
reviewed
Jul 7, 2026
…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.
mablr
reviewed
Jul 7, 2026
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.
mablr
reviewed
Jul 7, 2026
…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
previously requested changes
Jul 8, 2026
mattsse
left a comment
Member
There was a problem hiding this comment.
Found one blocker:
crates/lint/src/sol/info/internal_function_used_once.rs:196: the reference counter treats everyIdent/Membertyped 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.
Contributor
Author
|
Fixed in |
# Conflicts: # crates/lint/README.md # crates/lint/src/sol/info/mod.rs
mattsse
previously approved these changes
Jul 9, 2026
mattsse
left a comment
Member
There was a problem hiding this comment.
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.
mattsse
approved these changes
Jul 9, 2026
mablr
enabled auto-merge (squash)
July 9, 2026 19:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the
internal-function-used-oncelint 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 andusing forforms 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:
virtualfunctions 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.