`todo!()` should prevent "unused_variables" warning

It would be great if this code would not trigger unused_variables warnings:

fn foo(a: i32) {
  todo!()
}

fn main() {
  foo();
}

This has been discussed before Make #[allow(unused_variables)] implicit when calling todo!() and Don't warn on unused arguments in unimplemented (sic: todo) functions.

The request seems to be generally in favor, but gets closed or times out. I am not sure what the next step is. Can I help with anything?

7 Likes

You can flag a topic and ask for it to be reopened :slight_smile:

This has come up multiple times, but there isn't a team consensus in favor of it. It's a matter of preference: some people prefer that todo!() suppress warnings, others want to continue to see those warnings until they're fixed. In the absence of a consensus to change, we keep the current behavior.

3 Likes

While todo! can be compiler magic, I question how a macro would apply an attribute to the enclosing scope. By what mechanism could this be described? Would it apply to the enclosing fn no matter how nested? What about within a closure within a function?

I have a related nit when implementing the default behavior for a trait method that doesn't use all parameters: I want to keep the name as used in common cases instead of prefixing with _ and also avoid using "ignore all unused variables" instead of "just this one". C and C++ have (void)unused; to indicate it; I don't think Rust has such a spelling beyond #[used] on the declaration (which doesn't work very narrowly for conditionally used variables).

drop(foo); used to work, but I think at least clippy complains about it now.

Maybe _ = foo;?

But the macro wouldn't know what identifiers to do that with?

Couldn't the lint suppress itself/consider variables to to have been used in the event that todo!() is encountered within their scope?

1 Like

#![allow(unused_variables)] works in a function, if you want to make a different macro that includes that attribute.

1 Like

But only if invoked at the start of the scope? And the todo!() might not only appear after other statements, but furthermore within some nested scope?

1 Like

It's not about the "right" behavior, but about providing a configurable mechanism so people can choose. The default can by all means stay as is.

My question is how can Rust be extended so that those who want a more sophisticated treatment of todo!() can configure their workflow accordingly, and also how could I help to implement this.

