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