# The caller nobody edited

> Source: <https://prod.codes/blog/the-caller-nobody-edited/>
> Published: 2026-10-18 00:00:00+00:00

# The caller nobody edited

A declaration edit can leave call text unchanged while breaking type inference. The original generify report illustrates why callers still need checking, and why failed caller discovery must stop the plan.

## On this page · 4 sections

A function that takes `&Vec<u32>` and only reads it as a slice is asking for more than it needs.
`fn total<T: AsRef<[u32]>>(v: &T)` takes a `Vec`, an array, a boxed slice - anything that can
lend a `[u32]`. The refactoring entered this project’s catalog as `generify` in
[PR #118](https://github.com/alex09x/prod-code/pull/118), merged on September 23, 2026.
In the [probe recorded in issue #117](https://github.com/alex09x/prod-code/issues/117),
rust-analyzer offered documentation templates, signature type aliases and inlining at the
function’s name. At the type it offered `unwrap_type_to_generic_arg`, which turns
`Vec<u32>` into `u32` - a different change. That is a result at the tested positions,
not a claim about every rust-analyzer version or context.

This account follows the Rust implementation at revision
[`951b70e8`](https://github.com/alex09x/prod-code/blob/951b70e806cb3f9b881cf5bd5aad66c61e57a224/crates/prod-code-mcp/src/generify.rs).
The run outcomes and timings below are reported in PR #118; this review checked source and
test definitions, without independently authenticating or repeating those historical runs.
The tool subsequently changed, including a correction to caller discovery described below.

The initial `code_generify` implementation takes the function, the parameter and the bound:

``` bash
$ prod-code generify total --param v --bound 'AsRef<[u32]>'
```

The declaration is text: the parameter’s type becomes `T`, a reference in front of it stays
(`&Vec<u32>` becomes `&T`, not `T`), and `<T: AsRef<[u32]>>` goes after the name - or at the end of
the generics the function already has. A name already in use is refused, and so is a parameter
whose type is abstract to begin with. These illustrative examples use the refusal messages
from the pinned source; the `pick` and `show` sessions are not backed by a separate execution
record in the material reviewed:

``` bash
$ prod-code generify pick --param b --bound 'Into<String>'
Error: `pick` already has a generic parameter `T`; pass another `type_param`
$ prod-code generify pick --param b --bound 'Into<String>' --as U
…
- now: `fn pick<T: Clone, U: Into<String>>(a: T, b: U)`
$ prod-code generify show --param x --bound Clone
Error: `x: impl std::fmt::Display` is already abstract
```

The bound is not guessed: which trait a function should ask for is a design decision, and the text of its body does not say.

## The body is checked first

PR #118 reports a scratch-crate run where `total` was still written for a `Vec`.
The following is an abridged illustration of that report, with the renderer’s explanatory
sentence expanded from the pinned source:

```
`total` (src/lib.rs)

- was: `fn total(v: &Vec<u32>)`
- now: `fn total<T: AsRef<[u32]>>(v: &T)`
- 1 file(s) that call it checked against the new signature; their calls do not change, the type argument is inferred

the analyzer rejects the result — the body uses something the bound does not promise, or a caller passes a type that does not satisfy it or can no longer be inferred:
  no method `iter` on type `&T` [E0599] (src/lib.rs:4:7)

nothing was written; pass `apply: true` to make this edit
```

`v.iter()` worked because `v` was a `Vec`. `T` promises only `as_ref()`, so the body becomes
`v.as_ref().iter().sum()`; PR #118 reports acceptance after that edit. That half is expected. The other half is
the reason this post exists.

## The callers do not change, and are checked anyway

The declaration edit leaves call sites untouched: `total(&days)` stays `total(&days)`, and the compiler
works out `T` from what is passed. So a tool could rewrite the declaration, check the one file it
touched, and stop. It would be wrong about one kind of caller:

`label(name: String)` becoming `label<D: Display>(name: D)` - its body only formats the name - is
the same change. The call `label(week.len().to_string())` is fine: `D` is `String`. The call
`label("all".into())` is not. It compiled because `.into()` took its target type from the
parameter: `String`. Under `D` there is no target; `&str` converts into many things, and nothing
says which. PR #118 reports the following refusal for the unedited call (abridged):

```
Error: the change does not compile (1 error(s)); nothing was written. …
  type annotations needed [E0282] (src/report.rs:6:45)
```

The report attributes that diagnostic to the analyzer. In the pinned code, files returned by
the references query go into the same overlay as the new declaration for diagnostics.
There was a gap: `references(...).await.unwrap_or_default()` treated a failed query as an empty
caller list and continued. A clean result could therefore omit the callers entirely.
[Issue #446](https://github.com/alex09x/prod-code/issues/446) documented this failure mode;
[PR #468](https://github.com/alex09x/prod-code/pull/468), merged September 27, changed this path
to refuse when the required query fails. That repairs error handling; it does not prove that a
language server discovers every reference.

PR #118 also reports forcing the original change through and running `cargo check` on a build
node. According to that account, the compiler rejected the call with `` error[E0283]: type annotations needed: cannot infer type of the type parameter `D` declared on the function `label` ``, at the start of the same
call.

PR #118 reports the opposite case too: `let days = vec![3, 5, 8];` still compiled when passed
to `total(&days)` after the parameter became generic. The concrete `Vec<u32>` parameter was
gone, but `T: AsRef<[u32]>` still named the required slice element type. According to the PR,
`days` was inferred as `Vec<u32>` and both the analyzer and `cargo check` accepted it. This is
an author-reported check result, not a new compiler run. The contrast explains why caller
validation matters even when the edit only changes a declaration - and why that validation
depends on finding the relevant files.

## What it costs, and how it is pinned

PR #118’s Measurements section reports 9.99 seconds for the first dry run on the scratch crate and 0.20 to 0.29 seconds for the next four. It describes the first run as the node’s first sight of the crate. Those are reported timings, not an independently verified benchmark or a measurement that isolates mirroring, loading and analysis costs.

The [orchestration test](https://github.com/alex09x/prod-code/blob/951b70e806cb3f9b881cf5bd5aad66c61e57a224/crates/prod-code-mcp/tests/orchestration.rs#L2111-L2237)
covers a reference parameter, existing generics, a name already in use, a missing parameter,
and a caller whose file is clean on disk but rejected in the proposal. Its gateway supplies
scripted references and diagnostics, including the caller error. The assertions check that
the error is reported and the declaration remains unchanged; this fixture does not itself
prove rust-analyzer’s real inference behavior.

The PR’s Sabotage check section says that emptying the additional caller-file list made the
test fail: the write went through with `applied: true`. That is the author’s mutation-test
report. The feature and this test arrived in the same feature commit; their presence in source
is not a separate receipt for the reported successful run or sabotage experiment.

## Limits of that revision

- **Choose the bound.** It is given. The methods used in a body may suggest a bound, but do
not decide the API contract the function should expose.
- **Edit callers.** A caller that no longer compiles is reported, not fixed:`"all".into()` could
become`"all".to_string()` or`String::from("all")` , and neither is the tool’s to pick.
- **One parameter at a time.** Two parameters of the same type become two runs, the second with`--as U` , and two type parameters, not one shared`T` .
- **`impl Trait` and `dyn Trait`.** A parameter whose type is already abstract is refused, as
above.
- **Rust only at this revision.**[PR #715](https://github.com/alex09x/prod-code/pull/715) added implementations for other languages on September 29. Later changes, including[`20700937`](https://github.com/alex09x/prod-code/commit/20700937f57b6837b60341b1a6dcc40629e46316) ,
changed caller-file validation and C/C++ refusal rules in that path. This post describes the
initial Rust design, not the current cross-language support contract.

## Cite this article

```
Alexander Panasenko (2026-10-18). The caller nobody edited. https://prod.codes/blog/the-caller-nobody-edited/
```


