about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGuillaume Gomez <guillaume1.gomez@gmail.com>2025-01-27 16:48:11 +0100
committerGuillaume Gomez <guillaume1.gomez@gmail.com>2025-01-29 13:57:27 +0100
commit0323af93b4fa4c816fcd400c5171d7c723c1fe18 (patch)
treec4e3ed0726cda0917c6c2b0c2e6890b31e0ffaec
parentccc9ba5c30c675824e9ca62b960830ff4a1858ea (diff)
downloadrust-0323af93b4fa4c816fcd400c5171d7c723c1fe18.tar.gz
rust-0323af93b4fa4c816fcd400c5171d7c723c1fe18.zip
Add new output-format
-rw-r--r--src/librustdoc/config.rs14
-rw-r--r--src/librustdoc/doctest.rs77
-rw-r--r--src/librustdoc/html/markdown.rs16
-rw-r--r--src/librustdoc/lib.rs10
4 files changed, 105 insertions, 12 deletions
diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs
index 80bc6cebd2a..91f27166e47 100644
--- a/src/librustdoc/config.rs
+++ b/src/librustdoc/config.rs
@@ -33,6 +33,7 @@ pub(crate) enum OutputFormat {
     Json,
     #[default]
     Html,
+    Doctest,
 }
 
 impl OutputFormat {
@@ -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}`")),
         }
     }
@@ -446,12 +448,20 @@ impl Options {
         }
 
         // check for `--output-format=json`
-        if !matches!(matches.opt_str("output-format").as_deref(), None | Some("html"))
+        if let Some(format) = matches.opt_str("output-format").as_deref()
+            && format != "html"
             && !matches.opt_present("show-coverage")
             && !nightly_options::is_unstable_enabled(matches)
         {
+            let extra = if format == "json" {
+                " (see https://github.com/rust-lang/rust/issues/76578)"
+            } else {
+                ""
+            };
             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)",
+                format!(
+                    "the -Z unstable-options flag must be passed to enable --output-format for documentation generation{extra}",
+                ),
             );
         }
 
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs
index 8c3e28ecec3..48fe41c8b46 100644
--- a/src/librustdoc/doctest.rs
+++ b/src/librustdoc/doctest.rs
@@ -26,11 +26,12 @@ use rustc_span::FileName;
 use rustc_span::edition::Edition;
 use rustc_span::symbol::sym;
 use rustc_target::spec::{Target, TargetTuple};
+use serde::{Serialize, Serializer};
 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;
 
@@ -133,6 +134,14 @@ fn get_doctest_dir() -> io::Result<TempDir> {
     TempFileBuilder::new().prefix("rustdoctest").tempdir()
 }
 
+#[derive(Serialize)]
+struct ExtractedDoctest {
+    /// `None` if the code syntax is invalid.
+    doctest_code: Option<String>,
+    #[serde(flatten)] // We make all `ScrapedDocTest` fields at the same level as `doctest_code`.
+    scraped_test: ScrapedDocTest,
+}
+
 pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions) {
     let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name;
 
@@ -209,6 +218,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.output_format == OutputFormat::Doctest;
     let CreateRunnableDocTests {
         standalone_tests,
         mergeable_tests,
@@ -217,7 +227,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| {
@@ -226,21 +236,64 @@ 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 extracted = tests
+                    .into_iter()
+                    .map(|scraped_test| {
+                        let edition = scraped_test.edition(&options);
+                        let doctest = DocTestBuilder::new(
+                            &scraped_test.text,
+                            Some(&opts.crate_name),
+                            edition,
+                            false,
+                            None,
+                            Some(&scraped_test.langstr),
+                        );
+                        let (full_test_code, size) = doctest.generate_unique_doctest(
+                            &scraped_test.text,
+                            scraped_test.langstr.test_harness,
+                            &opts,
+                            Some(&opts.crate_name),
+                        );
+                        ExtractedDoctest {
+                            doctest_code: if size != 0 { Some(full_test_code) } else { None },
+                            scraped_test,
+                        }
+                    })
+                    .collect::<Vec<_>>();
+
+                let stdout = std::io::stdout();
+                let mut stdout = stdout.lock();
+                if let Err(error) = serde_json::ser::to_writer(&mut stdout, &extracted) {
+                    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
-    });
+    }) {
+        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);
 
@@ -752,6 +805,14 @@ impl IndividualTestOptions {
     }
 }
 
+fn filename_to_string<S: Serializer>(
+    filename: &FileName,
+    serializer: S,
+) -> Result<S::Ok, S::Error> {
+    let filename = filename.prefer_remapped_unconditionaly().to_string();
+    serializer.serialize_str(&filename)
+}
+
 /// A doctest scraped from the code, ready to be turned into a runnable test.
 ///
 /// The pipeline goes: [`clean`] AST -> `ScrapedDoctest` -> `RunnableDoctest`.
@@ -761,10 +822,14 @@ impl IndividualTestOptions {
 /// [`clean`]: crate::clean
 /// [`run_merged_tests`]: crate::doctest::runner::DocTestRunner::run_merged_tests
 /// [`generate_unique_doctest`]: crate::doctest::make::DocTestBuilder::generate_unique_doctest
+#[derive(Serialize)]
 pub(crate) struct ScrapedDocTest {
+    #[serde(serialize_with = "filename_to_string")]
     filename: FileName,
     line: usize,
+    #[serde(rename = "doctest_attributes")]
     langstr: LangString,
+    #[serde(rename = "original_code")]
     text: String,
     name: String,
 }
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 7e835585b73..7b4ed7a4d47 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::{Serialize, Serializer};
 use tracing::{debug, trace};
 
 use crate::clean::RenderedLink;
@@ -820,7 +821,17 @@ impl<'tcx> ExtraInfo<'tcx> {
     }
 }
 
-#[derive(Eq, PartialEq, Clone, Debug)]
+fn edition_to_string<S: Serializer>(
+    edition: &Option<Edition>,
+    serializer: S,
+) -> Result<S::Ok, S::Error> {
+    match edition {
+        Some(edition) => serializer.serialize_some(&edition.to_string()),
+        None => serializer.serialize_none(),
+    }
+}
+
+#[derive(Eq, PartialEq, Clone, Debug, Serialize)]
 pub(crate) struct LangString {
     pub(crate) original: String,
     pub(crate) should_panic: bool,
@@ -831,12 +842,13 @@ pub(crate) struct LangString {
     pub(crate) compile_fail: bool,
     pub(crate) standalone_crate: bool,
     pub(crate) error_codes: Vec<String>,
+    #[serde(serialize_with = "edition_to_string")]
     pub(crate) edition: Option<Edition>,
     pub(crate) added_classes: Vec<String>,
     pub(crate) unknown: Vec<String>,
 }
 
-#[derive(Eq, PartialEq, Clone, Debug)]
+#[derive(Eq, PartialEq, Clone, Debug, Serialize)]
 pub(crate) enum Ignore {
     All,
     None,
diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs
index bb954a31891..44adf92ff0e 100644
--- a/src/librustdoc/lib.rs
+++ b/src/librustdoc/lib.rs
@@ -814,7 +814,12 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) {
         }
     };
 
-    match (options.should_test, config::markdown_input(&input)) {
+    let output_format = options.output_format;
+
+    match (
+        options.should_test || output_format == config::OutputFormat::Doctest,
+        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)) => {
@@ -849,7 +854,6 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) {
     // plug/cleaning passes.
     let crate_version = options.crate_version.clone();
 
-    let output_format = options.output_format;
     let scrape_examples_options = options.scrape_examples_options.clone();
     let bin_crate = options.bin_crate;
 
@@ -899,6 +903,8 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) {
                 config::OutputFormat::Json => sess.time("render_json", || {
                     run_renderer::<json::JsonRenderer<'_>>(krate, render_opts, cache, tcx)
                 }),
+                // Already handled above with doctest runners.
+                config::OutputFormat::Doctest => unreachable!(),
             }
         })
     })