Skip to content

Commit 46b1685

Browse files
committed
Separate doctest collection from running
1 parent ec6416b commit 46b1685

File tree

3 files changed

+231
-189
lines changed

3 files changed

+231
-189
lines changed

src/librustdoc/doctest.rs

+36-146
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use rustc_data_structures::fx::{FxHashMap, FxHashSet};
88
use rustc_data_structures::sync::Lrc;
99
use rustc_errors::emitter::stderr_destination;
1010
use rustc_errors::{ColorConfig, ErrorGuaranteed, FatalError};
11-
use rustc_hir::def_id::{CRATE_DEF_ID, LOCAL_CRATE};
11+
use rustc_hir::def_id::LOCAL_CRATE;
1212
use rustc_hir::CRATE_HIR_ID;
1313
use rustc_interface::interface;
1414
use rustc_parse::maybe_new_parser_from_source_str;
@@ -19,10 +19,9 @@ use rustc_session::parse::ParseSess;
1919
use rustc_span::edition::Edition;
2020
use rustc_span::source_map::SourceMap;
2121
use rustc_span::symbol::sym;
22-
use rustc_span::{BytePos, FileName, Pos, Span, DUMMY_SP};
22+
use rustc_span::FileName;
2323
use rustc_target::spec::{Target, TargetTriple};
2424

25-
use std::env;
2625
use std::fs::File;
2726
use std::io::{self, Write};
2827
use std::panic;
@@ -38,7 +37,8 @@ use crate::config::Options as RustdocOptions;
3837
use crate::html::markdown::{ErrorCodes, Ignore, LangString};
3938
use crate::lint::init_lints;
4039

41-
use self::rust::HirCollector;
40+
use self::markdown::MdDoctest;
41+
use self::rust::{HirCollector, RustDoctest};
4242

4343
/// Options that apply to all doctests in a crate or Markdown file (for `rustdoc foo.md`).
4444
#[derive(Clone, Default)]
@@ -182,29 +182,19 @@ pub(crate) fn run(
182182
let mut collector = Collector::new(
183183
tcx.crate_name(LOCAL_CRATE).to_string(),
184184
options,
185-
false,
186185
opts,
187-
Some(compiler.sess.psess.clone_source_map()),
188-
None,
189-
enable_per_target_ignores,
190186
file_path,
191187
);
192188

193-
let mut hir_collector = HirCollector {
194-
sess: &compiler.sess,
195-
collector: &mut collector,
196-
map: tcx.hir(),
197-
codes: ErrorCodes::from(
198-
compiler.sess.opts.unstable_features.is_nightly_build(),
199-
),
189+
let hir_collector = HirCollector::new(
190+
&compiler.sess,
191+
tcx.hir(),
192+
ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
193+
enable_per_target_ignores,
200194
tcx,
201-
};
202-
hir_collector.visit_testable(
203-
"".to_string(),
204-
CRATE_DEF_ID,
205-
tcx.hir().span(CRATE_HIR_ID),
206-
|this| tcx.hir().walk_toplevel_module(this),
207195
);
196+
let tests = hir_collector.collect_crate();
197+
tests.into_iter().for_each(|t| collector.add_test(ScrapedDoctest::Rust(t)));
208198

209199
collector
210200
});
@@ -986,6 +976,12 @@ impl IndividualTestOptions {
986976
}
987977
}
988978

