about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--compiler/rustc_ast_passes/src/feature_gate.rs1
-rw-r--r--compiler/rustc_attr_parsing/src/attributes/prelude.rs12
-rw-r--r--compiler/rustc_attr_parsing/src/parser.rs59
-rw-r--r--compiler/rustc_codegen_llvm/src/back/lto.rs32
-rw-r--r--compiler/rustc_codegen_ssa/src/back/symbol_export.rs49
-rw-r--r--compiler/rustc_codegen_ssa/src/back/write.rs13
-rw-r--r--compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp6
-rw-r--r--compiler/rustc_parse/src/parser/expr.rs4
-rw-r--r--compiler/rustc_passes/messages.ftl14
-rw-r--r--compiler/rustc_passes/src/check_attr.rs79
-rw-r--r--compiler/rustc_passes/src/errors.rs19
-rw-r--r--compiler/rustc_resolve/src/late.rs2
-rw-r--r--compiler/rustc_resolve/src/rustdoc.rs6
-rw-r--r--compiler/rustc_session/src/errors.rs24
-rw-r--r--compiler/rustc_span/src/symbol.rs1
-rw-r--r--src/bootstrap/src/utils/render_tests.rs8
-rw-r--r--src/doc/rustdoc/src/unstable-features.md19
-rw-r--r--src/librustdoc/clean/types.rs49
-rw-r--r--src/librustdoc/clean/utils.rs4
-rw-r--r--src/librustdoc/fold.rs3
-rw-r--r--src/librustdoc/formats/cache.rs3
-rw-r--r--src/librustdoc/formats/item_type.rs3
-rw-r--r--src/librustdoc/html/render/context.rs2
-rw-r--r--src/librustdoc/html/render/mod.rs5
-rw-r--r--src/librustdoc/html/render/print_item.rs9
-rw-r--r--src/librustdoc/html/static/css/noscript.css2
-rw-r--r--src/librustdoc/html/static/css/rustdoc.css7
-rw-r--r--src/librustdoc/html/static/js/main.js1
-rw-r--r--src/librustdoc/html/static/js/search.js5
-rw-r--r--src/librustdoc/json/conversions.rs14
-rw-r--r--src/librustdoc/passes/check_doc_test_visibility.rs1
-rw-r--r--src/librustdoc/passes/collect_intra_doc_links.rs4
-rw-r--r--src/librustdoc/passes/propagate_stability.rs3
-rw-r--r--src/librustdoc/passes/stripper.rs2
-rw-r--r--src/librustdoc/visit.rs3
-rw-r--r--src/rustdoc-json-types/lib.rs9
-rw-r--r--src/tools/jsondoclint/src/item_kind.rs4
-rw-r--r--tests/rustdoc-gui/links-color.goml6
-rw-r--r--tests/rustdoc-gui/module-items-font.goml9
-rw-r--r--tests/rustdoc-gui/search-result-color.goml11
-rw-r--r--tests/rustdoc-gui/sidebar-links-color.goml20
-rw-r--r--tests/rustdoc-gui/src/test_docs/lib.rs4
-rw-r--r--tests/rustdoc-json/doc_attribute.rs18
-rw-r--r--tests/rustdoc-ui/doc-attribute-unsupported.rs7
-rw-r--r--tests/rustdoc-ui/doc-attribute-unsupported.stderr10
-rw-r--r--tests/rustdoc-ui/invalid-attribute.rs10
-rw-r--r--tests/rustdoc-ui/invalid-attribute.stderr18
-rw-r--r--tests/rustdoc/doc-attribute.rs24
-rw-r--r--tests/ui/feature-gates/feature-gate-rustdoc_internals.rs4
-rw-r--r--tests/ui/feature-gates/feature-gate-rustdoc_internals.stderr16
-rw-r--r--tests/ui/parser/bad-lit-suffixes.rs8
-rw-r--r--tests/ui/parser/bad-lit-suffixes.stderr40
-rw-r--r--tests/ui/sanitizer/cfi/no_builtins.rs22
53 files changed, 514 insertions, 194 deletions
diff --git a/compiler/rustc_ast_passes/src/feature_gate.rs b/compiler/rustc_ast_passes/src/feature_gate.rs
index e763c9d69fc..9ab5b0b3547 100644
--- a/compiler/rustc_ast_passes/src/feature_gate.rs
+++ b/compiler/rustc_ast_passes/src/feature_gate.rs
@@ -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
diff --git a/compiler/rustc_attr_parsing/src/attributes/prelude.rs b/compiler/rustc_attr_parsing/src/attributes/prelude.rs
index 2bcdee55c75..6aef7e7a67b 100644
--- a/compiler/rustc_attr_parsing/src/attributes/prelude.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/prelude.rs
@@ -1,20 +1,30 @@
-// parsing
 // templates
+#[doc(hidden)]
 pub(super) use rustc_feature::{AttributeTemplate, template};
 // data structures
+#[doc(hidden)]
 pub(super) use rustc_hir::attrs::AttributeKind;
+#[doc(hidden)]
 pub(super) use rustc_hir::lints::AttributeLintKind;
+#[doc(hidden)]
 pub(super) use rustc_hir::{MethodKind, Target};
