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
33 changes: 33 additions & 0 deletions compiler/rustc_lint_defs/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub mod hardwired {
HIDDEN_GLOB_REEXPORTS,
ILL_FORMED_ATTRIBUTE_INPUT,
INCOMPLETE_INCLUDE,
INEFFECTIVE_UNSTABLE_REEXPORTS,
INEFFECTIVE_UNSTABLE_TRAIT_IMPL,
INLINE_NO_SANITIZE,
INVALID_DOC_ATTRIBUTES,
Expand Down Expand Up @@ -2791,6 +2792,38 @@ declare_lint! {
"detects deprecation attributes with no effect",
}

declare_lint! {
/// The `ineffective_unstable_reexports` lint detects `#[unstable]`
/// attributes on re-exports where the attribute does not make the
/// re-exported path unstable.
///
/// ### Example
///
/// ```rust,compile_fail
/// #![feature(staged_api)]
/// #![stable(feature = "test", since = "1.0.0")]
///
/// #[stable(feature = "test", since = "1.0.0")]
/// pub struct S;
///
/// #[unstable(feature = "reexport", issue = "none")]
/// pub use self::S as T;
///
/// fn main() {}
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// `#[unstable]` on a re-export does not currently make an otherwise
/// stable re-exported path unstable.
pub INEFFECTIVE_UNSTABLE_REEXPORTS,
Deny,
"detects ineffective `#[unstable]` attributes 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("`#[unstable]` does not make this re-exported path unstable")]
pub(crate) struct IneffectiveUnstableReexport;

// FIXME(jdonszelmann): move back to rustc_attr
#[derive(Diagnostic)]
#[diag(
Expand Down
156 changes: 154 additions & 2 deletions compiler/rustc_passes/src/stability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ 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, INEFFECTIVE_UNSTABLE_REEXPORTS,
INEFFECTIVE_UNSTABLE_TRAIT_IMPL, STABLE_FEATURES,
};
use rustc_middle::hir::nested_filter;
use rustc_middle::middle::lib_features::{FeatureStability, LibFeatures};
Expand Down Expand Up @@ -523,7 +524,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, unstable_reexports: FxIndexMap::default() };
tcx.hir_visit_item_likes_in_module(mod_id, &mut checker);
checker.emit_ineffective_unstable_reexports();

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

struct UnstableReexport {
hir_id: HirId,
span: Span,
has_target: bool,
all_targets_stable: bool,
}

struct Checker<'tcx> {
tcx: TyCtxt<'tcx>,
mod_id: LocalModId,
unstable_reexports: FxIndexMap<Span, UnstableReexport>,
}

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

stability.level.is_unstable().then_some(span)
}

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

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

match self.tcx.lookup_stability(def_id) {
Some(stability) if stability.level.is_unstable() => {
all_targets_stable = false;
}
Some(_) => {}

None => {
// Items from crates without staged API metadata are
// effectively stable. Unmarked items in staged API
// crates are diagnosed by the existing stability checks.
if self.tcx.lookup_stability(def_id.krate.as_def_id()).is_some() {
all_targets_stable = false;
}
}
}
}

// Primitives are stable and have no DefId.
Res::PrimTy(_) => {
has_target = true;
}

// Do not lint if the target cannot be classified.
_ => {
all_targets_stable = false;
}
}
}

(has_target, all_targets_stable)
}

fn record_unstable_reexport(
&mut self,
item: &'tcx hir::Item<'tcx>,
attr_span: Span,
span: Span,
has_target: bool,
all_targets_stable: bool,
) {
let entry = self.unstable_reexports.entry(attr_span).or_insert(UnstableReexport {
hir_id: item.hir_id(),
span,
has_target: false,
all_targets_stable: true,
});

entry.has_target |= has_target;
entry.all_targets_stable &= all_targets_stable;
}

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

let (has_target, all_targets_stable) =
self.classify_reexport_targets(path.res.present_items());

self.record_unstable_reexport(item, attr_span, path.span, has_target, all_targets_stable);
}

fn check_glob_unstable_reexport(
&mut self,
item: &'tcx hir::Item<'tcx>,
path: &'tcx UsePath<'tcx>,
) {
let Some(attr_span) = self.unstable_reexport_span(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| reexport.id() == Some(glob_def_id))
})
.map(|child| child.res);

let (has_target, all_targets_stable) = self.classify_reexport_targets(targets);

self.record_unstable_reexport(item, attr_span, path.span, has_target, all_targets_stable);
}

fn emit_ineffective_unstable_reexports(&self) {
for reexport in self.unstable_reexports.values() {
if reexport.has_target && reexport.all_targets_stable {
self.tcx.emit_node_span_lint(
INEFFECTIVE_UNSTABLE_REEXPORTS,
reexport.hir_id,
reexport.span,
diagnostics::IneffectiveUnstableReexport,
);
}
}
}
}

