Skip to content
Open
20 changes: 12 additions & 8 deletions compiler/rustc_lint/src/levels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -969,14 +969,18 @@ where
let mut lint = Diag::new(dcx, level, msg!("unknown lint: `{$name}`"))
.with_arg("name", lint_id.lint.name_lower())
.with_note(msg!("the `{$name}` lint is unstable"));
rustc_session::diagnostics::add_feature_diagnostics_for_issue(
&mut lint,
sess,
feature,
GateIssue::Language,
lint_from_cli,
None,
);
// `staged_api` is only intended for the standard library, so don't
// suggest enabling it just to use this lint.
if feature != sym::staged_api {
rustc_session::diagnostics::add_feature_diagnostics_for_issue(
&mut lint,
sess,
feature,
GateIssue::Language,
lint_from_cli,
None,
);
}
lint
}
}
Expand Down
36 changes: 36 additions & 0 deletions compiler/rustc_lint_defs/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub mod hardwired {
FUNCTION_ITEM_REFERENCES,
HIDDEN_GLOB_REEXPORTS,
ILL_FORMED_ATTRIBUTE_INPUT,
INCOMPATIBLE_REEXPORT_STABILITY,
INCOMPLETE_INCLUDE,
INEFFECTIVE_UNSTABLE_TRAIT_IMPL,
INLINE_NO_SANITIZE,
Expand Down Expand Up @@ -2791,6 +2792,41 @@ declare_lint! {
"detects deprecation attributes with no effect",
}

declare_lint! {
/// The `incompatible_reexport_stability` lint detects stability
/// annotations on re-exports that are incompatible with the stability
/// metadata of the re-exported item.
///
/// ### Example
///
/// ```rust,compile_fail
/// #![feature(staged_api)]
/// #![stable(feature = "test", since = "1.0.0")]
///
/// #[stable(feature = "original", since = "1.0.0")]
/// pub struct S;
///
/// #[stable(feature = "different", since = "1.0.0")]
/// pub use self::S as T;
///
/// fn main() {}
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// Stability annotations on re-exports should be compatible with the
/// stability metadata of the item being re-exported. Stable metadata is
/// compared by feature and `since`, while unstable metadata is compared by
/// feature and issue. Stable re-exports of unstable definitions remain
Comment thread
clarfonthey marked this conversation as resolved.
Outdated
/// handled by the existing stability machinery.
pub INCOMPATIBLE_REEXPORT_STABILITY,
Deny,
"detects incompatible stability annotations on re-exports",
@feature_gate = staged_api;
}
Comment on lines +2795 to +2825

@mejrs mejrs Aug 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also feature gate this lint behind the staged_api feature? and add a feature gate test for it?

(If you can suppress the "use #![feature(staged_api)] .." suggestion that'd be nice - we suppress that suggestion elsewhere as well.)


declare_lint! {
/// The `ineffective_unstable_trait_impl` lint detects `#[unstable]` attributes which are not used.
///
Expand Down
4 changes: 4 additions & 0 deletions compiler/rustc_passes/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,10 @@ pub(crate) struct UnnecessaryPartialStableFeature {
#[note("see issue #55436 <https://github.com/rust-lang/rust/issues/55436> for more information")]
pub(crate) struct IneffectiveUnstableImpl;

#[derive(Diagnostic)]
#[diag("stability annotation on this re-export does not match the re-exported item")]
pub(crate) struct IncompatibleReexportStability;

@clarfonthey clarfonthey Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to provide the spans of the two attributes being checked, but it does appear that these spans aren't retained in the Stability type you seem to be comparing against. So, perhaps as a compromise, simply stating what the two stability attributes were (even debug-printing them) might be helpful in the output.

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, gonna say that debug-printing is fine for now. Since this is just an internal lint, I think it should be fine to just show raw debug output on an initial version.

One small concern is that storing the format string directly might affect perf, but will run perf before merging anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good thanks. it works for me and we can revisit the representation if the perf run shows anything concerning

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's fine


// FIXME(jdonszelmann): move back to rustc_attr
#[derive(Diagnostic)]
#[diag(
Expand Down
197 changes: 195 additions & 2 deletions compiler/rustc_passes/src/stability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ use rustc_hir::{
};
use rustc_lint_defs as lint;
use rustc_lint_defs::builtin::{
DEPRECATED, DUPLICATE_FEATURES, INEFFECTIVE_UNSTABLE_TRAIT_IMPL, STABLE_FEATURES,
DEPRECATED, DUPLICATE_FEATURES, INCOMPATIBLE_REEXPORT_STABILITY,
INEFFECTIVE_UNSTABLE_TRAIT_IMPL, STABLE_FEATURES,
};
use rustc_middle::hir::nested_filter;
use rustc_middle::metadata::Reexport;
use rustc_middle::middle::lib_features::{FeatureStability, LibFeatures};
use rustc_middle::middle::privacy::EffectiveVisibilities;
use rustc_middle::middle::stability::{AllowUnstable, Deprecated, DeprecationEntry, EvalResult};
Expand Down Expand Up @@ -523,7 +525,9 @@ impl<'tcx> Visitor<'tcx> for MissingStabilityAnnotations<'tcx> {
/// Cross-references the feature names of unstable APIs with enabled
/// features and possibly prints errors.
fn check_mod_unstable_api_usage(tcx: TyCtxt<'_>, mod_id: LocalModId) {
tcx.hir_visit_item_likes_in_module(mod_id, &mut Checker { tcx });
let mut checker = Checker { tcx, mod_id, reexport_stability: FxIndexMap::default() };
tcx.hir_visit_item_likes_in_module(mod_id, &mut checker);
checker.emit_incompatible_reexport_stability();

let is_staged_api =
tcx.sess.opts.unstable_opts.force_unstable_if_unmarked || tcx.features().staged_api();
Expand Down Expand Up @@ -553,8 +557,183 @@ pub(crate) fn provide(providers: &mut Providers) {
};
}

struct ReexportStability {
hir_id: HirId,
span: Span,
has_target: bool,
all_targets_compatible: bool,
}

struct Checker<'tcx> {
tcx: TyCtxt<'tcx>,
mod_id: LocalModId,
reexport_stability: FxIndexMap<Span, ReexportStability>,
}

impl<'tcx> Checker<'tcx> {
fn reexport_stability_attr(&self, item: &'tcx hir::Item<'tcx>) -> Option<(Stability, Span)> {
let attrs = self.tcx.hir_attrs(item.hir_id());
find_attr!(attrs, Stability { stability, span } => (*stability, *span))
}

fn stability_is_compatible(reexport: &Stability, target: &Stability) -> bool {
match (&reexport.level, &target.level) {
(
StabilityLevel::Stable { since: reexport_since, .. },
StabilityLevel::Stable { since: target_since, .. },
) => {
// Avoid another error for an invalid `since`.
matches!(
(*reexport_since, *target_since),
(StableSince::Err(_), _) | (_, StableSince::Err(_))
) || (reexport.feature == target.feature && reexport_since == target_since)
}

(
StabilityLevel::Unstable { issue: reexport_issue, .. },
StabilityLevel::Unstable { issue: target_issue, .. },
) => reexport.feature == target.feature && reexport_issue == target_issue,

// An unstable re-export cannot make a stable item unstable.
(StabilityLevel::Unstable { .. }, StabilityLevel::Stable { .. }) => false,

// Stable re-exports of unstable items are handled elsewhere.
(StabilityLevel::Stable { .. }, StabilityLevel::Unstable { .. }) => true,
}
}
Comment on lines +573 to +579

@mejrs mejrs Aug 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this span would be more helpful if it pointed to the item being re-exported

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the attribute span is only kept as the grouping key for grouped reexport for now


fn classify_reexport_targets<Id>(
&self,
own_stability: &Stability,
targets: impl IntoIterator<Item = Res<Id>>,
) -> (bool, bool) {
let mut has_target = false;
let mut all_targets_compatible = true;

for res in targets {
match res {
Res::Def(_, def_id) => {
has_target = true;

if let Some(target_stability) = self.tcx.lookup_stability(def_id)
&& !Self::stability_is_compatible(own_stability, &target_stability)
{
all_targets_compatible = false;
}
}

Res::PrimTy(_) => {
has_target = true;

// Primitives are stable and have no DefId.
if own_stability.level.is_unstable() {
all_targets_compatible = false;
}
}

// No stability metadata to compare.
_ => {}
}
}

(has_target, all_targets_compatible)
}

fn record_reexport_stability(
&mut self,
item: &'tcx hir::Item<'tcx>,
attr_span: Span,
span: Span,
has_target: bool,
all_targets_compatible: bool,
) {
let entry = self.reexport_stability.entry(attr_span).or_insert(ReexportStability {
hir_id: item.hir_id(),
span,
has_target: false,
all_targets_compatible: true,
});

entry.has_target |= has_target;

// Keep the first bad path for the diagnostic.
if entry.all_targets_compatible && !all_targets_compatible {
entry.span = span;
}

entry.all_targets_compatible &= all_targets_compatible;
}

fn check_single_reexport_stability(
&mut self,
item: &'tcx hir::Item<'tcx>,
path: &'tcx UsePath<'tcx>,
) {
let Some((own_stability, attr_span)) = self.reexport_stability_attr(item) else {
return;
};

let (has_target, all_targets_compatible) =
self.classify_reexport_targets(&own_stability, path.res.present_items());

self.record_reexport_stability(
item,
attr_span,
path.span,
has_target,
all_targets_compatible,
);
}

fn check_glob_reexport_stability(
&mut self,
item: &'tcx hir::Item<'tcx>,
path: &'tcx UsePath<'tcx>,
) {
let Some((own_stability, attr_span)) = self.reexport_stability_attr(item) else {
return;
};

let glob_def_id = item.owner_id.def_id.to_def_id();

let targets = self
.tcx
.module_children_local(self.mod_id.to_local_def_id())
.iter()
.filter(|child| {
child.reexport_chain.iter().any(|reexport| {
matches!(
*reexport,
Reexport::Glob(def_id) if def_id == glob_def_id
)
})
})
.map(|child| child.res);

let (has_target, all_targets_compatible) =
self.classify_reexport_targets(&own_stability, targets);

self.record_reexport_stability(
item,
attr_span,
path.span,
has_target,
all_targets_compatible,
);
}

fn emit_incompatible_reexport_stability(&self) {
for reexport in self.reexport_stability.values() {
if reexport.has_target && !reexport.all_targets_compatible {
self.tcx.emit_node_span_lint(
INCOMPATIBLE_REEXPORT_STABILITY,
reexport.hir_id,
reexport.span,
diagnostics::IncompatibleReexportStability,
);
}
}
}
}

impl<'tcx> Visitor<'tcx> for Checker<'tcx> {
Expand Down Expand Up @@ -583,6 +762,20 @@ impl<'tcx> Visitor<'tcx> for Checker<'tcx> {
self.tcx.check_stability(def_id, Some(item.hir_id()), item.span, None);
}

hir::ItemKind::Use(path, hir::UseKind::Single(_))
if self.tcx.features().staged_api()
&& self.tcx.local_visibility(item.owner_id.def_id).is_public() =>
{
self.check_single_reexport_stability(item, path);
}

hir::ItemKind::Use(path, hir::UseKind::Glob)
if self.tcx.features().staged_api()
&& self.tcx.local_visibility(item.owner_id.def_id).is_public() =>
{
self.check_glob_reexport_stability(item, path);
}

// For implementations of traits, check the stability of each item
// individually as it's possible to have a stable trait with unstable
// items.
Expand Down
2 changes: 2 additions & 0 deletions library/alloc/src/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#[stable(feature = "alloc_module", since = "1.28.0")]
#[doc(inline)]
#[allow(clippy::useless_attribute)]
#[allow(incompatible_reexport_stability)] // This facade has its own path stability.
pub use core::alloc::*;
use core::mem::Alignment;
use core::ptr::{self, NonNull};
Expand Down
4 changes: 4 additions & 0 deletions library/alloc/src/collections/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub mod btree_map {
//! An ordered map based on a B-Tree.
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg(not(test))]
#[allow(clippy::useless_attribute)]
#[allow(incompatible_reexport_stability)] // Public facade over internal B-tree items.
pub use super::btree::map::*;
}

Expand All @@ -29,6 +31,8 @@ pub mod btree_set {
//! An ordered set based on a B-Tree.
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg(not(test))]
#[allow(clippy::useless_attribute)]
#[allow(incompatible_reexport_stability)] // Public facade over internal B-tree items.
pub use super::btree::set::*;
}

