about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGuillaume Gomez <guillaume1.gomez@gmail.com>2025-06-07 22:22:55 +0200
committerGitHub <noreply@github.com>2025-06-07 22:22:55 +0200
commit2c8a9cccd9497d20d3f2a30c1370dd53c8f21296 (patch)
tree5e623e4b7e5944f0e192855b647e710d6b625301
parent2f2c8c3512e82e4315db83bbb53eb79e2c566270 (diff)
parentd96d3bed6fc14fa03b077b1b7acf493815a6ef31 (diff)
downloadrust-2c8a9cccd9497d20d3f2a30c1370dd53c8f21296.tar.gz
rust-2c8a9cccd9497d20d3f2a30c1370dd53c8f21296.zip
Rollup merge of #140560 - Urgau:test_attr-module-level, r=GuillaumeGomez
Allow `#![doc(test(attr(..)))]` everywhere

This PR adds the ability to specify [`#![doc(test(attr(..)))]`](https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#testattr) ~~at module level~~ everywhere in addition to allowing it at crate-root.

This is motivated by a recent PR #140323 (by ````@tgross35)```` where we have to duplicate 2 attributes to every single `f16` and `f128` doctests, by allowing `#![doc(test(attr(..)))]` at module level (and everywhere else) we can omit them entirely and just have (in both module):

```rust
#![doc(test(attr(feature(cfg_target_has_reliable_f16_f128))))]
#![doc(test(attr(expect(internal_features))))]
```

Those new attributes are appended to the one found at crate-root or at a previous module. Those "global" attributes are compatible with merged doctests (they already were before).

Given the small addition that this is, I'm proposing to insta-stabilize it, but I can feature-gate it if preferred.

Best reviewed commit by commit.

r? ````@GuillaumeGomez````
-rw-r--r--compiler/rustc_passes/src/check_attr.rs16
-rw-r--r--src/doc/rustdoc/src/write-documentation/the-doc-attribute.md32
-rw-r--r--src/librustdoc/doctest.rs49
-rw-r--r--src/librustdoc/doctest/extracted.rs5
-rw-r--r--src/librustdoc/doctest/make.rs21
-rw-r--r--src/librustdoc/doctest/markdown.rs2
-rw-r--r--src/librustdoc/doctest/runner.rs11
-rw-r--r--src/librustdoc/doctest/rust.rs29
-rw-r--r--src/librustdoc/doctest/tests.rs68
-rw-r--r--src/librustdoc/html/markdown.rs1
-rw-r--r--tests/run-make/doctests-keep-binaries-2024/rmake.rs17
-rw-r--r--tests/rustdoc-ui/doctest/dead-code-items.rs116
-rw-r--r--tests/rustdoc-ui/doctest/dead-code-items.stdout146
-rw-r--r--tests/rustdoc-ui/doctest/dead-code-module-2.rs27
-rw-r--r--tests/rustdoc-ui/doctest/dead-code-module-2.stdout35
-rw-r--r--tests/rustdoc-ui/doctest/dead-code-module.rs18
-rw-r--r--tests/rustdoc-ui/doctest/dead-code-module.stdout29
-rw-r--r--tests/rustdoc-ui/doctest/doc-test-attr-pass-module.rs11
-rw-r--r--tests/ui/future-incompatible-lint-group.rs10
-rw-r--r--tests/ui/future-incompatible-lint-group.stderr40
-rw-r--r--tests/ui/lint/unused/useless-comment.rs7
-rw-r--r--tests/ui/lint/unused/useless-comment.stderr73
-rw-r--r--tests/ui/rustdoc/doc-test-attr-pass.rs42
23 files changed, 704 insertions, 101 deletions
diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs
index 5b7d45bb152..dc29b03083f 100644
--- a/compiler/rustc_passes/src/check_attr.rs
+++ b/compiler/rustc_passes/src/check_attr.rs
@@ -1266,13 +1266,17 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
         true
     }
 
