From b791eaa4480a8e3acffe3faad4de0462b8476aca Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 30 Apr 2025 20:29:29 +0200 Subject: [PATCH] Emit a warning if the doctest `main` function will not be run --- src/librustdoc/doctest.rs | 12 +++++--- src/librustdoc/doctest/extracted.rs | 5 +++- src/librustdoc/doctest/make.rs | 28 +++++++++++++++---- src/librustdoc/doctest/markdown.rs | 13 +++++++-- src/librustdoc/doctest/rust.rs | 23 ++++++++++++++- src/librustdoc/doctest/tests.rs | 3 ++ src/librustdoc/html/markdown.rs | 6 ++-- ...led-doctest-extra-semicolon-on-item.stderr | 8 ++++++ .../doctest/main-alongside-stmts.stderr | 14 ++++++++++ .../doctest/test-main-alongside-exprs.stderr | 8 ++++++ 10 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stderr create mode 100644 tests/rustdoc-ui/doctest/main-alongside-stmts.stderr create mode 100644 tests/rustdoc-ui/doctest/test-main-alongside-exprs.stderr diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 829a9ca6e7dd..5b85eb54a5c4 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -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; @@ -239,7 +239,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)) } @@ -872,6 +872,7 @@ pub(crate) struct ScrapedDocTest { langstr: LangString, text: String, name: String, + span: Span, } impl ScrapedDocTest { @@ -881,6 +882,7 @@ impl ScrapedDocTest { logical_path: Vec, langstr: LangString, text: String, + span: Span, ) -> Self { let mut item_path = logical_path.join("::"); item_path.retain(|c| c != ' '); @@ -890,7 +892,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) @@ -946,7 +948,7 @@ impl CreateRunnableDocTests { } } - fn add_test(&mut self, scraped_test: ScrapedDocTest) { + fn add_test(&mut self, scraped_test: ScrapedDocTest, dcx: Option>) { // For example `module/file.rs` would become `module_file_rs` let file = scraped_test .filename @@ -977,6 +979,8 @@ impl CreateRunnableDocTests { self.can_merge_doctests, Some(test_id), Some(&scraped_test.langstr), + dcx, + scraped_test.span, ); let is_standalone = !doctest.can_be_merged || scraped_test.langstr.compile_fail diff --git a/src/librustdoc/doctest/extracted.rs b/src/librustdoc/doctest/extracted.rs index ce362eabfc4c..d82bca3279db 100644 --- a/src/librustdoc/doctest/extracted.rs +++ b/src/librustdoc/doctest/extracted.rs @@ -3,6 +3,7 @@ //! This module contains the logic to extract doctests and output a JSON containing this //! information. +use rustc_span::DUMMY_SP; use serde::Serialize; use super::{DocTestBuilder, ScrapedDocTest}; @@ -35,7 +36,7 @@ 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, @@ -44,6 +45,8 @@ impl ExtractedDocTests { false, None, Some(&langstr), + None, + DUMMY_SP, ); let (full_test_code, size) = doctest.generate_unique_doctest( &text, diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs index d4fbfb12582e..759b139e2887 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::source_map::SourceMap; use rustc_span::symbol::sym; -use rustc_span::{FileName, kw}; +use rustc_span::{FileName, Span, kw}; use tracing::debug; use super::GlobalTestOptions; @@ -61,6 +61,8 @@ impl DocTestBuilder { // If `test_id` is `None`, it means we're generating code for a code example "run" link. test_id: Option, lang_str: Option<&LangString>, + dcx: Option>, + span: Span, ) -> Self { let can_merge_doctests = can_merge_doctests && lang_str.is_some_and(|lang_str| { @@ -69,7 +71,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) }) }); @@ -289,7 +291,12 @@ fn reset_error_count(psess: &ParseSess) { const DOCTEST_CODE_WRAPPER: &str = "fn f(){"; -fn parse_source(source: &str, crate_name: &Option<&str>) -> Result { +fn parse_source( + source: &str, + crate_name: &Option<&str>, + parent_dcx: Option>, + span: Span, +) -> Result { use rustc_errors::DiagCtxt; use rustc_errors::emitter::{Emitter, HumanEmitter}; use rustc_span::source_map::FilePathMapping; @@ -466,8 +473,17 @@ fn parse_source(source: &str, crate_name: &Option<&str>) -> Result 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 43dcfab880b5..f9d2aa3d3b4b 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 49add73e9d64..ce27a20540e4 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use rustc_span::DUMMY_SP; use rustc_span::edition::DEFAULT_EDITION; use super::{DocTestBuilder, GlobalTestOptions}; @@ -18,6 +19,8 @@ fn make_test( false, test_id.map(|s| s.to_string()), None, + None, + DUMMY_SP, ); let (code, line_offset) = doctest.generate_unique_doctest(test_code, dont_insert_main, opts, crate_name); diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index fc46293e7eaa..5014a5198c8e 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -45,7 +45,7 @@ use rustc_middle::ty::TyCtxt; pub(crate) use rustc_resolve::rustdoc::main_body_opts; use rustc_resolve::rustdoc::may_be_doc_link; use rustc_span::edition::Edition; -use rustc_span::{Span, Symbol}; +use rustc_span::{DUMMY_SP, Span, Symbol}; use tracing::{debug, trace}; use crate::clean::RenderedLink; @@ -303,7 +303,9 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { attrs: vec![], args_file: PathBuf::new(), }; - let doctest = doctest::DocTestBuilder::new(&test, krate, edition, false, None, None); + let doctest = doctest::DocTestBuilder::new( + &test, krate, edition, false, None, None, None, DUMMY_SP, + ); let (test, _) = doctest.generate_unique_doctest(&test, false, &opts, krate); let channel = if test.contains("#![feature(") { "&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 000000000000..113fb7ccb60e --- /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 000000000000..d90a289ca698 --- /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 000000000000..0dc7c2a2eea9 --- /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 +