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 as ast;
9use rustc_errors::emitter::stderr_destination;
10use rustc_errors::{ColorConfig, FatalError};
11use rustc_parse::new_parser_from_source_str;
12use rustc_parse::parser::attr::InnerAttrPolicy;
13use rustc_session::parse::ParseSess;
14use rustc_span::FileName;
15use rustc_span::edition::Edition;
16use rustc_span::source_map::SourceMap;
17use rustc_span::symbol::sym;
18use tracing::debug;
19
20use super::GlobalTestOptions;
21use crate::display::Joined as _;
22use crate::html::markdown::LangString;
23
24/// This struct contains information about the doctest itself which is then used to generate
25/// doctest source code appropriately.
26pub(crate) struct DocTestBuilder {
27    pub(crate) supports_color: bool,
28    pub(crate) already_has_extern_crate: bool,
29    pub(crate) has_main_fn: bool,
30    pub(crate) crate_attrs: String,
31    /// If this is a merged doctest, it will be put into `everything_else`, otherwise it will
32    /// put into `crate_attrs`.
33    pub(crate) maybe_crate_attrs: String,
34    pub(crate) crates: String,
35    pub(crate) everything_else: String,
36    pub(crate) test_id: Option<String>,
37    pub(crate) failed_ast: bool,
38    pub(crate) can_be_merged: bool,
39}
40
41impl DocTestBuilder {
42    pub(crate) fn new(
43        source: &str,
44        crate_name: Option<&str>,
45        edition: Edition,
46        can_merge_doctests: bool,
47        // If `test_id` is `None`, it means we're generating code for a code example "run" link.
48        test_id: Option<String>,
49        lang_str: Option<&LangString>,
50    ) -> Self {
51        let can_merge_doctests = can_merge_doctests
52            && lang_str.is_some_and(|lang_str| {
53                !lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
54            });
55
56        let Some(SourceInfo { crate_attrs, maybe_crate_attrs, crates, everything_else }) =
57            partition_source(source, edition)
58        else {
59            return Self::invalid(
60                String::new(),
61                String::new(),
62                String::new(),
63                source.to_string(),
64                test_id,
65            );
66        };
67
68        // Uses librustc_ast to parse the doctest and find if there's a main fn and the extern
69        // crate already is included.
70        let Ok((
71            ParseSourceInfo {
72                has_main_fn,
73                found_extern_crate,
74                supports_color,
75                has_global_allocator,
76                has_macro_def,
77                ..
78            },
79            failed_ast,
80        )) = check_for_main_and_extern_crate(
81            crate_name,
82            source,
83            &everything_else,
84            &crates,
85            edition,
86            can_merge_doctests,
87        )
88        else {
89            // If the parser panicked due to a fatal error, pass the test code through unchanged.
90            // The error will be reported during compilation.
91            return Self::invalid(crate_attrs, maybe_crate_attrs, crates, everything_else, test_id);
92        };
93        // If the AST returned an error, we don't want this doctest to be merged with the
94        // others. Same if it contains `#[feature]` or `#[no_std]`.
95        let can_be_merged = can_merge_doctests
96            && !failed_ast
97            && !has_global_allocator
98            && crate_attrs.is_empty()
99            // If this is a merged doctest and a defined macro uses `$crate`, then the path will
100            // not work, so better not put it into merged doctests.
101            && !(has_macro_def && everything_else.contains("$crate"));
102        Self {
103            supports_color,
104            has_main_fn,
105            crate_attrs,
106            maybe_crate_attrs,
107            crates,
108            everything_else,
109            already_has_extern_crate: found_extern_crate,
110            test_id,
111            failed_ast: false,
112            can_be_merged,
113        }
114    }
115
116    fn invalid(
117        crate_attrs: String,
118        maybe_crate_attrs: String,
119        crates: String,
120        everything_else: String,
121        test_id: Option<String>,
122    ) -> Self {
123        Self {
124            supports_color: false,
125            has_main_fn: false,
126            crate_attrs,
127            maybe_crate_attrs,
128            crates,
129            everything_else,
130            already_has_extern_crate: false,
131            test_id,
132            failed_ast: true,
133            can_be_merged: false,
134        }
135    }
136
137    /// Transforms a test into code that can be compiled into a Rust binary, and returns the number of
138    /// lines before the test code begins.
139    pub(crate) fn generate_unique_doctest(
140        &self,
141        test_code: &str,
142        dont_insert_main: bool,
143        opts: &GlobalTestOptions,
144        crate_name: Option<&str>,
145    ) -> (String, usize) {
146        if self.failed_ast {
147            // If the AST failed to compile, no need to go generate a complete doctest, the error
148            // will be better this way.
149            return (test_code.to_string(), 0);
150        }
151        let mut line_offset = 0;
152        let mut prog = String::new();
153        let everything_else = self.everything_else.trim();
154        if opts.attrs.is_empty() {
155            // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
156            // lints that are commonly triggered in doctests. The crate-level test attributes are
157            // commonly used to make tests fail in case they trigger warnings, so having this there in
158            // that case may cause some tests to pass when they shouldn't have.
159            prog.push_str("#![allow(unused)]\n");
160            line_offset += 1;
161        }
162
163        // Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
164        for attr in &opts.attrs {
165            prog.push_str(&format!("#![{attr}]\n"));
166            line_offset += 1;
167        }
168
169        // Now push any outer attributes from the example, assuming they
170        // are intended to be crate attributes.
171        prog.push_str(&self.crate_attrs);
172        prog.push_str(&self.maybe_crate_attrs);
173        prog.push_str(&self.crates);
174
175        // Don't inject `extern crate std` because it's already injected by the
176        // compiler.
177        if !self.already_has_extern_crate &&
178            !opts.no_crate_inject &&
179            let Some(crate_name) = crate_name &&
180            crate_name != "std" &&
181            // Don't inject `extern crate` if the crate is never used.
182            // NOTE: this is terribly inaccurate because it doesn't actually
183            // parse the source, but only has false positives, not false
184            // negatives.
185            test_code.contains(crate_name)
186        {
187            // rustdoc implicitly inserts an `extern crate` item for the own crate
188            // which may be unused, so we need to allow the lint.
189            prog.push_str("#[allow(unused_extern_crates)]\n");
190
191            prog.push_str(&format!("extern crate r#{crate_name};\n"));
192            line_offset += 1;
193        }
194
195        // FIXME: This code cannot yet handle no_std test cases yet
196        if dont_insert_main || self.has_main_fn || prog.contains("![no_std]") {
197            prog.push_str(everything_else);
198        } else {
199            let returns_result = everything_else.ends_with("(())");
200            // Give each doctest main function a unique name.
201            // This is for example needed for the tooling around `-C instrument-coverage`.
202            let inner_fn_name = if let Some(ref test_id) = self.test_id {
203                format!("_doctest_main_{test_id}")
204            } else {
205                "_inner".into()
206            };
207            let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
208            let (main_pre, main_post) = if returns_result {
209                (
210                    format!(
211                        "fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n",
212                    ),
213                    format!("\n}} {inner_fn_name}().unwrap() }}"),
214                )
215            } else if self.test_id.is_some() {
216                (
217                    format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
218                    format!("\n}} {inner_fn_name}() }}"),
219                )
220            } else {
221                ("fn main() {\n".into(), "\n}".into())
222            };
223            // Note on newlines: We insert a line/newline *before*, and *after*
224            // the doctest and adjust the `line_offset` accordingly.
225            // In the case of `-C instrument-coverage`, this means that the generated
226            // inner `main` function spans from the doctest opening codeblock to the
227            // closing one. For example
228            // /// ``` <- start of the inner main
229            // /// <- code under doctest
230            // /// ``` <- end of the inner main
231            line_offset += 1;
232
233            prog.push_str(&main_pre);
234
235            // add extra 4 spaces for each line to offset the code block
236            if opts.insert_indent_space {
237                write!(
238                    prog,
239                    "{}",
240                    fmt::from_fn(|f| everything_else
241                        .lines()
242                        .map(|line| fmt::from_fn(move |f| write!(f, "    {line}")))
243                        .joined("\n", f))
244                )
245                .unwrap();
246            } else {
247                prog.push_str(everything_else);
248            };
249            prog.push_str(&main_post);
250        }
251
252        debug!("final doctest:\n{prog}");
253
254        (prog, line_offset)
255    }
256}
257
258#[derive(PartialEq, Eq, Debug)]
259enum ParsingResult {
260    Failed,
261    AstError,
262    Ok,
263}
264
265fn cancel_error_count(psess: &ParseSess) {
266    // Reset errors so that they won't be reported as compiler bugs when dropping the
267    // dcx. Any errors in the tests will be reported when the test file is compiled,
268    // Note that we still need to cancel the errors above otherwise `Diag` will panic on
269    // drop.
270    psess.dcx().reset_err_count();
271}
272
273fn parse_source(
274    source: String,
275    info: &mut ParseSourceInfo,
276    crate_name: &Option<&str>,
277) -> ParsingResult {
278    use rustc_errors::DiagCtxt;
279    use rustc_errors::emitter::{Emitter, HumanEmitter};
280    use rustc_parse::parser::ForceCollect;
281    use rustc_span::source_map::FilePathMapping;
282
283    let filename = FileName::anon_source_code(&source);
284
285    // Any errors in parsing should also appear when the doctest is compiled for real, so just
286    // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
287    let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
288    let fallback_bundle = rustc_errors::fallback_fluent_bundle(
289        rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
290        false,
291    );
292    info.supports_color =
293        HumanEmitter::new(stderr_destination(ColorConfig::Auto), fallback_bundle.clone())
294            .supports_color();
295
296    let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle);
297
298    // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
299    let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
300    let psess = ParseSess::with_dcx(dcx, sm);
301
302    let mut parser = match new_parser_from_source_str(&psess, filename, source) {
303        Ok(p) => p,
304        Err(errs) => {
305            errs.into_iter().for_each(|err| err.cancel());
306            cancel_error_count(&psess);
307            return ParsingResult::Failed;
308        }
309    };
310    let mut parsing_result = ParsingResult::Ok;
311
312    // Recurse through functions body. It is necessary because the doctest source code is
313    // wrapped in a function to limit the number of AST errors. If we don't recurse into
314    // functions, we would thing all top-level items (so basically nothing).
315    fn check_item(
316        item: &ast::Item,
317        info: &mut ParseSourceInfo,
318        crate_name: &Option<&str>,
319        is_top_level: bool,
320    ) {
321        if !info.has_global_allocator
322            && item.attrs.iter().any(|attr| attr.name_or_empty() == sym::global_allocator)
323        {
324            info.has_global_allocator = true;
325        }
326        match item.kind {
327            ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => {
328                if item.ident.name == sym::main && is_top_level {
329                    info.has_main_fn = true;
330                }
331                if let Some(ref body) = fn_item.body {
332                    for stmt in &body.stmts {
333                        match stmt.kind {
334                            ast::StmtKind::Item(ref item) => {
335                                check_item(item, info, crate_name, false)
336                            }
337                            ast::StmtKind::MacCall(..) => info.found_macro = true,
338                            _ => {}
339                        }
340                    }
341                }
342            }
343            ast::ItemKind::ExternCrate(original) => {
344                if !info.found_extern_crate
345                    && let Some(crate_name) = crate_name
346                {
347                    info.found_extern_crate = match original {
348                        Some(name) => name.as_str() == *crate_name,
349                        None => item.ident.as_str() == *crate_name,
350                    };
351                }
352            }
353            ast::ItemKind::MacCall(..) => info.found_macro = true,
354            ast::ItemKind::MacroDef(..) => info.has_macro_def = true,
355            _ => {}
356        }
357    }
358
359    loop {
360        match parser.parse_item(ForceCollect::No) {
361            Ok(Some(item)) => {
362                check_item(&item, info, crate_name, true);
363
364                if info.has_main_fn && info.found_extern_crate {
365                    break;
366                }
367            }
368            Ok(None) => break,
369            Err(e) => {
370                parsing_result = ParsingResult::AstError;
371                e.cancel();
372                break;
373            }
374        }
375
376        // The supplied item is only used for diagnostics,
377        // which are swallowed here anyway.
378        parser.maybe_consume_incorrect_semicolon(None);
379    }
380
381    cancel_error_count(&psess);
382    parsing_result
383}
384
385#[derive(Default)]
386struct ParseSourceInfo {
387    has_main_fn: bool,
388    found_extern_crate: bool,
389    found_macro: bool,
390    supports_color: bool,
391    has_global_allocator: bool,
392    has_macro_def: bool,
393}
394
395fn check_for_main_and_extern_crate(
396    crate_name: Option<&str>,
397    original_source_code: &str,
398    everything_else: &str,
399    crates: &str,
400    edition: Edition,
401    can_merge_doctests: bool,
402) -> Result<(ParseSourceInfo, bool), FatalError> {
403    let result = rustc_driver::catch_fatal_errors(|| {
404        rustc_span::create_session_if_not_set_then(edition, |_| {
405            let mut info =
406                ParseSourceInfo { found_extern_crate: crate_name.is_none(), ..Default::default() };
407
408            let mut parsing_result =
409                parse_source(format!("{crates}{everything_else}"), &mut info, &crate_name);
410            // No need to double-check this if the "merged doctests" feature isn't enabled (so
411            // before the 2024 edition).
412            if can_merge_doctests && parsing_result != ParsingResult::Ok {
413                // If we found an AST error, we want to ensure it's because of an expression being
414                // used outside of a function.
415                //
416                // To do so, we wrap in a function in order to make sure that the doctest AST is
417                // correct. For example, if your doctest is `foo::bar()`, if we don't wrap it in a
418                // block, it would emit an AST error, which would be problematic for us since we
419                // want to filter out such errors which aren't "real" errors.
420                //
421                // The end goal is to be able to merge as many doctests as possible as one for much
422                // faster doctests run time.
423                parsing_result = parse_source(
424                    format!("{crates}\nfn __doctest_wrap(){{{everything_else}\n}}"),
425                    &mut info,
426                    &crate_name,
427                );
428            }
429
430            (info, parsing_result)
431        })
432    });
433    let (mut info, parsing_result) = match result {
434        Err(..) | Ok((_, ParsingResult::Failed)) => return Err(FatalError),
435        Ok((info, parsing_result)) => (info, parsing_result),
436    };
437
438    // If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't
439    // see it. In that case, run the old text-based scan to see if they at least have a main
440    // function written inside a macro invocation. See
441    // https://github.com/rust-lang/rust/issues/56898
442    if info.found_macro
443        && !info.has_main_fn
444        && original_source_code
445            .lines()
446            .map(|line| {
447                let comment = line.find("//");
448                if let Some(comment_begins) = comment { &line[0..comment_begins] } else { line }
449            })
450            .any(|code| code.contains("fn main"))
451    {
452        info.has_main_fn = true;
453    }
454
455    Ok((info, parsing_result != ParsingResult::Ok))
456}
457
458enum AttrKind {
459    CrateAttr,
460    Attr,
461}
462
463/// Returns `Some` if the attribute is complete and `Some(true)` if it is an attribute that can be
464/// placed at the crate root.
465fn check_if_attr_is_complete(source: &str, edition: Edition) -> Option<AttrKind> {
466    if source.is_empty() {
467        // Empty content so nothing to check in here...
468        return None;
469    }
470    let not_crate_attrs = [sym::forbid, sym::allow, sym::warn, sym::deny];
471
472    rustc_driver::catch_fatal_errors(|| {
473        rustc_span::create_session_if_not_set_then(edition, |_| {
474            use rustc_errors::DiagCtxt;
475            use rustc_errors::emitter::HumanEmitter;
476            use rustc_span::source_map::FilePathMapping;
477
478            let filename = FileName::anon_source_code(source);
479            // Any errors in parsing should also appear when the doctest is compiled for real, so just
480            // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
481            let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
482            let fallback_bundle = rustc_errors::fallback_fluent_bundle(
483                rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
484                false,
485            );
486
487            let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle);
488
489            let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
490            let psess = ParseSess::with_dcx(dcx, sm);
491            let mut parser = match new_parser_from_source_str(&psess, filename, source.to_owned()) {
492                Ok(p) => p,
493                Err(errs) => {
494                    errs.into_iter().for_each(|err| err.cancel());
495                    // If there is an unclosed delimiter, an error will be returned by the
496                    // tokentrees.
497                    return None;
498                }
499            };
500            // If a parsing error happened, it's very likely that the attribute is incomplete.
501            let ret = match parser.parse_attribute(InnerAttrPolicy::Permitted) {
502                Ok(attr) => {
503                    let attr_name = attr.name_or_empty();
504
505                    if not_crate_attrs.contains(&attr_name) {
506                        // There is one exception to these attributes:
507                        // `#![allow(internal_features)]`. If this attribute is used, we need to
508                        // consider it only as a crate-level attribute.
509                        if attr_name == sym::allow
510                            && let Some(list) = attr.meta_item_list()
511                            && list.iter().any(|sub_attr| {
512                                sub_attr.name_or_empty().as_str() == "internal_features"
513                            })
514                        {
515                            Some(AttrKind::CrateAttr)
516                        } else {
517                            Some(AttrKind::Attr)
518                        }
519                    } else {
520                        Some(AttrKind::CrateAttr)
521                    }
522                }
523                Err(e) => {
524                    e.cancel();
525                    None
526                }
527            };
528            ret
529        })
530    })
531    .unwrap_or(None)
532}
533
534fn handle_attr(mod_attr_pending: &mut String, source_info: &mut SourceInfo, edition: Edition) {
535    if let Some(attr_kind) = check_if_attr_is_complete(mod_attr_pending, edition) {
536        let push_to = match attr_kind {
537            AttrKind::CrateAttr => &mut source_info.crate_attrs,
538            AttrKind::Attr => &mut source_info.maybe_crate_attrs,
539        };
540        push_to.push_str(mod_attr_pending);
541        push_to.push('\n');
542        // If it's complete, then we can clear the pending content.
543        mod_attr_pending.clear();
544    } else {
545        mod_attr_pending.push('\n');
546    }
547}
548
549#[derive(Default)]
550struct SourceInfo {
551    crate_attrs: String,
552    maybe_crate_attrs: String,
553    crates: String,
554    everything_else: String,
555}
556
557fn partition_source(s: &str, edition: Edition) -> Option<SourceInfo> {
558    #[derive(Copy, Clone, PartialEq)]
559    enum PartitionState {
560        Attrs,
561        Crates,
562        Other,
563    }
564    let mut source_info = SourceInfo::default();
565    let mut state = PartitionState::Attrs;
566    let mut mod_attr_pending = String::new();
567
568    for line in s.lines() {
569        let trimline = line.trim();
570
571        // FIXME(misdreavus): if a doc comment is placed on an extern crate statement, it will be
572        // shunted into "everything else"
573        match state {
574            PartitionState::Attrs => {
575                state = if trimline.starts_with("#![") {
576                    mod_attr_pending = line.to_owned();
577                    handle_attr(&mut mod_attr_pending, &mut source_info, edition);
578                    continue;
579                } else if trimline.chars().all(|c| c.is_whitespace())
580                    || (trimline.starts_with("//") && !trimline.starts_with("///"))
581                {
582                    PartitionState::Attrs
583                } else if trimline.starts_with("extern crate")
584                    || trimline.starts_with("#[macro_use] extern crate")
585                {
586                    PartitionState::Crates
587                } else {
588                    // First we check if the previous attribute was "complete"...
589                    if !mod_attr_pending.is_empty() {
590                        // If not, then we append the new line into the pending attribute to check
591                        // if this time it's complete...
592                        mod_attr_pending.push_str(line);
593                        if !trimline.is_empty() {
594                            handle_attr(&mut mod_attr_pending, &mut source_info, edition);
595                        }
596                        continue;
597                    } else {
598                        PartitionState::Other
599                    }
600                };
601            }
602            PartitionState::Crates => {
603                state = if trimline.starts_with("extern crate")
604                    || trimline.starts_with("#[macro_use] extern crate")
605                    || trimline.chars().all(|c| c.is_whitespace())
606                    || (trimline.starts_with("//") && !trimline.starts_with("///"))
607                {
608                    PartitionState::Crates
609                } else {
610                    PartitionState::Other
611                };
612            }
613            PartitionState::Other => {}
614        }
615
616        match state {
617            PartitionState::Attrs => {
618                source_info.crate_attrs.push_str(line);
619                source_info.crate_attrs.push('\n');
620            }
621            PartitionState::Crates => {
622                source_info.crates.push_str(line);
623                source_info.crates.push('\n');
624            }
625            PartitionState::Other => {
626                source_info.everything_else.push_str(line);
627                source_info.everything_else.push('\n');
628            }
629        }
630    }
631
632    if !mod_attr_pending.is_empty() {
633        debug!("invalid doctest code: {s:?}");
634        return None;
635    }
636
637    source_info.everything_else = source_info.everything_else.trim().to_string();
638
639    debug!("crate_attrs:\n{}{}", source_info.crate_attrs, source_info.maybe_crate_attrs);
640    debug!("crates:\n{}", source_info.crates);
641    debug!("after:\n{}", source_info.everything_else);
642
643    Some(source_info)
644}