+#[doc(hidden)]
 pub(super) use rustc_span::{DUMMY_SP, Ident, Span, Symbol, sym};
+#[doc(hidden)]
 pub(super) use thin_vec::ThinVec;
 
+#[doc(hidden)]
 pub(super) use crate::attributes::{
     AcceptMapping, AttributeOrder, AttributeParser, CombineAttributeParser, ConvertFn,
     NoArgsAttributeParser, OnDuplicate, SingleAttributeParser,
 };
 // contexts
+#[doc(hidden)]
 pub(super) use crate::context::{AcceptContext, FinalizeContext, Stage};
+#[doc(hidden)]
 pub(super) use crate::parser::*;
 // target checking
+#[doc(hidden)]
 pub(super) use crate::target_checking::Policy::{Allow, Error, Warn};
+#[doc(hidden)]
 pub(super) use crate::target_checking::{ALL_TARGETS, AllowedTargets};
diff --git a/compiler/rustc_attr_parsing/src/parser.rs b/compiler/rustc_attr_parsing/src/parser.rs
index 6d3cf684296..4f903594225 100644
--- a/compiler/rustc_attr_parsing/src/parser.rs
+++ b/compiler/rustc_attr_parsing/src/parser.rs
@@ -10,11 +10,11 @@ use rustc_ast::token::{self, Delimiter, MetaVarKind};
 use rustc_ast::tokenstream::TokenStream;
 use rustc_ast::{AttrArgs, DelimArgs, Expr, ExprKind, LitKind, MetaItemLit, NormalAttr, Path};
 use rustc_ast_pretty::pprust;
-use rustc_errors::PResult;
+use rustc_errors::{Diag, PResult};
 use rustc_hir::{self as hir, AttrPath};
 use rustc_parse::exp;
 use rustc_parse::parser::{Parser, PathStyle, token_descr};
-use rustc_session::errors::report_lit_error;
+use rustc_session::errors::{create_lit_error, report_lit_error};
 use rustc_session::parse::ParseSess;
 use rustc_span::{ErrorGuaranteed, Ident, Span, Symbol, sym};
 use thin_vec::ThinVec;
