Dead_code lint for variants of a public enum?

Can one selectively lint if a variant of a public enum is never constructed within the crate, similarly to how the dead_code diagnostic does it for a private enum?

pub enum MyError {
	Overflow,
	Bar,
	Baz,
}

type MyResult<T> = Result<T, MyError>;

pub fn my_baz(input: u32) -> MyResult<u32> {
	input.checked_add(1).ok_or(MyError::Overflow)
}

Output for the code above is empty, as expected. But, for enums which are public but are unlikely to be constructed outside the crate, like this ErrorKind-like example, this would be useful to e.g. clean up unused error variants during development (in this case, Bar and Baz).

There's a workaround: changing pub enum to private works, dead_code kicks in and rustc warns about each unused variant, but there's also going to be one compile error for each "private type in public interface" (potentially a *lot*), so I wonder if there's a nicer way to do this.

1 Like

I guess the main issue is that all of the variants are always "used" by the crate's public interface. As long as you have a public function that returns MyError, the whole enum is considered used, even if it is inaccessible. One workaround is to temporarily disable the public interface altogether. To do this, you can surround your src/lib.rs file with:

#[path = "."]
mod lib {
    /* ... */
}

However, this will produce a dead_code lint for every type and function that is not used within the crate. To fix that, you can add #[allow(dead_code)] to all modules except the one containing MyError, then to all other types and functions in that module that are unused. This reduces the lints to the unused variants.

Not sure I understand this? Rustc does in fact seem to find unused variants when a private enum is used in a public interface (even though it's a hard error).

You can avoid that error by wrapping a private type with a public type. For example, you could structure your errors as follows:

enum ErrorKindInner {
    Overflow,
    Bar,
    Baz,
}

pub struct ErrorKind(ErrorKindInner);

Of course, now the end user cannot match on the inner enum, so you need to provide some variant-discriminating public functions.

Alternatively, you could mirror your private inner enum as a public enum, using the following bit of boilerplate:

#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
enum ErrorKindInner {
    Overflow,
    Bar,
    Baz,
}

#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum ErrorKind {
    Overflow,
    Bar,
    Baz,
}

impl ErrorKindInner {
    fn to_pub_error_kind(self) -> ErrorKind {
        match self {
            ErrorKindInner::Overflow => ErrorKind::Overflow,
            ErrorKindInner::Bar => ErrorKind::Bar,
            ErrorKindInner::Baz => ErrorKind::Baz,
        }
    }
}

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct Error(ErrorKindInner);

impl Error {
    pub fn error_kind(&self) -> ErrorKind { self.0.to_pub_error_kind() }
}

Since ErrorKindInner is private, you will get the "variant never constructed" lint as usual. If you remove the never constructed variant, the corresponding match arm inn to_pub_error_kind will also cause an error, hopefully nudging you to remove the corresponding public variant or to construct the unused variant somewhere. This can even be automated via some macro!

The downside is that the public ErrorKind variants can be explicitly matched on, which will cause a breakage in downstream code if you ever remove them. If this breakage is unacceptable for you, you should just go with the simple pub enum ErrorKind model that you started from.

1 Like

You can have an inaccessible public type, by putting it in a private module:

mod private {
    pub enum MyError {
        Overflow,
        Bar,
        Baz,
    }
}

fn get_overflow() -> private::MyError {
    private::MyError::Overflow
}

The compiler won't complain about it, since it is still a pub type, users are just unable to name it. This is most commonly seen in the sealed trait pattern.

1 Like