Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit a0ae8ac

Browse files
If there is any AST error with a doctest, we make it a standalone test
To do so, AST error detection was improved in order to not filter out too many doctests.
1 parent b7079c5 commit a0ae8ac

File tree

2 files changed

+183
-88
lines changed

2 files changed

+183
-88
lines changed

src/librustdoc/doctest.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,7 @@ impl CreateRunnableDoctests {
800800
let doctest =
801801
DocTest::new(&scraped_test.text, Some(&self.opts.crate_name), edition, Some(test_id));
802802
let is_standalone = !self.can_merge_doctests
803+
|| doctest.failed_ast
803804
|| scraped_test.langstr.compile_fail
804805
|| scraped_test.langstr.test_harness
805806
|| scraped_test.langstr.standalone

src/librustdoc/doctest/make.rs

Lines changed: 182 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub(crate) struct DocTest {
2525
pub(crate) crates: String,
2626
pub(crate) everything_else: String,
2727
pub(crate) test_id: Option<String>,
28+
pub(crate) failed_ast: bool,
2829
}
2930

3031
impl DocTest {
@@ -40,8 +41,15 @@ impl DocTest {
4041

4142
// Uses librustc_ast to parse the doctest and find if there's a main fn and the extern
4243
// crate already is included.
43-
let Ok((main_fn_span, already_has_extern_crate)) =
44-
check_for_main_and_extern_crate(crate_name, source, edition, &mut supports_color)
44+
let Ok((main_fn_span, already_has_extern_crate, failed_ast)) =
45+
check_for_main_and_extern_crate(
46+
crate_name,
47+
source,
48+
&everything_else,
49+
&crates,
50+
edition,
51+
&mut supports_color,
52+
)
4553
else {
4654
// If the parser panicked due to a fatal error, pass the test code through unchanged.
4755
// The error will be reported during compilation.
@@ -53,6 +61,7 @@ impl DocTest {
5361
everything_else,
5462
already_has_extern_crate: false,
5563
test_id,
64+
failed_ast: true,
5665
};
5766
};
5867
Self {
@@ -63,6 +72,7 @@ impl DocTest {
6372
everything_else,
6473
already_has_extern_crate,
6574
test_id,
75+
failed_ast,
6676
}
6777
}
6878

@@ -179,111 +189,195 @@ impl DocTest {
179189
}
180190
}
181191

182-
fn check_for_main_and_extern_crate(
183-
crate_name: Option<&str>,
184-
source: &str,
185-
edition: Edition,
186-
supports_color: &mut bool,
187-
) -> Result<(Option<Span>, bool), FatalError> {
188-
let result = rustc_driver::catch_fatal_errors(|| {
189-
rustc_span::create_session_if_not_set_then(edition, |_| {
190-
use rustc_errors::emitter::{Emitter, HumanEmitter};
191-
use rustc_errors::DiagCtxt;
192-
use rustc_parse::parser::ForceCollect;
193-
use rustc_span::source_map::FilePathMapping;
194-
195-
let filename = FileName::anon_source_code(source);
192+
#[derive(PartialEq, Eq, Debug)]
193+
enum ParsingResult {
194+
Failed,
195+
AstError,
196+
Ok,
197+
}
196198

197-
// Any errors in parsing should also appear when the doctest is compiled for real, so just
198-
// send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
199-
let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
200-
let fallback_bundle = rustc_errors::fallback_fluent_bundle(
201-
rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
202-
false,
203-
);
204-
*supports_color =
205-
HumanEmitter::new(stderr_destination(ColorConfig::Auto), fallback_bundle.clone())
206-
.supports_color();
199+
fn cancel_error_count(psess: &ParseSess) {
200+
// Reset errors so that they won't be reported as compiler bugs when dropping the
201+
// dcx. Any errors in the tests will be reported when the test file is compiled,
202+
// Note that we still need to cancel the errors above otherwise `Diag` will panic on
203+
// drop.
204+
psess.dcx().reset_err_count();
205+
}
207206

208-
let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle);
207+
fn parse_source(
208+
source: String,
209+
found_main_span: &mut Option<Span>,
210+
found_extern_crate: &mut bool,
211+
found_macro: &mut bool,
212+
crate_name: &Option<&str>,
213+
supports_color: &mut bool,
214+
) -> ParsingResult {
215+
use rustc_errors::emitter::{Emitter, HumanEmitter};
216+
use rustc_errors::DiagCtxt;
217+
use rustc_parse::parser::ForceCollect;
218+
use rustc_span::source_map::FilePathMapping;
219+
220+
let filename = FileName::anon_source_code(&source);
221+
222+
// Any errors in parsing should also appear when the doctest is compiled for real, so just
223+
// send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
224+
let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
225+
let fallback_bundle = rustc_errors::fallback_fluent_bundle(
226+
rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
227+
false,
228+
);
229+
*supports_color =
230+
HumanEmitter::new(stderr_destination(ColorConfig::Auto), fallback_bundle.clone())
231+
.supports_color();
232+
233+
let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle);
234+
235+
// FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
236+
let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
237+
let psess = ParseSess::with_dcx(dcx, sm);
238+
239+
let mut parser = match new_parser_from_source_str(&psess, filename, source) {
240+
Ok(p) => p,
241+
Err(errs) => {
242+
errs.into_iter().for_each(|err| err.cancel());
243+
cancel_error_count(&psess);
244+
return ParsingResult::Failed;
245+
}
246+
};
247+
let mut parsing_result = ParsingResult::Ok;
248+
249+
// Recurse through functions body. It is necessary because the doctest source code is
250+
// wrapped in a function to limit the number of AST errors. If we don't recurse into
251+
// functions, we would thing all top-level items (so basically nothing).
252+
fn check_item(
253+
item: &ast::Item,
254+
found_main_span: &mut Option<Span>,
255+
found_extern_crate: &mut bool,
256+
found_macro: &mut bool,
257+
crate_name: &Option<&str>,
258+
) {
259+
match item.kind {
260+
ast::ItemKind::Fn(ref fn_item) if found_main_span.is_none() => {
261+
if item.ident.name == sym::main {
262+
*found_main_span = Some(item.span);
263+
}
264+
if let Some(ref body) = fn_item.body {
265+
for stmt in &body.stmts {
266+
match stmt.kind {
267+
ast::StmtKind::Item(ref item) => check_item(
268+
item,
269+
found_main_span,
270+
found_extern_crate,
271+
found_macro,
272+
crate_name,
273+
),
274+
ast::StmtKind::MacCall(..) => *found_macro = true,
275+
_ => {}
276+
}
277+
}
278+
}
279+
}
280+
ast::ItemKind::ExternCrate(original) => {
281+
if !*found_extern_crate && let Some(ref crate_name) = crate_name {
282+
*found_extern_crate = match original {
283+
Some(name) => name.as_str() == *crate_name,
284+
None => item.ident.as_str() == *crate_name,
285+
};
286+
}
287+
}
288+
ast::ItemKind::MacCall(..) => *found_macro = true,
289+
_ => {}
290+
}
291+
}
209292

210-
// FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
211-
let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
212-
let psess = ParseSess::with_dcx(dcx, sm);
293+
loop {
294+
match parser.parse_item(ForceCollect::No) {
295+
Ok(Some(item)) => {
296+
check_item(&item, found_main_span, found_extern_crate, found_macro, crate_name);
213297

214-
let mut found_main = None;
215-
let mut found_extern_crate = crate_name.is_none();
216-
let mut found_macro = false;
217-
218-
let mut parser = match new_parser_from_source_str(&psess, filename, source.to_owned()) {
219-
Ok(p) => p,
220-
Err(errs) => {
221-
errs.into_iter().for_each(|err| err.cancel());
222-
return (found_main, found_extern_crate, found_macro);
298+
if found_main_span.is_some() && *found_extern_crate {
299+
break;
223300
}
224-
};
225-
226-
loop {
227-
match parser.parse_item(ForceCollect::No) {
228-
Ok(Some(item)) => {
229-
if found_main.is_none()
230-
&& let ast::ItemKind::Fn(..) = item.kind
231-
&& item.ident.name == sym::main
232-
{
233-
found_main = Some(item.span);
234-
}
301+
}
302+
Ok(None) => break,
303+
Err(e) => {
304+
parsing_result = ParsingResult::AstError;
305+
e.cancel();
306+
break;
307+
}
308+
}
235309

236-
if !found_extern_crate
237-
&& let ast::ItemKind::ExternCrate(original) = item.kind
238-
{
239-
// This code will never be reached if `crate_name` is none because
240-
// `found_extern_crate` is initialized to `true` if it is none.
241-
let crate_name = crate_name.unwrap();
242-
243-
match original {
244-
Some(name) => found_extern_crate = name.as_str() == crate_name,
245-
None => found_extern_crate = item.ident.as_str() == crate_name,
246-
}
247-
}
310+
// The supplied slice is only used for diagnostics,
311+
// which are swallowed here anyway.
312+
parser.maybe_consume_incorrect_semicolon(None);
313+
}
248314

249-
if !found_macro && let ast::ItemKind::MacCall(..) = item.kind {
250-
found_macro = true;
251-
}
315+
cancel_error_count(&psess);
316+
parsing_result
317+
}
252318

253-
if found_main.is_some() && found_extern_crate {
254-
break;
255-
}
256-
}
257-
Ok(None) => break,
258-
Err(e) => {
259-
e.cancel();
260-
break;
261-
}
262-
}
319+
fn check_for_main_and_extern_crate(
320+
crate_name: Option<&str>,
321+
original_source_code: &str,
322+
everything_else: &str,
323+
crates: &str,
324+
edition: Edition,
325+
supports_color: &mut bool,
326+
) -> Result<(Option<Span>, bool, bool), FatalError> {
327+
let result = rustc_driver::catch_fatal_errors(|| {
328+
rustc_span::create_session_if_not_set_then(edition, |_| {
329+
let mut found_main_span = None;
330+
let mut found_extern_crate = crate_name.is_none();
331+
let mut found_macro = false;
263332

264-
// The supplied item is only used for diagnostics,
265-
// which are swallowed here anyway.
266-
parser.maybe_consume_incorrect_semicolon(None);
333+
let mut parsing_result = parse_source(
334+
format!("{crates}{everything_else}"),
335+
&mut found_main_span,
336+
&mut found_extern_crate,
337+
&mut found_macro,
338+
&crate_name,
339+
supports_color,
340+
);
341+
// No need to double-check this if the "merged doctests" feature isn't enabled (so
342+
// before the 2024 edition).
343+
if edition >= Edition::Edition2024 && parsing_result != ParsingResult::Ok {
344+
// If we found an AST error, we want to ensure it's because of an expression being
345+
// used outside of a function.
346+
//
347+
// To do so, we wrap in a function in order to make sure that the doctest AST is
348+
// correct. For example, if your doctest is `foo::bar()`, if we don't wrap it in a
349+
// block, it would emit an AST error, which would be problematic for us since we
350+
// want to filter out such errors which aren't "real" errors.
351+
//
352+
// The end goal is to be able to merge as many doctests as possible as one for much
353+
// faster doctests run time.
354+
parsing_result = parse_source(
355+
format!("{crates}\nfn __doctest_wrap(){{{everything_else}\n}}"),
356+
&mut found_main_span,
357+
&mut found_extern_crate,
358+
&mut found_macro,
359+
&crate_name,
360+
supports_color,
361+
);
267362
}
268363

269-
// Reset errors so that they won't be reported as compiler bugs when dropping the
270-
// dcx. Any errors in the tests will be reported when the test file is compiled,
271-
// Note that we still need to cancel the errors above otherwise `Diag` will panic on
272-
// drop.
273-
psess.dcx().reset_err_count();
274-
275-
(found_main, found_extern_crate, found_macro)
364+
(found_main_span, found_extern_crate, found_macro, parsing_result)
276365
})
277366
});
278-
let (mut main_fn_span, already_has_extern_crate, found_macro) = result?;
367+
let (mut main_fn_span, already_has_extern_crate, found_macro, parsing_result) = match result {
368+
Err(..) | Ok((_, _, _, ParsingResult::Failed)) => return Err(FatalError),
369+
Ok((main_fn_span, already_has_extern_crate, found_macro, parsing_result)) => {
370+
(main_fn_span, already_has_extern_crate, found_macro, parsing_result)
371+
}
372+
};
279373

280374
// If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't
281375
// see it. In that case, run the old text-based scan to see if they at least have a main
282376
// function written inside a macro invocation. See
283377
// https://github.com/rust-lang/rust/issues/56898
284378
if found_macro
285379
&& main_fn_span.is_none()
286-
&& source
380+
&& original_source_code
287381
.lines()
288382
.map(|line| {
289383
let comment = line.find("//");
@@ -294,7 +388,7 @@ fn check_for_main_and_extern_crate(
294388
main_fn_span = Some(DUMMY_SP);
295389
}
296390

297-
Ok((main_fn_span, already_has_extern_crate))
391+
Ok((main_fn_span, already_has_extern_crate, parsing_result != ParsingResult::Ok))
298392
}
299393

300394
fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool {

0 commit comments

Comments
 (0)