# Re-exporting an enum with a type alias is breaking, but not major

_Published: 2023-03-06_

We've already explored some of the dark corners of Rust semantic versioning on this blog:

- [Moving and re-exporting a Rust type can be a major breaking change](https://predr.ag/blog/moving-and-reexporting-rust-type-can-be-major-breaking-change/)
- [Turning a Rust struct into an enum is not always a major breaking change](https://predr.ag/blog/turning-rust-struct-to-enum-is-not-always-breaking/)
- [Some Rust breaking changes don't require a major version](https://predr.ag/blog/some-rust-breaking-changes-do-not-require-major-version/)

I recently [learned of a new semver hazard](https://github.com/obi1kenobi/cargo-semver-checks/issues/413) related to re-exporting, thanks to a tip from [aDotInTheVoid](https://github.com/aDotInTheVoid/). [The last re-exporting hazard explored on this blog](https://predr.ag/blog/moving-and-reexporting-rust-type-can-be-major-breaking-change/) was breaking and major; this one is breaking but appears to not be major.

## Re-exporting an enum with a type alias

Rust allows items to be [re-exported via two mechanisms](https://predr.ag/blog/moving-and-reexporting-rust-type-can-be-major-breaking-change/#moving-and-re-exporting-an-item): by adding a new name via `pub use`, or by adding a [type alias](https://doc.rust-lang.org/reference/items/type-aliases.html) (also called "typedef") via `pub type`. These two approaches are similar, but not identical — a [previous post](https://predr.ag/blog/moving-and-reexporting-rust-type-can-be-major-breaking-change/) already explored a distinction between them that [can cause an unexpected major breaking change](https://predr.ag/blog/moving-and-reexporting-rust-type-can-be-major-breaking-change/#accidental-major-breaking-change-via-pub-type).

Say our `example` crate has the following code:
```rust
// in `example/src/lib.rs`:
pub enum Foo {
    A,
    B,
}
```

A subsequent version of `example` updates the code to this:
```rust
// in example/src/lib.rs
mod inner {
    pub enum RenamedFoo {
        A,
        B,
    }
}

pub type Foo = inner::RenamedFoo;
```

At first glance, this seems like it should behave identically: `example::Foo` is still an enum with the same variants as before, and `example::Foo::A` syntax works just fine.

But consider the following code in a downstream crate:
```rust
// in downstream/src/lib.rs
fn produce_foo(x: i64) -> example::Foo {
    use example::Foo::*;
    if x > 5 {
        A
    } else {
        B
    }
}
```

Surprsingly, this code is now broken:
```rust_errors
error[E0432]: unresolved import `example::Foo`
  --> src/lib.rs:15:9
   |
15 |     use example::Foo::*;
   |                  ^^^ `Foo` is a type alias, not a module
```

Replacing the enum with a type alias is a breaking change!

## But is it a major change?

[Some Rust breaking changes don't require a major version.](https://predr.ag/blog/some-rust-breaking-changes-do-not-require-major-version/) Does this one?

I believe it does not.

The Rust [API evolution RFC](https://rust-lang.github.io/rfcs/1105-api-evolution.html#principles-of-the-policy) says the following about breaking changes that are not semver-major: (emphasis in original)
> That means that any breakage in a minor release must be very "shallow": it must always be possible to locally fix the problem through some kind of disambiguation _that could have been done in advance_ (by using more explicit forms) or other annotation (like disabling a lint).
>
> Source: [RFC 1105: API evolution, Principles of the Policy](https://rust-lang.github.io/rfcs/1105-api-evolution.html#principles-of-the-policy)

In this case, the code could have been altered in advance in a way that would have avoided the breakage:
```rust
// in downstream/src/lib.rs
fn produce_foo(x: i64) -> example::Foo {
    if x > 5 {
        example::Foo::A
    } else {
        example::Foo::B
    }
}
```

## Replacing the glob with direct imports doesn't solve the problem

The last time [we saw a breaking change related to imports](https://predr.ag/blog/some-rust-breaking-changes-do-not-require-major-version/#adding-a-new-public-item-is-technically-a-breaking-change), replacing the glob import with direct imports of the contained items avoided the problem. This time we are not so lucky.

This code:
```rust
// in downstream/src/lib.rs
fn produce_foo(x: i64) -> example::Foo {
    use example::Foo::{A, B};
    if x > 5 {
        A
    } else {
        B
    }
}
```
produces a similar error as before:
```rust_errors
error[E0432]: unresolved import `Foo`
  --> src/lib.rs:15:9
   |
15 |     use example::Foo::{A, B};
   |                  ^^^ `Foo` is a type alias, not a module
```

To avoid this breaking change, we can import the *enum* (or its type alias) but we must not import the *variants*.

## Is this working as intended?

Whether this behavior is expected and intentional, or an ergonomics issue to be fixed, is [an open question at the moment](https://github.com/rust-lang/rust/issues/73191).
A future Rust edition may make `pub use` and `pub type` equivalent.
In the meantime, this is yet another breaking change that does not require a new major version.

*Thanks to [aDotInTheVoid](https://github.com/aDotInTheVoid/) for reviewing a draft of this post. Any mistakes are mine alone.*

Copyright (C) Predrag Gruevski 2023. [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
