Skip to content

Commit 5afc1c5

Browse files
Add --extract-doctests command-line flag
1 parent 3bf62cc commit 5afc1c5

File tree

4 files changed

+77
-5
lines changed

4 files changed

+77
-5
lines changed

src/librustdoc/config.rs

+5
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ pub(crate) struct Options {
172172
/// This is mainly useful for other tools that reads that debuginfo to figure out
173173
/// how to call the compiler with the same arguments.
174174
pub(crate) expanded_args: Vec<String>,
175+
176+
/// If `true`, it will doctest in JSON format and exit.
177+
pub(crate) extract_doctests: bool,
175178
}
176179

177180
impl fmt::Debug for Options {
@@ -762,6 +765,7 @@ impl Options {
762765
Ok(result) => result,
763766
Err(e) => dcx.fatal(format!("--merge option error: {e}")),
764767
};
768+
let extract_doctests = matches.opt_present("extract-doctests");
765769

766770
if generate_link_to_definition && (show_coverage || output_format != OutputFormat::Html) {
767771
dcx.struct_warn(
@@ -819,6 +823,7 @@ impl Options {
819823
scrape_examples_options,
820824
unstable_features,
821825
expanded_args: args,
826+
extract_doctests,
822827
};
823828
let render_options = RenderOptions {
824829
output,

src/librustdoc/doctest.rs

+40-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use rustc_span::FileName;
2626
use rustc_span::edition::Edition;
2727
use rustc_span::symbol::sym;
2828
use rustc_target::spec::{Target, TargetTuple};
29+
use serde::ser::{Serialize, SerializeStruct, Serializer};
2930
use tempfile::{Builder as TempFileBuilder, TempDir};
3031
use tracing::debug;
3132

@@ -165,6 +166,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
165166
let args_path = temp_dir.path().join("rustdoc-cfgs");
166167
crate::wrap_return(dcx, generate_args_file(&args_path, &options));
167168

169+
let extract_doctests = options.extract_doctests;
168170
let CreateRunnableDocTests {
169171
standalone_tests,
170172
mergeable_tests,
@@ -173,7 +175,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
173175
unused_extern_reports,
174176
compiling_test_count,
175177
..
176-
} = interface::run_compiler(config, |compiler| {
178+
} = match interface::run_compiler(config, |compiler| {
177179
let krate = rustc_interface::passes::parse(&compiler.sess);
178180

179181
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
189191
tcx,
190192
);
191193
let tests = hir_collector.collect_crate();
194+
if extract_doctests {
195+
let stdout = std::io::stdout();
196+
let mut stdout = stdout.lock();
197+
if let Err(error) = serde_json::ser::to_writer(&mut stdout, &tests) {
198+
eprintln!();
199+
return Err(format!("Failed to generate JSON output for doctests: {error:?}"));
200+
}
201+
return Ok(None);
202+
}
192203
tests.into_iter().for_each(|t| collector.add_test(t));
193204

194-
collector
205+
Ok(Some(collector))
195206
});
196207
compiler.sess.dcx().abort_if_errors();
197208

198209
collector
199-
});
210+
}) {
211+
Ok(Some(collector)) => collector,
212+
Ok(None) => return,
213+
Err(error) => {
214+
eprintln!("{error}");
215+
std::process::exit(1);
216+
}
217+
};
200218

201219
run_tests(opts, &rustdoc_options, &unused_extern_reports, standalone_tests, mergeable_tests);
202220

@@ -725,6 +743,25 @@ pub(crate) struct ScrapedDocTest {
725743
name: String,
726744
}
727745

746+
// This implementation is needed for 2 reasons:
747+
// 1. `FileName` doesn't implement `serde::Serialize`.
748+
// 2. We don't want to output `name`.
749+
impl Serialize for ScrapedDocTest {
750+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
751+
where
752+
S: Serializer,
753+
{
754+
// `5` is the number of fields we output (so all of them except `name`).
755+
let mut s = serializer.serialize_struct("ScrapedDocTest", 4)?;
756+
let filename = self.filename.prefer_remapped_unconditionaly().to_string();
757+
s.serialize_field("filename", &filename)?;
758+
s.serialize_field("line", &self.line)?;
759+
s.serialize_field("langstr", &self.langstr)?;
760+
s.serialize_field("text", &self.text)?;
761+
s.end()
762+
}
763+
}
764+
728765
impl ScrapedDocTest {
729766
fn new(
730767
filename: FileName,

src/librustdoc/html/markdown.rs

+30-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub(crate) use rustc_resolve::rustdoc::main_body_opts;
4646
use rustc_resolve::rustdoc::may_be_doc_link;
4747
use rustc_span::edition::Edition;
4848
use rustc_span::{Span, Symbol};
49+
use serde::ser::{Serialize, SerializeStruct, Serializer};
4950
use tracing::{debug, trace};
5051

5152
use crate::clean::RenderedLink;
@@ -836,7 +837,35 @@ pub(crate) struct LangString {
836837
pub(crate) unknown: Vec<String>,
837838
}
838839

839-
#[derive(Eq, PartialEq, Clone, Debug)]
840+
// This implementation is needed for `Edition` which doesn't implement `serde::Serialize` so
841+
// we need to implement it manually.
842+
impl Serialize for LangString {
843+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
844+
where
845+
S: Serializer,
846+
{
847+
// `12` is the number of fields.
848+
let mut s = serializer.serialize_struct("LangString", 12)?;
849+
850+
s.serialize_field("original", &self.original)?;
851+
s.serialize_field("should_panic", &self.should_panic)?;
852+
s.serialize_field("no_run", &self.no_run)?;
853+
s.serialize_field("ignore", &self.ignore)?;
854+
s.serialize_field("rust", &self.rust)?;
855+
s.serialize_field("test_harness", &self.test_harness)?;
856+
s.serialize_field("compile_fail", &self.compile_fail)?;
857+
s.serialize_field("standalone_crate", &self.standalone_crate)?;
858+
s.serialize_field("error_codes", &self.error_codes)?;
859+
let edition = self.edition.map(|edition| edition.to_string());
860+
s.serialize_field("edition", &edition)?;
861+
s.serialize_field("added_classes", &self.added_classes)?;
862+
s.serialize_field("unknown", &self.unknown)?;
863+
864+
s.end()
865+
}
866+
}
867+
868+
#[derive(Eq, PartialEq, Clone, Debug, serde::Serialize)]
840869
pub(crate) enum Ignore {
841870
All,
842871
None,

src/librustdoc/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ fn opts() -> Vec<RustcOptGroup> {
685685
"[rust]",
686686
),
687687
opt(Unstable, Flag, "", "html-no-source", "Disable HTML source code pages generation", ""),
688+
opt(Unstable, Flag, "", "extract-doctests", "Output doctests in JSON format", ""),
688689
]
689690
}
690691

@@ -804,7 +805,7 @@ fn main_args(
804805
}
805806
};
806807

807-
match (options.should_test, config::markdown_input(&input)) {
808+
match (options.should_test | options.extract_doctests, config::markdown_input(&input)) {
808809
(true, Some(_)) => return wrap_return(dcx, doctest::test_markdown(&input, options)),
809810
(true, None) => return doctest::run(dcx, input, options),
810811
(false, Some(md_input)) => {

0 commit comments

Comments
 (0)