Skip to content

Commit 1d67944

Browse files
committed
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`
2 parents 2cd3783 + f4d41a5 commit 1d67944

13 files changed

+253
-73
lines changed

src/librustdoc/doctest.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
1212
use std::sync::{Arc, Mutex};
1313
use std::{panic, str};
1414

15-
pub(crate) use make::DocTestBuilder;
15+
pub(crate) use make::{BuildDocTestBuilder, DocTestBuilder};
1616
pub(crate) use markdown::test as test_markdown;
1717
use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet};
1818
use rustc_errors::emitter::HumanReadableErrorType;
@@ -23,9 +23,9 @@ use rustc_hir::def_id::LOCAL_CRATE;
2323
use rustc_interface::interface;
2424
use rustc_session::config::{self, CrateType, ErrorOutputType, Input};
2525
use rustc_session::lint;
26-
use rustc_span::FileName;
2726
use rustc_span::edition::Edition;
2827
use rustc_span::symbol::sym;
28+
use rustc_span::{FileName, Span};
2929
use rustc_target::spec::{Target, TargetTuple};
3030
use tempfile::{Builder as TempFileBuilder, TempDir};
3131
use tracing::debug;
@@ -197,7 +197,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
197197
}
198198
} else {
199199
let mut collector = CreateRunnableDocTests::new(options, opts);
200-
tests.into_iter().for_each(|t| collector.add_test(t));
200+
tests.into_iter().for_each(|t| collector.add_test(t, Some(compiler.sess.dcx())));
201201

202202
Ok(Some(collector))
203203
}
@@ -847,6 +847,7 @@ pub(crate) struct ScrapedDocTest {
847847
langstr: LangString,
848848
text: String,
849849
name: String,
850+
span: Span,
850851
}
851852

852853
impl ScrapedDocTest {
@@ -856,6 +857,7 @@ impl ScrapedDocTest {
856857
logical_path: Vec<String>,
857858
langstr: LangString,
858859
text: String,
860+
span: Span,
859861
) -> Self {
860862
let mut item_path = logical_path.join("::");
861863
item_path.retain(|c| c != ' ');
@@ -865,7 +867,7 @@ impl ScrapedDocTest {
865867
let name =
866868
format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
867869

868-
Self { filename, line, langstr, text, name }
870+
Self { filename, line, langstr, text, name, span }
869871
}
870872
fn edition(&self, opts: &RustdocOptions) -> Edition {
871873
self.langstr.edition.unwrap_or(opts.edition)
@@ -921,7 +923,7 @@ impl CreateRunnableDocTests {
921923
}
922924
}
923925

924-
fn add_test(&mut self, scraped_test: ScrapedDocTest) {
926+
fn add_test(&mut self, scraped_test: ScrapedDocTest, dcx: Option<DiagCtxtHandle<'_>>) {
925927
// For example `module/file.rs` would become `module_file_rs`
926928
let file = scraped_test
927929
.filename
@@ -945,14 +947,14 @@ impl CreateRunnableDocTests {
945947
);
946948

947949
let edition = scraped_test.edition(&self.rustdoc_options);
948-
let doctest = DocTestBuilder::new(
949-
&scraped_test.text,
950-
Some(&self.opts.crate_name),
951-
edition,
952-
self.can_merge_doctests,
953-
Some(test_id),
954-
Some(&scraped_test.langstr),
955-
);
950+
let doctest = BuildDocTestBuilder::new(&scraped_test.text)
951+
.crate_name(&self.opts.crate_name)
952+
.edition(edition)
953+
.can_merge_doctests(self.can_merge_doctests)
954+
.test_id(test_id)
955+
.lang_str(&scraped_test.langstr)
956+
.span(scraped_test.span)
957+
.build(dcx);
956958
let is_standalone = !doctest.can_be_merged
957959
|| scraped_test.langstr.compile_fail
958960
|| scraped_test.langstr.test_harness

src/librustdoc/doctest/extracted.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use serde::Serialize;
77

8-
use super::{DocTestBuilder, ScrapedDocTest};
8+
use super::{BuildDocTestBuilder, ScrapedDocTest};
99
use crate::config::Options as RustdocOptions;
1010
use crate::html::markdown;
1111

@@ -35,16 +35,13 @@ impl ExtractedDocTests {
3535
) {
3636
let edition = scraped_test.edition(options);
3737

38-
let ScrapedDocTest { filename, line, langstr, text, name } = scraped_test;
38+
let ScrapedDocTest { filename, line, langstr, text, name, .. } = scraped_test;
3939

40-
let doctest = DocTestBuilder::new(
41-
&text,
42-
Some(&opts.crate_name),
43-
edition,
44-
false,
45-
None,
46-
Some(&langstr),
47-
);
40+
let doctest = BuildDocTestBuilder::new(&text)
41+
.crate_name(&opts.crate_name)
42+
.edition(edition)
43+
.lang_str(&langstr)
44+
.build(None);
4845
let (full_test_code, size) = doctest.generate_unique_doctest(
4946
&text,
5047
langstr.test_harness,

src/librustdoc/doctest/make.rs

Lines changed: 112 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ use std::sync::Arc;
88
use rustc_ast::token::{Delimiter, TokenKind};
99
use rustc_ast::tokenstream::TokenTree;
1010
use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind};
11-
use rustc_errors::ColorConfig;
1211
use rustc_errors::emitter::stderr_destination;
12+
use rustc_errors::{ColorConfig, DiagCtxtHandle};
1313
use rustc_parse::new_parser_from_source_str;
1414
use rustc_session::parse::ParseSess;
15-
use rustc_span::edition::Edition;
15+
use rustc_span::edition::{DEFAULT_EDITION, Edition};
1616
use rustc_span::source_map::SourceMap;
1717
use rustc_span::symbol::sym;
18-
use rustc_span::{FileName, kw};
18+
use rustc_span::{DUMMY_SP, FileName, Span, kw};
1919
use tracing::debug;
2020

2121
use super::GlobalTestOptions;
@@ -35,41 +35,86 @@ struct ParseSourceInfo {
3535
maybe_crate_attrs: String,
3636
}
3737

38-
/// This struct contains information about the doctest itself which is then used to generate
39-
/// doctest source code appropriately.
40-
pub(crate) struct DocTestBuilder {
41-
pub(crate) supports_color: bool,
42-
pub(crate) already_has_extern_crate: bool,
43-
pub(crate) has_main_fn: bool,
44-
pub(crate) crate_attrs: String,
45-
/// If this is a merged doctest, it will be put into `everything_else`, otherwise it will
46-
/// put into `crate_attrs`.
47-
pub(crate) maybe_crate_attrs: String,
48-
pub(crate) crates: String,
49-
pub(crate) everything_else: String,
50-
pub(crate) test_id: Option<String>,
51-
pub(crate) invalid_ast: bool,
52-
pub(crate) can_be_merged: bool,
38+
/// Builder type for `DocTestBuilder`.
39+
pub(crate) struct BuildDocTestBuilder<'a> {
40+
source: &'a str,
41+
crate_name: Option<&'a str>,
42+
edition: Edition,
43+
can_merge_doctests: bool,
44+
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
45+
test_id: Option<String>,
46+
lang_str: Option<&'a LangString>,
47+
span: Span,
5348
}
5449

55-
impl DocTestBuilder {
56-
pub(crate) fn new(
57-
source: &str,
58-
crate_name: Option<&str>,
59-
edition: Edition,
60-
can_merge_doctests: bool,
61-
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
62-
test_id: Option<String>,
63-
lang_str: Option<&LangString>,
64-
) -> Self {
50+
impl<'a> BuildDocTestBuilder<'a> {
51+
pub(crate) fn new(source: &'a str) -> Self {
52+
Self {
53+
source,
54+
crate_name: None,
55+
edition: DEFAULT_EDITION,
56+
can_merge_doctests: false,
57+
test_id: None,
58+
lang_str: None,
59+
span: DUMMY_SP,
60+
}
61+
}
62+
63+
#[inline]
64+
pub(crate) fn crate_name(mut self, crate_name: &'a str) -> Self {
65+
self.crate_name = Some(crate_name);
66+
self
67+
}
68+
69+
#[inline]
70+
pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: bool) -> Self {
71+
self.can_merge_doctests = can_merge_doctests;
72+
self
73+
}
74+
75+
#[inline]
76+
pub(crate) fn test_id(mut self, test_id: String) -> Self {
77+
self.test_id = Some(test_id);
78+
self
79+
}
80+
81+
#[inline]
82+
pub(crate) fn lang_str(mut self, lang_str: &'a LangString) -> Self {
83+
self.lang_str = Some(lang_str);
84+
self
85+
}
86+
87+
#[inline]
88+
pub(crate) fn span(mut self, span: Span) -> Self {
89+
self.span = span;
90+
self
91+
}
92+
93+
#[inline]
94+
pub(crate) fn edition(mut self, edition: Edition) -> Self {
95+
self.edition = edition;
96+
self
97+
}
98+
99+
pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder {
100+
let BuildDocTestBuilder {
101+
source,
102+
crate_name,
103+
edition,
104+
can_merge_doctests,
105+
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
106+
test_id,
107+
lang_str,
108+
span,
109+
} = self;
65110
let can_merge_doctests = can_merge_doctests
66111
&& lang_str.is_some_and(|lang_str| {
67112
!lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
68113
});
69114

70115
let result = rustc_driver::catch_fatal_errors(|| {
71116
rustc_span::create_session_if_not_set_then(edition, |_| {
72-
parse_source(source, &crate_name)
117+
parse_source(source, &crate_name, dcx, span)
73118
})
74119
});
75120

@@ -87,7 +132,7 @@ impl DocTestBuilder {
87132
else {
88133
// If the AST returned an error, we don't want this doctest to be merged with the
89134
// others.
90-
return Self::invalid(
135+
return DocTestBuilder::invalid(
91136
String::new(),
92137
String::new(),
93138
String::new(),
@@ -107,7 +152,7 @@ impl DocTestBuilder {
107152
// If this is a merged doctest and a defined macro uses `$crate`, then the path will
108153
// not work, so better not put it into merged doctests.
109154
&& !(has_macro_def && everything_else.contains("$crate"));
110-
Self {
155+
DocTestBuilder {
111156
supports_color,
112157
has_main_fn,
113158
crate_attrs,
@@ -120,7 +165,26 @@ impl DocTestBuilder {
120165
can_be_merged,
121166
}
122167
}
168+
}
123169

170+
/// This struct contains information about the doctest itself which is then used to generate
171+
/// doctest source code appropriately.
172+
pub(crate) struct DocTestBuilder {
173+
pub(crate) supports_color: bool,
174+
pub(crate) already_has_extern_crate: bool,
175+
pub(crate) has_main_fn: bool,
176+
pub(crate) crate_attrs: String,
177+
/// If this is a merged doctest, it will be put into `everything_else`, otherwise it will
178+
/// put into `crate_attrs`.
179+
pub(crate) maybe_crate_attrs: String,
180+
pub(crate) crates: String,
181+
pub(crate) everything_else: String,
182+
pub(crate) test_id: Option<String>,
183+
pub(crate) invalid_ast: bool,
184+
pub(crate) can_be_merged: bool,
185+
}
186+
187+
impl DocTestBuilder {
124188
fn invalid(
125189
crate_attrs: String,
126190
maybe_crate_attrs: String,
@@ -289,7 +353,12 @@ fn reset_error_count(psess: &ParseSess) {
289353

290354
const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
291355

292-
fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceInfo, ()> {
356+
fn parse_source(
357+
source: &str,
358+
crate_name: &Option<&str>,
359+
parent_dcx: Option<DiagCtxtHandle<'_>>,
360+
span: Span,
361+
) -> Result<ParseSourceInfo, ()> {
293362
use rustc_errors::DiagCtxt;
294363
use rustc_errors::emitter::{Emitter, HumanEmitter};
295364
use rustc_span::source_map::FilePathMapping;
@@ -466,8 +535,17 @@ fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceIn
466535
}
467536
}
468537
if has_non_items {
469-
// FIXME: if `info.has_main_fn` is `true`, emit a warning here to mention that
470-
// this code will not be called.
538+
if info.has_main_fn
539+
&& let Some(dcx) = parent_dcx
540+
&& !span.is_dummy()
541+
{
542+
dcx.span_warn(
543+
span,
544+
"the `main` function of this doctest won't be run as it contains \
545+
expressions at the top level, meaning that the whole doctest code will be \
546+
wrapped in a function",
547+
);
548+
}
471549
info.has_main_fn = false;
472550
}
473551
Ok(info)

src/librustdoc/doctest/markdown.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fs::read_to_string;
44
use std::sync::{Arc, Mutex};
55

66
use rustc_session::config::Input;
7-
use rustc_span::FileName;
7+
use rustc_span::{DUMMY_SP, FileName};
88
use tempfile::tempdir;
99

1010
use super::{
@@ -24,7 +24,14 @@ impl DocTestVisitor for MdCollector {
2424
let filename = self.filename.clone();
2525
// First line of Markdown is line 1.
2626
let line = 1 + rel_line.offset();
27-
self.tests.push(ScrapedDocTest::new(filename, line, self.cur_path.clone(), config, test));
27+
self.tests.push(ScrapedDocTest::new(
28+
filename,
29+
line,
30+
self.cur_path.clone(),
31+
config,
32+
test,
33+
DUMMY_SP,
34+
));
2835
}
2936

3037
fn visit_header(&mut self, name: &str, level: u32) {
@@ -107,7 +114,7 @@ pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> {
107114
find_testable_code(&input_str, &mut md_collector, codes, None);
108115

109116
let mut collector = CreateRunnableDocTests::new(options.clone(), opts);
110-
md_collector.tests.into_iter().for_each(|t| collector.add_test(t));
117+
md_collector.tests.into_iter().for_each(|t| collector.add_test(t, None));
111118
let CreateRunnableDocTests { opts, rustdoc_options, standalone_tests, mergeable_tests, .. } =
112119
collector;
113120
crate::doctest::run_tests(

0 commit comments

Comments
 (0)