From a9cf06b5ef622082521046e92cfc5abf2462f777 Mon Sep 17 00:00:00 2001 From: steamproof <93405617+pbkx@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:05:04 -0700 Subject: [PATCH 1/2] rustdoc: correct doctest span for trailing semicolon after item --- src/librustdoc/doctest.rs | 23 +++- src/librustdoc/doctest/make.rs | 48 +++++++-- src/librustdoc/doctest/markdown.rs | 13 ++- src/librustdoc/doctest/rust.rs | 18 +++- src/librustdoc/doctest/tests.rs | 1 + src/librustdoc/html/markdown.rs | 101 ++++++++++++++---- .../passes/check_doc_test_visibility.rs | 6 +- .../passes/lint/check_code_block_syntax.rs | 2 +- .../failed-doctest-extra-semicolon-on-item.rs | 9 +- ...led-doctest-extra-semicolon-on-item.stderr | 6 +- ...led-doctest-extra-semicolon-on-item.stdout | 2 +- .../doctest/main-alongside-stmts.rs | 2 +- .../doctest/main-alongside-stmts.stderr | 6 +- .../doctest/warn-main-not-called.rs | 4 +- .../doctest/warn-main-not-called.stderr | 12 +-- 15 files changed, 199 insertions(+), 54 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 6ec4aaf282238..2b7f9c4dbb7fa 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -36,7 +36,7 @@ use tracing::{debug, info}; use self::rust::HirCollector; use crate::config::{MergeDoctests, Options as RustdocOptions, OutputFormat}; -use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine}; +use crate::html::markdown::{CodeLineMapping, ErrorCodes, Ignore, LangString, MdRelLine}; use crate::lint::init_lints; /// Type used to display times (compilation and total) information for merged doctests. @@ -940,6 +940,7 @@ pub(crate) struct ScrapedDocTest { text: String, name: String, span: Span, + code_mappings: Vec, global_crate_attrs: Vec, } @@ -951,6 +952,7 @@ impl ScrapedDocTest { langstr: LangString, text: String, span: Span, + code_mappings: Vec, global_crate_attrs: Vec, ) -> Self { let mut item_path = logical_path.join("::"); @@ -963,7 +965,7 @@ impl ScrapedDocTest { filename.display(RemapPathScopeComponents::DOCUMENTATION) ); - Self { filename, line, langstr, text, name, span, global_crate_attrs } + Self { filename, line, langstr, text, name, span, code_mappings, global_crate_attrs } } fn edition(&self, opts: &RustdocOptions) -> Edition { self.langstr.edition.unwrap_or(opts.edition) @@ -984,7 +986,13 @@ impl ScrapedDocTest { } pub(crate) trait DocTestVisitor { - fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine); + fn visit_test( + &mut self, + test: String, + config: LangString, + rel_line: MdRelLine, + code_mappings: Vec, + ); fn visit_header(&mut self, _name: &str, _level: u32) {} } @@ -1055,6 +1063,7 @@ impl CreateRunnableDocTests { .test_id(test_id) .lang_str(&scraped_test.langstr) .span(scraped_test.span) + .code_mappings(&scraped_test.code_mappings) .build(dcx); let is_standalone = !doctest.can_be_merged || self.rustdoc_options.no_capture @@ -1228,7 +1237,13 @@ fn doctest_run_fn( #[cfg(test)] // used in tests impl DocTestVisitor for Vec { - fn visit_test(&mut self, _test: String, _config: LangString, rel_line: MdRelLine) { + fn visit_test( + &mut self, + _test: String, + _config: LangString, + rel_line: MdRelLine, + _code_mappings: Vec, + ) { self.push(1 + rel_line.offset()); } } diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs index 7dd738abca433..ac82829fa662e 100644 --- a/src/librustdoc/doctest/make.rs +++ b/src/librustdoc/doctest/make.rs @@ -16,10 +16,10 @@ use rustc_session::parse::ParseSess; use rustc_span::edition::{DEFAULT_EDITION, Edition}; use rustc_span::source_map::SourceMap; use rustc_span::symbol::sym; -use rustc_span::{DUMMY_SP, FileName, Span, kw}; +use rustc_span::{DUMMY_SP, FileName, InnerSpan, Span, kw}; use tracing::debug; -use super::GlobalTestOptions; +use super::{CodeLineMapping, GlobalTestOptions}; use crate::config::MergeDoctests; use crate::display::Joined as _; use crate::html::markdown::LangString; @@ -47,6 +47,7 @@ pub(crate) struct BuildDocTestBuilder<'a> { test_id: Option, lang_str: Option<&'a LangString>, span: Span, + code_mappings: &'a [CodeLineMapping], global_crate_attrs: Vec, } @@ -60,6 +61,7 @@ impl<'a> BuildDocTestBuilder<'a> { test_id: None, lang_str: None, span: DUMMY_SP, + code_mappings: &[], global_crate_attrs: Vec::new(), } } @@ -94,6 +96,12 @@ impl<'a> BuildDocTestBuilder<'a> { self } + #[inline] + pub(crate) fn code_mappings(mut self, code_mappings: &'a [CodeLineMapping]) -> Self { + self.code_mappings = code_mappings; + self + } + #[inline] pub(crate) fn edition(mut self, edition: Edition) -> Self { self.edition = edition; @@ -116,12 +124,13 @@ impl<'a> BuildDocTestBuilder<'a> { test_id, lang_str, span, + code_mappings, global_crate_attrs, } = self; let result = rustc_driver::catch_fatal_errors(|| { rustc_span::create_session_if_not_set_then(edition, |_| { - parse_source(source, &crate_name, dcx, span) + parse_source(source, &crate_name, dcx, span, code_mappings) }) }); @@ -454,6 +463,7 @@ fn parse_source( crate_name: &Option<&str>, parent_dcx: Option>, span: Span, + code_mappings: &[CodeLineMapping], ) -> Result { use rustc_errors::DiagCtxt; use rustc_errors::annotate_snippet_emitter_writer::AnnotateSnippetEmitter; @@ -504,6 +514,24 @@ fn parse_source( *prev_span_hi = hi; } + fn span_in_doctest_source(span: Span, code_mappings: &[CodeLineMapping]) -> Option { + let extra_len = DOCTEST_CODE_WRAPPER.len(); + let lo = (span.lo().0 as usize).checked_sub(extra_len)?; + let hi = (span.hi().0 as usize).checked_sub(extra_len)?; + if hi < lo { + return None; + } + code_mappings.iter().find_map(|mapping| { + if mapping.generated.start <= lo && hi <= mapping.generated.end { + let start = lo - mapping.generated.start; + let end = hi - mapping.generated.start; + Some(mapping.original.from_inner(InnerSpan::new(start, end))) + } else { + None + } + }) + } + fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool { let mut is_extern_crate = false; if !info.has_global_allocator @@ -574,6 +602,7 @@ fn parse_source( } } let mut has_non_items = false; + let mut first_non_item_span = None; for stmt in &body.stmts { let mut is_extern_crate = false; match stmt.kind { @@ -611,8 +640,12 @@ fn parse_source( return Err(()); } has_non_items = true; + first_non_item_span.get_or_insert(stmt.span); + } + StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => { + has_non_items = true; + first_non_item_span.get_or_insert(stmt.span); } - StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true, } // Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to @@ -638,12 +671,15 @@ fn parse_source( } } if has_non_items { + let warning_span = first_non_item_span + .and_then(|span| span_in_doctest_source(span, code_mappings)) + .unwrap_or(span); if info.has_main_fn && let Some(dcx) = parent_dcx - && !span.is_dummy() + && !warning_span.is_dummy() { dcx.span_warn( - span, + warning_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", diff --git a/src/librustdoc/doctest/markdown.rs b/src/librustdoc/doctest/markdown.rs index f02e0d6df1983..a994f1d974ae3 100644 --- a/src/librustdoc/doctest/markdown.rs +++ b/src/librustdoc/doctest/markdown.rs @@ -13,7 +13,9 @@ use super::{ CreateRunnableDocTests, DocTestVisitor, GlobalTestOptions, ScrapedDocTest, generate_args_file, }; use crate::config::Options; -use crate::html::markdown::{ErrorCodes, LangString, MdRelLine, find_testable_code}; +use crate::html::markdown::{ + CodeLineMapping, ErrorCodes, LangString, MdRelLine, find_testable_code, +}; struct MdCollector { tests: Vec, @@ -22,7 +24,13 @@ struct MdCollector { } impl DocTestVisitor for MdCollector { - fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) { + fn visit_test( + &mut self, + test: String, + config: LangString, + rel_line: MdRelLine, + code_mappings: Vec, + ) { let filename = self.filename.clone(); // First line of Markdown is line 1. let line = 1 + rel_line.offset(); @@ -33,6 +41,7 @@ impl DocTestVisitor for MdCollector { config, test, DUMMY_SP, + code_mappings, Vec::new(), )); } diff --git a/src/librustdoc/doctest/rust.rs b/src/librustdoc/doctest/rust.rs index b4397b1f01ffa..d89fb2ae1767b 100644 --- a/src/librustdoc/doctest/rust.rs +++ b/src/librustdoc/doctest/rust.rs @@ -17,7 +17,7 @@ use rustc_span::{BytePos, DUMMY_SP, FileName, Pos, Span}; use super::{DocTestVisitor, ScrapedDocTest}; use crate::clean::{Attributes, CfgInfo, extract_cfg_from_attrs}; -use crate::html::markdown::{self, ErrorCodes, LangString, MdRelLine}; +use crate::html::markdown::{self, CodeLineMapping, ErrorCodes, LangString, MdRelLine}; struct RustCollector { source_map: Arc, @@ -41,7 +41,13 @@ impl RustCollector { } impl DocTestVisitor for RustCollector { - fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) { + fn visit_test( + &mut self, + test: String, + config: LangString, + rel_line: MdRelLine, + code_mappings: Vec, + ) { let base_line = self.get_base_line(); let line = base_line + rel_line.offset(); let count = Cell::new(base_line); @@ -69,6 +75,7 @@ impl DocTestVisitor for RustCollector { config, test, span, + code_mappings, self.global_crate_attrs.clone(), )); } @@ -199,7 +206,12 @@ impl HirCollector<'_> { &doc, &mut self.collector, self.codes, - Some(&crate::html::markdown::ExtraInfo::new(self.tcx, def_id, span)), + Some(&crate::html::markdown::ExtraInfo::new( + self.tcx, + def_id, + span, + Some(&attrs.doc_strings), + )), ); } diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index ccc3e55a33122..94ac0765becd4 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -479,6 +479,7 @@ fn get_extracted_doctests(code: &str) -> ExtractedDocTests { code.to_string(), DUMMY_SP, Vec::new(), + Vec::new(), ), &opts, Edition::Edition2018, diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index b05a831d119cb..6240caf4ad196 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -41,10 +41,10 @@ use rustc_errors::{Diag, DiagMessage}; use rustc_hir::def_id::LocalDefId; use rustc_middle::ty::TyCtxt; pub(crate) use rustc_resolve::rustdoc::main_body_opts; -use rustc_resolve::rustdoc::may_be_doc_link; use rustc_resolve::rustdoc::pulldown_cmark::{ self, BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd, html, }; +use rustc_resolve::rustdoc::{DocFragment, may_be_doc_link, source_span_for_markdown_range}; use rustc_span::edition::Edition; use rustc_span::{Span, Symbol}; use tracing::{debug, trace}; @@ -719,11 +719,17 @@ impl MdRelLine { } } +#[derive(Clone, Debug)] +pub(crate) struct CodeLineMapping { + pub(crate) generated: Range, + pub(crate) original: Span, +} + pub(crate) fn find_testable_code( doc: &str, tests: &mut T, error_codes: ErrorCodes, - extra_info: Option<&ExtraInfo<'_>>, + extra_info: Option<&ExtraInfo<'_, '_>>, ) { find_codes(doc, tests, error_codes, extra_info, false) } @@ -732,7 +738,7 @@ pub(crate) fn find_codes( doc: &str, tests: &mut T, error_codes: ErrorCodes, - extra_info: Option<&ExtraInfo<'_>>, + extra_info: Option<&ExtraInfo<'_, '_>>, include_non_rust: bool, ) { let mut parser = Parser::new_ext(doc, main_body_opts()).into_offset_iter(); @@ -757,15 +763,14 @@ pub(crate) fn find_codes( } let mut test_s = String::new(); + let mut text_events = Vec::new(); - while let Some((Event::Text(s), _)) = parser.next() { + while let Some((Event::Text(s), offset)) = parser.next() { + let start = test_s.len(); test_s.push_str(&s); + text_events.push((start..test_s.len(), offset)); } - let text = test_s - .lines() - .map(|l| map_line(l).for_code()) - .collect::>>() - .join("\n"); + let (text, code_mappings) = map_code_block(doc, &test_s, &text_events, extra_info); nb_lines += doc[prev_offset..offset.start].lines().count(); // If there are characters between the preceding line ending and @@ -775,7 +780,7 @@ pub(crate) fn find_codes( nb_lines -= 1; } let line = MdRelLine::new(nb_lines); - tests.visit_test(text, block_info, line); + tests.visit_test(text, block_info, line, code_mappings); prev_offset = offset.start; } Event::Start(Tag::Heading { level, .. }) => { @@ -791,15 +796,75 @@ pub(crate) fn find_codes( } } -pub(crate) struct ExtraInfo<'tcx> { +fn map_code_block( + doc: &str, + code: &str, + text_events: &[(Range, Range)], + extra_info: Option<&ExtraInfo<'_, '_>>, +) -> (String, Vec) { + let mut text = String::new(); + let mut code_mappings = Vec::new(); + let mut code_line_start = 0; + + for (line_index, line) in code.lines().enumerate() { + if line_index != 0 { + text.push('\n'); + } + + let generated_start = text.len(); + let mapped_line = map_line(line).for_code(); + text.push_str(&mapped_line); + let generated = generated_start..text.len(); + + if mapped_line.as_ref() == line + && let Some(extra_info) = extra_info + && let Some(fragments) = extra_info.fragments + { + let code_line = code_line_start..code_line_start + line.len(); + if let Some(md_range) = markdown_range_for_code_range(text_events, code_line) + && let Some((original, _)) = + source_span_for_markdown_range(extra_info.tcx, doc, &md_range, fragments) + { + code_mappings.push(CodeLineMapping { generated, original }); + } + } + + code_line_start += line.len() + 1; + } + + (text, code_mappings) +} + +fn markdown_range_for_code_range( + text_events: &[(Range, Range)], + code_range: Range, +) -> Option> { + text_events.iter().find_map(|(event_code_range, event_md_range)| { + if event_code_range.start <= code_range.start && code_range.end <= event_code_range.end { + let start = event_md_range.start + code_range.start - event_code_range.start; + let end = event_md_range.start + code_range.end - event_code_range.start; + Some(start..end) + } else { + None + } + }) +} + +pub(crate) struct ExtraInfo<'doc, 'tcx> { def_id: LocalDefId, sp: Span, tcx: TyCtxt<'tcx>, + fragments: Option<&'doc [DocFragment]>, } -impl<'tcx> ExtraInfo<'tcx> { - pub(crate) fn new(tcx: TyCtxt<'tcx>, def_id: LocalDefId, sp: Span) -> ExtraInfo<'tcx> { - ExtraInfo { def_id, sp, tcx } +impl<'doc, 'tcx> ExtraInfo<'doc, 'tcx> { + pub(crate) fn new( + tcx: TyCtxt<'tcx>, + def_id: LocalDefId, + sp: Span, + fragments: Option<&'doc [DocFragment]>, + ) -> ExtraInfo<'doc, 'tcx> { + ExtraInfo { def_id, sp, tcx, fragments } } fn error_invalid_codeblock_attr(&self, msg: impl Into) { @@ -890,7 +955,7 @@ pub(crate) struct TagIterator<'a, 'tcx> { inner: Peekable>, data: &'a str, is_in_attribute_block: bool, - extra: Option<&'a ExtraInfo<'tcx>>, + extra: Option<&'a ExtraInfo<'a, 'tcx>>, is_error: bool, } @@ -917,7 +982,7 @@ struct Indices { } impl<'a, 'tcx> TagIterator<'a, 'tcx> { - pub(crate) fn new(data: &'a str, extra: Option<&'a ExtraInfo<'tcx>>) -> Self { + pub(crate) fn new(data: &'a str, extra: Option<&'a ExtraInfo<'a, 'tcx>>) -> Self { Self { inner: data.char_indices().peekable(), data, @@ -1172,7 +1237,7 @@ impl LangString { fn parse( string: &str, allow_error_code_check: ErrorCodes, - extra: Option<&ExtraInfo<'_>>, + extra: Option<&ExtraInfo<'_, '_>>, ) -> Self { let allow_error_code_check = allow_error_code_check.as_bool(); let mut seen_rust_tags = false; @@ -1955,7 +2020,7 @@ pub(crate) struct RustCodeBlock { /// Returns a range of bytes for each code block in the markdown that is tagged as `rust` or /// untagged (and assumed to be rust). -pub(crate) fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_>) -> Vec { +pub(crate) fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_, '_>) -> Vec { let mut code_blocks = vec![]; if md.is_empty() { diff --git a/src/librustdoc/passes/check_doc_test_visibility.rs b/src/librustdoc/passes/check_doc_test_visibility.rs index a8a9a42df4575..13452539c8295 100644 --- a/src/librustdoc/passes/check_doc_test_visibility.rs +++ b/src/librustdoc/passes/check_doc_test_visibility.rs @@ -15,7 +15,9 @@ use crate::clean; use crate::clean::utils::inherits_doc_hidden; use crate::clean::*; use crate::core::DocContext; -use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine, find_testable_code}; +use crate::html::markdown::{ + CodeLineMapping, ErrorCodes, Ignore, LangString, MdRelLine, find_testable_code, +}; use crate::visit::DocVisitor; pub(crate) const CHECK_DOC_TEST_VISIBILITY: Pass = Pass { @@ -47,7 +49,7 @@ pub(crate) struct Tests { } impl crate::doctest::DocTestVisitor for Tests { - fn visit_test(&mut self, _: String, config: LangString, _: MdRelLine) { + fn visit_test(&mut self, _: String, config: LangString, _: MdRelLine, _: Vec) { if config.rust && config.ignore == Ignore::None { self.found_tests += 1; } diff --git a/src/librustdoc/passes/lint/check_code_block_syntax.rs b/src/librustdoc/passes/lint/check_code_block_syntax.rs index 1c017f9a635c1..b25d081f6ae80 100644 --- a/src/librustdoc/passes/lint/check_code_block_syntax.rs +++ b/src/librustdoc/passes/lint/check_code_block_syntax.rs @@ -21,7 +21,7 @@ use crate::html::markdown::{self, RustCodeBlock}; pub(crate) fn visit_item(cx: &DocContext<'_>, item: &clean::Item, dox: &str) { if let Some(def_id) = item.item_id.as_local_def_id() { let sp = item.attr_span(cx.tcx); - let extra = crate::html::markdown::ExtraInfo::new(cx.tcx, def_id, sp); + let extra = crate::html::markdown::ExtraInfo::new(cx.tcx, def_id, sp, None); for code_block in markdown::rust_code_blocks(dox, &extra) { check_rust_syntax(cx, item, dox, code_block); } diff --git a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.rs b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.rs index 05e4a348d1190..ee2318b3165cb 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.rs +++ b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.rs @@ -1,5 +1,7 @@ // FIXME: if/when the output of the test harness can be tested on its own, this test should be // adapted to use that, and that normalize line can go away +// Regression test for #157371. The warning for a trailing semicolon after an item should +// point inside the doctest, not at unrelated source following the documentation. //@ compile-flags:--test //@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR" @@ -7,13 +9,16 @@ //@ check-pass /// +/// /// /// ```rust -//~^ WARN the `main` function of this doctest won't be run /// struct S {}; +//~^ WARN the `main` function of this doctest won't be run /// /// fn main() { /// assert_eq!(0, 1); /// } /// ``` -mod m {} +mod m { + // This line should not be highlighted by the doctest warning. +} 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 index cffda43ba1c94..1ea193a731e9d 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stderr +++ b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stderr @@ -1,8 +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 + --> $DIR/failed-doctest-extra-semicolon-on-item.rs:15:16 | -LL | /// ```rust - | ^^^^^^^^^^^ +LL | /// struct S {}; + | ^ warning: 1 warning emitted diff --git a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stdout b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stdout index 1068b98cb0fbb..bcb13c84cfa33 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stdout +++ b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stdout @@ -1,6 +1,6 @@ running 1 test -test $DIR/failed-doctest-extra-semicolon-on-item.rs - m (line 11) ... ok +test $DIR/failed-doctest-extra-semicolon-on-item.rs - m (line 14) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME diff --git a/tests/rustdoc-ui/doctest/main-alongside-stmts.rs b/tests/rustdoc-ui/doctest/main-alongside-stmts.rs index 595de13393293..73297d8ab915c 100644 --- a/tests/rustdoc-ui/doctest/main-alongside-stmts.rs +++ b/tests/rustdoc-ui/doctest/main-alongside-stmts.rs @@ -23,7 +23,7 @@ //! assert!(false); //! } //! ``` -//~v WARN the `main` function of this doctest won't be run +//~vvv WARN the `main` function of this doctest won't be run //! //! ``` //! let x = 2; diff --git a/tests/rustdoc-ui/doctest/main-alongside-stmts.stderr b/tests/rustdoc-ui/doctest/main-alongside-stmts.stderr index b7a5421f8f737..6e6055cff317e 100644 --- a/tests/rustdoc-ui/doctest/main-alongside-stmts.stderr +++ b/tests/rustdoc-ui/doctest/main-alongside-stmts.stderr @@ -5,10 +5,10 @@ LL | //! ``` | ^^^^^^^ 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:27:1 + --> $DIR/main-alongside-stmts.rs:29:5 | -LL | //! - | ^^^ +LL | //! let x = 2; + | ^^^^^^^^^^ warning: 2 warnings emitted diff --git a/tests/rustdoc-ui/doctest/warn-main-not-called.rs b/tests/rustdoc-ui/doctest/warn-main-not-called.rs index ec762486d5d42..78d085275f57f 100644 --- a/tests/rustdoc-ui/doctest/warn-main-not-called.rs +++ b/tests/rustdoc-ui/doctest/warn-main-not-called.rs @@ -8,7 +8,7 @@ // won't be called. //! ``` -//~^ WARN the `main` function of this doctest won't be run +//~vvvvv WARN the `main` function of this doctest won't be run //! macro_rules! bla { //! ($($x:tt)*) => {} //! } @@ -18,7 +18,7 @@ //! ``` //! //! ``` -//~^^ WARN the `main` function of this doctest won't be run +//~v WARN the `main` function of this doctest won't be run //! 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 index 5feca6f9175fe..7dc56b0fbf16e 100644 --- a/tests/rustdoc-ui/doctest/warn-main-not-called.stderr +++ b/tests/rustdoc-ui/doctest/warn-main-not-called.stderr @@ -1,14 +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 + --> $DIR/warn-main-not-called.rs:16:5 | -LL | //! ``` - | ^^^^^^^ +LL | //! let x = 12; + | ^^^^^^^^^^^ 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 + --> $DIR/warn-main-not-called.rs:22:5 | -LL | //! - | ^^^ +LL | //! let x = 12; + | ^^^^^^^^^^^ warning: 2 warnings emitted From 1380a580f623d92dddb565fd4b09adf272aa8ceb Mon Sep 17 00:00:00 2001 From: steamproof <93405617+pbkx@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:25:23 -0700 Subject: [PATCH 2/2] Add rustdoc regression for doctest warning span --- ...rning-span-outside-doctest-issue-157371.rs | 37 +++++++++++++++++++ ...g-span-outside-doctest-issue-157371.stderr | 8 ++++ ...g-span-outside-doctest-issue-157371.stdout | 6 +++ 3 files changed, 51 insertions(+) create mode 100644 tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.rs create mode 100644 tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.stderr create mode 100644 tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.stdout diff --git a/tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.rs b/tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.rs new file mode 100644 index 0000000000000..15fa684c74b65 --- /dev/null +++ b/tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.rs @@ -0,0 +1,37 @@ +// Regression test for #157371. +// The warning for a top-level expression in a doctest should point at the stray +// semicolon inside the doctest, not at unrelated source below the doc comment. + +//@ compile-flags:--test +//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR" +//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME" +//@ check-pass + +/// This part creates the doctest with a stray `;`. +/// +/// ``` +/// #[derive(Debug, PartialEq)] +/// struct Type { +/// left: i32, +/// right: i32, +/// }; // <- Stray `;`. +//~^ WARN the `main` function of this doctest won't be run +/// +/// fn main() { +/// let x = Type { +/// left: 10, +/// right: 20, +/// }; +/// assert_eq!( +/// x, +/// Type { +/// left: 10, +/// right: 20, +/// }, +/// ); +/// } +/// ``` +pub fn add(left: u64, right: u64) -> u64 { + // This code is completely unrelated to the doctest. + left + right +} diff --git a/tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.stderr b/tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.stderr new file mode 100644 index 0000000000000..1aa1bfcc323bd --- /dev/null +++ b/tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.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/main-warning-span-outside-doctest-issue-157371.rs:17:6 + | +LL | /// }; // <- Stray `;`. + | ^ + +warning: 1 warning emitted + diff --git a/tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.stdout b/tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.stdout new file mode 100644 index 0000000000000..c39acc99dbd7f --- /dev/null +++ b/tests/rustdoc-ui/doctest/main-warning-span-outside-doctest-issue-157371.stdout @@ -0,0 +1,6 @@ + +running 1 test +test $DIR/main-warning-span-outside-doctest-issue-157371.rs - add (line 12) ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME +