src/librustdoc/doctest/make.rs RUST 699 lines View on github.com → Search inside
1//! Logic for transforming the raw code given by the user into something actually2//! runnable, e.g. by adding a `main` function if it doesn't already exist.34use std::fmt::{self, Write as _};5use std::io;6use std::sync::Arc;78use 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, InnerSpan, Span, kw};20use tracing::debug;2122use super::{CodeLineMapping, GlobalTestOptions};23use crate::config::MergeDoctests;24use crate::display::Joined as _;25use crate::html::markdown::LangString;2627#[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    /// Inner attributes (`#![...]`) from the source that have to be put at the crate level.37    crate_attrs: String,38    /// Inner attributes (`#![...]`) from the source that can be put into a module and therefore do39    /// not inhibit merging: even in the merged test, the attributes can be isolated to the test.40    module_attrs: String,41}4243/// Builder type for `DocTestBuilder`.44pub(crate) struct BuildDocTestBuilder<'a> {45    source: &'a str,46    crate_name: Option<&'a str>,47    edition: Edition,48    can_merge_doctests: MergeDoctests,49    // If `test_id` is `None`, it means we're generating code for a code example "run" link.50    test_id: Option<String>,51    lang_str: Option<&'a LangString>,52    span: Span,53    code_mappings: &'a [CodeLineMapping],54    global_crate_attrs: Vec<String>,55}5657impl<'a> BuildDocTestBuilder<'a> {58    pub(crate) fn new(source: &'a str) -> Self {59        Self {60            source,61            crate_name: None,62            edition: DEFAULT_EDITION,63            can_merge_doctests: MergeDoctests::Never,64            test_id: None,65            lang_str: None,66            span: DUMMY_SP,67            code_mappings: &[],68            global_crate_attrs: Vec::new(),69        }70    }7172    #[inline]73    pub(crate) fn crate_name(mut self, crate_name: &'a str) -> Self {74        self.crate_name = Some(crate_name);75        self76    }7778    #[inline]79    pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: MergeDoctests) -> Self {80        self.can_merge_doctests = can_merge_doctests;81        self82    }8384    #[inline]85    pub(crate) fn test_id(mut self, test_id: String) -> Self {86        self.test_id = Some(test_id);87        self88    }8990    #[inline]91    pub(crate) fn lang_str(mut self, lang_str: &'a LangString) -> Self {92        self.lang_str = Some(lang_str);93        self94    }9596    #[inline]97    pub(crate) fn span(mut self, span: Span) -> Self {98        self.span = span;99        self100    }101102    #[inline]103    pub(crate) fn code_mappings(mut self, code_mappings: &'a [CodeLineMapping]) -> Self {104        self.code_mappings = code_mappings;105        self106    }107108    #[inline]109    pub(crate) fn edition(mut self, edition: Edition) -> Self {110        self.edition = edition;111        self112    }113114    #[inline]115    pub(crate) fn global_crate_attrs(mut self, global_crate_attrs: Vec<String>) -> Self {116        self.global_crate_attrs = global_crate_attrs;117        self118    }119120    pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder {121        let BuildDocTestBuilder {122            source,123            crate_name,124            edition,125            can_merge_doctests,126            // If `test_id` is `None`, it means we're generating code for a code example "run" link.127            test_id,128            lang_str,129            span,130            code_mappings,131            global_crate_attrs,132        } = self;133134        let result = rustc_driver::catch_fatal_errors(|| {135            rustc_span::create_session_if_not_set_then(edition, |_| {136                parse_source(source, &crate_name, dcx, span, code_mappings)137            })138        });139140        let Ok(Ok(ParseSourceInfo {141            has_main_fn,142            already_has_extern_crate,143            supports_color,144            has_global_allocator,145            has_macro_def,146            everything_else,147            crates,148            crate_attrs,149            module_attrs,150        })) = result151        else {152            // If the AST returned an error, we don't want this doctest to be merged with the153            // others.154            return DocTestBuilder::invalid(155                Vec::new(),156                String::new(),157                String::new(),158                String::new(),159                source.to_string(),160                test_id,161            );162        };163164        debug!("crate_attrs:\n{crate_attrs}{module_attrs}");165        debug!("crates:\n{crates}");166        debug!("after:\n{everything_else}");167        debug!("merge-doctests: {can_merge_doctests:?}");168169        // Up until now, we've been dealing with settings for the whole crate.170        // Now, infer settings for this particular test.171        //172        // Avoid tests with incompatible attributes.173        let opt_out = lang_str.is_some_and(|lang_str| {174            lang_str.compile_fail || lang_str.test_harness || lang_str.standalone_crate175        });176        let can_be_merged = if can_merge_doctests == MergeDoctests::Auto {177            // We try to look at the contents of the test to detect whether it should be merged.178            // This is not a complete list of possible failures, but it catches many cases.179            let will_probably_fail = has_global_allocator180                || !crate_attrs.is_empty()181                // If this is a merged doctest and a defined macro uses `$crate`, then the path will182                // not work, so better not put it into merged doctests.183                || (has_macro_def && everything_else.contains("$crate"));184            !opt_out && !will_probably_fail185        } else {186            can_merge_doctests != MergeDoctests::Never && !opt_out187        };188        DocTestBuilder {189            supports_color,190            has_main_fn,191            global_crate_attrs,192            crate_attrs,193            module_attrs,194            crates,195            everything_else,196            already_has_extern_crate,197            test_id,198            invalid_ast: false,199            can_be_merged,200        }201    }202}203204/// This struct contains information about the doctest itself which is then used to generate205/// doctest source code appropriately.206pub(crate) struct DocTestBuilder {207    pub(crate) supports_color: bool,208    pub(crate) already_has_extern_crate: bool,209    pub(crate) has_main_fn: bool,210    pub(crate) global_crate_attrs: Vec<String>,211    pub(crate) crate_attrs: String,212    /// If this is a merged doctest, it will be put into `everything_else`, otherwise it will213    /// put into `crate_attrs`.214    pub(crate) module_attrs: String,215    pub(crate) crates: String,216    pub(crate) everything_else: String,217    pub(crate) test_id: Option<String>,218    pub(crate) invalid_ast: bool,219    pub(crate) can_be_merged: bool,220}221222/// Contains needed information for doctest to be correctly generated with expected "wrapping".223pub(crate) struct WrapperInfo {224    pub(crate) before: String,225    pub(crate) after: String,226    pub(crate) returns_result: bool,227    insert_indent_space: bool,228}229230impl WrapperInfo {231    fn len(&self) -> usize {232        self.before.len() + self.after.len()233    }234}235236/// Contains a doctest information. Can be converted into code with the `to_string()` method.237pub(crate) enum DocTestWrapResult {238    Valid {239        crate_level_code: String,240        /// This field can be `None` if one of the following conditions is true:241        ///242        /// * The doctest's codeblock has the `test_harness` attribute.243        /// * The doctest has a `main` function.244        /// * The doctest has the `![no_std]` attribute.245        wrapper: Option<WrapperInfo>,246        /// Contains the doctest processed code without the wrappers (which are stored in the247        /// `wrapper` field).248        code: String,249    },250    /// Contains the original source code.251    SyntaxError(String),252}253254impl std::string::ToString for DocTestWrapResult {255    fn to_string(&self) -> String {256        match self {257            Self::SyntaxError(s) => s.clone(),258            Self::Valid { crate_level_code, wrapper, code } => {259                let mut prog_len = code.len() + crate_level_code.len();260                if let Some(wrapper) = wrapper {261                    prog_len += wrapper.len();262                    if wrapper.insert_indent_space {263                        prog_len += code.lines().count() * 4;264                    }265                }266                let mut prog = String::with_capacity(prog_len);267268                prog.push_str(crate_level_code);269                if let Some(wrapper) = wrapper {270                    prog.push_str(&wrapper.before);271272                    // add extra 4 spaces for each line to offset the code block273                    if wrapper.insert_indent_space {274                        write!(275                            prog,276                            "{}",277                            fmt::from_fn(|f| code278                                .lines()279                                .map(|line| fmt::from_fn(move |f| write!(f, "    {line}")))280                                .joined("\n", f))281                        )282                        .unwrap();283                    } else {284                        prog.push_str(code);285                    }286                    prog.push_str(&wrapper.after);287                } else {288                    prog.push_str(code);289                }290                prog291            }292        }293    }294}295296impl DocTestBuilder {297    fn invalid(298        global_crate_attrs: Vec<String>,299        crate_attrs: String,300        module_attrs: String,301        crates: String,302        everything_else: String,303        test_id: Option<String>,304    ) -> Self {305        Self {306            supports_color: false,307            has_main_fn: false,308            global_crate_attrs,309            crate_attrs,310            module_attrs,311            crates,312            everything_else,313            already_has_extern_crate: false,314            test_id,315            invalid_ast: true,316            can_be_merged: false,317        }318    }319320    /// Transforms a test into code that can be compiled into a Rust binary, and returns the number of321    /// lines before the test code begins.322    pub(crate) fn generate_unique_doctest(323        &self,324        test_code: &str,325        dont_insert_main: bool,326        opts: &GlobalTestOptions,327        crate_name: Option<&str>,328    ) -> (DocTestWrapResult, usize) {329        if self.invalid_ast {330            // If the AST failed to compile, no need to go generate a complete doctest, the error331            // will be better this way.332            debug!("invalid AST:\n{test_code}");333            return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0);334        }335        let mut line_offset = 0;336        let mut crate_level_code = String::new();337        let processed_code = self.everything_else.trim();338        if self.global_crate_attrs.is_empty() {339            // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some340            // lints that are commonly triggered in doctests. The crate-level test attributes are341            // commonly used to make tests fail in case they trigger warnings, so having this there in342            // that case may cause some tests to pass when they shouldn't have.343            crate_level_code.push_str("#![allow(unused)]\n");344            line_offset += 1;345        }346347        // Next, any attributes that came from #![doc(test(attr(...)))].348        for attr in &self.global_crate_attrs {349            crate_level_code.push_str(&format!("#![{attr}]\n"));350            line_offset += 1;351        }352353        // Now push any outer attributes from the example (both crate and module attributes).354        if !self.crate_attrs.is_empty() {355            crate_level_code.push_str(&self.crate_attrs);356            if !self.crate_attrs.ends_with('\n') {357                crate_level_code.push('\n');358            }359        }360        if !self.module_attrs.is_empty() {361            crate_level_code.push_str(&self.module_attrs);362            if !self.module_attrs.ends_with('\n') {363                crate_level_code.push('\n');364            }365        }366        if !self.crates.is_empty() {367            crate_level_code.push_str(&self.crates);368            if !self.crates.ends_with('\n') {369                crate_level_code.push('\n');370            }371        }372373        // Don't inject `extern crate std` because it's already injected by the374        // compiler.375        if !self.already_has_extern_crate &&376            !opts.no_crate_inject &&377            let Some(crate_name) = crate_name &&378            crate_name != "std" &&379            // Don't inject `extern crate` if the crate is never used.380            // NOTE: this is terribly inaccurate because it doesn't actually381            // parse the source, but only has false positives, not false382            // negatives.383            test_code.contains(crate_name)384        {385            // rustdoc implicitly inserts an `extern crate` item for the own crate386            // which may be unused, so we need to allow the lint.387            crate_level_code.push_str("#[allow(unused_extern_crates)]\n");388389            crate_level_code.push_str(&format!("extern crate r#{crate_name};\n"));390            line_offset += 1;391        }392393        // FIXME: This code cannot yet handle no_std test cases yet394        let wrapper = if dont_insert_main395            || self.has_main_fn396            || crate_level_code.contains("![no_std]")397        {398            None399        } else {400            let returns_result = processed_code.ends_with("(())");401            // Give each doctest main function a unique name.402            // This is for example needed for the tooling around `-C instrument-coverage`.403            let inner_fn_name = if let Some(ref test_id) = self.test_id {404                format!("_doctest_main_{test_id}")405            } else {406                "_inner".into()407            };408            let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };409            let (main_pre, main_post) = if returns_result {410                (411                    format!(412                        "fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n",413                    ),414                    format!("\n}} {inner_fn_name}().unwrap() }}"),415                )416            } else if self.test_id.is_some() {417                (418                    format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),419                    format!("\n}} {inner_fn_name}() }}"),420                )421            } else {422                ("fn main() {\n".into(), "\n}".into())423            };424            // Note on newlines: We insert a line/newline *before*, and *after*425            // the doctest and adjust the `line_offset` accordingly.426            // In the case of `-C instrument-coverage`, this means that the generated427            // inner `main` function spans from the doctest opening codeblock to the428            // closing one. For example429            // /// ``` <- start of the inner main430            // /// <- code under doctest431            // /// ``` <- end of the inner main432            line_offset += 1;433434            Some(WrapperInfo {435                before: main_pre,436                after: main_post,437                returns_result,438                insert_indent_space: opts.insert_indent_space,439            })440        };441442        (443            DocTestWrapResult::Valid {444                code: processed_code.to_string(),445                wrapper,446                crate_level_code,447            },448            line_offset,449        )450    }451}452453fn reset_error_count(psess: &ParseSess) {454    // Reset errors so that they won't be reported as compiler bugs when dropping the455    // dcx. Any errors in the tests will be reported when the test file is compiled,456    // Note that we still need to cancel the errors above otherwise `Diag` will panic on457    // drop.458    psess.dcx().reset_err_count();459}460461const DOCTEST_CODE_WRAPPER: &str = "fn f(){";462463fn parse_source(464    source: &str,465    crate_name: &Option<&str>,466    parent_dcx: Option<DiagCtxtHandle<'_>>,467    span: Span,468    code_mappings: &[CodeLineMapping],469) -> Result<ParseSourceInfo, ()> {470    use rustc_errors::DiagCtxt;471    use rustc_errors::annotate_snippet_emitter_writer::AnnotateSnippetEmitter;472    use rustc_span::source_map::FilePathMapping;473474    let mut info =475        ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };476477    let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");478479    let filename = FileName::anon_source_code(&wrapped_source);480481    let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));482    let supports_color = match get_stderr_color_choice(ColorConfig::Auto, &std::io::stderr()) {483        ColorChoice::Auto => unreachable!(),484        ColorChoice::AlwaysAnsi | ColorChoice::Always => true,485        ColorChoice::Never => false,486    };487    info.supports_color = supports_color;488    // Any errors in parsing should also appear when the doctest is compiled for real, so just489    // send all the errors that the parser emits directly into a `Sink` instead of stderr.490    let emitter = AnnotateSnippetEmitter::new(AutoStream::never(Box::new(io::sink())));491492    // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser493    let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();494    let psess = ParseSess::with_dcx(dcx, sm);495496    // Don't strip any tokens; it wouldn't matter anyway because the source is wrapped in a function.497    let mut parser =498        match new_parser_from_source_str(&psess, filename, wrapped_source, StripTokens::Nothing) {499            Ok(p) => p,500            Err(errs) => {501                errs.into_iter().for_each(|err| err.cancel());502                reset_error_count(&psess);503                return Err(());504            }505        };506507    fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {508        let extra_len = DOCTEST_CODE_WRAPPER.len();509        // We need to shift by the length of `DOCTEST_CODE_WRAPPER` because we510        // added it at the beginning of the source we provided to the parser.511        let mut hi = span.hi().0 as usize - extra_len;512        if hi > source.len() {513            hi = source.len();514        }515        s.push_str(&source[*prev_span_hi..hi]);516        *prev_span_hi = hi;517    }518519    fn span_in_doctest_source(span: Span, code_mappings: &[CodeLineMapping]) -> Option<Span> {520        let extra_len = DOCTEST_CODE_WRAPPER.len();521        let lo = (span.lo().0 as usize).checked_sub(extra_len)?;522        let hi = (span.hi().0 as usize).checked_sub(extra_len)?;523        if hi < lo {524            return None;525        }526        code_mappings.iter().find_map(|mapping| {527            if mapping.generated.start <= lo && hi <= mapping.generated.end {528                let start = lo - mapping.generated.start;529                let end = hi - mapping.generated.start;530                Some(mapping.original.from_inner(InnerSpan::new(start, end)))531            } else {532                None533            }534        })535    }536537    fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool {538        let mut is_extern_crate = false;539        if !info.has_global_allocator540            && item.attrs.iter().any(|attr| attr.has_name(sym::global_allocator))541        {542            info.has_global_allocator = true;543        }544        match item.kind {545            ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => {546                if fn_item.ident.name == sym::main {547                    info.has_main_fn = true;548                }549            }550            ast::ItemKind::ExternCrate(original, ident) => {551                is_extern_crate = true;552                if !info.already_has_extern_crate553                    && let Some(crate_name) = crate_name554                {555                    info.already_has_extern_crate = match original {556                        Some(name) => name.as_str() == *crate_name,557                        None => ident.as_str() == *crate_name,558                    };559                }560            }561            ast::ItemKind::MacroDef(..) => {562                info.has_macro_def = true;563            }564            _ => {}565        }566        is_extern_crate567    }568569    let mut prev_span_hi = 0;570    let not_crate_attrs = &[sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect];571    let parsed = parser.parse_item(572        rustc_parse::parser::ForceCollect::No,573        rustc_parse::parser::AllowConstBlockItems::No,574    );575576    let result = match parsed {577        Ok(Some(ref item))578            if let ast::ItemKind::Fn(ref fn_item) = item.kind579                && let Some(ref body) = fn_item.body =>580        {581            for attr in &item.attrs {582                if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {583                    // There is one exception to these attributes:584                    // `#![allow(internal_features)]`. If this attribute is used, we need to585                    // consider it only as a crate-level attribute.586                    if attr.has_name(sym::allow)587                        && let Some(list) = attr.meta_item_list()588                        && list.iter().any(|sub_attr| {589                            sub_attr.has_name(sym::internal_features)590                                || sub_attr.has_name(sym::incomplete_features)591                        })592                    {593                        push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);594                    } else {595                        push_to_s(&mut info.module_attrs, source, attr.span, &mut prev_span_hi);596                    }597                } else {598                    push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);599                }600            }601            let mut has_non_items = false;602            let mut first_non_item_span = None;603            for stmt in &body.stmts {604                let mut is_extern_crate = false;605                match stmt.kind {606                    StmtKind::Item(ref item) => {607                        is_extern_crate = check_item(item, &mut info, crate_name);608                    }609                    // We assume that the macro calls will expand to item(s) even though they could610                    // expand to statements and expressions.611                    StmtKind::MacCall(ref mac_call) => {612                        if !info.has_main_fn {613                            // For backward compatibility, we look for the token sequence `fn main(…)`614                            // in the macro input (!) to crudely detect main functions "masked by a615                            // wrapper macro". For the record, this is a horrible heuristic!616                            // See <https://github.com/rust-lang/rust/issues/56898>.617                            let mut iter = mac_call.mac.args.tokens.iter();618                            while let Some(token) = iter.next() {619                                if let TokenTree::Token(token, _) = token620                                    && let TokenKind::Ident(kw::Fn, _) = token.kind621                                    && let Some(TokenTree::Token(ident, _)) = iter.peek()622                                    && let TokenKind::Ident(sym::main, _) = ident.kind623                                    && let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = {624                                        iter.next();625                                        iter.peek()626                                    }627                                {628                                    info.has_main_fn = true;629                                    break;630                                }631                            }632                        }633                    }634                    StmtKind::Expr(ref expr) => {635                        if matches!(expr.kind, ast::ExprKind::Err(_)) {636                            reset_error_count(&psess);637                            return Err(());638                        }639                        has_non_items = true;640                        first_non_item_span.get_or_insert(stmt.span);641                    }642                    StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => {643                        has_non_items = true;644                        first_non_item_span.get_or_insert(stmt.span);645                    }646                }647648                // Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to649                // tweak the span to include the attributes as well.650                let mut span = stmt.span;651                if let Some(attr) =652                    stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer)653                {654                    span = span.with_lo(attr.span.lo());655                }656                if info.everything_else.is_empty()657                    && (!info.module_attrs.is_empty() || !info.crate_attrs.is_empty())658                {659                    // To keep the doctest code "as close as possible" to the original, we insert660                    // all the code located between this new span and the previous span which661                    // might contain code comments and backlines.662                    push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);663                }664                if !is_extern_crate {665                    push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);666                } else {667                    push_to_s(&mut info.crates, source, span, &mut prev_span_hi);668                }669            }670            if has_non_items {671                let warning_span = first_non_item_span672                    .and_then(|span| span_in_doctest_source(span, code_mappings))673                    .unwrap_or(span);674                if info.has_main_fn675                    && let Some(dcx) = parent_dcx676                    && !warning_span.is_dummy()677                {678                    dcx.span_warn(679                        warning_span,680                        "the `main` function of this doctest won't be run as it contains \681                         expressions at the top level, meaning that the whole doctest code will be \682                         wrapped in a function",683                    );684                }685                info.has_main_fn = false;686            }687            Ok(info)688        }689        Err(e) => {690            e.cancel();691            Err(())692        }693        _ => Err(()),694    };695696    reset_error_count(&psess);697    result698}

Code quality findings 3

Warning: '.unwrap()' will panic on None/Err variants. Prefer using pattern matching (match, if let), combinators (map, and_then), or the '?' operator for robust error handling.
warning correctness unwrap-usage
.unwrap();
Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
warning correctness unchecked-indexing
s.push_str(&source[*prev_span_hi..hi]);
Performance Info: Calling .push() repeatedly inside a loop without prior capacity reservation can lead to multiple reallocations. Consider using `Vec::with_capacity(n)` or `vec.reserve(n)` if the approximate number of elements is known.
info performance push-without-reserve
crate_level_code.push('\n');

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.

Generated by searchcode.com · powered by code_get_file + code_get_findings