From 0323af93b4fa4c816fcd400c5171d7c723c1fe18 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 27 Jan 2025 16:48:11 +0100 Subject: [PATCH 1/7] Add new output-format --- src/librustdoc/config.rs | 14 +++++- src/librustdoc/doctest.rs | 77 ++++++++++++++++++++++++++++++--- src/librustdoc/html/markdown.rs | 16 ++++++- src/librustdoc/lib.rs | 10 ++++- 4 files changed, 105 insertions(+), 12 deletions(-) diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 80bc6cebd2aa9..91f27166e471b 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 8c3e28ecec38c..48fe41c8b46d0 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 { TempFileBuilder::new().prefix("rustdoctest").tempdir() } +#[derive(Serialize)] +struct ExtractedDoctest { + /// `None` if the code syntax is invalid. + doctest_code: Option, + #[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::>(); + + 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( + filename: &FileName, + serializer: S, +) -> Result { + 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 7e835585b73e8..7b4ed7a4d4771 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( + edition: &Option, + serializer: S, +) -> Result { + 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, + #[serde(serialize_with = "edition_to_string")] pub(crate) edition: Option, pub(crate) added_classes: Vec, pub(crate) unknown: Vec, } -#[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 bb954a31891ad..44adf92ff0eef 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::>(krate, render_opts, cache, tcx) }), + // Already handled above with doctest runners. + config::OutputFormat::Doctest => unreachable!(), } }) }) From 7fa2094cb1bfccc37fa93baf8a45eb6ca24d43c4 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 27 Jan 2025 17:14:35 +0100 Subject: [PATCH 2/7] Add ui test for new rustdoc `--output-format=doctest` option --- 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..06bd35969d0c4 --- /dev/null +++ b/tests/rustdoc-ui/extract-doctests.rs @@ -0,0 +1,15 @@ +// Test to ensure that it generates expected output for `--output-format=doctest` command-line +// flag. + +//@ compile-flags:-Z unstable-options --output-format=doctest +//@ normalize-stdout: "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..9d42af3044964 --- /dev/null +++ b/tests/rustdoc-ui/extract-doctests.stdout @@ -0,0 +1 @@ +[{"doctest_code":"#![allow(unused)]\nfn main() {\nlet x = 12;\nlet y = 14;\n}","filename":"$DIR/extract-doctests.rs","line":8,"doctest_attributes":{"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":[]},"original_code":"let x = 12;\nlet y = 14;","name":"$DIR/extract-doctests.rs - (line 8)"},{"doctest_code":"#![allow(unused)]\nfn main() {\nlet\n}","filename":"$DIR/extract-doctests.rs","line":13,"doctest_attributes":{"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":[]},"original_code":"let","name":"$DIR/extract-doctests.rs - (line 13)"}] \ No newline at end of file From 43bf52989a86b7eaa4ebac68dcac9ca627f5cf54 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 27 Jan 2025 17:51:01 +0100 Subject: [PATCH 3/7] Move extracted doctest code and types into its own file --- src/librustdoc/doctest.rs | 50 +-------- src/librustdoc/doctest/extracted.rs | 132 +++++++++++++++++++++++ src/librustdoc/html/markdown.rs | 16 +-- tests/rustdoc-ui/extract-doctests.stdout | 2 +- 4 files changed, 139 insertions(+), 61 deletions(-) create mode 100644 src/librustdoc/doctest/extracted.rs diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 48fe41c8b46d0..46d0776699ace 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -1,3 +1,4 @@ +mod extracted; mod make; mod markdown; mod runner; @@ -26,7 +27,6 @@ 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; @@ -134,14 +134,6 @@ fn get_doctest_dir() -> io::Result { TempFileBuilder::new().prefix("rustdoctest").tempdir() } -#[derive(Serialize)] -struct ExtractedDoctest { - /// `None` if the code syntax is invalid. - doctest_code: Option, - #[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; @@ -243,34 +235,12 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions ); let tests = hir_collector.collect_crate(); 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::>(); + 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, &extracted) { + if let Err(error) = serde_json::ser::to_writer(&mut stdout, &collector) { eprintln!(); Err(format!("Failed to generate JSON output for doctests: {error:?}")) } else { @@ -805,14 +775,6 @@ impl IndividualTestOptions { } } -fn filename_to_string( - filename: &FileName, - serializer: S, -) -> Result { - 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`. @@ -822,14 +784,10 @@ fn filename_to_string( /// [`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/doctest/extracted.rs b/src/librustdoc/doctest/extracted.rs new file mode 100644 index 0000000000000..b45cc907635f3 --- /dev/null +++ b/src/librustdoc/doctest/extracted.rs @@ -0,0 +1,132 @@ +use serde::Serialize; + +use super::{DocTestBuilder, ScrapedDocTest}; +use crate::config::Options as RustdocOptions; +use crate::html::markdown; + +const FORMAT_VERSION: u32 = 1; + +#[derive(Serialize)] +pub(crate) struct ExtractedDocTests { + #[allow(non_snake_case)] + format_version: u32, + doctests: Vec, +} + +impl ExtractedDocTests { + pub(crate) fn new() -> Self { + Self { format_version: FORMAT_VERSION, doctests: Vec::new() } + } + + pub(crate) fn add_test( + &mut self, + scraped_test: ScrapedDocTest, + opts: &super::GlobalTestOptions, + options: &RustdocOptions, + ) { + let edition = scraped_test.edition(&options); + + let ScrapedDocTest { filename, line, langstr, text, name } = scraped_test; + + let doctest = DocTestBuilder::new( + &text, + Some(&opts.crate_name), + edition, + false, + None, + Some(&langstr), + ); + let (full_test_code, size) = doctest.generate_unique_doctest( + &text, + langstr.test_harness, + &opts, + Some(&opts.crate_name), + ); + self.doctests.push(ExtractedDocTest { + file: filename.prefer_remapped_unconditionaly().to_string(), + line, + doctest_attributes: langstr.into(), + doctest_code: if size != 0 { Some(full_test_code) } else { None }, + original_code: text, + name, + }); + } +} + +#[derive(Serialize)] +pub(crate) struct ExtractedDocTest { + file: String, + line: usize, + doctest_attributes: LangString, + original_code: String, + /// `None` if the code syntax is invalid. + doctest_code: Option, + name: String, +} + +#[derive(Serialize)] +pub(crate) enum Ignore { + All, + None, + Some(Vec), +} + +impl From for Ignore { + fn from(original: markdown::Ignore) -> Self { + match original { + markdown::Ignore::All => Self::All, + markdown::Ignore::None => Self::None, + markdown::Ignore::Some(values) => Self::Some(values), + } + } +} + +#[derive(Serialize)] +struct LangString { + pub(crate) original: String, + pub(crate) should_panic: bool, + pub(crate) no_run: bool, + pub(crate) ignore: Ignore, + pub(crate) rust: bool, + pub(crate) test_harness: bool, + pub(crate) compile_fail: bool, + pub(crate) standalone_crate: bool, + pub(crate) error_codes: Vec, + pub(crate) edition: Option, + pub(crate) added_css_classes: Vec, + pub(crate) unknown: Vec, +} + +impl From for LangString { + fn from(original: markdown::LangString) -> Self { + let markdown::LangString { + original, + should_panic, + no_run, + ignore, + rust, + test_harness, + compile_fail, + standalone_crate, + error_codes, + edition, + added_classes, + unknown, + } = original; + + Self { + original, + should_panic, + no_run, + ignore: ignore.into(), + rust, + test_harness, + compile_fail, + standalone_crate, + error_codes, + edition: edition.map(|edition| edition.to_string()), + added_css_classes: added_classes, + unknown, + } + } +} diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 7b4ed7a4d4771..7e835585b73e8 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -46,7 +46,6 @@ 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; @@ -821,17 +820,7 @@ impl<'tcx> ExtraInfo<'tcx> { } } -fn edition_to_string( - edition: &Option, - serializer: S, -) -> Result { - match edition { - Some(edition) => serializer.serialize_some(&edition.to_string()), - None => serializer.serialize_none(), - } -} - -#[derive(Eq, PartialEq, Clone, Debug, Serialize)] +#[derive(Eq, PartialEq, Clone, Debug)] pub(crate) struct LangString { pub(crate) original: String, pub(crate) should_panic: bool, @@ -842,13 +831,12 @@ pub(crate) struct LangString { pub(crate) compile_fail: bool, pub(crate) standalone_crate: bool, pub(crate) error_codes: Vec, - #[serde(serialize_with = "edition_to_string")] pub(crate) edition: Option, pub(crate) added_classes: Vec, pub(crate) unknown: Vec, } -#[derive(Eq, PartialEq, Clone, Debug, Serialize)] +#[derive(Eq, PartialEq, Clone, Debug)] pub(crate) enum Ignore { All, None, diff --git a/tests/rustdoc-ui/extract-doctests.stdout b/tests/rustdoc-ui/extract-doctests.stdout index 9d42af3044964..fa8604cae948a 100644 --- a/tests/rustdoc-ui/extract-doctests.stdout +++ b/tests/rustdoc-ui/extract-doctests.stdout @@ -1 +1 @@ -[{"doctest_code":"#![allow(unused)]\nfn main() {\nlet x = 12;\nlet y = 14;\n}","filename":"$DIR/extract-doctests.rs","line":8,"doctest_attributes":{"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":[]},"original_code":"let x = 12;\nlet y = 14;","name":"$DIR/extract-doctests.rs - (line 8)"},{"doctest_code":"#![allow(unused)]\nfn main() {\nlet\n}","filename":"$DIR/extract-doctests.rs","line":13,"doctest_attributes":{"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":[]},"original_code":"let","name":"$DIR/extract-doctests.rs - (line 13)"}] \ No newline at end of file +{"format_version":1,"doctests":[{"file":"$DIR/extract-doctests.rs","line":8,"doctest_attributes":{"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_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nlet y = 14;","doctest_code":"#![allow(unused)]\nfn main() {\nlet x = 12;\nlet y = 14;\n}","name":"$DIR/extract-doctests.rs - (line 8)"},{"file":"$DIR/extract-doctests.rs","line":13,"doctest_attributes":{"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_css_classes":[],"unknown":[]},"original_code":"let","doctest_code":"#![allow(unused)]\nfn main() {\nlet\n}","name":"$DIR/extract-doctests.rs - (line 13)"}]} \ No newline at end of file From 5e9e27e21b6363bfe8eab98f4aaa1ebb00a0f2f6 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 27 Jan 2025 17:55:29 +0100 Subject: [PATCH 4/7] Mention the tracking issue of `--output-format=doctest` --- src/librustdoc/config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 91f27166e471b..aa313af94ca77 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -455,6 +455,8 @@ impl Options { { let extra = if format == "json" { " (see https://github.com/rust-lang/rust/issues/76578)" + } else if format == "doctest" { + " (see https://github.com/rust-lang/rust/issues/134529)" } else { "" }; From 07c878910bd2243b67549808dc65e24a3e9c9528 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 27 Jan 2025 18:05:35 +0100 Subject: [PATCH 5/7] Add documentation for `--output-format=doctest` --- src/doc/rustdoc/src/unstable-features.md | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index d1d42e4732258..7eb1b8df2333d 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -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. @@ -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) From a5d66e07e1775e3d5a9a8bb073fc50bf03101198 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 29 Jan 2025 14:18:37 +0100 Subject: [PATCH 6/7] Improve code and add missing docs for new `doctest::extracted` module --- src/librustdoc/config.rs | 10 ++++------ src/librustdoc/doctest.rs | 22 ++++++++++++---------- src/librustdoc/doctest/extracted.rs | 11 ++++++++++- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index aa313af94ca77..1ba9dcaac1d2f 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -453,12 +453,10 @@ impl Options { && !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 if format == "doctest" { - " (see https://github.com/rust-lang/rust/issues/134529)" - } else { - "" + let extra = match format { + "json" => " (see https://github.com/rust-lang/rust/issues/76578)", + "doctest" => " (see https://github.com/rust-lang/rust/issues/134529)", + _ => "", }; dcx.fatal( format!( diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 46d0776699ace..8b522e614b813 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -211,15 +211,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions crate::wrap_return(dcx, generate_args_file(&args_path, &options)); let extract_doctests = options.output_format == OutputFormat::Doctest; - let CreateRunnableDocTests { - standalone_tests, - mergeable_tests, - rustdoc_options, - opts, - unused_extern_reports, - compiling_test_count, - .. - } = match interface::run_compiler(config, |compiler| { + 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| { @@ -256,7 +248,17 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions 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) => { diff --git a/src/librustdoc/doctest/extracted.rs b/src/librustdoc/doctest/extracted.rs index b45cc907635f3..03c8814a4c960 100644 --- a/src/librustdoc/doctest/extracted.rs +++ b/src/librustdoc/doctest/extracted.rs @@ -1,14 +1,23 @@ +//! Rustdoc's doctest extraction. +//! +//! This module contains the logic to extract doctests and output a JSON containing this +//! information. + use serde::Serialize; use super::{DocTestBuilder, ScrapedDocTest}; use crate::config::Options as RustdocOptions; use crate::html::markdown; +/// The version of JSON output that this code generates. +/// +/// This integer is incremented with every breaking change to the API, +/// and is returned along with the JSON blob into the `format_version` root field. +/// Consuming code should assert that this value matches the format version(s) that it supports. const FORMAT_VERSION: u32 = 1; #[derive(Serialize)] pub(crate) struct ExtractedDocTests { - #[allow(non_snake_case)] format_version: u32, doctests: Vec, } From b7951380ca9e8eddcd67bf947b592d10e21bdea5 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 29 Jan 2025 15:05:21 +0100 Subject: [PATCH 7/7] Improve check for `--output-format` combinations and add ui regression test --- src/librustdoc/config.rs | 66 ++++++++++++++------------ tests/rustdoc-ui/coverage/html.stderr | 2 +- tests/rustdoc-ui/doctest-output.rs | 1 + tests/rustdoc-ui/doctest-output.stderr | 2 + 4 files changed, 39 insertions(+), 32 deletions(-) create mode 100644 tests/rustdoc-ui/doctest-output.rs create mode 100644 tests/rustdoc-ui/doctest-output.stderr diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 1ba9dcaac1d2f..cfba78952085c 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -447,22 +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 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 = match format { - "json" => " (see https://github.com/rust-lang/rust/issues/76578)", - "doctest" => " (see https://github.com/rust-lang/rust/issues/134529)", - _ => "", - }; - dcx.fatal( - format!( - "the -Z unstable-options flag must be passed to enable --output-format for documentation generation{extra}", - ), - ); + 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"); @@ -714,8 +734,6 @@ 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) => { @@ -723,20 +741,6 @@ impl Options { } }; - 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); diff --git a/tests/rustdoc-ui/coverage/html.stderr b/tests/rustdoc-ui/coverage/html.stderr index adca375d4bce5..764179820c5ba 100644 --- a/tests/rustdoc-ui/coverage/html.stderr +++ b/tests/rustdoc-ui/coverage/html.stderr @@ -1,2 +1,2 @@ -error: html output format isn't supported for the --show-coverage option +error: `--output-format=html` is not supported for the `--show-coverage` option diff --git a/tests/rustdoc-ui/doctest-output.rs b/tests/rustdoc-ui/doctest-output.rs new file mode 100644 index 0000000000000..720f2952980ed --- /dev/null +++ b/tests/rustdoc-ui/doctest-output.rs @@ -0,0 +1 @@ +//@ compile-flags:-Z unstable-options --show-coverage --output-format=doctest diff --git a/tests/rustdoc-ui/doctest-output.stderr b/tests/rustdoc-ui/doctest-output.stderr new file mode 100644 index 0000000000000..20c618dc61b16 --- /dev/null +++ b/tests/rustdoc-ui/doctest-output.stderr @@ -0,0 +1,2 @@ +error: `--output-format=doctest` is not supported for the `--show-coverage` option +