@@ -379,22 +379,23 @@ struct MetaItemListParserContext<'a, 'sess> {
 
 impl<'a, 'sess> MetaItemListParserContext<'a, 'sess> {
     fn parse_unsuffixed_meta_item_lit(&mut self) -> PResult<'sess, MetaItemLit> {
-        let uninterpolated_span = self.parser.token_uninterpolated_span();
-        let Some(token_lit) = self.parser.eat_token_lit() else {
-            return self.parser.handle_missing_lit(Parser::mk_meta_item_lit_char);
-        };
+        let Some(token_lit) = self.parser.eat_token_lit() else { return Err(self.expected_lit()) };
+        self.unsuffixed_meta_item_from_lit(token_lit)
+    }
 
+    fn unsuffixed_meta_item_from_lit(
+        &mut self,
+        token_lit: token::Lit,
+    ) -> PResult<'sess, MetaItemLit> {
         let lit = match MetaItemLit::from_token_lit(token_lit, self.parser.prev_token.span) {
             Ok(lit) => lit,
             Err(err) => {
-                let guar =
-                    report_lit_error(&self.parser.psess, err, token_lit, uninterpolated_span);
-                // Pack possible quotes and prefixes from the original literal into
-                // the error literal's symbol so they can be pretty-printed faithfully.
-                let suffixless_lit = token::Lit::new(token_lit.kind, token_lit.symbol, None);
-                let symbol = Symbol::intern(&suffixless_lit.to_string());
-                let token_lit = token::Lit::new(token::Err(guar), symbol, token_lit.suffix);
-                MetaItemLit::from_token_lit(token_lit, uninterpolated_span).unwrap()
+                return Err(create_lit_error(
+                    &self.parser.psess,
+                    err,
+                    token_lit,
+                    self.parser.prev_token_uninterpolated_span(),
+                ));
             }
         };
 
@@ -448,16 +449,28 @@ impl<'a, 'sess> MetaItemListParserContext<'a, 'sess> {
     }
 
     fn parse_meta_item_inner(&mut self) -> PResult<'sess, MetaItemOrLitParser<'static>> {
-        match self.parse_unsuffixed_meta_item_lit() {
-            Ok(lit) => return Ok(MetaItemOrLitParser::Lit(lit)),
-            Err(err) => err.cancel(), // we provide a better error below
-        }
-
-        match self.parse_attr_item() {
-            Ok(mi) => return Ok(MetaItemOrLitParser::MetaItemParser(mi)),
-            Err(err) => err.cancel(), // we provide a better error below
+        if let Some(token_lit) = self.parser.eat_token_lit() {
+            // If a literal token is parsed, we commit to parsing a MetaItemLit for better errors
+            Ok(MetaItemOrLitParser::Lit(self.unsuffixed_meta_item_from_lit(token_lit)?))
+        } else {
+            let prev_pros = self.parser.approx_token_stream_pos();
+            match self.parse_attr_item() {
+                Ok(item) => Ok(MetaItemOrLitParser::MetaItemParser(item)),
+                Err(err) => {
+                    // If `parse_attr_item` made any progress, it likely has a more precise error we should prefer
+                    // If it didn't make progress we use the `expected_lit` from below
+                    if self.parser.approx_token_stream_pos() != prev_pros {
+                        Err(err)
+                    } else {
+                        err.cancel();
+                        Err(self.expected_lit())
+                    }
+                }
+            }
         }
+    }
 
+    fn expected_lit(&mut self) -> Diag<'sess> {
         let mut err = InvalidMetaItem {
             span: self.parser.token.span,
             descr: token_descr(&self.parser.token),
@@ -492,7 +505,7 @@ impl<'a, 'sess> MetaItemListParserContext<'a, 'sess> {
             self.parser.bump();
         }
 
-        Err(self.parser.dcx().create_err(err))
+        self.parser.dcx().create_err(err)
     }
 
     fn parse(
diff --git a/compiler/rustc_codegen_llvm/src/back/lto.rs b/compiler/rustc_codegen_llvm/src/back/lto.rs
index d85f432702c..fc38c4f3e51 100644
--- a/compiler/rustc_codegen_llvm/src/back/lto.rs
+++ b/compiler/rustc_codegen_llvm/src/back/lto.rs
@@ -15,6 +15,7 @@ use rustc_codegen_ssa::{ModuleCodegen, ModuleKind, looks_like_rust_object_file};
 use rustc_data_structures::fx::FxHashMap;
 use rustc_data_structures::memmap::Mmap;
 use rustc_errors::DiagCtxtHandle;
+use rustc_hir::attrs::SanitizerSet;
 use rustc_middle::bug;
 use rustc_middle::dep_graph::WorkProduct;
 use rustc_session::config::{self, Lto};
@@ -42,6 +43,37 @@ fn prepare_lto(
         .map(|symbol| CString::new(symbol.to_owned()).unwrap())
         .collect::<Vec<CString>>();
 
+    if cgcx.regular_module_config.instrument_coverage
+        || cgcx.regular_module_config.pgo_gen.enabled()
+    {
+        // These are weak symbols that point to the profile version and the
+        // profile name, which need to be treated as exported so LTO doesn't nix
+        // them.
+        const PROFILER_WEAK_SYMBOLS: [&CStr; 2] =
+            [c"__llvm_profile_raw_version", c"__llvm_profile_filename"];
+
+        symbols_below_threshold.extend(PROFILER_WEAK_SYMBOLS.iter().map(|&sym| sym.to_owned()));
+    }
+
+    if cgcx.regular_module_config.sanitizer.contains(SanitizerSet::MEMORY) {
+        let mut msan_weak_symbols = Vec::new();
+
+        // Similar to profiling, preserve weak msan symbol during LTO.
+        if cgcx.regular_module_config.sanitizer_recover.contains(SanitizerSet::MEMORY) {
+            msan_weak_symbols.push(c"__msan_keep_going");
+        }
+
+        if cgcx.regular_module_config.sanitizer_memory_track_origins != 0 {
+            msan_weak_symbols.push(c"__msan_track_origins");
+        }
+
+        symbols_below_threshold.extend(msan_weak_symbols.into_iter().map(|sym| sym.to_owned()));
+    }
+
+    // Preserve LLVM-injected, ASAN-related symbols.
+    // See also https://github.com/rust-lang/rust/issues/113404.
+    symbols_below_threshold.push(c"___asan_globals_registered".to_owned());
+
     // __llvm_profile_counter_bias is pulled in at link time by an undefined reference to
     // __llvm_profile_runtime, therefore we won't know until link time if this symbol
     // should have default visibility.
diff --git a/compiler/rustc_codegen_ssa/src/back/symbol_export.rs b/compiler/rustc_codegen_ssa/src/back/symbol_export.rs
index 13419bcb22c..d8a1480e911 100644
--- a/compiler/rustc_codegen_ssa/src/back/symbol_export.rs
+++ b/compiler/rustc_codegen_ssa/src/back/symbol_export.rs
@@ -15,7 +15,7 @@ use rustc_middle::ty::{self, GenericArgKind, GenericArgsRef, Instance, SymbolNam
 use rustc_middle::util::Providers;
 use rustc_session::config::{CrateType, OomStrategy};
 use rustc_symbol_mangling::mangle_internal_symbol;
-use rustc_target::spec::{SanitizerSet, TlsModel};
+use rustc_target::spec::TlsModel;
 use tracing::debug;
 
 use crate::base::allocator_kind_for_codegen;
@@ -242,53 +242,6 @@ fn exported_non_generic_symbols_provider_local<'tcx>(
         }
     }
 
-    if tcx.sess.instrument_coverage() || tcx.sess.opts.cg.profile_generate.enabled() {
-        // These are weak symbols that point to the profile version and the
-        // profile name, which need to be treated as exported so LTO doesn't nix
-        // them.
-        const PROFILER_WEAK_SYMBOLS: [&str; 2] =
-            ["__llvm_profile_raw_version", "__llvm_profile_filename"];
-
-        symbols.extend(PROFILER_WEAK_SYMBOLS.iter().map(|sym| {
-            let exported_symbol = ExportedSymbol::NoDefId(SymbolName::new(tcx, sym));
-            (
-                exported_symbol,
-                SymbolExportInfo {
-                    level: SymbolExportLevel::C,
-                    kind: SymbolExportKind::Data,
-                    used: false,
-                    rustc_std_internal_symbol: false,
-                },
-            )
-        }));
-    }
-
-    if tcx.sess.opts.unstable_opts.sanitizer.contains(SanitizerSet::MEMORY) {
-        let mut msan_weak_symbols = Vec::new();
-
-        // Similar to profiling, preserve weak msan symbol during LTO.
-        if tcx.sess.opts.unstable_opts.sanitizer_recover.contains(SanitizerSet::MEMORY) {
-            msan_weak_symbols.push("__msan_keep_going");
-        }
-
-        if tcx.sess.opts.unstable_opts.sanitizer_memory_track_origins != 0 {
-            msan_weak_symbols.push("__msan_track_origins");
-        }
-
-        symbols.extend(msan_weak_symbols.into_iter().map(|sym| {
-            let exported_symbol = ExportedSymbol::NoDefId(SymbolName::new(tcx, sym));
-            (
-                exported_symbol,
-                SymbolExportInfo {
-                    level: SymbolExportLevel::C,
-                    kind: SymbolExportKind::Data,
-                    used: false,
-                    rustc_std_internal_symbol: false,
-                },
-            )
-        }));
-    }
-
     // Sort so we get a stable incr. comp. hash.
     symbols.sort_by_cached_key(|s| s.0.symbol_name_for_local_instance(tcx));
 
diff --git a/compiler/rustc_codegen_ssa/src/back/write.rs b/compiler/rustc_codegen_ssa/src/back/write.rs
index 8586615f7c7..92582dcc399 100644
--- a/compiler/rustc_codegen_ssa/src/back/write.rs
+++ b/compiler/rustc_codegen_ssa/src/back/write.rs
@@ -138,23 +138,12 @@ impl ModuleConfig {
 
         let emit_obj = if !should_emit_obj {
             EmitObj::None
-        } else if sess.target.obj_is_bitcode
-            || (sess.opts.cg.linker_plugin_lto.enabled() && !no_builtins)
-        {
+        } else if sess.target.obj_is_bitcode || sess.opts.cg.linker_plugin_lto.enabled() {
             // This case is selected if the target uses objects as bitcode, or
             // if linker plugin LTO is enabled. In the linker plugin LTO case
             // the assumption is that the final link-step will read the bitcode
             // and convert it to object code. This may be done by either the
             // native linker or rustc itself.
-            //
-            // Note, however, that the linker-plugin-lto requested here is
-            // explicitly ignored for `#![no_builtins]` crates. These crates are
-            // specifically ignored by rustc's LTO passes and wouldn't work if
-            // loaded into the linker. These crates define symbols that LLVM
-            // lowers intrinsics to, and these symbol dependencies aren't known
-            // until after codegen. As a result any crate marked
-            // `#![no_builtins]` is assumed to not participate in LTO and
-            // instead goes on to generate object code.
             EmitObj::Bitcode
         } else if need_bitcode_in_object(tcx) {
             EmitObj::ObjectCode(BitcodeSection::Full)
diff --git a/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp b/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp
index 8c34052770e..dd492325814 100644
--- a/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp
+++ b/compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp
@@ -1226,12 +1226,6 @@ extern "C" void LLVMRustPrintPasses() {
 extern "C" void LLVMRustRunRestrictionPass(LLVMModuleRef M, char **Symbols,
                                            size_t Len) {
   auto PreserveFunctions = [=](const GlobalValue &GV) {
-    // Preserve LLVM-injected, ASAN-related symbols.
-    // See also https://github.com/rust-lang/rust/issues/113404.
-    if (GV.getName() == "___asan_globals_registered") {
-      return true;
-    }
-
     // Preserve symbols exported from Rust modules.
     for (size_t I = 0; I < Len; I++) {
       if (GV.getName() == Symbols[I]) {
diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs
index f0f58e901a9..7de4f6efd0b 100644
--- a/compiler/rustc_parse/src/parser/expr.rs
+++ b/compiler/rustc_parse/src/parser/expr.rs
@@ -2077,7 +2077,7 @@ impl<'a> Parser<'a> {
         (token::Lit { symbol: name, suffix: None, kind: token::Char }, span)
     }
 
-    pub fn mk_meta_item_lit_char(name: Symbol, span: Span) -> MetaItemLit {
+    fn mk_meta_item_lit_char(name: Symbol, span: Span) -> MetaItemLit {
         ast::MetaItemLit {
             symbol: name,
             suffix: None,
@@ -2086,7 +2086,7 @@ impl<'a> Parser<'a> {
         }
     }
 
-    pub fn handle_missing_lit<L>(
+    fn handle_missing_lit<L>(
         &mut self,
         mk_lit_char: impl FnOnce(Symbol, Span) -> L,
     ) -> PResult<'a, L> {
diff --git a/compiler/rustc_passes/messages.ftl b/compiler/rustc_passes/messages.ftl
index 3be06a3704c..498d9a405fa 100644
--- a/compiler/rustc_passes/messages.ftl
+++ b/compiler/rustc_passes/messages.ftl
@@ -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
 
diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs
index c47b488465b..2cd4830b5d9 100644
--- a/compiler/rustc_passes/src/check_attr.rs
+++ b/compiler/rustc_passes/src/check_attr.rs
@@ -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,
+                                );
                             }
                         }
 
diff --git a/compiler/rustc_passes/src/errors.rs b/compiler/rustc_passes/src/errors.rs
index 01972067978..680e2a26d84 100644
--- a/compiler/rustc_passes/src/errors.rs
+++ b/compiler/rustc_passes/src/errors.rs
@@ -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)]
diff --git a/compiler/rustc_resolve/src/late.rs b/compiler/rustc_resolve/src/late.rs
index 679e663f886..d4f7fb276a9 100644
--- a/compiler/rustc_resolve/src/late.rs
+++ b/compiler/rustc_resolve/src/late.rs
@@ -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;
             }
diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs
index f9f2f84bc50..804792c6f28 100644
--- a/compiler/rustc_resolve/src/rustdoc.rs
+++ b/compiler/rustc_resolve/src/rustdoc.rs
@@ -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;
                 }
             }
diff --git a/compiler/rustc_session/src/errors.rs b/compiler/rustc_session/src/errors.rs
index 34da54a20bf..50bc7348dc9 100644
--- a/compiler/rustc_session/src/errors.rs
+++ b/compiler/rustc_session/src/errors.rs
@@ -384,6 +384,10 @@ pub fn report_lit_error(
     lit: token::Lit,
     span: Span,
 ) -> ErrorGuaranteed {
+    create_lit_error(psess, err, lit, span).emit()
+}
+
+pub fn create_lit_error(psess: &ParseSess, err: LitError, lit: token::Lit, span: Span) -> Diag<'_> {
     // Checks if `s` looks like i32 or u1234 etc.
     fn looks_like_width_suffix(first_chars: &[char], s: &str) -> bool {
         s.len() > 1 && s.starts_with(first_chars) && s[1..].chars().all(|c| c.is_ascii_digit())
@@ -414,32 +418,32 @@ pub fn report_lit_error(
     let dcx = psess.dcx();
     match err {
         LitError::InvalidSuffix(suffix) => {
-            dcx.emit_err(InvalidLiteralSuffix { span, kind: lit.kind.descr(), suffix })
+            dcx.create_err(InvalidLiteralSuffix { span, kind: lit.kind.descr(), suffix })
         }
         LitError::InvalidIntSuffix(suffix) => {
             let suf = suffix.as_str();
             if looks_like_width_suffix(&['i', 'u'], suf) {
                 // If it looks like a width, try to be helpful.
-                dcx.emit_err(InvalidIntLiteralWidth { span, width: suf[1..].into() })
+                dcx.create_err(InvalidIntLiteralWidth { span, width: suf[1..].into() })
             } else if let Some(fixed) = fix_base_capitalisation(lit.symbol.as_str(), suf) {
-                dcx.emit_err(InvalidNumLiteralBasePrefix { span, fixed })
+                dcx.create_err(InvalidNumLiteralBasePrefix { span, fixed })
             } else {
-                dcx.emit_err(InvalidNumLiteralSuffix { span, suffix: suf.to_string() })
+                dcx.create_err(InvalidNumLiteralSuffix { span, suffix: suf.to_string() })
             }
         }
         LitError::InvalidFloatSuffix(suffix) => {
             let suf = suffix.as_str();
             if looks_like_width_suffix(&['f'], suf) {
                 // If it looks like a width, try to be helpful.
-                dcx.emit_err(InvalidFloatLiteralWidth { span, width: suf[1..].to_string() })
+                dcx.create_err(InvalidFloatLiteralWidth { span, width: suf[1..].to_string() })
             } else {
-                dcx.emit_err(InvalidFloatLiteralSuffix { span, suffix: suf.to_string() })
+                dcx.create_err(InvalidFloatLiteralSuffix { span, suffix: suf.to_string() })
             }
         }
         LitError::NonDecimalFloat(base) => match base {
-            16 => dcx.emit_err(HexadecimalFloatLiteralNotSupported { span }),
-            8 => dcx.emit_err(OctalFloatLiteralNotSupported { span }),
-            2 => dcx.emit_err(BinaryFloatLiteralNotSupported { span }),
+            16 => dcx.create_err(HexadecimalFloatLiteralNotSupported { span }),
+            8 => dcx.create_err(OctalFloatLiteralNotSupported { span }),
+            2 => dcx.create_err(BinaryFloatLiteralNotSupported { span }),
             _ => unreachable!(),
         },
         LitError::IntTooLarge(base) => {
@@ -450,7 +454,7 @@ pub fn report_lit_error(
                 16 => format!("{max:#x}"),
                 _ => format!("{max}"),
             };
-            dcx.emit_err(IntLiteralTooLarge { span, limit })
+            dcx.create_err(IntLiteralTooLarge { span, limit })
         }
     }
 }
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index 89b07dba5c8..77260d07c99 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -541,6 +541,7 @@ symbols! {
         att_syntax,
         attr,
         attr_literals,
+        attribute,
         attributes,
         audit_that,
         augmented_assignments,
diff --git a/src/bootstrap/src/utils/render_tests.rs b/src/bootstrap/src/utils/render_tests.rs
index 40006aca5c5..90fd57d976d 100644
--- a/src/bootstrap/src/utils/render_tests.rs
+++ b/src/bootstrap/src/utils/render_tests.rs
@@ -250,8 +250,14 @@ impl<'a> Renderer<'a> {
                 if failure.stdout.is_some() || failure.message.is_some() {
                     println!("---- {} stdout ----", failure.name);
                     if let Some(stdout) = &failure.stdout {
-                        println!("{stdout}");
+                        // Captured test output normally ends with a newline,
+                        // so only use `println!` if it doesn't.
+                        print!("{stdout}");
+                        if !stdout.ends_with('\n') {
+                            println!("\n\\ (no newline at end of output)");
+                        }
                     }
+                    println!("---- {} stdout end ----", failure.name);
                     if let Some(message) = &failure.message {
                         println!("NOTE: {message}");
                     }
diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md
index bb28e3abbf3..25c929a1dba 100644
--- a/src/doc/rustdoc/src/unstable-features.md
+++ b/src/doc/rustdoc/src/unstable-features.md
@@ -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.
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index 92bd4a498ca..fcff15650ce 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -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(),
         }
     }
 
diff --git a/src/librustdoc/clean/utils.rs b/src/librustdoc/clean/utils.rs
index 813fdee57e1..6fc1e43c724 100644
--- a/src/librustdoc/clean/utils.rs
+++ b/src/librustdoc/clean/utils.rs
@@ -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)) }
diff --git a/src/librustdoc/fold.rs b/src/librustdoc/fold.rs
index c03d16ad081..ee5f260615d 100644
--- a/src/librustdoc/fold.rs
+++ b/src/librustdoc/fold.rs
@@ -96,7 +96,8 @@ pub(crate) trait DocFolder: Sized {
             | ImplAssocConstItem(..)
             | RequiredAssocTypeItem(..)
             | AssocTypeItem(..)
-            | KeywordItem => kind,
+            | KeywordItem
+            | AttributeItem => kind,
         }
     }
 
diff --git a/src/librustdoc/formats/cache.rs b/src/librustdoc/formats/cache.rs
index cb6837dd614..29b4c4caaf8 100644
--- a/src/librustdoc/formats/cache.rs
+++ b/src/librustdoc/formats/cache.rs
@@ -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,
diff --git a/src/librustdoc/formats/item_type.rs b/src/librustdoc/formats/item_type.rs
index 142a9d7d8af..e94ef517309 100644
--- a/src/librustdoc/formats/item_type.rs
+++ b/src/librustdoc/formats/item_type.rs
@@ -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 {
diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs
index faaecaf0cbb..5f92ab2fada 100644
--- a/src/librustdoc/html/render/context.rs
+++ b/src/librustdoc/html/render/context.rs
@@ -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 ");
             }
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 673947ad308..6db90c9bf2a 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -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,
diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs
index b86a2c94697..afa438f2596 100644
--- a/src/librustdoc/html/render/print_item.rs
+++ b/src/librustdoc/html/render/print_item.rs
@@ -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)
 }
 
diff --git a/src/librustdoc/html/static/css/noscript.css b/src/librustdoc/html/static/css/noscript.css
index a3c6bf98161..5c02e2eb26a 100644
--- a/src/librustdoc/html/static/css/noscript.css
+++ b/src/librustdoc/html/static/css/noscript.css
@@ -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;
diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index 86f1a42bc01..09d289d570c 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -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;
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index 4fcba5f120b..75febd6f737 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -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");
diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js
index 3fb4db3a89c..b003bcc7bf9 100644
--- a/src/librustdoc/html/static/js/search.js
+++ b/src/librustdoc/html/static/js/search.js
@@ -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;
diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs
index 5fab8ad2a4b..f0520716228 100644
--- a/src/librustdoc/json/conversions.rs
+++ b/src/librustdoc/json/conversions.rs
@@ -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,
diff --git a/src/librustdoc/passes/check_doc_test_visibility.rs b/src/librustdoc/passes/check_doc_test_visibility.rs
index 8028afea363..e0ea760cf3b 100644
--- a/src/librustdoc/passes/check_doc_test_visibility.rs
+++ b/src/librustdoc/passes/check_doc_test_visibility.rs
@@ -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(..)
diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs
index bad51d7f5b2..719b7c6ab89 100644
--- a/src/librustdoc/passes/collect_intra_doc_links.rs
+++ b/src/librustdoc/passes/collect_intra_doc_links.rs
@@ -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;
diff --git a/src/librustdoc/passes/propagate_stability.rs b/src/librustdoc/passes/propagate_stability.rs
index 14ec58702e3..5139ca301dd 100644
--- a/src/librustdoc/passes/propagate_stability.rs
+++ b/src/librustdoc/passes/propagate_stability.rs
@@ -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!(),
                 }
