Skip to content

Commit bfb4fcf

Browse files
committed
rustdoc: Slightly refactor code pertaining to doctest parsing
1 parent a830eef commit bfb4fcf

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;
@@ -136,16 +136,21 @@ impl<'a> BuildDocTestBuilder<'a> {
136136
maybe_crate_attrs,
137137
})) = result
138138
else {
139-
// If the AST returned an error, we don't want this doctest to be merged with the
140-
// others.
141-
return DocTestBuilder::invalid(
142-
Vec::new(),
143-
String::new(),
144-
String::new(),
145-
String::new(),
146-
source.to_string(),
139+
// If we failed to parse the doctest, create a dummy doctest.
140+
return DocTestBuilder {
141+
supports_color: false,
142+
has_main_fn: false,
143+
global_crate_attrs: Vec::new(),
144+
crate_attrs: String::new(),
145+
maybe_crate_attrs: String::new(),
146+
crates: String::new(),
147+
everything_else: source.to_string(),
148+
already_has_extern_crate: false,
147149
test_id,
148-
);
150+
failed_to_parse: true,
151+
// We certainly don't want to merge such a doctest with others.
152+
can_be_merged: false,
153+
};
149154
};
150155

151156
debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}");
@@ -182,7 +187,7 @@ impl<'a> BuildDocTestBuilder<'a> {
182187
everything_else,
183188
already_has_extern_crate,
184189
test_id,
185-
invalid_ast: false,
190+
failed_to_parse: false,
186191
can_be_merged,
187192
}
188193
}
@@ -202,7 +207,7 @@ pub(crate) struct DocTestBuilder {
202207
pub(crate) crates: String,
203208
pub(crate) everything_else: String,
204209
pub(crate) test_id: Option<String>,
205-
pub(crate) invalid_ast: bool,
210+
pub(crate) failed_to_parse: bool,
206211
pub(crate) can_be_merged: bool,
207212
}
208213

@@ -281,29 +286,6 @@ impl std::string::ToString for DocTestWrapResult {
281286
}
282287

283288
impl DocTestBuilder {
284-
fn invalid(
285-
global_crate_attrs: Vec<String>,
286-
crate_attrs: String,
287-
maybe_crate_attrs: String,
288-
crates: String,
289-
everything_else: String,
290-
test_id: Option<String>,
291-
) -> Self {
292-
Self {
293-
supports_color: false,
294-
has_main_fn: false,
295-
global_crate_attrs,
296-
crate_attrs,
297-
maybe_crate_attrs,
298-
crates,
299-
everything_else,
300-
already_has_extern_crate: false,
301-
test_id,
302-
invalid_ast: true,
303-
can_be_merged: false,
304-
}
305-
}
306-
307289
/// Transforms a test into code that can be compiled into a Rust binary, and returns the number of
308290
/// lines before the test code begins.
309291
pub(crate) fn generate_unique_doctest(
@@ -313,10 +295,10 @@ impl DocTestBuilder {
313295
opts: &GlobalTestOptions,
314296
crate_name: Option<&str>,
315297
) -> (DocTestWrapResult, usize) {
316-
if self.invalid_ast {
317-
// If the AST failed to compile, no need to go generate a complete doctest, the error
318-
// will be better this way.
319-
debug!("invalid AST:\n{test_code}");
298+
if self.failed_to_parse {
299+
// If we failed to parse the doctest, there's no need to go generate a complete doctest,
300+
// the diagnostics will be better this way.
301+
debug!("failed to parse doctest:\n{test_code}");
320302
return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0);
321303
}
322304
let mut line_offset = 0;
@@ -438,16 +420,6 @@ impl DocTestBuilder {
438420
}
439421
}
440422

441-
fn reset_error_count(psess: &ParseSess) {
442-
// Reset errors so that they won't be reported as compiler bugs when dropping the
443-
// dcx. Any errors in the tests will be reported when the test file is compiled,
444-
// Note that we still need to cancel the errors above otherwise `Diag` will panic on
445-
// drop.
446-
psess.dcx().reset_err_count();
447-
}
448-
449-
const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
450-
451423
fn parse_source(
452424
source: &str,
453425
crate_name: &Option<&str>,
@@ -457,15 +429,17 @@ fn parse_source(
457429
let mut info =
458430
ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };
459431

432+
const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
460433
let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");
461434

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

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

470444
// Don't strip any tokens; it wouldn't matter anyway because the source is wrapped in a function.
471445
let mut parser = match new_parser_from_source_str(
@@ -477,7 +451,6 @@ fn parse_source(
477451
Ok(p) => p,
478452
Err(errs) => {
479453
errs.into_iter().for_each(|err| err.cancel());
480-
reset_error_count(&psess);
481454
return Err(());
482455
}
483456
};
@@ -526,124 +499,120 @@ fn parse_source(
526499
is_extern_crate
527500
}
528501

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

647-
reset_error_count(&psess);
648-
result
617+
Ok(info)
649618
}

0 commit comments

Comments
 (0)