rustdoc/doctest/
markdown.rs

1//! Doctest functionality used only for doctests in `.md` Markdown files.
2
3use std::fs::read_to_string;
4use std::sync::{Arc, Mutex};
5
6use rustc_session::config::Input;
7use rustc_span::FileName;
8use tempfile::tempdir;
9
10use super::{
11    CreateRunnableDocTests, DocTestVisitor, GlobalTestOptions, ScrapedDocTest, generate_args_file,
12};
13use crate::config::Options;
14use crate::html::markdown::{ErrorCodes, LangString, MdRelLine, find_testable_code};
15
16struct MdCollector {
17    tests: Vec<ScrapedDocTest>,
18    cur_path: Vec<String>,
19    filename: FileName,
20}
21
22impl DocTestVisitor for MdCollector {
23    fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) {
24        let filename = self.filename.clone();
25        // First line of Markdown is line 1.
26        let line = 1 + rel_line.offset();
27        self.tests.push(ScrapedDocTest::new(filename, line, self.cur_path.clone(), config, test));
28    }
29
30    fn visit_header(&mut self, name: &str, level: u32) {
31        // We use these headings as test names, so it's good if
32        // they're valid identifiers.
33        let name = name
34            .chars()
35            .enumerate()
36            .map(|(i, c)| {
37                if (i == 0 && rustc_lexer::is_id_start(c))
38                    || (i != 0 && rustc_lexer::is_id_continue(c))
39                {
40                    c
41                } else {
42                    '_'
43                }
44            })
45            .collect::<String>();
46
47        // Here we try to efficiently assemble the header titles into the
48        // test name in the form of `h1::h2::h3::h4::h5::h6`.
49        //
50        // Suppose that originally `self.cur_path` contains `[h1, h2, h3]`...
51        let level = level as usize;
52        if level <= self.cur_path.len() {
53            // ... Consider `level == 2`. All headers in the lower levels
54            // are irrelevant in this new level. So we should reset
55            // `self.names` to contain headers until <h2>, and replace that
56            // slot with the new name: `[h1, name]`.
57            self.cur_path.truncate(level);
58            self.cur_path[level - 1] = name;
59        } else {
60            // ... On the other hand, consider `level == 5`. This means we
61            // need to extend `self.names` to contain five headers. We fill
62            // in the missing level (<h4>) with `_`. Thus `self.names` will
63            // become `[h1, h2, h3, "_", name]`.
64            if level - 1 > self.cur_path.len() {
65                self.cur_path.resize(level - 1, "_".to_owned());
66            }
67            self.cur_path.push(name);
68        }
69    }
70}
71
72/// Runs any tests/code examples in the markdown file `options.input`.
73pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> {
74    let input_str = match input {
75        Input::File(path) => {
76            read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?
77        }
78        Input::Str { name: _, input } => input.clone(),
79    };
80
81    // Obviously not a real crate name, but close enough for purposes of doctests.
82    let crate_name = input.filestem().to_string();
83    let temp_dir =
84        tempdir().map_err(|error| format!("failed to create temporary directory: {error:?}"))?;
85    let args_file = temp_dir.path().join("rustdoc-cfgs");
86    generate_args_file(&args_file, &options)?;
87
88    let opts = GlobalTestOptions {
89        crate_name,
90        no_crate_inject: true,
91        insert_indent_space: false,
92        attrs: vec![],
93        args_file,
94    };
95
96    let mut md_collector = MdCollector {
97        tests: vec![],
98        cur_path: vec![],
99        filename: input
100            .opt_path()
101            .map(ToOwned::to_owned)
102            .map(FileName::from)
103            .unwrap_or(FileName::Custom("input".to_owned())),
104    };
105    let codes = ErrorCodes::from(options.unstable_features.is_nightly_build());
106
107    find_testable_code(
108        &input_str,
109        &mut md_collector,
110        codes,
111        options.enable_per_target_ignores,
112        None,
113    );
114
115    let mut collector = CreateRunnableDocTests::new(options.clone(), opts);
116    md_collector.tests.into_iter().for_each(|t| collector.add_test(t));
117    let CreateRunnableDocTests { opts, rustdoc_options, standalone_tests, mergeable_tests, .. } =
118        collector;
119    crate::doctest::run_tests(
120        opts,
121        &rustdoc_options,
122        &Arc::new(Mutex::new(Vec::new())),
123        standalone_tests,
124        mergeable_tests,
125    );
126    Ok(())
127}