Zero errors, and it did not compile
Every write tool in this series ends by saying the analyzer accepts the result. Building the one that bundles parameters into a struct produced a case where that sentence was printed and the code still did not build - and a four-line probe that says exactly how far the promise goes.

On this page · 7 sections
Every tool in this series that writes to your files ends with a line like this:
the analyzer accepts the result: 0 errors
It is the sentence that makes them safe to hand to an agent. Validating an edit before writing
it is the whole argument: the change is
assembled in memory, the analyzer judges it, and apply refuses a result that does not compile.
That post also found the first hole in it - rust-analyzer says nothing about a qualified call to a function you just deleted - and filled that one with a warning the tool synthesises itself. This is the post where the same silence came back in a shape that warning cannot cover, and this time it was wrong about code I had just written rather than code I had just removed.
What I was building
Parameter bloat is the everyday refactoring with nothing behind it. Here is a real eight-parameter function and everything rust-analyzer offers at it:
$ prod-code assists crates/prod-code-mcp/src/move_item.rs 578 14
inline_into_callers [RefactorInline] Inline into all callers
generate_fn_type_alias_named [Generate] Generate a type alias for function with named params
generate_fn_type_alias_unnamed [Generate] Generate a type alias for function with unnamed params
Nothing bundles anything. And changing what a function takes cannot do it either, because bundling is three edits that have to agree: a new type; a declaration whose parameters became fields, with a body that now reaches them through one name; and every call site passing a literal.
$ prod-code parameter-object move_item --path crates/prod-code-mcp/src/move_item.rs \
--param apply --param force --name MoveOptions
`move_item` (crates/prod-code-mcp/src/move_item.rs)
- was: (remote: SocketAddr, root: &Path, file: &Path, line: u32, col: u32, target: &Path, apply: bool, force: bool,)
- now: (remote: SocketAddr, root: &Path, file: &Path, line: u32, col: u32, target: &Path, move_options: MoveOptions)
- 4 call site(s) rewritten, 2 use(s) in the body
/// The parameters `move_item` takes together.
pub struct MoveOptions {
pub apply: bool,
pub force: bool,
}
39 changed line(s) in 3 file(s)
The first thing it could not borrow
The structural engine rewrites call sites beautifully, and it is what change signature uses. It refuses this one:
Error: prodCode/structuralReplace failed: Parse error: Failed to resolve path MoveOptions
Structural search and replace resolves the paths in its replacement template against the code as
it is. MoveOptions does not exist yet - it is the thing this refactoring is creating. The
engine is right to refuse, and there is no ordering that fixes it: writing the struct first to
make the path resolve means writing before the check, which is the one thing these tools do not
do.
So the call sites are rewritten as text, at the positions the analyzer reports. That needs a
splitter that knows what a comma in an argument list means, which is less obvious than it
sounds - f(a, |x, y| x + y) passes two arguments, not three:
let call = "f(a, |x, y| x + y, \"one, two\", b.iter().map(|v| v).collect())";
let args = split_args(&call[start..end]);
assert_eq!(args, ["a", "|x, y| x + y", "\"one, two\"", "b.iter().map(|v| v).collect()"]);
The second thing it could not borrow
The first working version generated the struct in one module and used it, by its bare name, from another. The report said what it always says:
the analyzer accepts the result: 0 errors
[applied to 3 file(s)]
And then the compiler:
$ prod-code check
rust check: FAILED (exit 101) in 0.9s; 1 error(s)
error: [E0422] cannot find struct, variant or union type `MoveOptions` in this scope
(crates/prod-code-mcp/src/tools.rs:1654:115)
A missing use. Obvious in hindsight, and the tool now adds it. The part worth your attention
is why the check did not say so, because the mitigation that covers the known case does not
cover this one. That mitigation works by diffing the symbols of a file before and after the
edit: a name that vanishes is looked for in the other files and a warning is synthesised where
the analyzer stayed quiet. Here nothing vanished. MoveOptions had never existed anywhere, so
there was no disappearance to notice and nothing to diff against.
Four lines and what they cost
The probe is one line appended to a real file, checked with prod-code validate against a warm
gateway, then checked again with prod-code check:
| appended to the file | the analyzer says | the compiler says |
|---|---|---|
let v = NoSuchStructAtAll { a: 1 }; |
0 errors | E0422 cannot find struct |
crate::no_such_module::value() |
0 errors | E0433 failed to resolve |
no_such_function_at_all(1) |
1 error | error |
let s: u32 = "not a number"; |
1 error | error |
A type error is reported, and so is an unresolved bare call. An unresolved type is not - and neither is a call through a module path that does not resolve, which is the second row and is the same hole the post on validating an edit measured from the other side.
This is not a bug in rust-analyzer. Its diagnostics are a curated set aimed at an editor, which stays useful in a file that is mid-edit precisely by not shouting about things that are merely missing. It is a bug in the sentence I had been printing, which an agent reads as this will compile when what it means is nothing the analyzer models is wrong with this.
The distance between those two is exactly one class of mistake: introducing a name that is not in scope. Which is, inconveniently, what a refactoring that generates a new type does every single time.
What the tool does about it
It does not rely on the check for imports. It works out, per file, how the new type should be named there:
- the declaring module - bare, the type is right there;
- another module of the same crate - bare, and the file gets
use crate::home::Size;; - a file that is not a module of the crate at all, like anything under
tests/- the full path,the_crate::home::Size, because ause crate::…there means something else.
imports:
crates/prod-code-mcp/src/tools.rs: added `use crate::move_item::MoveOptions;`
And the acceptance test for the feature is not the check’s opinion of it. It is the compiler’s:
run the bundling for real on this repository, then prod-code check and the test suite.
$ prod-code check
rust check: OK in 1.5s
$ prod-code test crates/prod-code-mcp --filter move
rust test: OK in 2.5s; 17 passed, 0 failed
Seventeen tests that were written against the old eight-parameter signature, passing against the new one, is a better sentence than anything the tool could say about itself.
What it does not do
- Two parameters at least. Bundling one is renaming it, badly.
- The receiver stays.
&selfis not a field. - One lifetime, or none. If any bundled type borrows, the struct takes
'aand every anonymous borrow is tied to it. Two independent lifetimes would need to be inferred from the body, and guessing there is worse than refusing. - A use that is not a call - a function pointer, a macro - is named under “not rewritten” rather than mangled into one.
- Rust only, and re-run your formatter: the struct arrives above the function with the indentation of a thing that was generated.
What it costs
One references call for the function, one per bundled parameter, and one overlay check across
every changed file - the same shape as moving a
declaration. About a second against a warm
gateway for the four call sites above.
The honest closing line is not about the second. It is that the check I have been quoting for twelve posts is a good check and not a compiler, and that a tool which generates code has to know the difference. The tools that only move existing names around - rename, move, change signature - never introduce an unresolved type; the mitigation built for them watches names leave, and a tool that creates one needs the opposite kind of care.
Cite this article
Alexander Panasenko (2026-10-04). Zero errors, and it did not compile. https://prod.codes/blog/zero-errors-and-it-did-not-compile/