1use std::fmt::{self, Write as _};
5use std::io;
6use std::sync::Arc;
7
8use rustc_ast::token::{Delimiter, TokenKind};
9use rustc_ast::tokenstream::TokenTree;
10use rustc_ast::{self as ast, AttrStyle, HasAttrs, StmtKind};
11use rustc_errors::emitter::get_stderr_color_choice;
12use rustc_errors::{AutoStream, ColorChoice, ColorConfig, DiagCtxtHandle};
13use rustc_parse::lexer::StripTokens;
14use rustc_parse::new_parser_from_source_str;
15use rustc_session::parse::ParseSess;
16use rustc_span::edition::{DEFAULT_EDITION, Edition};
17use rustc_span::source_map::SourceMap;
18use rustc_span::symbol::sym;
19use rustc_span::{DUMMY_SP, FileName, InnerSpan, Span, kw};
20use tracing::debug;
21
22use super::{CodeLineMapping, GlobalTestOptions};
23use crate::config::MergeDoctests;
24use crate::display::Joined as _;
25use crate::html::markdown::LangString;
26
27#[derive(Default)]
28struct ParseSourceInfo {
29 has_main_fn: bool,
30 already_has_extern_crate: bool,
31 supports_color: bool,
32 has_global_allocator: bool,
33 has_macro_def: bool,
34 everything_else: String,
35 crates: String,
36 crate_attrs: String,
37 maybe_crate_attrs: String,
38}
39
40pub(crate) struct BuildDocTestBuilder<'a> {
42 source: &'a str,
43 crate_name: Option<&'a str>,
44 edition: Edition,
45 can_merge_doctests: MergeDoctests,
46 test_id: Option<String>,
48 lang_str: Option<&'a LangString>,
49 span: Span,
50 code_mappings: &'a [CodeLineMapping],
51 global_crate_attrs: Vec<String>,
52}
53
54impl<'a> BuildDocTestBuilder<'a> {
55 pub(crate) fn new(source: &'a str) -> Self {
56 Self {
57 source,
58 crate_name: None,
59 edition: DEFAULT_EDITION,
60 can_merge_doctests: MergeDoctests::Never,
61 test_id: None,
62 lang_str: None,
63 span: DUMMY_SP,
64 code_mappings: &[],
65 global_crate_attrs: Vec::new(),
66 }
67 }
68
69 #[inline]
70 pub(crate) fn crate_name(mut self, crate_name: &'a str) -> Self {
71 self.crate_name = Some(crate_name);
72 self
73 }
74
75 #[inline]
76 pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: MergeDoctests) -> Self {
77 self.can_merge_doctests = can_merge_doctests;
78 self
79 }
80
81 #[inline]
82 pub(crate) fn test_id(mut self, test_id: String) -> Self {
83 self.test_id = Some(test_id);
84 self
85 }
86
87 #[inline]
88 pub(crate) fn lang_str(mut self, lang_str: &'a LangString) -> Self {
89 self.lang_str = Some(lang_str);
90 self
91 }
92
93 #[inline]
94 pub(crate) fn span(mut self, span: Span) -> Self {
95 self.span = span;
96 self
97 }
98
99 #[inline]
100 pub(crate) fn code_mappings(mut self, code_mappings: &'a [CodeLineMapping]) -> Self {
101 self.code_mappings = code_mappings;
102 self
103 }
104
105 #[inline]
106 pub(crate) fn edition(mut self, edition: Edition) -> Self {
107 self.edition = edition;
108 self
109 }
110
111 #[inline]
112 pub(crate) fn global_crate_attrs(mut self, global_crate_attrs: Vec<String>) -> Self {
113 self.global_crate_attrs = global_crate_attrs;
114 self
115 }
116
117 pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder {
118 let BuildDocTestBuilder {
119 source,
120 crate_name,
121 edition,
122 can_merge_doctests,
123 test_id,
125 lang_str,
126 span,
127 code_mappings,
128 global_crate_attrs,
129 } = self;
130
131 let result = rustc_driver::catch_fatal_errors(|| {
132 rustc_span::create_session_if_not_set_then(edition, |_| {
133 parse_source(source, &crate_name, dcx, span, code_mappings)
134 })
135 });
136
137 let Ok(Ok(ParseSourceInfo {
138 has_main_fn,
139 already_has_extern_crate,
140 supports_color,
141 has_global_allocator,
142 has_macro_def,
143 everything_else,
144 crates,
145 crate_attrs,
146 maybe_crate_attrs,
147 })) = result
148 else {
149 return DocTestBuilder::invalid(
152 Vec::new(),
153 String::new(),
154 String::new(),
155 String::new(),
156 source.to_string(),
157 test_id,
158 );
159 };
160
161 debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}");
162 debug!("crates:\n{crates}");
163 debug!("after:\n{everything_else}");
164 debug!("merge-doctests: {can_merge_doctests:?}");
165
166 let opt_out = lang_str.is_some_and(|lang_str| {
171 lang_str.compile_fail || lang_str.test_harness || lang_str.standalone_crate
172 });
173 let can_be_merged = if can_merge_doctests == MergeDoctests::Auto {
174 let will_probably_fail = has_global_allocator
177 || !crate_attrs.is_empty()
178 || (has_macro_def && everything_else.contains("$crate"));
181 !opt_out && !will_probably_fail
182 } else {
183 can_merge_doctests != MergeDoctests::Never && !opt_out
184 };
185 DocTestBuilder {
186 supports_color,
187 has_main_fn,
188 global_crate_attrs,
189 crate_attrs,
190 maybe_crate_attrs,
191 crates,
192 everything_else,
193 already_has_extern_crate,
194 test_id,
195 invalid_ast: false,
196 can_be_merged,
197 }
198 }
199}
200
201pub(crate) struct DocTestBuilder {
204 pub(crate) supports_color: bool,
205 pub(crate) already_has_extern_crate: bool,
206 pub(crate) has_main_fn: bool,
207 pub(crate) global_crate_attrs: Vec<String>,
208 pub(crate) crate_attrs: String,
209 pub(crate) maybe_crate_attrs: String,
212 pub(crate) crates: String,
213 pub(crate) everything_else: String,
214 pub(crate) test_id: Option<String>,
215 pub(crate) invalid_ast: bool,
216 pub(crate) can_be_merged: bool,
217}
218
219pub(crate) struct WrapperInfo {
221 pub(crate) before: String,
222 pub(crate) after: String,
223 pub(crate) returns_result: bool,
224 insert_indent_space: bool,
225}
226
227impl WrapperInfo {
228 fn len(&self) -> usize {
229 self.before.len() + self.after.len()
230 }
231}
232
233pub(crate) enum DocTestWrapResult {
235 Valid {
236 crate_level_code: String,
237 wrapper: Option<WrapperInfo>,
243 code: String,
246 },
247 SyntaxError(String),
249}
250
251impl std::string::ToString for DocTestWrapResult {
252 fn to_string(&self) -> String {
253 match self {
254 Self::SyntaxError(s) => s.clone(),
255 Self::Valid { crate_level_code, wrapper, code } => {
256 let mut prog_len = code.len() + crate_level_code.len();
257 if let Some(wrapper) = wrapper {
258 prog_len += wrapper.len();
259 if wrapper.insert_indent_space {
260 prog_len += code.lines().count() * 4;
261 }
262 }
263 let mut prog = String::with_capacity(prog_len);
264
265 prog.push_str(crate_level_code);
266 if let Some(wrapper) = wrapper {
267 prog.push_str(&wrapper.before);
268
269 if wrapper.insert_indent_space {
271 write!(
272 prog,
273 "{}",
274 fmt::from_fn(|f| code
275 .lines()
276 .map(|line| fmt::from_fn(move |f| write!(f, " {line}")))
277 .joined("\n", f))
278 )
279 .unwrap();
280 } else {
281 prog.push_str(code);
282 }
283 prog.push_str(&wrapper.after);
284 } else {
285 prog.push_str(code);
286 }
287 prog
288 }
289 }
290 }
291}
292
293impl DocTestBuilder {
294 fn invalid(
295 global_crate_attrs: Vec<String>,
296 crate_attrs: String,
297 maybe_crate_attrs: String,
298 crates: String,
299 everything_else: String,
300 test_id: Option<String>,
301 ) -> Self {
302 Self {
303 supports_color: false,
304 has_main_fn: false,
305 global_crate_attrs,
306 crate_attrs,
307 maybe_crate_attrs,
308 crates,
309 everything_else,
310 already_has_extern_crate: false,
311 test_id,
312 invalid_ast: true,
313 can_be_merged: false,
314 }
315 }
316
317 pub(crate) fn generate_unique_doctest(
320 &self,
321 test_code: &str,
322 dont_insert_main: bool,
323 opts: &GlobalTestOptions,
324 crate_name: Option<&str>,
325 ) -> (DocTestWrapResult, usize) {
326 if self.invalid_ast {
327 debug!("invalid AST:\n{test_code}");
330 return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0);
331 }
332 let mut line_offset = 0;
333 let mut crate_level_code = String::new();
334 let processed_code = self.everything_else.trim();
335 if self.global_crate_attrs.is_empty() {
336 crate_level_code.push_str("#![allow(unused)]\n");
341 line_offset += 1;
342 }
343
344 for attr in &self.global_crate_attrs {
346 crate_level_code.push_str(&format!("#![{attr}]\n"));
347 line_offset += 1;
348 }
349
350 if !self.crate_attrs.is_empty() {
353 crate_level_code.push_str(&self.crate_attrs);
354 if !self.crate_attrs.ends_with('\n') {
355 crate_level_code.push('\n');
356 }
357 }
358 if !self.maybe_crate_attrs.is_empty() {
359 crate_level_code.push_str(&self.maybe_crate_attrs);
360 if !self.maybe_crate_attrs.ends_with('\n') {
361 crate_level_code.push('\n');
362 }
363 }
364 if !self.crates.is_empty() {
365 crate_level_code.push_str(&self.crates);
366 if !self.crates.ends_with('\n') {
367 crate_level_code.push('\n');
368 }
369 }
370
371 if !self.already_has_extern_crate &&
374 !opts.no_crate_inject &&
375 let Some(crate_name) = crate_name &&
376 crate_name != "std" &&
377 test_code.contains(crate_name)
382 {
383 crate_level_code.push_str("#[allow(unused_extern_crates)]\n");
386
387 crate_level_code.push_str(&format!("extern crate r#{crate_name};\n"));
388 line_offset += 1;
389 }
390
391 let wrapper = if dont_insert_main
393 || self.has_main_fn
394 || crate_level_code.contains("![no_std]")
395 {
396 None
397 } else {
398 let returns_result = processed_code.ends_with("(())");
399 let inner_fn_name = if let Some(ref test_id) = self.test_id {
402 format!("_doctest_main_{test_id}")
403 } else {
404 "_inner".into()
405 };
406 let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
407 let (main_pre, main_post) = if returns_result {
408 (
409 format!(
410 "fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n",
411 ),
412 format!("\n}} {inner_fn_name}().unwrap() }}"),
413 )
414 } else if self.test_id.is_some() {
415 (
416 format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
417 format!("\n}} {inner_fn_name}() }}"),
418 )
419 } else {
420 ("fn main() {\n".into(), "\n}".into())
421 };
422 line_offset += 1;
431
432 Some(WrapperInfo {
433 before: main_pre,
434 after: main_post,
435 returns_result,
436 insert_indent_space: opts.insert_indent_space,
437 })
438 };
439
440 (
441 DocTestWrapResult::Valid {
442 code: processed_code.to_string(),
443 wrapper,
444 crate_level_code,
445 },
446 line_offset,
447 )
448 }
449}
450
451fn reset_error_count(psess: &ParseSess) {
452 psess.dcx().reset_err_count();
457}
458
459const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
460
461fn parse_source(
462 source: &str,
463 crate_name: &Option<&str>,
464 parent_dcx: Option<DiagCtxtHandle<'_>>,
465 span: Span,
466 code_mappings: &[CodeLineMapping],
467) -> Result<ParseSourceInfo, ()> {
468 use rustc_errors::DiagCtxt;
469 use rustc_errors::annotate_snippet_emitter_writer::AnnotateSnippetEmitter;
470 use rustc_span::source_map::FilePathMapping;
471
472 let mut info =
473 ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };
474
475 let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");
476
477 let filename = FileName::anon_source_code(&wrapped_source);
478
479 let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
480 let supports_color = match get_stderr_color_choice(ColorConfig::Auto, &std::io::stderr()) {
481 ColorChoice::Auto => unreachable!(),
482 ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
483 ColorChoice::Never => false,
484 };
485 info.supports_color = supports_color;
486 let emitter = AnnotateSnippetEmitter::new(AutoStream::never(Box::new(io::sink())));
489
490 let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
492 let psess = ParseSess::with_dcx(dcx, sm);
493
494 let mut parser =
496 match new_parser_from_source_str(&psess, filename, wrapped_source, StripTokens::Nothing) {
497 Ok(p) => p,
498 Err(errs) => {
499 errs.into_iter().for_each(|err| err.cancel());
500 reset_error_count(&psess);
501 return Err(());
502 }
503 };
504
505 fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {
506 let extra_len = DOCTEST_CODE_WRAPPER.len();
507 let mut hi = span.hi().0 as usize - extra_len;
510 if hi > source.len() {
511 hi = source.len();
512 }
513 s.push_str(&source[*prev_span_hi..hi]);
514 *prev_span_hi = hi;
515 }
516
517 fn span_in_doctest_source(span: Span, code_mappings: &[CodeLineMapping]) -> Option<Span> {
518 let extra_len = DOCTEST_CODE_WRAPPER.len();
519 let lo = (span.lo().0 as usize).checked_sub(extra_len)?;
520 let hi = (span.hi().0 as usize).checked_sub(extra_len)?;
521 if hi < lo {
522 return None;
523 }
524 code_mappings.iter().find_map(|mapping| {
525 if mapping.generated.start <= lo && hi <= mapping.generated.end {
526 let start = lo - mapping.generated.start;
527 let end = hi - mapping.generated.start;
528 Some(mapping.original.from_inner(InnerSpan::new(start, end)))
529 } else {
530 None
531 }
532 })
533 }
534
535 fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool {
536 let mut is_extern_crate = false;
537 if !info.has_global_allocator
538 && item.attrs.iter().any(|attr| attr.has_name(sym::global_allocator))
539 {
540 info.has_global_allocator = true;
541 }
542 match item.kind {
543 ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => {
544 if fn_item.ident.name == sym::main {
545 info.has_main_fn = true;
546 }
547 }
548 ast::ItemKind::ExternCrate(original, ident) => {
549 is_extern_crate = true;
550 if !info.already_has_extern_crate
551 && let Some(crate_name) = crate_name
552 {
553 info.already_has_extern_crate = match original {
554 Some(name) => name.as_str() == *crate_name,
555 None => ident.as_str() == *crate_name,
556 };
557 }
558 }
559 ast::ItemKind::MacroDef(..) => {
560 info.has_macro_def = true;
561 }
562 _ => {}
563 }
564 is_extern_crate
565 }
566
567 let mut prev_span_hi = 0;
568 let not_crate_attrs = &[sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect];
569 let parsed = parser.parse_item(
570 rustc_parse::parser::ForceCollect::No,
571 rustc_parse::parser::AllowConstBlockItems::No,
572 );
573
574 let result = match parsed {
575 Ok(Some(ref item))
576 if let ast::ItemKind::Fn(ref fn_item) = item.kind
577 && let Some(ref body) = fn_item.body =>
578 {
579 for attr in &item.attrs {
580 if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {
581 if attr.has_name(sym::allow)
585 && let Some(list) = attr.meta_item_list()
586 && list.iter().any(|sub_attr| {
587 sub_attr.has_name(sym::internal_features)
588 || sub_attr.has_name(sym::incomplete_features)
589 })
590 {
591 push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
592 } else {
593 push_to_s(
594 &mut info.maybe_crate_attrs,
595 source,
596 attr.span,
597 &mut prev_span_hi,
598 );
599 }
600 } else {
601 push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
602 }
603 }
604 let mut has_non_items = false;
605 let mut first_non_item_span = None;
606 for stmt in &body.stmts {
607 let mut is_extern_crate = false;
608 match stmt.kind {
609 StmtKind::Item(ref item) => {
610 is_extern_crate = check_item(item, &mut info, crate_name);
611 }
612 StmtKind::MacCall(ref mac_call) => {
615 if !info.has_main_fn {
616 let mut iter = mac_call.mac.args.tokens.iter();
621 while let Some(token) = iter.next() {
622 if let TokenTree::Token(token, _) = token
623 && let TokenKind::Ident(kw::Fn, _) = token.kind
624 && let Some(TokenTree::Token(ident, _)) = iter.peek()
625 && let TokenKind::Ident(sym::main, _) = ident.kind
626 && let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = {
627 iter.next();
628 iter.peek()
629 }
630 {
631 info.has_main_fn = true;
632 break;
633 }
634 }
635 }
636 }
637 StmtKind::Expr(ref expr) => {
638 if matches!(expr.kind, ast::ExprKind::Err(_)) {
639 reset_error_count(&psess);
640 return Err(());
641 }
642 has_non_items = true;
643 first_non_item_span.get_or_insert(stmt.span);
644 }
645 StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => {
646 has_non_items = true;
647 first_non_item_span.get_or_insert(stmt.span);
648 }
649 }
650
651 let mut span = stmt.span;
654 if let Some(attr) =
655 stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer)
656 {
657 span = span.with_lo(attr.span.lo());
658 }
659 if info.everything_else.is_empty()
660 && (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty())
661 {
662 push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);
666 }
667 if !is_extern_crate {
668 push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);
669 } else {
670 push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
671 }
672 }
673 if has_non_items {
674 let warning_span = first_non_item_span
675 .and_then(|span| span_in_doctest_source(span, code_mappings))
676 .unwrap_or(span);
677 if info.has_main_fn
678 && let Some(dcx) = parent_dcx
679 && !warning_span.is_dummy()
680 {
681 dcx.span_warn(
682 warning_span,
683 "the `main` function of this doctest won't be run as it contains \
684 expressions at the top level, meaning that the whole doctest code will be \
685 wrapped in a function",
686 );
687 }
688 info.has_main_fn = false;
689 }
690 Ok(info)
691 }
692 Err(e) => {
693 e.cancel();
694 Err(())
695 }
696 _ => Err(()),
697 };
698
699 reset_error_count(&psess);
700 result
701}