Expand Down
4 changes: 4 additions & 0 deletions library/alloc/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -600,10 +600,14 @@ pub use core::fmt::{Arguments, write};
#[stable(feature = "rust1", since = "1.0.0")]
pub use core::fmt::{Binary, Octal};
#[stable(feature = "rust1", since = "1.0.0")]
#[allow(clippy::useless_attribute)]
#[allow(incompatible_reexport_stability)] // This re-export has its own stability.
pub use core::fmt::{Debug, Display};
#[unstable(feature = "formatting_options", issue = "118117")]
pub use core::fmt::{DebugAsHex, FormattingOptions, Sign};
#[stable(feature = "rust1", since = "1.0.0")]
#[allow(clippy::useless_attribute)]
#[allow(incompatible_reexport_stability)] // This re-export has its own stability.
pub use core::fmt::{DebugList, DebugMap, DebugSet, DebugStruct, DebugTuple};
#[stable(feature = "rust1", since = "1.0.0")]
pub use core::fmt::{Formatter, Result, Write};
Expand Down
4 changes: 4 additions & 0 deletions library/alloc/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ pub use core::io::SimpleMessage;
pub use core::io::const_error;
#[unstable(feature = "core_io_borrowed_buf", issue = "117693")]
pub use core::io::{BorrowedBuf, BorrowedCursor};
#[allow(clippy::useless_attribute)]
#[allow(incompatible_reexport_stability)] // FIXME(#161153)
#[unstable(feature = "alloc_io", issue = "154046")]
pub use core::io::{
Chain, Cursor, Empty, Error, ErrorKind, IoSlice, IoSliceMut, Repeat, Result, Seek, SeekFrom,
Expand All @@ -209,6 +211,8 @@ use core::io::{

use self::read::{append_to_string, default_read_buf_exact, default_read_exact};
use self::util::{bytes, lines, split, uninlined_slow_read_byte};
#[allow(clippy::useless_attribute)]
#[allow(incompatible_reexport_stability)] // FIXME(#161153)
#[unstable(feature = "alloc_io", issue = "154046")]
pub use self::{
buf_read::BufRead,
Expand Down
Loading
Loading