Allow attribute parsers to specify a list of allowed targets

Every acceptor gets an `ALLOWED_TARGETS` specification which can specify per target whether it is allowed, warned, or errored.
This commit is contained in:
Jonathan Brouwer 2025-08-09 20:37:32 +02:00
parent e7ef23e90e
commit 744d39ebe6
No known key found for this signature in database
GPG key ID: 13619B051B673C52
8 changed files with 304 additions and 13 deletions

View file

@ -3480,6 +3480,7 @@ dependencies = [
name = "rustc_attr_parsing"
version = "0.0.0"
dependencies = [
"itertools",
"rustc_abi",
"rustc_ast",
"rustc_ast_pretty",

View file

@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
# tidy-alphabetical-start
itertools = "0.12"
rustc_abi = { path = "../rustc_abi" }
rustc_ast = { path = "../rustc_ast" }
rustc_ast_pretty = { path = "../rustc_ast_pretty" }

View file

@ -10,6 +10,12 @@ attr_parsing_empty_attribute =
unused attribute
.suggestion = remove this attribute
attr_parsing_invalid_target = `#[{$name}]` attribute cannot be used on {$target}
.help = `#[{$name}]` can {$only}be applied to {$applied}
attr_parsing_invalid_target_lint = `#[{$name}]` attribute cannot be used on {$target}
.warn = {-attr_parsing_previously_accepted}
.help = `#[{$name}]` can {$only}be applied to {$applied}
attr_parsing_empty_confusables =
expected at least one confusable name
attr_parsing_expected_one_cfg_pattern =

View file

@ -21,7 +21,7 @@ use rustc_hir::attrs::AttributeKind;
use rustc_span::{Span, Symbol};
use thin_vec::ThinVec;
use crate::context::{AcceptContext, FinalizeContext, Stage};
use crate::context::{AcceptContext, AllowedTargets, FinalizeContext, Stage};
use crate::parser::ArgParser;
use crate::session_diagnostics::UnusedMultiple;
@ -80,6 +80,8 @@ pub(crate) trait AttributeParser<S: Stage>: Default + 'static {
/// If an attribute has this symbol, the `accept` function will be called on it.
const ATTRIBUTES: AcceptMapping<Self, S>;
const ALLOWED_TARGETS: AllowedTargets;
/// The parser has gotten a chance to accept the attributes on an item,
/// here it can produce an attribute.
///
@ -116,6 +118,8 @@ pub(crate) trait SingleAttributeParser<S: Stage>: 'static {
/// and this specified whether to, for example, warn or error on the other one.
const ON_DUPLICATE: OnDuplicate<S>;
const ALLOWED_TARGETS: AllowedTargets;
/// The template this attribute parser should implement. Used for diagnostics.
const TEMPLATE: AttributeTemplate;
@ -163,6 +167,7 @@ impl<T: SingleAttributeParser<S>, S: Stage> AttributeParser<S> for Single<T, S>
}
},
)];
const ALLOWED_TARGETS: AllowedTargets = T::ALLOWED_TARGETS;
fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
Some(self.1?.0)
@ -247,6 +252,7 @@ pub(crate) enum AttributeOrder {
pub(crate) trait NoArgsAttributeParser<S: Stage>: 'static {
const PATH: &[Symbol];
const ON_DUPLICATE: OnDuplicate<S>;
const ALLOWED_TARGETS: AllowedTargets;
/// Create the [`AttributeKind`] given attribute's [`Span`].
const CREATE: fn(Span) -> AttributeKind;
@ -264,6 +270,7 @@ impl<T: NoArgsAttributeParser<S>, S: Stage> SingleAttributeParser<S> for Without
const PATH: &[Symbol] = T::PATH;
const ATTRIBUTE_ORDER: AttributeOrder = AttributeOrder::KeepOutermost;
const ON_DUPLICATE: OnDuplicate<S> = T::ON_DUPLICATE;
const ALLOWED_TARGETS: AllowedTargets = T::ALLOWED_TARGETS;
const TEMPLATE: AttributeTemplate = template!(Word);
fn convert(cx: &mut AcceptContext<'_, '_, S>, args: &ArgParser<'_>) -> Option<AttributeKind> {
@ -293,6 +300,8 @@ pub(crate) trait CombineAttributeParser<S: Stage>: 'static {
/// where `x` is a vec of these individual reprs.
const CONVERT: ConvertFn<Self::Item>;
const ALLOWED_TARGETS: AllowedTargets;
/// The template this attribute parser should implement. Used for diagnostics.
const TEMPLATE: AttributeTemplate;
@ -324,15 +333,13 @@ impl<T: CombineAttributeParser<S>, S: Stage> Default for Combine<T, S> {
}
impl<T: CombineAttributeParser<S>, S: Stage> AttributeParser<S> for Combine<T, S> {
const ATTRIBUTES: AcceptMapping<Self, S> = &[(
T::PATH,
<T as CombineAttributeParser<S>>::TEMPLATE,
|group: &mut Combine<T, S>, cx, args| {
const ATTRIBUTES: AcceptMapping<Self, S> =
&[(T::PATH, T::TEMPLATE, |group: &mut Combine<T, S>, cx, args| {
// Keep track of the span of the first attribute, for diagnostics
group.first_span.get_or_insert(cx.attr_span);
group.items.extend(T::extend(cx, args))
},
)];
})];
const ALLOWED_TARGETS: AllowedTargets = T::ALLOWED_TARGETS;
fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
if let Some(first_span) = self.first_span {

View file

@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use std::sync::LazyLock;
use itertools::Itertools;
use private::Sealed;
use rustc_ast::{self as ast, LitKind, MetaItemLit, NodeId};
use rustc_errors::{DiagCtxtHandle, Diagnostic};
@ -63,8 +64,11 @@ use crate::attributes::traits::{
};
use crate::attributes::transparency::TransparencyParser;
use crate::attributes::{AttributeParser as _, Combine, Single, WithoutArgs};
use crate::context::MaybeWarn::{Allow, Error, Warn};
use crate::parser::{ArgParser, MetaItemParser, PathParser};
use crate::session_diagnostics::{AttributeParseError, AttributeParseErrorReason, UnknownMetaItem};
use crate::session_diagnostics::{
AttributeParseError, AttributeParseErrorReason, InvalidTarget, UnknownMetaItem,
};
type GroupType<S> = LazyLock<GroupTypeInner<S>>;
@ -76,6 +80,7 @@ struct GroupTypeInner<S: Stage> {
struct GroupTypeInnerAccept<S: Stage> {
template: AttributeTemplate,
accept_fn: AcceptFn<S>,
allowed_targets: AllowedTargets,
}
type AcceptFn<S> =
@ -123,7 +128,8 @@ macro_rules! attribute_parsers {
STATE_OBJECT.with_borrow_mut(|s| {
accept_fn(s, cx, args)
})
})
}),
allowed_targets: <$names as crate::attributes::AttributeParser<$stage>>::ALLOWED_TARGETS,
});
}
@ -645,6 +651,64 @@ impl ShouldEmit {
}
}
#[derive(Debug)]
pub(crate) enum AllowedTargets {
AllowList(&'static [MaybeWarn]),
AllowListWarnRest(&'static [MaybeWarn]),
}
pub(crate) enum AllowedResult {
Allowed,
Warn,
Error,
}
impl AllowedTargets {
pub(crate) fn is_allowed(&self, target: Target) -> AllowedResult {
match self {
AllowedTargets::AllowList(list) => {
if list.contains(&Allow(target)) {
AllowedResult::Allowed
} else if list.contains(&Warn(target)) {
AllowedResult::Warn
} else {
AllowedResult::Error
}
}
AllowedTargets::AllowListWarnRest(list) => {
if list.contains(&Allow(target)) {
AllowedResult::Allowed
} else if list.contains(&Error(target)) {
AllowedResult::Error
} else {
AllowedResult::Warn
}
}
}
}
pub(crate) fn allowed_targets(&self) -> Vec<Target> {
match self {
AllowedTargets::AllowList(list) => list,
AllowedTargets::AllowListWarnRest(list) => list,
}
.iter()
.filter_map(|target| match target {
Allow(target) => Some(*target),
Warn(_) => None,
Error(_) => None,
})
.collect()
}
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum MaybeWarn {
Allow(Target),
Warn(Target),
Error(Target),
}
/// Context created once, for example as part of the ast lowering
/// context, through which all attributes can be lowered.
pub struct AttributeParser<'sess, S: Stage = Late> {
@ -852,7 +916,48 @@ impl<'sess, S: Stage> AttributeParser<'sess, S> {
attr_path: path.get_attribute_path(),
};
(accept.accept_fn)(&mut cx, args)
(accept.accept_fn)(&mut cx, args);
if self.stage.should_emit().should_emit() {
match accept.allowed_targets.is_allowed(target) {
AllowedResult::Allowed => {}
AllowedResult::Warn => {
let allowed_targets =
accept.allowed_targets.allowed_targets();
let (applied, only) = allowed_targets_applied(
allowed_targets,
target,
self.features,
);
emit_lint(AttributeLint {
id: target_id,
span: attr.span,
kind: AttributeLintKind::InvalidTarget {
name: parts[0],
target,
only: if only { "only " } else { "" },
applied,
},
});
}
AllowedResult::Error => {
let allowed_targets =
accept.allowed_targets.allowed_targets();
let (applied, only) = allowed_targets_applied(
allowed_targets,
target,
self.features,
);
self.dcx().emit_err(InvalidTarget {
span: attr.span,
name: parts[0],
target: target.plural_name(),
only: if only { "only " } else { "" },
applied,
});
}
}
}
}
} else {
// If we're here, we must be compiling a tool attribute... Or someone
@ -940,6 +1045,132 @@ impl<'sess, S: Stage> AttributeParser<'sess, S> {
}
}
/// Takes a list of `allowed_targets` for an attribute, and the `target` the attribute was applied to.
/// Does some heuristic-based filtering to remove uninteresting targets, and formats the targets into a string
pub(crate) fn allowed_targets_applied(
mut allowed_targets: Vec<Target>,
target: Target,
features: Option<&Features>,
) -> (String, bool) {
// Remove unstable targets from `allowed_targets` if their features are not enabled
if let Some(features) = features {
if !features.fn_delegation() {
allowed_targets.retain(|t| !matches!(t, Target::Delegation { .. }));
}
if !features.stmt_expr_attributes() {
allowed_targets.retain(|t| !matches!(t, Target::Expression | Target::Statement));
}
}
// We define groups of "similar" targets.
// If at least two of the targets are allowed, and the `target` is not in the group,
// we collapse the entire group to a single entry to simplify the target list
const FUNCTION_LIKE: &[Target] = &[
Target::Fn,
Target::Closure,
Target::ForeignFn,
Target::Method(MethodKind::Inherent),
Target::Method(MethodKind::Trait { body: false }),
Target::Method(MethodKind::Trait { body: true }),
Target::Method(MethodKind::TraitImpl),
];
const METHOD_LIKE: &[Target] = &[
Target::Method(MethodKind::Inherent),
Target::Method(MethodKind::Trait { body: false }),
Target::Method(MethodKind::Trait { body: true }),
Target::Method(MethodKind::TraitImpl),
];
const IMPL_LIKE: &[Target] =
&[Target::Impl { of_trait: false }, Target::Impl { of_trait: true }];
const ADT_LIKE: &[Target] = &[Target::Struct, Target::Enum];
let mut added_fake_targets = Vec::new();
filter_targets(
&mut allowed_targets,
FUNCTION_LIKE,
"functions",
target,
&mut added_fake_targets,
);
filter_targets(&mut allowed_targets, METHOD_LIKE, "methods", target, &mut added_fake_targets);
filter_targets(&mut allowed_targets, IMPL_LIKE, "impl blocks", target, &mut added_fake_targets);
filter_targets(&mut allowed_targets, ADT_LIKE, "data types", target, &mut added_fake_targets);
// If there is now only 1 target left, show that as the only possible target
(
added_fake_targets
.iter()
.copied()
.chain(allowed_targets.iter().map(|t| t.plural_name()))
.join(", "),
allowed_targets.len() + added_fake_targets.len() == 1,
)
}
fn filter_targets(
allowed_targets: &mut Vec<Target>,
target_group: &'static [Target],
target_group_name: &'static str,
target: Target,
added_fake_targets: &mut Vec<&'static str>,
) {
if target_group.contains(&target) {
return;
}
if allowed_targets.iter().filter(|at| target_group.contains(at)).count() < 2 {
return;
}
allowed_targets.retain(|t| !target_group.contains(t));
added_fake_targets.push(target_group_name);
}
/// This is the list of all targets to which a attribute can be applied
/// This is used for:
/// - `rustc_dummy`, which can be applied to all targets
/// - Attributes that are not parted to the new target system yet can use this list as a placeholder
pub(crate) const ALL_TARGETS: &'static [MaybeWarn] = &[
Allow(Target::ExternCrate),
Allow(Target::Use),
Allow(Target::Static),
Allow(Target::Const),
Allow(Target::Fn),
Allow(Target::Closure),
Allow(Target::Mod),
Allow(Target::ForeignMod),
Allow(Target::GlobalAsm),
Allow(Target::TyAlias),
Allow(Target::Enum),
Allow(Target::Variant),
Allow(Target::Struct),
Allow(Target::Field),
Allow(Target::Union),
Allow(Target::Trait),
Allow(Target::TraitAlias),
Allow(Target::Impl { of_trait: false }),
Allow(Target::Impl { of_trait: true }),
Allow(Target::Expression),
Allow(Target::Statement),
Allow(Target::Arm),
Allow(Target::AssocConst),
Allow(Target::Method(MethodKind::Inherent)),
Allow(Target::Method(MethodKind::Trait { body: false })),
Allow(Target::Method(MethodKind::Trait { body: true })),
Allow(Target::Method(MethodKind::TraitImpl)),
Allow(Target::AssocTy),
Allow(Target::ForeignFn),
Allow(Target::ForeignStatic),
Allow(Target::ForeignTy),
Allow(Target::MacroDef),
Allow(Target::Param),
Allow(Target::PatField),
Allow(Target::ExprField),
Allow(Target::WherePredicate),
Allow(Target::MacroCall),
Allow(Target::Crate),
Allow(Target::Delegation { mac: false }),
Allow(Target::Delegation { mac: true }),
];
/// Parse a single integer.
///
/// Used by attributes that take a single integer as argument, such as

