Skip to content

Commit 96d0d79

Browse files
authored
Merge pull request #3928 from llogiq/todo-overreach
Avoid linting `unreachable_code` on `todo!()`
2 parents 49c9be5 + 1796af0 commit 96d0d79

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

text/3928-todo-overreach.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
- Feature Name: todo overreach
2+
- Start Date: 2026-01-31
3+
- RFC PR: [rust-lang/rfcs#3928](https://github.com/rust-lang/rfcs/pull/3928)
4+
- Rust Issue: [rust-lang/rust#149543](https://github.com/rust-lang/rust/pull/149543)
5+
6+
## Summary
7+
[summary]: #summary
8+
9+
The `todo!()` macro is meant as a placeholder, often used to make type check pass while still writing the code. However, since it diverges, all code that comes after it is marked as unreachable. The author proposes changing that to reduce the `unreachable_code` churn, while adding a `todo_macro_calls` lint that can be deactivated while the code is still work in progress and activated before pushing to production.
10+
11+
## Motivation
12+
[motivation]: #motivation
13+
14+
While working on Rust code, many IDEs as well as rust-analyzer will insert `todo!()` as a placeholder in autogenerated code. Programmers even sometimes manually insert `todo!()` to appease the type checker. However, the `todo!()` macro panics, which means that any code that comes afterwards is unreachable. This leads to `unreachable_code` lint message churn, especially because there is no distinction between code that is unreachable because of `todo!()` and code that is unreachable because of other panics, `return`s, `breaks` or calling other diverging functions.
15+
16+
The goal of this RFC is to instate such a distinction so that users can avoid the useless lint messages while keeping the important ones as they work on the code. This lets the user insert `todo!()` without being bothered by `unreachable_code` warnings, and they can ensure there are no `todo!()` macros left once they're done.
17+
18+
## Guide-level explanation
19+
[guide-level-explanation]: #guide-level-explanation
20+
21+
- First, the `unreachable_code` lint is extended to avoid linting on expressions that come from `todo!()` macro calls, if those calls are directly in the current crate's code. This means code expanded from another macro which contains `todo!()` will still lint `unreachable_code` because the `todo!()` is outside the purview of the programmer's crate – we generally don't require the programmer to modify their dependencies to quell a lint warning.
22+
- Second, for the `todo!()` macro calls appearing in the current crate and not expanded from external code, a warn-by-default `todo_macro_calls` lint is introcuced. This lint can be deactivated while working on the code, and re-activated once finished.
23+
24+
So for example:
25+
26+
```rust
27+
fn test(x: Option<String>) {
28+
let y = match x.as_ref() {
29+
Some(s) => todo!(),
30+
None => todo!(),
31+
};
32+
println!("{y}"); // <- this code would no longer be marked unreachable
33+
}
34+
35+
/* in `other_crate`:
36+
macro_rules! macro_containing_todo! {
37+
{ $x:expr } => {
38+
println!("{x:?}");
39+
todo!();
40+
}
41+
}
42+
*/
43+
44+
fn test_external(x: Result<String, std::io::Error>) {
45+
let y = other_crate::macro_containing_todo!(x);
46+
println!("{y}"); // <- this however would still be marked unreachable
47+
}
48+
```
49+
50+
## Reference-level explanation
51+
[reference-level-explanation]: #reference-level-explanation
52+
53+
To implement this, in the `rustc_hir_typeck` crate, there is one check for diverging code. There, we insert a check for a local `todo!()` macro call, and when that check returns `true`, we modify the result to "warned already" so that the code is marked as diverging (and e.g. the rest of type checking works as before), but the `unreachable_code` lint will no longer warn. To compensate for that, we emit the newly added `todo_macro_calls` lint for the expression.
54+
55+
A proof of concept implementation is in [#149543](https://github.com/rust-lang/rust/pull/149543).
56+
57+
## Drawbacks
58+
[drawbacks]: #drawbacks
59+
60+
- From a theoretical standpoint, implementing this RFC disturbs the conceptual purity of `todo!()`, which is now somehow more than just any old diverging expression with a suggestive name
61+
- There is a reasonably small bit of complexity added to the compiler, which we need to maintain
62+
- There is one more lint that people will encounter (although it will merely replace `unreachable_code` on all `todo!()` macro expressions, not add any new messages)
63+
- People might be confused why they get a lint for `todo!()` instead for unreachable code, especially if they had `#[allow(unreachable_code)]` in their code. The author suggests that this is easily countered with documentation
64+
65+
## Rationale and alternatives
66+
[rationale-and-alternatives]: #rationale-and-alternatives
67+
68+
- We could do nothing. This leaves the users with the problem stated above.
69+
- We could make `unreachable_code` ignore `todo!()` without adding a `todo_macro_calls` lint. However, people would need to run clippy (or use a search or other such IDE feature) to get rid of their `todo!()` calls. This is deemed suboptimal from a user experience standpoint.
70+
- We could only add a `todo_macro_calls` lint without omitting the `todo!()` macro from `unreachable_code` warnings. That would allow people to allow the `unreachable_code` lint while fixing up all `todo!()`s and only then fix the other unreachable code. However, again, the user might forget reactivating the `unreachable_code` lint, potentially leaving their code in a subpar state
71+
- We could make `allow(unreachable_code)` also suppress `todo_macro_calls`. However, this doesn't seem like a logical parent group.
72+
73+
Please note that this is a language proposal only insofar as that a lint (which is technically part of the language) is changed in accordance with a standard library item. It is not possible to do this without compiler support. There may be a way to implement this so that users could write their own `placeholder!()` macros that do not trip up the `unreachable_code` lint, although at this point the author sees no value in that.
74+
75+
## Prior art
76+
[prior-art]: #prior-art
77+
78+
In clippy, there is a `todo` lint that would be deprecated as soon as `todo_macro_calls` is available on stable. Also in clippy, we have done a lot of work to reduce low-benefit lint warnings churn over the last years, so this is merely an extension of that work to the compiler.
79+
80+
## Unresolved questions
81+
[unresolved-questions]: #unresolved-questions
82+
83+
none
84+
85+
## Future possibilities
86+
[future-possibilities]: #future-possibilities
87+
88+
There could be a shortcut to disable the `todo_macro_calls` lint while working on the code, or e.g. in a rust-analyzer configuration. Also we can document how to deactivate the `todo_macro_calls` lint on `Debug` builds, avoiding lint message churn during development while making sure no `todo!()` call makes it into a release build.

0 commit comments

Comments
 (0)