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