The refactoring that changes nothing
Promoting a magic value to a parameter is only safe if every existing caller keeps passing the value it had. That constraint is the whole design - and it is also why the tool refuses some extractions outright, and why the analyzer's answer to 'what function is this in' was the wrong question.

On this page · 6 sections
Select an expression in a Rust file and ask the analyzer what it can do with it:
$ prod-code assists crates/prod-code-mcp/src/move_item.rs 629 19 --to 629:39
extract_variable [RefactorExtract] Extract into variable
extract_constant [RefactorExtract] Extract into constant
extract_static [RefactorExtract] Extract into static
extract_function [RefactorExtract] Extract into function
Four ways to take an expression out of where it is. All four stay inside the file, and that is why they are there: the fifth one, making it a parameter, has to touch every place that calls the function.
The success condition is that nothing happens
Adding a parameter is easy. Adding one without changing what any existing caller does is the entire job, and there is exactly one way to do it: every call site passes the expression the body used to have. Afterwards the program computes what it computed before, byte for byte, in every caller - provided the expression does not depend on when it is evaluated, because it is now evaluated at the call rather than inside the body.
The value of the refactoring is not in what it changes. It is in what it makes possible for the next caller, which now has a choice where before there was a constant buried in a body.
$ prod-code extract-parameter crates/prod-code-mcp/src/signature.rs 96 37 --to 96:38 \
--name context_lines --type usize
`render` (crates/prod-code-mcp/src/signature.rs)
- new parameter: `context_lines: usize`
- from the body: `2`
- 1 place(s) in the body now read it, 1 call site(s) pass it
6 changed line(s) in 2 file(s)
--- a/crates/prod-code-mcp/src/signature.rs
- pub fn render(&self, diff_budget: usize) -> String {
+ pub fn render(&self, diff_budget: usize, context_lines: usize) -> String {
- .context_radius(2)
+ .context_radius(context_lines)
--- a/crates/prod-code-mcp/src/tools.rs
- let text = change.render(6000);
+ let text = change.render(6000, 2);
the analyzer accepts the result: 0 errors
Three edits, and the third is the one that matters: render(6000) became render(6000, 2), not
render(6000, some_new_default). The one existing caller still asks for a radius of two,
because two is what the body had.
What it refuses, and why that follows from the same rule
If the expression is written at every call site, it has to be spellable at every call site. An expression that names a local of the function it came from is not:
$ prod-code extract-parameter crates/prod-code-mcp/src/move_item.rs 629 19 --to 629:39 \
--name removed_lines --type u32
`move_item` (crates/prod-code-mcp/src/move_item.rs)
- new parameter: `removed_lines: u32`
- from the body: `decl_end - start + 1`
- 1 place(s) in the body now read it, 4 call site(s) pass it
…
the analyzer rejects the result:
no such value in this scope [E0425] (crates/prod-code-mcp/src/tools.rs:1726:36)
no such value in this scope [E0425] (crates/prod-code-mcp/tests/orchestration.rs:577:99)
…
the expression is now written at every call site: if it names a local, a parameter or anything
private to the function it came from, it cannot be spelled there. Extract something the callers
can see.
nothing was written; pass `apply: true` to make these edits
decl_end and start are locals of move_item, and the call sites are four, so eight
diagnostics say the same thing in the analyzer’s words; the line underneath says it in the
reader’s. This is the same refusal as
the private sibling a moved item leaves behind -
a thing that was in scope where it was written and is not in scope where it is going.
Worth noting, after the last post: here the check does catch it. An unresolved value is reported where an unresolved type is not, so this particular refusal is one the analyzer can be trusted for.
The smallest declaration is not the enclosing function
The first run against this repository failed like this:
Error: `removed` is not a function
The tool asks the analyzer for the file’s symbols and takes the smallest one containing the
selection. For an expression inside let removed = decl_end - start + 1;, the smallest symbol
containing it is removed - textDocument/documentSymbol reports local bindings, and a local
binding is a perfectly good symbol. It is just not something that can take a parameter.
The fix is one clause: consider only symbols whose kind is a function or a method. The general lesson is smaller than it looks and worth writing down anyway - “the innermost thing” and “the thing that can do what I am about to do” are different queries, and the analyzer answers the first one because that is what it was asked.
Asking for the type instead of guessing it
The parameter needs a type. Hover knows it, and hover says it in whatever shape suits the thing under the cursor:
let decl_end: u32 # a local binding
core::str # a type
The first parses. The second is not a type annotation at all, and an arbitrary expression can
produce either, or nothing. So the rule is: take the type when hover gives the shape this can
read, and ask the caller when it does not. A type guessed out of a hover string is precisely the
kind of plausible wrong answer this whole series exists to avoid, and --type usize is four
words.
What it does not do
- It moves when the expression runs. Evaluation happens at the call now, not in the body.
For a literal or a constant that is nothing; for
Instant::now()or anything reading state the body changes first, it is a real difference the tool cannot see. - It puts the parameter last. Position and ordering are
code_change_signature’s job. - No default. Rust has none, and inventing one would change what a caller does, which is the one thing this refactoring is defined not to do.
- One function. The selection has to be inside a body; a selection in the signature is refused rather than reinterpreted.
replace_allis per function, not per file: every identical occurrence inside the body this selection belongs to.- Rust only, and re-run your formatter.
What it costs
One documentSymbol, one references, one hover when the type was not given, and one overlay
check across the changed files. About a second against a warm gateway.
The number that matters is a different one: zero. That is how many existing callers behave differently afterwards, and the tool is built entirely around keeping it there.
Cite this article
Alexander Panasenko (2026-10-05). The refactoring that changes nothing. https://prod.codes/blog/the-refactoring-that-changes-nothing/