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::stderr_destination;
12use rustc_errors::{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::{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 info.supports_color =
462 HumanEmitter::new(stderr_destination(ColorConfig::Auto), translator.clone())
463 .supports_color();
464 let emitter = HumanEmitter::new(Box::new(io::sink()), translator);
467
468 let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
470 let psess = ParseSess::with_dcx(dcx, sm);
471
472 let mut parser =
474 match new_parser_from_source_str(&psess, filename, wrapped_source, StripTokens::Nothing) {
475 Ok(p) => p,
476 Err(errs) => {
477 errs.into_iter().for_each(|err| err.cancel());
478 reset_error_count(&psess);
479 return Err(());
480 }
481 };
482
483 fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {
484 let extra_len = DOCTEST_CODE_WRAPPER.len();
485 let mut hi = span.hi().0 as usize - extra_len;
488 if hi > source.len() {
489 hi = source.len();
490 }
491 s.push_str(&source[*prev_span_hi..hi]);
492 *prev_span_hi = hi;
493 }
494
495 fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool {
496 let mut is_extern_crate = false;
497 if !info.has_global_allocator
498 && item.attrs.iter().any(|attr| attr.has_name(sym::global_allocator))
499 {
500 info.has_global_allocator = true;
501 }
502 match item.kind {
503 ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => {
504 if fn_item.ident.name == sym::main {
505 info.has_main_fn = true;
506 }
507 }
508 ast::ItemKind::ExternCrate(original, ident) => {
509 is_extern_crate = true;
510 if !info.already_has_extern_crate
511 && let Some(crate_name) = crate_name
512 {
513 info.already_has_extern_crate = match original {
514 Some(name) => name.as_str() == *crate_name,
515 None => ident.as_str() == *crate_name,
516 };
517 }
518 }
519 ast::ItemKind::MacroDef(..) => {
520 info.has_macro_def = true;
521 }
522 _ => {}
523 }
524 is_extern_crate
525 }
526
527 let mut prev_span_hi = 0;
528 let not_crate_attrs = &[sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect];
529 let parsed = parser.parse_item(rustc_parse::parser::ForceCollect::No);
530
531 let result = match parsed {
532 Ok(Some(ref item))
533 if let ast::ItemKind::Fn(ref fn_item) = item.kind
534 && let Some(ref body) = fn_item.body =>
535 {
536 for attr in &item.attrs {
537 if attr.style == AttrStyle::Outer || attr.has_any_name(not_crate_attrs) {
538 if attr.has_name(sym::allow)
542 && let Some(list) = attr.meta_item_list()
543 && list.iter().any(|sub_attr| sub_attr.has_name(sym::internal_features))
544 {
545 push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
546 } else {
547 push_to_s(
548 &mut info.maybe_crate_attrs,
549 source,
550 attr.span,
551 &mut prev_span_hi,
552 );
553 }
554 } else {
555 push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
556 }
557 }
558 let mut has_non_items = false;
559 for stmt in &body.stmts {
560 let mut is_extern_crate = false;
561 match stmt.kind {
562 StmtKind::Item(ref item) => {
563 is_extern_crate = check_item(item, &mut info, crate_name);
564 }
565 StmtKind::MacCall(ref mac_call) => {
568 if !info.has_main_fn {
569 let mut iter = mac_call.mac.args.tokens.iter();
574 while let Some(token) = iter.next() {
575 if let TokenTree::Token(token, _) = token
576 && let TokenKind::Ident(kw::Fn, _) = token.kind
577 && let Some(TokenTree::Token(ident, _)) = iter.peek()
578 && let TokenKind::Ident(sym::main, _) = ident.kind
579 && let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = {
580 iter.next();
581 iter.peek()
582 }
583 {
584 info.has_main_fn = true;
585 break;
586 }
587 }
588 }
589 }
590 StmtKind::Expr(ref expr) => {
591 if matches!(expr.kind, ast::ExprKind::Err(_)) {
592 reset_error_count(&psess);
593 return Err(());
594 }
595 has_non_items = true;
596 }
597 StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true,
598 }
599
600 let mut span = stmt.span;
603 if let Some(attr) =
604 stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer)
605 {
606 span = span.with_lo(attr.span.lo());
607 }
608 if info.everything_else.is_empty()
609 && (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty())
610 {
611 push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);
615 }
616 if !is_extern_crate {
617 push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);
618 } else {
619 push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
620 }
621 }
622 if has_non_items {
623 if info.has_main_fn
624 && let Some(dcx) = parent_dcx
625 && !span.is_dummy()
626 {
627 dcx.span_warn(
628 span,
629 "the `main` function of this doctest won't be run as it contains \
630 expressions at the top level, meaning that the whole doctest code will be \
631 wrapped in a function",
632 );
633 }
634 info.has_main_fn = false;
635 }
636 Ok(info)
637 }
638 Err(e) => {
639 e.cancel();
640 Err(())
641 }
642 _ => Err(()),
643 };
644
645 reset_error_count(&psess);
646 result
647}