1
Fork 0

Add new output-format

This commit is contained in:
Guillaume Gomez 2025-01-27 16:48:11 +01:00
parent ccc9ba5c30
commit 0323af93b4
4 changed files with 105 additions and 12 deletions

View file

@ -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}",
),
);
}

View file

@ -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<_>>();
collector
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));
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,
}

View file

@ -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,

View file

@ -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!(),
}
})
})