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, Span, kw};
20use tracing::debug;
21
22use super::GlobalTestOptions;
23use crate::display::Joined as _;
24use crate::html::markdown::LangString;
25
26#[derive(Default)]
27struct ParseSourceInfo {
28 has_main_fn: bool,
29 already_has_extern_crate: bool,
30 supports_color: bool,
31 has_global_allocator: bool,
32 has_macro_def: bool,
33 everything_else: String,
34 crates: String,
35 crate_attrs: String,
36 maybe_crate_attrs: String,
37}
38
39pub(crate) struct BuildDocTestBuilder<'a> {
41 source: &'a str,
42 crate_name: Option<&'a str>,
43 edition: Edition,
44 can_merge_doctests: bool,
45 test_id: Option<String>,
47 lang_str: Option<&'a LangString>,
48 span: Span,
49 global_crate_attrs: Vec<String>,
50}
51
52impl<'a> BuildDocTestBuilder<'a> {
53 pub(crate) fn new(source: &'a str) -> Self {
54 Self {
55 source,
56 crate_name: None,
57 edition: DEFAULT_EDITION,
58 can_merge_doctests: false,
59 test_id: None,
60 lang_str: None,
61 span: DUMMY_SP,
62 global_crate_attrs: Vec::new(),
63 }
64 }
65
66 #[inline]
67 pub(crate) fn crate_name(mut self, crate_name: &'a str) -> Self {
68 self.crate_name = Some(crate_name);
69 self
70 }
71
72 #[inline]
73 pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: bool) -> Self {
74 self.can_merge_doctests = can_merge_doctests;
75 self
76 }
77
78 #[inline]
79 pub(crate) fn test_id(mut self, test_id: String) -> Self {
80 self.test_id = Some(test_id);
81 self
82 }
83
84 #[inline]
85 pub(crate) fn lang_str(mut self, lang_str: &'a LangString) -> Self {
86 self.lang_str = Some(lang_str);
87 self
88 }
89
90 #[inline]
91 pub(crate) fn span(mut self, span: Span) -> Self {
92 self.span = span;
93 self
94 }
95
96 #[inline]
97 pub(crate) fn edition(mut self, edition: Edition) -> Self {
98 self.edition = edition;
99 self
100 }
101
102 #[inline]
103 pub(crate) fn global_crate_attrs(mut self, global_crate_attrs: Vec<String>) -> Self {
104 self.global_crate_attrs = global_crate_attrs;
105 self
106 }
107
108 pub(crate) fn build(self, dcx: Option<DiagCtxtHandle<'_>>) -> DocTestBuilder {
109 let BuildDocTestBuilder {
110 source,
111 crate_name,
112 edition,
113 can_merge_doctests,
114 test_id,
116 lang_str,
117 span,
118 global_crate_attrs,
119 } = self;
120 let can_merge_doctests = can_merge_doctests
121 && lang_str.is_some_and(|lang_str| {
122 !lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
123 });
124
125 let result = rustc_driver::catch_fatal_errors(|| {
126 rustc_span::create_session_if_not_set_then(edition, |_| {
127 parse_source(source, &crate_name, dcx, span)
128 })
129 });
130
131 let Ok(Ok(ParseSourceInfo {
132 has_main_fn,
133 already_has_extern_crate,
134 supports_color,
135 has_global_allocator,
136 has_macro_def,
137 everything_else,
138 crates,
139 crate_attrs,
140 maybe_crate_attrs,
141 })) = result
142 else {
143 return DocTestBuilder::invalid(
146 Vec::new(),
147 String::new(),
148 String::new(),
149 String::new(),
150 source.to_string(),
151 test_id,
152 );
153 };
154
155 debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}");
156 debug!("crates:\n{crates}");
157 debug!("after:\n{everything_else}");
158
159 let can_be_merged = can_merge_doctests
161 && !has_global_allocator
162 && crate_attrs.is_empty()
163 && !(has_macro_def && everything_else.contains("$crate"));
166 DocTestBuilder {
167 supports_color,
168 has_main_fn,
169 global_crate_attrs,
170 crate_attrs,
171 maybe_crate_attrs,
172 crates,
173 everything_else,
174 already_has_extern_crate,
175 test_id,
176 invalid_ast: false,
177 can_be_merged,
178 }
179 }
180}
181
182pub(crate) struct DocTestBuilder {
185 pub(crate) supports_color: bool,
186 pub(crate) already_has_extern_crate: bool,
187 pub(crate) has_main_fn: bool,
188 pub(crate) global_crate_attrs: Vec<String>,
189 pub(crate) crate_attrs: String,
190 pub(crate) maybe_crate_attrs: String,
193 pub(crate) crates: String,
194 pub(crate) everything_else: String,
195 pub(crate) test_id: Option<String>,
196 pub(crate) invalid_ast: bool,
197 pub(crate) can_be_merged: bool,
198}
199
200pub(crate) struct WrapperInfo {
202 pub(crate) before: String,
203 pub(crate) after: String,
204 pub(crate) returns_result: bool,
205 insert_indent_space: bool,
206}
207
208impl WrapperInfo {
209 fn len(&self) -> usize {
210 self.before.len() + self.after.len()
211 }
212}
213
214pub(crate) enum DocTestWrapResult {
216 Valid {
217 crate_level_code: String,
218 wrapper: Option<WrapperInfo>,
224 code: String,
227 },
228 SyntaxError(String),
230}
231
232impl std::string::ToString for DocTestWrapResult {
233 fn to_string(&self) -> String {
234 match self {
235 Self::SyntaxError(s) => s.clone(),
236 Self::Valid { crate_level_code, wrapper, code } => {
237 let mut prog_len = code.len() + crate_level_code.len();
238 if let Some(wrapper) = wrapper {
239 prog_len += wrapper.len();
240 if wrapper.insert_indent_space {
241 prog_len += code.lines().count() * 4;
242 }
243 }
244 let mut prog = String::with_capacity(prog_len);
245
246 prog.push_str(crate_level_code);
247 if let Some(wrapper) = wrapper {
248 prog.push_str(&wrapper.before);
249
250 if wrapper.insert_indent_space {
252 write!(
253 prog,
254 "{}",
255 fmt::from_fn(|f| code
256 .lines()
257 .map(|line| fmt::from_fn(move |f| write!(f, " {line}")))
258 .joined("\n", f))
259 )
260 .unwrap();
261 } else {
262 prog.push_str(code);
263 }
264 prog.push_str(&wrapper.after);
265 } else {
266 prog.push_str(code);
267 }
268 prog
269 }
270 }
271 }
272}
273
274impl DocTestBuilder {
275 fn invalid(
276 global_crate_attrs: Vec<String>,
277 crate_attrs: String,
278 maybe_crate_attrs: String,
279 crates: String,
280 everything_else: String,
281 test_id: Option<String>,
282 ) -> Self {
283 Self {
284 supports_color: false,
285 has_main_fn: false,
286 global_crate_attrs,
287 crate_attrs,
288 maybe_crate_attrs,
289 crates,
290 everything_else,
291 already_has_extern_crate: false,
292 test_id,
293 invalid_ast: true,
294 can_be_merged: false,
295 }
296 }
297
298 pub(crate) fn generate_unique_doctest(
301 &self,
302 test_code: &str,
303 dont_insert_main: bool,
304 opts: &GlobalTestOptions,
305 crate_name: Option<&str>,
306 ) -> (DocTestWrapResult, usize) {
307 if self.invalid_ast {
308 debug!("invalid AST:\n{test_code}");
311 return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0);
312 }
313 let mut line_offset = 0;
314 let mut crate_level_code = String::new();
315 let processed_code = self.everything_else.trim();
316 if self.global_crate_attrs.is_empty() {
317 crate_level_code.push_str("#![allow(unused)]\n");
322 line_offset += 1;
323 }
324
325 for attr in &self.global_crate_attrs {
327 crate_level_code.push_str(&format!("#![{attr}]\n"));
328 line_offset += 1;
329 }
330
331 if !self.crate_attrs.is_empty() {
334 crate_level_code.push_str(&self.crate_attrs);
335 if !self.crate_attrs.ends_with('\n') {
336 crate_level_code.push('\n');
337 }
338 }
339 if !self.maybe_crate_attrs.is_empty() {
340 crate_level_code.push_str(&self.maybe_crate_attrs);
341 if !self.maybe_crate_attrs.ends_with('\n') {
342 crate_level_code.push('\n');
343 }
344 }
345 if !self.crates.is_empty() {
346 crate_level_code.push_str(&self.crates);
347 if !self.crates.ends_with('\n') {
348 crate_level_code.push('\n');
349 }
350 }
351
352 if !self.already_has_extern_crate &&
355 !opts.no_crate_inject &&
356 let Some(crate_name) = crate_name &&
357 crate_name != "std" &&
358 test_code.contains(crate_name)
363 {
364 crate_level_code.push_str("#[allow(unused_extern_crates)]\n");
367
368 crate_level_code.push_str(&format!("extern crate r#{crate_name};\n"));
369 line_offset += 1;
370 }
371
372 let wrapper = if dont_insert_main
374 || self.has_main_fn
375 || crate_level_code.contains("![no_std]")
376 {
377 None
378 } else {
379 let returns_result = processed_code.ends_with("(())");
380 let inner_fn_name = if let Some(ref test_id) = self.test_id {
383 format!("_doctest_main_{test_id}")
384 } else {
385 "_inner".into()
386 };
387 let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
388 let (main_pre, main_post) = if returns_result {
389 (
390 format!(
391 "fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n",
392 ),
393 format!("\n}} {inner_fn_name}().unwrap() }}"),
394 )
395 } else if self.test_id.is_some() {
396 (
397 format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
398 format!("\n}} {inner_fn_name}() }}"),
399 )
400 } else {
401 ("fn main() {\n".into(), "\n}".into())
402 };
403 line_offset += 1;
412
413 Some(WrapperInfo {
414 before: main_pre,
415 after: main_post,
416 returns_result,
417 insert_indent_space: opts.insert_indent_space,
418 })
419 };
420
421 (
422 DocTestWrapResult::Valid {
423 code: processed_code.to_string(),
424 wrapper,
425 crate_level_code,
426 },
427 line_offset,
428 )
429 }
430}
431
432fn reset_error_count(psess: &ParseSess) {
433 psess.dcx().reset_err_count();
438}
439
440const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
441
442fn parse_source(
443 source: &str,
444 crate_name: &Option<&str>,
445 parent_dcx: Option<DiagCtxtHandle<'_>>,
446 span: Span,
447) -> Result<ParseSourceInfo, ()> {
448 use rustc_errors::DiagCtxt;
449 use rustc_errors::emitter::HumanEmitter;
450 use rustc_span::source_map::FilePathMapping;
451
452 let mut info =
453 ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };
454
455 let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");
456
457 let filename = FileName::anon_source_code(&wrapped_source);
458
459 let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
460 let translator = rustc_driver::default_translator();
461 let supports_color = match get_stderr_color_choice(ColorConfig::Auto, &std::io::stderr()) {
462 ColorChoice::Auto => unreachable!(),
463 ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
464 ColorChoice::Never => false,
465 };
466 info.supports_color = supports_color;
467 let emitter = HumanEmitter::new(AutoStream::never(Box::new(io::sink())), translator);
470
471 let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
473 let psess = ParseSess::with_dcx(dcx, sm);
474
475 let mut parser =
477 match new_parser_from_source_str(&psess, filename, wrapped_source, StripTokens::Nothing) {
478 Ok(p) => p,
479 Err(errs) => {
480 errs.into_iter().for_each(|err| err.cancel());
481 reset_error_count(&psess);
482 return Err(());
483 }
484 };
485
486 fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {
487 let extra_len = DOCTEST_CODE_WRAPPER.len();
488 let mut hi = span.hi().0 as usize - extra_len;
491 if hi > source.len() {
492 hi = source.len();
493 }
494 s.push_str(&source[*prev_span_hi..hi]);
495 *prev_span_hi = hi;
496 }
497
498 fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool {
499 let mut is_extern_crate = false;
500 if !info.has_global_allocator
501 && item.attrs.iter().any(|attr| attr.has_name(sym::global_allocator))
502 {
503 info.has_global_allocator = true;
504 }
505 match item.kind {
506 ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => {
507 if fn_item.ident.name == sym::main {
508 info.has_main_fn = true;
509 }
510 }
511 ast::ItemKind::ExternCrate(original, ident) => {
512 is_extern_crate = true;
513 if !info.already_has_extern_crate
514 && let Some(crate_name) = crate_name
515 {
516 info.already_has_extern_crate = match original {
517 Some(name) => name.as_str() == *crate_name,
518 None => ident.as_str() == *crate_name,
519 };
520 }
521 }
522 ast::ItemKind::MacroDef(..) => {
523 info.has_macro_def = true;
524 }
525 _ => {}
526 }
527 is_extern_crate
528 }
529
530 let mut prev_span_hi = 0;
531 let not_crate_attrs = &[sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect];
532 let parsed = parser.parse_item(rustc_parse::parser::ForceCollect::No);
533
534 let result = match parsed {
535 Ok(Some(ref item))
536 if let ast::ItemKind::Fn(ref fn_item) = item.kind
537 && let Some(ref body) = fn_item.body =>
538 {
539 for attr in &item.attrs {
540 if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {
541 if attr.has_name(sym::allow)
545 && let Some(list) = attr.meta_item_list()
546 && list.iter().any(|sub_attr| sub_attr.has_name(sym::internal_features))
547 {
548 push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
549 } else {
550 push_to_s(
551 &mut info.maybe_crate_attrs,
552 source,
553 attr.span,
554 &mut prev_span_hi,
555 );
556 }
557 } else {
558 push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
559 }
560 }
561 let mut has_non_items = false;
562 for stmt in &body.stmts {
563 let mut is_extern_crate = false;
564 match stmt.kind {
565 StmtKind::Item(ref item) => {
566 is_extern_crate = check_item(item, &mut info, crate_name);
567 }
568 StmtKind::MacCall(ref mac_call) => {
571 if !info.has_main_fn {
572 let mut iter = mac_call.mac.args.tokens.iter();
577 while let Some(token) = iter.next() {
578 if let TokenTree::Token(token, _) = token
579 && let TokenKind::Ident(kw::Fn, _) = token.kind
580 && let Some(TokenTree::Token(ident, _)) = iter.peek()
581 && let TokenKind::Ident(sym::main, _) = ident.kind
582 && let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = {
583 iter.next();
584 iter.peek()
585 }
586 {
587 info.has_main_fn = true;
588 break;
589 }
590 }
591 }
592 }
593 StmtKind::Expr(ref expr) => {
594 if matches!(expr.kind, ast::ExprKind::Err(_)) {
595 reset_error_count(&psess);
596 return Err(());
597 }
598 has_non_items = true;
599 }
600 StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true,
601 }
602
603 let mut span = stmt.span;
606 if let Some(attr) =
607 stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer)
608 {
609 span = span.with_lo(attr.span.lo());
610 }
611 if info.everything_else.is_empty()
612 && (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty())
613 {
614 push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);
618 }
619 if !is_extern_crate {
620 push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);
621 } else {
622 push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
623 }
624 }
625 if has_non_items {
626 if info.has_main_fn
627 && let Some(dcx) = parent_dcx
628 && !span.is_dummy()
629 {
630 dcx.span_warn(
631 span,
632 "the `main` function of this doctest won't be run as it contains \
633 expressions at the top level, meaning that the whole doctest code will be \
634 wrapped in a function",
635 );
636 }
637 info.has_main_fn = false;
638 }
639 Ok(info)
640 }
641 Err(e) => {
642 e.cancel();
643 Err(())
644 }
645 _ => Err(()),
646 };
647
648 reset_error_count(&psess);
649 result
650}