Rollup merge of #153335 - Ozzy1423:removed-features, r=jdonszelmann

Add #![unstable_removed(..)] attribute to track removed features

Adds the #![unstable_removed(..)] attribute to enable tracking removed library features.
Produce an error when a removed attribute is used.
Add a test that it works.

For https://github.com/rust-lang/rust/issues/141617

r? @jyn514
This commit is contained in:
Jonathan Brouwer
2026-04-13 14:02:30 +02:00
committed by GitHub
18 changed files with 292 additions and 20 deletions
@@ -2,6 +2,7 @@
use rustc_errors::ErrorGuaranteed;
use rustc_feature::ACCEPTED_LANG_FEATURES;
use rustc_hir::attrs::UnstableRemovedFeature;
use rustc_hir::target::GenericParamKind;
use rustc_hir::{
DefaultBodyStability, MethodKind, PartialConstStability, Stability, StabilityLevel,
@@ -476,3 +477,89 @@ pub(crate) fn parse_unstability<S: Stage>(
(Err(ErrorGuaranteed { .. }), _) | (_, Err(ErrorGuaranteed { .. })) => None,
}
}
pub(crate) struct UnstableRemovedParser;
impl<S: Stage> CombineAttributeParser<S> for UnstableRemovedParser {
type Item = UnstableRemovedFeature;
const PATH: &[Symbol] = &[sym::unstable_removed];
const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
const TEMPLATE: AttributeTemplate =
template!(List: &[r#"feature = "name", reason = "...", link = "...", since = "version""#]);
const CONVERT: ConvertFn<Self::Item> = |items, _| AttributeKind::UnstableRemoved(items);
fn extend(
cx: &mut AcceptContext<'_, '_, S>,
args: &ArgParser,
) -> impl IntoIterator<Item = Self::Item> {
let mut feature = None;
let mut reason = None;
let mut link = None;
let mut since = None;
if !cx.features().staged_api() {
cx.emit_err(session_diagnostics::StabilityOutsideStd { span: cx.attr_span });
return None;
}
let ArgParser::List(list) = args else {
let attr_span = cx.attr_span;
cx.adcx().expected_list(attr_span, args);
return None;
};
for param in list.mixed() {
let Some(param) = param.meta_item() else {
cx.adcx().expected_not_literal(param.span());
return None;
};
let Some(word) = param.path().word() else {
cx.adcx().expected_specific_argument(
param.span(),
&[sym::feature, sym::reason, sym::link, sym::since],
);
return None;
};
match word.name {
sym::feature => insert_value_into_option_or_error(cx, &param, &mut feature, word)?,
sym::since => insert_value_into_option_or_error(cx, &param, &mut since, word)?,
sym::reason => insert_value_into_option_or_error(cx, &param, &mut reason, word)?,
sym::link => insert_value_into_option_or_error(cx, &param, &mut link, word)?,
_ => {
cx.adcx().expected_specific_argument(
param.span(),
&[sym::feature, sym::reason, sym::link, sym::since],
);
return None;
}
}
}
// Check all the arguments are present
let Some(feature) = feature else {
cx.adcx().missing_name_value(list.span, sym::feature);
return None;
};
let Some(reason) = reason else {
cx.adcx().missing_name_value(list.span, sym::reason);
return None;
};
let Some(link) = link else {
cx.adcx().missing_name_value(list.span, sym::link);
return None;
};
let Some(since) = since else {
cx.adcx().missing_name_value(list.span, sym::since);
return None;
};
let Some(version) = parse_version(since) else {
cx.emit_err(session_diagnostics::InvalidSince { span: cx.attr_span });
return None;
};
Some(UnstableRemovedFeature { feature, reason, link, since: version })
}
}
@@ -179,6 +179,7 @@ mod late {
Combine<RustcThenThisWouldNeedParser>,
Combine<TargetFeatureParser>,
Combine<UnstableFeatureBoundParser>,
Combine<UnstableRemovedParser>,
// tidy-alphabetical-end
// tidy-alphabetical-start
@@ -776,6 +777,11 @@ pub(crate) fn expected_name_value(
self.emit_parse_error(span, AttributeParseErrorReason::ExpectedNameValue(name))
}
/// Emit an error that a `name = value` argument is missing in a list of name-value pairs.
pub(crate) fn missing_name_value(&mut self, span: Span, name: Symbol) -> ErrorGuaranteed {
self.emit_parse_error(span, AttributeParseErrorReason::MissingNameValue(name))
}
/// Emit an error that a `name = value` pair was found where that name was already seen.
pub(crate) fn duplicate_key(&mut self, span: Span, key: Symbol) -> ErrorGuaranteed {
self.emit_parse_error(span, AttributeParseErrorReason::DuplicateKey(key))
@@ -568,6 +568,7 @@ pub(crate) enum AttributeParseErrorReason<'a> {
ExpectedNonEmptyStringLiteral,
ExpectedNotLiteral,
ExpectedNameValue(Option<Symbol>),
MissingNameValue(Symbol),
DuplicateKey(Symbol),
ExpectedSpecificArgument {
possibilities: &'a [Symbol],
@@ -823,6 +824,9 @@ fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, G> {
format!("expected this to be of the form `{name} = \"...\"`"),
);
}
AttributeParseErrorReason::MissingNameValue(name) => {
diag.span_label(self.span, format!("missing argument `{name} = \"...\"`"));
}
AttributeParseErrorReason::ExpectedSpecificArgument {
possibilities,
strings,
@@ -928,6 +928,11 @@ pub struct BuiltinAttribute {
unstable_feature_bound, Normal, template!(Word, List: &["feat1, feat2, ..."]),
DuplicatesOk, EncodeCrossCrate::No,
),
ungated!(
unstable_removed, CrateLevel,
template!(List: &[r#"feature = "name", reason = "...", link = "...", since = "version""#]),
DuplicatesOk, EncodeCrossCrate::Yes
),
ungated!(
rustc_const_unstable, Normal, template!(List: &[r#"feature = "name""#]),
DuplicatesOk, EncodeCrossCrate::Yes
-14
View File
@@ -310,18 +310,4 @@ macro_rules! declare_features {
// -------------------------------------------------------------------------
// feature-group-end: removed features
// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// feature-group-start: removed library features
// -------------------------------------------------------------------------
//
// FIXME(#141617): we should have a better way to track removed library features, but we reuse
// the infrastructure here so users still get hints. The symbols used here can be remove from
// `symbol.rs` when that happens.
(removed, concat_idents, "1.90.0", Some(29599),
Some("use the `${concat(..)}` metavariable expression instead"), 142704),
// -------------------------------------------------------------------------
// feature-group-end: removed library features
// -------------------------------------------------------------------------
);
@@ -894,6 +894,14 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
}
}
#[derive(Clone, Debug, HashStable_Generic, Encodable, Decodable, PrintAttribute)]
pub struct UnstableRemovedFeature {
pub feature: Symbol,
pub reason: Symbol,
pub link: Symbol,
pub since: RustcVersion,
}
/// Represents parsed *built-in* inert attributes.
///
/// ## Overview
@@ -1648,6 +1656,9 @@ pub enum AttributeKind {
/// Represents `#[unstable_feature_bound]`.
UnstableFeatureBound(ThinVec<(Symbol, Span)>),
/// Represents all `#![unstable_removed(...)]` features
UnstableRemoved(ThinVec<UnstableRemovedFeature>),
/// Represents `#[used]`
Used {
used_by: UsedBy,
@@ -199,6 +199,7 @@ pub fn encode_cross_crate(&self) -> EncodeCrossCrate {
TrackCaller(..) => Yes,
TypeLengthLimit { .. } => No,
UnstableFeatureBound(..) => No,
UnstableRemoved(..) => Yes,
Used { .. } => No,
WindowsSubsystem(..) => No,
// tidy-alphabetical-end
+1
View File
@@ -385,6 +385,7 @@ fn check_attributes(
| AttributeKind::ThreadLocal
| AttributeKind::TypeLengthLimit { .. }
| AttributeKind::UnstableFeatureBound(..)
| AttributeKind::UnstableRemoved(..)
| AttributeKind::Used { .. }
| AttributeKind::WindowsSubsystem(..)
// tidy-alphabetical-end
+14
View File
@@ -895,6 +895,20 @@ pub(crate) struct ImpliedFeatureNotExist {
pub implied_by: Symbol,
}
#[derive(Diagnostic)]
#[diag("feature `{$feature}` has been removed", code = E0557)]
#[note("removed in {$since}; see <{$link}> for more information")]
#[note("{$reason}")]
pub(crate) struct FeatureRemoved {
#[primary_span]
#[label("feature has been removed")]
pub span: Span,
pub feature: Symbol,
pub reason: Symbol,
pub since: String,
pub link: Symbol,
}
#[derive(Diagnostic)]
#[diag(
"attributes `#[rustc_const_unstable]`, `#[rustc_const_stable]` and `#[rustc_const_stable_indirect]` require the function or method to be `const`"
+27 -5
View File
@@ -1097,7 +1097,7 @@ fn check_features<'tcx>(
let lang_features =
UNSTABLE_LANG_FEATURES.iter().map(|feature| feature.name).collect::<Vec<_>>();
let lib_features = crates
.into_iter()
.iter()
.flat_map(|&cnum| {
tcx.lib_features(cnum).stability.keys().copied().into_sorted_stable_ord()
})
@@ -1105,11 +1105,33 @@ fn check_features<'tcx>(
let valid_feature_names = [lang_features, lib_features].concat();
// Collect all of the marked as "removed" features
let unstable_removed_features = crates
.iter()
.flat_map(|&cnum| {
find_attr!(tcx, cnum.as_def_id(), UnstableRemoved(rem_features) => rem_features)
.into_iter()
.flatten()
})
.collect::<Vec<_>>();
for (feature, span) in remaining_lib_features {
let suggestion = feature
.find_similar(&valid_feature_names)
.map(|(actual_name, _)| errors::MisspelledFeature { span, actual_name });
tcx.dcx().emit_err(errors::UnknownFeature { span, feature, suggestion });
if let Some(removed) =
unstable_removed_features.iter().find(|removed| removed.feature == feature)
{
tcx.dcx().emit_err(errors::FeatureRemoved {
span,
feature,
reason: removed.reason,
link: removed.link,
since: removed.since.to_string(),
});
} else {
let suggestion = feature
.find_similar(&valid_feature_names)
.map(|(actual_name, _)| errors::MisspelledFeature { span, actual_name });
tcx.dcx().emit_err(errors::UnknownFeature { span, feature, suggestion });
}
}
}
}
+1 -1
View File
@@ -641,7 +641,6 @@
compiler_move,
concat,
concat_bytes,
concat_idents,
conservative_impl_trait,
console,
const_allocate,
@@ -2185,6 +2184,7 @@
unstable_location_reason_default: "this crate is being loaded from the sysroot, an \
unstable location; did you mean to load this crate \
from crates.io via `Cargo.toml` instead?",
unstable_removed,
untagged_unions,
unused_imports,
unwind,
+7
View File
@@ -418,6 +418,13 @@
// tidy-alphabetical-end
//
#![default_lib_allocator]
// Removed features
#![unstable_removed(
feature = "concat_idents",
reason = "Replaced by the macro_metavar_expr_concat feature",
link = "https://github.com/rust-lang/rust/issues/29599#issuecomment-2986866250",
since = "1.90.0"
)]
// The Rust prelude
// The compiler expects the prelude definition to be defined before its use statement.
+10
View File
@@ -23,6 +23,9 @@ The `unstable` attribute infects all sub-items, where the attribute doesn't
have to be reapplied. So if you apply this to a module, all items in the module
will be unstable.
If you rename a feature, you can add `old_name = "old_name"` to produce a
useful error message.
You can make specific sub-items stable by using the `#[stable]` attribute on
them. The stability scheme works similarly to how `pub` works. You can have
public functions of nonpublic modules and you can have stable functions in
@@ -189,4 +192,11 @@ Currently, the items that can be annotated with `#[unstable_feature_bound]` are:
- free function
- trait
## renamed and removed features
Unstable features can get renamed and removed. If you rename a feature, you can add `old_name = "old_name"` to the `#[unstable]` attribute.
If you remove a feature, the `#!unstable_removed(feature = "foo", reason = "brief description", link = "link", since = "1.90.0")`
attribute should be used to produce a good error message for users of the removed feature.
The `link` field can be used to link to the most relevant information on the removal of the feature such as a GitHub issue, comment or PR.
[blog]: https://www.ralfj.de/blog/2018/07/19/const.html
@@ -0,0 +1,9 @@
#![feature(staged_api)]
#![stable(feature = "unstable_removed_test", since = "1.0.0")]
#![unstable_removed(
feature="old_feature",
reason="deprecated",
link="https://github.com/rust-lang/rust/issues/141617",
since="1.92.0"
)]
@@ -0,0 +1,16 @@
#![feature(staged_api)]
#![unstable_removed(feature = "old_feature")]
//~^ ERROR: malformed `unstable_removed` attribute
#![unstable_removed(invalid = "old_feature")]
//~^ ERROR: malformed `unstable_removed` attribute
#![unstable_removed("invalid literal")]
//~^ ERROR: malformed `unstable_removed` attribute
#![unstable_removed = "invalid literal"]
//~^ ERROR: malformed `unstable_removed` attribute
#![stable(feature="main", since="1.0.0")]
fn main() {}
@@ -0,0 +1,40 @@
error[E0539]: malformed `unstable_removed` attribute input
--> $DIR/malformed-unstable-removed.rs:3:1
|
LL | #![unstable_removed(feature = "old_feature")]
| ^^^^^^^^^^^^^^^^^^^-------------------------^
| | |
| | missing argument `reason = "..."`
| help: must be of the form: `#![unstable_removed(feature = "name", reason = "...", link = "...", since = "version")]`
error[E0539]: malformed `unstable_removed` attribute input
--> $DIR/malformed-unstable-removed.rs:6:1
|
LL | #![unstable_removed(invalid = "old_feature")]
| ^^^^^^^^^^^^^^^^^^^^-----------------------^^
| | |
| | valid arguments are `feature`, `reason`, `link` or `since`
| help: must be of the form: `#![unstable_removed(feature = "name", reason = "...", link = "...", since = "version")]`
error[E0565]: malformed `unstable_removed` attribute input
--> $DIR/malformed-unstable-removed.rs:9:1
|
LL | #![unstable_removed("invalid literal")]
| ^^^^^^^^^^^^^^^^^^^^-----------------^^
| | |
| | didn't expect a literal here
| help: must be of the form: `#![unstable_removed(feature = "name", reason = "...", link = "...", since = "version")]`
error[E0539]: malformed `unstable_removed` attribute input
--> $DIR/malformed-unstable-removed.rs:12:1
|
LL | #![unstable_removed = "invalid literal"]
| ^^^^^^^^^^^^^^^^^^^^-------------------^
| | |
| | expected this to be a list
| help: must be of the form: `#![unstable_removed(feature = "name", reason = "...", link = "...", since = "version")]`
error: aborting due to 4 previous errors
Some errors have detailed explanations: E0539, E0565.
For more information about an error, try `rustc --explain E0539`.
+19
View File
@@ -0,0 +1,19 @@
//@ aux-build:unstable_removed_feature.rs
#![feature(old_feature)]
//~^ ERROR: feature `old_feature` has been removed
#![feature(concat_idents)]
//~^ ERROR: feature `concat_idents` has been removed
#![unstable_removed(
//~^ ERROR: stability attributes may not be used outside of the standard library
feature = "old_feature",
reason = "a good one",
link = "https://github.com/rust-lang/rust/issues/141617",
since="1.92.0"
)]
extern crate unstable_removed_feature;
fn main() {}
@@ -0,0 +1,34 @@
error[E0734]: stability attributes may not be used outside of the standard library
--> $DIR/unstable_removed.rs:9:1
|
LL | / #![unstable_removed(
LL | |
LL | | feature = "old_feature",
LL | | reason = "a good one",
LL | | link = "https://github.com/rust-lang/rust/issues/141617",
LL | | since="1.92.0"
LL | | )]
| |__^
error[E0557]: feature `old_feature` has been removed
--> $DIR/unstable_removed.rs:3:12
|
LL | #![feature(old_feature)]
| ^^^^^^^^^^^ feature has been removed
|
= note: removed in 1.92.0; see <https://github.com/rust-lang/rust/issues/141617> for more information
= note: deprecated
error[E0557]: feature `concat_idents` has been removed
--> $DIR/unstable_removed.rs:6:12
|
LL | #![feature(concat_idents)]
| ^^^^^^^^^^^^^ feature has been removed
|
= note: removed in 1.90.0; see <https://github.com/rust-lang/rust/issues/29599#issuecomment-2986866250> for more information
= note: Replaced by the macro_metavar_expr_concat feature
error: aborting due to 3 previous errors
Some errors have detailed explanations: E0557, E0734.
For more information about an error, try `rustc --explain E0557`.