From 5afc1c5280284684ae0a45d6857592c11ebd4ace Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 19 Dec 2024 19:50:14 +0100 Subject: [PATCH 1/4] Add `--extract-doctests` command-line flag --- src/librustdoc/config.rs | 5 ++++ src/librustdoc/doctest.rs | 43 ++++++++++++++++++++++++++++++--- src/librustdoc/html/markdown.rs | 31 +++++++++++++++++++++++- src/librustdoc/lib.rs | 3 ++- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 34c91e33db700..fed446da5cff5 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -172,6 +172,9 @@ pub(crate) struct Options { /// This is mainly useful for other tools that reads that debuginfo to figure out /// how to call the compiler with the same arguments. pub(crate) expanded_args: Vec, + + /// If `true`, it will doctest in JSON format and exit. + pub(crate) extract_doctests: bool, } impl fmt::Debug for Options { @@ -762,6 +765,7 @@ impl Options { Ok(result) => result, Err(e) => dcx.fatal(format!("--merge option error: {e}")), }; + let extract_doctests = matches.opt_present("extract-doctests"); if generate_link_to_definition && (show_coverage || output_format != OutputFormat::Html) { dcx.struct_warn( @@ -819,6 +823,7 @@ impl Options { scrape_examples_options, unstable_features, expanded_args: args, + extract_doctests, }; let render_options = RenderOptions { output, diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index ce44cb1c319aa..bed337d301a4f 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -26,6 +26,7 @@ use rustc_span::FileName; use rustc_span::edition::Edition; use rustc_span::symbol::sym; use rustc_target::spec::{Target, TargetTuple}; +use serde::ser::{Serialize, SerializeStruct, Serializer}; use tempfile::{Builder as TempFileBuilder, TempDir}; use tracing::debug; @@ -165,6 +166,7 @@ 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 extract_doctests = options.extract_doctests; let CreateRunnableDocTests { standalone_tests, mergeable_tests, @@ -173,7 +175,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions unused_extern_reports, compiling_test_count, .. - } = interface::run_compiler(config, |compiler| { + } = match 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| { @@ -189,14 +191,30 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions tcx, ); let tests = hir_collector.collect_crate(); + if extract_doctests { + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + if let Err(error) = serde_json::ser::to_writer(&mut stdout, &tests) { + eprintln!(); + return Err(format!("Failed to generate JSON output for doctests: {error:?}")); + } + return Ok(None); + } tests.into_iter().for_each(|t| collector.add_test(t)); - collector + Ok(Some(collector)) }); compiler.sess.dcx().abort_if_errors(); collector - }); + }) { + 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); @@ -725,6 +743,25 @@ pub(crate) struct ScrapedDocTest { name: String, } +// This implementation is needed for 2 reasons: +// 1. `FileName` doesn't implement `serde::Serialize`. +// 2. We don't want to output `name`. +impl Serialize for ScrapedDocTest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // `5` is the number of fields we output (so all of them except `name`). + let mut s = serializer.serialize_struct("ScrapedDocTest", 4)?; + let filename = self.filename.prefer_remapped_unconditionaly().to_string(); + s.serialize_field("filename", &filename)?; + s.serialize_field("line", &self.line)?; + s.serialize_field("langstr", &self.langstr)?; + s.serialize_field("text", &self.text)?; + s.end() + } +} + impl ScrapedDocTest { fn new( filename: FileName, diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index aa8fdaaee4cb8..3313cac77223c 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -46,6 +46,7 @@ pub(crate) use rustc_resolve::rustdoc::main_body_opts; use rustc_resolve::rustdoc::may_be_doc_link; use rustc_span::edition::Edition; use rustc_span::{Span, Symbol}; +use serde::ser::{Serialize, SerializeStruct, Serializer}; use tracing::{debug, trace}; use crate::clean::RenderedLink; @@ -836,7 +837,35 @@ pub(crate) struct LangString { pub(crate) unknown: Vec, } -#[derive(Eq, PartialEq, Clone, Debug)] +// This implementation is needed for `Edition` which doesn't implement `serde::Serialize` so +// we need to implement it manually. +impl Serialize for LangString { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // `12` is the number of fields. + let mut s = serializer.serialize_struct("LangString", 12)?; + + s.serialize_field("original", &self.original)?; + s.serialize_field("should_panic", &self.should_panic)?; + s.serialize_field("no_run", &self.no_run)?; + s.serialize_field("ignore", &self.ignore)?; + s.serialize_field("rust", &self.rust)?; + s.serialize_field("test_harness", &self.test_harness)?; + s.serialize_field("compile_fail", &self.compile_fail)?; + s.serialize_field("standalone_crate", &self.standalone_crate)?; + s.serialize_field("error_codes", &self.error_codes)?; + let edition = self.edition.map(|edition| edition.to_string()); + s.serialize_field("edition", &edition)?; + s.serialize_field("added_classes", &self.added_classes)?; + s.serialize_field("unknown", &self.unknown)?; + + s.end() + } +} + +#[derive(Eq, PartialEq, Clone, Debug, serde::Serialize)] pub(crate) enum Ignore { All, None, diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 7655c2e0e15e1..ac42b44cde7df 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -685,6 +685,7 @@ fn opts() -> Vec { "[rust]", ), opt(Unstable, Flag, "", "html-no-source", "Disable HTML source code pages generation", ""), + opt(Unstable, Flag, "", "extract-doctests", "Output doctests in JSON format", ""), ] } @@ -804,7 +805,7 @@ fn main_args( } }; - match (options.should_test, config::markdown_input(&input)) { + match (options.should_test | options.extract_doctests, config::markdown_input(&input)) { (true, Some(_)) => return wrap_return(dcx, doctest::test_markdown(&input, options)), (true, None) => return doctest::run(dcx, input, options), (false, Some(md_input)) => { From 5adedd719399b7252332b688d95b1e449285e3b1 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 19 Dec 2024 19:50:36 +0100 Subject: [PATCH 2/4] Add UI test for `--extra-doctests` command-line flag --- tests/rustdoc-ui/extract-doctests.rs | 15 +++++++++++++++ tests/rustdoc-ui/extract-doctests.stdout | 1 + 2 files changed, 16 insertions(+) create mode 100644 tests/rustdoc-ui/extract-doctests.rs create mode 100644 tests/rustdoc-ui/extract-doctests.stdout diff --git a/tests/rustdoc-ui/extract-doctests.rs b/tests/rustdoc-ui/extract-doctests.rs new file mode 100644 index 0000000000000..23b5cbcd255bf --- /dev/null +++ b/tests/rustdoc-ui/extract-doctests.rs @@ -0,0 +1,15 @@ +// Test to ensure that it generates expected output for `--extract-doctests` command-line +// flag. + +//@ compile-flags:-Z unstable-options --extract-doctests +//@ normalize-stdout-test: "tests/rustdoc-ui" -> "$$DIR" +//@ check-pass + +//! ```ignore (checking attributes) +//! let x = 12; +//! let y = 14; +//! ``` +//! +//! ```edition2018,compile_fail +//! let +//! ``` diff --git a/tests/rustdoc-ui/extract-doctests.stdout b/tests/rustdoc-ui/extract-doctests.stdout new file mode 100644 index 0000000000000..eb96dd4309bdd --- /dev/null +++ b/tests/rustdoc-ui/extract-doctests.stdout @@ -0,0 +1 @@ +[{"filename":"$DIR/extract-doctests.rs","line":8,"langstr":{"original":"ignore (checking attributes)","should_panic":false,"no_run":false,"ignore":"All","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_classes":[],"unknown":[]},"text":"let x = 12;\nlet y = 14;"},{"filename":"$DIR/extract-doctests.rs","line":13,"langstr":{"original":"edition2018,compile_fail","should_panic":false,"no_run":true,"ignore":"None","rust":true,"test_harness":false,"compile_fail":true,"standalone_crate":false,"error_codes":[],"edition":"2018","added_classes":[],"unknown":[]},"text":"let"}] \ No newline at end of file From 67a0fef49ed8d741d60bc6a5ede880fdca85d9e9 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 19 Dec 2024 20:03:02 +0100 Subject: [PATCH 3/4] Add documentation for `--extract-doctests --- src/doc/rustdoc/src/unstable-features.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index f19c3a51f619b..f7496c9b1f380 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -664,3 +664,22 @@ Similar to cargo `build.rustc-wrapper` option, this flag takes a `rustc` wrapper The first argument to the program will be the test builder program. This flag can be passed multiple times to nest wrappers. + +### `--extract-doctests`: outputs doctests in JSON format + + * Tracking issue: [#134529](https://github.com/rust-lang/rust/issues/134529) + +When this flag is used, it outputs the doctests original source code alongside information +such as: + + * File where they are located. + * Line where they are located. + * Codeblock attributes (more information about this [here](./write-documentation/documentation-tests.html#attributes)). + +The output format is JSON. + +Using this flag looks like this: + +```bash +$ rustdoc -Zunstable-options --extract-doctests src/lib.rs +``` From 651988d0351a74a3a1c1d8f8e24b3c040cc3a79a Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 20 Dec 2024 15:48:53 +0100 Subject: [PATCH 4/4] Update `run-make/rustdoc-default-output` test --- tests/run-make/rustdoc-default-output/output-default.stdout | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/run-make/rustdoc-default-output/output-default.stdout b/tests/run-make/rustdoc-default-output/output-default.stdout index 125443ce61964..d2e24cad30bc1 100644 --- a/tests/run-make/rustdoc-default-output/output-default.stdout +++ b/tests/run-make/rustdoc-default-output/output-default.stdout @@ -211,6 +211,8 @@ Options: more information --html-no-source Disable HTML source code pages generation + --extract-doctests + Output doctests in JSON format @path Read newline separated options from `path`