about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2025-05-22 08:40:58 +0000
committerbors <bors@rust-lang.org>2025-05-22 08:40:58 +0000
commit1d679446b01e65f9bc9ae609d0ae1e4a9c0ccaa3 (patch)
tree99e7223e4264ae071bf3e6fac30b7f12278965af
parent2cd37831b0706c9f2d6e7e20eb556dc7548bf732 (diff)
parentf4d41a5cbd00ee3490beb3a6fff9bd909c137153 (diff)
downloadrust-1d679446b01e65f9bc9ae609d0ae1e4a9c0ccaa3.tar.gz
rust-1d679446b01e65f9bc9ae609d0ae1e4a9c0ccaa3.zip
Auto merge of #140527 - GuillaumeGomez:doctest-main-fn, r=notriddle
Emit a warning if the doctest `main` function will not be run

Fixes #140310.

I think we could try to go much further like adding a "link" (ie UI annotations) on the `main` function in the doctest. However that will require some more computation, not sure if it's worth it or not. Can still be done in a follow-up if we want it.

For now, this PR does two things:
1. Pass the `DiagCtxt` to the doctest parser to emit the warning.
2. Correctly generate the `Span` to where the doctest is starting (I hope the way I did it isn't too bad either...).

cc `@fmease`
r? `@notriddle`
-rw-r--r--src/librustdoc/doctest.rs28
-rw-r--r--src/librustdoc/doctest/extracted.rs17
-rw-r--r--src/librustdoc/doctest/make.rs146
-rw-r--r--src/librustdoc/doctest/markdown.rs13
-rw-r--r--src/librustdoc/doctest/rust.rs23
-rw-r--r--src/librustdoc/doctest/tests.rs20
-rw-r--r--src/librustdoc/html/markdown.rs6
-rw-r--r--tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stderr8
-rw-r--r--tests/rustdoc-ui/doctest/main-alongside-stmts.stderr14
-rw-r--r--tests/rustdoc-ui/doctest/test-main-alongside-exprs.stderr8
-rw-r--r--tests/rustdoc-ui/doctest/warn-main-not-called.rs22
-rw-r--r--tests/rustdoc-ui/doctest/warn-main-not-called.stderr14
-rw-r--r--tests/rustdoc-ui/doctest/warn-main-not-called.stdout7
13 files changed, 253 insertions, 73 deletions
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs
index ef70b862185..b2fe24db0a2 100644
--- a/src/librustdoc/doctest.rs
+++ b/src/librustdoc/doctest.rs
@@ -12,7 +12,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
 use std::sync::{Arc, Mutex};
 use std::{panic, str};
 
-pub(crate) use make::DocTestBuilder;
+pub(crate) use make::{BuildDocTestBuilder, DocTestBuilder};
 pub(crate) use markdown::test as test_markdown;
 use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet};
 use rustc_errors::emitter::HumanReadableErrorType;
@@ -23,9 +23,9 @@ use rustc_hir::def_id::LOCAL_CRATE;
 use rustc_interface::interface;
 use rustc_session::config::{self, CrateType, ErrorOutputType, Input};
 use rustc_session::lint;
-use rustc_span::FileName;
 use rustc_span::edition::Edition;
 use rustc_span::symbol::sym;
+use rustc_span::{FileName, Span};
 use rustc_target::spec::{Target, TargetTuple};
 use tempfile::{Builder as TempFileBuilder, TempDir};
 use tracing::debug;
@@ -197,7 +197,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
                 }
             } else {
                 let mut collector = CreateRunnableDocTests::new(options, opts);
-                tests.into_iter().for_each(|t| collector.add_test(t));
+                tests.into_iter().for_each(|t| collector.add_test(t, Some(compiler.sess.dcx())));
 
                 Ok(Some(collector))
             }