diff --git a/src/librustdoc/passes/stripper.rs b/src/librustdoc/passes/stripper.rs
index eedbbca0f8d..99d22526f85 100644
--- a/src/librustdoc/passes/stripper.rs
+++ b/src/librustdoc/passes/stripper.rs
@@ -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 {
diff --git a/src/librustdoc/visit.rs b/src/librustdoc/visit.rs
index b8b619514aa..4d31409afe8 100644
--- a/src/librustdoc/visit.rs
+++ b/src/librustdoc/visit.rs
@@ -49,7 +49,8 @@ pub(crate) trait DocVisitor<'a>: Sized {
             | ImplAssocConstItem(..)
             | RequiredAssocTypeItem(..)
             | AssocTypeItem(..)
-            | KeywordItem => {}
+            | KeywordItem
+            | AttributeItem => {}
         }
     }
 
diff --git a/src/rustdoc-json-types/lib.rs b/src/rustdoc-json-types/lib.rs
index 40f89009a43..658d3791d25 100644
--- a/src/rustdoc-json-types/lib.rs
+++ b/src/rustdoc-json-types/lib.rs
@@ -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.
diff --git a/src/tools/jsondoclint/src/item_kind.rs b/src/tools/jsondoclint/src/item_kind.rs
index 51146831efa..e2738636a14 100644
--- a/src/tools/jsondoclint/src/item_kind.rs
+++ b/src/tools/jsondoclint/src/item_kind.rs
@@ -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,
diff --git a/tests/rustdoc-gui/links-color.goml b/tests/rustdoc-gui/links-color.goml
index f11920cdd8c..a363175c1dd 100644
--- a/tests/rustdoc-gui/links-color.goml
+++ b/tests/rustdoc-gui/links-color.goml
@@ -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",
diff --git a/tests/rustdoc-gui/module-items-font.goml b/tests/rustdoc-gui/module-items-font.goml
index 0e6dd81c05b..bed95b378c6 100644
--- a/tests/rustdoc-gui/module-items-font.goml
+++ b/tests/rustdoc-gui/module-items-font.goml
@@ -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'},
+)
diff --git a/tests/rustdoc-gui/search-result-color.goml b/tests/rustdoc-gui/search-result-color.goml
index e136eab6a7d..fe0f6401089 100644
--- a/tests/rustdoc-gui/search-result-color.goml
+++ b/tests/rustdoc-gui/search-result-color.goml
@@ -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|})
@@ -47,6 +48,11 @@ define-function: (
             "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|,
             "hover_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",
diff --git a/tests/rustdoc-gui/sidebar-links-color.goml b/tests/rustdoc-gui/sidebar-links-color.goml
index 57c45555a76..9a398655f8f 100644
--- a/tests/rustdoc-gui/sidebar-links-color.goml
+++ b/tests/rustdoc-gui/sidebar-links-color.goml
@@ -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",
     }
 )
