rustdoc/doctest/
make.rs

1//! Logic for transforming the raw code given by the user into something actually
2//! runnable, e.g. by adding a `main` function if it doesn't already exist.
3
4use std::fmt::{self, Write as _};
5use std::io;
6use std::sync::Arc;
7
8use rustc_ast::token::{Delimiter, TokenKind};
9use rustc_ast::tokenstream::TokenTree;
10use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind};
11use rustc_errors::emitter::get_stderr_color_choice;
12use rustc_errors::{AutoStream, ColorChoice, ColorConfig, DiagCtxtHandle};
13use rustc_parse::lexer::StripTokens;
14use rustc_parse::new_parser_from_source_str;
15use rustc_session::parse::ParseSess;
16use rustc_span::edition::{DEFAULT_EDITION, Edition};
17use rustc_span::source_map::SourceMap;
18use rustc_span::symbol::sym;
19use rustc_span::{DUMMY_SP, FileName, Span, kw};
20use tracing::debug;
21
22use super::GlobalTestOptions;
23use crate::display::Joined as _;
24use crate::html::markdown::LangString;
25
26#[derive(Default)]
27struct ParseSourceInfo {
28    has_main_fn: bool,
29    already_has_extern_crate: bool,
30    supports_color: bool,
31    has_global_allocator: bool,
32    has_macro_def: bool,
33    everything_else: String,
34    crates: String,
35    crate_attrs: String,
36    maybe_crate_attrs: String,
37}
38
39/// Builder type for `DocTestBuilder`.
40pub(crate) struct BuildDocTestBuilder<'a> {
41    source: &'a str,
42    crate_name: Option<&'a str>,
43    edition: Edition,
44    can_merge_doctests: bool,
45    // If `test_id` is `None`, it means we're generating code for a code example "run" link.
46    test_id: Option<String>,
47    lang_str: Option<&'a LangString>,
48    span: Span,
49    global_crate_attrs: Vec<String>,
50}
51
52impl<'a> BuildDocTestBuilder<'a> {
53    pub(crate) fn new(source: &'a str) -> Self {
54        Self {
55            source,
56            crate_name: None,
57            edition: DEFAULT_EDITION,
58            can_merge_doctests: false,
59            test_id: None,
60            lang_str: None,
61            span: DUMMY_SP,
62            global_crate_attrs: Vec::new(),
63        }
64    }
65
66    #[inline]
67    pub(crate) fn crate_name(mut self, crate_name: &'a str) -> Self {
68        self.crate_name = Some(crate_name);
69        self
70    }
71
72    #[inline]
73    pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: bool) -> Self {
74        self.can_merge_doctests = can_merge_doctests;
75        self
76    }
77
78    #[inline]
79    pub(crate) fn test_id(mut self, test_id: String) -> Self {
80        self.test_id = Some(test_id);
81        self
82    }
83
84    #[inline]
85    pub(crate) fn lang_str(mut self, lang_str: &'a LangString) -> Self {
86        self.lang_str = Some(lang_str);
87        self
88    }
89
90    #[inline]
91    pub(crate) fn span(mut self, span: Span) -> Self {
92        self.span = span;
93        self
94    }
95
96    #[inline]
97    pub(crate) fn edition(mut self, edition: Edition) -> Self {
98        self.edition = edition;
99        self
100    }
101
102    #[inline]
103    pub(crate) fn global_crate_attrs(mut self, global_crate_attrs: Vec<String>) -> Self {
104        self.global_crate_attrs = global_crate_attrs;
105        self
106    }
107
108    pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder {
109        let BuildDocTestBuilder {
110            source,
111            crate_name,
112            edition,
113            can_merge_doctests,
114            // If `test_id` is `None`, it means we're generating code for a code example "run" link.
115            test_id,
116            lang_str,
117            span,
118            global_crate_attrs,
119        } = self;
120        let can_merge_doctests = can_merge_doctests
121            && lang_str.is_some_and(|lang_str| {
122                !lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
123            });
124
125        let result = rustc_driver::catch_fatal_errors(|| {
126            rustc_span::create_session_if_not_set_then(edition, |_| {
127                parse_source(source, &crate_name, dcx, span)
128            })
129        });
130
131        let Ok(Ok(ParseSourceInfo {
132            has_main_fn,
133            already_has_extern_crate,
134            supports_color,
135            has_global_allocator,
136            has_macro_def,
137            everything_else,
138            crates,
139            crate_attrs,
140            maybe_crate_attrs,
141        })) = result
142        else {
143            // If the AST returned an error, we don't want this doctest to be merged with the
144            // others.
145            return DocTestBuilder::invalid(
146                Vec::new(),
147                String::new(),
148                String::new(),
149                String::new(),
150                source.to_string(),
151                test_id,
152            );
153        };
154
155        debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}");
156        debug!("crates:\n{crates}");
157        debug!("after:\n{everything_else}");
158
159        // If it contains `#[feature]` or `#[no_std]`, we don't want it to be merged either.
160        let can_be_merged = can_merge_doctests
161            && !has_global_allocator
162            && crate_attrs.is_empty()
163            // If this is a merged doctest and a defined macro uses `$crate`, then the path will
164            // not work, so better not put it into merged doctests.
165            && !(has_macro_def && everything_else.contains("$crate"));
166        DocTestBuilder {
167            supports_color,
168            has_main_fn,
169            global_crate_attrs,
170            crate_attrs,
171            maybe_crate_attrs,
172            crates,
173            everything_else,
174            already_has_extern_crate,
175            test_id,
176            invalid_ast: false,
177            can_be_merged,
178        }
179    }
180}
181
182/// This struct contains information about the doctest itself which is then used to generate
183/// doctest source code appropriately.
184pub(crate) struct DocTestBuilder {
185    pub(crate) supports_color: bool,
186    pub(crate) already_has_extern_crate: bool,
187    pub(crate) has_main_fn: bool,
188    pub(crate) global_crate_attrs: Vec<String>,
189    pub(crate) crate_attrs: String,
190    /// If this is a merged doctest, it will be put into `everything_else`, otherwise it will
191    /// put into `crate_attrs`.
192    pub(crate) maybe_crate_attrs: String,
193    pub(crate) crates: String,
194    pub(crate) everything_else: String,
195    pub(crate) test_id: Option<String>,
196    pub(crate) invalid_ast: bool,
197    pub(crate) can_be_merged: bool,
198}
199
200/// Contains needed information for doctest to be correctly generated with expected "wrapping".
201pub(crate) struct WrapperInfo {
202    pub(crate) before: String,
203    pub(crate) after: String,
204    pub(crate) returns_result: bool,
205    insert_indent_space: bool,
206}
207
208impl WrapperInfo {
209    fn len(&self) -> usize {
210        self.before.len() + self.after.len()
211    }
212}
213
214/// Contains a doctest information. Can be converted into code with the `to_string()` method.
215pub(crate) enum DocTestWrapResult {
216    Valid {
217        crate_level_code: String,
218        /// This field can be `None` if one of the following conditions is true:
219        ///
220        /// * The doctest's codeblock has the `test_harness` attribute.
221        /// * The doctest has a `main` function.
222        /// * The doctest has the `![no_std]` attribute.
223        wrapper: Option<WrapperInfo>,
224        /// Contains the doctest processed code without the wrappers (which are stored in the
225        /// `wrapper` field).
226        code: String,
227    },
228    /// Contains the original source code.
229    SyntaxError(String),
230}
231
232impl std::string::ToString for DocTestWrapResult {
233    fn to_string(&self) -> String {
234        match self {
235            Self::SyntaxError(s) => s.clone(),
236            Self::Valid { crate_level_code, wrapper, code } => {
237                let mut prog_len = code.len() + crate_level_code.len();
238                if let Some(wrapper) = wrapper {
239                    prog_len += wrapper.len();
240                    if wrapper.insert_indent_space {
241                        prog_len += code.lines().count() * 4;
242                    }
243                }
244                let mut prog = String::with_capacity(prog_len);
245
246                prog.push_str(crate_level_code);
247                if let Some(wrapper) = wrapper {
248                    prog.push_str(&wrapper.before);
249
250                    // add extra 4 spaces for each line to offset the code block
251                    if wrapper.insert_indent_space {
252                        write!(
253                            prog,
254                            "{}",
255                            fmt::from_fn(|f| code
256                                .lines()
257                                .map(|line| fmt::from_fn(move |f| write!(f, "    {line}")))
258                                .joined("\n", f))
259                        )
260                        .unwrap();
261                    } else {
262                        prog.push_str(code);
263                    }
264                    prog.push_str(&wrapper.after);
265                } else {
266                    prog.push_str(code);
267                }
268                prog
269            }
270        }
271    }
272}
273
274impl DocTestBuilder {
275    fn invalid(
276        global_crate_attrs: Vec<String>,
277        crate_attrs: String,
278        maybe_crate_attrs: String,
279        crates: String,
280        everything_else: String,
281        test_id: Option<String>,
282    ) -> Self {
283        Self {
284            supports_color: false,
285            has_main_fn: false,
286            global_crate_attrs,
287            crate_attrs,
288            maybe_crate_attrs,
289            crates,
290            everything_else,
291            already_has_extern_crate: false,
292            test_id,
293            invalid_ast: true,
294            can_be_merged: false,
295        }
296    }
297
298    /// Transforms a test into code that can be compiled into a Rust binary, and returns the number of
299    /// lines before the test code begins.
300    pub(crate) fn generate_unique_doctest(
301        &self,
302        test_code: &str,
303        dont_insert_main: bool,
304        opts: &GlobalTestOptions,
305        crate_name: Option<&str>,
306    ) -> (DocTestWrapResult, usize) {
307        if self.invalid_ast {
308            // If the AST failed to compile, no need to go generate a complete doctest, the error
309            // will be better this way.
310            debug!("invalid AST:\n{test_code}");
311            return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0);
312        }
313        let mut line_offset = 0;
314        let mut crate_level_code = String::new();
315        let processed_code = self.everything_else.trim();
316        if self.global_crate_attrs.is_empty() {
317            // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
318            // lints that are commonly triggered in doctests. The crate-level test attributes are
319            // commonly used to make tests fail in case they trigger warnings, so having this there in
320            // that case may cause some tests to pass when they shouldn't have.
321            crate_level_code.push_str("#![allow(unused)]\n");
322            line_offset += 1;
323        }
324
325        // Next, any attributes that came from #![doc(test(attr(...)))].
326        for attr in &self.global_crate_attrs {
327            crate_level_code.push_str(&format!("#![{attr}]\n"));
328            line_offset += 1;
329        }
330
331        // Now push any outer attributes from the example, assuming they
332        // are intended to be crate attributes.
333        if !self.crate_attrs.is_empty() {
334            crate_level_code.push_str(&self.crate_attrs);
335            if !self.crate_attrs.ends_with('\n') {
336                crate_level_code.push('\n');
337            }
338        }
339        if !self.maybe_crate_attrs.is_empty() {
340            crate_level_code.push_str(&self.maybe_crate_attrs);
341            if !self.maybe_crate_attrs.ends_with('\n') {
342                crate_level_code.push('\n');
343            }
344        }
345        if !self.crates.is_empty() {
346            crate_level_code.push_str(&self.crates);
347            if !self.crates.ends_with('\n') {
348                crate_level_code.push('\n');
349            }
350        }
351
352        // Don't inject `extern crate std` because it's already injected by the
353        // compiler.
354        if !self.already_has_extern_crate &&
355            !opts.no_crate_inject &&
356            let Some(crate_name) = crate_name &&
357            crate_name != "std" &&
358            // Don't inject `extern crate` if the crate is never used.
359            // NOTE: this is terribly inaccurate because it doesn't actually
360            // parse the source, but only has false positives, not false
361            // negatives.
362            test_code.contains(crate_name)
363        {
364            // rustdoc implicitly inserts an `extern crate` item for the own crate
365            // which may be unused, so we need to allow the lint.
366            crate_level_code.push_str("#[allow(unused_extern_crates)]\n");
367
368            crate_level_code.push_str(&format!("extern crate r#{crate_name};\n"));
369            line_offset += 1;
370        }
371
372        // FIXME: This code cannot yet handle no_std test cases yet
373        let wrapper = if dont_insert_main
374            || self.has_main_fn
375            || crate_level_code.contains("![no_std]")
376        {
377            None
378        } else {
379            let returns_result = processed_code.ends_with("(())");
380            // Give each doctest main function a unique name.
381            // This is for example needed for the tooling around `-C instrument-coverage`.
382            let inner_fn_name = if let Some(ref test_id) = self.test_id {
383                format!("_doctest_main_{test_id}")
384            } else {
385                "_inner".into()
386            };
387            let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
388            let (main_pre, main_post) = if returns_result {
389                (
390                    format!(
391                        "fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n",
392                    ),
393                    format!("\n}} {inner_fn_name}().unwrap() }}"),
394                )
395            } else if self.test_id.is_some() {
396                (
397                    format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
398                    format!("\n}} {inner_fn_name}() }}"),
399                )
400            } else {
401                ("fn main() {\n".into(), "\n}".into())
402            };
403            // Note on newlines: We insert a line/newline *before*, and *after*
404            // the doctest and adjust the `line_offset` accordingly.
405            // In the case of `-C instrument-coverage`, this means that the generated
406            // inner `main` function spans from the doctest opening codeblock to the
407            // closing one. For example
408            // /// ``` <- start of the inner main
409            // /// <- code under doctest
410            // /// ``` <- end of the inner main
411            line_offset += 1;
412
413            Some(WrapperInfo {
414                before: main_pre,
415                after: main_post,
416                returns_result,
417                insert_indent_space: opts.insert_indent_space,
418            })
419        };
420
421        (
422            DocTestWrapResult::Valid {
423                code: processed_code.to_string(),
424                wrapper,
425                crate_level_code,
426            },
427            line_offset,
428        )
429    }
430}
431
432fn reset_error_count(psess: &ParseSess) {
433    // Reset errors so that they won't be reported as compiler bugs when dropping the
434    // dcx. Any errors in the tests will be reported when the test file is compiled,
435    // Note that we still need to cancel the errors above otherwise `Diag` will panic on
436    // drop.
437    psess.dcx().reset_err_count();
438}
439
440const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
441
442fn parse_source(
443    source: &str,
444    crate_name: &Option<&str>,
445    parent_dcx: Option<DiagCtxtHandle<'_>>,
446    span: Span,
447) -> Result<ParseSourceInfo, ()> {
448    use rustc_errors::DiagCtxt;
449    use rustc_errors::emitter::HumanEmitter;
450    use rustc_span::source_map::FilePathMapping;
451
452    let mut info =
453        ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };
454
455    let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");
456
457    let filename = FileName::anon_source_code(&wrapped_source);
458
459    let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
460    let translator = rustc_driver::default_translator();
461    let supports_color = match get_stderr_color_choice(ColorConfig::Auto, &std::io::stderr()) {
462        ColorChoice::Auto => unreachable!(),
463        ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
464        ColorChoice::Never => false,
465    };
466    info.supports_color = supports_color;
467    // Any errors in parsing should also appear when the doctest is compiled for real, so just
468    // send all the errors that the parser emits directly into a `Sink` instead of stderr.
469    let emitter = HumanEmitter::new(AutoStream::never(Box::new(io::sink())), translator);
470
471    // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
472    let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
473    let psess = ParseSess::with_dcx(dcx, sm);
474
475    // Don't strip any tokens; it wouldn't matter anyway because the source is wrapped in a function.
476    let mut parser =
477        match new_parser_from_source_str(&psess, filename, wrapped_source, StripTokens::Nothing) {
478            Ok(p) => p,
479            Err(errs) => {
480                errs.into_iter().for_each(|err| err.cancel());
481                reset_error_count(&psess);
482                return Err(());
483            }
484        };
485
486    fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {
487        let extra_len = DOCTEST_CODE_WRAPPER.len();
488        // We need to shift by the length of `DOCTEST_CODE_WRAPPER` because we
489        // added it at the beginning of the source we provided to the parser.
490        let mut hi = span.hi().0 as usize - extra_len;
491        if hi > source.len() {
492            hi = source.len();
493        }
494        s.push_str(&source[*prev_span_hi..hi]);
495        *prev_span_hi = hi;
496    }
497
498    fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool {
499        let mut is_extern_crate = false;
500        if !info.has_global_allocator
501            && item.attrs.iter().any(|attr| attr.has_name(sym::global_allocator))
502        {
503            info.has_global_allocator = true;
504        }
505        match item.kind {
506            ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => {
507                if fn_item.ident.name == sym::main {
508                    info.has_main_fn = true;
509                }
510            }
511            ast::ItemKind::ExternCrate(original, ident) => {
512                is_extern_crate = true;
513                if !info.already_has_extern_crate
514                    && let Some(crate_name) = crate_name
515                {
516                    info.already_has_extern_crate = match original {
517                        Some(name) => name.as_str() == *crate_name,
518                        None => ident.as_str() == *crate_name,
519                    };
520                }
521            }
522            ast::ItemKind::MacroDef(..) => {
523                info.has_macro_def = true;
524            }
525            _ => {}
526        }
527        is_extern_crate
528    }
529
530    let mut prev_span_hi = 0;
531    let not_crate_attrs = &[sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect];
532    let parsed = parser.parse_item(rustc_parse::parser::ForceCollect::No);
533
534    let result = match parsed {
535        Ok(Some(ref item))
536            if let ast::ItemKind::Fn(ref fn_item) = item.kind
537                && let Some(ref body) = fn_item.body =>
538        {
539            for attr in &item.attrs {
540                if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {
541                    // There is one exception to these attributes:
542                    // `#![allow(internal_features)]`. If this attribute is used, we need to
543                    // consider it only as a crate-level attribute.
544                    if attr.has_name(sym::allow)
545                        && let Some(list) = attr.meta_item_list()
546                        && list.iter().any(|sub_attr| sub_attr.has_name(sym::internal_features))
547                    {
548                        push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
549                    } else {
550                        push_to_s(
551                            &mut info.maybe_crate_attrs,
552                            source,
553                            attr.span,
554                            &mut prev_span_hi,
555                        );
556                    }
557                } else {
558                    push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
559                }
560            }
561            let mut has_non_items = false;
562            for stmt in &body.stmts {
563                let mut is_extern_crate = false;
564                match stmt.kind {
565                    StmtKind::Item(ref item) => {
566                        is_extern_crate = check_item(item, &mut info, crate_name);
567                    }
568                    // We assume that the macro calls will expand to item(s) even though they could
569                    // expand to statements and expressions.
570                    StmtKind::MacCall(ref mac_call) => {
571                        if !info.has_main_fn {
572                            // For backward compatibility, we look for the token sequence `fn main(…)`
573                            // in the macro input (!) to crudely detect main functions "masked by a
574                            // wrapper macro". For the record, this is a horrible heuristic!
575                            // See <https://github.com/rust-lang/rust/issues/56898>.
576                            let mut iter = mac_call.mac.args.tokens.iter();
577                            while let Some(token) = iter.next() {
578                                if let TokenTree::Token(token, _) = token
579                                    && let TokenKind::Ident(kw::Fn, _) = token.kind
580                                    && let Some(TokenTree::Token(ident, _)) = iter.peek()
581                                    && let TokenKind::Ident(sym::main, _) = ident.kind
582                                    && let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = {
583                                        iter.next();
584                                        iter.peek()
585                                    }
586                                {
587                                    info.has_main_fn = true;
588                                    break;
589                                }
590                            }
591                        }
592                    }
593                    StmtKind::Expr(ref expr) => {
594                        if matches!(expr.kind, ast::ExprKind::Err(_)) {
595                            reset_error_count(&psess);
596                            return Err(());
597                        }
598                        has_non_items = true;
599                    }
600                    StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true,
601                }
602
603                // Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to
604                // tweak the span to include the attributes as well.
605                let mut span = stmt.span;
606                if let Some(attr) =
607                    stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer)
608                {
609                    span = span.with_lo(attr.span.lo());
610                }
611                if info.everything_else.is_empty()
612                    && (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty())
613                {
614                    // To keep the doctest code "as close as possible" to the original, we insert
615                    // all the code located between this new span and the previous span which
616                    // might contain code comments and backlines.
617                    push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);
618                }
619                if !is_extern_crate {
620                    push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);
621                } else {
622                    push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
623                }
624            }
625            if has_non_items {
626                if info.has_main_fn
627                    && let Some(dcx) = parent_dcx
628                    && !span.is_dummy()
629                {
630                    dcx.span_warn(
631                        span,
632                        "the `main` function of this doctest won't be run as it contains \
633                         expressions at the top level, meaning that the whole doctest code will be \
634                         wrapped in a function",
635                    );
636                }
637                info.has_main_fn = false;
638            }
639            Ok(info)
640        }
641        Err(e) => {
642            e.cancel();
643            Err(())
644        }
645        _ => Err(()),
646    };
647
648    reset_error_count(&psess);
649    result
650}