View file

@ -1,6 +1,7 @@
use rustc_errors::{DiagArgValue, LintEmitter};
use rustc_hir::HirId;
use rustc_hir::lints::{AttributeLint, AttributeLintKind};
use rustc_hir::{HirId, Target};
use rustc_span::sym;
use crate::session_diagnostics;
@ -34,5 +35,25 @@ pub fn emit_attribute_lint<L: LintEmitter>(lint: &AttributeLint<HirId>, lint_emi
*first_span,
session_diagnostics::EmptyAttributeList { attr_span: *first_span },
),
&AttributeLintKind::InvalidTarget { name, target, ref applied, only } => lint_emitter
.emit_node_span_lint(
// This check is here because `deprecated` had its own lint group and removing this would be a breaking change
if name == sym::deprecated
&& ![Target::Closure, Target::Expression, Target::Statement, Target::Arm]
.contains(&target)
{
rustc_session::lint::builtin::USELESS_DEPRECATED
} else {
rustc_session::lint::builtin::UNUSED_ATTRIBUTES
},
*id,
*span,
session_diagnostics::InvalidTargetLint {
name,
target: target.plural_name(),
applied: applied.clone(),
only,
},
),
}
}

View file

@ -480,6 +480,29 @@ pub(crate) struct EmptyAttributeList {
pub attr_span: Span,
}
#[derive(LintDiagnostic)]
#[diag(attr_parsing_invalid_target_lint)]
#[warning]
#[help]
pub(crate) struct InvalidTargetLint {
pub name: Symbol,
pub target: &'static str,
pub applied: String,
pub only: &'static str,
}
#[derive(Diagnostic)]
#[help]
#[diag(attr_parsing_invalid_target)]
pub(crate) struct InvalidTarget {
#[primary_span]
pub span: Span,
pub name: Symbol,
pub target: &'static str,
pub applied: String,
pub only: &'static str,
}
#[derive(Diagnostic)]
#[diag(attr_parsing_invalid_alignment_value, code = E0589)]
pub(crate) struct InvalidAlignmentValue {

View file

@ -1,8 +1,8 @@
use rustc_data_structures::fingerprint::Fingerprint;
use rustc_macros::HashStable_Generic;
use rustc_span::Span;
use rustc_span::{Span, Symbol};
use crate::HirId;
use crate::{HirId, Target};
#[derive(Debug)]
pub struct DelayedLints {
@ -34,4 +34,5 @@ pub enum AttributeLintKind {
UnusedDuplicate { this: Span, other: Span, warning: bool },
IllFormedAttributeInput { suggestions: Vec<String> },
EmptyAttribute { first_span: Span },
InvalidTarget { name: Symbol, target: Target, applied: String, only: &'static str },
}