Skip to content

Commit 66c1c70

Browse files
authored
Unrolled build for #141399
Rollup merge of #141399 - GuillaumeGomez:extracted-doctest, r=aDotInTheVoid [rustdoc] Give more information into extracted doctest information Follow-up of #134531. This update fragment the doctest code into its sub-parts to give more control to the end users on how they want to use it. The new JSON looks like this: ```json { "format_version":2, "doctests":[ { "file":"$DIR/extract-doctests-result.rs", "line":8, "doctest_attributes":{ "original":"", "should_panic":false, "no_run":false, "ignore":"None", "rust":true, "test_harness":false, "compile_fail":false, "standalone_crate":false, "error_codes":[], "edition":null, "added_css_classes":[], "unknown":[] }, "original_code":"let x = 12;\nOk(())", "doctest_code":{ "crate_level":"#![allow(unused)]\n", "code":"let x = 12;\nOk(())", "wrapper":{ "before":"fn main() { fn _inner() -> core::result::Result<(), impl core::fmt::Debug> {\n", "after":"\n} _inner().unwrap() }", "returns_result":true } }, "name":"$DIR/extract-doctests-result.rs - (line 8)" } ] } ``` for this doctest: ```rust let x = 12; Ok(()) ``` With this, I think it matches what you need ``@ojeda?`` If so, once merged I'll update the patch I sent to RfL. r? ``@aDotInTheVoid``
2 parents 64033a4 + f1ceb07 commit 66c1c70

File tree

9 files changed

+251
-56
lines changed

9 files changed

+251
-56
lines changed

src/doc/rustdoc/src/unstable-features.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,9 @@ For this rust code:
581581

