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