@@ -847,6 +847,7 @@ pub(crate) struct ScrapedDocTest {
     langstr: LangString,
     text: String,
     name: String,
+    span: Span,
 }
 
 impl ScrapedDocTest {
@@ -856,6 +857,7 @@ impl ScrapedDocTest {
         logical_path: Vec<String>,
         langstr: LangString,
         text: String,
+        span: Span,
     ) -> Self {
         let mut item_path = logical_path.join("::");
         item_path.retain(|c| c != ' ');
@@ -865,7 +867,7 @@ impl ScrapedDocTest {
         let name =
             format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
 
-        Self { filename, line, langstr, text, name }
+        Self { filename, line, langstr, text, name, span }
     }
     fn edition(&self, opts: &RustdocOptions) -> Edition {
         self.langstr.edition.unwrap_or(opts.edition)
@@ -921,7 +923,7 @@ impl CreateRunnableDocTests {
         }
     }
 
-    fn add_test(&mut self, scraped_test: ScrapedDocTest) {
+    fn add_test(&mut self, scraped_test: ScrapedDocTest, dcx: Option<DiagCtxtHandle<'_>>) {
         // For example `module/file.rs` would become `module_file_rs`
         let file = scraped_test
             .filename
@@ -945,14 +947,14 @@ impl CreateRunnableDocTests {
         );
 
         let edition = scraped_test.edition(&self.rustdoc_options);
-        let doctest = DocTestBuilder::new(
-            &scraped_test.text,
-            Some(&self.opts.crate_name),
-            edition,
-            self.can_merge_doctests,
-            Some(test_id),
-            Some(&scraped_test.langstr),
-        );
+        let doctest = BuildDocTestBuilder::new(&scraped_test.text)
+            .crate_name(&self.opts.crate_name)
+            .edition(edition)
+            .can_merge_doctests(self.can_merge_doctests)
+            .test_id(test_id)
+            .lang_str(&scraped_test.langstr)
+            .span(scraped_test.span)
+            .build(dcx);
         let is_standalone = !doctest.can_be_merged
             || scraped_test.langstr.compile_fail
             || scraped_test.langstr.test_harness
diff --git a/src/librustdoc/doctest/extracted.rs b/src/librustdoc/doctest/extracted.rs
index ce362eabfc4..3b17ccc78c7 100644
--- a/src/librustdoc/doctest/extracted.rs
+++ b/src/librustdoc/doctest/extracted.rs
@@ -5,7 +5,7 @@
 
 use serde::Serialize;
 
-use super::{DocTestBuilder, ScrapedDocTest};
+use super::{BuildDocTestBuilder, ScrapedDocTest};
 use crate::config::Options as RustdocOptions;
 use crate::html::markdown;
 
@@ -35,16 +35,13 @@ impl ExtractedDocTests {
     ) {
         let edition = scraped_test.edition(options);
 
-        let ScrapedDocTest { filename, line, langstr, text, name } = scraped_test;
+        let ScrapedDocTest { filename, line, langstr, text, name, .. } = scraped_test;
 
-        let doctest = DocTestBuilder::new(
-            &text,
-            Some(&opts.crate_name),
-            edition,
-            false,
-            None,
-            Some(&langstr),
-        );
+        let doctest = BuildDocTestBuilder::new(&text)
+            .crate_name(&opts.crate_name)
+            .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 d4fbfb12582..66647b88018 100644
--- a/src/librustdoc/doctest/make.rs
+++ b/src/librustdoc/doctest/make.rs
@@ -8,14 +8,14 @@ use std::sync::Arc;
 use rustc_ast::token::{Delimiter, TokenKind};
 use rustc_ast::tokenstream::TokenTree;
 use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind};
-use rustc_errors::ColorConfig;
 use rustc_errors::emitter::stderr_destination;
+use rustc_errors::{ColorConfig, DiagCtxtHandle};
 use rustc_parse::new_parser_from_source_str;
 use rustc_session::parse::ParseSess;
-use rustc_span::edition::Edition;
+use rustc_span::edition::{DEFAULT_EDITION, Edition};
 use rustc_span::source_map::SourceMap;
 use rustc_span::symbol::sym;
-use rustc_span::{FileName, kw};
+use rustc_span::{DUMMY_SP, FileName, Span, kw};
 use tracing::debug;
 
 use super::GlobalTestOptions;
@@ -35,33 +35,78 @@ struct ParseSourceInfo {
     maybe_crate_attrs: String,
 }
 
-/// This struct contains information about the doctest itself which is then used to generate
-/// doctest source code appropriately.
-pub(crate) struct DocTestBuilder {
-    pub(crate) supports_color: bool,
-    pub(crate) already_has_extern_crate: bool,
-    pub(crate) has_main_fn: bool,
-    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`.
-    pub(crate) maybe_crate_attrs: String,
-    pub(crate) crates: String,
-    pub(crate) everything_else: String,
-    pub(crate) test_id: Option<String>,
-    pub(crate) invalid_ast: bool,
-    pub(crate) can_be_merged: bool,
+/// Builder type for `DocTestBuilder`.
+pub(crate) struct BuildDocTestBuilder<'a> {
+    source: &'a str,
+    crate_name: Option<&'a str>,
+    edition: Edition,
+    can_merge_doctests: bool,
+    // If `test_id` is `None`, it means we're generating code for a code example "run" link.
+    test_id: Option<String>,
+    lang_str: Option<&'a LangString>,
+    span: Span,
 }
 
-impl DocTestBuilder {
-    pub(crate) fn new(
-        source: &str,
-        crate_name: Option<&str>,
-        edition: Edition,
-        can_merge_doctests: bool,
-        // If `test_id` is `None`, it means we're generating code for a code example "run" link.
-        test_id: Option<String>,
-        lang_str: Option<&LangString>,
-    ) -> Self {
+impl<'a> BuildDocTestBuilder<'a> {
+    pub(crate) fn new(source: &'a str) -> Self {
+        Self {
+            source,
+            crate_name: None,
+            edition: DEFAULT_EDITION,
+            can_merge_doctests: false,
+            test_id: None,
+            lang_str: None,
+            span: DUMMY_SP,
+        }
+    }
+
+    #[inline]
+    pub(crate) fn crate_name(mut self, crate_name: &'a str) -> Self {
+        self.crate_name = Some(crate_name);
+        self
+    }
+
+    #[inline]
+    pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: bool) -> Self {
+        self.can_merge_doctests = can_merge_doctests;
+        self
+    }
+
+    #[inline]
+    pub(crate) fn test_id(mut self, test_id: String) -> Self {
+        self.test_id = Some(test_id);
+        self
+    }
+
+    #[inline]
+    pub(crate) fn lang_str(mut self, lang_str: &'a LangString) -> Self {
+        self.lang_str = Some(lang_str);
+        self
+    }
+
+    #[inline]
+    pub(crate) fn span(mut self, span: Span) -> Self {
+        self.span = span;
+        self
+    }
+
+    #[inline]
+    pub(crate) fn edition(mut self, edition: Edition) -> Self {
+        self.edition = edition;
+        self
+    }
+
+    pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder {
+        let BuildDocTestBuilder {
+            source,
+            crate_name,
+            edition,
+            can_merge_doctests,
+            // If `test_id` is `None`, it means we're generating code for a code example "run" link.
+            test_id,
+            lang_str,
+            span,
+        } = self;
         let can_merge_doctests = can_merge_doctests
             && lang_str.is_some_and(|lang_str| {
                 !lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
@@ -69,7 +114,7 @@ impl DocTestBuilder {
 
         let result = rustc_driver::catch_fatal_errors(|| {
             rustc_span::create_session_if_not_set_then(edition, |_| {
-                parse_source(source, &crate_name)
+                parse_source(source, &crate_name, dcx, span)
             })
         });
 
@@ -87,7 +132,7 @@ impl DocTestBuilder {
         else {
             // If the AST returned an error, we don't want this doctest to be merged with the
             // others.
-            return Self::invalid(
+            return DocTestBuilder::invalid(
                 String::new(),
                 String::new(),
                 String::new(),
@@ -107,7 +152,7 @@ impl DocTestBuilder {
             // If this is a merged doctest and a defined macro uses `$crate`, then the path will
             // not work, so better not put it into merged doctests.
             && !(has_macro_def && everything_else.contains("$crate"));
-        Self {
+        DocTestBuilder {
             supports_color,
             has_main_fn,
             crate_attrs,
@@ -120,7 +165,26 @@ impl DocTestBuilder {
             can_be_merged,
         }
     }
+}
 
+/// This struct contains information about the doctest itself which is then used to generate
+/// doctest source code appropriately.
+pub(crate) struct DocTestBuilder {
+    pub(crate) supports_color: bool,
+    pub(crate) already_has_extern_crate: bool,
+    pub(crate) has_main_fn: bool,
+    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`.
+    pub(crate) maybe_crate_attrs: String,
+    pub(crate) crates: String,
+    pub(crate) everything_else: String,
+    pub(crate) test_id: Option<String>,
+    pub(crate) invalid_ast: bool,
+    pub(crate) can_be_merged: bool,
+}
+
+impl DocTestBuilder {
     fn invalid(
         crate_attrs: String,
         maybe_crate_attrs: String,
@@ -289,7 +353,12 @@ fn reset_error_count(psess: &ParseSess) {
 
 const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
 
-fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceInfo, ()> {
+fn parse_source(
+    source: &str,
+    crate_name: &Option<&str>,
+    parent_dcx: Option<DiagCtxtHandle<'_>>,
+    span: Span,
+) -> Result<ParseSourceInfo, ()> {
     use rustc_errors::DiagCtxt;
     use rustc_errors::emitter::{Emitter, HumanEmitter};
     use rustc_span::source_map::FilePathMapping;
@@ -466,8 +535,17 @@ fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceIn
                 }
             }
             if has_non_items {
-                // FIXME: if `info.has_main_fn` is `true`, emit a warning here to mention that
-                // this code will not be called.
+                if info.has_main_fn
+                    && let Some(dcx) = parent_dcx
+                    && !span.is_dummy()
+                {
+                    dcx.span_warn(
+                        span,
+                        "the `main` function of this doctest won't be run as it contains \
+                         expressions at the top level, meaning that the whole doctest code will be \
+                         wrapped in a function",
+                    );
+                }
                 info.has_main_fn = false;
             }
             Ok(info)
diff --git a/src/librustdoc/doctest/markdown.rs b/src/librustdoc/doctest/markdown.rs
index b3a3ce08a05..e358a7e44e5 100644
--- a/src/librustdoc/doctest/markdown.rs
+++ b/src/librustdoc/doctest/markdown.rs
@@ -4,7 +4,7 @@ use std::fs::read_to_string;
 use std::sync::{Arc, Mutex};
 
 use rustc_session::config::Input;
-use rustc_span::FileName;
+use rustc_span::{DUMMY_SP, FileName};
 use tempfile::tempdir;
 
 use super::{
@@ -24,7 +24,14 @@ impl DocTestVisitor for MdCollector {
         let filename = self.filename.clone();
         // First line of Markdown is line 1.
         let line = 1 + rel_line.offset();
-        self.tests.push(ScrapedDocTest::new(filename, line, self.cur_path.clone(), config, test));
+        self.tests.push(ScrapedDocTest::new(
+            filename,
+            line,
+            self.cur_path.clone(),
+            config,
+            test,
+            DUMMY_SP,
+        ));
     }
 
     fn visit_header(&mut self, name: &str, level: u32) {
@@ -107,7 +114,7 @@ pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> {
     find_testable_code(&input_str, &mut md_collector, codes, None);
 
     let mut collector = CreateRunnableDocTests::new(options.clone(), opts);
-    md_collector.tests.into_iter().for_each(|t| collector.add_test(t));
+    md_collector.tests.into_iter().for_each(|t| collector.add_test(t, None));
     let CreateRunnableDocTests { opts, rustdoc_options, standalone_tests, mergeable_tests, .. } =
         collector;
     crate::doctest::run_tests(
diff --git a/src/librustdoc/doctest/rust.rs b/src/librustdoc/doctest/rust.rs
index 43dcfab880b..f9d2aa3d3b4 100644
--- a/src/librustdoc/doctest/rust.rs
+++ b/src/librustdoc/doctest/rust.rs
@@ -1,5 +1,6 @@
 //! Doctest functionality used only for doctests in `.rs` source files.
 
+use std::cell::Cell;
 use std::env;
 use std::sync::Arc;
 
@@ -47,13 +48,33 @@ impl RustCollector {
 
 impl DocTestVisitor for RustCollector {
     fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) {
-        let line = self.get_base_line() + rel_line.offset();
+        let base_line = self.get_base_line();
+        let line = base_line + rel_line.offset();
+        let count = Cell::new(base_line);
+        let span = if line > base_line {
+            match self.source_map.span_extend_while(self.position, |c| {
+                if c == '\n' {
+                    let count_v = count.get();
+                    count.set(count_v + 1);
+                    if count_v >= line {
+                        return false;
+                    }
+                }
+                true
+            }) {
+                Ok(sp) => self.source_map.span_extend_to_line(sp.shrink_to_hi()),
+                _ => self.position,
+            }
+        } else {
+            self.position
+        };
         self.tests.push(ScrapedDocTest::new(
             self.get_filename(),
             line,
             self.cur_path.clone(),
             config,
             test,
+            span,
         ));
     }
 
diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs
index 618c2041b43..08248fdf39b 100644
--- a/src/librustdoc/doctest/tests.rs
+++ b/src/librustdoc/doctest/tests.rs
@@ -1,8 +1,6 @@
 use std::path::PathBuf;
 
-use rustc_span::edition::DEFAULT_EDITION;
-
-use super::{DocTestBuilder, GlobalTestOptions};
+use super::{BuildDocTestBuilder, GlobalTestOptions};
 
 fn make_test(
     test_code: &str,
@@ -11,14 +9,14 @@ fn make_test(
     opts: &GlobalTestOptions,
     test_id: Option<&str>,
 ) -> (String, usize) {
-    let doctest = DocTestBuilder::new(
-        test_code,
-        crate_name,
-        DEFAULT_EDITION,
-        false,
-        test_id.map(|s| s.to_string()),
-        None,
-    );
+    let mut builder = BuildDocTestBuilder::new(test_code);
+    if let Some(crate_name) = crate_name {
+        builder = builder.crate_name(crate_name);
+    }
+    if let Some(test_id) = test_id {
+        builder = builder.test_id(test_id.to_string());
+    }
+    let doctest = builder.build(None);
     let (code, line_offset) =
         doctest.generate_unique_doctest(test_code, dont_insert_main, opts, crate_name);
     (code, line_offset)
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index fc46293e7ea..ad7dfafd90c 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -303,7 +303,11 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
                 attrs: vec![],
                 args_file: PathBuf::new(),
             };
-            let doctest = doctest::DocTestBuilder::new(&test, krate, edition, false, None, None);
+            let mut builder = doctest::BuildDocTestBuilder::new(&test).edition(edition);
+            if let Some(krate) = krate {
+                builder = builder.crate_name(krate);
+            }
+            let doctest = builder.build(None);
             let (test, _) = doctest.generate_unique_doctest(&test, false, &opts, krate);
             let channel = if test.contains("#![feature(") { "&amp;version=nightly" } else { "" };
 
diff --git a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stderr b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stderr
new file mode 100644
index 00000000000..113fb7ccb60
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stderr
@@ -0,0 +1,8 @@
+warning: the `main` function of this doctest won't be run as it contains expressions at the top level, meaning that the whole doctest code will be wrapped in a function
+  --> $DIR/failed-doctest-extra-semicolon-on-item.rs:11:1
+   |
+11 | /// ```rust
+   | ^^^^^^^^^^^
+
+warning: 1 warning emitted
+
diff --git a/tests/rustdoc-ui/doctest/main-alongside-stmts.stderr b/tests/rustdoc-ui/doctest/main-alongside-stmts.stderr
new file mode 100644
index 00000000000..d90a289ca69
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/main-alongside-stmts.stderr
@@ -0,0 +1,14 @@
+warning: the `main` function of this doctest won't be run as it contains expressions at the top level, meaning that the whole doctest code will be wrapped in a function
+  --> $DIR/main-alongside-stmts.rs:17:1
+   |
+17 | //! ```
+   | ^^^^^^^
+
+warning: the `main` function of this doctest won't be run as it contains expressions at the top level, meaning that the whole doctest code will be wrapped in a function
+  --> $DIR/main-alongside-stmts.rs:26:1
+   |
+26 | //! ```
+   | ^^^^^^^
+
+warning: 2 warnings emitted
+
diff --git a/tests/rustdoc-ui/doctest/test-main-alongside-exprs.stderr b/tests/rustdoc-ui/doctest/test-main-alongside-exprs.stderr
new file mode 100644
index 00000000000..0dc7c2a2eea
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/test-main-alongside-exprs.stderr
@@ -0,0 +1,8 @@
+warning: the `main` function of this doctest won't be run as it contains expressions at the top level, meaning that the whole doctest code will be wrapped in a function
+  --> $DIR/test-main-alongside-exprs.rs:15:1
+   |
+15 | //! ```
+   | ^^^^^^^
+
+warning: 1 warning emitted
+
diff --git a/tests/rustdoc-ui/doctest/warn-main-not-called.rs b/tests/rustdoc-ui/doctest/warn-main-not-called.rs
new file mode 100644
index 00000000000..25d92e9cee9
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/warn-main-not-called.rs
@@ -0,0 +1,22 @@
+//@ check-pass
+//@ 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"
+
+// In case there is a `main` function in the doctest alongside expressions,
+// the whole doctest will be wrapped into a function and the `main` function
+// won't be called.
+
+//! ```
+//! macro_rules! bla {
+//!     ($($x:tt)*) => {}
+//! }
+//!
+//! let x = 12;
+//! bla!(fn main ());
+//! ```
+//!
+//! ```
+//! let x = 12;
+//! fn main() {}
+//! ```
diff --git a/tests/rustdoc-ui/doctest/warn-main-not-called.stderr b/tests/rustdoc-ui/doctest/warn-main-not-called.stderr
new file mode 100644
index 00000000000..3a079f47555
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/warn-main-not-called.stderr
@@ -0,0 +1,14 @@
+warning: the `main` function of this doctest won't be run as it contains expressions at the top level, meaning that the whole doctest code will be wrapped in a function
+  --> $DIR/warn-main-not-called.rs:10:1
+   |
+10 | //! ```
+   | ^^^^^^^
+
+warning: the `main` function of this doctest won't be run as it contains expressions at the top level, meaning that the whole doctest code will be wrapped in a function
+  --> $DIR/warn-main-not-called.rs:19:1
+   |
+19 | //! ```
+   | ^^^^^^^
+
+warning: 2 warnings emitted
+
diff --git a/tests/rustdoc-ui/doctest/warn-main-not-called.stdout b/tests/rustdoc-ui/doctest/warn-main-not-called.stdout
new file mode 100644
index 00000000000..07cdddc7b94
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/warn-main-not-called.stdout
@@ -0,0 +1,7 @@
+
+running 2 tests
+test $DIR/warn-main-not-called.rs - (line 10) ... ok
+test $DIR/warn-main-not-called.rs - (line 19) ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+