impl<'tcx> Visitor<'tcx> for Checker<'tcx> {
Expand Down Expand Up @@ -583,6 +721,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_unstable_reexport(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_unstable_reexport(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
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)]
#[expect(ineffective_unstable_reexports, reason = "This re-export has its own stability")]
#[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)]
#[expect(ineffective_unstable_reexports, reason = "This re-export has its own stability")]
#[unstable(feature = "alloc_io", issue = "154046")]
pub use self::{
buf_read::BufRead,
Expand Down
2 changes: 2 additions & 0 deletions library/core/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub use self::error::RawOsError;
pub use self::error::SimpleMessage;
#[unstable(feature = "io_const_error", issue = "133448")]
pub use self::error::const_error;
#[allow(clippy::useless_attribute)]
#[expect(ineffective_unstable_reexports, reason = "This re-export has its own stability")]
#[unstable(feature = "core_io", issue = "154046")]
pub use self::{
cursor::Cursor,
Expand Down
2 changes: 2 additions & 0 deletions library/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ pub mod offload;
#[unstable(feature = "contracts", issue = "128044")]
pub mod contracts;

#[allow(clippy::useless_attribute)]
#[expect(ineffective_unstable_reexports, reason = "This re-export has its own stability")]
#[unstable(feature = "derive_macro_global_path", issue = "154645")]
pub use crate::macros::builtin::derive;
#[stable(feature = "cfg_select", since = "1.95.0")]
Expand Down
2 changes: 1 addition & 1 deletion library/core/src/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ mod unsize;
pub use self::arith::{Add, Div, Mul, Neg, Rem, Sub};
#[stable(feature = "op_assign_traits", since = "1.8.0")]
pub use self::arith::{AddAssign, DivAssign, MulAssign, RemAssign, SubAssign};
#[unstable(feature = "async_fn_traits", issue = "none")]
#[stable(feature = "async_closure", since = "1.85.0")]
pub use self::async_function::{AsyncFn, AsyncFnMut, AsyncFnOnce};
#[stable(feature = "rust1", since = "1.0.0")]
pub use self::bit::{BitAnd, BitOr, BitXor, Not, Shl, Shr};
Expand Down
6 changes: 6 additions & 0 deletions library/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,8 +712,12 @@ pub mod arch {
pub use std_detect::is_aarch64_feature_detected;
#[unstable(feature = "stdarch_arm_feature_detection", issue = "111190")]
pub use std_detect::is_arm_feature_detected;
#[allow(clippy::useless_attribute)]
#[expect(ineffective_unstable_reexports, reason = "This re-export has its own stability")]
#[unstable(feature = "is_loongarch_feature_detected", issue = "117425")]
pub use std_detect::is_loongarch_feature_detected;
#[allow(clippy::useless_attribute)]
#[expect(ineffective_unstable_reexports, reason = "This re-export has its own stability")]
#[unstable(feature = "is_riscv_feature_detected", issue = "111192")]
pub use std_detect::is_riscv_feature_detected;
#[stable(feature = "stdarch_s390x_feature_detection", since = "1.93.0")]
Expand Down Expand Up @@ -750,6 +754,8 @@ pub use core::cfg_select;
reason = "`concat_bytes` is not stable enough for use and is subject to change"
)]
pub use core::concat_bytes;
#[allow(clippy::useless_attribute)]
#[expect(ineffective_unstable_reexports, reason = "This re-export has its own stability")]
#[unstable(feature = "derive_macro_global_path", issue = "154645")]
pub use core::derive;
#[stable(feature = "matches_macro", since = "1.42.0")]
Expand Down
2 changes: 2 additions & 0 deletions library/std/src/prelude/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ pub mod rust_future {

// There are two different panic macros, one in `core` and one in `std`. They are slightly
// different. For `std` we explicitly want the one defined in `std`.
#[allow(clippy::useless_attribute)]
#[expect(ineffective_unstable_reexports, reason = "This re-export has its own stability")]
#[unstable(feature = "prelude_next", issue = "none")]
pub use super::v1::panic;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//@ check-pass
//@ normalize-stderr: "(\n)\n$" -> "$1"
// This lint is only available with `staged_api`.
#![allow(ineffective_unstable_reexports)]
//~^ WARNING unknown lint: `ineffective_unstable_reexports`

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
warning: unknown lint: `ineffective_unstable_reexports`
--> $DIR/feature-gate-ineffective_unstable_reexports.rs:4:10
|
LL | #![allow(ineffective_unstable_reexports)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: the `ineffective_unstable_reexports` lint is unstable
= note: `#[warn(unknown_lints)]` on by default

warning: 1 warning emitted
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#![crate_type = "lib"]
#![crate_name = "non_staged_reexport_source"]

pub fn stable() {}
10 changes: 10 additions & 0 deletions tests/ui/stability-attribute/auxiliary/stable-glob-source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#![crate_type = "lib"]
#![crate_name = "stable_glob_source"]
#![feature(staged_api)]
#![stable(feature = "stable_glob_source", since = "1.0.0")]

#[stable(feature = "stable_glob_source", since = "1.0.0")]
pub fn stable_a() {}

#[stable(feature = "stable_glob_source", since = "1.0.0")]
pub fn stable_b() {}
13 changes: 13 additions & 0 deletions tests/ui/stability-attribute/auxiliary/unstable-glob-source.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#![crate_type = "lib"]
#![feature(staged_api)]
#![stable(feature = "unstable_glob_source_crate", since = "1.0.0")]

#[unstable(feature = "unstable_glob_source", issue = "none")]
pub fn unstable_a() {}

#[unstable(
feature = "unstable_glob_source",
reason = "different reason",
issue = "none"
)]
pub fn unstable_b() {}
Loading
Loading