design note · · 5 min

The analyzer already knows your patch is broken

An agent writes a file, runs a build, reads the errors and tries again. The database that answered its last hover could have told it in 100 ms, without writing anything. What that takes, and the one error class rust-analyzer stays silent about.

On this page · 4 sections
  1. Show it the text, don’t write the file
  2. The silence that cost us a tool
  3. Validation is a fast gate, not the compiler
  4. What it does not do yet

The standard agent loop for changing code is: write the file, run the build, read the errors, write the file again. Every iteration costs a compile, and the first thing it does is put code you already suspect is wrong into your working tree, where the next process to look, a watcher, a hook, another agent, sees it as your intent.

The analyzer that answered the agent’s last hover is holding the whole workspace in memory. It can type-check a proposed file against that workspace in the time it takes to send it.

Write-then-build loop compared with draft, validate in 0.11 seconds without writing, then write
The gate is cheap enough to run on every draft, which is what makes the expensive step rare.

Show it the text, don’t write the file

code_validate_edit takes a path and the complete proposed content. The session opens the document with that text, pulls textDocument/diagnostic, and returns what the analyzer says. Nothing is written anywhere; the overlay is private to that session and disappears with it. The path may be a file that does not exist yet.

For Rust the answer comes from rust-analyzer’s own diagnostics over the in-memory database, which means unresolved names, type mismatches, wrong field names, unused items, and it means no cargo invocation. Measured on fixtures: Rust 0.11 s, C++ 0.26 s, TypeScript 0.28 s, Go 0.6 s, against a build that starts in seconds and ends in tens of them. The intended loop is draft, validate, fix, then write, and the thing that makes it worth doing is that the expensive step happens only when the cheap step is clean.

Here is the cheap step catching an invented method, on a file from this repository with one line changed. Nothing was written to disk at any point:

$ code_validate_edit { "path": "crates/prod-code-gateway/src/shadow.rs",
                       "new_text": "…the whole file, with append_slice instead of extend_from_slice…" }
crates/prod-code-gateway/src/shadow.rs: 1 error(s), 0 warning(s)
  error: no method `append_slice` on type `Vec<u8, Global>` [E0599]
         (crates/prod-code-gateway/src/shadow.rs:65:18)

One edit is rarely one file, so code_validate_edits takes several proposed files and places them all in one overlay, judging each against the proposed state of the others: a changed signature and its updated callers are checked together rather than one at a time, which is the only way either of them is correct. also_check adds unchanged files that might break, the callers you did not plan to touch. A consistent two-file rename across our own gateway reports zero errors. Rename only the definition, with the caller in also_check, and the caller’s error comes back, although not the one you would expect: rust-analyzer surfaces it as E0282 type annotations needed at the call site rather than as an unresolved path.

The silence that cost us a tool

That last detail is the general problem. rust-analyzer is an IDE engine, and an IDE’s job is to stay useful in a file that is mid-edit, so it reports much less than rustc does about things that are merely missing. Delete a function and call it from another file with a qualified path, module::gone(&x), and rust-analyzer says nothing at all about that line. Only a real compile produces E0425. For an agent that validated its patch and got a clean report, that silence is a lie by omission.

So the tool no longer trusts the diagnostics alone. For every edited file it takes the document symbols of the text on disk and of the proposed text, and diffs them. Any symbol that has disappeared or been renamed is then looked for in the other files being checked. Where the analyzer already reported an error on a line that mentions the vanished name, the error gets a note saying which symbol went and which file it went from. Where the analyzer stayed silent, the tool synthesises its own warning, prod-code::stale-reference, on that line. Renaming one function in one file and checking its caller now produces three warnings naming the function, where before it produced nothing.

This is that case, run against the real pair: the gateway’s sweep renamed in the file that defines it, with the file that calls it as shadow::sweep(…) passed in also_check.

$ code_validate_edits { "edits": [ { "path": "crates/prod-code-gateway/src/shadow.rs",
                                     "new_text": "…sweep renamed to sweep_shadow_dirs…" } ],
                        "also_check": [ "crates/prod-code-gateway/src/main.rs" ] }
2 file(s) checked together: 2 error(s), 1 warning(s)
crates/prod-code-gateway/src/shadow.rs: 0 error(s), 0 warning(s)
crates/prod-code-gateway/src/main.rs:   2 error(s), 1 warning(s)
  warning: uses `sweep`, which the proposed edits remove or rename (the analyzer reports no
           error for a plain call to a missing function; run code_check to be sure)
           [prod-code::stale-reference] (crates/prod-code-gateway/src/main.rs:3238:25)
    note: this line uses `sweep`, which the proposed edit to
          crates/prod-code-gateway/src/shadow.rs removed or renamed; update the caller or
          keep the symbol
  error: the trait bound `dyn Value: PartialOrd<i32>` is not satisfied [E0277]
         (crates/prod-code-gateway/src/main.rs:3239:8)
  error: the trait bound `dyn Value: PartialEq<i32>` is not satisfied [E0277]
         (crates/prod-code-gateway/src/main.rs:3239:8)

The edited file itself is clean, which is what makes the naive report dangerous. The two E0277s on the next line are the cascade: with the call unresolved, the variable it assigns has no type, so the comparison against an integer fails in a way that says nothing about the actual mistake. The warning above them is the one that names it.

The same report drops one class of message entirely: inactive-code hints, the analyzer’s note that a block is behind a cfg that is currently off. Code that is not compiled in this configuration is not an error, and an agent that sees it as one starts “fixing” platform-specific branches.

Validation is a fast gate, not the compiler

the proposed text has the analyzer says
a name that does not exist unresolved, with a code
a type mismatch or a wrong field reported, at the right span
a call into code generated by a build script unresolved, wrongly
a qualified call to a function you just deleted nothing — hence our own warning

The honest caveat is that the analyzer’s view is not the compiler’s view. Without build scripts and proc-macro expansion, generated code does not exist, and a name that only appears at build time reads as unresolved. The rule we run with: validation is a gate you must pass before writing, and the build on the node is what decides whether you were right. It is cheap enough to run on every draft, which is the entire point, and the build is rare enough afterwards that the loop is shorter even when the gate is imperfect.

What it does not do yet

  • The analyzer is not the compiler. Missing build scripts and unexpanded proc macros make generated names look unresolved; code_check remains the truth.
  • The stale-reference warning is lexical: it fires on a line that mentions the removed name as a whole word, so a comment or a string containing that name is flagged too.
  • Only symbols that the document-symbol request reports are tracked, so a removed local or a name produced by a macro is not.
  • The first semantic diagnostics of a large crate after the workspace loads cost about 28 s of type inference on a 32-core node; every call after that is about a second.
  • Nothing is queued: two agents validating different texts for the same file in the same session would fight, so each session keeps its own overlay and the tool is single-shot.
Cite this article
Citation
Alexander Panasenko (2026-09-23). The analyzer already knows your patch is broken. https://prod.codes/blog/the-analyzer-already-knows-your-patch-is-broken/