Deprecated on re-exports

Deprecated is an attribute you can put on items to mark them as, well, deprecated. Any use of that item will give a warning, telling you that you're using something that's deprecated. The thing is, it doesn't always work...

Press for table of Contents

The deprecated attribute

Let’s start with some examples that work. Here, foo is clearly deprecated:

1
#[deprecated]
2
fn foo() {}
3
4
fn main() {
5
foo();
6
//~^ WARNING use of deprecated function `foo`
7
}

The attribute is slightly more flexible; You can also give a version and a reason like so:

1
#[deprecated(
2
since = "5.2.0",
3
note = "foo was rarely used. Users should instead use bar"
4
)]
5
fn foo() {}

Or, just a reason:

1
#[deprecated = "foo was rarely used. Users should instead use bar"]
2
fn foo() {}

Alright, makes sense.

Now, the reference says one more thing about the attribute:

When applied to an item containing other items, such as a module or implementation, all child items inherit the deprecation attribute.

Let’s take a look:

1
#[deprecated]
2
mod a {
3
pub struct Foo;
4
}
5
6
fn main() {
7
let _x = a::Foo;
8
//~^ ERROR use of deprecated unit struct `a::Foo`
9
}

The struct Foo is still deprecated, while the attribute only applies to a. And this doesn’t just work for functions or structs. pretty much any kind of item works. I say prety much, since use items are not supported. However, if they were that’d be pretty useful. Just look at this example:

1
fn foo() {}
2
3
#[deprecated]
4
use foo as bar;
5
6
fn main() {
7
bar();
8
}

You might expect this to mean thattThe use of the name bar is deprecated, but foo isn’t. But what does the rust compiler say? Make your best guess now!

Turns out: it doesn’t work

Believe it or not, the answer is: absolutely nothing. Nothing is deprecated, but the compiler is absolutely fine with the attribute being there. The reason is that after name resolution, the use of bar doesn’t pass through the re-export anymore. It just resolves directly to foo. So, the fact that the use item is deprecated isn’t noticed.

WaffleLapkin's github avatar WaffleLapkin were planning to fix that. It seems reasonable that this should work, or otherwise at least warn that it doesn’t do anything. What happened is that we found multiple other bugs with #[deprecated].

So, it’s quiz time. Based on what you read sofar, and what you maybe already knew. How many deprecation warnings does this code produce? I’ve annotated lines that potentially use a deprecated item.

1
#[deprecated]
2
pub mod a {
3
#[macro_export]
4
macro_rules! foo {() => {};}
5
6
pub fn bar() {}
7
8
macro_rules! foo_no_export {() => {};}
9
foo_no_export! {} // <-- 1
10
}
11
12
#[deprecated]
13
macro_rules! baz {() => {};}
14
baz! {} // <-- 2
15
16
use a::bar as bar1; // <-- 3
17
use foo as foo1; // <-- 4
18
19
fn main() {
20
foo! {} // <-- 5
21
a::bar(); // <-- 6
22
}

You might think: 6. There are 6 usages, so that’s what I expected as well. Except, the answer is only 4. It turns out, the inheriting of #[deprecated] through modules doesn’t work, so 1 and 5 don’t produce a warning. Though what surprised us was that 4 does give a warning!

What’s going on here is that the reason inheriting doesn’t work on macros, is that when a macro expands, we pretty much remove any trace of it from the AST. The macro is replaced by whatever the macro expanded to. Checking deprecation only happens later, once the macros are already gone, so their uses go unnoticed.

So why does 2 work then? Turns out, there’s special code to handle deprecated on macros. To make sure that in most cases it works as it is supposed to. But, that codepath only works when #[deprecated] is used directly on a macro. It doesn’t look up.

There’s more???

While playing around with what does and doesn’t work with #[deprecated], we tried the following code. Again, I’ve annotated lines that potentially use a deprecated item. How many deprecation warnings do you think this produces?

1
#[deprecated]
2
pub mod a {
3
pub struct Foo;
4
pub struct Bar();
5
pub struct Baz {}
6
}
7
8
9
use a::Foo; // <-- 1
10
use a::Bar; // <-- 2
11
use a::Baz; // <-- 3
12
13
fn main() {
14
a::Foo; // <-- 4
15
a::Bar(); // <-- 5
16
a::Baz {}; // <-- 6
17
}

This time, it’s not fewer, it’s more! Not 6, but 8. 1 and 2 count twice, since in a way they’re exporting two things. Unit and tuple structs can be constructed without braces. The way that works is that the compiler effectively does the following transformation:

1
struct Foo
2
// roughly expands to
3
struct Foo {}
4
const Foo: Foo = Foo{};

And use imports both definitions of Foo. Hence, two warnings.

Structs with braces don’t get this treatment, and 3 only produces one warning.

This last bug is fixed with this pr.