Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 6eabffb

Browse files
Greatly improve handling of doctests attributes, making it possible to merge doctests more efficiently
1 parent 03118fa commit 6eabffb

File tree

3 files changed

+116
-52
lines changed

3 files changed

+116
-52
lines changed

src/librustdoc/doctest.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -536,17 +536,15 @@ fn run_test(
536536
compiler.arg("--error-format=short");
537537
let input_file =
538538
doctest.test_opts.outdir.path().join(&format!("doctest_{}.rs", doctest.edition));
539-
eprintln!("OUUUUUUUT>>>>>>> {input_file:?}");
540539
if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
541540
// If we cannot write this file for any reason, we leave. All combined tests will be
542541
// tested as standalone tests.
543542
return Err(TestFailure::CompileError);
544543
}
545544
compiler.arg(input_file);
545+
// FIXME: Remove once done fixing bugs.
546+
// FIXME: Should this call only be done if `nocapture` is not set?
546547
// compiler.stderr(Stdio::null());
547-
let mut buffer = String::new();
548-
eprintln!("Press ENTER");
549-
let _ = std::io::stdin().read_line(&mut buffer);
550548
} else {
551549
compiler.arg("-");
552550
compiler.stdin(Stdio::piped());
@@ -768,7 +766,7 @@ struct CreateRunnableDoctests {
768766

769767
impl CreateRunnableDoctests {
770768
fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDoctests {
771-
let can_merge_doctests = true;//rustdoc_options.edition >= Edition::Edition2024;
769+
let can_merge_doctests = true; //rustdoc_options.edition >= Edition::Edition2024;
772770
CreateRunnableDoctests {
773771
standalone_tests: Vec::new(),
774772
mergeable_tests: FxHashMap::default(),
@@ -818,8 +816,7 @@ impl CreateRunnableDoctests {
818816
|| scraped_test.langstr.test_harness
819817
|| scraped_test.langstr.standalone
820818
|| self.rustdoc_options.nocapture
821-
|| self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output")
822-
|| doctest.crate_attrs.contains("#![no_std]");
819+
|| self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output");
823820
if is_standalone {
824821
let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
825822
self.standalone_tests.push(test_desc);

src/librustdoc/doctest/make.rs

Lines changed: 105 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pub(crate) struct DocTest {
2222
pub(crate) already_has_extern_crate: bool,
2323
pub(crate) has_main_fn: bool,
2424
pub(crate) crate_attrs: String,
25+
/// If this is a merged doctest, it will be put into `everything_else`, otherwise it will
26+
/// put into `crate_attrs`.
27+
pub(crate) maybe_crate_attrs: String,
2528
pub(crate) crates: String,
2629
pub(crate) everything_else: String,
2730
pub(crate) test_id: Option<String>,
@@ -38,7 +41,14 @@ impl DocTest {
3841
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
3942
test_id: Option<String>,
4043
) -> Self {
41-
let (crate_attrs, everything_else, crates) = partition_source(source, edition);
44+
let SourceInfo {
45+
crate_attrs,
46+
maybe_crate_attrs,
47+
crates,
48+
everything_else,
49+
has_features,
50+
has_no_std,
51+
} = partition_source(source, edition);
4252
let mut supports_color = false;
4353

4454
// Uses librustc_ast to parse the doctest and find if there's a main fn and the extern
@@ -56,10 +66,11 @@ impl DocTest {
5666
else {
5767
// If the parser panicked due to a fatal error, pass the test code through unchanged.
5868
// The error will be reported during compilation.
59-
return DocTest {
69+
return Self {
6070
supports_color: false,
6171
has_main_fn: false,
6272
crate_attrs,
73+
maybe_crate_attrs,
6374
crates,
6475
everything_else,
6576
already_has_extern_crate: false,
@@ -72,14 +83,15 @@ impl DocTest {
7283
supports_color,
7384
has_main_fn,
7485
crate_attrs,
86+
maybe_crate_attrs,
7587
crates,
7688
everything_else,
7789
already_has_extern_crate,
7890
test_id,
7991
failed_ast: false,
8092
// If the AST returned an error, we don't want this doctest to be merged with the
81-
// others.
82-
can_be_merged: !failed_ast,
93+
// others. Same if it contains `#[feature]` or `#[no_std]`.
94+
can_be_merged: !failed_ast && !has_no_std && !has_features,
8395
}
8496
}
8597

@@ -118,6 +130,7 @@ impl DocTest {
118130
// Now push any outer attributes from the example, assuming they
119131
// are intended to be crate attributes.
120132
prog.push_str(&self.crate_attrs);
133+
prog.push_str(&self.maybe_crate_attrs);
121134
prog.push_str(&self.crates);
122135

123136
// Don't inject `extern crate std` because it's already injected by the
@@ -405,11 +418,22 @@ fn check_for_main_and_extern_crate(
405418
Ok((has_main_fn, already_has_extern_crate, parsing_result != ParsingResult::Ok))
406419
}
407420

408-
fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool {
421+
enum AttrKind {
422+
CrateAttr,
423+
Attr,
424+
Feature,
425+
NoStd,
426+
}
427+
428+
/// Returns `Some` if the attribute is complete and `Some(true)` if it is an attribute that can be
429+
/// placed at the crate root.
430+
fn check_if_attr_is_complete(source: &str, edition: Edition) -> Option<AttrKind> {
409431
if source.is_empty() {
410432
// Empty content so nothing to check in here...
411-
return true;
433+
return None;
412434
}
435+
let not_crate_attrs = [sym::forbid, sym::allow, sym::warn, sym::deny];
436+
413437
rustc_driver::catch_fatal_errors(|| {
414438
rustc_span::create_session_if_not_set_then(edition, |_| {
415439
use rustc_errors::emitter::HumanEmitter;
@@ -435,33 +459,77 @@ fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool {
435459
errs.into_iter().for_each(|err| err.cancel());
436460
// If there is an unclosed delimiter, an error will be returned by the
437461
// tokentrees.
438-
return false;
462+
return None;
439463
}
440464
};
441465
// If a parsing error happened, it's very likely that the attribute is incomplete.
442-
if let Err(e) = parser.parse_attribute(InnerAttrPolicy::Permitted) {
443-
e.cancel();
444-
return false;
445-
}
446-
true
466+
let ret = match parser.parse_attribute(InnerAttrPolicy::Permitted) {
467+
Ok(attr) => {
468+
let attr_name = attr.name_or_empty();
469+
470+
if attr_name == sym::feature {
471+
Some(AttrKind::Feature)
472+
} else if attr_name == sym::no_std {
473+
Some(AttrKind::NoStd)
474+
} else if not_crate_attrs.contains(&attr_name) {
475+
Some(AttrKind::Attr)
476+
} else {
477+
Some(AttrKind::CrateAttr)
478+
}
479+
}
480+
Err(e) => {
481+
e.cancel();
482+
None
483+
}
484+
};
485+
ret
447486
})
448487
})
449-
.unwrap_or(false)
488+
.unwrap_or(None)
450489
}
451490

452-
/// Returns `(crate_attrs, content, crates)`.
453-
fn partition_source(s: &str, edition: Edition) -> (String, String, String) {
491+
fn handle_attr(mod_attr_pending: &mut String, source_info: &mut SourceInfo, edition: Edition) {
492+
if let Some(attr_kind) = check_if_attr_is_complete(mod_attr_pending, edition) {
493+
let push_to = match attr_kind {
494+
AttrKind::CrateAttr => &mut source_info.crate_attrs,
495+
AttrKind::Attr => &mut source_info.maybe_crate_attrs,
496+
AttrKind::Feature => {
497+
source_info.has_features = true;
498+
&mut source_info.crate_attrs
499+
}
500+
AttrKind::NoStd => {
501+
source_info.has_no_std = true;
502+
&mut source_info.crate_attrs
503+
}
504+
};
505+
push_to.push_str(mod_attr_pending);
506+
push_to.push('\n');
507+
// If it's complete, then we can clear the pending content.
508+
mod_attr_pending.clear();
509+
} else if mod_attr_pending.ends_with('\\') {
510+
mod_attr_pending.push('n');
511+
}
512+
}
513+
514+
#[derive(Default)]
515+
struct SourceInfo {
516+
crate_attrs: String,
517+
maybe_crate_attrs: String,
518+
crates: String,
519+
everything_else: String,
520+
has_features: bool,
521+
has_no_std: bool,
522+
}
523+
524+
fn partition_source(s: &str, edition: Edition) -> SourceInfo {
454525
#[derive(Copy, Clone, PartialEq)]
455526
enum PartitionState {
456527
Attrs,
457528
Crates,
458529
Other,
459530
}
531+
let mut source_info = SourceInfo::default();
460532
let mut state = PartitionState::Attrs;
461-
let mut crate_attrs = String::new();
462-
let mut crates = String::new();
463-
let mut after = String::new();
464-
465533
let mut mod_attr_pending = String::new();
466534

467535
for line in s.lines() {
@@ -472,12 +540,9 @@ fn partition_source(s: &str, edition: Edition) -> (String, String, String) {
472540
match state {
473541
PartitionState::Attrs => {
474542
state = if trimline.starts_with("#![") {
475-
if !check_if_attr_is_complete(line, edition) {
476-
mod_attr_pending = line.to_owned();
477-
} else {
478-
mod_attr_pending.clear();
479-
}
480-
PartitionState::Attrs
543+
mod_attr_pending = line.to_owned();
544+
handle_attr(&mut mod_attr_pending, &mut source_info, edition);
545+
continue;
481546
} else if trimline.chars().all(|c| c.is_whitespace())
482547
|| (trimline.starts_with("//") && !trimline.starts_with("///"))
483548
{
@@ -492,15 +557,10 @@ fn partition_source(s: &str, edition: Edition) -> (String, String, String) {
492557
// If not, then we append the new line into the pending attribute to check
493558
// if this time it's complete...
494559
mod_attr_pending.push_str(line);
495-
if !trimline.is_empty()
496-
&& check_if_attr_is_complete(&mod_attr_pending, edition)
497-
{
498-
// If it's complete, then we can clear the pending content.
499-
mod_attr_pending.clear();
560+
if !trimline.is_empty() {
561+
handle_attr(&mut mod_attr_pending, &mut source_info, edition);
500562
}
501-
// In any case, this is considered as `PartitionState::Attrs` so it's
502-
// prepended before rustdoc's inserts.
503-
PartitionState::Attrs
563+
continue;
504564
} else {
505565
PartitionState::Other
506566
}
@@ -522,23 +582,25 @@ fn partition_source(s: &str, edition: Edition) -> (String, String, String) {
522582

523583
match state {
524584
PartitionState::Attrs => {
525-
crate_attrs.push_str(line);
526-
crate_attrs.push('\n');
585+
source_info.crate_attrs.push_str(line);
586+
source_info.crate_attrs.push('\n');
527587
}
528588
PartitionState::Crates => {
529-
crates.push_str(line);
530-
crates.push('\n');
589+
source_info.crates.push_str(line);
590+
source_info.crates.push('\n');
531591
}
532592
PartitionState::Other => {
533-
after.push_str(line);
534-
after.push('\n');
593+
source_info.everything_else.push_str(line);
594+
source_info.everything_else.push('\n');
535595
}
536596
}
537597
}
538598

539-
debug!("before:\n{before}");
540-
debug!("crates:\n{crates}");
541-
debug!("after:\n{after}");
599+
source_info.everything_else = source_info.everything_else.trim().to_string();
600+
601+
debug!("crate_attrs:\n{}{}", source_info.crate_attrs, source_info.maybe_crate_attrs);
602+
debug!("crates:\n{}", source_info.crates);
603+
debug!("after:\n{}", source_info.everything_else);
542604

543-
(before, after.trim().to_owned(), crates)
605+
source_info
544606
}

src/librustdoc/doctest/runner.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,11 @@ impl DocTestRunner {
107107
#[rustc_main]
108108
#[coverage(off)]
109109
fn main() {{
110-
test::test_main(&[{test_args}], vec![{ids}], None);
110+
test::test_main_static_with_args(
111+
&[{test_args}],
112+
&mut [{ids}],
113+
None,
114+
);
111115
}}",
112116
output = self.output,
113117
ids = self.ids,
@@ -148,7 +152,8 @@ fn generate_mergeable_doctest(
148152
// We generate nothing else.
149153
writeln!(output, "mod {test_id} {{\n").unwrap();
150154
} else {
151-
writeln!(output, "mod {test_id} {{\n{}", doctest.crates).unwrap();
155+
writeln!(output, "mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs)
156+
.unwrap();
152157
if doctest.has_main_fn {
153158
output.push_str(&doctest.everything_else);
154159
} else {

0 commit comments

Comments
 (0)