Skip to content

Commit 39d00f4

Browse files
committed
rustdoc: Properly detect syntactically invalid doctests
That is, even in the presence of parse error recovery.
1 parent ab92564 commit 39d00f4

File tree

7 files changed

+78
-46
lines changed

7 files changed

+78
-46
lines changed

compiler/rustc_attr_parsing/src/attributes/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ pub(crate) enum AttributeOrder {
232232
///
233233
/// Attributes are processed from bottom to top, so this raises a warning/error on all the attributes
234234
/// further above the lowest one:
235-
/// ```
235+
/// ```ignore (illustrative)
236236
/// #[stable(since="1.0")] //~ WARNING duplicated attribute
237237
/// #[stable(since="2.0")]
238238
/// ```
@@ -243,7 +243,7 @@ pub(crate) enum AttributeOrder {
243243
///
244244
/// Attributes are processed from bottom to top, so this raises a warning/error on all the attributes
245245
/// below the highest one:
246-
/// ```
246+
/// ```ignore (illustrative)
247247
/// #[path="foo.rs"]
248248
/// #[path="bar.rs"] //~ WARNING duplicated attribute
249249
/// ```

compiler/rustc_errors/src/emitter.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,6 @@ pub trait Emitter {
205205
true
206206
}
207207

208-
/// Checks if we can use colors in the current output stream.
209-
fn supports_color(&self) -> bool {
210-
false
211-
}
212-
213208
fn source_map(&self) -> Option<&SourceMap>;
214209

215210
fn translator(&self) -> &Translator;

src/librustdoc/doctest/make.rs

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@
22
//! runnable, e.g. by adding a `main` function if it doesn't already exist.
33
44
use std::fmt::{self, Write as _};
5-
use std::io;
65
use std::sync::Arc;
76

87
use rustc_ast::token::{Delimiter, TokenKind};
98
use rustc_ast::tokenstream::TokenTree;
109
use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind};
11-
use rustc_errors::emitter::stderr_destination;
12-
use rustc_errors::{AutoStream, ColorConfig, DiagCtxtHandle};
10+
use rustc_errors::emitter::SilentEmitter;
11+
use rustc_errors::{DiagCtxt, DiagCtxtHandle};
1312
use rustc_parse::lexer::StripTokens;
1413
use rustc_parse::new_parser_from_source_str;
1514
use rustc_session::parse::ParseSess;
1615
use rustc_span::edition::{DEFAULT_EDITION, Edition};
17-
use rustc_span::source_map::SourceMap;
16+
use rustc_span::source_map::{FilePathMapping, SourceMap};
1817
use rustc_span::symbol::sym;
1918
use rustc_span::{DUMMY_SP, FileName, Span, kw};
2019
use tracing::debug;
@@ -445,40 +444,33 @@ fn parse_source(
445444
parent_dcx: Option<DiagCtxtHandle<'_>>,
446445
span: Span,
447446
) -> Result<ParseSourceInfo, ()> {
448-
use rustc_errors::DiagCtxt;
449-
use rustc_errors::emitter::{Emitter, HumanEmitter};
450-
use rustc_span::source_map::FilePathMapping;
451-
452447
let mut info =
453448
ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };
454449

455450
let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");
456451

457-
let filename = FileName::anon_source_code(&wrapped_source);
458-
459-
let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
460-
let translator = rustc_driver::default_translator();
461-
info.supports_color =
462-
HumanEmitter::new(stderr_destination(ColorConfig::Auto), translator.clone())
463-
.supports_color();
464-
// Any errors in parsing should also appear when the doctest is compiled for real, so just
465-
// send all the errors that the parser emits directly into a `Sink` instead of stderr.
466-
let emitter = HumanEmitter::new(AutoStream::never(Box::new(io::sink())), translator);
452+
// Any parse errors should also appear when the doctest is compiled for real,
453+
// so just suppress them here.
454+
let emitter = SilentEmitter { translator: rustc_driver::default_translator() };
467455

468456
// FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
469457
let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
470-
let psess = ParseSess::with_dcx(dcx, sm);
458+
let psess = ParseSess::with_dcx(dcx, Arc::new(SourceMap::new(FilePathMapping::empty())));
471459

472460
// Don't strip any tokens; it wouldn't matter anyway because the source is wrapped in a function.
473-
let mut parser =
474-
match new_parser_from_source_str(&psess, filename, wrapped_source, StripTokens::Nothing) {
475-
Ok(p) => p,
476-
Err(errs) => {
477-
errs.into_iter().for_each(|err| err.cancel());
478-
reset_error_count(&psess);
479-
return Err(());
480-
}
481-
};
461+
let mut parser = match new_parser_from_source_str(
462+
&psess,
463+
FileName::anon_source_code(&wrapped_source),
464+
wrapped_source,
465+
StripTokens::Nothing,
466+
) {
467+
Ok(p) => p,
468+
Err(errs) => {
469+
errs.into_iter().for_each(|err| err.cancel());
470+
reset_error_count(&psess);
471+
return Err(());
472+
}
473+
};
482474

483475
fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {
484476
let extra_len = DOCTEST_CODE_WRAPPER.len();
@@ -531,7 +523,12 @@ fn parse_source(
531523
let result = match parsed {
532524
Ok(Some(ref item))
533525
if let ast::ItemKind::Fn(ref fn_item) = item.kind
534-
&& let Some(ref body) = fn_item.body =>
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() =>
535532
{
536533
for attr in &item.attrs {
537534
if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {
@@ -587,14 +584,9 @@ fn parse_source(
587584
}
588585
}
589586
}
590-
StmtKind::Expr(ref expr) => {
591-
if matches!(expr.kind, ast::ExprKind::Err(_)) {
592-
reset_error_count(&psess);
593-
return Err(());
594-
}
595-
has_non_items = true;
587+
StmtKind::Let(_) | StmtKind::Expr(_) | StmtKind::Semi(_) | StmtKind::Empty => {
588+
has_non_items = true
596589
}
597-
StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true,
598590
}
599591

600592
// Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to

tests/rustdoc-ui/doctest/doctest-output-include-fail.stdout

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ LL | let x = 234 // no semicolon here! oh no!
1313
LL | }
1414
| - unexpected token
1515

16-
error: aborting due to 1 previous error
16+
warning: unused variable: `x`
17+
--> $DIR/doctest-output-include-fail.md:5:9
18+
|
19+
LL | let x = 234 // no semicolon here! oh no!
20+
| ^ help: if this is intentional, prefix it with an underscore: `_x`
21+
|
22+
= note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
23+
24+
error: aborting due to 1 previous error; 1 warning emitted
1725

1826
Couldn't compile the test.
1927

@@ -22,4 +30,3 @@ failures:
2230

2331
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
2432

25-
all doctests ran in $TIME; merged doctests compilation took $TIME

tests/rustdoc-ui/doctest/nocapture-fail.stderr

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,12 @@ LL | Input: 123
1414
LL ~ } }
1515
|
1616

17-
error: aborting due to 1 previous error
17+
error[E0601]: `main` function not found in crate `rust_out`
18+
--> $DIR/nocapture-fail.rs:10:2
19+
|
20+
LL | }
21+
| ^ consider adding a `main` function to `$DIR/nocapture-fail.rs`
22+
23+
error: aborting due to 2 previous errors
1824

25+
For more information about this error, try `rustc --explain E0601`.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// issue: <https://github.com/rust-lang/rust/issues/147999>
2+
//@ compile-flags: --test
3+
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
4+
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
5+
//@ failure-status: 101
6+
7+
//! ```
8+
//! #[derive(Clone)]
9+
//! ```
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
running 1 test
3+
test $DIR/recovered-syntax-error.rs - (line 7) ... FAILED
4+
5+
failures:
6+
7+
---- $DIR/recovered-syntax-error.rs - (line 7) stdout ----
8+
error: expected item after attributes
9+
--> $DIR/recovered-syntax-error.rs:8:1
10+
|
11+
LL | #[derive(Clone)]
12+
| ^^^^^^^^^^^^^^^^
13+
14+
error: aborting due to 1 previous error
15+
16+
Couldn't compile the test.
17+
18+
failures:
19+
$DIR/recovered-syntax-error.rs - (line 7)
20+
21+
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
22+

0 commit comments

Comments
 (0)