Rollup merge of #142472 - GuillaumeGomez:doc-attribute-attribute, r=fmease

Add new `doc(attribute = "...")` attribute

Fixes rust-lang/rust#141123.

The implementation and purpose of this new `#[doc(attribute = "...")]` attribute is very close to `#[doc(keyword = "...")]`. Which means that luckily for us, most of the code needed was already in place and `@Noratrieb` nicely wrote a first draft that helped me implement this new attribute very fast.

Now with all this said, there is one thing I didn't do yet: adding a `rustdoc-js-std` test. I added GUI tests with search results for attributes so should be fine but I still plan on adding one for it once documentation for builtin attributes will be written into the core/std libs.

You can test it [here](https://rustdoc.crud.net/imperio/doc-attribute-attribute/foo/index.html).

cc `@Noratrieb` `@Veykril`
This commit is contained in:
Guillaume Gomez 2025-08-28 21:41:00 +02:00 committed by GitHub
commit f948c79911
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 376 additions and 55 deletions

View file

@ -188,6 +188,7 @@ impl<'a> Visitor<'a> for PostExpansionVisitor<'a> {
notable_trait => doc_notable_trait
}
"meant for internal use only" {
attribute => rustdoc_internals
keyword => rustdoc_internals
fake_variadic => rustdoc_internals
search_unbox => rustdoc_internals

View file

@ -145,6 +145,10 @@ passes_doc_alias_start_end =
passes_doc_attr_not_crate_level =
`#![doc({$attr_name} = "...")]` isn't allowed as a crate-level attribute
passes_doc_attribute_not_attribute =
nonexistent builtin attribute `{$attribute}` used in `#[doc(attribute = "...")]`
.help = only existing builtin attributes are allowed in core/std
passes_doc_cfg_hide_takes_list =
`#[doc(cfg_hide(...))]` takes a list of attributes
@ -173,16 +177,16 @@ passes_doc_inline_only_use =
passes_doc_invalid =
invalid `doc` attribute
passes_doc_keyword_empty_mod =
`#[doc(keyword = "...")]` should be used on empty modules
passes_doc_keyword_attribute_empty_mod =
`#[doc({$attr_name} = "...")]` should be used on empty modules
passes_doc_keyword_attribute_not_mod =
`#[doc({$attr_name} = "...")]` should be used on modules
passes_doc_keyword_not_keyword =
nonexistent keyword `{$keyword}` used in `#[doc(keyword = "...")]`
.help = only existing keywords are allowed in core/std
passes_doc_keyword_not_mod =
`#[doc(keyword = "...")]` should be used on modules
passes_doc_keyword_only_impl =
`#[doc(keyword = "...")]` should be used on impl blocks

View file

@ -99,6 +99,21 @@ impl IntoDiagArg for ProcMacroKind {
}
}
#[derive(Clone, Copy)]
enum DocFakeItemKind {
Attribute,
Keyword,
}
impl DocFakeItemKind {
fn name(self) -> &'static str {
match self {
Self::Attribute => "attribute",
Self::Keyword => "keyword",
}
}
}
struct CheckAttrVisitor<'tcx> {
tcx: TyCtxt<'tcx>,
@ -853,7 +868,12 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
}
}
fn check_doc_keyword(&self, meta: &MetaItemInner, hir_id: HirId) {
fn check_doc_keyword_and_attribute(
&self,
meta: &MetaItemInner,
hir_id: HirId,
attr_kind: DocFakeItemKind,
) {
fn is_doc_keyword(s: Symbol) -> bool {
// FIXME: Once rustdoc can handle URL conflicts on case insensitive file systems, we
// can remove the `SelfTy` case here, remove `sym::SelfTy`, and update the
@ -861,9 +881,14 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
s.is_reserved(|| edition::LATEST_STABLE_EDITION) || s.is_weak() || s == sym::SelfTy
}
let doc_keyword = match meta.value_str() {
// FIXME: This should support attributes with namespace like `diagnostic::do_not_recommend`.
fn is_builtin_attr(s: Symbol) -> bool {
rustc_feature::BUILTIN_ATTRIBUTE_MAP.contains_key(&s)
}
let value = match meta.value_str() {
Some(value) if value != sym::empty => value,
_ => return self.doc_attr_str_error(meta, "keyword"),
_ => return self.doc_attr_str_error(meta, attr_kind.name()),
};
let item_kind = match self.tcx.hir_node(hir_id) {
@ -873,20 +898,38 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
match item_kind {
Some(ItemKind::Mod(_, module)) => {
if !module.item_ids.is_empty() {
self.dcx().emit_err(errors::DocKeywordEmptyMod { span: meta.span() });
self.dcx().emit_err(errors::DocKeywordAttributeEmptyMod {
span: meta.span(),
attr_name: attr_kind.name(),
});
return;
}
}
_ => {
self.dcx().emit_err(errors::DocKeywordNotMod { span: meta.span() });
self.dcx().emit_err(errors::DocKeywordAttributeNotMod {
span: meta.span(),
attr_name: attr_kind.name(),
});
return;
}
}
if !is_doc_keyword(doc_keyword) {
self.dcx().emit_err(errors::DocKeywordNotKeyword {
span: meta.name_value_literal_span().unwrap_or_else(|| meta.span()),
keyword: doc_keyword,
});
match attr_kind {
DocFakeItemKind::Keyword => {
if !is_doc_keyword(value) {
self.dcx().emit_err(errors::DocKeywordNotKeyword {
span: meta.name_value_literal_span().unwrap_or_else(|| meta.span()),
keyword: value,
});
}
}
DocFakeItemKind::Attribute => {
if !is_builtin_attr(value) {
self.dcx().emit_err(errors::DocAttributeNotAttribute {
span: meta.name_value_literal_span().unwrap_or_else(|| meta.span()),
attribute: value,
});
}
}
}
}
@ -1146,7 +1189,21 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
Some(sym::keyword) => {
if self.check_attr_not_crate_level(meta, hir_id, "keyword") {
self.check_doc_keyword(meta, hir_id);
self.check_doc_keyword_and_attribute(
meta,
hir_id,
DocFakeItemKind::Keyword,
);
}
}
Some(sym::attribute) => {
if self.check_attr_not_crate_level(meta, hir_id, "attribute") {
self.check_doc_keyword_and_attribute(
meta,
hir_id,
DocFakeItemKind::Attribute,
);
}
}

View file

@ -195,10 +195,11 @@ pub(crate) struct DocAliasMalformed {
}
#[derive(Diagnostic)]
#[diag(passes_doc_keyword_empty_mod)]
pub(crate) struct DocKeywordEmptyMod {
#[diag(passes_doc_keyword_attribute_empty_mod)]
pub(crate) struct DocKeywordAttributeEmptyMod {
#[primary_span]
pub span: Span,
pub attr_name: &'static str,
}
#[derive(Diagnostic)]
@ -211,10 +212,20 @@ pub(crate) struct DocKeywordNotKeyword {
}
#[derive(Diagnostic)]
#[diag(passes_doc_keyword_not_mod)]
pub(crate) struct DocKeywordNotMod {
#[diag(passes_doc_attribute_not_attribute)]
#[help]
pub(crate) struct DocAttributeNotAttribute {
#[primary_span]
pub span: Span,
pub attribute: Symbol,
}
#[derive(Diagnostic)]
#[diag(passes_doc_keyword_attribute_not_mod)]
pub(crate) struct DocKeywordAttributeNotMod {
#[primary_span]
pub span: Span,
pub attr_name: &'static str,
}
#[derive(Diagnostic)]

View file

@ -5016,7 +5016,7 @@ impl<'a, 'ast, 'ra, 'tcx> LateResolutionVisitor<'a, 'ast, 'ra, 'tcx> {
}
ResolveDocLinks::Exported
if !maybe_exported.eval(self.r)
&& !rustdoc::has_primitive_or_keyword_docs(attrs) =>
&& !rustdoc::has_primitive_or_keyword_or_attribute_docs(attrs) =>
{
return;
}

View file

@ -373,8 +373,8 @@ pub fn inner_docs(attrs: &[impl AttributeExt]) -> bool {
true
}
/// Has `#[rustc_doc_primitive]` or `#[doc(keyword)]`.
pub fn has_primitive_or_keyword_docs(attrs: &[impl AttributeExt]) -> bool {
/// Has `#[rustc_doc_primitive]` or `#[doc(keyword)]` or `#[doc(attribute)]`.
pub fn has_primitive_or_keyword_or_attribute_docs(attrs: &[impl AttributeExt]) -> bool {
for attr in attrs {
if attr.has_name(sym::rustc_doc_primitive) {
return true;
@ -382,7 +382,7 @@ pub fn has_primitive_or_keyword_docs(attrs: &[impl AttributeExt]) -> bool {
&& let Some(items) = attr.meta_item_list()
{
for item in items {
if item.has_name(sym::keyword) {
if item.has_name(sym::keyword) || item.has_name(sym::attribute) {
return true;
}
}

View file

@ -541,6 +541,7 @@ symbols! {
att_syntax,
attr,
attr_literals,
attribute,
attributes,
audit_that,
augmented_assignments,

View file

@ -196,7 +196,7 @@ to enable.
### Document keywords
This is for Rust compiler internal use only.
This is for internal use in the std library.
Rust keywords are documented in the standard library (look for `match` for example).
@ -211,6 +211,23 @@ To do so, the `#[doc(keyword = "...")]` attribute is used. Example:
mod empty_mod {}
```
### Document builtin attributes
This is for internal use in the std library.
Rust builtin attributes are documented in the standard library (look for `repr` for example).
To do so, the `#[doc(attribute = "...")]` attribute is used. Example:
```rust
#![feature(rustdoc_internals)]
#![allow(internal_features)]
/// Some documentation about the attribute.
#[doc(attribute = "repr")]
mod empty_mod {}
```
### Use the Rust logo as the crate logo
This is for official Rust project use only.

View file

@ -226,15 +226,28 @@ impl ExternalCrate {
}
pub(crate) fn keywords(&self, tcx: TyCtxt<'_>) -> impl Iterator<Item = (DefId, Symbol)> {
fn as_keyword(did: DefId, tcx: TyCtxt<'_>) -> Option<(DefId, Symbol)> {
self.retrieve_keywords_or_documented_attributes(tcx, sym::keyword)
}
pub(crate) fn documented_attributes(
&self,
tcx: TyCtxt<'_>,
) -> impl Iterator<Item = (DefId, Symbol)> {
self.retrieve_keywords_or_documented_attributes(tcx, sym::attribute)
}
fn retrieve_keywords_or_documented_attributes(
&self,
tcx: TyCtxt<'_>,
name: Symbol,
) -> impl Iterator<Item = (DefId, Symbol)> {
let as_target = move |did: DefId, tcx: TyCtxt<'_>| -> Option<(DefId, Symbol)> {
tcx.get_attrs(did, sym::doc)
.flat_map(|attr| attr.meta_item_list().unwrap_or_default())
.filter(|meta| meta.has_name(sym::keyword))
.filter(|meta| meta.has_name(name))
.find_map(|meta| meta.value_str())
.map(|value| (did, value))
}
self.mapped_root_modules(tcx, as_keyword)
};
self.mapped_root_modules(tcx, as_target)
}
pub(crate) fn primitives(
@ -592,6 +605,20 @@ impl Item {
pub(crate) fn is_keyword(&self) -> bool {
self.type_() == ItemType::Keyword
}
pub(crate) fn is_attribute(&self) -> bool {
self.type_() == ItemType::Attribute
}
/// Returns `true` if the item kind is one of the following:
///
/// * `ItemType::Primitive`
/// * `ItemType::Keyword`
/// * `ItemType::Attribute`
///
/// They are considered fake because they only exist thanks to their
/// `#[doc(primitive|keyword|attribute)]` attribute.
pub(crate) fn is_fake_item(&self) -> bool {
matches!(self.type_(), ItemType::Primitive | ItemType::Keyword | ItemType::Attribute)
}
pub(crate) fn is_stripped(&self) -> bool {
match self.kind {
StrippedItem(..) => true,
@ -735,7 +762,9 @@ impl Item {
// Primitives and Keywords are written in the source code as private modules.
// The modules need to be private so that nobody actually uses them, but the
// keywords and primitives that they are documenting are public.
ItemKind::KeywordItem | ItemKind::PrimitiveItem(_) => return Some(Visibility::Public),
ItemKind::KeywordItem | ItemKind::PrimitiveItem(_) | ItemKind::AttributeItem => {
return Some(Visibility::Public);
}
// Variant fields inherit their enum's visibility.
StructFieldItem(..) if is_field_vis_inherited(tcx, def_id) => {
return None;
@ -942,7 +971,12 @@ pub(crate) enum ItemKind {
AssocTypeItem(Box<TypeAlias>, Vec<GenericBound>),
/// An item that has been stripped by a rustdoc pass
StrippedItem(Box<ItemKind>),
/// This item represents a module with a `#[doc(keyword = "...")]` attribute which is used
/// to generate documentation for Rust keywords.
KeywordItem,
/// This item represents a module with a `#[doc(attribute = "...")]` attribute which is used
/// to generate documentation for Rust builtin attributes.
AttributeItem,
}
impl ItemKind {
@ -983,7 +1017,8 @@ impl ItemKind {
| RequiredAssocTypeItem(..)
| AssocTypeItem(..)
| StrippedItem(_)
| KeywordItem => [].iter(),
| KeywordItem
| AttributeItem => [].iter(),
}
}

View file

@ -60,6 +60,7 @@ pub(crate) fn krate(cx: &mut DocContext<'_>) -> Crate {
let local_crate = ExternalCrate { crate_num: LOCAL_CRATE };
let primitives = local_crate.primitives(cx.tcx);
let keywords = local_crate.keywords(cx.tcx);
let documented_attributes = local_crate.documented_attributes(cx.tcx);
{
let ItemKind::ModuleItem(m) = &mut module.inner.kind else { unreachable!() };
m.items.extend(primitives.map(|(def_id, prim)| {
@ -73,6 +74,9 @@ pub(crate) fn krate(cx: &mut DocContext<'_>) -> Crate {
m.items.extend(keywords.map(|(def_id, kw)| {
Item::from_def_id_and_parts(def_id, Some(kw), ItemKind::KeywordItem, cx)
}));
m.items.extend(documented_attributes.into_iter().map(|(def_id, kw)| {
Item::from_def_id_and_parts(def_id, Some(kw), ItemKind::AttributeItem, cx)
}));
}
Crate { module, external_traits: Box::new(mem::take(&mut cx.external_traits)) }

View file

@ -96,7 +96,8 @@ pub(crate) trait DocFolder: Sized {
| ImplAssocConstItem(..)
| RequiredAssocTypeItem(..)
| AssocTypeItem(..)
| KeywordItem => kind,
| KeywordItem
| AttributeItem => kind,
}
}

View file

@ -372,7 +372,8 @@ impl DocFolder for CacheBuilder<'_, '_> {
| clean::RequiredAssocTypeItem(..)
| clean::AssocTypeItem(..)
| clean::StrippedItem(..)
| clean::KeywordItem => {
| clean::KeywordItem
| clean::AttributeItem => {
// FIXME: Do these need handling?
// The person writing this comment doesn't know.
// So would rather leave them to an expert,

View file

@ -57,6 +57,7 @@ pub(crate) enum ItemType {
TraitAlias = 25,
// This number is reserved for use in JavaScript
// Generic = 26,
Attribute = 27,
}
impl Serialize for ItemType {
@ -148,6 +149,7 @@ impl<'a> From<&'a clean::Item> for ItemType {
clean::RequiredAssocTypeItem(..) | clean::AssocTypeItem(..) => ItemType::AssocType,
clean::ForeignTypeItem => ItemType::ForeignType,
clean::KeywordItem => ItemType::Keyword,
clean::AttributeItem => ItemType::Attribute,
clean::TraitAliasItem(..) => ItemType::TraitAlias,
clean::ProcMacroItem(mac) => match mac.kind {
MacroKind::Bang => ItemType::Macro,
@ -236,6 +238,7 @@ impl ItemType {
ItemType::ProcAttribute => "attr",
ItemType::ProcDerive => "derive",
ItemType::TraitAlias => "traitalias",
ItemType::Attribute => "attribute",
}
}
pub(crate) fn is_method(&self) -> bool {

View file

@ -218,7 +218,7 @@ impl<'tcx> Context<'tcx> {
} else {
it.name.as_ref().unwrap().as_str()
};
if !it.is_primitive() && !it.is_keyword() {
if !it.is_fake_item() {
if !is_module {
title.push_str(" in ");
}

View file

@ -2535,6 +2535,7 @@ pub(crate) enum ItemSection {
AssociatedConstants,
ForeignTypes,
Keywords,
Attributes,
AttributeMacros,
DeriveMacros,
TraitAliases,
@ -2567,6 +2568,7 @@ impl ItemSection {
AssociatedConstants,
ForeignTypes,
Keywords,
Attributes,
AttributeMacros,
DeriveMacros,
TraitAliases,
@ -2596,6 +2598,7 @@ impl ItemSection {
Self::AssociatedConstants => "associated-consts",
Self::ForeignTypes => "foreign-types",
Self::Keywords => "keywords",
Self::Attributes => "attributes",
Self::AttributeMacros => "attributes",
Self::DeriveMacros => "derives",
Self::TraitAliases => "trait-aliases",
@ -2625,6 +2628,7 @@ impl ItemSection {
Self::AssociatedConstants => "Associated Constants",
Self::ForeignTypes => "Foreign Types",
Self::Keywords => "Keywords",
Self::Attributes => "Attributes",
Self::AttributeMacros => "Attribute Macros",
Self::DeriveMacros => "Derive Macros",
Self::TraitAliases => "Trait Aliases",
@ -2655,6 +2659,7 @@ fn item_ty_to_section(ty: ItemType) -> ItemSection {
ItemType::AssocConst => ItemSection::AssociatedConstants,
ItemType::ForeignType => ItemSection::ForeignTypes,
ItemType::Keyword => ItemSection::Keywords,
ItemType::Attribute => ItemSection::Attributes,
ItemType::ProcAttribute => ItemSection::AttributeMacros,
ItemType::ProcDerive => ItemSection::DeriveMacros,
ItemType::TraitAlias => ItemSection::TraitAliases,

View file

@ -173,6 +173,7 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp
clean::ConstantItem(..) => "Constant ",
clean::ForeignTypeItem => "Foreign Type ",
clean::KeywordItem => "Keyword ",
clean::AttributeItem => "Attribute ",
clean::TraitAliasItem(..) => "Trait Alias ",
_ => {
// We don't generate pages for any other type.
@ -193,7 +194,7 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp
let src_href =
if cx.info.include_sources && !item.is_primitive() { cx.src_href(item) } else { None };
let path_components = if item.is_primitive() || item.is_keyword() {
let path_components = if item.is_fake_item() {
vec![]
} else {
let cur = &cx.current;
@ -252,7 +253,9 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp
clean::ForeignTypeItem => {
write!(buf, "{}", item_foreign_type(cx, item))
}
clean::KeywordItem => write!(buf, "{}", item_keyword(cx, item)),
clean::KeywordItem | clean::AttributeItem => {
write!(buf, "{}", item_keyword_or_attribute(cx, item))
}
clean::TraitAliasItem(ta) => {
write!(buf, "{}", item_trait_alias(cx, item, ta))
}
@ -2151,7 +2154,7 @@ fn item_foreign_type(cx: &Context<'_>, it: &clean::Item) -> impl fmt::Display {
})
}
fn item_keyword(cx: &Context<'_>, it: &clean::Item) -> impl fmt::Display {
fn item_keyword_or_attribute(cx: &Context<'_>, it: &clean::Item) -> impl fmt::Display {
document(cx, it, None, HeadingOffset::H2)
}

View file

@ -75,6 +75,7 @@ nav.sub {
--function-link-color: #ad7c37;
--macro-link-color: #068000;
--keyword-link-color: #3873ad;
--attribute-link-color: #3873ad;
--mod-link-color: #3873ad;
--link-color: #3873ad;
--sidebar-link-color: #356da4;
@ -180,6 +181,7 @@ nav.sub {
--function-link-color: #2bab63;
--macro-link-color: #09bd00;
--keyword-link-color: #d2991d;
--attribute-link-color: #d2991d;
--mod-link-color: #d2991d;
--link-color: #d2991d;
--sidebar-link-color: #fdbf35;

View file

@ -400,6 +400,10 @@ span.keyword, a.keyword {
color: var(--keyword-link-color);
}
span.attribute, a.attribute {
color: var(--attribute-link-color);
}
a {
color: var(--link-color);
text-decoration: none;
@ -3190,6 +3194,7 @@ by default.
--function-link-color: #ad7c37;
--macro-link-color: #068000;
--keyword-link-color: #3873ad;
--attribute-link-color: #3873ad;
--mod-link-color: #3873ad;
--link-color: #3873ad;
--sidebar-link-color: #356da4;
@ -3294,6 +3299,7 @@ by default.
--function-link-color: #2bab63;
--macro-link-color: #09bd00;
--keyword-link-color: #d2991d;
--attribute-link-color: #d2991d;
--mod-link-color: #d2991d;
--link-color: #d2991d;
--sidebar-link-color: #fdbf35;
@ -3407,6 +3413,7 @@ Original by Dempfi (https://github.com/dempfi/ayu)
--function-link-color: #fdd687;
--macro-link-color: #a37acc;
--keyword-link-color: #39afd7;
--attribute-link-color: #39afd7;
--mod-link-color: #39afd7;
--link-color: #39afd7;
--sidebar-link-color: #53b1db;

View file

@ -790,6 +790,7 @@ function preLoadCss(cssUrl) {
//block("associatedconstant", "associated-consts", "Associated Constants");
block("foreigntype", "foreign-types", "Foreign Types");
block("keyword", "keywords", "Keywords");
block("attribute", "attributes", "Attributes");
block("attr", "attributes", "Attribute Macros");
block("derive", "derives", "Derive Macros");
block("traitalias", "trait-aliases", "Trait Aliases");

View file

@ -119,6 +119,7 @@ const itemTypes = [
"derive",
"traitalias", // 25
"generic",
"attribute",
];
// used for special search precedence
@ -2058,7 +2059,7 @@ class DocSearch {
displayPath = item.modulePath + "::";
href = this.rootPath + item.modulePath.replace(/::/g, "/") +
"/index.html#reexport." + name;
} else if (type === "primitive" || type === "keyword") {
} else if (type === "primitive" || type === "keyword" || type === "attribute") {
displayPath = "";
exactPath = "";
href = this.rootPath + path.replace(/::/g, "/") +
@ -4560,6 +4561,8 @@ const longItemTypes = [
"attribute macro",
"derive macro",
"trait alias",
"",
"attribute",
];
// @ts-expect-error
let currentResults;

View file

@ -52,7 +52,7 @@ impl JsonRenderer<'_> {
let clean::ItemInner { name, item_id, .. } = *item.inner;
let id = self.id_from_item(item);
let inner = match item.kind {
clean::KeywordItem => return None,
clean::KeywordItem | clean::AttributeItem => return None,
clean::StrippedItem(ref inner) => {
match &**inner {
// We document stripped modules as with `Module::is_stripped` set to
@ -85,7 +85,7 @@ impl JsonRenderer<'_> {
fn ids(&self, items: &[clean::Item]) -> Vec<Id> {
items
.iter()
.filter(|i| !i.is_stripped() && !i.is_keyword())
.filter(|i| !i.is_stripped() && !i.is_keyword() && !i.is_attribute())
.map(|i| self.id_from_item(i))
.collect()
}
@ -93,7 +93,10 @@ impl JsonRenderer<'_> {
fn ids_keeping_stripped(&self, items: &[clean::Item]) -> Vec<Option<Id>> {
items
.iter()
.map(|i| (!i.is_stripped() && !i.is_keyword()).then(|| self.id_from_item(i)))
.map(|i| {
(!i.is_stripped() && !i.is_keyword() && !i.is_attribute())
.then(|| self.id_from_item(i))
})
.collect()
}
}
@ -332,8 +335,8 @@ fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> ItemEnum
bounds: b.into_json(renderer),
type_: Some(t.item_type.as_ref().unwrap_or(&t.type_).into_json(renderer)),
},
// `convert_item` early returns `None` for stripped items and keywords.
KeywordItem => unreachable!(),
// `convert_item` early returns `None` for stripped items, keywords and attributes.
KeywordItem | AttributeItem => unreachable!(),
StrippedItem(inner) => {
match inner.as_ref() {
ModuleItem(m) => ItemEnum::Module(Module {
@ -887,6 +890,7 @@ impl FromClean<ItemType> for ItemKind {
AssocType => ItemKind::AssocType,
ForeignType => ItemKind::ExternType,
Keyword => ItemKind::Keyword,
Attribute => ItemKind::Attribute,
TraitAlias => ItemKind::TraitAlias,
ProcAttribute => ItemKind::ProcAttribute,
ProcDerive => ItemKind::ProcDerive,

View file

@ -67,6 +67,7 @@ pub(crate) fn should_have_doc_example(cx: &DocContext<'_>, item: &clean::Item) -
| clean::ImportItem(_)
| clean::PrimitiveItem(_)
| clean::KeywordItem
| clean::AttributeItem
| clean::ModuleItem(_)
| clean::TraitAliasItem(_)
| clean::ForeignFunctionItem(..)

View file

@ -19,7 +19,7 @@ use rustc_hir::{Mutability, Safety};
use rustc_middle::ty::{Ty, TyCtxt};
use rustc_middle::{bug, span_bug, ty};
use rustc_resolve::rustdoc::{
MalformedGenerics, has_primitive_or_keyword_docs, prepare_to_doc_link_resolution,
MalformedGenerics, has_primitive_or_keyword_or_attribute_docs, prepare_to_doc_link_resolution,
source_span_for_markdown_range, strip_generics_from_path,
};
use rustc_session::config::CrateType;
@ -1073,7 +1073,7 @@ impl LinkCollector<'_, '_> {
&& let Some(def_id) = item.item_id.as_def_id()
&& let Some(def_id) = def_id.as_local()
&& !self.cx.tcx.effective_visibilities(()).is_exported(def_id)
&& !has_primitive_or_keyword_docs(&item.attrs.other_attrs)
&& !has_primitive_or_keyword_or_attribute_docs(&item.attrs.other_attrs)
{
// Skip link resolution for non-exported items.
return;

View file

@ -106,7 +106,8 @@ impl DocFolder for StabilityPropagator<'_, '_> {
| ItemKind::RequiredAssocTypeItem(..)
| ItemKind::AssocTypeItem(..)
| ItemKind::PrimitiveItem(..)
| ItemKind::KeywordItem => own_stability,
| ItemKind::KeywordItem
| ItemKind::AttributeItem => own_stability,
ItemKind::StrippedItem(..) => unreachable!(),
}

View file

@ -133,6 +133,8 @@ impl DocFolder for Stripper<'_, '_> {
// Keywords are never stripped
clean::KeywordItem => {}
// Attributes are never stripped
clean::AttributeItem => {}
}
let fastreturn = match i.kind {

View file

@ -49,7 +49,8 @@ pub(crate) trait DocVisitor<'a>: Sized {
| ImplAssocConstItem(..)
| RequiredAssocTypeItem(..)
| AssocTypeItem(..)
| KeywordItem => {}
| KeywordItem
| AttributeItem => {}
}
}

View file

@ -37,8 +37,8 @@ pub type FxHashMap<K, V> = HashMap<K, V>; // re-export for use in src/librustdoc
// will instead cause conflicts. See #94591 for more. (This paragraph and the "Latest feature" line
// are deliberately not in a doc comment, because they need not be in public docs.)
//
// Latest feature: Add Attribute::MacroUse
pub const FORMAT_VERSION: u32 = 55;
// Latest feature: Add `ItemKind::Attribute`.
pub const FORMAT_VERSION: u32 = 56;
/// The root of the emitted JSON blob.
///
@ -552,6 +552,11 @@ pub enum ItemKind {
/// [`Item`]s of this kind only come from the come library and exist solely
/// to carry documentation for the respective keywords.
Keyword,
/// An attribute declaration.
///
/// [`Item`]s of this kind only come from the core library and exist solely
/// to carry documentation for the respective builtin attributes.
Attribute,
}
/// Specific fields of an item.

View file

@ -26,6 +26,7 @@ pub(crate) enum Kind {
AssocType,
Primitive,
Keyword,
Attribute,
// Not in ItemKind
ProcMacro,
}
@ -53,6 +54,7 @@ impl Kind {
ExternType => true,
// FIXME(adotinthevoid): I'm not sure if these are correct
Attribute => false,
Keyword => false,
ProcAttribute => false,
ProcDerive => false,
@ -109,6 +111,7 @@ impl Kind {
Kind::Primitive => false,
Kind::Keyword => false,
Kind::ProcMacro => false,
Kind::Attribute => false,
}
}
@ -163,6 +166,7 @@ impl Kind {
match s.kind {
ItemKind::AssocConst => AssocConst,
ItemKind::AssocType => AssocType,
ItemKind::Attribute => Attribute,
ItemKind::Constant => Constant,
ItemKind::Enum => Enum,
ItemKind::ExternCrate => ExternCrate,

View file

@ -9,7 +9,7 @@ show-text: true
define-function: (
"check-colors",
[theme, mod, macro, struct, enum, trait, fn, type, union, keyword,
sidebar, sidebar_current, sidebar_current_background],
attribute, sidebar, sidebar_current, sidebar_current_background],
block {
call-function: ("switch-theme", {"theme": |theme|})
// Checking results colors.
@ -22,6 +22,7 @@ define-function: (
assert-css: (".item-table .type", {"color": |type|}, ALL)
assert-css: (".item-table .union", {"color": |union|}, ALL)
assert-css: (".item-table .keyword", {"color": |keyword|}, ALL)
assert-css: (".item-table .attribute", {"color": |attribute|}, ALL)
// Checking sidebar elements.
assert-css: (
".sidebar-elems li:not(.current) a",
@ -58,6 +59,7 @@ call-function: (
"type": "#ffa0a5",
"union": "#ffa0a5",
"keyword": "#39afd7",
"attribute": "#39afd7",
"sidebar": "#53b1db",
"sidebar_current": "#ffb44c",
"sidebar_current_background": "transparent",
@ -76,6 +78,7 @@ call-function: (
"type": "#2dbfb8",
"union": "#2dbfb8",
"keyword": "#d2991d",
"attribute": "#d2991d",
"sidebar": "#fdbf35",
"sidebar_current": "#fdbf35",
"sidebar_current_background": "#444",
@ -94,6 +97,7 @@ call-function: (
"type": "#ad378a",
"union": "#ad378a",
"keyword": "#3873ad",
"attribute": "#3873ad",
"sidebar": "#356da4",
"sidebar_current": "#356da4",
"sidebar_current_background": "#fff",

View file

@ -65,3 +65,12 @@ assert-css: (
"#keywords + .item-table dd",
{"font-family": '"Source Serif 4", NanumBarunGothic, serif'},
)
// attributes
assert-css: (
"#attributes + .item-table dt a",
{"font-family": '"Fira Sans", Arial, NanumBarunGothic, sans-serif'},
)
assert-css: (
"#attributes + .item-table dd",
{"font-family": '"Source Serif 4", NanumBarunGothic, serif'},
)

View file

@ -7,7 +7,8 @@ define-function: (
[
theme, count_color, desc_color, path_color, bottom_border_color, keyword_color,
struct_color, associatedtype_color, tymethod_color, method_color, structfield_color,
structfield_hover_color, macro_color, fn_color, hover_path_color, hover_background, grey
structfield_hover_color, macro_color, fn_color, hover_path_color, hover_background,
attribute_color, grey
],
block {
call-function: ("switch-theme", {"theme": |theme|})
@ -46,6 +47,11 @@ define-function: (
"color": |keyword_color|,
"hover_color": |keyword_color|,
})
call-function: ("check-result-color", {
"result_kind": "attribute",
"color": |attribute_color|,
"hover_color": |attribute_color|,
})
call-function: ("check-result-color", {
"result_kind": "struct",
"color": |struct_color|,
@ -155,6 +161,7 @@ call-function: ("check-search-color", {
"path_color": "#0096cf",
"bottom_border_color": "#aaa3",
"keyword_color": "#39afd7",
"attribute_color": "#39afd7",
"struct_color": "#ffa0a5",
"associatedtype_color": "#39afd7",
"tymethod_color": "#fdd687",
@ -176,6 +183,7 @@ call-function: ("check-search-color", {
"path_color": "#ddd",
"bottom_border_color": "#aaa3",
"keyword_color": "#d2991d",
"attribute_color": "#d2991d",
"struct_color": "#2dbfb8",
"associatedtype_color": "#d2991d",
"tymethod_color": "#2bab63",
@ -197,6 +205,7 @@ call-function: ("check-search-color", {
"path_color": "#000",
"bottom_border_color": "#aaa3",
"keyword_color": "#3873ad",
"attribute_color": "#3873ad",
"struct_color": "#ad378a",
"associatedtype_color": "#3873ad",
"tymethod_color": "#ad7c37",

View file

@ -12,6 +12,7 @@ define-function: (
enum_hover_background, union, union_hover, union_hover_background, trait, trait_hover,
trait_hover_background, fn, fn_hover, fn_hover_background, type, type_hover,
type_hover_background, keyword, keyword_hover, keyword_hover_background,
attribute, attribute_hover, attribute_hover_background,
],
block {
call-function: ("switch-theme", {"theme": |theme|})
@ -85,6 +86,16 @@ define-function: (
".sidebar .block.keyword a:hover",
{"color": |keyword_hover|, "background-color": |keyword_hover_background|},
)
// Attribute
assert-css: (
".sidebar .block.attribute a",
{"color": |attribute|, "background-color": "rgba(0, 0, 0, 0)"},
)
move-cursor-to: ".sidebar .block.attribute a"
assert-css: (
".sidebar .block.attribute a:hover",
{"color": |attribute_hover|, "background-color": |attribute_hover_background|},
)
}
)
@ -113,6 +124,9 @@ call-function: (
"keyword": "#53b1db",
"keyword_hover": "#ffb44c",
"keyword_hover_background": "transparent",
"attribute": "#53b1db",
"attribute_hover": "#ffb44c",
"attribute_hover_background": "transparent",
}
)
call-function: (
@ -140,6 +154,9 @@ call-function: (
"keyword": "#fdbf35",
"keyword_hover": "#fdbf35",
"keyword_hover_background": "#444",
"attribute": "#fdbf35",
"attribute_hover": "#fdbf35",
"attribute_hover_background": "#444",
}
)
call-function: (
@ -167,5 +184,8 @@ call-function: (
"keyword": "#356da4",
"keyword_hover": "#356da4",
"keyword_hover_background": "#fff",
"attribute": "#356da4",
"attribute_hover": "#356da4",
"attribute_hover_background": "#fff",
}
)

View file

@ -161,6 +161,10 @@ pub enum AnEnum {
/// Some keyword.
pub mod keyword {}
#[doc(attribute = "forbid")]
/// Some attribute.
pub mod repr {}
/// Just some type alias.
pub type SomeType = u32;

View file

@ -0,0 +1,18 @@
// Doc attributes (`#[doc(attribute = "...")]` should not be generated in rustdoc JSON output
// and this test ensures it.
#![feature(rustdoc_internals)]
#![no_std]
//@ !has "$.index[?(@.name=='repr')]"
//@ has "$.index[?(@.name=='foo')]"
#[doc(attribute = "repr")]
/// this is a test!
pub mod foo {}
//@ !has "$.index[?(@.name=='forbid')]"
//@ !has "$.index[?(@.name=='bar')]"
#[doc(attribute = "forbid")]
/// hello
mod bar {}

View file

@ -0,0 +1,7 @@
// This is currently not supported but should be!
#![feature(rustdoc_internals)]
#[doc(attribute = "diagnostic::do_not_recommend")] //~ ERROR
/// bla
mod yup {}

View file

@ -0,0 +1,10 @@
error: nonexistent builtin attribute `diagnostic::do_not_recommend` used in `#[doc(attribute = "...")]`
--> $DIR/doc-attribute-unsupported.rs:5:19
|
LL | #[doc(attribute = "diagnostic::do_not_recommend")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: only existing builtin attributes are allowed in core/std
error: aborting due to 1 previous error

View file

@ -0,0 +1,10 @@
// Testing the output when an invalid builtin attribute is passed as value
// to `doc(attribute = "...")`.
#![feature(rustdoc_internals)]
#[doc(attribute = "foo df")] //~ ERROR
mod foo {}
#[doc(attribute = "fooyi")] //~ ERROR
mod foo2 {}

View file

@ -0,0 +1,18 @@
error: nonexistent builtin attribute `foo df` used in `#[doc(attribute = "...")]`
--> $DIR/invalid-attribute.rs:6:19
|
LL | #[doc(attribute = "foo df")]
| ^^^^^^^^
|
= help: only existing builtin attributes are allowed in core/std
error: nonexistent builtin attribute `fooyi` used in `#[doc(attribute = "...")]`
--> $DIR/invalid-attribute.rs:9:19
|
LL | #[doc(attribute = "fooyi")]
| ^^^^^^^
|
= help: only existing builtin attributes are allowed in core/std
error: aborting due to 2 previous errors

View file

@ -0,0 +1,24 @@
// Test checking the `#[doc(attribute = "...")]` attribute.
#![crate_name = "foo"]
#![feature(rustdoc_internals)]
//@ has foo/index.html '//h2[@id="attributes"]' 'Attributes'
//@ has foo/index.html '//a[@href="attribute.no_mangle.html"]' 'no_mangle'
//@ has foo/index.html '//div[@class="sidebar-elems"]//li/a' 'Attributes'
//@ has foo/index.html '//div[@class="sidebar-elems"]//li/a/@href' '#attributes'
//@ has foo/attribute.no_mangle.html '//h1' 'Attribute no_mangle'
//@ has foo/attribute.no_mangle.html '//section[@id="main-content"]//div[@class="docblock"]//p' 'this is a test!'
//@ has foo/index.html '//a/@href' '../foo/index.html'
//@ !has foo/foo/index.html
//@ !has-dir foo/foo
//@ !has foo/index.html '//span' '🔒'
#[doc(attribute = "no_mangle")]
/// this is a test!
mod foo{}
//@ has foo/attribute.repr.html '//section[@id="main-content"]//div[@class="docblock"]//p' 'hello'
#[doc(attribute = "repr")]
/// hello
mod bar {}

View file

@ -2,6 +2,10 @@
/// wonderful
mod foo {}
#[doc(attribute = "repr")] //~ ERROR: `#[doc(attribute)]` is meant for internal use only
/// wonderful
mod foo2 {}
trait Mine {}
#[doc(fake_variadic)] //~ ERROR: `#[doc(fake_variadic)]` is meant for internal use only

View file

@ -8,8 +8,18 @@ LL | #[doc(keyword = "match")]
= help: add `#![feature(rustdoc_internals)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error[E0658]: `#[doc(attribute)]` is meant for internal use only
--> $DIR/feature-gate-rustdoc_internals.rs:5:1
|
LL | #[doc(attribute = "repr")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #90418 <https://github.com/rust-lang/rust/issues/90418> for more information
= help: add `#![feature(rustdoc_internals)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error[E0658]: `#[doc(fake_variadic)]` is meant for internal use only
--> $DIR/feature-gate-rustdoc_internals.rs:7:1
--> $DIR/feature-gate-rustdoc_internals.rs:11:1
|
LL | #[doc(fake_variadic)]
| ^^^^^^^^^^^^^^^^^^^^^
@ -19,7 +29,7 @@ LL | #[doc(fake_variadic)]
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error[E0658]: `#[doc(search_unbox)]` is meant for internal use only
--> $DIR/feature-gate-rustdoc_internals.rs:10:1
--> $DIR/feature-gate-rustdoc_internals.rs:14:1
|
LL | #[doc(search_unbox)]
| ^^^^^^^^^^^^^^^^^^^^
@ -28,6 +38,6 @@ LL | #[doc(search_unbox)]
= help: add `#![feature(rustdoc_internals)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error: aborting due to 3 previous errors
error: aborting due to 4 previous errors
For more information about this error, try `rustc --explain E0658`.