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