Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
/// 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 +2828

@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;

// 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,
}
}

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