- Feature Name: todo overreach
- Start Date: 2026-01-31
- RFC PR: rust-lang/rfcs#3928
- Rust Issue: rust-lang/rust#149543
Summary
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.
Motivation
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, returns, breaks or calling other diverging functions.
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.
Guide-level explanation
- First, the
unreachable_codelint is extended to avoid linting on expressions that come fromtodo!()macro calls, if those calls are directly in the current crate’s code. This means code expanded from another macro which containstodo!()will still lintunreachable_codebecause thetodo!()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. - Second, for the
todo!()macro calls appearing in the current crate and not expanded from external code, a warn-by-defaulttodo_macro_callslint is introcuced. This lint can be deactivated while working on the code, and re-activated once finished.
So for example:
fn test(x: Option<String>) {
let y = match x.as_ref() {
Some(s) => todo!(),
None => todo!(),
};
println!("{y}"); // <- this code would no longer be marked unreachable
}
/* in `other_crate`:
macro_rules! macro_containing_todo! {
{ $x:expr } => {
println!("{x:?}");
todo!();
}
}
*/
fn test_external(x: Result<String, std::io::Error>) {
let y = other_crate::macro_containing_todo!(x);
println!("{y}"); // <- this however would still be marked unreachable
}
Reference-level explanation
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.
A proof of concept implementation is in #149543.
Drawbacks
- 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 - There is a reasonably small bit of complexity added to the compiler, which we need to maintain
- There is one more lint that people will encounter (although it will merely replace
unreachable_codeon alltodo!()macro expressions, not add any new messages) - 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
Rationale and alternatives
- We could do nothing. This leaves the users with the problem stated above.
- We could make
unreachable_codeignoretodo!()without adding atodo_macro_callslint. However, people would need to run clippy (or use a search or other such IDE feature) to get rid of theirtodo!()calls. This is deemed suboptimal from a user experience standpoint. - We could only add a
todo_macro_callslint without omitting thetodo!()macro fromunreachable_codewarnings. That would allow people to allow theunreachable_codelint while fixing up alltodo!()s and only then fix the other unreachable code. However, again, the user might forget reactivating theunreachable_codelint, potentially leaving their code in a subpar state - We could make
allow(unreachable_code)also suppresstodo_macro_calls. However, this doesn’t seem like a logical parent group.
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.
Prior art
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.
Unresolved questions
none
Future possibilities
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.