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::ColorConfig;
12use rustc_errors::emitter::stderr_destination;
13use rustc_parse::new_parser_from_source_str;
14use rustc_session::parse::ParseSess;
15use rustc_span::edition::Edition;
16use rustc_span::source_map::SourceMap;
17use rustc_span::symbol::sym;
18use rustc_span::{FileName, 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 DocTestBuilder {
41 pub(crate) supports_color: bool,
42 pub(crate) already_has_extern_crate: bool,
43 pub(crate) has_main_fn: bool,
44 pub(crate) crate_attrs: String,
45 pub(crate) maybe_crate_attrs: String,
48 pub(crate) crates: String,
49 pub(crate) everything_else: String,
50 pub(crate) test_id: Option<String>,
51 pub(crate) invalid_ast: bool,
52 pub(crate) can_be_merged: bool,
53}
54
55impl DocTestBuilder {
56 pub(crate) fn new(
57 source: &str,
58 crate_name: Option<&str>,
59 edition: Edition,
60 can_merge_doctests: bool,
61 test_id: Option<String>,
63 lang_str: Option<&LangString>,
64 ) -> Self {
65 let can_merge_doctests = can_merge_doctests
66 && lang_str.is_some_and(|lang_str| {
67 !lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
68 });
69
70 let result = rustc_driver::catch_fatal_errors(|| {
71 rustc_span::create_session_if_not_set_then(edition, |_| {
72 parse_source(source, &crate_name)
73 })
74 });
75
76 let Ok(Ok(ParseSourceInfo {
77 has_main_fn,
78 already_has_extern_crate,
79 supports_color,
80 has_global_allocator,
81 has_macro_def,
82 everything_else,
83 crates,
84 crate_attrs,
85 maybe_crate_attrs,
86 })) = result
87 else {
88 return Self::invalid(
91 String::new(),
92 String::new(),
93 String::new(),
94 source.to_string(),
95 test_id,
96 );
97 };
98
99 debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}");
100 debug!("crates:\n{crates}");
101 debug!("after:\n{everything_else}");
102
103 let can_be_merged = can_merge_doctests
105 && !has_global_allocator
106 && crate_attrs.is_empty()
107 && !(has_macro_def && everything_else.contains("$crate"));
110 Self {
111 supports_color,
112 has_main_fn,
113 crate_attrs,
114 maybe_crate_attrs,
115 crates,
116 everything_else,
117 already_has_extern_crate,
118 test_id,
119 invalid_ast: false,
120 can_be_merged,
121 }
122 }
123
124 fn invalid(
125 crate_attrs: String,
126 maybe_crate_attrs: String,
127 crates: String,
128 everything_else: String,
129 test_id: Option<String>,
130 ) -> Self {
131 Self {
132 supports_color: false,
133 has_main_fn: false,
134 crate_attrs,
135 maybe_crate_attrs,
136 crates,
137 everything_else,
138 already_has_extern_crate: false,
139 test_id,
140 invalid_ast: true,
141 can_be_merged: false,
142 }
143 }
144
145 pub(crate) fn generate_unique_doctest(
148 &self,
149 test_code: &str,
150 dont_insert_main: bool,
151 opts: &GlobalTestOptions,
152 crate_name: Option<&str>,
153 ) -> (String, usize) {
154 if self.invalid_ast {
155 debug!("invalid AST:\n{test_code}");
158 return (test_code.to_string(), 0);
159 }
160 let mut line_offset = 0;
161 let mut prog = String::new();
162 let everything_else = self.everything_else.trim();
163 if opts.attrs.is_empty() {
164 prog.push_str("#![allow(unused)]\n");
169 line_offset += 1;
170 }
171
172 for attr in &opts.attrs {
174 prog.push_str(&format!("#![{attr}]\n"));
175 line_offset += 1;
176 }
177
178 if !self.crate_attrs.is_empty() {
181 prog.push_str(&self.crate_attrs);
182 if !self.crate_attrs.ends_with('\n') {
183 prog.push('\n');
184 }
185 }
186 if !self.maybe_crate_attrs.is_empty() {
187 prog.push_str(&self.maybe_crate_attrs);
188 if !self.maybe_crate_attrs.ends_with('\n') {
189 prog.push('\n');
190 }
191 }
192 if !self.crates.is_empty() {
193 prog.push_str(&self.crates);
194 if !self.crates.ends_with('\n') {
195 prog.push('\n');
196 }
197 }
198
199 if !self.already_has_extern_crate &&
202 !opts.no_crate_inject &&
203 let Some(crate_name) = crate_name &&
204 crate_name != "std" &&
205 test_code.contains(crate_name)
210 {
211 prog.push_str("#[allow(unused_extern_crates)]\n");
214
215 prog.push_str(&format!("extern crate r#{crate_name};\n"));
216 line_offset += 1;
217 }
218
219 if dont_insert_main || self.has_main_fn || prog.contains("![no_std]") {
221 prog.push_str(everything_else);
222 } else {
223 let returns_result = everything_else.ends_with("(())");
224 let inner_fn_name = if let Some(ref test_id) = self.test_id {
227 format!("_doctest_main_{test_id}")
228 } else {
229 "_inner".into()
230 };
231 let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
232 let (main_pre, main_post) = if returns_result {
233 (
234 format!(
235 "fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n",
236 ),
237 format!("\n}} {inner_fn_name}().unwrap() }}"),
238 )
239 } else if self.test_id.is_some() {
240 (
241 format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
242 format!("\n}} {inner_fn_name}() }}"),
243 )
244 } else {
245 ("fn main() {\n".into(), "\n}".into())
246 };
247 line_offset += 1;
256
257 prog.push_str(&main_pre);
258
259 if opts.insert_indent_space {
261 write!(
262 prog,
263 "{}",
264 fmt::from_fn(|f| everything_else
265 .lines()
266 .map(|line| fmt::from_fn(move |f| write!(f, " {line}")))
267 .joined("\n", f))
268 )
269 .unwrap();
270 } else {
271 prog.push_str(everything_else);
272 };
273 prog.push_str(&main_post);
274 }
275
276 debug!("final doctest:\n{prog}");
277
278 (prog, line_offset)
279 }
280}
281
282fn reset_error_count(psess: &ParseSess) {
283 psess.dcx().reset_err_count();
288}
289
290const DOCTEST_CODE_WRAPPER: &str = "fn f(){";
291
292fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceInfo, ()> {
293 use rustc_errors::DiagCtxt;
294 use rustc_errors::emitter::{Emitter, HumanEmitter};
295 use rustc_span::source_map::FilePathMapping;
296
297 let mut info =
298 ParseSourceInfo { already_has_extern_crate: crate_name.is_none(), ..Default::default() };
299
300 let wrapped_source = format!("{DOCTEST_CODE_WRAPPER}{source}\n}}");
301
302 let filename = FileName::anon_source_code(&wrapped_source);
303
304 let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
307 let fallback_bundle = rustc_errors::fallback_fluent_bundle(
308 rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
309 false,
310 );
311 info.supports_color =
312 HumanEmitter::new(stderr_destination(ColorConfig::Auto), fallback_bundle.clone())
313 .supports_color();
314
315 let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle);
316
317 let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings();
319 let psess = ParseSess::with_dcx(dcx, sm);
320
321 let mut parser = match new_parser_from_source_str(&psess, filename, wrapped_source) {
322 Ok(p) => p,
323 Err(errs) => {
324 errs.into_iter().for_each(|err| err.cancel());
325 reset_error_count(&psess);
326 return Err(());
327 }
328 };
329
330 fn push_to_s(s: &mut String, source: &str, span: rustc_span::Span, prev_span_hi: &mut usize) {
331 let extra_len = DOCTEST_CODE_WRAPPER.len();
332 let mut hi = span.hi().0 as usize - extra_len;
335 if hi > source.len() {
336 hi = source.len();
337 }
338 s.push_str(&source[*prev_span_hi..hi]);
339 *prev_span_hi = hi;
340 }
341
342 fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool {
346 let mut is_extern_crate = false;
347 if !info.has_global_allocator
348 && item.attrs.iter().any(|attr| attr.name_or_empty() == sym::global_allocator)
349 {
350 info.has_global_allocator = true;
351 }
352 match item.kind {
353 ast::ItemKind::Fn(_) if !info.has_main_fn => {
354 if item.ident.name == sym::main {
357 info.has_main_fn = true;
358 }
359 }
360 ast::ItemKind::ExternCrate(original) => {
361 is_extern_crate = true;
362 if !info.already_has_extern_crate
363 && let Some(crate_name) = crate_name
364 {
365 info.already_has_extern_crate = match original {
366 Some(name) => name.as_str() == *crate_name,
367 None => item.ident.as_str() == *crate_name,
368 };
369 }
370 }
371 ast::ItemKind::MacroDef(..) => {
372 info.has_macro_def = true;
373 }
374 _ => {}
375 }
376 is_extern_crate
377 }
378
379 let mut prev_span_hi = 0;
380 let not_crate_attrs = [sym::forbid, sym::allow, sym::warn, sym::deny, sym::expect];
381 let parsed = parser.parse_item(rustc_parse::parser::ForceCollect::No);
382
383 let result = match parsed {
384 Ok(Some(ref item))
385 if let ast::ItemKind::Fn(ref fn_item) = item.kind
386 && let Some(ref body) = fn_item.body =>
387 {
388 for attr in &item.attrs {
389 let attr_name = attr.name_or_empty();
390
391 if attr.style == AttrStyle::Outer || not_crate_attrs.contains(&attr_name) {
392 if attr_name == sym::allow
396 && let Some(list) = attr.meta_item_list()
397 && list.iter().any(|sub_attr| {
398 sub_attr.name_or_empty().as_str() == "internal_features"
399 })
400 {
401 push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
402 } else {
403 push_to_s(
404 &mut info.maybe_crate_attrs,
405 source,
406 attr.span,
407 &mut prev_span_hi,
408 );
409 }
410 } else {
411 push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
412 }
413 }
414 for stmt in &body.stmts {
415 let mut is_extern_crate = false;
416 match stmt.kind {
417 StmtKind::Item(ref item) => {
418 is_extern_crate = check_item(&item, &mut info, crate_name);
419 }
420 StmtKind::Expr(ref expr) if matches!(expr.kind, ast::ExprKind::Err(_)) => {
421 reset_error_count(&psess);
422 return Err(());
423 }
424 StmtKind::MacCall(ref mac_call) if !info.has_main_fn => {
425 let mut iter = mac_call.mac.args.tokens.iter();
426
427 while let Some(token) = iter.next() {
428 if let TokenTree::Token(token, _) = token
429 && let TokenKind::Ident(name, _) = token.kind
430 && name == kw::Fn
431 && let Some(TokenTree::Token(fn_token, _)) = iter.peek()
432 && let TokenKind::Ident(fn_name, _) = fn_token.kind
433 && fn_name == sym::main
434 && let Some(TokenTree::Delimited(_, _, Delimiter::Parenthesis, _)) = {
435 iter.next();
436 iter.peek()
437 }
438 {
439 info.has_main_fn = true;
440 break;
441 }
442 }
443 }
444 _ => {}
445 }
446
447 let mut span = stmt.span;
450 if let Some(attr) =
451 stmt.kind.attrs().iter().find(|attr| attr.style == AttrStyle::Outer)
452 {
453 span = span.with_lo(attr.span.lo());
454 }
455 if info.everything_else.is_empty()
456 && (!info.maybe_crate_attrs.is_empty() || !info.crate_attrs.is_empty())
457 {
458 push_to_s(&mut info.crates, source, span.shrink_to_lo(), &mut prev_span_hi);
462 }
463 if !is_extern_crate {
464 push_to_s(&mut info.everything_else, source, span, &mut prev_span_hi);
465 } else {
466 push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
467 }
468 }
469 Ok(info)
470 }
471 Err(e) => {
472 e.cancel();
473 Err(())
474 }
475 _ => Err(()),
476 };
477
478 reset_error_count(&psess);
479 result
480}