Skip to content

Commit

Permalink
Rollup merge of rust-lang#134531 - GuillaumeGomez:extract-doctests, r…
Browse files Browse the repository at this point in the history
…=notriddle,aDotInTheVoid

[rustdoc] Add `--extract-doctests` command-line flag

Part of rust-lang#134529.

It was discussed with the Rust-for-Linux project recently that they needed a way to extract doctests so they can modify them and then run them more easily (look for "a way to extract doctests" [here](Rust-for-Linux/linux#2)).

For now, I output most of `ScrapedDoctest` fields in JSON format with `serde_json`. So it outputs the following information:

 * filename
 * line
 * langstr
 * text

cc `@ojeda`
r? `@notriddle`
  • Loading branch information
matthiaskrgr authored Jan 31, 2025
2 parents 25a1657 + b795138 commit 86595e4
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 39 deletions.
64 changes: 64 additions & 0 deletions src/doc/rustdoc/src/unstable-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,8 @@ use `-o -`.

## `-w`/`--output-format`: output format

### json

`--output-format json` emits documentation in the experimental
[JSON format](https://doc.rust-lang.org/nightly/nightly-rustc/rustdoc_json_types/). `--output-format html` has no effect,
and is also accepted on stable toolchains.
Expand All @@ -542,6 +544,68 @@ It can also be used with `--show-coverage`. Take a look at its
[documentation](#--show-coverage-calculate-the-percentage-of-items-with-documentation) for more
information.

### doctest

`--output-format doctest` emits JSON on stdout which gives you information about doctests in the
provided crate.

Tracking issue: [#134529](https://github.com/rust-lang/rust/issues/134529)

You can use this option like this:

```bash
rustdoc -Zunstable-options --output-format=doctest src/lib.rs
```

For this rust code:

```rust
/// ```
/// let x = 12;
/// ```
pub trait Trait {}
```

The generated output (formatted) will look like this:

```json
{
"format_version": 1,
"doctests": [
{
"file": "foo.rs",
"line": 1,
"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;",
"doctest_code": "#![allow(unused)]\nfn main() {\nlet x = 12;\n}",
"name": "foo.rs - Trait (line 1)"
}
]
}
```

* `format_version` gives you the current version of the generated JSON. If we change the output in any way, the number will increase.
* `doctests` contains the list of doctests present in the crate.
* `file` is the file path where the doctest is located.
* `line` is the line where the doctest starts (so where the \`\`\` is located in the current code).
* `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).
* `original_code` is the code as written in the source code before rustdoc modifies it.
* `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.
* `name` is the name generated by rustdoc which represents this doctest.

## `--enable-per-target-ignores`: allow `ignore-foo` style filters for doctests

* Tracking issue: [#64245](https://github.com/rust-lang/rust/issues/64245)
Expand Down
60 changes: 37 additions & 23 deletions src/librustdoc/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub(crate) enum OutputFormat {
Json,
#[default]
Html,
Doctest,
}

impl OutputFormat {
Expand All @@ -48,6 +49,7 @@ impl TryFrom<&str> for OutputFormat {
match value {
"json" => Ok(OutputFormat::Json),
"html" => Ok(OutputFormat::Html),
"doctest" => Ok(OutputFormat::Doctest),
_ => Err(format!("unknown output format `{value}`")),
}
}
Expand Down Expand Up @@ -445,14 +447,42 @@ impl Options {
}
}

let show_coverage = matches.opt_present("show-coverage");
let output_format_s = matches.opt_str("output-format");
let output_format = match output_format_s {
Some(ref s) => match OutputFormat::try_from(s.as_str()) {
Ok(out_fmt) => out_fmt,
Err(e) => dcx.fatal(e),
},
None => OutputFormat::default(),
};

// check for `--output-format=json`
if !matches!(matches.opt_str("output-format").as_deref(), None | Some("html"))
&& !matches.opt_present("show-coverage")
&& !nightly_options::is_unstable_enabled(matches)
{
dcx.fatal(
"the -Z unstable-options flag must be passed to enable --output-format for documentation generation (see https://github.com/rust-lang/rust/issues/76578)",
);
match (
output_format_s.as_ref().map(|_| output_format),
show_coverage,
nightly_options::is_unstable_enabled(matches),
) {
(None | Some(OutputFormat::Json), true, _) => {}
(_, true, _) => {
dcx.fatal(format!(
"`--output-format={}` is not supported for the `--show-coverage` option",
output_format_s.unwrap_or_default(),
));
}
// If `-Zunstable-options` is used, nothing to check after this point.
(_, false, true) => {}
(None | Some(OutputFormat::Html), false, _) => {}
(Some(OutputFormat::Json), false, false) => {
dcx.fatal(
"the -Z unstable-options flag must be passed to enable --output-format for documentation generation (see https://github.com/rust-lang/rust/issues/76578)",
);
}
(Some(OutputFormat::Doctest), false, false) => {
dcx.fatal(
"the -Z unstable-options flag must be passed to enable --output-format for documentation generation (see https://github.com/rust-lang/rust/issues/134529)",
);
}
}

let to_check = matches.opt_strs("check-theme");
Expand Down Expand Up @@ -704,29 +734,13 @@ impl Options {
})
.collect();

let show_coverage = matches.opt_present("show-coverage");

let crate_types = match parse_crate_types_from_list(matches.opt_strs("crate-type")) {
Ok(types) => types,
Err(e) => {
dcx.fatal(format!("unknown crate type: {e}"));
}
};

let output_format = match matches.opt_str("output-format") {
Some(s) => match OutputFormat::try_from(s.as_str()) {
Ok(out_fmt) => {
if !out_fmt.is_json() && show_coverage {
dcx.fatal(
"html output format isn't supported for the --show-coverage option",
);
}
out_fmt
}
Err(e) => dcx.fatal(e),
},
None => OutputFormat::default(),
};
let crate_name = matches.opt_str("crate-name");
let bin_crate = crate_types.contains(&CrateType::Executable);
let proc_macro_crate = crate_types.contains(&CrateType::ProcMacro);
Expand Down
51 changes: 38 additions & 13 deletions src/librustdoc/doctest.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod extracted;
mod make;
mod markdown;
mod runner;
Expand Down Expand Up @@ -30,7 +31,7 @@ use tempfile::{Builder as TempFileBuilder, TempDir};
use tracing::debug;

use self::rust::HirCollector;
use crate::config::Options as RustdocOptions;
use crate::config::{Options as RustdocOptions, OutputFormat};
use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine};
use crate::lint::init_lints;

Expand Down Expand Up @@ -209,15 +210,8 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
let args_path = temp_dir.path().join("rustdoc-cfgs");
crate::wrap_return(dcx, generate_args_file(&args_path, &options));

let CreateRunnableDocTests {
standalone_tests,
mergeable_tests,
rustdoc_options,
opts,
unused_extern_reports,
compiling_test_count,
..
} = interface::run_compiler(config, |compiler| {
let extract_doctests = options.output_format == OutputFormat::Doctest;
let result = interface::run_compiler(config, |compiler| {
let krate = rustc_interface::passes::parse(&compiler.sess);

let collector = rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| {
Expand All @@ -226,22 +220,53 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
let opts = scrape_test_config(crate_name, crate_attrs, args_path);
let enable_per_target_ignores = options.enable_per_target_ignores;

let mut collector = CreateRunnableDocTests::new(options, opts);
let hir_collector = HirCollector::new(
ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
enable_per_target_ignores,
tcx,
);
let tests = hir_collector.collect_crate();
tests.into_iter().for_each(|t| collector.add_test(t));
if extract_doctests {
let mut collector = extracted::ExtractedDocTests::new();
tests.into_iter().for_each(|t| collector.add_test(t, &opts, &options));

let stdout = std::io::stdout();
let mut stdout = stdout.lock();
if let Err(error) = serde_json::ser::to_writer(&mut stdout, &collector) {
eprintln!();
Err(format!("Failed to generate JSON output for doctests: {error:?}"))
} else {
Ok(None)
}
} else {
let mut collector = CreateRunnableDocTests::new(options, opts);
tests.into_iter().for_each(|t| collector.add_test(t));

collector
Ok(Some(collector))
}
});
compiler.sess.dcx().abort_if_errors();

collector
});

let CreateRunnableDocTests {
standalone_tests,
mergeable_tests,
rustdoc_options,
opts,
unused_extern_reports,
compiling_test_count,
..
} = match result {
Ok(Some(collector)) => collector,
Ok(None) => return,
Err(error) => {
eprintln!("{error}");
std::process::exit(1);
}
};

run_tests(opts, &rustdoc_options, &unused_extern_reports, standalone_tests, mergeable_tests);

let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
Expand Down
Loading

0 comments on commit 86595e4

Please sign in to comment.