Skip to content

Commit 81c015e

Browse files
committed
rustdoc: Slightly refactor code pertaining to doctest parsing
1 parent 321d156 commit 81c015e

File tree

1 file changed

+129
-160
lines changed

1 file changed

+129
-160
lines changed

src/librustdoc/doctest/make.rs

Lines changed: 129 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use rustc_ast::token::{Delimiter, TokenKind};
88
use rustc_ast::tokenstream::TokenTree;
99
use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind};
1010
use rustc_errors::emitter::SilentEmitter;
11-
use rustc_errors::{DiagCtxt, DiagCtxtHandle};
11+
use rustc_errors::{Diag, DiagCtxt, DiagCtxtHandle};
1212
use rustc_parse::lexer::StripTokens;
1313
use rustc_parse::new_parser_from_source_str;
1414
use rustc_session::parse::ParseSess;
@@ -139,16 +139,21 @@ impl<'a> BuildDocTestBuilder<'a> {
139139
maybe_crate_attrs,
140140
})) = result
141141
else {
142-
// If the AST returned an error, we don't want this doctest to be merged with the
143-
// others.
144-
return DocTestBuilder::invalid(
145-
Vec::new(),
146-
String::new(),
147-
String::new(),
148-
String::new(),
149-
source.to_string(),
142+
// If we failed to parse the doctest, create a dummy doctest.
143+
return DocTestBuilder {
144+
supports_color: false,
145+
has_main_fn: false,
146+
global_crate_attrs: Vec::new(),
147+
crate_attrs: String::new(),
148+
maybe_crate_attrs: String::new(),
149+
crates: String::new(),
150+
everything_else: source.to_string(),
151+
already_has_extern_crate: false,
150152
test_id,
151-
);
153+
failed_to_parse: true,
154+
// We certainly don't want to merge such a doctest with others.
155+
can_be_merged: false,
156+
};
152157
};
153158

154159
debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}");
@@ -172,7 +177,7 @@ impl<'a> BuildDocTestBuilder<'a> {
172177
everything_else,
173178
already_has_extern_crate,
174179
test_id,
175-
invalid_ast: false,
180+
failed_to_parse: false,
176181
can_be_merged,
177182
}
178183
}
@@ -192,7 +197,7 @@ pub(crate) struct DocTestBuilder {
192197
pub(crate) crates: String,
193198
pub(crate) everything_else: String,
194199
pub(crate) test_id: Option<String>,
195-
pub(crate) invalid_ast: bool,
200+
pub(crate) failed_to_parse: bool,
196201
pub(crate) can_be_merged: bool,
197202
}
198203

@@ -271,29 +276,6 @@ impl std::string::ToString for DocTestWrapResult {
271276
}
272277