diff --git a/tests/rustdoc-gui/src/test_docs/lib.rs b/tests/rustdoc-gui/src/test_docs/lib.rs
index 623f5b33e9b..42f2fbd93b1 100644
--- a/tests/rustdoc-gui/src/test_docs/lib.rs
+++ b/tests/rustdoc-gui/src/test_docs/lib.rs
@@ -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;
 
diff --git a/tests/rustdoc-json/doc_attribute.rs b/tests/rustdoc-json/doc_attribute.rs
new file mode 100644
index 00000000000..9e1a711f0b7
--- /dev/null
+++ b/tests/rustdoc-json/doc_attribute.rs
@@ -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 {}
diff --git a/tests/rustdoc-ui/doc-attribute-unsupported.rs b/tests/rustdoc-ui/doc-attribute-unsupported.rs
new file mode 100644
index 00000000000..3bd153117a9
--- /dev/null
+++ b/tests/rustdoc-ui/doc-attribute-unsupported.rs
@@ -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 {}
diff --git a/tests/rustdoc-ui/doc-attribute-unsupported.stderr b/tests/rustdoc-ui/doc-attribute-unsupported.stderr
new file mode 100644
index 00000000000..e2480a548ac
--- /dev/null
+++ b/tests/rustdoc-ui/doc-attribute-unsupported.stderr
@@ -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
+
diff --git a/tests/rustdoc-ui/invalid-attribute.rs b/tests/rustdoc-ui/invalid-attribute.rs
new file mode 100644
index 00000000000..2fc7be2cf34
--- /dev/null
+++ b/tests/rustdoc-ui/invalid-attribute.rs
@@ -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 {}
diff --git a/tests/rustdoc-ui/invalid-attribute.stderr b/tests/rustdoc-ui/invalid-attribute.stderr
new file mode 100644
index 00000000000..66e68ce44b4
--- /dev/null
+++ b/tests/rustdoc-ui/invalid-attribute.stderr
@@ -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
+
diff --git a/tests/rustdoc/doc-attribute.rs b/tests/rustdoc/doc-attribute.rs
new file mode 100644
index 00000000000..92e603ae6e5
--- /dev/null
+++ b/tests/rustdoc/doc-attribute.rs
@@ -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 {}
diff --git a/tests/ui/feature-gates/feature-gate-rustdoc_internals.rs b/tests/ui/feature-gates/feature-gate-rustdoc_internals.rs
index 57d6b591287..0ad3b2aead4 100644
--- a/tests/ui/feature-gates/feature-gate-rustdoc_internals.rs
+++ b/tests/ui/feature-gates/feature-gate-rustdoc_internals.rs
@@ -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
diff --git a/tests/ui/feature-gates/feature-gate-rustdoc_internals.stderr b/tests/ui/feature-gates/feature-gate-rustdoc_internals.stderr
index f3c00a2156b..5a6d4d3b45e 100644
--- a/tests/ui/feature-gates/feature-gate-rustdoc_internals.stderr
+++ b/tests/ui/feature-gates/feature-gate-rustdoc_internals.stderr
@@ -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`.
diff --git a/tests/ui/parser/bad-lit-suffixes.rs b/tests/ui/parser/bad-lit-suffixes.rs
index 91b32fd4539..0a1ee120730 100644
--- a/tests/ui/parser/bad-lit-suffixes.rs
+++ b/tests/ui/parser/bad-lit-suffixes.rs
@@ -38,10 +38,14 @@ fn g() {}
 
 #[link(name = "string"suffix)]
 //~^ ERROR suffixes on string literals are invalid
-//~| ERROR malformed `link` attribute input
 extern "C" {}
 
 #[rustc_layout_scalar_valid_range_start(0suffix)]
 //~^ ERROR invalid suffix `suffix` for number literal
-//~| ERROR malformed `rustc_layout_scalar_valid_range_start` attribute input
 struct S;
+
+impl S {
+    #[rustc_confusables("blah"suffix)]
+    //~^ ERROR suffixes on string literals are invalid
+    fn woof() { }
+}
diff --git a/tests/ui/parser/bad-lit-suffixes.stderr b/tests/ui/parser/bad-lit-suffixes.stderr
index 217cc74b8eb..6c3dbbcec64 100644
--- a/tests/ui/parser/bad-lit-suffixes.stderr
+++ b/tests/ui/parser/bad-lit-suffixes.stderr
@@ -160,48 +160,20 @@ error: suffixes on string literals are invalid
 LL | #[link(name = "string"suffix)]
    |               ^^^^^^^^^^^^^^ invalid suffix `suffix`
 
-error[E0539]: malformed `link` attribute input
-  --> $DIR/bad-lit-suffixes.rs:39:1
-   |
-LL | #[link(name = "string"suffix)]
-   | ^^^^^^^---------------------^^
-   |        |
-   |        expected this to be of the form `name = "..."`
-   |
-   = note: for more information, visit <https://doc.rust-lang.org/reference/items/external-blocks.html#the-link-attribute>
-help: try changing it to one of the following valid forms of the attribute
-   |
-LL - #[link(name = "string"suffix)]
-LL + #[link(name = "...")]
-   |
-LL - #[link(name = "string"suffix)]
-LL + #[link(name = "...", import_name_type = "decorated|noprefix|undecorated")]
-   |
-LL - #[link(name = "string"suffix)]
-LL + #[link(name = "...", kind = "dylib|static|...")]
-   |
-LL - #[link(name = "string"suffix)]
-LL + #[link(name = "...", kind = "dylib|static|...", wasm_import_module = "...", import_name_type = "decorated|noprefix|undecorated")]
-   |
-   = and 1 other candidate
-
 error: invalid suffix `suffix` for number literal
-  --> $DIR/bad-lit-suffixes.rs:44:41
+  --> $DIR/bad-lit-suffixes.rs:43:41
    |
 LL | #[rustc_layout_scalar_valid_range_start(0suffix)]
    |                                         ^^^^^^^ invalid suffix `suffix`
    |
    = help: the suffix must be one of the numeric types (`u32`, `isize`, `f32`, etc.)
 
-error[E0539]: malformed `rustc_layout_scalar_valid_range_start` attribute input
-  --> $DIR/bad-lit-suffixes.rs:44:1
+error: suffixes on string literals are invalid
+  --> $DIR/bad-lit-suffixes.rs:48:25
    |
-LL | #[rustc_layout_scalar_valid_range_start(0suffix)]
-   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-------^^
-   | |                                       |
-   | |                                       expected an integer literal here
-   | help: must be of the form: `#[rustc_layout_scalar_valid_range_start(start)]`
+LL |     #[rustc_confusables("blah"suffix)]
+   |                         ^^^^^^^^^^^^ invalid suffix `suffix`
 
-error: aborting due to 23 previous errors; 2 warnings emitted
+error: aborting due to 22 previous errors; 2 warnings emitted
 
 For more information about this error, try `rustc --explain E0539`.
diff --git a/tests/ui/sanitizer/cfi/no_builtins.rs b/tests/ui/sanitizer/cfi/no_builtins.rs
new file mode 100644
index 00000000000..949057689ab
--- /dev/null
+++ b/tests/ui/sanitizer/cfi/no_builtins.rs
@@ -0,0 +1,22 @@
+// Verifies that `#![no_builtins]` crates can be built with linker-plugin-lto and CFI.
+// See Issue #142284
+//
+//@ needs-sanitizer-cfi
+//@ compile-flags: -Clinker-plugin-lto -Copt-level=0 -Zsanitizer=cfi -Ctarget-feature=-crt-static
+//@ compile-flags: --crate-type rlib
+//@ build-pass
+
+#![no_builtins]
+#![no_std]
+
+pub static FUNC: fn() = initializer;
+
+pub fn initializer() {
+    call(fma_with_fma);
+}
+
+pub fn call(fn_ptr: fn()) {
+    fn_ptr();
+}
+
+pub fn fma_with_fma() {}