Pub type exported or not

While working on a Rust library, I encountered this thing I didn't realize before:

A pub type Foo can be accessed by external code even when Foo was not exported in lib.rs.

For example, lib.rs has exported a public function like:

pub fn my_fn() -> Foo

The external user can call my_fn() to get a Foo and even call Foo.foo_func as long as foo_func is pub.

It means the external code can use Foo for quite a bit use cases by Rust's type inference. But this creates an interesting scenario: in cargo doc output, the user can see this my_fn returns a Foo, but they cannot get the doc of Foo as it's not exported.

A random example: ArgMatches in clap - Rust

There is no doc for the public type Occurrences.

I guess basically Rust does not force an external public type to be exported in a lib. (Or, should I call such type half-pub type?) But I'm curious if that is a design choice or by accident?

Have you actually tried calling my_fn from an external crate? You should get a compilation failure; and if not, then I believe you have found a bug. While you can define a pub fn like you have, that doesn't mean the function is in fact callable by external code.

Ignore this post. I apologize.

foo crate:

/// Hi.
struct Foo;
/// I'll show up in documentation, but external code
/// won't be able to call me since at least one part of
/// my signature (i.e., the return type) is not accessible.
pub fn not_callable() -> Foo {
    Foo
}

bar crate:

/// This won't compile since `bar` does not have access
/// to all parts of the signature of `foo::not_callable`.
fn wont_compile() {
    foo::not_callable();
}

You should pay heed to compiler warnings, and I'd even recommend elevating the private_interfaces lint from a warning to a compilation failure (e.g., #![deny(private_interfaces)]).

The reason there is no documentation for Occurrences is that type constructor lacks documentation. It is pub though.

I'm pretty sure this is not a bug but instead just how the language works. This has always been the case and also known for quite a long time. A similar situation happens with traits (you can use as supertrait a pub trait defined in a private module) and it has been used as a "feature" to make public traits that can only be implemented by the defining crate (due to requiring a supertrait that can only be named and thus implemented by that crate).

2 Likes

I know about "sealed" traits—and even sparingly use them—but what's described here seems to be different. I didn't try much, but I was unable to make a pub fn in one crate that returns a private type and have it be callable from an external crate. Can you show an example where this is possible?

Ignore this post. I apologize.

Ignore everything I said. I see what @SkiFire13 was talking about. I've only experienced "sealed" traits, but the same "technique" applies to normal types too which makes sense. Sorry for the brain fart.

As @SkiFire13 stated, this is known behavior and can be even useful especially when one wants a trait to be callable but not implementable from external code.

The trick is to define a private mod that has a pub trait and a separate sub-trait of this "sealed" trait that is pub. A library author will then control exactly what types implement the trait. External code cannot implement this trait since it's a sub-trait of an inaccessible trait.

The same thing can be used for types:

/// Prevent external code from "naming" `Foo`.
mod sealed {
    /// Foo.
    pub struct Foo;
    impl Foo {
        /// External code can still call me even though they
        /// can't name the type I belong to.
        pub fn can_call_me(&self) {}
    }
}
use sealed::Foo;
/// Perfectly fine to call.
pub fn callable() -> Foo {
    Foo
}

If you want to prevent this from happening, you might be able to #![deny(private_interfaces)] or look at the compiler warnings; however if you intentionally opt-into this behavior like above, then the lint won't fire.

You can also simply not make Foo pub at all; but at most, make it pub(crate).

Here's an instructive example of an intentionally sealed type.

1 Like

I don't have real issues with this behavior. I just felt it's somewhat weird that in cargo doc, we will see such type in a public interface, but won't have its document.

For the record, rustc has an unnameable_types lint that can be used to warn or error about any types like this in your code.

2 Likes

It wasn't meant to be allowed originally, but Rust didn't fix it before 1.0 (it's not easy to remove it without losing flexibility of per-module visibility within a crate, ability to re-export items, and having complex generic types).

There is a #![deny(private_in_public)] lint for this particular case, but there are still some more complex combinations that can create inaccessible types:

2 Likes

Thanks for that! I'll need to add that to my list of deny lints.