Skip to main content

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