273278
impl DocTestBuilder {
274-
fn invalid(
275-
global_crate_attrs: Vec<String>,
276-
crate_attrs: String,
277-
maybe_crate_attrs: String,
278-
crates: String,
279-
everything_else: String,
280-
test_id: Option<String>,
281-
) -> Self {
282-
Self {
283-
supports_color: false,
284-
has_main_fn: false,
285-
global_crate_attrs,
286-
crate_attrs,
287-
maybe_crate_attrs,
288-
crates,
289-
everything_else,
290-
already_has_extern_crate: false,
291-
test_id,
292-
invalid_ast: true,
293-
can_be_merged: false,
294-
}
295-
}
296-
297279
/// Transforms a test into code that can be compiled into a Rust binary, and returns the number of
298280
/// lines before the test code begins.
299281
pub(crate) fn generate_unique_doctest(
@@ -303,10 +285,10 @@ impl DocTestBuilder {
303285
opts: &GlobalTestOptions,
304286
crate_name: Option<&str>,
305287
) -> (DocTestWrapResult, usize) {
306-
if self.invalid_ast {
307-
// If the AST failed to compile, no need to go generate a complete doctest, the error
308-
// will be better this way.
309-
debug!("invalid AST:\n{test_code}");
288+
if self.failed_to_parse {
289+
// If we failed to parse the doctest, there's no need to go generate a complete doctest,
290+
// the diagnostics will be better this way.
291+
debug!("failed to parse doctest:\n{test_code}");
310292
return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0);
311293
}
312294
let mut line_offset = 0;
@@ -428,16 +410,6 @@ impl DocTestBuilder {
428410
}
429411
}
430412

431-
fn reset_error_count(psess: &ParseSess) {
432-
// Reset errors so that they won't be reported as compiler bugs when dropping the
433-
// dcx. Any errors in the tests will be reported when the test file is compiled,
434-
// Note that we still need to cancel the errors above otherwise `Diag` will panic on
435-
// drop.
436-
psess.dcx().reset_err_count();
437-
}
438-
439-
const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
440-
441413
fn parse_source(
442414
source: &str,
443415
crate_name: &Option<&str>,
@@ -447,15 +419,17 @@ fn parse_source(
447419
let mut info =
448420
ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };
449421

422+
const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
450423
let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");
451424

452425
// Any parse errors should also appear when the doctest is compiled for real,
453426
// so just suppress them here.
454427
let emitter = SilentEmitter { translator: rustc_driver::default_translator() };
455428

456-
// FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
457429
let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
458430
let psess = ParseSess::with_dcx(dcx, Arc::new(SourceMap::new(FilePathMapping::empty())));
431+
// Reset errors at the end so that we don't panic on delayed bugs.
432+
let _defer = rustc_data_structures::defer(|| psess.dcx().reset_err_count());
459433

460434
// Don't strip any tokens; it wouldn't matter anyway because the source is wrapped in a function.
461435
let mut parser = match new_parser_from_source_str(
@@ -467,7 +441,6 @@ fn parse_source(
467441
Ok(p) => p,
468442
Err(errs) => {
469443
errs.into_iter().for_each(|err| err.cancel());
470-
reset_error_count(&psess);
471444
return Err(());
472445
}
473446
};
@@ -516,124 +489,120 @@ fn parse_source(
516489
is_extern_crate
517490
}
518491

519-
let mut prev_span_hi = 0;
520492
let not_crate_attrs = &[sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect];
521-
let parsed = parser.parse_item(rustc_parse::parser::ForceCollect::No);
522-
523-
let result = match parsed {
524-
Ok(Some(ref item))
525-
if let ast::ItemKind::Fn(ref fn_item) = item.kind
526-
&& let Some(ref body) = fn_item.body
527-
// The parser might've recovered from some syntax errors and thus returned `Ok(_)`.
528-
// However, since we've suppressed diagnostic emission above, we must ensure that
529-
// we mark the doctest as syntactically invalid. Otherwise, we can end up generating
530-
// a runnable doctest that's free of any errors even though the source was malformed.
531-
&& psess.dcx().has_errors().is_none() =>
532-
{
533-
for attr in &item.attrs {
534-
if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {
535-
// There is one exception to these attributes:
536-
// `#![allow(internal_features)]`. If this attribute is used, we need to
537-
// consider it only as a crate-level attribute.
538-
if attr.has_name(sym::allow)
539-
&& let Some(list) = attr.meta_item_list()
540-
&& list.iter().any(|sub_attr| sub_attr.has_name(sym::internal_features))
541-
{
542-
push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
543-
} else {
544-
push_to_s(
545-
&mut info.maybe_crate_attrs,
546-
source,
547-
attr.span,
548-
&mut prev_span_hi,
549-
);
550-
}
551-
} else {
552-
push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
553-
}
493+
let mut prev_span_hi = 0;
494+
495+
let item = parser.parse_item(rustc_parse::parser::ForceCollect::No).map_err(Diag::cancel)?;
496+
497+
// The parser might've recovered from some syntax errors and thus returned `Ok(_)`.
498+
// However, since we've suppressed diagnostic emission above, we must ensure that
499+
// we mark the doctest as syntactically invalid. Otherwise, we can end up generating
500+
// a runnable doctest that's free of any errors even though the source was malformed.
501+
if psess.dcx().has_errors().is_some() {
502+
return Err(());
503+
}
504+
505+
let Some(box ast::Item {
506+
attrs,
507+
kind: ast::ItemKind::Fn(box ast::Fn { body: Some(body), .. }),
508+
..
509+
}) = item
510+
else {
511+
// We know that this *has* to be a function with a body
512+
// because we've wrapped the whole source in one above.
513+
unreachable!()
514+
};
515+
516+
for attr in attrs {
517+
if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {
518+
// There is one exception to these attributes:
519+
// `#![allow(internal_features)]`. If this attribute is used, we need to
520+
// consider it only as a crate-level attribute.
521+
if attr.has_name(sym::allow)
522+
&& let Some(list) = attr.meta_item_list()
523+
&& list.iter().any(|sub_attr| sub_attr.has_name(sym::internal_features))
524+
{
525+
push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
526+
} else {
527+
push_to_s(&mut info.maybe_crate_attrs, source, attr.span, &mut prev_span_hi);
554528
}
555-
let mut has_non_items = false;
556-
for stmt in &body.stmts {
557-
let mut is_extern_crate = false;
558-
match stmt.kind {
559-
StmtKind::Item(ref item) => {
560-
is_extern_crate = check_item(item, &mut info, crate_name);
561-
}
562-
// We assume that the macro calls will expand to item(s) even though they could
563-
// expand to statements and expressions.
564-
StmtKind::MacCall(ref mac_call) => {
565-
if !info.has_main_fn {
566-
// For backward compatibility, we look for the token sequence `fn main(…)`
567-
// in the macro input (!) to crudely detect main functions "masked by a
568-
// wrapper macro". For the record, this is a horrible heuristic!
569-
// See <https://github.com/rust-lang/rust/issues/56898>.
570-
let mut iter = mac_call.mac.args.tokens.iter();
571-
while let Some(token) = iter.next() {
572-
if let TokenTree::Token(token, _) = token
573-
&& let TokenKind::Ident(kw::Fn, _) = token.kind
574-
&& let Some(TokenTree::Token(ident, _)) = iter.peek()
575-
&& let TokenKind::Ident(sym::main, _) = ident.kind
576-
&& let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = {
577-
iter.next();
578-
iter.peek()
579-
}
580-
{
581-
info.has_main_fn = true;
582-
break;
583-
}
529+
} else {
530+
push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
531+
}
532+
}
533+
534+
let mut has_non_items = false;
535+
for stmt in &body.stmts {
536+
let mut is_extern_crate = false;
537+
match stmt.kind {
538+
StmtKind::Item(ref item) => {
539+
is_extern_crate = check_item(item, &mut info, crate_name);
540+
}
541+
// We assume that the macro calls will expand to item(s) even though they could
542+
// expand to statements and expressions.
543+
StmtKind::MacCall(ref mac_call) => {
544+
if !info.has_main_fn {
545+
// For backward compatibility, we look for the token sequence `fn main(…)`
546+
// in the macro input (!) to crudely detect main functions "masked by a
547+
// wrapper macro". For the record, this is a horrible heuristic!
548+
// See <https://github.com/rust-lang/rust/issues/56898>.
549+
let mut iter = mac_call.mac.args.tokens.iter();
550+
while let Some(token) = iter.next() {
551+
if let TokenTree::Token(token, _) = token
552+
&& let TokenKind::Ident(kw::Fn, _) = token.kind
553+
&& let Some(TokenTree::Token(ident, _)) = iter.peek()
554+
&& let TokenKind::Ident(sym::main, _) = ident.kind
555+
&& let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = {
556+
iter.next();
557+
iter.peek()
584558
}
559+
{
560+
info.has_main_fn = true;
561+
break;
585562
}
586563
}
587-
StmtKind::Let(_) | StmtKind::Expr(_) | StmtKind::Semi(_) | StmtKind::Empty => {
588-
has_non_items = true
589-
}
590-
}
591-
592-
// Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to
593-
// tweak the span to include the attributes as well.
594-
let mut span = stmt.span;
595-
if let Some(attr) =
596-
stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer)
597-
{
598-
span = span.with_lo(attr.span.lo());
599-
}
600-
if info.everything_else.is_empty()
601-
&& (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty())
602-
{
603-
// To keep the doctest code "as close as possible" to the original, we insert
604-
// all the code located between this new span and the previous span which
605-
// might contain code comments and backlines.
606-
push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);
607-
}
608-
if !is_extern_crate {
609-
push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);
610-
} else {
611-
push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
612564
}
613565
}
614-
if has_non_items {
615-
if info.has_main_fn
616-
&& let Some(dcx) = parent_dcx
617-
&& !span.is_dummy()
618-
{
619-
dcx.span_warn(
620-
span,
621-
"the `main` function of this doctest won't be run as it contains \
622-
expressions at the top level, meaning that the whole doctest code will be \
623-
wrapped in a function",
624-
);
625-
}
626-
info.has_main_fn = false;
566+
StmtKind::Let(_) | StmtKind::Expr(_) | StmtKind::Semi(_) | StmtKind::Empty => {
567+
has_non_items = true
627568
}
628-
Ok(info)
629569
}
630-
Err(e) => {
631-
e.cancel();
632-
Err(())
570+
571+
// Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to
572+
// tweak the span to include the attributes as well.
573+
let mut span = stmt.span;
574+
if let Some(attr) = stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer) {
575+
span = span.with_lo(attr.span.lo());
633576
}
634-
_ => Err(()),
635-
};
577+
if info.everything_else.is_empty()
578+
&& (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty())
579+
{
580+
// To keep the doctest code "as close as possible" to the original, we insert
581+
// all the code located between this new span and the previous span which
582+
// might contain code comments and backlines.
583+
push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);
584+
}
585+
if !is_extern_crate {
586+
push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);
587+
} else {
588+
push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
589+
}
590+
}
591+
592+
if has_non_items {
593+
if info.has_main_fn
594+
&& let Some(dcx) = parent_dcx
595+
&& !span.is_dummy()
596+
{
597+
dcx.span_warn(
598+
span,
599+
"the `main` function of this doctest won't be run as it contains \
600+
expressions at the top level, meaning that the whole doctest code will be \
601+
wrapped in a function",
602+
);
603+
}
604+
info.has_main_fn = false;
605+
}
636606

637-
reset_error_count(&psess);
638-
result
607+
Ok(info)
639608
}

0 commit comments

Comments
 (0)