Unstable attribute behavior on re-exports is surprising
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
Currently, #[unstable] on re-exports (both direct and glob ones) appears to be ignored.
This caused an issue identified in https://github.com/rust-lang/rust/issues/159856 where core::derive was unintentionally stabilized as a result of the unintuitive and non-obvious current behavior here.
This is the cause of further unintuitive (and I'd argue undesirable) behaviors like the following:
#![crate_type = "lib"]
#![crate_name = "unstable_example"]
#![allow(internal_features, unused_features)]
#![feature(staged_api, unstable_test_feature)]
#![stable(feature = "example", since = "1.0.0")]
mod implementation {
#[stable(feature = "example", since = "1.0.0")]
pub struct Example;
}
mod intermediate {
#[unstable(feature = "unstable_test_feature", issue = "none")]
pub use super::implementation::*;
}
#[stable(feature = "example", since = "1.0.0")]
pub use intermediate::*;
Here, unstable_example::Example is considered stable. This means that the unstable glob re-export inside mod intermediate is load-bearing for stability and non-breakage. That doesn't seem set up for success!
@nia-e and I bounced some ideas around, and I'd like to pitch a path forward based on what we discussed. Here's the guiding principle:
To the maximum extent possible, removing or altering #[unstable] code should not cause stable breakage.
Within a single crate, a given path's stability is resolved by (i.e., as-if) walking the path components, checking each of them for stability, and checking whether the name of that component was brought into scope by an unstable re-export. Specifically:
- names that exist in a namespace as a result of an
#[unstable]re-export are themselves considered unstable; any further#[stable]re-exports of such an unstably-re-exported item are considered unstable - if an item is re-exported into a namespace more than once and at least one of those re-exports is
#[stable], the re-export is considered stable — even if the actual#[stable]re-export is merely a glob while the unstable re-export is direct and normally would have precedence
This path-dependent stability analysis ends at crate boundaries. If one crate wants to stably re-export another crate's unstable items, it can do so — meaning e.g. core::io::Error can remain unstable while std::io::Error is still a stable re-export.
This scheme is designed to be 1-1 compatible with how we (should) treat #[doc(hidden)]. Just swap #[unstable] for #[doc(hidden)] and the same guiding principle and rules apply. This would include the "ends at crate boundaries" rule, so that a maintainer of multiple crates can choose to define non-public-API functionality in one and only commit to it in public API in another.
I believe this to be an intuitive and sensible interpretation of the goals that #[stable], #[unstable], and #[doc(hidden)] set out to achieve. I hope you agree! cargo-semver-checks can enforce this interpretation as well, of course.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
No source files, tests, or compiler entry points are named. Start by locating the stability handling for direct and glob re-exports, then trace how path components and unstable re-exports are analyzed within a crate. Done means the proposed path-dependent rules are implemented and the core::derive regression and examples in this issue behave as intended.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100