Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ impl<'a> Visitor<'a> for PostExpansionVisitor<'a> {
auto_cfg => doc_cfg
masked => doc_masked
notable_trait => doc_notable_trait
label_trait => doc_label_trait
}
"meant for internal use only" {
attribute => rustdoc_internals
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_attr_parsing/src/attributes/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ impl DocParser {
match path.word_sym() {
Some(sym::alias) => self.parse_alias(cx, path, args),
Some(sym::hidden) => no_args!(hidden),
Some(sym::label_trait) => no_args!(label_trait),
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),
Expand Down Expand Up @@ -688,6 +689,7 @@ impl AttributeParser for DocParser {
"masked",
"cfg",
"notable_trait",
"label_trait",
"keyword",
"fake_variadic",
"search_unbox",
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_feature/src/unstable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ declare_features! (
(unstable, box_patterns, "1.0.0", Some(29641)),
/// Allows builtin # foo() syntax
(internal, builtin_syntax, "1.71.0", Some(110680)),
/// Allows `#[doc(label_trait)]`.
(unstable, doc_label_trait, "CURRENT_RUSTC_VERSION", Some(156865)),
/// Allows `#[doc(notable_trait)]`.
/// Renamed from `doc_spotlight`.
(unstable, doc_notable_trait, "1.52.0", Some(45040)),
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_hir/src/attrs/data_structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ pub struct DocAttribute {

pub aliases: FxIndexMap<Symbol, Span>,
pub hidden: Option<Span>,
pub label_trait: Option<Span>,
// Because we need to emit the error if there is more than one `inline` attribute on an item
// at the same time as the other doc attributes, we store a list instead of using `Option`.
pub inline: ThinVec<(DocInline, Span)>,
Expand Down Expand Up @@ -566,6 +567,7 @@ impl<E: rustc_span::SpanEncoder> rustc_serialize::Encodable<E> for DocAttribute
first_span,
aliases,
hidden,
label_trait,
inline,
cfg,
auto_cfg,
Expand All @@ -589,6 +591,7 @@ impl<E: rustc_span::SpanEncoder> rustc_serialize::Encodable<E> for DocAttribute
rustc_serialize::Encodable::<E>::encode(first_span, encoder);
rustc_serialize::Encodable::<E>::encode(aliases, encoder);
rustc_serialize::Encodable::<E>::encode(hidden, encoder);
rustc_serialize::Encodable::<E>::encode(label_trait, encoder);

// FIXME: The `doc(inline)` attribute is never encoded, but is it actually the right thing
// to do? I suspect the condition was broken, should maybe instead not encode anything if we
Expand Down
5 changes: 5 additions & 0 deletions compiler/rustc_middle/src/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,11 @@ rustc_queries! {
separate_provide_extern
}

/// Determines whether an item is annotated with `#[doc(label_trait)]`.
query is_doc_label_trait(def_id: DefId) -> bool {
desc { "checking whether `{}` is `doc(label_trait)`", tcx.def_path_str(def_id) }
}

/// Determines whether an item is annotated with `#[doc(notable_trait)]`.
query is_doc_notable_trait(def_id: DefId) -> bool {
desc { "checking whether `{}` is `doc(notable_trait)`", tcx.def_path_str(def_id) }
Expand Down
6 changes: 6 additions & 0 deletions compiler/rustc_middle/src/ty/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,11 @@ pub fn is_doc_notable_trait(tcx: TyCtxt<'_>, def_id: DefId) -> bool {
find_attr!(tcx, def_id, Doc(doc) if doc.notable_trait.is_some())
}

/// Determines whether an item is annotated with `doc(notable_trait)`.
pub fn is_doc_label_trait(tcx: TyCtxt<'_>, def_id: DefId) -> bool {
find_attr!(tcx, def_id, Doc(doc) if doc.label_trait.is_some())
}

/// Determines whether an item is an intrinsic (which may be via Abi or via the `rustc_intrinsic` attribute).
///
/// We double check the feature gate here because whether a function may be defined as an intrinsic causes
Expand Down Expand Up @@ -1693,6 +1698,7 @@ pub fn provide(providers: &mut Providers) {
*providers = Providers {
reveal_opaque_types_in_bounds,
is_doc_hidden,
is_doc_label_trait,
is_doc_notable_trait,
intrinsic_raw,
..*providers
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_passes/src/check_attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,8 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
// valid pretty much anywhere, not checked here?
// FIXME: should we?
hidden: _,
// FIXME: valid for traits, should be checked in attr_parsing
label_trait: _,
Comment on lines +1040 to +1041
Copy link
Copy Markdown
Author

@ThierryBerger ThierryBerger May 28, 2026

Choose a reason for hiding this comment

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

should we have strong verification on which items those are applied?

View changes since the review

inline,
// FIXME: currently unchecked
cfg: _,
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,7 @@ symbols! {
doc_cfg,
doc_cfg_hide,
doc_keyword,
doc_label_trait,
doc_masked,
doc_notable_trait,
doc_primitive,
Expand Down Expand Up @@ -1141,6 +1142,7 @@ symbols! {
kreg0,
label,
label_break_value,
label_trait,
lahfsahf_target_feature,
lang,
lang_items,
Expand Down
12 changes: 12 additions & 0 deletions src/doc/rustdoc/src/unstable-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ It is also not emitted for foreign items, aliases, extern crates and imports.
These features operate by extending the `#[doc]` attribute, and thus can be caught by the compiler
and enabled with a `#![feature(...)]` attribute in your crate.

### Making your trait more discoverable

* Tracking issue: [#156865](https://github.com/rust-lang/rust/issues/156865)

Important traits can be difficult to discover when lost in the noise.
This `#![feature(doc_label_trait)]` allows you to tag traits important for your code base.

The traits with the attribute #![doc(label_trait)]` are rendered with a colored badge at the top of their dedicated page.

Consider lookint into the `notable_trait` unstable attribure, which help with
discoverability in other ways.

### Adding your trait to the "Notable traits" dialog

* Tracking issue: [#45040](https://github.com/rust-lang/rust/issues/45040)
Expand Down
1 change: 1 addition & 0 deletions src/librustdoc/clean/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2789,6 +2789,7 @@ fn add_without_unwanted_attributes<'hir>(
first_span: _,
aliases,
hidden,
label_trait: _,
inline,
cfg,
auto_cfg: _,
Expand Down
3 changes: 3 additions & 0 deletions src/librustdoc/clean/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,9 @@ impl Trait {
pub(crate) fn is_auto(&self, tcx: TyCtxt<'_>) -> bool {
tcx.trait_is_auto(self.def_id)
}
pub(crate) fn is_label_trait(&self, tcx: TyCtxt<'_>) -> bool {
tcx.is_doc_label_trait(self.def_id)
}
pub(crate) fn is_notable_trait(&self, tcx: TyCtxt<'_>) -> bool {
tcx.is_doc_notable_trait(self.def_id)
}
Expand Down
43 changes: 42 additions & 1 deletion src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ mod type_layout;
mod write_shared;

use std::borrow::Cow;
use std::collections::VecDeque;
use std::collections::{BTreeMap, VecDeque};
use std::fmt::{self, Display as _, Write};
use std::iter::Peekable;
use std::path::PathBuf;
Expand Down Expand Up @@ -1770,6 +1770,47 @@ fn notable_traits_json<'a>(tys: impl Iterator<Item = &'a clean::Type>, cx: &Cont
serde_json::to_string(&mp).expect("serialize (string, string) -> json object cannot fail")
}

pub(crate) struct LabelTraitInfo {
pub name: String,
pub full_path: String,
/// Relative URL to the trait page, or `None` if it cannot be linked.
pub href: Option<String>,
}

/// Returns all `#[doc(label_trait)]` traits that `item` implements.
pub(crate) fn label_traits_for_item(item: &clean::Item, cx: &Context<'_>) -> Vec<LabelTraitInfo> {
let Some(did) = item.def_id() else { return Vec::new() };

if Some(did) == cx.tcx().lang_items().owned_box()
|| Some(did) == cx.tcx().lang_items().pin_type()
{
return Vec::new();
}

let Some(impls) = cx.cache().impls.get(&did) else { return Vec::new() };

impls
.iter()
.map(Impl::inner_impl)
.filter(|impl_| impl_.polarity == ty::ImplPolarity::Positive)
.filter_map(|impl_| {
let path_ = impl_.trait_.as_ref()?;
let trait_did = path_.def_id();
if !cx.cache().traits.get(&trait_did)?.is_label_trait(cx.tcx()) {
return None;
}
let name = cx.tcx().item_name(trait_did).to_string();
let (full_path, href) = match href(trait_did, cx) {
Ok(info) => (join_path_syms(&info.rust_path), Some(info.url)),
Err(_) => (cx.tcx().def_path_str(trait_did), None),
};
Some((name.clone(), LabelTraitInfo { name, full_path, href }))
})
.collect::<BTreeMap<String, LabelTraitInfo>>()
.into_values()
.collect()
}

#[derive(Clone, Copy, Debug)]
struct ImplRenderingParameters {
show_def_docs: bool,
Expand Down
39 changes: 38 additions & 1 deletion src/librustdoc/html/render/print_item.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::fmt::{self, Display, Write as _};
use std::hash::{Hash, Hasher};
use std::iter;

use askama::Template;
Expand Down Expand Up @@ -37,7 +39,7 @@ use crate::html::format::{
};
use crate::html::markdown::{HeadingOffset, MarkdownSummaryLine};
use crate::html::render::sidebar::filters;
use crate::html::render::{document_full, document_item_info};
use crate::html::render::{document_full, document_item_info, label_traits_for_item};
use crate::html::url_parts_builder::UrlPartsBuilder;

const ITEM_TABLE_OPEN: &str = "<dl class=\"item-table\">";
Expand All @@ -50,6 +52,15 @@ struct PathComponent {
name: Symbol,
}

struct LabelTraitVars {
name: String,
full_path: String,
/// Relative URL to the trait page, or empty when not linkable.
href: String,
Comment on lines +58 to +59
Copy link
Copy Markdown
Author

@ThierryBerger ThierryBerger May 28, 2026

Choose a reason for hiding this comment

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

should it be an option?

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should always link to the trait, so I'd say no.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Maybe doc_hidden would make us not be able to do so ? It would be quite far-fetched to add label_trait to doc_hidden but... Maybe other cases like... visibility? not sure.

/// Pre-rendered `style="..."` attribute.
style_attr: String,
}

#[derive(Template)]
#[template(path = "print_item.html")]
struct ItemVars<'a> {
Expand All @@ -58,6 +69,7 @@ struct ItemVars<'a> {
item_type: &'a str,
path_components: Vec<PathComponent>,
stability_since_raw: &'a str,
impl_label_traits: Vec<LabelTraitVars>,
src_href: Option<&'a str>,
}

Expand Down Expand Up @@ -111,6 +123,30 @@ 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 impl_label_traits: Vec<LabelTraitVars> = label_traits_for_item(item, cx)
.into_iter()
.map(|info| {
// Stable per-trait color from a hash of the DefId so the same
// trait gets the same badge color across pages.
// This won't be stable between releases though.
let mut h = DefaultHasher::new();
info.full_path.hash(&mut h);
let v = h.finish();
let style_attr = format!(
"style=\"background: rgb({}, {}, {})\"",
v as u8,
(v >> 8) as u8,
(v >> 16) as u8,
);
LabelTraitVars {
name: info.name,
full_path: info.full_path,
href: info.href.unwrap_or_default(),
style_attr,
}
})
.collect();

let path_components = if item.is_fake_item() {
vec![]
} else {
Expand All @@ -134,6 +170,7 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp
item_type: &item.type_().to_string(),
path_components,
stability_since_raw: &stability_since_raw,
impl_label_traits,
src_href: src_href.as_deref(),
};

Expand Down
19 changes: 19 additions & 0 deletions src/librustdoc/html/static/css/rustdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -1644,6 +1644,25 @@ so that we can apply CSS-filters to change the arrow color in themes */
font-size: initial;
}

.impl-label-trait-full-badge-container {
padding: 0.5rem 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}

.impl-label-trait-full-badge {
display: flex;
align-items: center;
width: fit-content;
height: 1.5rem;
padding: 0 0.5rem;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: normal;
color: white;
}

.rightside {
padding-left: 12px;
float: right;
Expand Down
13 changes: 11 additions & 2 deletions src/librustdoc/html/templates/print_item.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
<div class="rustdoc-breadcrumbs">
{% for (i, component) in path_components.iter().enumerate() %}
{% if i != 0 %}
::<wbr>
::<wbr>
{% endif %}
<a href="{{component.path|safe}}index.html">{{component.name}}</a>
{% endfor %}
</div>
{% endif %}
<h1>
{{typ}}
<span{% if item_type != "mod" +%} class="{{item_type}}"{% endif %}>
<span{% if item_type !="mod" +%} class="{{item_type}}" {% endif %}>
Comment thread
ThierryBerger marked this conversation as resolved.
{{name|wrapped|safe}}
</span>&nbsp;{# #}
<button id="copy-path" title="Copy item path to clipboard"> {# #}
Expand All @@ -20,6 +20,15 @@ <h1>
</h1> {# #}
<rustdoc-toolbar></rustdoc-toolbar> {# #}
<span class="sub-heading">
{% if !impl_label_traits.is_empty() %}
<div class="impl-label-trait-full-badge-container">
{% for label_trait in impl_label_traits.iter() %}
<a class="impl-label-trait-full-badge" {# #} {% if !label_trait.href.is_empty()
%}href="{{label_trait.href|safe}}" {% endif %} {#+ #} title="{{label_trait.full_path}}" {#+ #}
{{label_trait.style_attr|safe}}>{{label_trait.name}}</a>
{% endfor %}
</div>
{% endif %}
{% if !stability_since_raw.is_empty() %}
{{ stability_since_raw|safe +}}
{% endif %}
Expand Down
2 changes: 2 additions & 0 deletions src/librustdoc/json/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@ fn maybe_from_hir_attr(attr: &hir::Attribute, item_id: ItemId, tcx: TyCtxt<'_>)
first_span: _,
aliases,
hidden,
label_trait,
inline,
cfg,
auto_cfg,
Expand Down Expand Up @@ -993,6 +994,7 @@ fn maybe_from_hir_attr(attr: &hir::Attribute, item_id: ItemId, tcx: TyCtxt<'_>)
ret.push(Attribute::Other(format!("#[doc(alias = {:?})]", alias.as_str())));
}
toggle_attr(&mut ret, "hidden", hidden);
toggle_attr(&mut ret, "label_trait", label_trait);
if let Some(inline) = inline.first() {
ret.push(Attribute::Other(format!(
"#[doc({})]",
Expand Down
25 changes: 25 additions & 0 deletions tests/rustdoc-html/label_trait/label-trait-badge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#![feature(doc_label_trait)]
#![crate_name = "foo"]

#[doc(label_trait)]
pub trait Labeled {}

#[doc(label_trait)]
pub trait AlsoLabeled {}

pub trait Plain {}

//@ has 'foo/struct.Tagged.html'
//@ has - '//a[@class="impl-label-trait-full-badge"][@href="trait.Labeled.html"][@title="foo::Labeled"]' 'Labeled'
// Badges are sorted by trait name, so `AlsoLabeled` precedes `Labeled`.
//@ has - '//div[@class="impl-label-trait-full-badge-container"]/a[1]' 'AlsoLabeled'
//@ has - '//div[@class="impl-label-trait-full-badge-container"]/a[2]' 'Labeled'
pub struct Tagged;
impl Labeled for Tagged {}
impl AlsoLabeled for Tagged {}
impl Plain for Tagged {}

//@ has 'foo/struct.Untagged.html'
//@ count - '//div[@class="impl-label-trait-full-badge-container"]' 0
pub struct Untagged;
impl Plain for Untagged {}
Loading
Loading