-    /// Checks that `doc(test(...))` attribute contains only valid attributes. Returns `true` if
-    /// valid.
-    fn check_test_attr(&self, meta: &MetaItemInner, hir_id: HirId) {
+    /// Checks that `doc(test(...))` attribute contains only valid attributes and are at the right place.
+    fn check_test_attr(&self, attr: &Attribute, meta: &MetaItemInner, hir_id: HirId) {
         if let Some(metas) = meta.meta_item_list() {
             for i_meta in metas {
                 match (i_meta.name(), i_meta.meta_item()) {
-                    (Some(sym::attr | sym::no_crate_inject), _) => {}
+                    (Some(sym::attr), _) => {
+                        // Allowed everywhere like `#[doc]`
+                    }
+                    (Some(sym::no_crate_inject), _) => {
+                        self.check_attr_crate_level(attr, meta, hir_id);
+                    }
                     (_, Some(m)) => {
                         self.tcx.emit_node_span_lint(
                             INVALID_DOC_ATTRIBUTES,
@@ -1359,9 +1363,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
                         }
 
                         Some(sym::test) => {
-                            if self.check_attr_crate_level(attr, meta, hir_id) {
-                                self.check_test_attr(meta, hir_id);
-                            }
+                            self.check_test_attr(attr, meta, hir_id);
                         }
 
                         Some(
diff --git a/src/doc/rustdoc/src/write-documentation/the-doc-attribute.md b/src/doc/rustdoc/src/write-documentation/the-doc-attribute.md
index 6ec93d1746c..65e6b417427 100644
--- a/src/doc/rustdoc/src/write-documentation/the-doc-attribute.md
+++ b/src/doc/rustdoc/src/write-documentation/the-doc-attribute.md
@@ -143,15 +143,6 @@ But if you include this:
 
 it will not.
 
-### `test(attr(...))`
-
-This form of the `doc` attribute allows you to add arbitrary attributes to all your doctests. For
-example, if you want your doctests to fail if they have dead code, you could add this:
-
-```rust,no_run
-#![doc(test(attr(deny(dead_code))))]
-```
-
 ## At the item level
 
 These forms of the `#[doc]` attribute are used on individual items, to control how
@@ -283,3 +274,26 @@ To get around this limitation, we just add `#[doc(alias = "lib_name_do_something
 on the `do_something` method and then it's all good!
 Users can now look for `lib_name_do_something` in our crate directly and find
 `Obj::do_something`.
+
+### `test(attr(...))`
+
+This form of the `doc` attribute allows you to add arbitrary attributes to all your doctests. For
+example, if you want your doctests to fail if they have dead code, you could add this:
+
+```rust,no_run
+#![doc(test(attr(deny(dead_code))))]
+
+mod my_mod {
+    #![doc(test(attr(allow(dead_code))))] // but allow `dead_code` for this module
+}
+```
+
+`test(attr(..))` attributes are appended to the parent module's, they do not replace the current
+list of attributes. In the previous example, both attributes would be present:
+
+```rust,no_run
+// For every doctest in `my_mod`
+
+#![deny(dead_code)] // from the crate-root
+#![allow(dead_code)] // from `my_mod`
+```
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs
index b2fe24db0a2..a81d6020f71 100644
--- a/src/librustdoc/doctest.rs
+++ b/src/librustdoc/doctest.rs
@@ -5,6 +5,7 @@ mod runner;
 mod rust;
 
 use std::fs::File;
+use std::hash::{Hash, Hasher};
 use std::io::{self, Write};
 use std::path::{Path, PathBuf};
 use std::process::{self, Command, Stdio};
@@ -14,7 +15,7 @@ use std::{panic, str};
 
 pub(crate) use make::{BuildDocTestBuilder, DocTestBuilder};
 pub(crate) use markdown::test as test_markdown;
-use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet};
+use rustc_data_structures::fx::{FxHashMap, FxHasher, FxIndexMap, FxIndexSet};
 use rustc_errors::emitter::HumanReadableErrorType;
 use rustc_errors::{ColorConfig, DiagCtxtHandle};
 use rustc_hir as hir;
@@ -45,8 +46,6 @@ pub(crate) struct GlobalTestOptions {
     /// Whether inserting extra indent spaces in code block,
     /// default is `false`, only `true` for generating code link of Rust playground
     pub(crate) insert_indent_space: bool,
-    /// Additional crate-level attributes to add to doctests.
-    pub(crate) attrs: Vec<String>,
     /// Path to file containing arguments for the invocation of rustc.
     pub(crate) args_file: PathBuf,
 }
@@ -283,7 +282,7 @@ pub(crate) fn run_tests(
     rustdoc_options: &Arc<RustdocOptions>,
     unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
     mut standalone_tests: Vec<test::TestDescAndFn>,
-    mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
+    mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>,
     // We pass this argument so we can drop it manually before using `exit`.
     mut temp_dir: Option<TempDir>,
 ) {
@@ -298,7 +297,7 @@ pub(crate) fn run_tests(
     let mut ran_edition_tests = 0;
     let target_str = rustdoc_options.target.to_string();
 
-    for (edition, mut doctests) in mergeable_tests {
+    for (MergeableTestKey { edition, global_crate_attrs_hash }, mut doctests) in mergeable_tests {
         if doctests.is_empty() {
             continue;
         }
@@ -308,8 +307,8 @@ pub(crate) fn run_tests(
 
         let rustdoc_test_options = IndividualTestOptions::new(
             rustdoc_options,
-            &Some(format!("merged_doctest_{edition}")),
-            PathBuf::from(format!("doctest_{edition}.rs")),
+            &Some(format!("merged_doctest_{edition}_{global_crate_attrs_hash}")),
+            PathBuf::from(format!("doctest_{edition}_{global_crate_attrs_hash}.rs")),
         );
 
         for (doctest, scraped_test) in &doctests {
@@ -371,12 +370,9 @@ fn scrape_test_config(
     attrs: &[hir::Attribute],
     args_file: PathBuf,
 ) -> GlobalTestOptions {
-    use rustc_ast_pretty::pprust;
-
     let mut opts = GlobalTestOptions {
         crate_name,
         no_crate_inject: false,
-        attrs: Vec::new(),
         insert_indent_space: false,
         args_file,
     };
@@ -393,13 +389,7 @@ fn scrape_test_config(
         if attr.has_name(sym::no_crate_inject) {
             opts.no_crate_inject = true;
         }
-        if attr.has_name(sym::attr)
-            && let Some(l) = attr.meta_item_list()
-        {
-            for item in l {
-                opts.attrs.push(pprust::meta_list_item_to_string(item));
-            }
-        }
+        // NOTE: `test(attr(..))` is handled when discovering the individual tests
     }
 
     opts
@@ -848,6 +838,7 @@ pub(crate) struct ScrapedDocTest {
     text: String,
     name: String,
     span: Span,
+    global_crate_attrs: Vec<String>,
 }
 
 impl ScrapedDocTest {
@@ -858,6 +849,7 @@ impl ScrapedDocTest {
         langstr: LangString,
         text: String,
         span: Span,
+        global_crate_attrs: Vec<String>,
     ) -> Self {
         let mut item_path = logical_path.join("::");
         item_path.retain(|c| c != ' ');
@@ -867,7 +859,7 @@ impl ScrapedDocTest {
         let name =
             format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
 
-        Self { filename, line, langstr, text, name, span }
+        Self { filename, line, langstr, text, name, span, global_crate_attrs }
     }
     fn edition(&self, opts: &RustdocOptions) -> Edition {
         self.langstr.edition.unwrap_or(opts.edition)
@@ -896,9 +888,15 @@ pub(crate) trait DocTestVisitor {
     fn visit_header(&mut self, _name: &str, _level: u32) {}
 }
 
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
+pub(crate) struct MergeableTestKey {
+    edition: Edition,
+    global_crate_attrs_hash: u64,
+}
+
 struct CreateRunnableDocTests {
     standalone_tests: Vec<test::TestDescAndFn>,
-    mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
+    mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>,
 
     rustdoc_options: Arc<RustdocOptions>,
     opts: GlobalTestOptions,
@@ -949,6 +947,7 @@ impl CreateRunnableDocTests {
         let edition = scraped_test.edition(&self.rustdoc_options);
         let doctest = BuildDocTestBuilder::new(&scraped_test.text)
             .crate_name(&self.opts.crate_name)
+            .global_crate_attrs(scraped_test.global_crate_attrs.clone())
             .edition(edition)
             .can_merge_doctests(self.can_merge_doctests)
             .test_id(test_id)
@@ -965,7 +964,17 @@ impl CreateRunnableDocTests {
             let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
             self.standalone_tests.push(test_desc);
         } else {
-            self.mergeable_tests.entry(edition).or_default().push((doctest, scraped_test));
+            self.mergeable_tests
+                .entry(MergeableTestKey {
+                    edition,
+                    global_crate_attrs_hash: {
+                        let mut hasher = FxHasher::default();
+                        scraped_test.global_crate_attrs.hash(&mut hasher);
+                        hasher.finish()
+                    },
+                })
+                .or_default()
+                .push((doctest, scraped_test));
         }
     }
 
diff --git a/src/librustdoc/doctest/extracted.rs b/src/librustdoc/doctest/extracted.rs
index 3b17ccc78c7..ebe6bfd22ba 100644
--- a/src/librustdoc/doctest/extracted.rs
+++ b/src/librustdoc/doctest/extracted.rs
@@ -35,13 +35,16 @@ impl ExtractedDocTests {
     ) {
         let edition = scraped_test.edition(options);
 
-        let ScrapedDocTest { filename, line, langstr, text, name, .. } = scraped_test;
+        let ScrapedDocTest { filename, line, langstr, text, name, global_crate_attrs, .. } =
+            scraped_test;
 
         let doctest = BuildDocTestBuilder::new(&text)
             .crate_name(&opts.crate_name)
+            .global_crate_attrs(global_crate_attrs)
             .edition(edition)
             .lang_str(&langstr)
             .build(None);
+
         let (full_test_code, size) = doctest.generate_unique_doctest(
             &text,
             langstr.test_harness,
diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs
index 66647b88018..5e571613d6f 100644
--- a/src/librustdoc/doctest/make.rs
+++ b/src/librustdoc/doctest/make.rs
@@ -45,6 +45,7 @@ pub(crate) struct BuildDocTestBuilder<'a> {
     test_id: Option<String>,
     lang_str: Option<&'a LangString>,
     span: Span,
+    global_crate_attrs: Vec<String>,
 }
 
 impl<'a> BuildDocTestBuilder<'a> {
@@ -57,6 +58,7 @@ impl<'a> BuildDocTestBuilder<'a> {
             test_id: None,
             lang_str: None,
             span: DUMMY_SP,
+            global_crate_attrs: Vec::new(),
         }
     }
 
@@ -96,6 +98,12 @@ impl<'a> BuildDocTestBuilder<'a> {
         self
     }
 
+    #[inline]
+    pub(crate) fn global_crate_attrs(mut self, global_crate_attrs: Vec<String>) -> Self {
+        self.global_crate_attrs = global_crate_attrs;
+        self
+    }
+
     pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder {
         let BuildDocTestBuilder {
             source,
@@ -106,6 +114,7 @@ impl<'a> BuildDocTestBuilder<'a> {
             test_id,
             lang_str,
             span,
+            global_crate_attrs,
         } = self;
         let can_merge_doctests = can_merge_doctests
             && lang_str.is_some_and(|lang_str| {
@@ -133,6 +142,7 @@ impl<'a> BuildDocTestBuilder<'a> {
             // If the AST returned an error, we don't want this doctest to be merged with the
             // others.
             return DocTestBuilder::invalid(
+                Vec::new(),
                 String::new(),
                 String::new(),
                 String::new(),
@@ -155,6 +165,7 @@ impl<'a> BuildDocTestBuilder<'a> {
         DocTestBuilder {
             supports_color,
             has_main_fn,
+            global_crate_attrs,
             crate_attrs,
             maybe_crate_attrs,
             crates,
@@ -173,6 +184,7 @@ pub(crate) struct DocTestBuilder {
     pub(crate) supports_color: bool,
     pub(crate) already_has_extern_crate: bool,
     pub(crate) has_main_fn: bool,
+    pub(crate) global_crate_attrs: Vec<String>,
     pub(crate) crate_attrs: String,
     /// If this is a merged doctest, it will be put into `everything_else`, otherwise it will
     /// put into `crate_attrs`.
@@ -186,6 +198,7 @@ pub(crate) struct DocTestBuilder {
 
 impl DocTestBuilder {
     fn invalid(
+        global_crate_attrs: Vec<String>,
         crate_attrs: String,
         maybe_crate_attrs: String,
         crates: String,
@@ -195,6 +208,7 @@ impl DocTestBuilder {
         Self {
             supports_color: false,
             has_main_fn: false,
+            global_crate_attrs,
             crate_attrs,
             maybe_crate_attrs,
             crates,
@@ -224,7 +238,8 @@ impl DocTestBuilder {
         let mut line_offset = 0;
         let mut prog = String::new();
         let everything_else = self.everything_else.trim();
-        if opts.attrs.is_empty() {
+
+        if self.global_crate_attrs.is_empty() {
             // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
             // lints that are commonly triggered in doctests. The crate-level test attributes are
             // commonly used to make tests fail in case they trigger warnings, so having this there in
@@ -233,8 +248,8 @@ impl DocTestBuilder {
             line_offset += 1;
         }
 
-        // Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
-        for attr in &opts.attrs {
+        // Next, any attributes that came from #![doc(test(attr(...)))].
+        for attr in &self.global_crate_attrs {
             prog.push_str(&format!("#![{attr}]\n"));
             line_offset += 1;
         }
diff --git a/src/librustdoc/doctest/markdown.rs b/src/librustdoc/doctest/markdown.rs
index e358a7e44e5..7f26605f256 100644
--- a/src/librustdoc/doctest/markdown.rs
+++ b/src/librustdoc/doctest/markdown.rs
@@ -31,6 +31,7 @@ impl DocTestVisitor for MdCollector {
             config,
             test,
             DUMMY_SP,
+            Vec::new(),
         ));
     }
 
@@ -96,7 +97,6 @@ pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> {
         crate_name,
         no_crate_inject: true,
         insert_indent_space: false,
-        attrs: vec![],
         args_file,
     };
 
diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs
index 39a4f23560a..f0914474c79 100644
--- a/src/librustdoc/doctest/runner.rs
+++ b/src/librustdoc/doctest/runner.rs
@@ -12,6 +12,7 @@ use crate::html::markdown::{Ignore, LangString};
 /// Convenient type to merge compatible doctests into one.
 pub(crate) struct DocTestRunner {
     crate_attrs: FxIndexSet<String>,
+    global_crate_attrs: FxIndexSet<String>,
     ids: String,
     output: String,
     output_merged_tests: String,
@@ -23,6 +24,7 @@ impl DocTestRunner {
     pub(crate) fn new() -> Self {
         Self {
             crate_attrs: FxIndexSet::default(),
+            global_crate_attrs: FxIndexSet::default(),
             ids: String::new(),
             output: String::new(),
             output_merged_tests: String::new(),
@@ -46,6 +48,9 @@ impl DocTestRunner {
             for line in doctest.crate_attrs.split('\n') {
                 self.crate_attrs.insert(line.to_string());
             }
+            for line in &doctest.global_crate_attrs {
+                self.global_crate_attrs.insert(line.to_string());
+            }
         }
         self.ids.push_str(&format!(
             "tests.push({}::TEST);\n",
@@ -85,7 +90,7 @@ impl DocTestRunner {
             code_prefix.push('\n');
         }
 
-        if opts.attrs.is_empty() {
+        if self.global_crate_attrs.is_empty() {
             // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
             // lints that are commonly triggered in doctests. The crate-level test attributes are
             // commonly used to make tests fail in case they trigger warnings, so having this there in
@@ -93,8 +98,8 @@ impl DocTestRunner {
             code_prefix.push_str("#![allow(unused)]\n");
         }
 
-        // Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
-        for attr in &opts.attrs {
+        // Next, any attributes that came from #![doc(test(attr(...)))].
+        for attr in &self.global_crate_attrs {
             code_prefix.push_str(&format!("#![{attr}]\n"));
         }
 
diff --git a/src/librustdoc/doctest/rust.rs b/src/librustdoc/doctest/rust.rs
index f9d2aa3d3b4..96975105ac5 100644
--- a/src/librustdoc/doctest/rust.rs
+++ b/src/librustdoc/doctest/rust.rs
@@ -4,6 +4,7 @@ use std::cell::Cell;
 use std::env;
 use std::sync::Arc;
 
+use rustc_ast_pretty::pprust;
 use rustc_data_structures::fx::FxHashSet;
 use rustc_hir::def_id::{CRATE_DEF_ID, LocalDefId};
 use rustc_hir::{self as hir, CRATE_HIR_ID, intravisit};
@@ -11,7 +12,7 @@ use rustc_middle::hir::nested_filter;
 use rustc_middle::ty::TyCtxt;
 use rustc_resolve::rustdoc::span_of_fragments;
 use rustc_span::source_map::SourceMap;
-use rustc_span::{BytePos, DUMMY_SP, FileName, Pos, Span};
+use rustc_span::{BytePos, DUMMY_SP, FileName, Pos, Span, sym};
 
 use super::{DocTestVisitor, ScrapedDocTest};
 use crate::clean::{Attributes, extract_cfg_from_attrs};
@@ -22,6 +23,7 @@ struct RustCollector {
     tests: Vec<ScrapedDocTest>,
     cur_path: Vec<String>,
     position: Span,
+    global_crate_attrs: Vec<String>,
 }
 
 impl RustCollector {
@@ -75,6 +77,7 @@ impl DocTestVisitor for RustCollector {
             config,
             test,
             span,
+            self.global_crate_attrs.clone(),
         ));
     }
 
@@ -94,6 +97,7 @@ impl<'tcx> HirCollector<'tcx> {
             cur_path: vec![],
             position: DUMMY_SP,
             tests: vec![],
+            global_crate_attrs: Vec::new(),
         };
         Self { codes, tcx, collector }
     }
@@ -123,6 +127,26 @@ impl HirCollector<'_> {
             return;
         }
 
+        // Try collecting `#[doc(test(attr(...)))]`
+        let old_global_crate_attrs_len = self.collector.global_crate_attrs.len();
+        for doc_test_attrs in ast_attrs
+            .iter()
+            .filter(|a| a.has_name(sym::doc))
+            .flat_map(|a| a.meta_item_list().unwrap_or_default())
+            .filter(|a| a.has_name(sym::test))
+        {
+            let Some(doc_test_attrs) = doc_test_attrs.meta_item_list() else { continue };
+            for attr in doc_test_attrs
+                .iter()
+                .filter(|a| a.has_name(sym::attr))
+                .flat_map(|a| a.meta_item_list().unwrap_or_default())
+                .map(|i| pprust::meta_list_item_to_string(i))
+            {
+                // Add the additional attributes to the global_crate_attrs vector
+                self.collector.global_crate_attrs.push(attr);
+            }
+        }
+
         let mut has_name = false;
         if let Some(name) = name {
             self.collector.cur_path.push(name);
@@ -157,6 +181,9 @@ impl HirCollector<'_> {
 
         nested(self);
 
+        // Restore global_crate_attrs to it's previous size/content
+        self.collector.global_crate_attrs.truncate(old_global_crate_attrs_len);
+
         if has_name {
             self.collector.cur_path.pop();
         }
diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs
index 08248fdf39b..ce2984ced79 100644
--- a/src/librustdoc/doctest/tests.rs
+++ b/src/librustdoc/doctest/tests.rs
@@ -7,9 +7,11 @@ fn make_test(
     crate_name: Option<&str>,
     dont_insert_main: bool,
     opts: &GlobalTestOptions,
+    global_crate_attrs: Vec<&str>,
     test_id: Option<&str>,
 ) -> (String, usize) {
-    let mut builder = BuildDocTestBuilder::new(test_code);
+    let mut builder = BuildDocTestBuilder::new(test_code)
+        .global_crate_attrs(global_crate_attrs.into_iter().map(|a| a.to_string()).collect());
     if let Some(crate_name) = crate_name {
         builder = builder.crate_name(crate_name);
     }
@@ -28,7 +30,6 @@ fn default_global_opts(crate_name: impl Into<String>) -> GlobalTestOptions {
         crate_name: crate_name.into(),
         no_crate_inject: false,
         insert_indent_space: false,
-        attrs: vec![],
         args_file: PathBuf::new(),
     }
 }
@@ -43,7 +44,7 @@ fn main() {
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -58,7 +59,7 @@ fn main() {
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
+    let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -77,7 +78,7 @@ use asdf::qwop;
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
+    let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 3));
 }
 
@@ -94,7 +95,7 @@ use asdf::qwop;
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
+    let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -112,7 +113,7 @@ use std::*;
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, Some("std"), false, &opts, None);
+    let (output, len) = make_test(input, Some("std"), false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -131,7 +132,7 @@ use asdf::qwop;
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
+    let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -148,7 +149,7 @@ use asdf::qwop;
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
+    let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -156,8 +157,7 @@ assert_eq!(2+2, 4);
 fn make_test_opts_attrs() {
     // If you supplied some doctest attributes with `#![doc(test(attr(...)))]`, it will use
     // those instead of the stock `#![allow(unused)]`.
-    let mut opts = default_global_opts("asdf");
-    opts.attrs.push("feature(sick_rad)".to_string());
+    let opts = default_global_opts("asdf");
     let input = "use asdf::qwop;
 assert_eq!(2+2, 4);";
     let expected = "#![feature(sick_rad)]
@@ -168,11 +168,10 @@ use asdf::qwop;
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
+    let (output, len) =
+        make_test(input, Some("asdf"), false, &opts, vec!["feature(sick_rad)"], None);
     assert_eq!((output, len), (expected, 3));
 
-    // Adding more will also bump the returned line offset.
-    opts.attrs.push("feature(hella_dope)".to_string());
     let expected = "#![feature(sick_rad)]
 #![feature(hella_dope)]
 #[allow(unused_extern_crates)]
@@ -182,7 +181,18 @@ use asdf::qwop;
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
+    let (output, len) = make_test(
+        input,
+        Some("asdf"),
+        false,
+        &opts,
+        vec![
+            "feature(sick_rad)",
+            // Adding more will also bump the returned line offset.
+            "feature(hella_dope)",
+        ],
+        None,
+    );
     assert_eq!((output, len), (expected, 4));
 }
 
@@ -200,7 +210,7 @@ fn main() {
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -216,7 +226,7 @@ fn main() {
     assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 1));
 }
 
@@ -232,7 +242,7 @@ fn main() {
 assert_eq!(2+2, 4);
 }"
     .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -246,7 +256,7 @@ assert_eq!(2+2, 4);";
 //Ceci n'est pas une `fn main`
 assert_eq!(2+2, 4);"
         .to_string();
-    let (output, len) = make_test(input, None, true, &opts, None);
+    let (output, len) = make_test(input, None, true, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 1));
 }
 
@@ -264,7 +274,7 @@ assert_eq!(2+2, 4);
 }"
     .to_string();
 
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -284,7 +294,7 @@ assert_eq!(asdf::foo, 4);
 }"
     .to_string();
 
-    let (output, len) = make_test(input, Some("asdf"), false, &opts, None);
+    let (output, len) = make_test(input, Some("asdf"), false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 3));
 }
 
