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