Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rustdoc] Add --extract-doctests command-line flag #134531

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
Loading