| //! Logic for transforming the raw code given by the user into something actually | |
| //! runnable, e.g. by adding a `main` function if it doesn't already exist. | |
| use std::fmt::{self, Write as _}; | |
| use std::io; | |
| use std::sync::Arc; | |
| use rustc_ast::token::{Delimiter, TokenKind}; | |
| use rustc_ast::tokenstream::TokenTree; | |
| use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind}; | |
| use rustc_errors::emitter::get_stderr_color_choice; | |
| use rustc_errors::{AutoStream, ColorChoice, ColorConfig, DiagCtxtHandle}; | |
| use rustc_parse::lexer::StripTokens; | |
| use rustc_parse::new_parser_from_source_str; | |
| use rustc_session::parse::ParseSess; | |
| use rustc_span::edition::{DEFAULT_EDITION, Edition}; | |
| use rustc_span::source_map::SourceMap; | |
| use rustc_span::symbol::sym; | |
| use rustc_span::{DUMMY_SP, FileName, Span, kw}; | |
| use tracing::debug; | |
| use super::GlobalTestOptions; | |
| use crate::config::MergeDoctests; | |
| use crate::display::Joined as _; | |
| use crate::html::markdown::LangString; | |
| #[derive(Default)] | |
| struct ParseSourceInfo { | |
| has_main_fn: bool, | |
| already_has_extern_crate: bool, | |
| supports_color: bool, | |
| has_global_allocator: bool, | |
| has_macro_def: bool, | |
| everything_else: String, | |
| crates: String, | |
| crate_attrs: String, | |
| maybe_crate_attrs: String, | |
| } | |
| /// Builder type for `DocTestBuilder`. | |
| pub(crate) struct BuildDocTestBuilder<'a> { | |
| source: &'a str, | |
| crate_name: Option<&'a str>, | |
| edition: Edition, | |
| can_merge_doctests: MergeDoctests, | |
| // If `test_id` is `None`, it means we're generating code for a code example "run" link. | |
| test_id: Option<String>, | |
| lang_str: Option<&'a LangString>, | |
| span: Span, | |
| global_crate_attrs: Vec<String>, | |
| } | |
| impl<'a> BuildDocTestBuilder<'a> { | |
| pub(crate) fn new(source: &'a str) -> Self { | |
| Self { | |
| source, | |
| crate_name: None, | |
| edition: DEFAULT_EDITION, | |
| can_merge_doctests: MergeDoctests::Never, | |
| test_id: None, | |
| lang_str: None, | |
| span: DUMMY_SP, | |
| global_crate_attrs: Vec::new(), | |
| } | |
| } | |
| #[inline] | |
| pub(crate) fn crate_name(mut self, crate_name: &'a str) -> Self { | |
| self.crate_name = Some(crate_name); | |
| self | |
| } | |
| #[inline] | |
| pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: MergeDoctests) -> Self { | |
| self.can_merge_doctests = can_merge_doctests; | |
| self | |
| } | |
| #[inline] | |
| pub(crate) fn test_id(mut self, test_id: String) -> Self { | |
| self.test_id = Some(test_id); | |
| self | |
| } | |
| #[inline] | |
| pub(crate) fn lang_str(mut self, lang_str: &'a LangString) -> Self { | |
| self.lang_str = Some(lang_str); | |
| self | |
| } | |
| #[inline] | |
| pub(crate) fn span(mut self, span: Span) -> Self { | |
| self.span = span; | |
| self | |
| } | |
| #[inline] | |
| pub(crate) fn edition(mut self, edition: Edition) -> Self { | |
| self.edition = edition; | |
| self | |
| } | |
| #[inline] | |
| pub(crate) fn global_crate_attrs(mut self, global_crate_attrs: Vec<String>) -> Self { | |
| self.global_crate_attrs = global_crate_attrs; | |
| self | |
| } | |
| pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder { | |
| let BuildDocTestBuilder { | |
| source, | |
| crate_name, | |
| edition, | |
| can_merge_doctests, | |
| // If `test_id` is `None`, it means we're generating code for a code example "run" link. | |
| test_id, | |
| lang_str, | |
| span, | |
| global_crate_attrs, | |
| } = self; | |
| let result = rustc_driver::catch_fatal_errors(|| { | |
| rustc_span::create_session_if_not_set_then(edition, |_| { | |
| parse_source(source, &crate_name, dcx, span) | |
| }) | |
| }); | |
| let Ok(Ok(ParseSourceInfo { | |
| has_main_fn, | |
| already_has_extern_crate, | |
| supports_color, | |
| has_global_allocator, | |
| has_macro_def, | |
| everything_else, | |
| crates, | |
| crate_attrs, | |
| maybe_crate_attrs, | |
| })) = result | |
| else { | |
| // If the AST returned an error, we don't want this doctest to be merged with the | |
| // others. | |
| return DocTestBuilder::invalid( | |
| Vec::new(), | |
| String::new(), | |
| String::new(), | |
| String::new(), | |
| source.to_string(), | |
| test_id, | |
| ); | |
| }; | |
| debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}"); | |
| debug!("crates:\n{crates}"); | |
| debug!("after:\n{everything_else}"); | |
| debug!("merge-doctests: {can_merge_doctests:?}"); | |
| // Up until now, we've been dealing with settings for the whole crate. | |
| // Now, infer settings for this particular test. | |
| // | |
| // Avoid tests with incompatible attributes. | |
| let opt_out = lang_str.is_some_and(|lang_str| { | |
| lang_str.compile_fail || lang_str.test_harness || lang_str.standalone_crate | |
| }); | |
| let can_be_merged = if can_merge_doctests == MergeDoctests::Auto { | |
| // We try to look at the contents of the test to detect whether it should be merged. | |
| // This is not a complete list of possible failures, but it catches many cases. | |
| let will_probably_fail = has_global_allocator | |
| || !crate_attrs.is_empty() | |
| // If this is a merged doctest and a defined macro uses `$crate`, then the path will | |
| // not work, so better not put it into merged doctests. | |
| || (has_macro_def && everything_else.contains("$crate")); | |
| !opt_out && !will_probably_fail | |
| } else { | |
| can_merge_doctests != MergeDoctests::Never && !opt_out | |
| }; | |
| DocTestBuilder { | |
| supports_color, | |
| has_main_fn, | |
| global_crate_attrs, | |
| crate_attrs, | |
| maybe_crate_attrs, | |
| crates, | |
| everything_else, | |
| already_has_extern_crate, | |
| test_id, | |
| invalid_ast: false, | |
| can_be_merged, | |
| } | |
| } | |
| } | |
| /// This struct contains information about the doctest itself which is then used to generate | |
| /// doctest source code appropriately. | |
| pub(crate) struct DocTestBuilder { | |
| pub(crate) supports_color: bool, | |
| pub(crate) already_has_extern_crate: bool, | |
| pub(crate) has_main_fn: bool, | |
| pub(crate) global_crate_attrs: Vec<String>, | |
| pub(crate) crate_attrs: String, | |
| /// If this is a merged doctest, it will be put into `everything_else`, otherwise it will | |
| /// put into `crate_attrs`. | |
| pub(crate) maybe_crate_attrs: String, | |
| pub(crate) crates: String, | |
| pub(crate) everything_else: String, | |
| pub(crate) test_id: Option<String>, | |
| pub(crate) invalid_ast: bool, | |
| pub(crate) can_be_merged: bool, | |
| } | |
| /// Contains needed information for doctest to be correctly generated with expected "wrapping". | |
| pub(crate) struct WrapperInfo { | |
| pub(crate) before: String, | |
| pub(crate) after: String, | |
| pub(crate) returns_result: bool, | |
| insert_indent_space: bool, | |
| } | |
| impl WrapperInfo { | |
| fn len(&self) -> usize { | |
| self.before.len() + self.after.len() | |
| } | |
| } | |
| /// Contains a doctest information. Can be converted into code with the `to_string()` method. | |
| pub(crate) enum DocTestWrapResult { | |
| Valid { | |
| crate_level_code: String, | |
| /// This field can be `None` if one of the following conditions is true: | |
| /// | |
| /// * The doctest's codeblock has the `test_harness` attribute. | |
| /// * The doctest has a `main` function. | |
| /// * The doctest has the `![no_std]` attribute. | |
| wrapper: Option<WrapperInfo>, | |
| /// Contains the doctest processed code without the wrappers (which are stored in the | |
| /// `wrapper` field). | |
| code: String, | |
| }, | |
| /// Contains the original source code. | |
| SyntaxError(String), | |
| } | |
| impl std::string::ToString for DocTestWrapResult { | |
| fn to_string(&self) -> String { | |
| match self { | |
| Self::SyntaxError(s) => s.clone(), | |
| Self::Valid { crate_level_code, wrapper, code } => { | |
| let mut prog_len = code.len() + crate_level_code.len(); | |
| if let Some(wrapper) = wrapper { | |
| prog_len += wrapper.len(); | |
| if wrapper.insert_indent_space { | |
| prog_len += code.lines().count() * 4; | |
| } | |
| } | |
| let mut prog = String::with_capacity(prog_len); | |
| prog.push_str(crate_level_code); | |
| if let Some(wrapper) = wrapper { | |
| prog.push_str(&wrapper.before); | |
| // add extra 4 spaces for each line to offset the code block | |
| if wrapper.insert_indent_space { | |
| write!( | |
| prog, | |
| "{}", | |
| fmt::from_fn(|f| code | |
| .lines() | |
| .map(|line| fmt::from_fn(move |f| write!(f, " {line}"))) | |
| .joined("\n", f)) | |
| ) | |
| .unwrap(); | |
| } else { | |
| prog.push_str(code); | |
| } | |
| prog.push_str(&wrapper.after); | |
| } else { | |
| prog.push_str(code); | |
| } | |
| prog | |
| } | |
| } | |
| } | |
| } | |
| impl DocTestBuilder { | |
| fn invalid( | |
| global_crate_attrs: Vec<String>, | |
| crate_attrs: String, | |
| maybe_crate_attrs: String, | |
| crates: String, | |
| everything_else: String, | |
| test_id: Option<String>, | |
| ) -> Self { | |
| Self { | |
| supports_color: false, | |
| has_main_fn: false, | |
| global_crate_attrs, | |
| crate_attrs, | |
| maybe_crate_attrs, | |
| crates, | |
| everything_else, | |
| already_has_extern_crate: false, | |
| test_id, | |
| invalid_ast: true, | |
| can_be_merged: false, | |
| } | |
| } | |
| /// Transforms a test into code that can be compiled into a Rust binary, and returns the number of | |
| /// lines before the test code begins. | |
| pub(crate) fn generate_unique_doctest( | |
| &self, | |
| test_code: &str, | |
| dont_insert_main: bool, | |
| opts: &GlobalTestOptions, | |
| crate_name: Option<&str>, | |
| ) -> (DocTestWrapResult, usize) { | |
| if self.invalid_ast { | |
| // If the AST failed to compile, no need to go generate a complete doctest, the error | |
| // will be better this way. | |
| debug!("invalid AST:\n{test_code}"); | |
| return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0); | |
| } | |
| let mut line_offset = 0; | |
| let mut crate_level_code = String::new(); | |
| let processed_code = self.everything_else.trim(); | |
| if self.global_crate_attrs.is_empty() { | |
| // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some | |
| // lints that are commonly triggered in doctests. The crate-level test attributes are | |
| // commonly used to make tests fail in case they trigger warnings, so having this there in | |
| // that case may cause some tests to pass when they shouldn't have. | |
| crate_level_code.push_str("#![allow(unused)]\n"); | |
| line_offset += 1; | |
| } | |
| // Next, any attributes that came from #![doc(test(attr(...)))]. | |
| for attr in &self.global_crate_attrs { | |
| crate_level_code.push_str(&format!("#![{attr}]\n")); | |
| line_offset += 1; | |
| } | |
| // Now push any outer attributes from the example, assuming they | |
| // are intended to be crate attributes. | |
| if !self.crate_attrs.is_empty() { | |
| crate_level_code.push_str(&self.crate_attrs); | |
| if !self.crate_attrs.ends_with('\n') { | |
| crate_level_code.push('\n'); | |
| } | |
| } | |
| if !self.maybe_crate_attrs.is_empty() { | |
| crate_level_code.push_str(&self.maybe_crate_attrs); | |
| if !self.maybe_crate_attrs.ends_with('\n') { | |
| crate_level_code.push('\n'); | |
| } | |
| } | |
| if !self.crates.is_empty() { | |
| crate_level_code.push_str(&self.crates); | |
| if !self.crates.ends_with('\n') { | |
| crate_level_code.push('\n'); | |
| } | |
| } | |
| // Don't inject `extern crate std` because it's already injected by the | |
| // compiler. | |
| if !self.already_has_extern_crate && | |
| !opts.no_crate_inject && | |
| let Some(crate_name) = crate_name && | |
| crate_name != "std" && | |
| // Don't inject `extern crate` if the crate is never used. | |
| // NOTE: this is terribly inaccurate because it doesn't actually | |
| // parse the source, but only has false positives, not false | |
| // negatives. | |
| test_code.contains(crate_name) | |
| { | |
| // rustdoc implicitly inserts an `extern crate` item for the own crate | |
| // which may be unused, so we need to allow the lint. | |
| crate_level_code.push_str("#[allow(unused_extern_crates)]\n"); | |
| crate_level_code.push_str(&format!("extern crate r#{crate_name};\n")); | |
| line_offset += 1; | |
| } | |
| // FIXME: This code cannot yet handle no_std test cases yet | |
| let wrapper = if dont_insert_main | |
| || self.has_main_fn | |
| || crate_level_code.contains("![no_std]") | |
| { | |
| None | |
| } else { | |
| let returns_result = processed_code.ends_with("(())"); | |
| // Give each doctest main function a unique name. | |
| // This is for example needed for the tooling around `-C instrument-coverage`. | |
| let inner_fn_name = if let Some(ref test_id) = self.test_id { | |
| format!("_doctest_main_{test_id}") | |
| } else { | |
| "_inner".into() | |
| }; | |
| let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" }; | |
| let (main_pre, main_post) = if returns_result { | |
| ( | |
| format!( | |
| "fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n", | |
| ), | |
| format!("\n}} {inner_fn_name}().unwrap() }}"), | |
| ) | |
| } else if self.test_id.is_some() { | |
| ( | |
| format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",), | |
| format!("\n}} {inner_fn_name}() }}"), | |
| ) | |
| } else { | |
| ("fn main() {\n".into(), "\n}".into()) | |
| }; | |
| // Note on newlines: We insert a line/newline *before*, and *after* | |
| // the doctest and adjust the `line_offset` accordingly. | |
| // In the case of `-C instrument-coverage`, this means that the generated | |
| // inner `main` function spans from the doctest opening codeblock to the | |
| // closing one. For example | |
| // /// ``` <- start of the inner main | |
| // /// <- code under doctest | |
| // /// ``` <- end of the inner main | |
| line_offset += 1; | |
| Some(WrapperInfo { | |
| before: main_pre, | |
| after: main_post, | |
| returns_result, | |
| insert_indent_space: opts.insert_indent_space, | |
| }) | |
| }; | |
| ( | |
| DocTestWrapResult::Valid { | |
| code: processed_code.to_string(), | |
| wrapper, | |
| crate_level_code, | |
| }, | |
| line_offset, | |
| ) | |
| } | |
| } | |
| fn reset_error_count(psess: &ParseSess) { | |
| // Reset errors so that they won't be reported as compiler bugs when dropping the | |
| // dcx. Any errors in the tests will be reported when the test file is compiled, | |
| // Note that we still need to cancel the errors above otherwise `Diag` will panic on | |
| // drop. | |
| psess.dcx().reset_err_count(); | |
| } | |
| const DOCTEST_CODE_WRAPPER: &str = "fn f(){"; | |
| fn parse_source( | |
| source: &str, | |
| crate_name: &Option<&str>, | |
| parent_dcx: Option<DiagCtxtHandle<'_>>, | |
| span: Span, | |
| ) -> Result<ParseSourceInfo, ()> { | |
| use rustc_errors::DiagCtxt; | |
| use rustc_errors::annotate_snippet_emitter_writer::AnnotateSnippetEmitter; | |
| use rustc_span::source_map::FilePathMapping; | |
| let mut info = | |
| ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() }; | |
| let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}"); | |
| let filename = FileName::anon_source_code(&wrapped_source); | |
| let sm = Arc::new(SourceMap::new(FilePathMapping::empty())); | |
| let supports_color = match get_stderr_color_choice(ColorConfig::Auto, &std::io::stderr()) { | |
| ColorChoice::Auto => unreachable!(), | |
| ColorChoice::AlwaysAnsi | ColorChoice::Always => true, | |
| ColorChoice::Never => false, | |
| }; | |
| info.supports_color = supports_color; | |
| // Any errors in parsing should also appear when the doctest is compiled for real, so just | |
| // send all the errors that the parser emits directly into a `Sink` instead of stderr. | |
| let emitter = AnnotateSnippetEmitter::new(AutoStream::never(Box::new(io::sink()))); | |
| // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser | |
| let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings(); | |
| let psess = ParseSess::with_dcx(dcx, sm); | |
| // Don't strip any tokens; it wouldn't matter anyway because the source is wrapped in a function. | |
| let mut parser = | |
| match new_parser_from_source_str(&psess, filename, wrapped_source, StripTokens::Nothing) { | |
| Ok(p) => p, | |
| Err(errs) => { | |
| errs.into_iter().for_each(|err| err.cancel()); | |
| reset_error_count(&psess); | |
| return Err(()); | |
| } | |
| }; | |
| fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) { | |
| let extra_len = DOCTEST_CODE_WRAPPER.len(); | |
| // We need to shift by the length of `DOCTEST_CODE_WRAPPER` because we | |
| // added it at the beginning of the source we provided to the parser. | |
| let mut hi = span.hi().0 as usize - extra_len; | |
| if hi > source.len() { | |
| hi = source.len(); | |
| } | |
| s.push_str(&source[*prev_span_hi..hi]); | |
| *prev_span_hi = hi; | |
| } | |
| fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool { | |
| let mut is_extern_crate = false; | |
| if !info.has_global_allocator | |
| && item.attrs.iter().any(|attr| attr.has_name(sym::global_allocator)) | |
| { | |
| info.has_global_allocator = true; | |
| } | |
| match item.kind { | |
| ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => { | |
| if fn_item.ident.name == sym::main { | |
| info.has_main_fn = true; | |
| } | |
| } | |
| ast::ItemKind::ExternCrate(original, ident) => { | |
| is_extern_crate = true; | |
| if !info.already_has_extern_crate | |
| && let Some(crate_name) = crate_name | |
| { | |
| info.already_has_extern_crate = match original { | |
| Some(name) => name.as_str() == *crate_name, | |
| None => ident.as_str() == *crate_name, | |
| }; | |
| } | |
| } | |
| ast::ItemKind::MacroDef(..) => { | |
| info.has_macro_def = true; | |
| } | |
| _ => {} | |
| } | |
| is_extern_crate | |
| } | |
| let mut prev_span_hi = 0; | |
| let not_crate_attrs = &[sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect]; | |
| let parsed = parser.parse_item( | |
| rustc_parse::parser::ForceCollect::No, | |
| rustc_parse::parser::AllowConstBlockItems::No, | |
| ); | |
| let result = match parsed { | |
| Ok(Some(ref item)) | |
| if let ast::ItemKind::Fn(ref fn_item) = item.kind | |
| && let Some(ref body) = fn_item.body => | |
| { | |
| for attr in &item.attrs { | |
| if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) { | |
| // There is one exception to these attributes: | |
| // `#![allow(internal_features)]`. If this attribute is used, we need to | |
| // consider it only as a crate-level attribute. | |
| if attr.has_name(sym::allow) | |
| && let Some(list) = attr.meta_item_list() | |
| && list.iter().any(|sub_attr| { | |
| sub_attr.has_name(sym::internal_features) | |
| || sub_attr.has_name(sym::incomplete_features) | |
| }) | |
| { | |
| push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi); | |
| } else { | |
| push_to_s( | |
| &mut info.maybe_crate_attrs, | |
| source, | |
| attr.span, | |
| &mut prev_span_hi, | |
| ); | |
| } | |
| } else { | |
| push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi); | |
| } | |
| } | |
| let mut has_non_items = false; | |
| for stmt in &body.stmts { | |
| let mut is_extern_crate = false; | |
| match stmt.kind { | |
| StmtKind::Item(ref item) => { | |
| is_extern_crate = check_item(item, &mut info, crate_name); | |
| } | |
| // We assume that the macro calls will expand to item(s) even though they could | |
| // expand to statements and expressions. | |
| StmtKind::MacCall(ref mac_call) => { | |
| if !info.has_main_fn { | |
| // For backward compatibility, we look for the token sequence `fn main(…)` | |
| // in the macro input (!) to crudely detect main functions "masked by a | |
| // wrapper macro". For the record, this is a horrible heuristic! | |
| // See <https://github.com/rust-lang/rust/issues/56898>. | |
| let mut iter = mac_call.mac.args.tokens.iter(); | |
| while let Some(token) = iter.next() { | |
| if let TokenTree::Token(token, _) = token | |
| && let TokenKind::Ident(kw::Fn, _) = token.kind | |
| && let Some(TokenTree::Token(ident, _)) = iter.peek() | |
| && let TokenKind::Ident(sym::main, _) = ident.kind | |
| && let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = { | |
| iter.next(); | |
| iter.peek() | |
| } | |
| { | |
| info.has_main_fn = true; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| StmtKind::Expr(ref expr) => { | |
| if matches!(expr.kind, ast::ExprKind::Err(_)) { | |
| reset_error_count(&psess); | |
| return Err(()); | |
| } | |
| has_non_items = true; | |
| } | |
| StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true, | |
| } | |
| // Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to | |
| // tweak the span to include the attributes as well. | |
| let mut span = stmt.span; | |
| if let Some(attr) = | |
| stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer) | |
| { | |
| span = span.with_lo(attr.span.lo()); | |
| } | |
| if info.everything_else.is_empty() | |
| && (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty()) | |
| { | |
| // To keep the doctest code "as close as possible" to the original, we insert | |
| // all the code located between this new span and the previous span which | |
| // might contain code comments and backlines. | |
| push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi); | |
| } | |
| if !is_extern_crate { | |
| push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi); | |
| } else { | |
| push_to_s(&mut info.crates, source, span, &mut prev_span_hi); | |
| } | |
| } | |
| if has_non_items { | |
| if info.has_main_fn | |
| && let Some(dcx) = parent_dcx | |
| && !span.is_dummy() | |
| { | |
| dcx.span_warn( | |
| span, | |
| "the `main` function of this doctest won't be run as it contains \ | |
| expressions at the top level, meaning that the whole doctest code will be \ | |
| wrapped in a function", | |
| ); | |
| } | |
| info.has_main_fn = false; | |
| } | |
| Ok(info) | |
| } | |
| Err(e) => { | |
| e.cancel(); | |
| Err(()) | |
| } | |
| _ => Err(()), | |
| }; | |
| reset_error_count(&psess); | |
| result | |
| } |