Skip to content
Merged
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
34 changes: 34 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,39 @@ 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
///
#[cfg_attr(bootstrap, doc = "```rust,ignore")]
#[cfg_attr(not(bootstrap), doc = "```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() {}
#[doc = "```"]
///
#[cfg_attr(not(bootstrap), doc = "{{produces}}")]
///
/// ### Explanation
///
/// `#[unstable]` on a re-export does not make a stable path unstable
/// re-exports inside unstable modules are already on an unstable path
pub INEFFECTIVE_UNSTABLE_REEXPORTS,
Deny,
"detects ineffective `#[unstable]` attributes on re-exports",
@feature_gate = staged_api;
}
Comment thread
amirHdev marked this conversation as resolved.

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
168 changes: 166 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,155 @@ 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 containing_module_is_unstable(&self) -> bool {
self.tcx
.lookup_stability(self.mod_id.to_local_def_id())
.is_some_and(|stability| stability.level.is_unstable())
}

fn emit_ineffective_unstable_reexports(&self) {
// an unstable module already makes its re-exports unstable
// keep the explicit annotation without linting it as ineffective
if self.unstable_reexports.is_empty() || self.containing_module_is_unstable() {
return;
}

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 +733,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
5 changes: 5 additions & 0 deletions library/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ pub mod offload;
#[unstable(feature = "contracts", issue = "128044")]
pub mod contracts;

#[allow(clippy::useless_attribute)]
#[expect(
ineffective_unstable_reexports,
reason = "accepted as stable after accidental stabilization in 1.96, see #154645"
)]
#[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
9 changes: 7 additions & 2 deletions library/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,9 +712,9 @@ 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;
#[unstable(feature = "is_loongarch_feature_detected", issue = "117425")]
#[stable(feature = "stdarch_loongarch_feature", since = "1.89.0")]
pub use std_detect::is_loongarch_feature_detected;
#[unstable(feature = "is_riscv_feature_detected", issue = "111192")]
#[stable(feature = "riscv_ratified", since = "1.78.0")]
pub use std_detect::is_riscv_feature_detected;
#[stable(feature = "stdarch_s390x_feature_detection", since = "1.93.0")]
pub use std_detect::is_s390x_feature_detected;
Expand Down Expand Up @@ -750,6 +750,11 @@ 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 = "accepted as stable after accidental stabilization in 1.96, see #154645"
)]
#[unstable(feature = "derive_macro_global_path", issue = "154645")]
pub use core::derive;
Comment thread
amirHdev marked this conversation as resolved.
#[stable(feature = "matches_macro", since = "1.42.0")]
Expand Down
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() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//@ aux-build:stable-glob-source.rs
//@ aux-build:unstable-glob-source.rs
//@ normalize-stderr: "(\n)\n$" -> "$1"

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

extern crate stable_glob_source;
extern crate unstable_glob_source;

// The unstable annotation is ineffective because every target is stable.
#[unstable(feature = "stable_glob_reexport", issue = "none")]
pub use stable_glob_source::*;
//~^ ERROR `#[unstable]` does not make this re-exported path unstable

// The annotation remains meaningful because these targets are unstable.
#[unstable(feature = "unstable_glob_source", issue = "none")]
pub use unstable_glob_source::*;
Loading
Loading