Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions compiler/rustc_attr_parsing/src/attributes/mod.rs
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prime example ^^

Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ pub(crate) enum AttributeOrder {
///
/// Attributes are processed from bottom to top, so this raises a warning/error on all the attributes
/// further above the lowest one:
/// ```
/// ```ignore (illustrative)
/// #[stable(since="1.0")] //~ WARNING duplicated attribute
/// #[stable(since="2.0")]
/// ```
Expand All @@ -243,7 +243,7 @@ pub(crate) enum AttributeOrder {
///
/// Attributes are processed from bottom to top, so this raises a warning/error on all the attributes
/// below the highest one:
/// ```
/// ```ignore (illustrative)
/// #[path="foo.rs"]
/// #[path="bar.rs"] //~ WARNING duplicated attribute
/// ```
Expand Down
5 changes: 0 additions & 5 deletions compiler/rustc_errors/src/emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,6 @@ pub trait Emitter {
true
}

/// Checks if we can use colors in the current output stream.
fn supports_color(&self) -> bool {
false
}
Comment on lines -208 to -211
Copy link
Member Author

@fmease fmease Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


fn source_map(&self) -> Option<&SourceMap>;

fn translator(&self) -> &Translator;
Expand Down
64 changes: 28 additions & 36 deletions src/librustdoc/doctest/make.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
//! runnable, e.g. by adding a `main` function if it doesn't already exist.

use std::fmt::{self, Write as _};
use std::io;
use std::sync::Arc;

use rustc_ast::token::{Delimiter, TokenKind};
use rustc_ast::tokenstream::TokenTree;
use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind};
use rustc_errors::emitter::stderr_destination;
use rustc_errors::{AutoStream, ColorConfig, DiagCtxtHandle};
use rustc_errors::emitter::SilentEmitter;
use rustc_errors::{DiagCtxt, DiagCtxtHandle};
use rustc_parse::lexer::StripTokens;
use rustc_parse::new_parser_from_source_str;
use rustc_session::parse::ParseSess;
use rustc_span::edition::{DEFAULT_EDITION, Edition};
use rustc_span::source_map::SourceMap;
use rustc_span::source_map::{FilePathMapping, SourceMap};
use rustc_span::symbol::sym;
use rustc_span::{DUMMY_SP, FileName, Span, kw};
use tracing::debug;
Expand Down Expand Up @@ -445,40 +444,33 @@ fn parse_source(
parent_dcx: Option<DiagCtxtHandle<'_>>,
span: Span,
) -> Result<ParseSourceInfo, ()> {
use rustc_errors::DiagCtxt;
use rustc_errors::emitter::{Emitter, HumanEmitter};
use rustc_span::source_map::FilePathMapping;

let mut info =
ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };

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

let filename = FileName::anon_source_code(&wrapped_source);

let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
let translator = rustc_driver::default_translator();
info.supports_color =
HumanEmitter::new(stderr_destination(ColorConfig::Auto), translator.clone())
.supports_color();
Comment on lines -461 to -463
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now fully removed this logic. Before I rebased past #147207, I actually simplified it to info.supports_color = stderr_destination(ColorConfig::Auto).supports_color();.

However, since said PR (#147207: migrating coloring crates), HumanEmitter::supports_color() unconditionally(!) returns false (in fact, Emitter::supports_color is no longer used by anyone else and should be removed), so there's no reason to keep it. Rephrased, since that PR all compiler diagnostics for doctests are uncolored.

You could argue that I should keep it and patch supports_color in rustc to "work again". However, I'd rather rework our doctest coloring wholesale in a separate PR. At least before that migration PR, our setup was quite busted:

  1. First of all, it didn't query+set supports_color for syntactically invalid doctests, so syntax errors were always shown without color (contrary to e.g., name resolution errors).
  2. Second of all, calling supports_color() here was quite frankly wrong: Piping the output of rustdoc … --test into a file (or | cat or whatever) did not suppress colors. I'm not actually sure if we can ever address that nicely (without stripping ANSI codes after the fact) since we pass that diagnostic to libtest, right? I might very well be wrong here, maybe it's a non-issue.
// Any errors in parsing should also appear when the doctest is compiled for real, so just
// send all the errors that the parser emits directly into a `Sink` instead of stderr.
let emitter = HumanEmitter::new(AutoStream::never(Box::new(io::sink())), translator);
Comment on lines -464 to -466
Copy link
Member Author

@fmease fmease Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes no sense to use a HumanEmitter here that sends things into the void. I'm pretty sure that leads to extra work (rendering the diagnostic) that gets thrown away immediately after. Use a SilentEmitter instead which exists precisely for such use cases.

// Any parse errors should also appear when the doctest is compiled for real,
// so just suppress them here.
Comment on lines +425 to +426
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also for e.g., compile_fail we don't necessary want to output any errors "unless e.g., --nocapture or --test-args --show-output" …; so it really isn't our responsibility.

Therefore elaborate that source comment.

let emitter = SilentEmitter { translator: rustc_driver::default_translator() };

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

// Don't strip any tokens; it wouldn't matter anyway because the source is wrapped in a function.
let mut parser =
match new_parser_from_source_str(&psess, filename, wrapped_source, StripTokens::Nothing) {
Ok(p) => p,
Err(errs) => {
errs.into_iter().for_each(|err| err.cancel());
reset_error_count(&psess);
return Err(());
}
};
let mut parser = match new_parser_from_source_str(
&psess,
FileName::anon_source_code(&wrapped_source),
wrapped_source,
StripTokens::Nothing,
) {
Ok(p) => p,
Err(errs) => {
errs.into_iter().for_each(|err| err.cancel());
reset_error_count(&psess);
return Err(());
}
};

fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {
let extra_len = DOCTEST_CODE_WRAPPER.len();
Expand Down Expand Up @@ -531,7 +523,12 @@ fn parse_source(
let result = match parsed {
Ok(Some(ref item))
if let ast::ItemKind::Fn(ref fn_item) = item.kind
&& let Some(ref body) = fn_item.body =>
&& let Some(ref body) = fn_item.body
// The parser might've recovered from some syntax errors and thus returned `Ok(_)`.
// However, since we've suppressed diagnostic emission above, we must ensure that
// we mark the doctest as syntactically invalid. Otherwise, we can end up generating
// a runnable doctest that's free of any errors even though the source was malformed.
&& psess.dcx().has_errors().is_none() =>
{
for attr in &item.attrs {
if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {
Expand Down Expand Up @@ -587,14 +584,9 @@ fn parse_source(
}
}
}
StmtKind::Expr(ref expr) => {
if matches!(expr.kind, ast::ExprKind::Err(_)) {
reset_error_count(&psess);
return Err(());
}
Comment on lines -591 to -594
Copy link
Member Author

@fmease fmease Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer need to do that. Due to the ErrorGuaranteed in ExprKind::Err we know that an error diagnostic had to be emitted prior meaning dcx.has_errors() has to be some (meaning we already bailed out with an Err(_) above)!

has_non_items = true;
StmtKind::Let(_) | StmtKind::Expr(_) | StmtKind::Semi(_) | StmtKind::Empty => {
has_non_items = true
}
StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true,
}

// Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to
Expand Down
11 changes: 9 additions & 2 deletions tests/rustdoc-ui/doctest/doctest-output-include-fail.stdout
Copy link
Member Author

@fmease fmease Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the parser recovers from a missing semicolon and returns Ok(_). As a result, we used to consider the doctest syntactically valid and thus applied all the usual preprocessing, most importantly adding #![allow(unused)].

Now that we consider it syntactically invalid (rightfully so!), it no longer enjoys this privilege. Hence the warning.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ LL | let x = 234 // no semicolon here! oh no!
LL | }
| - unexpected token

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

error: aborting due to 1 previous error; 1 warning emitted

Couldn't compile the test.

Expand All @@ -22,4 +30,3 @@ failures:

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

all doctests ran in $TIME; merged doctests compilation took $TIME
9 changes: 8 additions & 1 deletion tests/rustdoc-ui/doctest/nocapture-fail.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,12 @@ LL | Input: 123
LL ~ } }
|

error: aborting due to 1 previous error
error[E0601]: `main` function not found in crate `rust_out`
Copy link
Member Author

@fmease fmease Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider this an unfortunate regression. As mentioned in my other review comment, this doctest no longer gets preprocessed (like adding a main if absent).

If you like to see this addressed, I can of course change it to not bail out on has_errors (but still set failed_to_parse to true) in order to forward has_main and so on correctly (the code might get a bit gnarly tho).

However in general (not necessarily in this specific case), wrapping invalid doctests in a function is always a trade-off (it may lead to better errors in some cases but to worse ones in others; and it may leak internal names in diagnostics like _doctest_main_tests_rustdoc_ui_doctest_comment_in_attr_134221_2_rs_11_0; see #142446 (comment) for context).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, due to the existence of error code checks like compile_fail,ENNNN I have to preserve some of the original behavior actually (namely continuing to preprocess certain syntactically invalid doctests (at the very least ones that have syntax errors that the parser recovered from)).

--> $DIR/nocapture-fail.rs:10:2
|
LL | }
| ^ consider adding a `main` function to `$DIR/nocapture-fail.rs`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0601`.
9 changes: 9 additions & 0 deletions tests/rustdoc-ui/doctest/recovered-syntax-error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// issue: <https://github.com/rust-lang/rust/issues/147999>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[add description]

//@ compile-flags: --test
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
//@ failure-status: 101

//! ```
//! #[derive(Clone)]
//! ```
22 changes: 22 additions & 0 deletions tests/rustdoc-ui/doctest/recovered-syntax-error.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

running 1 test
test $DIR/recovered-syntax-error.rs - (line 7) ... FAILED

failures:

---- $DIR/recovered-syntax-error.rs - (line 7) stdout ----
error: expected item after attributes
--> $DIR/recovered-syntax-error.rs:8:1
|
LL | #[derive(Clone)]
| ^^^^^^^^^^^^^^^^

error: aborting due to 1 previous error

Couldn't compile the test.

failures:
$DIR/recovered-syntax-error.rs - (line 7)

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