about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJonathan Brouwer <jonathantbrouwer@gmail.com>2025-08-09 20:37:32 +0200
committerJonathan Brouwer <jonathantbrouwer@gmail.com>2025-08-14 18:11:56 +0200
commit744d39ebe61ad8cc674797793c743493af078d74 (patch)
treef2c8fbde192c4d2a80c84d03713aeffef039b9b4
parente7ef23e90e5b63e86bd77061ab92b86190a4eb3c (diff)
downloadrust-744d39ebe61ad8cc674797793c743493af078d74.tar.gz
rust-744d39ebe61ad8cc674797793c743493af078d74.zip
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.
-rw-r--r--Cargo.lock1
-rw-r--r--compiler/rustc_attr_parsing/Cargo.toml1
-rw-r--r--compiler/rustc_attr_parsing/messages.ftl6
-rw-r--r--compiler/rustc_attr_parsing/src/attributes/mod.rs21
-rw-r--r--compiler/rustc_attr_parsing/src/context.rs237
-rw-r--r--compiler/rustc_attr_parsing/src/lints.rs23
-rw-r--r--compiler/rustc_attr_parsing/src/session_diagnostics.rs23
-rw-r--r--compiler/rustc_hir/src/lints.rs5
8 files changed, 304 insertions, 13 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 8a878faecbc..8c980616e83 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3480,6 +3480,7 @@ dependencies = [
 name = "rustc_attr_parsing"
 version = "0.0.0"
 dependencies = [
+ "itertools",
  "rustc_abi",
  "rustc_ast",
  "rustc_ast_pretty",
diff --git a/compiler/rustc_attr_parsing/Cargo.toml b/compiler/rustc_attr_parsing/Cargo.toml
index cec9d62e656..bac89373b67 100644
--- a/compiler/rustc_attr_parsing/Cargo.toml
+++ b/compiler/rustc_attr_parsing/Cargo.toml
@@ -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" }
diff --git a/compiler/rustc_attr_parsing/messages.ftl b/compiler/rustc_attr_parsing/messages.ftl
index de22ea322c7..4fb66a81652 100644
--- a/compiler/rustc_attr_parsing/messages.ftl
+++ b/compiler/rustc_attr_parsing/messages.ftl
@@ -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 =
diff --git a/compiler/rustc_attr_parsing/src/attributes/mod.rs b/compiler/rustc_attr_parsing/src/attributes/mod.rs
index f7946ade6d2..ed5d1d92b8c 100644
--- a/compiler/rustc_attr_parsing/src/attributes/mod.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/mod.rs
@@ -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 {
diff --git a/compiler/rustc_attr_parsing/src/context.rs b/compiler/rustc_attr_parsing/src/context.rs
index dc98f2c8df1..bebe3350c4e 100644
--- a/compiler/rustc_attr_parsing/src/context.rs
+++ b/compiler/rustc_attr_parsing/src/context.rs
@@ -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
diff --git a/compiler/rustc_attr_parsing/src/lints.rs b/compiler/rustc_attr_parsing/src/lints.rs
index 22f5531bc80..733225bab59 100644
--- a/compiler/rustc_attr_parsing/src/lints.rs
+++ b/compiler/rustc_attr_parsing/src/lints.rs
@@ -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,
+                },
+            ),
     }
 }
diff --git a/compiler/rustc_attr_parsing/src/session_diagnostics.rs b/compiler/rustc_attr_parsing/src/session_diagnostics.rs
index 41179844152..95e85667cd6 100644
--- a/compiler/rustc_attr_parsing/src/session_diagnostics.rs
+++ b/compiler/rustc_attr_parsing/src/session_diagnostics.rs
@@ -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 {
diff --git a/compiler/rustc_hir/src/lints.rs b/compiler/rustc_hir/src/lints.rs
index c55a41eb2b7..e3cde2d3bb6 100644
--- a/compiler/rustc_hir/src/lints.rs
+++ b/compiler/rustc_hir/src/lints.rs
@@ -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 },
 }