The receiver that did something
Removing an unused receiver can still remove an evaluation at the call site. A September prod-code implementation blocked selected receiver shapes; its textual rule shows why type checks alone cannot establish that a rewrite preserves behaviour.
On this page · 4 sections
A method whose body never reads self can look like a candidate for an associated function.
settings.default_port() reads as if its result depended on settings, while
Settings::default_port() does not take that argument. But changing the signature also changes
how the receiver is evaluated, borrowed or moved. An unused parameter is not a proof that
removing it preserves behaviour.
This post follows the Rust implementation in PR #112 and its immediate trait-implementation correction in #114, both merged on September 23, 2026. The code discussed is pinned at c69c647b. The development output and timings are historical reports, not executions repeated for this post.
The probe recorded in PR #108 found no receiver-removal
assist at the tested method positions. That is a result for those probes, not a claim about every
rust-analyzer version or possible trigger. So code_make_static implemented a separate operation.
Its declaration check looks for self as a whole identifier in the body text, then removes the
receiver parameter if it finds no such mention. This textual check is not a semantic dependency analysis.
The two proposed call-site transformations look simple:
s.uses_self() → S::uses_self()
S::twice(s, 2) → S::twice(2)
The part that needs care is the expression being removed.
A receiver is an expression
In the simple borrowed-receiver fixture, s is a local name. A receiver can also be a more
complicated expression, whose evaluation does work:
make().uses_self()
config.load()?.default_port()
Before the rewrite, make() runs and then the method is called. After a textual rewrite,
S::uses_self() runs and make() never does. If make opens a file, increments a counter or takes a
lock, or if load()? would have returned early, removing that evaluation changes the program.
The rewritten call may still type-check: a successful type check alone does not establish
that these effects were preserved.
For each reference it recognizes as a call, this revision tests the receiver’s text for (,
?, !, .await or [. It flags those strings and proposes removal otherwise. This blocks
some suspicious forms conservatively, including parentheses that need not denote an effectful call.
It does not analyze all possible effects.
The following historical report is abridged: the changed-line count, diff headers and final
apply instruction are omitted.
`S::uses_self` (src/lib.rs)
- the receiver `&self` is removed: it was never used
- 1 call site(s) now call `S::uses_self`
- pub fn uses_self(&self) -> u32 {
+ pub fn uses_self() -> u32 {
- s.uses_self() + S::no_self(2) + plain()
+ S::uses_self() + S::no_self(2) + plain()
1 call site(s) would drop a receiver that does something when it is evaluated; bind it to a variable first, or keep the method:
src/lib.rs:54:12 `make()` is evaluated for what it does
the analyzer rejects the result:
no method `uses_self` on type `S`, but an associated function with a similar name exists [E0599] (src/lib.rs:54:12)
nothing was written
PR #112 reports that the only analyzer error in this example concerned the untouched call.
The excerpt is a dry run, so no write would occur even with an empty error list. On an apply
request, the historical implementation refuses if its blocked list or remaining analyzer errors
are nonempty, unless force is set. Its overlay check supplies analyzer diagnostics;
verify: "compile" was a separate compiler-check option. Neither establishes behavioural equivalence.
An index such as items[0] is flagged: out-of-bounds indexing can panic, and removing that
panic would change behaviour. But absence of the listed characters is not enough to establish
that a receiver is harmless. Rust method calls can involve
automatic dereferencing or borrowing.
Removing a by-value receiver can also change ownership and
destructor timing,
even if the method body never reads self. The character test does not model those cases.
Binding make() to a local variable before the call is one possible manual edit, not a universal
fix: the binding can change how long the value lives. The reviewer has to preserve the intended
evaluation and lifetime, rather than only make the type error disappear.
What the check does not see
One thing the rewrite leaves behind is not an error: let s = S; above s.uses_self() may now be
unused. With ordinary lint settings that may produce a warning; a project can make such
warnings errors. The tool leaves the binding. Removing its initializer would raise the same
evaluation question again, one line up.
The report is also limited by reference discovery. In this revision a failed reference query becomes an empty list. References that cannot be read as calls, including method values, are listed as unmatched. That list alone does not block an apply request; an empty diagnostic result is not proof that every call site was found or repaired.
What it costs, and how it is pinned
PR #112 reports two warm dry runs on a scratch crate taking 0.24 and 0.20 seconds. These two
reported observations are not a general latency guarantee. Its orchestration test builds a
fixture with the listed shapes - a method call, a path call with the receiver as its first argument, a
receiver with effects (load().twice(3)), the method used as a value, and a second method that does use self - and
asserts the proposed text and classifications. The test uses a scripted gateway that supplies
references and no analyzer diagnostics; it is not an independent language-server execution.
The PR reports a mutation check: making the effects rule answer “no” for everything caused
the blocked-call assertion to fail because load() was dropped. We inspected the test source
but did not rerun that check.
What it does not do
- The other direction was added later that morning. The initial PR lacked
convert_to_method; PR #126 added it at 04:36 UTC on September 23, 2026. - Trait methods need a separate boundary. Issue #113
reports finding the missing trait-implementation check while writing the original post.
PR #114 then added a check for
forin the enclosingimplheader and a test for the demonstrated case. That is the specific textual check in this revision, not a full trait analysis. - Rust-only describes the historical implementation. PR #703 later added other languages. The October 4–5 UTC changes 30d4354d and 18091d24 revised call resolution and refusal on unmatched references. Those later implementations are outside this historical account’s correctness assessment.
Cite this article
Alexander Panasenko (2026-10-14). The receiver that did something. https://prod.codes/blog/the-receiver-that-did-something/