{"slug": "the-caller-nobody-edited", "title": "The caller nobody edited", "summary": "A `generify` refactoring tool merged into the prod-code project as PR #118 on September 23, 2026 rewrites a function declaration to a generic bound but leaves call sites unchanged, so the tool checks every calling file against the new signature and refuses to write anything unless `apply: true` is passed. The tool's `code_generify` command, documented at revision 951b70e8, converts `fn total(v: &Vec<u32>)` into `fn total<T: AsRef<[u32]>>(v: &T)` and rejects edits whose bodies use methods the bound does not promise, such as `v.iter()` failing with E0599 until rewritten as `v.as_ref().iter().sum()`. The account stresses that failed caller discovery must halt the plan, since a declaration edit can leave call text unchanged while breaking type inference.", "body_md": "# The caller nobody edited\n\nA 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.\n\n## On this page · 4 sections\n\nA function that takes `&Vec<u32>` and only reads it as a slice is asking for more than it needs.\n`fn total<T: AsRef<[u32]>>(v: &T)` takes a `Vec`, an array, a boxed slice - anything that can\nlend a `[u32]`. The refactoring entered this project’s catalog as `generify` in\n[PR #118](https://github.com/alex09x/prod-code/pull/118), merged on September 23, 2026.\nIn the [probe recorded in issue #117](https://github.com/alex09x/prod-code/issues/117),\nrust-analyzer offered documentation templates, signature type aliases and inlining at the\nfunction’s name. At the type it offered `unwrap_type_to_generic_arg`, which turns\n`Vec<u32>` into `u32` - a different change. That is a result at the tested positions,\nnot a claim about every rust-analyzer version or context.\n\nThis account follows the Rust implementation at revision\n[`951b70e8`](https://github.com/alex09x/prod-code/blob/951b70e806cb3f9b881cf5bd5aad66c61e57a224/crates/prod-code-mcp/src/generify.rs).\nThe run outcomes and timings below are reported in PR #118; this review checked source and\ntest definitions, without independently authenticating or repeating those historical runs.\nThe tool subsequently changed, including a correction to caller discovery described below.\n\nThe initial `code_generify` implementation takes the function, the parameter and the bound:\n\n``` bash\n$ prod-code generify total --param v --bound 'AsRef<[u32]>'\n```\n\nThe declaration is text: the parameter’s type becomes `T`, a reference in front of it stays\n(`&Vec<u32>` becomes `&T`, not `T`), and `<T: AsRef<[u32]>>` goes after the name - or at the end of\nthe generics the function already has. A name already in use is refused, and so is a parameter\nwhose type is abstract to begin with. These illustrative examples use the refusal messages\nfrom the pinned source; the `pick` and `show` sessions are not backed by a separate execution\nrecord in the material reviewed:\n\n``` bash\n$ prod-code generify pick --param b --bound 'Into<String>'\nError: `pick` already has a generic parameter `T`; pass another `type_param`\n$ prod-code generify pick --param b --bound 'Into<String>' --as U\n…\n- now: `fn pick<T: Clone, U: Into<String>>(a: T, b: U)`\n$ prod-code generify show --param x --bound Clone\nError: `x: impl std::fmt::Display` is already abstract\n```\n\nThe bound is not guessed: which trait a function should ask for is a design decision, and the text of its body does not say.\n\n## The body is checked first\n\nPR #118 reports a scratch-crate run where `total` was still written for a `Vec`.\nThe following is an abridged illustration of that report, with the renderer’s explanatory\nsentence expanded from the pinned source:\n\n```\n`total` (src/lib.rs)\n\n- was: `fn total(v: &Vec<u32>)`\n- now: `fn total<T: AsRef<[u32]>>(v: &T)`\n- 1 file(s) that call it checked against the new signature; their calls do not change, the type argument is inferred\n\nthe 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:\n  no method `iter` on type `&T` [E0599] (src/lib.rs:4:7)\n\nnothing was written; pass `apply: true` to make this edit\n```\n\n`v.iter()` worked because `v` was a `Vec`. `T` promises only `as_ref()`, so the body becomes\n`v.as_ref().iter().sum()`; PR #118 reports acceptance after that edit. That half is expected. The other half is\nthe reason this post exists.\n\n## The callers do not change, and are checked anyway\n\nThe declaration edit leaves call sites untouched: `total(&days)` stays `total(&days)`, and the compiler\nworks out `T` from what is passed. So a tool could rewrite the declaration, check the one file it\ntouched, and stop. It would be wrong about one kind of caller:\n\n`label(name: String)` becoming `label<D: Display>(name: D)` - its body only formats the name - is\nthe same change. The call `label(week.len().to_string())` is fine: `D` is `String`. The call\n`label(\"all\".into())` is not. It compiled because `.into()` took its target type from the\nparameter: `String`. Under `D` there is no target; `&str` converts into many things, and nothing\nsays which. PR #118 reports the following refusal for the unedited call (abridged):\n\n```\nError: the change does not compile (1 error(s)); nothing was written. …\n  type annotations needed [E0282] (src/report.rs:6:45)\n```\n\nThe report attributes that diagnostic to the analyzer. In the pinned code, files returned by\nthe references query go into the same overlay as the new declaration for diagnostics.\nThere was a gap: `references(...).await.unwrap_or_default()` treated a failed query as an empty\ncaller list and continued. A clean result could therefore omit the callers entirely.\n[Issue #446](https://github.com/alex09x/prod-code/issues/446) documented this failure mode;\n[PR #468](https://github.com/alex09x/prod-code/pull/468), merged September 27, changed this path\nto refuse when the required query fails. That repairs error handling; it does not prove that a\nlanguage server discovers every reference.\n\nPR #118 also reports forcing the original change through and running `cargo check` on a build\nnode. 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\ncall.\n\nPR #118 reports the opposite case too: `let days = vec![3, 5, 8];` still compiled when passed\nto `total(&days)` after the parameter became generic. The concrete `Vec<u32>` parameter was\ngone, but `T: AsRef<[u32]>` still named the required slice element type. According to the PR,\n`days` was inferred as `Vec<u32>` and both the analyzer and `cargo check` accepted it. This is\nan author-reported check result, not a new compiler run. The contrast explains why caller\nvalidation matters even when the edit only changes a declaration - and why that validation\ndepends on finding the relevant files.\n\n## What it costs, and how it is pinned\n\nPR #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.\n\nThe [orchestration test](https://github.com/alex09x/prod-code/blob/951b70e806cb3f9b881cf5bd5aad66c61e57a224/crates/prod-code-mcp/tests/orchestration.rs#L2111-L2237)\ncovers a reference parameter, existing generics, a name already in use, a missing parameter,\nand a caller whose file is clean on disk but rejected in the proposal. Its gateway supplies\nscripted references and diagnostics, including the caller error. The assertions check that\nthe error is reported and the declaration remains unchanged; this fixture does not itself\nprove rust-analyzer’s real inference behavior.\n\nThe PR’s Sabotage check section says that emptying the additional caller-file list made the\ntest fail: the write went through with `applied: true`. That is the author’s mutation-test\nreport. The feature and this test arrived in the same feature commit; their presence in source\nis not a separate receipt for the reported successful run or sabotage experiment.\n\n## Limits of that revision\n\n- **Choose the bound.** It is given. The methods used in a body may suggest a bound, but do\nnot decide the API contract the function should expose.\n- **Edit callers.** A caller that no longer compiles is reported, not fixed:`\"all\".into()` could\nbecome`\"all\".to_string()` or`String::from(\"all\")` , and neither is the tool’s to pick.\n- **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` .\n- **`impl Trait` and `dyn Trait`.** A parameter whose type is already abstract is refused, as\nabove.\n- **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) ,\nchanged caller-file validation and C/C++ refusal rules in that path. This post describes the\ninitial Rust design, not the current cross-language support contract.\n\n## Cite this article\n\n```\nAlexander Panasenko (2026-10-18). The caller nobody edited. https://prod.codes/blog/the-caller-nobody-edited/\n```\n\n", "url": "https://wpnews.pro/news/the-caller-nobody-edited", "canonical_source": "https://prod.codes/blog/the-caller-nobody-edited/", "published_at": "2026-10-18 00:00:00+00:00", "updated_at": "2026-11-06 08:51:38.568895+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["prod-code", "generify", "rust-analyzer", "PR #118", "issue #117", "alex09x"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-caller-nobody-edited", "markdown": "https://wpnews.pro/news/the-caller-nobody-edited.md", "text": "https://wpnews.pro/news/the-caller-nobody-edited.txt", "jsonld": "https://wpnews.pro/news/the-caller-nobody-edited.jsonld"}}