{"slug": "the-callers-a-return-type-assist-leaves-behind", "title": "The callers a return-type assist leaves behind", "summary": "A September 23, 2026 Rust implementation in PR #110 to the prod-code project added a return-type wrapping assist that updates callers by proposing a `?` after each call when the calling function's written return type name ends in `Option` or `Result`, and reports call sites it cannot propagate. The tool, described at source revision 0517151c, uses a textual test rather than full type resolution and sends proposed texts to an analyzer overlay to report remaining error diagnostics, which the author notes is not compiler proof. For callers that return anything else, `code_wrap_return` names the caller, its return type and the line rather than choosing a recovery policy such as a default, panic or early return.", "body_md": "# The callers a return-type assist leaves behind\n\nA September rust-analyzer probe wrapped a function’s return type without updating its callers. The first prod-code implementation added propagation candidates and a report of unresolved calls. Its limits show why the caller side needs a separate check.\n\n## On this page · 4 sections\n\nThis post describes the Rust implementation introduced on September 23, 2026 in\n[PR #110](https://github.com/alex09x/prod-code/pull/110), at\n[source revision 0517151c](https://github.com/alex09x/prod-code/blob/0517151c6f37fc6b923d04cd1a638362b38d6389/crates/prod-code-mcp/src/wrap_return.rs).\nThe development checks and timings below are reported in the linked PRs; they were not\nindependently reproduced for this post. Later versions changed this tool.\n\nThe roadmap for this project listed five refactorings as “probed and not offered”: point\nrust-analyzer at the obvious place and nothing comes back. It also said the probe might have\nmissed a trigger. [PR #108](https://github.com/alex09x/prod-code/pull/108) records a second\nprobe, with one example of each shape in a scratch crate: two of the five were offered.\nSelected output follows; other assists and probe cases are omitted, and the labels are shortened:\n\n```\n== line 2 col 5 (an `if let` that ends the function)\nconvert_to_guarded_return  [RefactorRewrite]  Convert to guarded return\n== line 21 col 18 (the return type of `pub fn plain() -> u32`)\nwrap_return_type_in_option  [RefactorRewrite]  Wrap return type in Option\nwrap_return_type_in_result  [RefactorRewrite]  Wrap return type in Result\n```\n\nThe first was recorded as reachable through the assists tool. The second was half done in that probe, and it is the more instructive half to look at.\n\n## What the assist does\n\n``` php\n-pub fn plain() -> u32 {\n-    5\n+pub fn plain() -> Option<u32> {\n+    Some(5)\n```\n\nIn the [reported example](https://github.com/alex09x/prod-code/issues/109), the signature and\nreturned value change, but the callers do not. A caller adding the result of `plain` to a\n`u32` now has a type error: it gets an `Option<u32>`. A caller that discards the result need\nnot break; the assist leaving callers untouched is the problem.\nThe `Result` version is worse in a small way - it writes `Result<u32, _>`, a placeholder for an\nerror type nobody has chosen yet.\n\n## One question per caller\n\nFor each call found in the analyzer’s reference list, the implementation examines the calling function’s written return type.\n\nIf the written type name ends in `Option` for an Option wrapper, or `Result` for a Result\nwrapper, the tool proposes a `?` after the call. This is a textual test, not full type\nresolution. In the simple example, `?` unwraps `Some` or `Ok`; `None` or an error propagates\nfrom the enclosing function. That does not prove preservation of every program’s behaviour.\nFor a `Result`, the caller’s error type must support conversion from the new error type.\nThe tool sends proposed texts to an analyzer overlay and reports remaining error diagnostics;\nit does not establish that every invalid conversion will be detected. Analyzer acceptance\nis not compiler proof. The shared `verify: \"compile\"` gate was a separate option.\n\nIf it returns anything else, there is no mechanical answer. The call used to produce a `u32`;\nnow it may produce nothing, and what the caller should do then - use a default, panic, return\nearly from something that cannot return early, change its own signature too - is exactly the\ndecision the wrapping was meant to force. So `code_wrap_return` does not choose that recovery policy. It names the\ncaller, the type it returns, and the line. This abridged historical example omits diff headers,\nthe changed-line count and the final apply instruction:\n\n```\n`plain` (src/lib.rs)\n\n- returned: `u32`\n- now returns: `Option<u32>`\n- 1 call site(s) propagate with `?`\n\n-    Some(plain() + 1)\n+    Some(plain()? + 1)\n\n1 call site(s) cannot propagate: the calling function does not return an `Option`. Each needs a decision - unwrap, match, or wrap that caller too:\n  src/lib.rs:42:37 the caller returns `u32`: `s.uses_self() + S::no_self(2) + plain()`\n\nthe analyzer rejects the result:\n  the trait bound `u32: Add<Option<u32>>` is not satisfied [E0277] (src/lib.rs:42:5)\n\nnothing was written\n```\n\nPR #110 reports that the analyzer’s complaints in this example concerned the blocked caller.\nA dry run writes nothing regardless of diagnostics. On an apply request, this revision refuses\nif its blocked-caller list or remaining analyzer errors are nonempty, unless `force` is set;\nforcing permits the write despite those findings. For `Result`, the `_` in the signature\nbecomes the `error` type the caller of the tool gives, and without one the tool asks for it instead\nof writing a placeholder into the code.\n\n## Positions that moved\n\nThe implementation starts from positions returned by the analyzer. Callers in the file that declares the function are harder, because the assist has just rewritten that file: every position after the function has moved by however much the rewrite added. The rewrite only touches the function’s own span, so a position before it means the same thing it did, and a position after it moves by the difference in length.\n\nFor a reference to the selected function, a position *inside* its own span is treated as a\nrecursive call. PR #110 reports that the tool’s test caught it being handled wrongly on its\nfirst run; the available source commit contains the feature and test together, not a separate\nexecution record establishing that chronology. The test’s function calls itself - `count(n - 1) + 1` - and the tool\nreported that call as a caller that “returns `u32`”, which was true of the file on disk and false\nof the file being written, where `count` now returns an `Option`. The recursive call sits inside the\nspan the assist rewrote, so it is now reported as what it is: a call inside the\nfunction, for a person to finish. The PR also reports that removing the shift for positions\nafter the function makes the same test fail with the `?` in the wrong place. That mutation-check\nresult is author-reported; we have not rerun it.\n\n## Cost and limits\n\nPR #110 reports two warm dry runs on the scratch crate taking 0.25 and 0.23 seconds. The cost that matters is elsewhere: every caller the tool cannot rewrite is a decision someone has to make, and the report is built to make that list short to read - one line per caller, with the type that stands in the way.\n\nThe limits below belong to that September revision:\n\n- **It does not choose for a caller.** No`unwrap` , no`unwrap_or_default` , no`match` written on\nanyone’s behalf.\n- **Closures are not modeled separately.** A call inside a closure is judged by the enclosing\nfunction’s header. A proposed`?` there may be wrong; the overlay may report an error, but\nthis heuristic does not itself prevent an incorrect proposal.\n- **The reference list is not a completeness guarantee.** This revision turns a failed reference\nquery into an empty list. Recursive calls and non-call references are reported as unmatched;\nthat list alone does not block an apply request. They need manual review even if diagnostics\nare empty.\n- **Rust only at that revision.** Later changes added other languages\n([PR #713](https://github.com/alex09x/prod-code/pull/713) ) and custom envelopes\n([#727](https://github.com/alex09x/prod-code/pull/727) ); empty-reference fallback was later\nrevised in[#985](https://github.com/alex09x/prod-code/pull/985) and[#1027](https://github.com/alex09x/prod-code/pull/1027) . This post is not a current capability\nor safety assessment of those later implementations.\n\n## Cite this article\n\n```\nAlexander Panasenko (2026-10-13). The callers a return-type assist leaves behind. https://prod.codes/blog/the-half-that-breaks-every-caller/\n```\n\n", "url": "https://wpnews.pro/news/the-callers-a-return-type-assist-leaves-behind", "canonical_source": "https://prod.codes/blog/the-half-that-breaks-every-caller/", "published_at": "2026-10-13 00:00:00+00:00", "updated_at": "2026-11-06 08:51:42.617767+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["prod-code", "rust-analyzer", "PR #110", "PR #108", "code_wrap_return"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-callers-a-return-type-assist-leaves-behind", "markdown": "https://wpnews.pro/news/the-callers-a-return-type-assist-leaves-behind.md", "text": "https://wpnews.pro/news/the-callers-a-return-type-assist-leaves-behind.txt", "jsonld": "https://wpnews.pro/news/the-callers-a-return-type-assist-leaves-behind.jsonld"}}