Skip to content

Commit

Permalink
rustdoc: Avoid panic when parsing codeblocks for playground links
Browse files Browse the repository at this point in the history
`make_test` is also called when parsing codeblocks for the playground links so it should handle unwinds from the parser internally.
  • Loading branch information
ollie27 committed Jan 4, 2020
1 parent cd8377d commit efb876f
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 76 deletions.
148 changes: 72 additions & 76 deletions src/librustdoc/test.rs
Expand Up @@ -202,17 +202,7 @@ fn run_test(
opts: &TestOptions,
edition: Edition,
) -> Result<(), TestFailure> {
let (test, line_offset) = match panic::catch_unwind(|| {
make_test(test, Some(cratename), as_test_harness, opts, edition)
}) {
Ok((test, line_offset)) => (test, line_offset),
Err(cause) if cause.is::<errors::FatalErrorMarker>() => {
// If the parser used by `make_test` panicked due to a fatal error, pass the test code
// through unchanged. The error will be reported during compilation.
(test.to_owned(), 0)
}
Err(cause) => panic::resume_unwind(cause),
};
let (test, line_offset) = make_test(test, Some(cratename), as_test_harness, opts, edition);

// FIXME(#44940): if doctests ever support path remapping, then this filename
// needs to be the result of `SourceMap::span_to_unmapped_path`.
Expand Down Expand Up @@ -362,11 +352,6 @@ fn run_test(

/// Transforms a test into code that can be compiled into a Rust binary, and returns the number of
/// lines before the test code begins.
///
/// # Panics
///
/// This function uses the compiler's parser internally. The parser will panic if it encounters a
/// fatal error while parsing the test.
pub fn make_test(
s: &str,
cratename: Option<&str>,
Expand Down Expand Up @@ -401,83 +386,94 @@ pub fn make_test(

// Uses libsyntax to parse the doctest and find if there's a main fn and the extern
// crate already is included.
let (already_has_main, already_has_extern_crate, found_macro) = with_globals(edition, || {
use errors::emitter::EmitterWriter;
use errors::Handler;
use rustc_parse::maybe_new_parser_from_source_str;
use rustc_span::source_map::FilePathMapping;
use syntax::sess::ParseSess;

let filename = FileName::anon_source_code(s);
let source = crates + &everything_else;

// Any errors in parsing should also appear when the doctest is compiled for real, so just
// send all the errors that libsyntax emits directly into a `Sink` instead of stderr.
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let emitter = EmitterWriter::new(box io::sink(), None, false, false, false, None, false);
// FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
let handler = Handler::with_emitter(false, None, box emitter);
let sess = ParseSess::with_span_handler(handler, cm);

let mut found_main = false;
let mut found_extern_crate = cratename.is_none();
let mut found_macro = false;

let mut parser = match maybe_new_parser_from_source_str(&sess, filename, source) {
Ok(p) => p,
Err(errs) => {
for mut err in errs {
err.cancel();
let result = rustc_driver::catch_fatal_errors(|| {
with_globals(edition, || {
use errors::emitter::EmitterWriter;
use errors::Handler;
use rustc_parse::maybe_new_parser_from_source_str;
use rustc_span::source_map::FilePathMapping;
use syntax::sess::ParseSess;

let filename = FileName::anon_source_code(s);
let source = crates + &everything_else;

// Any errors in parsing should also appear when the doctest is compiled for real, so just
// send all the errors that libsyntax emits directly into a `Sink` instead of stderr.
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let emitter =
EmitterWriter::new(box io::sink(), None, false, false, false, None, false);
// FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
let handler = Handler::with_emitter(false, None, box emitter);
let sess = ParseSess::with_span_handler(handler, cm);

let mut found_main = false;
let mut found_extern_crate = cratename.is_none();
let mut found_macro = false;

let mut parser = match maybe_new_parser_from_source_str(&sess, filename, source) {
Ok(p) => p,
Err(errs) => {
for mut err in errs {
err.cancel();
}

return (found_main, found_extern_crate, found_macro);
}
};

loop {
match parser.parse_item() {
Ok(Some(item)) => {
if !found_main {
if let ast::ItemKind::Fn(..) = item.kind {
if item.ident.name == sym::main {
found_main = true;
}
}
}

return (found_main, found_extern_crate, found_macro);
}
};
if !found_extern_crate {
if let ast::ItemKind::ExternCrate(original) = item.kind {
// This code will never be reached if `cratename` is none because
// `found_extern_crate` is initialized to `true` if it is none.
let cratename = cratename.unwrap();

loop {
match parser.parse_item() {
Ok(Some(item)) => {
if !found_main {
if let ast::ItemKind::Fn(..) = item.kind {
if item.ident.name == sym::main {
found_main = true;
match original {
Some(name) => found_extern_crate = name.as_str() == cratename,
None => found_extern_crate = item.ident.as_str() == cratename,
}
}
}
}

if !found_extern_crate {
if let ast::ItemKind::ExternCrate(original) = item.kind {
// This code will never be reached if `cratename` is none because
// `found_extern_crate` is initialized to `true` if it is none.
let cratename = cratename.unwrap();

match original {
Some(name) => found_extern_crate = name.as_str() == cratename,
None => found_extern_crate = item.ident.as_str() == cratename,
if !found_macro {
if let ast::ItemKind::Mac(..) = item.kind {
found_macro = true;
}
}
}

if !found_macro {
if let ast::ItemKind::Mac(..) = item.kind {
found_macro = true;
if found_main && found_extern_crate {
break;
}
}

if found_main && found_extern_crate {
Ok(None) => break,
Err(mut e) => {
e.cancel();
break;
}
}
Ok(None) => break,
Err(mut e) => {
e.cancel();
break;
}
}
}

(found_main, found_extern_crate, found_macro)
(found_main, found_extern_crate, found_macro)
})
});
let (already_has_main, already_has_extern_crate, found_macro) = match result {
Ok(result) => result,
Err(ErrorReported) => {
// If the parser panicked due to a fatal error, pass the test code through unchanged.
// The error will be reported during compilation.
return (s.to_owned(), 0);
}
};

// If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't
// see it. In that case, run the old text-based scan to see if they at least have a main
Expand Down
21 changes: 21 additions & 0 deletions src/test/rustdoc/playground-syntax-error.rs
@@ -0,0 +1,21 @@
#![crate_name = "foo"]
#![doc(html_playground_url = "https://play.rust-lang.org/")]

/// bar docs
///
/// ```edition2015
/// use std::future::Future;
/// use std::pin::Pin;
/// fn foo_recursive(n: usize) -> Pin<Box<dyn Future<Output = ()>>> {
/// Box::pin(async move {
/// if n > 0 {
/// foo_recursive(n - 1).await;
/// }
/// })
/// }
/// ```
pub fn bar() {}

// @has foo/fn.bar.html
// @has - '//a[@class="test-arrow"]' "Run"
// @has - '//*[@class="docblock"]' 'foo_recursive'

0 comments on commit efb876f

Please sign in to comment.