@@ -302,7 +312,7 @@ test_wrapper! {
 }"
     .to_string();
 
-    let (output, len) = make_test(input, Some("my_crate"), false, &opts, None);
+    let (output, len) = make_test(input, Some("my_crate"), false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 1));
 }
 
@@ -322,7 +332,7 @@ io::stdin().read_line(&mut input)?;
 Ok::<(), io:Error>(())
 } _inner().unwrap() }"
         .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -336,7 +346,7 @@ fn main() { #[allow(non_snake_case)] fn _doctest_main__some_unique_name() {
 assert_eq!(2+2, 4);
 } _doctest_main__some_unique_name() }"
         .to_string();
-    let (output, len) = make_test(input, None, false, &opts, Some("_some_unique_name"));
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), Some("_some_unique_name"));
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -355,7 +365,7 @@ fn main() {
     eprintln!(\"hello anan\");
 }"
     .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
 
@@ -375,7 +385,7 @@ fn main() {
     eprintln!(\"hello anan\");
 }"
     .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 1));
 }
 
@@ -400,7 +410,7 @@ fn main() {
 
 }"
     .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 
     // And same, if there is a `main` function provided by the user, we ensure that it's
@@ -420,7 +430,7 @@ fn main() {}";
 
 fn main() {}"
         .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 1));
 }
 
