Move checks from check_doc_attrs directly into rustc_attr_parsing

This commit is contained in:
Guillaume Gomez 2026-01-10 15:54:09 +01:00
parent 1279939b38
commit ef1e4e65b7
18 changed files with 204 additions and 160 deletions

View file

@ -23,6 +23,9 @@ attr_parsing_doc_alias_malformed =
attr_parsing_doc_alias_start_end =
{$attr_str} cannot start or end with ' '
attr_parsing_doc_attr_not_crate_level =
`#![doc({$attr_name} = "...")]` isn't allowed as a crate-level attribute
attr_parsing_doc_attribute_not_attribute =
nonexistent builtin attribute `{$attribute}` used in `#[doc(attribute = "...")]`
.help = only existing builtin attributes are allowed in core/std

View file

@ -1,5 +1,6 @@
use rustc_ast::ast::{AttrStyle, LitKind, MetaItemLit};
use rustc_feature::template;
use rustc_hir::Target;
use rustc_hir::attrs::{
AttributeKind, CfgEntry, CfgHideShow, CfgInfo, DocAttribute, DocInline, HideOrShow,
};
@ -12,8 +13,8 @@ use super::{AcceptMapping, AttributeParser};
use crate::context::{AcceptContext, FinalizeContext, Stage};
use crate::parser::{ArgParser, MetaItemOrLitParser, MetaItemParser, OwnedPathParser};
use crate::session_diagnostics::{
DocAliasBadChar, DocAliasEmpty, DocAliasMalformed, DocAliasStartEnd, DocAttributeNotAttribute,
DocKeywordNotKeyword,
DocAliasBadChar, DocAliasEmpty, DocAliasMalformed, DocAliasStartEnd, DocAttrNotCrateLevel,
DocAttributeNotAttribute, DocKeywordNotKeyword,
};
fn check_keyword<S: Stage>(cx: &mut AcceptContext<'_, '_, S>, keyword: Symbol, span: Span) -> bool {
@ -43,16 +44,39 @@ fn check_attribute<S: Stage>(
false
}
fn parse_keyword_and_attribute<S, F>(
/// Checks that an attribute is *not* used at the crate level. Returns `true` if valid.
fn check_attr_not_crate_level<S: Stage>(
cx: &mut AcceptContext<'_, '_, S>,
span: Span,
attr_name: Symbol,
) -> bool {
if cx.shared.target.is_some_and(|target| target == Target::Crate) {
cx.emit_err(DocAttrNotCrateLevel { span, attr_name });
return false;
}
true
}
/// Checks that an attribute is used at the crate level. Returns `true` if valid.
fn check_attr_crate_level<S: Stage>(cx: &mut AcceptContext<'_, '_, S>, span: Span) -> bool {
if cx.shared.target.is_some_and(|target| target != Target::Crate) {
cx.emit_lint(
rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
AttributeLintKind::AttrCrateLevelOnly,
span,
);
return false;
}
true
}
fn parse_keyword_and_attribute<S: Stage>(
cx: &mut AcceptContext<'_, '_, S>,
path: &OwnedPathParser,
args: &ArgParser,
attr_value: &mut Option<(Symbol, Span)>,
callback: F,
) where
S: Stage,
F: FnOnce(&mut AcceptContext<'_, '_, S>, Symbol, Span) -> bool,
{
attr_name: Symbol,
) {
let Some(nv) = args.name_value() else {
cx.expected_name_value(args.span().unwrap_or(path.span()), path.word_sym());
return;
@ -63,16 +87,26 @@ fn parse_keyword_and_attribute<S, F>(
return;
};
if !callback(cx, value, nv.value_span) {
let ret = if attr_name == sym::keyword {
check_keyword(cx, value, nv.value_span)
} else {
check_attribute(cx, value, nv.value_span)
};
if !ret {
return;
}
let span = path.span();
if attr_value.is_some() {
cx.duplicate_key(path.span(), path.word_sym().unwrap());
cx.duplicate_key(span, path.word_sym().unwrap());
return;
}
*attr_value = Some((value, path.span()));
if !check_attr_not_crate_level(cx, span, attr_name) {
return;
}
*attr_value = Some((value, span));
}
#[derive(Default, Debug)]
@ -102,6 +136,10 @@ impl DocParser {
return;
}
if !check_attr_crate_level(cx, path.span()) {
return;
}
self.attribute.no_crate_inject = Some(path.span())
}
Some(sym::attr) => {
@ -155,6 +193,9 @@ impl DocParser {
cx.emit_err(DocAliasStartEnd { span, attr_str });
return;
}
if !check_attr_not_crate_level(cx, span, sym::alias) {
return;
}
if let Some(first_definition) = self.attribute.aliases.get(&alias).copied() {
cx.emit_lint(
@ -366,7 +407,33 @@ impl DocParser {
self.attribute.$ident = Some(path.span());
}};
}
macro_rules! string_arg {
macro_rules! no_args_and_not_crate_level {
($ident: ident) => {{
if let Err(span) = args.no_args() {
cx.expected_no_args(span);
return;
}
let span = path.span();
if !check_attr_not_crate_level(cx, span, sym::$ident) {
return;
}
self.attribute.$ident = Some(span);
}};
}
macro_rules! no_args_and_crate_level {
($ident: ident) => {{
if let Err(span) = args.no_args() {
cx.expected_no_args(span);
return;
}
let span = path.span();
if !check_attr_crate_level(cx, span) {
return;
}
self.attribute.$ident = Some(span);
}};
}
macro_rules! string_arg_and_crate_level {
($ident: ident) => {{
let Some(nv) = args.name_value() else {
cx.expected_name_value(args.span().unwrap_or(path.span()), path.word_sym());
@ -378,6 +445,10 @@ impl DocParser {
return;
};
if !check_attr_crate_level(cx, path.span()) {
return;
}
// FIXME: It's errorring when the attribute is passed multiple times on the command
// line.
// The right fix for this would be to only check this rule if the attribute is
@ -394,12 +465,14 @@ impl DocParser {
match path.word_sym() {
Some(sym::alias) => self.parse_alias(cx, path, args),
Some(sym::hidden) => no_args!(hidden),
Some(sym::html_favicon_url) => string_arg!(html_favicon_url),
Some(sym::html_logo_url) => string_arg!(html_logo_url),
Some(sym::html_no_source) => no_args!(html_no_source),
Some(sym::html_playground_url) => string_arg!(html_playground_url),
Some(sym::html_root_url) => string_arg!(html_root_url),
Some(sym::issue_tracker_base_url) => string_arg!(issue_tracker_base_url),
Some(sym::html_favicon_url) => string_arg_and_crate_level!(html_favicon_url),
Some(sym::html_logo_url) => string_arg_and_crate_level!(html_logo_url),
Some(sym::html_no_source) => no_args_and_crate_level!(html_no_source),
Some(sym::html_playground_url) => string_arg_and_crate_level!(html_playground_url),
Some(sym::html_root_url) => string_arg_and_crate_level!(html_root_url),
Some(sym::issue_tracker_base_url) => {
string_arg_and_crate_level!(issue_tracker_base_url)
}
Some(sym::inline) => self.parse_inline(cx, path, args, DocInline::Inline),
Some(sym::no_inline) => self.parse_inline(cx, path, args, DocInline::NoInline),
Some(sym::masked) => no_args!(masked),
@ -410,18 +483,18 @@ impl DocParser {
path,
args,
&mut self.attribute.keyword,
check_keyword,
sym::keyword,
),
Some(sym::attribute) => parse_keyword_and_attribute(
cx,
path,
args,
&mut self.attribute.attribute,
check_attribute,
sym::attribute,
),
Some(sym::fake_variadic) => no_args!(fake_variadic),
Some(sym::search_unbox) => no_args!(search_unbox),
Some(sym::rust_logo) => no_args!(rust_logo),
Some(sym::fake_variadic) => no_args_and_not_crate_level!(fake_variadic),
Some(sym::search_unbox) => no_args_and_not_crate_level!(search_unbox),
Some(sym::rust_logo) => no_args_and_crate_level!(rust_logo),
Some(sym::auto_cfg) => self.parse_auto_cfg(cx, path, args),
Some(sym::test) => {
let Some(list) = args.list() else {

View file

@ -653,6 +653,7 @@ pub struct SharedContext<'p, 'sess, S: Stage> {
pub(crate) target_span: Span,
/// The id ([`NodeId`] if `S` is `Early`, [`HirId`] if `S` is `Late`) of the syntactical component this attribute was applied to
pub(crate) target_id: S::Id,
pub(crate) target: Option<rustc_hir::Target>,
pub(crate) emit_lint: &'p mut dyn FnMut(AttributeLint<S::Id>),
}

View file

@ -218,6 +218,7 @@ impl<'sess> AttributeParser<'sess, Early> {
cx: &mut parser,
target_span,
target_id: target_node_id,
target: None,
emit_lint: &mut emit_lint,
},
attr_span,
@ -378,6 +379,7 @@ impl<'sess, S: Stage> AttributeParser<'sess, S> {
cx: self,
target_span,
target_id,
target: Some(target),
emit_lint: &mut emit_lint,
},
attr_span,
@ -429,6 +431,7 @@ impl<'sess, S: Stage> AttributeParser<'sess, S> {
cx: self,
target_span,
target_id,
target: Some(target),
emit_lint: &mut emit_lint,
},
all_attrs: &attr_paths,

View file

@ -47,6 +47,14 @@ pub(crate) struct DocAliasStartEnd<'a> {
pub attr_str: &'a str,
}
#[derive(Diagnostic)]
#[diag(attr_parsing_doc_attr_not_crate_level)]
pub(crate) struct DocAttrNotCrateLevel {
#[primary_span]
pub span: Span,
pub attr_name: Symbol,
}
#[derive(Diagnostic)]
#[diag(attr_parsing_doc_keyword_not_keyword)]
#[help]

View file

@ -40,6 +40,11 @@ lint_atomic_ordering_load = atomic loads cannot have `Release` or `AcqRel` order
lint_atomic_ordering_store = atomic stores cannot have `Acquire` or `AcqRel` ordering
.help = consider using ordering modes `Release`, `SeqCst` or `Relaxed`
lint_attr_crate_level =
this attribute can only be applied at the crate level
.suggestion = to apply to the crate, use an inner attribute
.note = read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#at-the-crate-level> for more information
lint_bad_attribute_argument = bad attribute argument
lint_bad_opt_access = {$msg}

View file

@ -417,5 +417,7 @@ pub fn decorate_attribute_lint(
}
&AttributeLintKind::DocTestLiteral => lints::DocTestLiteral.decorate_lint(diag),
&AttributeLintKind::AttrCrateLevelOnly => lints::AttrCrateLevelOnly.decorate_lint(diag),
}
}

View file

@ -3312,3 +3312,8 @@ pub(crate) struct DocTestUnknown {
#[derive(LintDiagnostic)]
#[diag(lint_doc_test_literal)]
pub(crate) struct DocTestLiteral;
#[derive(LintDiagnostic)]
#[diag(lint_attr_crate_level)]
#[note]
pub(crate) struct AttrCrateLevelOnly;

View file

@ -820,6 +820,7 @@ pub enum AttributeLintKind {
name: Symbol,
},
DocTestLiteral,
AttrCrateLevelOnly,
}
pub type RegisteredTools = FxIndexSet<Ident>;

View file

@ -29,11 +29,6 @@ passes_attr_application_struct_union =
attribute should be applied to a struct or union
.label = not a struct or union
passes_attr_crate_level =
this attribute can only be applied at the crate level
.suggestion = to apply to the crate, use an inner attribute
.note = read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#at-the-crate-level> for more information
passes_autodiff_attr =
`#[autodiff]` should be applied to a function
.label = not a function
@ -112,9 +107,6 @@ passes_doc_alias_bad_location =
passes_doc_alias_not_an_alias =
`#[doc(alias = "{$attr_str}"]` is the same as the item's name
passes_doc_attr_not_crate_level =
`#![doc({$attr_name} = "...")]` isn't allowed as a crate-level attribute
passes_doc_fake_variadic_not_valid =
`#[doc(fake_variadic)]` must be used on the first of a set of tuple or fn pointer trait impls with varying arity

View file

@ -1034,29 +1034,6 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
}
}
/// Checks that an attribute is *not* used at the crate level. Returns `true` if valid.
fn check_attr_not_crate_level(&self, span: Span, hir_id: HirId, attr_name: &str) -> bool {
if CRATE_HIR_ID == hir_id {
self.dcx().emit_err(errors::DocAttrNotCrateLevel { span, attr_name });
return false;
}
true
}
/// Checks that an attribute is used at the crate level. Returns `true` if valid.
fn check_attr_crate_level(&self, span: Span, hir_id: HirId) -> bool {
if hir_id != CRATE_HIR_ID {
self.tcx.emit_node_span_lint(
INVALID_DOC_ATTRIBUTES,
hir_id,
span,
errors::AttrCrateLevelOnly {},
);
return false;
}
true
}
/// Runs various checks on `#[doc]` attributes.
///
/// `specified_inline` should be initialized to `None` and kept for the scope
@ -1072,9 +1049,9 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
inline,
// FIXME: currently unchecked
cfg: _,
// already check in attr_parsing
// already checked in attr_parsing
auto_cfg: _,
// already check in attr_parsing
// already checked in attr_parsing
auto_cfg_change: _,
fake_variadic,
keyword,
@ -1082,70 +1059,48 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
// FIXME: currently unchecked
notable_trait: _,
search_unbox,
html_favicon_url,
html_logo_url,
html_playground_url,
html_root_url,
html_no_source,
issue_tracker_base_url,
// already checked in attr_parsing
html_favicon_url: _,
// already checked in attr_parsing
html_logo_url: _,
// already checked in attr_parsing
html_playground_url: _,
// already checked in attr_parsing
html_root_url: _,
// already checked in attr_parsing
html_no_source: _,
// already checked in attr_parsing
issue_tracker_base_url: _,
rust_logo,
// allowed anywhere
test_attrs: _,
no_crate_inject,
// already checked in attr_parsing
no_crate_inject: _,
attribute,
} = attr;
for (alias, span) in aliases {
if self.check_attr_not_crate_level(*span, hir_id, "alias") {
self.check_doc_alias_value(*span, hir_id, target, *alias);
}
self.check_doc_alias_value(*span, hir_id, target, *alias);
}
if let Some((_, span)) = keyword
&& self.check_attr_not_crate_level(*span, hir_id, "keyword")
{
if let Some((_, span)) = keyword {
self.check_doc_keyword_and_attribute(*span, hir_id, "keyword");
}
if let Some((_, span)) = attribute
&& self.check_attr_not_crate_level(*span, hir_id, "attribute")
{
if let Some((_, span)) = attribute {
self.check_doc_keyword_and_attribute(*span, hir_id, "attribute");
}
if let Some(span) = fake_variadic
&& self.check_attr_not_crate_level(*span, hir_id, "fake_variadic")
{
if let Some(span) = fake_variadic {
self.check_doc_fake_variadic(*span, hir_id);
}
if let Some(span) = search_unbox
&& self.check_attr_not_crate_level(*span, hir_id, "search_unbox")
{
if let Some(span) = search_unbox {
self.check_doc_search_unbox(*span, hir_id);
}
for i in [
html_favicon_url,
html_logo_url,
html_playground_url,
issue_tracker_base_url,
html_root_url,
] {
if let Some((_, span)) = i {
self.check_attr_crate_level(*span, hir_id);
}
}
for i in [html_no_source, no_crate_inject] {
if let Some(span) = i {
self.check_attr_crate_level(*span, hir_id);
}
}
self.check_doc_inline(hir_id, target, inline);
if let Some(span) = rust_logo
&& self.check_attr_crate_level(*span, hir_id)
&& !self.tcx.features().rustdoc_internals()
{
feature_err(

View file

@ -179,14 +179,6 @@ pub(crate) struct DocMaskedNotExternCrateSelf {
pub item_span: Span,
}
#[derive(Diagnostic)]
#[diag(passes_doc_attr_not_crate_level)]
pub(crate) struct DocAttrNotCrateLevel<'a> {
#[primary_span]
pub span: Span,
pub attr_name: &'a str,
}
#[derive(Diagnostic)]
#[diag(passes_both_ffi_const_and_pure, code = E0757)]
pub(crate) struct BothFfiConstAndPure {
@ -1104,11 +1096,6 @@ pub(crate) struct UnnecessaryPartialStableFeature {
#[note]
pub(crate) struct IneffectiveUnstableImpl;
#[derive(LintDiagnostic)]
#[diag(passes_attr_crate_level)]
#[note]
pub(crate) struct AttrCrateLevelOnly {}
/// "sanitize attribute not allowed here"
#[derive(Diagnostic)]
#[diag(passes_sanitize_attribute_not_allowed)]

View file

@ -0,0 +1,13 @@
#![crate_type = "lib"]
#[doc(test(no_crate_inject))]
//~^ ERROR can only be applied at the crate level
pub mod bar {
#![doc(test(no_crate_inject))]
//~^ ERROR can only be applied at the crate level
#[doc(test(no_crate_inject))]
//~^ ERROR can only be applied at the crate level
fn foo() {}
}

View file

@ -0,0 +1,27 @@
error: this attribute can only be applied at the crate level
--> $DIR/invalid-crate-level-lint.rs:3:12
|
LL | #[doc(test(no_crate_inject))]
| ^^^^^^^^^^^^^^^
|
= note: read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#at-the-crate-level> for more information
= note: `#[deny(invalid_doc_attributes)]` on by default
error: this attribute can only be applied at the crate level
--> $DIR/invalid-crate-level-lint.rs:7:17
|
LL | #![doc(test(no_crate_inject))]
| ^^^^^^^^^^^^^^^
|
= note: read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#at-the-crate-level> for more information
error: this attribute can only be applied at the crate level
--> $DIR/invalid-crate-level-lint.rs:10:16
|
LL | #[doc(test(no_crate_inject))]
| ^^^^^^^^^^^^^^^
|
= note: read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#at-the-crate-level> for more information
error: aborting due to 3 previous errors

View file

@ -4,18 +4,11 @@
#![doc(masked)]
//~^ ERROR this attribute can only be applied to an `extern crate` item
#[doc(test(no_crate_inject))]
//~^ ERROR can only be applied at the crate level
#[doc(inline)]
//~^ ERROR can only be applied to a `use` item
pub fn foo() {}
pub mod bar {
#![doc(test(no_crate_inject))]
//~^ ERROR can only be applied at the crate level
#[doc(test(no_crate_inject))]
//~^ ERROR can only be applied at the crate level
#[doc(inline)]
//~^ ERROR can only be applied to a `use` item
pub fn baz() {}

View file

@ -1,14 +1,5 @@
error: this attribute can only be applied at the crate level
--> $DIR/invalid-doc-attr.rs:7:12
|
LL | #[doc(test(no_crate_inject))]
| ^^^^^^^^^^^^^^^
|
= note: read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#at-the-crate-level> for more information
= note: `#[deny(invalid_doc_attributes)]` on by default
error: this attribute can only be applied to a `use` item
--> $DIR/invalid-doc-attr.rs:9:7
--> $DIR/invalid-doc-attr.rs:7:7
|
LL | #[doc(inline)]
| ^^^^^^ only applicable on `use` items
@ -17,17 +8,10 @@ LL | pub fn foo() {}
| ------------ not a `use` item
|
= note: read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#inline-and-no_inline> for more information
error: this attribute can only be applied at the crate level
--> $DIR/invalid-doc-attr.rs:14:17
|
LL | #![doc(test(no_crate_inject))]
| ^^^^^^^^^^^^^^^
|
= note: read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#at-the-crate-level> for more information
= note: `#[deny(invalid_doc_attributes)]` on by default
error: conflicting doc inlining attributes
--> $DIR/invalid-doc-attr.rs:24:7
--> $DIR/invalid-doc-attr.rs:17:7
|
LL | #[doc(inline)]
| ^^^^^^ this attribute...
@ -37,7 +21,7 @@ LL | #[doc(no_inline)]
= help: remove one of the conflicting attributes
error: this attribute can only be applied to an `extern crate` item
--> $DIR/invalid-doc-attr.rs:30:7
--> $DIR/invalid-doc-attr.rs:23:7
|
LL | #[doc(masked)]
| ^^^^^^ only applicable on `extern crate` items
@ -48,7 +32,7 @@ LL | pub struct Masked;
= note: read <https://doc.rust-lang.org/unstable-book/language-features/doc-masked.html> for more information
error: this attribute cannot be applied to an `extern crate self` item
--> $DIR/invalid-doc-attr.rs:34:7
--> $DIR/invalid-doc-attr.rs:27:7
|
LL | #[doc(masked)]
| ^^^^^^ not applicable on `extern crate self` items
@ -70,16 +54,8 @@ LL | | pub extern crate self as reexport;
|
= note: read <https://doc.rust-lang.org/unstable-book/language-features/doc-masked.html> for more information
error: this attribute can only be applied at the crate level
--> $DIR/invalid-doc-attr.rs:17:16
|
LL | #[doc(test(no_crate_inject))]
| ^^^^^^^^^^^^^^^
|
= note: read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#at-the-crate-level> for more information
error: this attribute can only be applied to a `use` item
--> $DIR/invalid-doc-attr.rs:19:11
--> $DIR/invalid-doc-attr.rs:12:11
|
LL | #[doc(inline)]
| ^^^^^^ only applicable on `use` items
@ -89,5 +65,5 @@ LL | pub fn baz() {}
|
= note: read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#inline-and-no_inline> for more information
error: aborting due to 9 previous errors
error: aborting due to 6 previous errors

View file

@ -1,14 +1,14 @@
error: '\'' character isn't allowed in `#[doc(alias = "...")]`
--> $DIR/doc-alias-crate-level.rs:7:15
|
LL | #[doc(alias = "shouldn't work!")]
| ^^^^^^^^^^^^^^^^^
error: `#![doc(alias = "...")]` isn't allowed as a crate-level attribute
--> $DIR/doc-alias-crate-level.rs:5:16
|
LL | #![doc(alias = "not working!")]
| ^^^^^^^^^^^^^^
error: '\'' character isn't allowed in `#[doc(alias = "...")]`
--> $DIR/doc-alias-crate-level.rs:7:15
|
LL | #[doc(alias = "shouldn't work!")]
| ^^^^^^^^^^^^^^^^^
error: aborting due to 2 previous errors

View file

@ -1,3 +1,9 @@
error: `#![doc(keyword = "...")]` isn't allowed as a crate-level attribute
--> $DIR/doc_keyword.rs:4:8
|
LL | #![doc(keyword = "match")]
| ^^^^^^^
error: nonexistent keyword `tadam` used in `#[doc(keyword = "...")]`
--> $DIR/doc_keyword.rs:22:17
|
@ -24,11 +30,5 @@ error: `#[doc(keyword = "...")]` should be used on modules
LL | #[doc(keyword = "match")]
| ^^^^^^^
error: `#![doc(keyword = "...")]` isn't allowed as a crate-level attribute
--> $DIR/doc_keyword.rs:4:8
|
LL | #![doc(keyword = "match")]
| ^^^^^^^
error: aborting due to 5 previous errors