1use std::fmt::{self, Write as _};
5use std::io;
6use std::sync::Arc;
7
8use rustc_ast as ast;
9use rustc_errors::emitter::stderr_destination;
10use rustc_errors::{ColorConfig, FatalError};
11use rustc_parse::new_parser_from_source_str;
12use rustc_parse::parser::attr::InnerAttrPolicy;
13use rustc_session::parse::ParseSess;
14use rustc_span::FileName;
15use rustc_span::edition::Edition;
16use rustc_span::source_map::SourceMap;
17use rustc_span::symbol::sym;
18use tracing::debug;
19
20use super::GlobalTestOptions;
21use crate::display::Joined as _;
22use crate::html::markdown::LangString;
23
24pub(crate) struct DocTestBuilder {
27 pub(crate) supports_color: bool,
28 pub(crate) already_has_extern_crate: bool,
29 pub(crate) has_main_fn: bool,
30 pub(crate) crate_attrs: String,
31 pub(crate) maybe_crate_attrs: String,
34 pub(crate) crates: String,
35 pub(crate) everything_else: String,
36 pub(crate) test_id: Option<String>,
37 pub(crate) failed_ast: bool,
38 pub(crate) can_be_merged: bool,
39}
40
41impl DocTestBuilder {
42 pub(crate) fn new(
43 source: &str,
44 crate_name: Option<&str>,
45 edition: Edition,
46 can_merge_doctests: bool,
47 test_id: Option<String>,
49 lang_str: Option<&LangString>,
50 ) -> Self {
51 let can_merge_doctests = can_merge_doctests
52 && lang_str.is_some_and(|lang_str| {
53 !lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
54 });
55
56 let Some(SourceInfo { crate_attrs, maybe_crate_attrs, crates, everything_else }) =
57 partition_source(source, edition)
58 else {
59 return Self::invalid(
60 String::new(),
61 String::new(),
62 String::new(),
63 source.to_string(),
64 test_id,
65 );
66 };
67
68 let Ok((
71 ParseSourceInfo {
72 has_main_fn,
73 found_extern_crate,
74 supports_color,
75 has_global_allocator,
76 has_macro_def,
77 ..
78 },
79 failed_ast,
80 )) = check_for_main_and_extern_crate(
81 crate_name,
82 source,
83 &everything_else,
84 &crates,
85 edition,
86 can_merge_doctests,
87 )
88 else {
89 return Self::invalid(crate_attrs, maybe_crate_attrs, crates, everything_else, test_id);
92 };
93 let can_be_merged = can_merge_doctests
96 && !failed_ast
97 && !has_global_allocator
98 && crate_attrs.is_empty()
99 && !(has_macro_def && everything_else.contains("$crate"));
102 Self {
103 supports_color,
104 has_main_fn,
105 crate_attrs,
106 maybe_crate_attrs,
107 crates,
108 everything_else,
109 already_has_extern_crate: found_extern_crate,
110 test_id,
111 failed_ast: false,
112 can_be_merged,
113 }
114 }
115
116 fn invalid(
117 crate_attrs: String,
118 maybe_crate_attrs: String,
119 crates: String,
120 everything_else: String,
121 test_id: Option<String>,
122 ) -> Self {
123 Self {
124 supports_color: false,
125 has_main_fn: false,
126 crate_attrs,
127 maybe_crate_attrs,
128 crates,
129 everything_else,
130 already_has_extern_crate: false,
131 test_id,
132 failed_ast: true,
133 can_be_merged: false,
134 }
135 }
136
137 pub(crate) fn generate_unique_doctest(
140 &self,
141 test_code: &str,
142 dont_insert_main: bool,
143 opts: &GlobalTestOptions,
144 crate_name: Option<&str>,
145 ) -> (String, usize) {
146 if self.failed_ast {
147 return (test_code.to_string(), 0);
150 }
151 let mut line_offset = 0;
152 let mut prog = String::new();
153 let everything_else = self.everything_else.trim();
154 if opts.attrs.is_empty() {
155 prog.push_str("#![allow(unused)]\n");
160 line_offset += 1;
161 }
162
163 for attr in &opts.attrs {
165 prog.push_str(&format!("#![{attr}]\n"));
166 line_offset += 1;
167 }
168
169 prog.push_str(&self.crate_attrs);
172 prog.push_str(&self.maybe_crate_attrs);
173 prog.push_str(&self.crates);
174
175 if !self.already_has_extern_crate &&
178 !opts.no_crate_inject &&
179 let Some(crate_name) = crate_name &&
180 crate_name != "std" &&
181 test_code.contains(crate_name)
186 {
187 prog.push_str("#[allow(unused_extern_crates)]\n");
190
191 prog.push_str(&format!("extern crate r#{crate_name};\n"));
192 line_offset += 1;
193 }
194
195 if dont_insert_main || self.has_main_fn || prog.contains("![no_std]") {
197 prog.push_str(everything_else);
198 } else {
199 let returns_result = everything_else.ends_with("(())");
200 let inner_fn_name = if let Some(ref test_id) = self.test_id {
203 format!("_doctest_main_{test_id}")
204 } else {
205 "_inner".into()
206 };
207 let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
208 let (main_pre, main_post) = if returns_result {
209 (
210 format!(
211 "fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n",
212 ),
213 format!("\n}} {inner_fn_name}().unwrap() }}"),
214 )
215 } else if self.test_id.is_some() {
216 (
217 format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
218 format!("\n}} {inner_fn_name}() }}"),
219 )
220 } else {
221 ("fn main() {\n".into(), "\n}".into())
222 };
223 line_offset += 1;
232
233 prog.push_str(&main_pre);
234
235 if opts.insert_indent_space {
237 write!(
238 prog,
239 "{}",
240 fmt::from_fn(|f| everything_else
241 .lines()
242 .map(|line| fmt::from_fn(move |f| write!(f, " {line}")))
243 .joined("\n", f))
244 )
245 .unwrap();
246 } else {
247 prog.push_str(everything_else);
248 };
249 prog.push_str(&main_post);
250 }
251
252 debug!("final doctest:\n{prog}");
253
254 (prog, line_offset)
255 }
256}
257
258#[derive(PartialEq, Eq, Debug)]
259enum ParsingResult {
260 Failed,
261 AstError,
262 Ok,
263}
264
265fn cancel_error_count(psess: &ParseSess) {
266 psess.dcx().reset_err_count();
271}
272
273fn parse_source(
274 source: String,
275 info: &mut ParseSourceInfo,
276 crate_name: &Option<&str>,
277) -> ParsingResult {
278 use rustc_errors::DiagCtxt;
279 use rustc_errors::emitter::{Emitter, HumanEmitter};
280 use rustc_parse::parser::ForceCollect;
281 use rustc_span::source_map::FilePathMapping;
282
283 let filename = FileName::anon_source_code(&source);
284
285 let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
288 let fallback_bundle = rustc_errors::fallback_fluent_bundle(
289 rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
290 false,
291 );
292 info.supports_color =
293 HumanEmitter::new(stderr_destination(ColorConfig::Auto), fallback_bundle.clone())
294 .supports_color();
295
296 let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle);
297
298 let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
300 let psess = ParseSess::with_dcx(dcx, sm);
301
302 let mut parser = match new_parser_from_source_str(&psess, filename, source) {
303 Ok(p) => p,
304 Err(errs) => {
305 errs.into_iter().for_each(|err| err.cancel());
306 cancel_error_count(&psess);
307 return ParsingResult::Failed;
308 }
309 };
310 let mut parsing_result = ParsingResult::Ok;
311
312 fn check_item(
316 item: &ast::Item,
317 info: &mut ParseSourceInfo,
318 crate_name: &Option<&str>,
319 is_top_level: bool,
320 ) {
321 if !info.has_global_allocator
322 && item.attrs.iter().any(|attr| attr.name_or_empty() == sym::global_allocator)
323 {
324 info.has_global_allocator = true;
325 }
326 match item.kind {
327 ast::ItemKind::Fn(ref fn_item) if !info.has_main_fn => {
328 if item.ident.name == sym::main && is_top_level {
329 info.has_main_fn = true;
330 }
331 if let Some(ref body) = fn_item.body {
332 for stmt in &body.stmts {
333 match stmt.kind {
334 ast::StmtKind::Item(ref item) => {
335 check_item(item, info, crate_name, false)
336 }
337 ast::StmtKind::MacCall(..) => info.found_macro = true,
338 _ => {}
339 }
340 }
341 }
342 }
343 ast::ItemKind::ExternCrate(original) => {
344 if !info.found_extern_crate
345 && let Some(crate_name) = crate_name
346 {
347 info.found_extern_crate = match original {
348 Some(name) => name.as_str() == *crate_name,
349 None => item.ident.as_str() == *crate_name,
350 };
351 }
352 }
353 ast::ItemKind::MacCall(..) => info.found_macro = true,
354 ast::ItemKind::MacroDef(..) => info.has_macro_def = true,
355 _ => {}
356 }
357 }
358
359 loop {
360 match parser.parse_item(ForceCollect::No) {
361 Ok(Some(item)) => {
362 check_item(&item, info, crate_name, true);
363
364 if info.has_main_fn && info.found_extern_crate {
365 break;
366 }
367 }
368 Ok(None) => break,
369 Err(e) => {
370 parsing_result = ParsingResult::AstError;
371 e.cancel();
372 break;
373 }
374 }
375
376 parser.maybe_consume_incorrect_semicolon(None);
379 }
380
381 cancel_error_count(&psess);
382 parsing_result
383}
384
385#[derive(Default)]
386struct ParseSourceInfo {
387 has_main_fn: bool,
388 found_extern_crate: bool,
389 found_macro: bool,
390 supports_color: bool,
391 has_global_allocator: bool,
392 has_macro_def: bool,
393}
394
395fn check_for_main_and_extern_crate(
396 crate_name: Option<&str>,
397 original_source_code: &str,
398 everything_else: &str,
399 crates: &str,
400 edition: Edition,
401 can_merge_doctests: bool,
402) -> Result<(ParseSourceInfo, bool), FatalError> {
403 let result = rustc_driver::catch_fatal_errors(|| {
404 rustc_span::create_session_if_not_set_then(edition, |_| {
405 let mut info =
406 ParseSourceInfo { found_extern_crate: crate_name.is_none(), ..Default::default() };
407
408 let mut parsing_result =
409 parse_source(format!("{crates}{everything_else}"), &mut info, &crate_name);
410 if can_merge_doctests && parsing_result != ParsingResult::Ok {
413 parsing_result = parse_source(
424 format!("{crates}\nfn __doctest_wrap(){{{everything_else}\n}}"),
425 &mut info,
426 &crate_name,
427 );
428 }
429
430 (info, parsing_result)
431 })
432 });
433 let (mut info, parsing_result) = match result {
434 Err(..) | Ok((_, ParsingResult::Failed)) => return Err(FatalError),
435 Ok((info, parsing_result)) => (info, parsing_result),
436 };
437
438 if info.found_macro
443 && !info.has_main_fn
444 && original_source_code
445 .lines()
446 .map(|line| {
447 let comment = line.find("//");
448 if let Some(comment_begins) = comment { &line[0..comment_begins] } else { line }
449 })
450 .any(|code| code.contains("fn main"))
451 {
452 info.has_main_fn = true;
453 }
454
455 Ok((info, parsing_result != ParsingResult::Ok))
456}
457
458enum AttrKind {
459 CrateAttr,
460 Attr,
461}
462
463fn check_if_attr_is_complete(source: &str, edition: Edition) -> Option<AttrKind> {
466 if source.is_empty() {
467 return None;
469 }
470 let not_crate_attrs = [sym::forbid, sym::allow, sym::warn, sym::deny];
471
472 rustc_driver::catch_fatal_errors(|| {
473 rustc_span::create_session_if_not_set_then(edition, |_| {
474 use rustc_errors::DiagCtxt;
475 use rustc_errors::emitter::HumanEmitter;
476 use rustc_span::source_map::FilePathMapping;
477
478 let filename = FileName::anon_source_code(source);
479 let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
482 let fallback_bundle = rustc_errors::fallback_fluent_bundle(
483 rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
484 false,
485 );
486
487 let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle);
488
489 let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
490 let psess = ParseSess::with_dcx(dcx, sm);
491 let mut parser = match new_parser_from_source_str(&psess, filename, source.to_owned()) {
492 Ok(p) => p,
493 Err(errs) => {
494 errs.into_iter().for_each(|err| err.cancel());
495 return None;
498 }
499 };
500 let ret = match parser.parse_attribute(InnerAttrPolicy::Permitted) {
502 Ok(attr) => {
503 let attr_name = attr.name_or_empty();
504
505 if not_crate_attrs.contains(&attr_name) {
506 if attr_name == sym::allow
510 && let Some(list) = attr.meta_item_list()
511 && list.iter().any(|sub_attr| {
512 sub_attr.name_or_empty().as_str() == "internal_features"
513 })
514 {
515 Some(AttrKind::CrateAttr)
516 } else {
517 Some(AttrKind::Attr)
518 }
519 } else {
520 Some(AttrKind::CrateAttr)
521 }
522 }
523 Err(e) => {
524 e.cancel();
525 None
526 }
527 };
528 ret
529 })
530 })
531 .unwrap_or(None)
532}
533
534fn handle_attr(mod_attr_pending: &mut String, source_info: &mut SourceInfo, edition: Edition) {
535 if let Some(attr_kind) = check_if_attr_is_complete(mod_attr_pending, edition) {
536 let push_to = match attr_kind {
537 AttrKind::CrateAttr => &mut source_info.crate_attrs,
538 AttrKind::Attr => &mut source_info.maybe_crate_attrs,
539 };
540 push_to.push_str(mod_attr_pending);
541 push_to.push('\n');
542 mod_attr_pending.clear();
544 } else {
545 mod_attr_pending.push('\n');
546 }
547}
548
549#[derive(Default)]
550struct SourceInfo {
551 crate_attrs: String,
552 maybe_crate_attrs: String,
553 crates: String,
554 everything_else: String,
555}
556
557fn partition_source(s: &str, edition: Edition) -> Option<SourceInfo> {
558 #[derive(Copy, Clone, PartialEq)]
559 enum PartitionState {
560 Attrs,
561 Crates,
562 Other,
563 }
564 let mut source_info = SourceInfo::default();
565 let mut state = PartitionState::Attrs;
566 let mut mod_attr_pending = String::new();
567
568 for line in s.lines() {
569 let trimline = line.trim();
570
571 match state {
574 PartitionState::Attrs => {
575 state = if trimline.starts_with("#![") {
576 mod_attr_pending = line.to_owned();
577 handle_attr(&mut mod_attr_pending, &mut source_info, edition);
578 continue;
579 } else if trimline.chars().all(|c| c.is_whitespace())
580 || (trimline.starts_with("//") && !trimline.starts_with("///"))
581 {
582 PartitionState::Attrs
583 } else if trimline.starts_with("extern crate")
584 || trimline.starts_with("#[macro_use] extern crate")
585 {
586 PartitionState::Crates
587 } else {
588 if !mod_attr_pending.is_empty() {
590 mod_attr_pending.push_str(line);
593 if !trimline.is_empty() {
594 handle_attr(&mut mod_attr_pending, &mut source_info, edition);
595 }
596 continue;
597 } else {
598 PartitionState::Other
599 }
600 };
601 }
602 PartitionState::Crates => {
603 state = if trimline.starts_with("extern crate")
604 || trimline.starts_with("#[macro_use] extern crate")
605 || trimline.chars().all(|c| c.is_whitespace())
606 || (trimline.starts_with("//") && !trimline.starts_with("///"))
607 {
608 PartitionState::Crates
609 } else {
610 PartitionState::Other
611 };
612 }
613 PartitionState::Other => {}
614 }
615
616 match state {
617 PartitionState::Attrs => {
618 source_info.crate_attrs.push_str(line);
619 source_info.crate_attrs.push('\n');
620 }
621 PartitionState::Crates => {
622 source_info.crates.push_str(line);
623 source_info.crates.push('\n');
624 }
625 PartitionState::Other => {
626 source_info.everything_else.push_str(line);
627 source_info.everything_else.push('\n');
628 }
629 }
630 }
631
632 if !mod_attr_pending.is_empty() {
633 debug!("invalid doctest code: {s:?}");
634 return None;
635 }
636
637 source_info.everything_else = source_info.everything_else.trim().to_string();
638
639 debug!("crate_attrs:\n{}{}", source_info.crate_attrs, source_info.maybe_crate_attrs);
640 debug!("crates:\n{}", source_info.crates);
641 debug!("after:\n{}", source_info.everything_else);
642
643 Some(source_info)
644}