diff options
| author | bors <bors@rust-lang.org> | 2025-05-22 08:40:58 +0000 |
|---|---|---|
| committer | bors <bors@rust-lang.org> | 2025-05-22 08:40:58 +0000 |
| commit | 1d679446b01e65f9bc9ae609d0ae1e4a9c0ccaa3 (patch) | |
| tree | 99e7223e4264ae071bf3e6fac30b7f12278965af | |
| parent | 2cd37831b0706c9f2d6e7e20eb556dc7548bf732 (diff) | |
| parent | f4d41a5cbd00ee3490beb3a6fff9bd909c137153 (diff) | |
| download | rust-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.rs | 28 | ||||
| -rw-r--r-- | src/librustdoc/doctest/extracted.rs | 17 | ||||
| -rw-r--r-- | src/librustdoc/doctest/make.rs | 146 | ||||
| -rw-r--r-- | src/librustdoc/doctest/markdown.rs | 13 | ||||
| -rw-r--r-- | src/librustdoc/doctest/rust.rs | 23 | ||||
| -rw-r--r-- | src/librustdoc/doctest/tests.rs | 20 | ||||
| -rw-r--r-- | src/librustdoc/html/markdown.rs | 6 | ||||
| -rw-r--r-- | tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stderr | 8 | ||||
| -rw-r--r-- | tests/rustdoc-ui/doctest/main-alongside-stmts.stderr | 14 | ||||
| -rw-r--r-- | tests/rustdoc-ui/doctest/test-main-alongside-exprs.stderr | 8 | ||||
| -rw-r--r-- | tests/rustdoc-ui/doctest/warn-main-not-called.rs | 22 | ||||
| -rw-r--r-- | tests/rustdoc-ui/doctest/warn-main-not-called.stderr | 14 | ||||
| -rw-r--r-- | tests/rustdoc-ui/doctest/warn-main-not-called.stdout | 7 |
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("#