I don't want to repeat the previous discussions but the major points which seemed to stand out where:

  • Disabling all "unused_variables" does not work, because it is an important warning.
  • Putting an additional hint for those functions (like #![allow(unused_variables)]) adds considerable overhead and is dangerous as one might forget to remove it.
  • todo!() has different semantics than unimplemented!().

I am unfortunately not familiar with the Rust compiler, but couldn't something like this work: Analyze a function and find out if any execution paths ends in "todo!()". If so first produce a (optional) warning a la "warning: you have still work to do!" and then (optionally) suppress other "unused_variables" warnings triggered by this function.

6 Likes

It would be great if it were possible to have a function-like macros!() called inside a function generate an #[attribute] in the enclosing function.

Something like

fn f() {
    mymacro!();
}

becomes

#[something]
fn f() {
    // expanded macro here
}

This would enable creating a todo!() alternative that also applied #[allow(unused_variables)] to the enclosing function.

What about a using clause? for example:

let _=arg1;
let _=arg2;
let _=arg3;
todo!("format string {}", extra_informations);

becomes:

todo!(using arg1,arg2,arg3; "format string {}", extra_informations);

since using xx is not part of the todo! grammar, so such extension might be acceptable.

3 Likes

This feels like it makes sense, but doesn't really solve the normal loop of "write a function with todo, get a warning, go back and suppress the warning". todo! is supposed to make it quick and easy.

I think most helpful/general would be a way to suppress warnings within a function from within a function, so the todo! macro could just include that suppression. But that still might be more effort than it's worth, if this is its only use. And it still doesn't quite cover todos that aren't top level in a function, but it still might be enough.

2 Likes

Ultimately my experience has been that in the transient state of a module where I'd have todo!s, I generally want a blanket #[allow(unused)] on the whole module anyway, because I'll have partial function/type drafts present which aren't "used" yet.

The papercut that annoys me more is todo! triggering unreachable_code on (and thus also sometimes hiding some borrowck errors in) anything following; I want to be able to use todo! as a hole (potentially multiple) in the interior of a function while I sketch it out top-down control flow (without needing to write out the holes' function signatures until necessary for type inference[1]).

My opinion: either todo! should suppress unused_variables or it shouldn't cause unreachable_code. Having unused_variables is nice when using todo! as a typed impl hole, but unreachable_code ends up discouraging such non-tail usage.

One reasonably straightforward case (although quite adhoc and probably far from simple to detect during lint passes) would be a function body of only a todo!. That case shouldn't lint for unused function parameters for the same reason trait method declarations without default bodies don't — in this state it's obviously just a declaration for an impl to be applied to later.

(But when param bounds permit, I do like to use todo!("fn_name({arg_name:?})") for quicker identification, which does actually use the parameters.


  1. Maybe I should familiarize myself with the "generate function" intent in my IDE more... ↩︎

10 Likes

I do this too (and ignore unused when refactoring and expanding API space as well). I hadn't thought of the annoying aspects from the perspective of function boundaries:

// This issues no warnings
pub fn f(i: i32) {
    let j: i32 = (|| todo!())();
    println!("{}", i + j);
}

(Macro version.)

I was pretty ambivalent around the original proposal,[1] but this behavior is nicer for non-tail todo!(). Perhaps there should be a single warning per "lazy todo" or a separate warning category.[2]


  1. for variety of reasons ↩︎

  2. I generally grep for todos (that may also be in comments, say) as part of reaching a dev milestone, but warnings are more reliable. ↩︎

3 Likes

FWIW, clippy has clippy::todo for linting uses of todo! (although that doesn't catch // TODO comments, ofc).

That is interesting how type inference works here (I suppose due to !'s pseudotype status and closures' inference hacks). If you enable #![feature(never_type)] and annotate the closure as returning !, then you get an unreachable_code warning again. If you don't, then the type inference favors giving the closure the return type determined by usage, pushing the ! as i32 coercion into the body of the closure, thus "hiding" the fact that anything after the closure is invoked is unreachable.

If you're sticking it in a macro anyway, might as well stick #[track_caller] on the closure to strip the otherwise meaningless closure stack frame. Your value_of! is basically a declval except guarded by a runtime panic instead of a codegen error.


  1. I generally grep for todos (that may also be in comments, say) as part of reaching a dev milestone, but warnings are more reliable. ↩︎

2 Likes

Mh, I think this is a matter of preference but also a matter of use cases.

If you are a library implementor the todo! is good for emitting warnings, if you are a library user todo! may avoid emitting warnings because maybe the feature that the library is forcing you to implement (with a trait) it not a priority for you.

I am most of the time myself inside the library implementor side, but I think we can allow the user to define a top-level feature to tell (Please do not emit winning when I was the todo macro), this should make everyone happy.

1 Like

This is one of those situations where I distinguish between todo!() ("I am going to implement this, but just haven't got around to it yet") and unimplemented!() ("I might never implement this, because it's not required").

6 Likes

Another annoying feature of todo!() is that you can't use it in functions returning impl Trait, i.e. this code is an error

fn foo() -> impl Bar { todo!() }

because inference treats a panic as (), and it doesn't implement Bar in general.

I get why in general panics are treated that way, but for todo!() specifically it would be very useful if the type checker would just shut up and acted as if everything typechecks. That's the way assume, believeme etc directives work in dependently-typed languages, for example. Whatever obligations were outstanding, they are discharged.

13 Likes

I agree that todo!() should not trigger the unreachable_code warning, but I don't think it should silence unused_variables by default. It would be better to allow it to explicitly accept values to mark them as "used", i.e. we could write todo!(foo, bar);. It would have no effect on the panic message, but for programmers it would be an indication to "add code which uses foo and bar".

UPD: We already can write todo!("{foo} {bar}"), but it requires implementation of a formatting trait by the types.

5 Likes

It would be nice if todo!() returned ! here and somehow we guaranteed that ! implemented Bar and other traits automatically (with all associated types also set to ! and all methods panicking)

1 Like