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
32 changes: 32 additions & 0 deletions compiler/rustc_lint_defs/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ pub mod hardwired {
UNUSED_MUT,
UNUSED_QUALIFICATIONS,
UNUSED_UNSAFE,
UNUSED_UNSTABLE_REEXPORT_ATTRIBUTES,
UNUSED_VARIABLES,
UNUSED_VISIBILITIES,
USELESS_DEPRECATED,
Expand Down Expand Up @@ -2791,6 +2792,37 @@ declare_lint! {
"detects deprecation attributes with no effect",
}

declare_lint! {
/// The `unused_unstable_reexport_attributes` 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 crate::S as T;
///
/// fn main() {}
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// Stability attributes on re-exports do not currently change the
/// stability of an otherwise stable re-exported item.
pub UNUSED_UNSTABLE_REEXPORT_ATTRIBUTES,
Deny,
"detects ineffective `#[unstable]` attributes on re-exports",
@feature_gate = staged_api;
}
Comment on lines +2795 to +2824

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

// FIXME(jdonszelmann): move back to rustc_attr
#[derive(Diagnostic)]
#[diag(
Expand Down
147 changes: 146 additions & 1 deletion compiler/rustc_passes/src/stability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ use rustc_hir::{
use rustc_lint_defs as lint;
use rustc_lint_defs::builtin::{
DEPRECATED, DUPLICATE_FEATURES, INEFFECTIVE_UNSTABLE_TRAIT_IMPL, STABLE_FEATURES,
UNUSED_UNSTABLE_REEXPORT_ATTRIBUTES,
};
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, unstable_reexports: FxIndexMap::default() };
tcx.hir_visit_item_likes_in_module(mod_id, &mut checker);
checker.emit_unused_unstable_reexport_attributes();

let is_staged_api =
tcx.sess.opts.unstable_opts.force_unstable_if_unmarked || tcx.features().staged_api();
Expand Down Expand Up @@ -553,8 +557,135 @@ 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))?;

matches!(stability.level, StabilityLevel::Unstable { .. }).then_some(span)
}
Comment on lines +574 to +580

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

if self.tcx.lookup_stability(def_id).is_some_and(|stab| !stab.level.is_stable())
{
all_targets_stable = false;
}
}

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

_ => {
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| {
matches!(
*reexport,
Reexport::Glob(def_id) if def_id == 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_unused_unstable_reexport_attributes(&self) {
for reexport in self.unstable_reexports.values() {
if reexport.has_target && reexport.all_targets_stable {
self.tcx.emit_node_span_lint(
UNUSED_UNSTABLE_REEXPORT_ATTRIBUTES,
reexport.hir_id,
reexport.span,
diagnostics::UnusedUnstableReexportAttributes,
);
}
}
}
}

impl<'tcx> Visitor<'tcx> for Checker<'tcx> {
Expand Down Expand Up @@ -583,6 +714,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)]
#[allow(unused_unstable_reexport_attributes)] // 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(unused_unstable_reexport_attributes)] // FIXME(#161153)
#[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)]
#[allow(unused_unstable_reexport_attributes)] // FIXME(#161153)
#[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)]
#[allow(unused_unstable_reexport_attributes)] // FIXME(#161153)
#[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: 2 additions & 0 deletions library/core/src/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ 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};
#[allow(clippy::useless_attribute)]
#[allow(unused_unstable_reexport_attributes)] // FIXME(#161153)
#[unstable(feature = "async_fn_traits", issue = "none")]
pub use self::async_function::{AsyncFn, AsyncFnMut, AsyncFnOnce};
#[stable(feature = "rust1", since = "1.0.0")]
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)]
#[allow(unused_unstable_reexport_attributes)] // FIXME(#161153)
#[unstable(feature = "is_loongarch_feature_detected", issue = "117425")]
pub use std_detect::is_loongarch_feature_detected;
#[allow(clippy::useless_attribute)]
#[allow(unused_unstable_reexport_attributes)] // FIXME(#161153)
#[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)]
#[allow(unused_unstable_reexport_attributes)] // FIXME(#161153)
#[unstable(feature = "derive_macro_global_path", issue = "154645")]
pub use core::derive;
#[stable(feature = "matches_macro", since = "1.42.0")]
Expand Down
4 changes: 4 additions & 0 deletions library/std/src/prelude/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,16 @@ pub mod rust_future {
#[doc(no_inline)]
pub use super::v1::*;

#[allow(clippy::useless_attribute)]
#[allow(unused_unstable_reexport_attributes)] // FIXME(#161153)
#[unstable(feature = "prelude_next", issue = "none")]
#[doc(no_inline)]
pub use core::prelude::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)]
#[allow(unused_unstable_reexport_attributes)] // FIXME(#161153)
#[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(unused_unstable_reexport_attributes)]
//~^ WARNING unknown lint: `unused_unstable_reexport_attributes`

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

warning: 1 warning emitted
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() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//@ aux-build:lint-stability.rs
//@ aux-build:stable-glob-source.rs
//@ normalize-stderr: "(\n)\n$" -> "$1"

#![crate_type = "lib"]
#![feature(staged_api)]
#![stable(feature = "reexport_test", since = "1.0.0")]

extern crate lint_stability;
extern crate stable_glob_source;

// Every item introduced by this glob is stable, so the unstable annotation
// cannot make the exported paths unstable.
#[unstable(feature = "stable_glob_reexport", issue = "none")]
pub use stable_glob_source::*; //~ ERROR `#[unstable]` does not make this re-exported path unstable

// This glob contains unstable items, so #94972 makes the annotation
// relevant when checking the import itself.
#[unstable(feature = "unstable_test_feature", issue = "none")]
pub use lint_stability::*;
Loading
Loading