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

Lines changed: 253 additions & 73 deletions

‎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::{BuilderasTempFileBuilder,TempDir};
3131
use tracing::debug;
@@ -197,7 +197,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
197197
}
198198
}else{
199199
letmut 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
implScrapedDocTest{
@@ -856,6 +857,7 @@ impl ScrapedDocTest {
856857
logical_path:Vec<String>,
857858
langstr:LangString,
858859
text:String,
860+
span:Span,
859861
) -> Self{
860862
letmut 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
fnedition(&self,opts:&RustdocOptions) -> Edition{
871873
self.langstr.edition.unwrap_or(opts.edition)
@@ -921,7 +923,7 @@ impl CreateRunnableDocTests {
921923
}
922924
}
923925

924-
fnadd_test(&mutself,scraped_test:ScrapedDocTest){
926+
fnadd_test(&mutself,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-
usesuper::{DocTestBuilder,ScrapedDocTest};
8+
usesuper::{BuildDocTestBuilder,ScrapedDocTest};
99
usecrate::config::OptionsasRustdocOptions;
1010
usecrate::html::markdown;
1111

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

38-
letScrapedDocTest{ filename, line, langstr, text, name } = scraped_test;
38+
letScrapedDocTest{ 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::{selfas 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
usesuper::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)structDocTestBuilder{
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)structBuildDocTestBuilder<'a>{
40+
source:&'astr,
41+
crate_name:Option<&'astr>,
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<&'aLangString>,
47+
span:Span,
5348
}
5449

55-
implDocTestBuilder{
56-
pub(crate)fnnew(
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)fnnew(source:&'astr) -> 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)fncrate_name(mutself,crate_name:&'astr) -> Self{
65+
self.crate_name = Some(crate_name);
66+
self
67+
}
68+
69+
#[inline]
70+
pub(crate)fncan_merge_doctests(mutself,can_merge_doctests:bool) -> Self{
71+
self.can_merge_doctests = can_merge_doctests;
72+
self
73+
}
74+
75+
#[inline]
76+
pub(crate)fntest_id(mutself,test_id:String) -> Self{
77+
self.test_id = Some(test_id);
78+
self
79+
}
80+
81+
#[inline]
82+
pub(crate)fnlang_str(mutself,lang_str:&'aLangString) -> Self{
83+
self.lang_str = Some(lang_str);
84+
self
85+
}
86+
87+
#[inline]
88+
pub(crate)fnspan(mutself,span:Span) -> Self{
89+
self.span = span;
90+
self
91+
}
92+
93+
#[inline]
94+
pub(crate)fnedition(mutself,edition:Edition) -> Self{
95+
self.edition = edition;
96+
self
97+
}
98+
99+
pub(crate)fnbuild(self,dcx:Option<DiagCtxtHandle<'_>>) -> DocTestBuilder{
100+
letBuildDocTestBuilder{
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-
returnSelf::invalid(
135+
returnDocTestBuilder::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)structDocTestBuilder{
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+
implDocTestBuilder{
124188
fninvalid(
125189
crate_attrs:String,
126190
maybe_crate_attrs:String,
@@ -289,7 +353,12 @@ fn reset_error_count(psess: &ParseSess) {
289353

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

292-
fnparse_source(source:&str,crate_name:&Option<&str>) -> Result<ParseSourceInfo,()>{
356+
fnparse_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+
&& letSome(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
usesuper::{
@@ -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
fnvisit_header(&mutself,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
letmut 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
letCreateRunnableDocTests{ opts, rustdoc_options, standalone_tests, mergeable_tests, .. } =
112119
collector;
113120
crate::doctest::run_tests(

0 commit comments

Comments
 (0)