Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add new doc(attribute = "...") attribute #142472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
bors merged 5 commits into rust-lang:master from GuillaumeGomez:doc-attribute-attribute
Aug 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/rustc_ast_passes/src/feature_gate.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions compiler/rustc_passes/messages.ftl
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +149 to +150
Copy link
Member

@fmease fmease Aug 13, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(minor) I'm not so keen on hard-coding core and std here but eh

Copy link
Member Author

@GuillaumeGomez GuillaumeGomez Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the same format as the existing passes_doc_keyword_not_keyword. If someone cares enough about it, I guess we'll see a follow-up PR hehe.


passes_doc_cfg_hide_takes_list =
`#[doc(cfg_hide(...))]` takes a list of attributes

Expand Down Expand Up @@ -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

Expand Down
79 changes: 68 additions & 11 deletions compiler/rustc_passes/src/check_attr.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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>,

Expand Down Expand Up @@ -851,17 +866,27 @@ 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
// `#[doc(keyword = "SelfTy")` attribute in `library/std/src/keyword_docs.rs`.
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)
Copy link
Member

@fmease fmease Aug 13, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this interact with "attribute namespaces" (I'm not referring to attribute tools) like #[diagnostic::{on_unimplemented,do_not_recommend}]? Does it support them? I haven't investigated if they're smh. contained in this map or not.

Copy link
Member Author

@GuillaumeGomez GuillaumeGomez Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I missed something, it's only supposed to be used on attributes provided "by default". Are there attributes with namespaces in this case?

Copy link
Member

@fmease fmease Aug 28, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, #[diagnostic::on_unimplemented(...)] and #[diagnostic::do_not_recommend] are "provided by default" aka built-in (namespaced) attributes.

It doesn't matter to me if your PR doesn't support them, we can always do that in a follow-up, I'm just curious to know what happens if you put those inside #[doc(attribute = "...")].

Copy link
Member Author

@GuillaumeGomez GuillaumeGomez Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious as well. Gonna add a test for it so we actually know. :)

If not supported, gonna do that in a follow-up then.

Copy link
Member Author

@GuillaumeGomez GuillaumeGomez Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not supported, added a FIXME and I'll send a follow-up.

fmease reacted with thumbs up emoji
}

let value = match meta.value_str() {
Copy link
Contributor

@jdonszelmann jdonszelmann Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sigh, I'll put the doc attribute high up on my todo list to get a proper parser... (this is fine for now)

lolbinarycat reacted with thumbs up emoji
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) {
Expand All @@ -871,20 +896,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,
});
}
}
}
}

Expand Down Expand Up @@ -1144,7 +1187,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,
);
}
}

Expand Down
19 changes: 15 additions & 4 deletions compiler/rustc_passes/src/errors.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_resolve/src/late.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions compiler/rustc_resolve/src/rustdoc.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -373,16 +373,16 @@ 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;
} else if attr.has_name(sym::doc)
&& 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;
}
}
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ symbols! {
att_syntax,
attr,
attr_literals,
attribute,
attributes,
audit_that,
augmented_assignments,
Expand Down
19 changes: 18 additions & 1 deletion src/doc/rustdoc/src/unstable-features.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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.
Copy link
Contributor

@jdonszelmann jdonszelmann Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we make sure this stays in sync with the compiler's view of attributes? Is the plan to keep this manual, or could we somehow generate this from for example rustc_attr_data_structures

Copy link
Member Author

@GuillaumeGomez GuillaumeGomez Jun 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like keywords: we can only check that the attribute is used on an existing one. If new ones are added, there is no check for that.

We could add a tidy check for it though.

Copy link
Contributor

@jdonszelmann jdonszelmann Jun 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that'd be nice

#[doc(attribute = "repr")]
mod empty_mod {}
```

### Use the Rust logo as the crate logo

This is for official Rust project use only.
Expand Down
49 changes: 42 additions & 7 deletions src/librustdoc/clean/types.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -983,7 +1017,8 @@ impl ItemKind {
| RequiredAssocTypeItem(..)
| AssocTypeItem(..)
| StrippedItem(_)
| KeywordItem => [].iter(),
| KeywordItem
| AttributeItem => [].iter(),
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/librustdoc/clean/utils.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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)| {
Expand All @@ -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)) }
Expand Down
3 changes: 2 additions & 1 deletion src/librustdoc/fold.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ pub(crate) trait DocFolder: Sized {
| ImplAssocConstItem(..)
| RequiredAssocTypeItem(..)
| AssocTypeItem(..)
| KeywordItem => kind,
| KeywordItem
| AttributeItem => kind,
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/librustdoc/formats/cache.rs
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading

AltStyle によって変換されたページ (->オリジナル) /