582582
```rust
583583
/// ```
584+
/// #![allow(dead_code)]
584585
/// let x = 12;
586+
/// Ok(())
585587
/// ```
586588
pub trait Trait {}
587589
```
@@ -590,10 +592,10 @@ The generated output (formatted) will look like this:
590592

591593
```json
592594
{
593-
"format_version": 1,
595+
"format_version": 2,
594596
"doctests": [
595597
{
596-
"file": "foo.rs",
598+
"file": "src/lib.rs",
597599
"line": 1,
598600
"doctest_attributes": {
599601
"original": "",
@@ -609,9 +611,17 @@ The generated output (formatted) will look like this:
609611
"added_css_classes": [],
610612
"unknown": []
611613
},
612-
"original_code": "let x = 12;",
613-
"doctest_code": "#![allow(unused)]\nfn main() {\nlet x = 12;\n}",
614-
"name": "foo.rs - Trait (line 1)"
614+
"original_code": "#![allow(dead_code)]\nlet x = 12;\nOk(())",
615+
"doctest_code": {
616+
"crate_level": "#![allow(unused)]\n#![allow(dead_code)]\n\n",
617+
"code": "let x = 12;\nOk(())",
618+
"wrapper": {
619+
"before": "fn main() { fn _inner() -> core::result::Result<(), impl core::fmt::Debug> {\n",
620+
"after": "\n} _inner().unwrap() }",
621+
"returns_result": true
622+
}
623+
},
624+
"name": "src/lib.rs - (line 1)"
615625
}
616626
]
617627
}
@@ -624,6 +634,10 @@ The generated output (formatted) will look like this:
624634
* `doctest_attributes` contains computed information about the attributes used on the doctests. For more information about doctest attributes, take a look [here](write-documentation/documentation-tests.html#attributes).
625635
* `original_code` is the code as written in the source code before rustdoc modifies it.
626636
* `doctest_code` is the code modified by rustdoc that will be run. If there is a fatal syntax error, this field will not be present.
637+
* `crate_level` is the crate level code (like attributes or `extern crate`) that will be added at the top-level of the generated doctest.
638+
* `code` is "naked" doctest without anything from `crate_level` and `wrapper` content.
639+
* `wrapper` contains extra code that will be added before and after `code`.
640+
* `returns_result` is a boolean. If `true`, it means that the doctest returns a `Result` type.
627641
* `name` is the name generated by rustdoc which represents this doctest.
628642

629643
### html

src/librustdoc/doctest.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,14 +1053,14 @@ fn doctest_run_fn(
10531053
let report_unused_externs = |uext| {
10541054
unused_externs.lock().unwrap().push(uext);
10551055
};
1056-
let (full_test_code, full_test_line_offset) = doctest.generate_unique_doctest(
1056+
let (wrapped, full_test_line_offset) = doctest.generate_unique_doctest(
10571057
&scraped_test.text,
10581058
scraped_test.langstr.test_harness,
10591059
&global_opts,
10601060
Some(&global_opts.crate_name),
10611061
);
10621062
let runnable_test = RunnableDocTest {
1063-
full_test_code,
1063+
full_test_code: wrapped.to_string(),
10641064
full_test_line_offset,
10651065
test_opts,
10661066
global_opts,

src/librustdoc/doctest/extracted.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
//! This module contains the logic to extract doctests and output a JSON containing this
44
//! information.
55
6+
use rustc_span::edition::Edition;
67
use serde::Serialize;
78

9+
use super::make::DocTestWrapResult;
810
use super::{BuildDocTestBuilder, ScrapedDocTest};
911
use crate::config::Options as RustdocOptions;
1012
use crate::html::markdown;
@@ -14,7 +16,7 @@ use crate::html::markdown;
1416
/// This integer is incremented with every breaking change to the API,
1517
/// and is returned along with the JSON blob into the `format_version` root field.
1618
/// Consuming code should assert that this value matches the format version(s) that it supports.
17-
const FORMAT_VERSION: u32 = 1;
19+
const FORMAT_VERSION: u32 = 2;
1820

1921
#[derive(Serialize)]
2022
pub(crate) struct ExtractedDocTests {
@@ -34,7 +36,16 @@ impl ExtractedDocTests {
3436
options: &RustdocOptions,
3537
) {
3638
let edition = scraped_test.edition(options);
39+
self.add_test_with_edition(scraped_test, opts, edition)
40+
}
3741

42+
/// This method is used by unit tests to not have to provide a `RustdocOptions`.
43+
pub(crate) fn add_test_with_edition(
44+
&mut self,
45+
scraped_test: ScrapedDocTest,
46+
opts: &super::GlobalTestOptions,
47+
edition: Edition,
48+
) {
3849
let ScrapedDocTest { filename, line, langstr, text, name, global_crate_attrs, .. } =
3950
scraped_test;
4051

@@ -44,8 +55,7 @@ impl ExtractedDocTests {
4455
.edition(edition)
4556
.lang_str(&langstr)
4657
.build(None);
47-
48-
let (full_test_code, size) = doctest.generate_unique_doctest(
58+
let (wrapped, _size) = doctest.generate_unique_doctest(
4959
&text,
5060
langstr.test_harness,
5161
opts,
@@ -55,11 +65,46 @@ impl ExtractedDocTests {
5565
file: filename.prefer_remapped_unconditionaly().to_string(),
5666
line,
5767
doctest_attributes: langstr.into(),
58-
doctest_code: if size != 0 { Some(full_test_code) } else { None },
68+
doctest_code: match wrapped {
69+
DocTestWrapResult::Valid { crate_level_code, wrapper, code } => Some(DocTest {
70+
crate_level: crate_level_code,
71+
code,
72+
wrapper: wrapper.map(
73+
|super::make::WrapperInfo { before, after, returns_result, .. }| {
74+
WrapperInfo { before, after, returns_result }
75+
},
76+
),
77+
}),
78+
DocTestWrapResult::SyntaxError { .. } => None,
79+
},
5980
original_code: text,
6081
name,
6182
});
6283
}
84+
85+
#[cfg(test)]
86+
pub(crate) fn doctests(&self) -> &[ExtractedDocTest] {
87+
&self.doctests
88+
}
89+
}
90+
91+
#[derive(Serialize)]
92+
pub(crate) struct WrapperInfo {
93+
before: String,
94+
after: String,
95+
returns_result: bool,
96+
}
97+
98+
#[derive(Serialize)]
99+
pub(crate) struct DocTest {
100+
crate_level: String,
101+
code: String,
102+
/// This field can be `None` if one of the following conditions is true:
103+
///
104+
/// * The doctest's codeblock has the `test_harness` attribute.
105+
/// * The doctest has a `main` function.
106+
/// * The doctest has the `![no_std]` attribute.
107+
pub(crate) wrapper: Option<WrapperInfo>,
63108
}
64109

65110
#[derive(Serialize)]
@@ -69,7 +114,7 @@ pub(crate) struct ExtractedDocTest {
69114
doctest_attributes: LangString,
70115
original_code: String,
71116
/// `None` if the code syntax is invalid.
72-
doctest_code: Option<String>,
117+
pub(crate) doctest_code: Option<DocTest>,
73118
name: String,
74119
}
75120

src/librustdoc/doctest/make.rs

Lines changed: 109 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,80 @@ pub(crate) struct DocTestBuilder {
196196
pub(crate) can_be_merged: bool,
197197
}
198198

199+
/// Contains needed information for doctest to be correctly generated with expected "wrapping".
200+
pub(crate) struct WrapperInfo {
201+
pub(crate) before: String,
202+
pub(crate) after: String,
203+
pub(crate) returns_result: bool,
204+
insert_indent_space: bool,
205+
}
206+
207+
impl WrapperInfo {
208+
fn len(&self) -> usize {
209+
self.before.len() + self.after.len()
210+
}
211+
}
212+
213+
/// Contains a doctest information. Can be converted into code with the `to_string()` method.
214+
pub(crate) enum DocTestWrapResult {
215+
Valid {
216+
crate_level_code: String,
217+
/// This field can be `None` if one of the following conditions is true:
218+
///
219+
/// * The doctest's codeblock has the `test_harness` attribute.
220+
/// * The doctest has a `main` function.
221+
/// * The doctest has the `![no_std]` attribute.
222+
wrapper: Option<WrapperInfo>,
223+
/// Contains the doctest processed code without the wrappers (which are stored in the
224+
/// `wrapper` field).
225+
code: String,
226+
},
227+
/// Contains the original source code.
228+
SyntaxError(String),
229+
}
230+
231+
impl std::string::ToString for DocTestWrapResult {
232+
fn to_string(&self) -> String {
233+
match self {
234+
Self::SyntaxError(s) => s.clone(),
235+
Self::Valid { crate_level_code, wrapper, code } => {
236+
let mut prog_len = code.len() + crate_level_code.len();
237+
if let Some(wrapper) = wrapper {
238+
prog_len += wrapper.len();
239+
if wrapper.insert_indent_space {
240+
prog_len += code.lines().count() * 4;
241+
}
242+
}
243+
let mut prog = String::with_capacity(prog_len);
244+
245+
prog.push_str(crate_level_code);
246+
if let Some(wrapper) = wrapper {
247+
prog.push_str(&wrapper.before);
248+
249+
// add extra 4 spaces for each line to offset the code block
250+
if wrapper.insert_indent_space {
251+
write!(
252+
prog,
253+
"{}",
254+
fmt::from_fn(|f| code
255+
.lines()
256+
.map(|line| fmt::from_fn(move |f| write!(f, " {line}")))
257+
.joined("\n", f))
258+
)
259+
.unwrap();
260+
} else {
261+
prog.push_str(code);
262+
}
263+
prog.push_str(&wrapper.after);
264+
} else {
265+
prog.push_str(code);
266+
}
267+
prog
268+
}
269+
}
270+
}
271+
}
272+
199273
impl DocTestBuilder {
200274
fn invalid(
201275
global_crate_attrs: Vec<String>,
@@ -228,50 +302,49 @@ impl DocTestBuilder {
228302
dont_insert_main: bool,
229303
opts: &GlobalTestOptions,
230304
crate_name: Option<&str>,
231-
) -> (String, usize) {
305+
) -> (DocTestWrapResult, usize) {
232306
if self.invalid_ast {
233307
// If the AST failed to compile, no need to go generate a complete doctest, the error
234308
// will be better this way.
235309
debug!("invalid AST:\n{test_code}");
236-
return (test_code.to_string(), 0);
310+
return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0);
237311
}
238312
let mut line_offset = 0;
239-
let mut prog = String::new();
240-
let everything_else = self.everything_else.trim();
241-
313+
let mut crate_level_code = String::new();
314+
let processed_code = self.everything_else.trim();
242315
if self.global_crate_attrs.is_empty() {
243316
// If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
244317
// lints that are commonly triggered in doctests. The crate-level test attributes are
245318
// commonly used to make tests fail in case they trigger warnings, so having this there in
246319
// that case may cause some tests to pass when they shouldn't have.
247-
prog.push_str("#![allow(unused)]\n");
320+
crate_level_code.push_str("#![allow(unused)]\n");
248321
line_offset += 1;
249322
}
250323

251324
// Next, any attributes that came from #![doc(test(attr(...)))].
252325
for attr in &self.global_crate_attrs {
253-
prog.push_str(&format!("#![{attr}]\n"));
326+
crate_level_code.push_str(&format!("#![{attr}]\n"));
254327
line_offset += 1;
255328
}
256329

257330
// Now push any outer attributes from the example, assuming they
258331
// are intended to be crate attributes.
259332
if !self.crate_attrs.is_empty() {
260-
prog.push_str(&self.crate_attrs);
333+
crate_level_code.push_str(&self.crate_attrs);
261334
if !self.crate_attrs.ends_with('\n') {
262-
prog.push('\n');
335+
crate_level_code.push('\n');
263336
}
264337
}
265338
if !self.maybe_crate_attrs.is_empty() {
266-
prog.push_str(&self.maybe_crate_attrs);
339+
crate_level_code.push_str(&self.maybe_crate_attrs);
267340
if !self.maybe_crate_attrs.ends_with('\n') {
268-
prog.push('\n');
341+
crate_level_code.push('\n');
269342
}
270343
}
271344
if !self.crates.is_empty() {
272-
prog.push_str(&self.crates);
345+
crate_level_code.push_str(&self.crates);
273346
if !self.crates.ends_with('\n') {
274-
prog.push('\n');
347+
crate_level_code.push('\n');
275348
}
276349
}
277350

@@ -289,17 +362,20 @@ impl DocTestBuilder {
289362
{
290363
// rustdoc implicitly inserts an `extern crate` item for the own crate
291364
// which may be unused, so we need to allow the lint.
292-
prog.push_str("#[allow(unused_extern_crates)]\n");
365+
crate_level_code.push_str("#[allow(unused_extern_crates)]\n");
293366

294-
prog.push_str(&format!("extern crate r#{crate_name};\n"));
367+
crate_level_code.push_str(&format!("extern crate r#{crate_name};\n"));
295368
line_offset += 1;
296369
}
297370

298371
// FIXME: This code cannot yet handle no_std test cases yet
299-
if dont_insert_main || self.has_main_fn || prog.contains("![no_std]") {
300-
prog.push_str(everything_else);
372+
let wrapper = if dont_insert_main
373+
|| self.has_main_fn
374+
|| crate_level_code.contains("![no_std]")
375+
{
376+
None
301377
} else {
302-
let returns_result = everything_else.ends_with("(())");
378+
let returns_result = processed_code.ends_with("(())");
303379
// Give each doctest main function a unique name.
304380
// This is for example needed for the tooling around `-C instrument-coverage`.
305381
let inner_fn_name = if let Some(ref test_id) = self.test_id {
@@ -333,28 +409,22 @@ impl DocTestBuilder {
333409
// /// ``` <- end of the inner main
334410
line_offset += 1;
335411

336-
prog.push_str(&main_pre);
337-
338-
// add extra 4 spaces for each line to offset the code block
339-
if opts.insert_indent_space {
340-
write!(
341-
prog,
342-
"{}",
343-
fmt::from_fn(|f| everything_else
344-
.lines()
345-
.map(|line| fmt::from_fn(move |f| write!(f, " {line}")))
346-
.joined("\n", f))
347-
)
348-
.unwrap();
349-
} else {
350-
prog.push_str(everything_else);
351-
};
352-
prog.push_str(&main_post);
353-
}
354-
355-
debug!("final doctest:\n{prog}");
412+
Some(WrapperInfo {
413+
before: main_pre,
414+
after: main_post,
415+
returns_result,
416+
insert_indent_space: opts.insert_indent_space,
417+
})
418+
};
356419

357-
(prog, line_offset)
420+
(
421+
DocTestWrapResult::Valid {
422+
code: processed_code.to_string(),
423+
wrapper,
424+
crate_level_code,
425+
},
426+
line_offset,
427+
)
358428
}
359429
}
360430

0 commit comments

Comments
 (0)