@@ -448,6 +458,6 @@ pub mod outer_module {
 }
 }"
     .to_string();
-    let (output, len) = make_test(input, None, false, &opts, None);
+    let (output, len) = make_test(input, None, false, &opts, Vec::new(), None);
     assert_eq!((output, len), (expected, 2));
 }
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 68ba1245520..d3701784f9d 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -300,7 +300,6 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
                 crate_name: krate.map(String::from).unwrap_or_default(),
                 no_crate_inject: false,
                 insert_indent_space: true,
-                attrs: vec![],
                 args_file: PathBuf::new(),
             };
             let mut builder = doctest::BuildDocTestBuilder::new(&test).edition(edition);
diff --git a/tests/run-make/doctests-keep-binaries-2024/rmake.rs b/tests/run-make/doctests-keep-binaries-2024/rmake.rs
index 3e8ffcbf244..97324e1dcbc 100644
--- a/tests/run-make/doctests-keep-binaries-2024/rmake.rs
+++ b/tests/run-make/doctests-keep-binaries-2024/rmake.rs
@@ -16,7 +16,22 @@ fn setup_test_env<F: FnOnce(&Path, &Path)>(callback: F) {
 }
 
 fn check_generated_binaries() {
-    run("doctests/merged_doctest_2024/rust_out");
+    let mut found_merged_doctest = false;
+    rfs::read_dir_entries("doctests/", |path| {
+        if path
+            .file_name()
+            .and_then(|name| name.to_str())
+            .is_some_and(|name| name.starts_with("merged_doctest_2024"))
+        {
+            found_merged_doctest = true;
+            let rust_out = path.join("rust_out");
+            let rust_out = rust_out.to_string_lossy();
+            run(&*rust_out);
+        }
+    });
+    if !found_merged_doctest {
+        panic!("no directory starting with `merged_doctest_2024` found under `doctests/`");
+    }
 }
 
 fn main() {
diff --git a/tests/rustdoc-ui/doctest/dead-code-items.rs b/tests/rustdoc-ui/doctest/dead-code-items.rs
new file mode 100644
index 00000000000..015504cbced
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/dead-code-items.rs
@@ -0,0 +1,116 @@
+// Same test as dead-code-module but with 2 doc(test(attr())) at different levels.
+
+//@ edition: 2024
+//@ compile-flags:--test --test-args=--test-threads=1
+//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
+//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
+//@ failure-status: 101
+
+#![doc(test(attr(deny(warnings))))]
+
+#[doc(test(attr(allow(dead_code))))]
+/// Example
+///
+/// ```rust,no_run
+/// trait OnlyWarning { fn no_deny_warnings(); }
+/// ```
+static S: u32 = 5;
+
+#[doc(test(attr(allow(dead_code))))]
+/// Example
+///
+/// ```rust,no_run
+/// let unused_error = 5;
+///
+/// fn dead_code_but_no_error() {}
+/// ```
+const C: u32 = 5;
+
+#[doc(test(attr(allow(dead_code))))]
+/// Example
+///
+/// ```rust,no_run
+/// trait OnlyWarningAtA { fn no_deny_warnings(); }
+/// ```
+struct A {
+    #[doc(test(attr(deny(dead_code))))]
+    /// Example
+    ///
+    /// ```rust,no_run
+    /// trait DeadCodeInField {}
+    /// ```
+    field: u32
+}
+
+#[doc(test(attr(allow(dead_code))))]
+/// Example
+///
+/// ```rust,no_run
+/// trait OnlyWarningAtU { fn no_deny_warnings(); }
+/// ```
+union U {
+    #[doc(test(attr(deny(dead_code))))]
+    /// Example
+    ///
+    /// ```rust,no_run
+    /// trait DeadCodeInUnionField {}
+    /// ```
+    field: u32,
+    /// Example
+    ///
+    /// ```rust,no_run
+    /// trait NotDeadCodeInUnionField {}
+    /// ```
+    field2: u64,
+}
+
+#[doc(test(attr(deny(dead_code))))]
+/// Example
+///
+/// ```rust,no_run
+/// let not_dead_code_but_unused = 5;
+/// ```
+enum Enum {
+    #[doc(test(attr(allow(dead_code))))]
+    /// Example
+    ///
+    /// ```rust,no_run
+    /// trait NotDeadCodeInVariant {}
+    ///
+    /// fn main() { let unused_in_variant = 5; }
+    /// ```
+    Variant1,
+}
+
+#[doc(test(attr(allow(dead_code))))]
+/// Example
+///
+/// ```rust,no_run
+/// trait OnlyWarningAtImplA { fn no_deny_warnings(); }
+/// ```
+impl A {
+    /// Example
+    ///
+    /// ```rust,no_run
+    /// trait NotDeadCodeInImplMethod {}
+    /// ```
+    fn method() {}
+}
+
+#[doc(test(attr(deny(dead_code))))]
+/// Example
+///
+/// ```rust,no_run
+/// trait StillDeadCodeAtMyTrait { }
+/// ```
+trait MyTrait {
+    #[doc(test(attr(allow(dead_code))))]
+    /// Example
+    ///
+    /// ```rust,no_run
+    /// trait NotDeadCodeAtImplFn {}
+    ///
+    /// fn main() { let unused_in_impl = 5; }
+    /// ```
+    fn my_trait_fn();
+}
diff --git a/tests/rustdoc-ui/doctest/dead-code-items.stdout b/tests/rustdoc-ui/doctest/dead-code-items.stdout
new file mode 100644
index 00000000000..4b9d8be94dd
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/dead-code-items.stdout
@@ -0,0 +1,146 @@
+
+running 13 tests
+test $DIR/dead-code-items.rs - A (line 32) - compile ... ok
+test $DIR/dead-code-items.rs - A (line 88) - compile ... ok
+test $DIR/dead-code-items.rs - A::field (line 39) - compile ... FAILED
+test $DIR/dead-code-items.rs - A::method (line 94) - compile ... ok
+test $DIR/dead-code-items.rs - C (line 22) - compile ... FAILED
+test $DIR/dead-code-items.rs - Enum (line 70) - compile ... FAILED
+test $DIR/dead-code-items.rs - Enum::Variant1 (line 77) - compile ... FAILED
+test $DIR/dead-code-items.rs - MyTrait (line 103) - compile ... FAILED
+test $DIR/dead-code-items.rs - MyTrait::my_trait_fn (line 110) - compile ... FAILED
+test $DIR/dead-code-items.rs - S (line 14) - compile ... ok
+test $DIR/dead-code-items.rs - U (line 48) - compile ... ok
+test $DIR/dead-code-items.rs - U::field (line 55) - compile ... FAILED
+test $DIR/dead-code-items.rs - U::field2 (line 61) - compile ... ok
+
+failures:
+
+---- $DIR/dead-code-items.rs - A::field (line 39) stdout ----
+error: trait `DeadCodeInField` is never used
+  --> $DIR/dead-code-items.rs:40:7
+   |
+LL | trait DeadCodeInField {}
+   |       ^^^^^^^^^^^^^^^
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-items.rs:38:9
+   |
+LL | #![deny(dead_code)]
+   |         ^^^^^^^^^
+
+error: aborting due to 1 previous error
+
+Couldn't compile the test.
+---- $DIR/dead-code-items.rs - C (line 22) stdout ----
+error: unused variable: `unused_error`
+  --> $DIR/dead-code-items.rs:23:5
+   |
+LL | let unused_error = 5;
+   |     ^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_unused_error`
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-items.rs:20:9
+   |
+LL | #![deny(warnings)]
+   |         ^^^^^^^^
+   = note: `#[deny(unused_variables)]` implied by `#[deny(warnings)]`
+
+error: aborting due to 1 previous error
+
+Couldn't compile the test.
+---- $DIR/dead-code-items.rs - Enum (line 70) stdout ----
+error: unused variable: `not_dead_code_but_unused`
+  --> $DIR/dead-code-items.rs:71:5
+   |
+LL | let not_dead_code_but_unused = 5;
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_not_dead_code_but_unused`
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-items.rs:68:9
+   |
+LL | #![deny(warnings)]
+   |         ^^^^^^^^
+   = note: `#[deny(unused_variables)]` implied by `#[deny(warnings)]`
+
+error: aborting due to 1 previous error
+
+Couldn't compile the test.
+---- $DIR/dead-code-items.rs - Enum::Variant1 (line 77) stdout ----
+error: unused variable: `unused_in_variant`
+  --> $DIR/dead-code-items.rs:80:17
+   |
+LL | fn main() { let unused_in_variant = 5; }
+   |                 ^^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_unused_in_variant`
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-items.rs:75:9
+   |
+LL | #![deny(warnings)]
+   |         ^^^^^^^^
+   = note: `#[deny(unused_variables)]` implied by `#[deny(warnings)]`
+
+error: aborting due to 1 previous error
+
+Couldn't compile the test.
+---- $DIR/dead-code-items.rs - MyTrait (line 103) stdout ----
+error: trait `StillDeadCodeAtMyTrait` is never used
+  --> $DIR/dead-code-items.rs:104:7
+   |
+LL | trait StillDeadCodeAtMyTrait { }
+   |       ^^^^^^^^^^^^^^^^^^^^^^
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-items.rs:102:9
+   |
+LL | #![deny(dead_code)]
+   |         ^^^^^^^^^
+
+error: aborting due to 1 previous error
+
+Couldn't compile the test.
+---- $DIR/dead-code-items.rs - MyTrait::my_trait_fn (line 110) stdout ----
+error: unused variable: `unused_in_impl`
+  --> $DIR/dead-code-items.rs:113:17
+   |
+LL | fn main() { let unused_in_impl = 5; }
+   |                 ^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_unused_in_impl`
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-items.rs:108:9
+   |
+LL | #![deny(warnings)]
+   |         ^^^^^^^^
+   = note: `#[deny(unused_variables)]` implied by `#[deny(warnings)]`
+
+error: aborting due to 1 previous error
+
+Couldn't compile the test.
+---- $DIR/dead-code-items.rs - U::field (line 55) stdout ----
+error: trait `DeadCodeInUnionField` is never used
+  --> $DIR/dead-code-items.rs:56:7
+   |
+LL | trait DeadCodeInUnionField {}
+   |       ^^^^^^^^^^^^^^^^^^^^
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-items.rs:54:9
+   |
+LL | #![deny(dead_code)]
+   |         ^^^^^^^^^
+
+error: aborting due to 1 previous error
+
+Couldn't compile the test.
+
+failures:
+    $DIR/dead-code-items.rs - A::field (line 39)
+    $DIR/dead-code-items.rs - C (line 22)
+    $DIR/dead-code-items.rs - Enum (line 70)
+    $DIR/dead-code-items.rs - Enum::Variant1 (line 77)
+    $DIR/dead-code-items.rs - MyTrait (line 103)
+    $DIR/dead-code-items.rs - MyTrait::my_trait_fn (line 110)
+    $DIR/dead-code-items.rs - U::field (line 55)
+
+test result: FAILED. 6 passed; 7 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
diff --git a/tests/rustdoc-ui/doctest/dead-code-module-2.rs b/tests/rustdoc-ui/doctest/dead-code-module-2.rs
new file mode 100644
index 00000000000..de7b11b91ec
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/dead-code-module-2.rs
@@ -0,0 +1,27 @@
+// Same test as dead-code-module but with 2 doc(test(attr())) at different levels.
+
+//@ edition: 2024
+//@ compile-flags:--test
+//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
+//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
+//@ failure-status: 101
+
+#![doc(test(attr(allow(unused_variables))))]
+
+mod my_mod {
+    #![doc(test(attr(deny(warnings))))]
+
+    /// Example
+    ///
+    /// ```rust,no_run
+    /// trait T { fn f(); }
+    /// ```
+    pub fn f() {}
+}
+
+/// Example
+///
+/// ```rust,no_run
+/// trait OnlyWarning { fn no_deny_warnings(); }
+/// ```
+pub fn g() {}
diff --git a/tests/rustdoc-ui/doctest/dead-code-module-2.stdout b/tests/rustdoc-ui/doctest/dead-code-module-2.stdout
new file mode 100644
index 00000000000..d44068dcbf5
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/dead-code-module-2.stdout
@@ -0,0 +1,35 @@
+
+running 1 test
+test $DIR/dead-code-module-2.rs - g (line 24) - compile ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
+
+running 1 test
+test $DIR/dead-code-module-2.rs - my_mod::f (line 16) - compile ... FAILED
+
+failures:
+
+---- $DIR/dead-code-module-2.rs - my_mod::f (line 16) stdout ----
+error: trait `T` is never used
+  --> $DIR/dead-code-module-2.rs:17:7
+   |
+LL | trait T { fn f(); }
+   |       ^
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-module-2.rs:15:9
+   |
+LL | #![deny(warnings)]
+   |         ^^^^^^^^
+   = note: `#[deny(dead_code)]` implied by `#[deny(warnings)]`
+
+error: aborting due to 1 previous error
+
+Couldn't compile the test.
+
+failures:
+    $DIR/dead-code-module-2.rs - my_mod::f (line 16)
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
diff --git a/tests/rustdoc-ui/doctest/dead-code-module.rs b/tests/rustdoc-ui/doctest/dead-code-module.rs
new file mode 100644
index 00000000000..f825749a6a2
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/dead-code-module.rs
@@ -0,0 +1,18 @@
+// Same test as dead-code but inside a module.
+
+//@ edition: 2024
+//@ compile-flags:--test
+//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
+//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
+//@ failure-status: 101
+
+mod my_mod {
+    #![doc(test(attr(allow(unused_variables), deny(warnings))))]
+
+    /// Example
+    ///
+    /// ```rust,no_run
+    /// trait T { fn f(); }
+    /// ```
+    pub fn f() {}
+}
diff --git a/tests/rustdoc-ui/doctest/dead-code-module.stdout b/tests/rustdoc-ui/doctest/dead-code-module.stdout
new file mode 100644
index 00000000000..b5ccf225d25
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/dead-code-module.stdout
@@ -0,0 +1,29 @@
+
+running 1 test
+test $DIR/dead-code-module.rs - my_mod::f (line 14) - compile ... FAILED
+
+failures:
+
+---- $DIR/dead-code-module.rs - my_mod::f (line 14) stdout ----
+error: trait `T` is never used
+  --> $DIR/dead-code-module.rs:15:7
+   |
+LL | trait T { fn f(); }
+   |       ^
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-module.rs:13:9
+   |
+LL | #![deny(warnings)]
+   |         ^^^^^^^^
+   = note: `#[deny(dead_code)]` implied by `#[deny(warnings)]`
+
+error: aborting due to 1 previous error
+
+Couldn't compile the test.
+
+failures:
+    $DIR/dead-code-module.rs - my_mod::f (line 14)
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
diff --git a/tests/rustdoc-ui/doctest/doc-test-attr-pass-module.rs b/tests/rustdoc-ui/doctest/doc-test-attr-pass-module.rs
new file mode 100644
index 00000000000..e916ca41ea0
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/doc-test-attr-pass-module.rs
@@ -0,0 +1,11 @@
+//@ check-pass
+
+#![crate_type = "lib"]
+#![deny(invalid_doc_attributes)]
+#![doc(test(no_crate_inject))]
+
+mod my_mod {
+    #![doc(test(attr(deny(warnings))))]
+
+    pub fn foo() {}
+}
diff --git a/tests/ui/future-incompatible-lint-group.rs b/tests/ui/future-incompatible-lint-group.rs
index c84538318f7..ed2c47bb609 100644
--- a/tests/ui/future-incompatible-lint-group.rs
+++ b/tests/ui/future-incompatible-lint-group.rs
@@ -2,16 +2,14 @@
 // lints for changes that are not tied to an edition
 #![deny(future_incompatible)]
 
+// Error since this is a `future_incompatible` lint
+macro_rules! m { ($i) => {} } //~ ERROR missing fragment specifier
+                              //~| WARN this was previously accepted
+
 trait Tr {
     // Warn only since this is not a `future_incompatible` lint
     fn f(u8) {} //~ WARN anonymous parameters are deprecated
                 //~| WARN this is accepted in the current edition
 }
 
-pub mod submodule {
-    // Error since this is a `future_incompatible` lint
-    #![doc(test(some_test))]
-    //~^ ERROR this attribute can only be applied at the crate level
-}
-
 fn main() {}
diff --git a/tests/ui/future-incompatible-lint-group.stderr b/tests/ui/future-incompatible-lint-group.stderr
index 4e6c434fa29..4c867e0aab3 100644
--- a/tests/ui/future-incompatible-lint-group.stderr
+++ b/tests/ui/future-incompatible-lint-group.stderr
@@ -1,5 +1,20 @@
+error: missing fragment specifier
+  --> $DIR/future-incompatible-lint-group.rs:6:19
+   |
+LL | macro_rules! m { ($i) => {} }
+   |                   ^^
+   |
+   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
+   = note: for more information, see issue #40107 <https://github.com/rust-lang/rust/issues/40107>
+note: the lint level is defined here
+  --> $DIR/future-incompatible-lint-group.rs:3:9
+   |
+LL | #![deny(future_incompatible)]
+   |         ^^^^^^^^^^^^^^^^^^^
+   = note: `#[deny(missing_fragment_specifier)]` implied by `#[deny(future_incompatible)]`
+
 warning: anonymous parameters are deprecated and will be removed in the next edition
-  --> $DIR/future-incompatible-lint-group.rs:7:10
+  --> $DIR/future-incompatible-lint-group.rs:11:10
    |
 LL |     fn f(u8) {}
    |          ^^ help: try naming the parameter or explicitly ignoring it: `_: u8`
@@ -8,14 +23,21 @@ LL |     fn f(u8) {}
    = note: for more information, see issue #41686 <https://github.com/rust-lang/rust/issues/41686>
    = note: `#[warn(anonymous_parameters)]` on by default
 
-error: this attribute can only be applied at the crate level
-  --> $DIR/future-incompatible-lint-group.rs:13:12
+error: aborting due to 1 previous error; 1 warning emitted
+
+Future incompatibility report: Future breakage diagnostic:
+error: missing fragment specifier
+  --> $DIR/future-incompatible-lint-group.rs:6:19
    |
-LL |     #![doc(test(some_test))]
-   |            ^^^^^^^^^^^^^^^
+LL | macro_rules! m { ($i) => {} }
+   |                   ^^
    |
-   = note: read <https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#at-the-crate-level> for more information
-   = note: `#[deny(invalid_doc_attributes)]` on by default
-
-error: aborting due to 1 previous error; 1 warning emitted
+   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
+   = note: for more information, see issue #40107 <https://github.com/rust-lang/rust/issues/40107>
+note: the lint level is defined here
+  --> $DIR/future-incompatible-lint-group.rs:3:9
+   |
+LL | #![deny(future_incompatible)]
+   |         ^^^^^^^^^^^^^^^^^^^
+   = note: `#[deny(missing_fragment_specifier)]` implied by `#[deny(future_incompatible)]`
 
diff --git a/tests/ui/lint/unused/useless-comment.rs b/tests/ui/lint/unused/useless-comment.rs
index 4ec52f20747..898665278e3 100644
--- a/tests/ui/lint/unused/useless-comment.rs
+++ b/tests/ui/lint/unused/useless-comment.rs
@@ -9,8 +9,13 @@ macro_rules! mac {
 /// foo //~ ERROR unused doc comment
 mac!();
 
+/// a //~ ERROR unused doc comment
+#[doc(test(attr(allow(dead_code))))] //~ ERROR unused doc comment
+unsafe extern "C" { }
+
 fn foo() {
     /// a //~ ERROR unused doc comment
+    #[doc(test(attr(allow(dead_code))))] //~ ERROR unused doc comment
     let x = 12;
 
     /// multi-line //~ ERROR unused doc comment
@@ -19,6 +24,7 @@ fn foo() {
     match x {
         /// c //~ ERROR unused doc comment
         1 => {},
+        #[doc(test(attr(allow(dead_code))))] //~ ERROR unused doc comment
         _ => {}
     }
 
@@ -32,6 +38,7 @@ fn foo() {
     /// bar //~ ERROR unused doc comment
     mac!();
 
+    #[doc(test(attr(allow(dead_code))))] //~ ERROR unused doc comment
     let x = /** comment */ 47; //~ ERROR unused doc comment
 
     /// dox //~ ERROR unused doc comment
diff --git a/tests/ui/lint/unused/useless-comment.stderr b/tests/ui/lint/unused/useless-comment.stderr
index 8bb5bdaeb25..39873b82b75 100644
--- a/tests/ui/lint/unused/useless-comment.stderr
+++ b/tests/ui/lint/unused/useless-comment.stderr
@@ -12,7 +12,28 @@ LL | #![deny(unused_doc_comments)]
    |         ^^^^^^^^^^^^^^^^^^^
 
 error: unused doc comment
-  --> $DIR/useless-comment.rs:32:5
+  --> $DIR/useless-comment.rs:12:1
+   |
+LL | /// a
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+LL | #[doc(test(attr(allow(dead_code))))]
+LL | unsafe extern "C" { }
+   | --------------------- rustdoc does not generate documentation for extern blocks
+   |
+   = help: use `//` for a plain comment
+
+error: unused doc comment
+  --> $DIR/useless-comment.rs:13:1
+   |
+LL | #[doc(test(attr(allow(dead_code))))]
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+LL | unsafe extern "C" { }
+   | --------------------- rustdoc does not generate documentation for extern blocks
+   |
+   = help: use `//` for a plain comment
+
+error: unused doc comment
+  --> $DIR/useless-comment.rs:38:5
    |
 LL |     /// bar
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ rustdoc does not generate documentation for macro invocations
@@ -20,17 +41,28 @@ LL |     /// bar
    = help: to document an item produced by a macro, the macro must produce the documentation as part of its expansion
 
 error: unused doc comment
-  --> $DIR/useless-comment.rs:13:5
+  --> $DIR/useless-comment.rs:17:5
    |
 LL |     /// a
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+LL |     #[doc(test(attr(allow(dead_code))))]
 LL |     let x = 12;
    |     ----------- rustdoc does not generate documentation for statements
    |
    = help: use `//` for a plain comment
 
 error: unused doc comment
-  --> $DIR/useless-comment.rs:16:5
+  --> $DIR/useless-comment.rs:18:5
+   |
+LL |     #[doc(test(attr(allow(dead_code))))]
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+LL |     let x = 12;
+   |     ----------- rustdoc does not generate documentation for statements
+   |
+   = help: use `//` for a plain comment
+
+error: unused doc comment
+  --> $DIR/useless-comment.rs:21:5
    |
 LL | /     /// multi-line
 LL | |     /// doc comment
@@ -39,6 +71,7 @@ LL | |     /// that is unused
 LL | /     match x {
 LL | |         /// c
 LL | |         1 => {},
+LL | |         #[doc(test(attr(allow(dead_code))))]
 LL | |         _ => {}
 LL | |     }
    | |_____- rustdoc does not generate documentation for expressions
@@ -46,7 +79,7 @@ LL | |     }
    = help: use `//` for a plain comment
 
 error: unused doc comment
-  --> $DIR/useless-comment.rs:20:9
+  --> $DIR/useless-comment.rs:25:9
    |
 LL |         /// c
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -56,7 +89,17 @@ LL |         1 => {},
    = help: use `//` for a plain comment
 
 error: unused doc comment
-  --> $DIR/useless-comment.rs:25:5
+  --> $DIR/useless-comment.rs:27:9
+   |
+LL |         #[doc(test(attr(allow(dead_code))))]
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+LL |         _ => {}
+   |         ------- rustdoc does not generate documentation for match arms
+   |
+   = help: use `//` for a plain comment
+
+error: unused doc comment
+  --> $DIR/useless-comment.rs:31:5
    |
 LL |     /// foo
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -66,7 +109,7 @@ LL |     unsafe {}
    = help: use `//` for a plain comment
 
 error: unused doc comment
-  --> $DIR/useless-comment.rs:28:5
+  --> $DIR/useless-comment.rs:34:5
    |
 LL |     #[doc = "foo"]
    |     ^^^^^^^^^^^^^^
@@ -77,7 +120,7 @@ LL |     3;
    = help: use `//` for a plain comment
 
 error: unused doc comment
-  --> $DIR/useless-comment.rs:29:5
+  --> $DIR/useless-comment.rs:35:5
    |
 LL |     #[doc = "bar"]
    |     ^^^^^^^^^^^^^^
@@ -87,7 +130,17 @@ LL |     3;
    = help: use `//` for a plain comment
 
 error: unused doc comment
-  --> $DIR/useless-comment.rs:35:13
+  --> $DIR/useless-comment.rs:41:5
+   |
+LL |     #[doc(test(attr(allow(dead_code))))]
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+LL |     let x = /** comment */ 47;
+   |     -------------------------- rustdoc does not generate documentation for statements
+   |
+   = help: use `//` for a plain comment
+
+error: unused doc comment
+  --> $DIR/useless-comment.rs:42:13
    |
 LL |     let x = /** comment */ 47;
    |             ^^^^^^^^^^^^^^ -- rustdoc does not generate documentation for expressions
@@ -95,7 +148,7 @@ LL |     let x = /** comment */ 47;
    = help: use `/* */` for a plain comment
 
 error: unused doc comment
-  --> $DIR/useless-comment.rs:37:5
+  --> $DIR/useless-comment.rs:44:5
    |
 LL |       /// dox
    |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -106,5 +159,5 @@ LL | |     }
    |
    = help: use `//` for a plain comment
 
-error: aborting due to 10 previous errors
+error: aborting due to 15 previous errors
 
diff --git a/tests/ui/rustdoc/doc-test-attr-pass.rs b/tests/ui/rustdoc/doc-test-attr-pass.rs
index f0120b7c2d0..60d1e3722bd 100644
--- a/tests/ui/rustdoc/doc-test-attr-pass.rs
+++ b/tests/ui/rustdoc/doc-test-attr-pass.rs
@@ -6,4 +6,46 @@
 #![doc(test(attr(deny(warnings))))]
 #![doc(test())]
 
+mod test {
+    #![doc(test(attr(allow(warnings))))]
+}
+
+#[doc(test(attr(allow(dead_code))))]
+static S: u32 = 5;
+
+#[doc(test(attr(allow(dead_code))))]
+const C: u32 = 5;
+
+#[doc(test(attr(deny(dead_code))))]
+struct A {
+    #[doc(test(attr(allow(dead_code))))]
+    field: u32
+}
+
+#[doc(test(attr(deny(dead_code))))]
+union U {
+    #[doc(test(attr(allow(dead_code))))]
+    field: u32,
+    field2: u64,
+}
+
+#[doc(test(attr(deny(dead_code))))]
+enum Enum {
+    #[doc(test(attr(allow(dead_code))))]
+    Variant1,
+}
+
+#[doc(test(attr(deny(dead_code))))]
+impl A {
+    #[doc(test(attr(deny(dead_code))))]
+    fn method() {}
+}
+
+#[doc(test(attr(deny(dead_code))))]
+trait MyTrait {
+    #[doc(test(attr(deny(dead_code))))]
+    fn my_trait_fn();
+}
+
+#[doc(test(attr(deny(dead_code))))]
 pub fn foo() {}