979+
/// A doctest scraped from the code, ready to be turned into a runnable test.
980+
enum ScrapedDoctest {
981+
Rust(RustDoctest),
982+
Markdown(MdDoctest),
983+
}
984+
989985
pub(crate) trait DoctestVisitor {
990986
fn visit_test(&mut self, test: String, config: LangString, line: usize);
991987
fn get_line(&self) -> usize {
@@ -997,36 +993,9 @@ pub(crate) trait DoctestVisitor {
997993
pub(crate) struct Collector {
998994
pub(crate) tests: Vec<test::TestDescAndFn>,
999995

1000-
// The name of the test displayed to the user, separated by `::`.
1001-
//
1002-
// In tests from Rust source, this is the path to the item
1003-
// e.g., `["std", "vec", "Vec", "push"]`.
1004-
//
1005-
// In tests from a markdown file, this is the titles of all headers (h1~h6)
1006-
// of the sections that contain the code block, e.g., if the markdown file is
1007-
// written as:
1008-
//
1009-
// ``````markdown
1010-
// # Title
1011-
//
1012-
// ## Subtitle
1013-
//
1014-
// ```rust
1015-
// assert!(true);
1016-
// ```
1017-
// ``````
1018-
//
1019-
// the `names` vector of that test will be `["Title", "Subtitle"]`.
1020-
names: Vec<String>,
1021-
1022996
rustdoc_options: RustdocOptions,
1023-
use_headers: bool,
1024-
enable_per_target_ignores: bool,
1025997
crate_name: String,
1026998
opts: GlobalTestOptions,
1027-
position: Span,
1028-
source_map: Option<Lrc<SourceMap>>,
1029-
filename: Option<PathBuf>,
1030999
visited_tests: FxHashMap<(String, usize), usize>,
10311000
unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
10321001
compiling_test_count: AtomicUsize,
@@ -1037,74 +1006,48 @@ impl Collector {
10371006
pub(crate) fn new(
10381007
crate_name: String,
10391008
rustdoc_options: RustdocOptions,
1040-
use_headers: bool,
10411009
opts: GlobalTestOptions,
1042-
source_map: Option<Lrc<SourceMap>>,
1043-
filename: Option<PathBuf>,
1044-
enable_per_target_ignores: bool,
10451010
arg_file: PathBuf,
10461011
) -> Collector {
10471012
Collector {
10481013
tests: Vec::new(),
1049-
names: Vec::new(),
10501014
rustdoc_options,
1051-
use_headers,
1052-
enable_per_target_ignores,
10531015
crate_name,
10541016
opts,
1055-
position: DUMMY_SP,
1056-
source_map,
1057-
filename,
10581017
visited_tests: FxHashMap::default(),
10591018
unused_extern_reports: Default::default(),
10601019
compiling_test_count: AtomicUsize::new(0),
10611020
arg_file,
10621021
}
10631022
}
10641023

1065-
fn generate_name(&self, line: usize, filename: &FileName) -> String {
1066-
let mut item_path = self.names.join("::");
1024+
fn generate_name(&self, filename: &FileName, line: usize, logical_path: &[String]) -> String {
1025+
let mut item_path = logical_path.join("::");
10671026
item_path.retain(|c| c != ' ');
10681027
if !item_path.is_empty() {
10691028
item_path.push(' ');
10701029
}
10711030
format!("{} - {item_path}(line {line})", filename.prefer_local())
10721031
}
10731032

1074-
pub(crate) fn set_position(&mut self, position: Span) {
1075-
self.position = position;
1076-
}
1077-
1078-
fn get_filename(&self) -> FileName {
1079-
if let Some(ref source_map) = self.source_map {
1080-
let filename = source_map.span_to_filename(self.position);
1081-
if let FileName::Real(ref filename) = filename
1082-
&& let Ok(cur_dir) = env::current_dir()
1083-
&& let Some(local_path) = filename.local_path()
1084-
&& let Ok(path) = local_path.strip_prefix(&cur_dir)
1085-
{
1086-
return path.to_owned().into();
1033+
fn add_test(&mut self, test: ScrapedDoctest) {
1034+
let (filename, line, logical_path, langstr, text) = match test {
1035+
ScrapedDoctest::Rust(RustDoctest { filename, line, logical_path, langstr, text }) => {
1036+
(filename, line, logical_path, langstr, text)
10871037
}
1088-
filename
1089-
} else if let Some(ref filename) = self.filename {
1090-
filename.clone().into()
1091-
} else {
1092-
FileName::Custom("input".to_owned())
1093-
}
1094-
}
1095-
}
1038+
ScrapedDoctest::Markdown(MdDoctest { filename, line, logical_path, langstr, text }) => {
1039+
(filename, line, logical_path, langstr, text)
1040+
}
1041+
};
10961042

1097-
impl DoctestVisitor for Collector {
1098-
fn visit_test(&mut self, test: String, config: LangString, line: usize) {
1099-
let filename = self.get_filename();
1100-
let name = self.generate_name(line, &filename);
1043+
let name = self.generate_name(&filename, line, &logical_path);
11011044
let crate_name = self.crate_name.clone();
11021045
let opts = self.opts.clone();
1103-
let edition = config.edition.unwrap_or(self.rustdoc_options.edition);
1046+
let edition = langstr.edition.unwrap_or(self.rustdoc_options.edition);
11041047
let target_str = self.rustdoc_options.target.to_string();
11051048
let unused_externs = self.unused_extern_reports.clone();
1106-
let no_run = config.no_run || self.rustdoc_options.no_run;
1107-
if !config.compile_fail {
1049+
let no_run = langstr.no_run || self.rustdoc_options.no_run;
1050+
if !langstr.compile_fail {
11081051
self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
11091052
}
11101053

@@ -1141,11 +1084,11 @@ impl DoctestVisitor for Collector {
11411084
let rustdoc_test_options =
11421085
IndividualTestOptions::new(&self.rustdoc_options, &self.arg_file, test_id);
11431086

1144-
debug!("creating test {name}: {test}");
1087+
debug!("creating test {name}: {text}");
11451088
self.tests.push(test::TestDescAndFn {
11461089
desc: test::TestDesc {
11471090
name: test::DynTestName(name),
1148-
ignore: match config.ignore {
1091+
ignore: match langstr.ignore {
11491092
Ignore::All => true,
11501093
Ignore::None => false,
11511094
Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
@@ -1158,7 +1101,7 @@ impl DoctestVisitor for Collector {
11581101
end_col: 0,
11591102
// compiler failures are test failures
11601103
should_panic: test::ShouldPanic::No,
1161-
compile_fail: config.compile_fail,
1104+
compile_fail: langstr.compile_fail,
11621105
no_run,
11631106
test_type: test::TestType::DocTest,
11641107
},
@@ -1167,11 +1110,11 @@ impl DoctestVisitor for Collector {
11671110
unused_externs.lock().unwrap().push(uext);
11681111
};
11691112
let res = run_test(
1170-
&test,
1113+
&text,
11711114
&crate_name,
11721115
line,
11731116
rustdoc_test_options,
1174-
config,
1117+
langstr,
11751118
no_run,
11761119
&opts,
11771120
edition,
@@ -1234,59 +1177,6 @@ impl DoctestVisitor for Collector {
12341177
})),
12351178
});
12361179
}
1237-
1238-
fn get_line(&self) -> usize {
1239-
if let Some(ref source_map) = self.source_map {
1240-
let line = self.position.lo().to_usize();
1241-
let line = source_map.lookup_char_pos(BytePos(line as u32)).line;
1242-
if line > 0 { line - 1 } else { line }
1243-
} else {
1244-
0
1245-
}
1246-
}
1247-
1248-
fn visit_header(&mut self, name: &str, level: u32) {
1249-
if self.use_headers {
1250-
// We use these headings as test names, so it's good if
1251-
// they're valid identifiers.
1252-
let name = name
1253-
.chars()
1254-
.enumerate()
1255-
.map(|(i, c)| {
1256-
if (i == 0 && rustc_lexer::is_id_start(c))
1257-
|| (i != 0 && rustc_lexer::is_id_continue(c))
1258-
{
1259-
c
1260-
} else {
1261-
'_'
1262-
}
1263-
})
1264-
.collect::<String>();
1265-
1266-
// Here we try to efficiently assemble the header titles into the
1267-
// test name in the form of `h1::h2::h3::h4::h5::h6`.
1268-
//
1269-
// Suppose that originally `self.names` contains `[h1, h2, h3]`...
1270-
let level = level as usize;
1271-
if level <= self.names.len() {
1272-
// ... Consider `level == 2`. All headers in the lower levels
1273-
// are irrelevant in this new level. So we should reset
1274-
// `self.names` to contain headers until <h2>, and replace that
1275-
// slot with the new name: `[h1, name]`.
1276-
self.names.truncate(level);
1277-
self.names[level - 1] = name;
1278-
} else {
1279-
// ... On the other hand, consider `level == 5`. This means we
1280-
// need to extend `self.names` to contain five headers. We fill
1281-
// in the missing level (<h4>) with `_`. Thus `self.names` will
1282-
// become `[h1, h2, h3, "_", name]`.
1283-
if level - 1 > self.names.len() {
1284-
self.names.resize(level - 1, "_".to_owned());
1285-
}
1286-
self.names.push(name);
1287-
}
1288-
}
1289-
}
12901180
}
12911181

12921182
#[cfg(test)] // used in tests

0 commit comments

Comments
 (0)