[go: up one dir, main page]

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