]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/doctest.rs
30bc2f90d2c5267781e893062322578761c9ebc7
[rust.git] / src / librustdoc / doctest.rs
1 use rustc_ast as ast;
2 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
3 use rustc_data_structures::sync::Lrc;
4 use rustc_errors::{ColorConfig, ErrorGuaranteed, FatalError};
5 use rustc_hir as hir;
6 use rustc_hir::def_id::LOCAL_CRATE;
7 use rustc_hir::intravisit;
8 use rustc_hir::{HirId, CRATE_HIR_ID};
9 use rustc_interface::interface;
10 use rustc_middle::hir::map::Map;
11 use rustc_middle::hir::nested_filter;
12 use rustc_middle::ty::TyCtxt;
13 use rustc_parse::maybe_new_parser_from_source_str;
14 use rustc_parse::parser::attr::InnerAttrPolicy;
15 use rustc_session::config::{self, CrateType, ErrorOutputType};
16 use rustc_session::parse::ParseSess;
17 use rustc_session::{lint, Session};
18 use rustc_span::edition::Edition;
19 use rustc_span::source_map::SourceMap;
20 use rustc_span::symbol::sym;
21 use rustc_span::{BytePos, FileName, Pos, Span, DUMMY_SP};
22 use rustc_target::spec::{Target, TargetTriple};
23 use tempfile::Builder as TempFileBuilder;
24
25 use std::env;
26 use std::io::{self, Write};
27 use std::panic;
28 use std::path::PathBuf;
29 use std::process::{self, Command, Stdio};
30 use std::str;
31 use std::sync::atomic::{AtomicUsize, Ordering};
32 use std::sync::{Arc, Mutex};
33
34 use crate::clean::{types::AttributesExt, Attributes};
35 use crate::config::Options as RustdocOptions;
36 use crate::html::markdown::{self, ErrorCodes, Ignore, LangString};
37 use crate::lint::init_lints;
38 use crate::passes::span_of_attrs;
39
40 /// Options that apply to all doctests in a crate or Markdown file (for `rustdoc foo.md`).
41 #[derive(Clone, Default)]
42 pub(crate) struct GlobalTestOptions {
43     /// Whether to disable the default `extern crate my_crate;` when creating doctests.
44     pub(crate) no_crate_inject: bool,
45     /// Additional crate-level attributes to add to doctests.
46     pub(crate) attrs: Vec<String>,
47 }
48
49 pub(crate) fn run(options: RustdocOptions) -> Result<(), ErrorGuaranteed> {
50     let input = config::Input::File(options.input.clone());
51
52     let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name;
53
54     // See core::create_config for what's going on here.
55     let allowed_lints = vec![
56         invalid_codeblock_attributes_name.to_owned(),
57         lint::builtin::UNKNOWN_LINTS.name.to_owned(),
58         lint::builtin::RENAMED_AND_REMOVED_LINTS.name.to_owned(),
59     ];
60
61     let (lint_opts, lint_caps) = init_lints(allowed_lints, options.lint_opts.clone(), |lint| {
62         if lint.name == invalid_codeblock_attributes_name {
63             None
64         } else {
65             Some((lint.name_lower(), lint::Allow))
66         }
67     });
68
69     debug!(?lint_opts);
70
71     let crate_types = if options.crate_types.is_empty() {
72         vec![CrateType::Rlib]
73     } else {
74         options.crate_types.clone()
75     };
76
77     let sessopts = config::Options {
78         maybe_sysroot: options.maybe_sysroot.clone(),
79         search_paths: options.libs.clone(),
80         crate_types,
81         lint_opts,
82         lint_cap: Some(options.lint_cap.unwrap_or(lint::Forbid)),
83         cg: options.codegen_options.clone(),
84         externs: options.externs.clone(),
85         unstable_features: options.unstable_features,
86         actually_rustdoc: true,
87         edition: options.edition,
88         target_triple: options.target.clone(),
89         crate_name: options.crate_name.clone(),
90         ..config::Options::default()
91     };
92
93     let mut cfgs = options.cfgs.clone();
94     cfgs.push("doc".to_owned());
95     cfgs.push("doctest".to_owned());
96     let config = interface::Config {
97         opts: sessopts,
98         crate_cfg: interface::parse_cfgspecs(cfgs),
99         crate_check_cfg: interface::parse_check_cfg(options.check_cfgs.clone()),
100         input,
101         input_path: None,
102         output_file: None,
103         output_dir: None,
104         file_loader: None,
105         lint_caps,
106         parse_sess_created: None,
107         register_lints: Some(Box::new(crate::lint::register_lints)),
108         override_queries: None,
109         make_codegen_backend: None,
110         registry: rustc_driver::diagnostics_registry(),
111     };
112
113     let test_args = options.test_args.clone();
114     let nocapture = options.nocapture;
115     let externs = options.externs.clone();
116     let json_unused_externs = options.json_unused_externs;
117
118     let (tests, unused_extern_reports, compiling_test_count) =
119         interface::run_compiler(config, |compiler| {
120             compiler.enter(|queries| {
121                 let mut global_ctxt = queries.global_ctxt()?.take();
122
123                 let collector = global_ctxt.enter(|tcx| {
124                     let crate_attrs = tcx.hir().attrs(CRATE_HIR_ID);
125
126                     let opts = scrape_test_config(crate_attrs);
127                     let enable_per_target_ignores = options.enable_per_target_ignores;
128                     let mut collector = Collector::new(
129                         tcx.crate_name(LOCAL_CRATE).to_string(),
130                         options,
131                         false,
132                         opts,
133                         Some(compiler.session().parse_sess.clone_source_map()),
134                         None,
135                         enable_per_target_ignores,
136                     );
137
138                     let mut hir_collector = HirCollector {
139                         sess: compiler.session(),
140                         collector: &mut collector,
141                         map: tcx.hir(),
142                         codes: ErrorCodes::from(
143                             compiler.session().opts.unstable_features.is_nightly_build(),
144                         ),
145                         tcx,
146                     };
147                     hir_collector.visit_testable(
148                         "".to_string(),
149                         CRATE_HIR_ID,
150                         tcx.hir().span(CRATE_HIR_ID),
151                         |this| tcx.hir().walk_toplevel_module(this),
152                     );
153
154                     collector
155                 });
156                 if compiler.session().diagnostic().has_errors_or_lint_errors().is_some() {
157                     FatalError.raise();
158                 }
159
160                 let unused_extern_reports = collector.unused_extern_reports.clone();
161                 let compiling_test_count = collector.compiling_test_count.load(Ordering::SeqCst);
162                 let ret: Result<_, ErrorGuaranteed> =
163                     Ok((collector.tests, unused_extern_reports, compiling_test_count));
164                 ret
165             })
166         })?;
167
168     run_tests(test_args, nocapture, tests);
169
170     // Collect and warn about unused externs, but only if we've gotten
171     // reports for each doctest
172     if json_unused_externs.is_enabled() {
173         let unused_extern_reports: Vec<_> =
174             std::mem::take(&mut unused_extern_reports.lock().unwrap());
175         if unused_extern_reports.len() == compiling_test_count {
176             let extern_names = externs.iter().map(|(name, _)| name).collect::<FxHashSet<&String>>();
177             let mut unused_extern_names = unused_extern_reports
178                 .iter()
179                 .map(|uexts| uexts.unused_extern_names.iter().collect::<FxHashSet<&String>>())
180                 .fold(extern_names, |uextsa, uextsb| {
181                     uextsa.intersection(&uextsb).copied().collect::<FxHashSet<&String>>()
182                 })
183                 .iter()
184                 .map(|v| (*v).clone())
185                 .collect::<Vec<String>>();
186             unused_extern_names.sort();
187             // Take the most severe lint level
188             let lint_level = unused_extern_reports
189                 .iter()
190                 .map(|uexts| uexts.lint_level.as_str())
191                 .max_by_key(|v| match *v {
192                     "warn" => 1,
193                     "deny" => 2,
194                     "forbid" => 3,
195                     // The allow lint level is not expected,
196                     // as if allow is specified, no message
197                     // is to be emitted.
198                     v => unreachable!("Invalid lint level '{}'", v),
199                 })
200                 .unwrap_or("warn")
201                 .to_string();
202             let uext = UnusedExterns { lint_level, unused_extern_names };
203             let unused_extern_json = serde_json::to_string(&uext).unwrap();
204             eprintln!("{unused_extern_json}");
205         }
206     }
207
208     Ok(())
209 }
210
211 pub(crate) fn run_tests(
212     mut test_args: Vec<String>,
213     nocapture: bool,
214     mut tests: Vec<test::TestDescAndFn>,
215 ) {
216     test_args.insert(0, "rustdoctest".to_string());
217     if nocapture {
218         test_args.push("--nocapture".to_string());
219     }
220     tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice()));
221     test::test_main(&test_args, tests, None);
222 }
223
224 // Look for `#![doc(test(no_crate_inject))]`, used by crates in the std facade.
225 fn scrape_test_config(attrs: &[ast::Attribute]) -> GlobalTestOptions {
226     use rustc_ast_pretty::pprust;
227
228     let mut opts = GlobalTestOptions { no_crate_inject: false, attrs: Vec::new() };
229
230     let test_attrs: Vec<_> = attrs
231         .iter()
232         .filter(|a| a.has_name(sym::doc))
233         .flat_map(|a| a.meta_item_list().unwrap_or_default())
234         .filter(|a| a.has_name(sym::test))
235         .collect();
236     let attrs = test_attrs.iter().flat_map(|a| a.meta_item_list().unwrap_or(&[]));
237
238     for attr in attrs {
239         if attr.has_name(sym::no_crate_inject) {
240             opts.no_crate_inject = true;
241         }
242         if attr.has_name(sym::attr) {
243             if let Some(l) = attr.meta_item_list() {
244                 for item in l {
245                     opts.attrs.push(pprust::meta_list_item_to_string(item));
246                 }
247             }
248         }
249     }
250
251     opts
252 }
253
254 /// Documentation test failure modes.
255 enum TestFailure {
256     /// The test failed to compile.
257     CompileError,
258     /// The test is marked `compile_fail` but compiled successfully.
259     UnexpectedCompilePass,
260     /// The test failed to compile (as expected) but the compiler output did not contain all
261     /// expected error codes.
262     MissingErrorCodes(Vec<String>),
263     /// The test binary was unable to be executed.
264     ExecutionError(io::Error),
265     /// The test binary exited with a non-zero exit code.
266     ///
267     /// This typically means an assertion in the test failed or another form of panic occurred.
268     ExecutionFailure(process::Output),
269     /// The test is marked `should_panic` but the test binary executed successfully.
270     UnexpectedRunPass,
271 }
272
273 enum DirState {
274     Temp(tempfile::TempDir),
275     Perm(PathBuf),
276 }
277
278 impl DirState {
279     fn path(&self) -> &std::path::Path {
280         match self {
281             DirState::Temp(t) => t.path(),
282             DirState::Perm(p) => p.as_path(),
283         }
284     }
285 }
286
287 // NOTE: Keep this in sync with the equivalent structs in rustc
288 // and cargo.
289 // We could unify this struct the one in rustc but they have different
290 // ownership semantics, so doing so would create wasteful allocations.
291 #[derive(serde::Serialize, serde::Deserialize)]
292 struct UnusedExterns {
293     /// Lint level of the unused_crate_dependencies lint
294     lint_level: String,
295     /// List of unused externs by their names.
296     unused_extern_names: Vec<String>,
297 }
298
299 fn add_exe_suffix(input: String, target: &TargetTriple) -> String {
300     let exe_suffix = match target {
301         TargetTriple::TargetTriple(_) => Target::expect_builtin(target).options.exe_suffix,
302         TargetTriple::TargetJson { contents, .. } => {
303             Target::from_json(contents.parse().unwrap()).unwrap().0.options.exe_suffix
304         }
305     };
306     input + &exe_suffix
307 }
308
309 fn run_test(
310     test: &str,
311     crate_name: &str,
312     line: usize,
313     rustdoc_options: RustdocOptions,
314     mut lang_string: LangString,
315     no_run: bool,
316     runtool: Option<String>,
317     runtool_args: Vec<String>,
318     target: TargetTriple,
319     opts: &GlobalTestOptions,
320     edition: Edition,
321     outdir: DirState,
322     path: PathBuf,
323     test_id: &str,
324     report_unused_externs: impl Fn(UnusedExterns),
325 ) -> Result<(), TestFailure> {
326     let (test, line_offset, supports_color) =
327         make_test(test, Some(crate_name), lang_string.test_harness, opts, edition, Some(test_id));
328
329     // Make sure we emit well-formed executable names for our target.
330     let rust_out = add_exe_suffix("rust_out".to_owned(), &target);
331     let output_file = outdir.path().join(rust_out);
332
333     let rustc_binary = rustdoc_options
334         .test_builder
335         .as_deref()
336         .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
337     let mut compiler = Command::new(&rustc_binary);
338     compiler.arg("--crate-type").arg("bin");
339     for cfg in &rustdoc_options.cfgs {
340         compiler.arg("--cfg").arg(&cfg);
341     }
342     if !rustdoc_options.check_cfgs.is_empty() {
343         compiler.arg("-Z").arg("unstable-options");
344         for check_cfg in &rustdoc_options.check_cfgs {
345             compiler.arg("--check-cfg").arg(&check_cfg);
346         }
347     }
348     if let Some(sysroot) = rustdoc_options.maybe_sysroot {
349         compiler.arg("--sysroot").arg(sysroot);
350     }
351     compiler.arg("--edition").arg(&edition.to_string());
352     compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", path);
353     compiler.env("UNSTABLE_RUSTDOC_TEST_LINE", format!("{}", line as isize - line_offset as isize));
354     compiler.arg("-o").arg(&output_file);
355     if lang_string.test_harness {
356         compiler.arg("--test");
357     }
358     if rustdoc_options.json_unused_externs.is_enabled() && !lang_string.compile_fail {
359         compiler.arg("--error-format=json");
360         compiler.arg("--json").arg("unused-externs");
361         compiler.arg("-Z").arg("unstable-options");
362         compiler.arg("-W").arg("unused_crate_dependencies");
363     }
364     for lib_str in &rustdoc_options.lib_strs {
365         compiler.arg("-L").arg(&lib_str);
366     }
367     for extern_str in &rustdoc_options.extern_strs {
368         compiler.arg("--extern").arg(&extern_str);
369     }
370     compiler.arg("-Ccodegen-units=1");
371     for codegen_options_str in &rustdoc_options.codegen_options_strs {
372         compiler.arg("-C").arg(&codegen_options_str);
373     }
374     for unstable_option_str in &rustdoc_options.unstable_opts_strs {
375         compiler.arg("-Z").arg(&unstable_option_str);
376     }
377     if no_run && !lang_string.compile_fail && rustdoc_options.persist_doctests.is_none() {
378         compiler.arg("--emit=metadata");
379     }
380     compiler.arg("--target").arg(match target {
381         TargetTriple::TargetTriple(s) => s,
382         TargetTriple::TargetJson { path_for_rustdoc, .. } => {
383             path_for_rustdoc.to_str().expect("target path must be valid unicode").to_string()
384         }
385     });
386     if let ErrorOutputType::HumanReadable(kind) = rustdoc_options.error_format {
387         let (short, color_config) = kind.unzip();
388
389         if short {
390             compiler.arg("--error-format").arg("short");
391         }
392
393         match color_config {
394             ColorConfig::Never => {
395                 compiler.arg("--color").arg("never");
396             }
397             ColorConfig::Always => {
398                 compiler.arg("--color").arg("always");
399             }
400             ColorConfig::Auto => {
401                 compiler.arg("--color").arg(if supports_color { "always" } else { "never" });
402             }
403         }
404     }
405
406     compiler.arg("-");
407     compiler.stdin(Stdio::piped());
408     compiler.stderr(Stdio::piped());
409
410     let mut child = compiler.spawn().expect("Failed to spawn rustc process");
411     {
412         let stdin = child.stdin.as_mut().expect("Failed to open stdin");
413         stdin.write_all(test.as_bytes()).expect("could write out test sources");
414     }
415     let output = child.wait_with_output().expect("Failed to read stdout");
416
417     struct Bomb<'a>(&'a str);
418     impl Drop for Bomb<'_> {
419         fn drop(&mut self) {
420             eprint!("{}", self.0);
421         }
422     }
423     let mut out = str::from_utf8(&output.stderr)
424         .unwrap()
425         .lines()
426         .filter(|l| {
427             if let Ok(uext) = serde_json::from_str::<UnusedExterns>(l) {
428                 report_unused_externs(uext);
429                 false
430             } else {
431                 true
432             }
433         })
434         .intersperse_with(|| "\n")
435         .collect::<String>();
436
437     // Add a \n to the end to properly terminate the last line,
438     // but only if there was output to be printed
439     if !out.is_empty() {
440         out.push('\n');
441     }
442
443     let _bomb = Bomb(&out);
444     match (output.status.success(), lang_string.compile_fail) {
445         (true, true) => {
446             return Err(TestFailure::UnexpectedCompilePass);
447         }
448         (true, false) => {}
449         (false, true) => {
450             if !lang_string.error_codes.is_empty() {
451                 // We used to check if the output contained "error[{}]: " but since we added the
452                 // colored output, we can't anymore because of the color escape characters before
453                 // the ":".
454                 lang_string.error_codes.retain(|err| !out.contains(&format!("error[{err}]")));
455
456                 if !lang_string.error_codes.is_empty() {
457                     return Err(TestFailure::MissingErrorCodes(lang_string.error_codes));
458                 }
459             }
460         }
461         (false, false) => {
462             return Err(TestFailure::CompileError);
463         }
464     }
465
466     if no_run {
467         return Ok(());
468     }
469
470     // Run the code!
471     let mut cmd;
472
473     if let Some(tool) = runtool {
474         cmd = Command::new(tool);
475         cmd.args(runtool_args);
476         cmd.arg(output_file);
477     } else {
478         cmd = Command::new(output_file);
479     }
480     if let Some(run_directory) = rustdoc_options.test_run_directory {
481         cmd.current_dir(run_directory);
482     }
483
484     let result = if rustdoc_options.nocapture {
485         cmd.status().map(|status| process::Output {
486             status,
487             stdout: Vec::new(),
488             stderr: Vec::new(),
489         })
490     } else {
491         cmd.output()
492     };
493     match result {
494         Err(e) => return Err(TestFailure::ExecutionError(e)),
495         Ok(out) => {
496             if lang_string.should_panic && out.status.success() {
497                 return Err(TestFailure::UnexpectedRunPass);
498             } else if !lang_string.should_panic && !out.status.success() {
499                 return Err(TestFailure::ExecutionFailure(out));
500             }
501         }
502     }
503
504     Ok(())
505 }
506
507 /// Transforms a test into code that can be compiled into a Rust binary, and returns the number of
508 /// lines before the test code begins as well as if the output stream supports colors or not.
509 pub(crate) fn make_test(
510     s: &str,
511     crate_name: Option<&str>,
512     dont_insert_main: bool,
513     opts: &GlobalTestOptions,
514     edition: Edition,
515     test_id: Option<&str>,
516 ) -> (String, usize, bool) {
517     let (crate_attrs, everything_else, crates) = partition_source(s, edition);
518     let everything_else = everything_else.trim();
519     let mut line_offset = 0;
520     let mut prog = String::new();
521     let mut supports_color = false;
522
523     if opts.attrs.is_empty() {
524         // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
525         // lints that are commonly triggered in doctests. The crate-level test attributes are
526         // commonly used to make tests fail in case they trigger warnings, so having this there in
527         // that case may cause some tests to pass when they shouldn't have.
528         prog.push_str("#![allow(unused)]\n");
529         line_offset += 1;
530     }
531
532     // Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
533     for attr in &opts.attrs {
534         prog.push_str(&format!("#![{attr}]\n"));
535         line_offset += 1;
536     }
537
538     // Now push any outer attributes from the example, assuming they
539     // are intended to be crate attributes.
540     prog.push_str(&crate_attrs);
541     prog.push_str(&crates);
542
543     // Uses librustc_ast to parse the doctest and find if there's a main fn and the extern
544     // crate already is included.
545     let result = rustc_driver::catch_fatal_errors(|| {
546         rustc_span::create_session_if_not_set_then(edition, |_| {
547             use rustc_errors::emitter::{Emitter, EmitterWriter};
548             use rustc_errors::Handler;
549             use rustc_parse::parser::ForceCollect;
550             use rustc_span::source_map::FilePathMapping;
551
552             let filename = FileName::anon_source_code(s);
553             let source = crates + everything_else;
554
555             // Any errors in parsing should also appear when the doctest is compiled for real, so just
556             // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
557             let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
558             let fallback_bundle =
559                 rustc_errors::fallback_fluent_bundle(rustc_errors::DEFAULT_LOCALE_RESOURCES, false);
560             supports_color = EmitterWriter::stderr(
561                 ColorConfig::Auto,
562                 None,
563                 None,
564                 fallback_bundle.clone(),
565                 false,
566                 false,
567                 Some(80),
568                 false,
569                 false,
570             )
571             .supports_color();
572
573             let emitter = EmitterWriter::new(
574                 Box::new(io::sink()),
575                 None,
576                 None,
577                 fallback_bundle,
578                 false,
579                 false,
580                 false,
581                 None,
582                 false,
583                 false,
584             );
585
586             // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
587             let handler = Handler::with_emitter(false, None, Box::new(emitter));
588             let sess = ParseSess::with_span_handler(handler, sm);
589
590             let mut found_main = false;
591             let mut found_extern_crate = crate_name.is_none();
592             let mut found_macro = false;
593
594             let mut parser = match maybe_new_parser_from_source_str(&sess, filename, source) {
595                 Ok(p) => p,
596                 Err(errs) => {
597                     drop(errs);
598                     return (found_main, found_extern_crate, found_macro);
599                 }
600             };
601
602             loop {
603                 match parser.parse_item(ForceCollect::No) {
604                     Ok(Some(item)) => {
605                         if !found_main {
606                             if let ast::ItemKind::Fn(..) = item.kind {
607                                 if item.ident.name == sym::main {
608                                     found_main = true;
609                                 }
610                             }
611                         }
612
613                         if !found_extern_crate {
614                             if let ast::ItemKind::ExternCrate(original) = item.kind {
615                                 // This code will never be reached if `crate_name` is none because
616                                 // `found_extern_crate` is initialized to `true` if it is none.
617                                 let crate_name = crate_name.unwrap();
618
619                                 match original {
620                                     Some(name) => found_extern_crate = name.as_str() == crate_name,
621                                     None => found_extern_crate = item.ident.as_str() == crate_name,
622                                 }
623                             }
624                         }
625
626                         if !found_macro {
627                             if let ast::ItemKind::MacCall(..) = item.kind {
628                                 found_macro = true;
629                             }
630                         }
631
632                         if found_main && found_extern_crate {
633                             break;
634                         }
635                     }
636                     Ok(None) => break,
637                     Err(e) => {
638                         e.cancel();
639                         break;
640                     }
641                 }
642
643                 // The supplied slice is only used for diagnostics,
644                 // which are swallowed here anyway.
645                 parser.maybe_consume_incorrect_semicolon(&[]);
646             }
647
648             // Reset errors so that they won't be reported as compiler bugs when dropping the
649             // handler. Any errors in the tests will be reported when the test file is compiled,
650             // Note that we still need to cancel the errors above otherwise `DiagnosticBuilder`
651             // will panic on drop.
652             sess.span_diagnostic.reset_err_count();
653
654             (found_main, found_extern_crate, found_macro)
655         })
656     });
657     let Ok((already_has_main, already_has_extern_crate, found_macro)) = result
658     else {
659         // If the parser panicked due to a fatal error, pass the test code through unchanged.
660         // The error will be reported during compilation.
661         return (s.to_owned(), 0, false);
662     };
663
664     // If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't
665     // see it. In that case, run the old text-based scan to see if they at least have a main
666     // function written inside a macro invocation. See
667     // https://github.com/rust-lang/rust/issues/56898
668     let already_has_main = if found_macro && !already_has_main {
669         s.lines()
670             .map(|line| {
671                 let comment = line.find("//");
672                 if let Some(comment_begins) = comment { &line[0..comment_begins] } else { line }
673             })
674             .any(|code| code.contains("fn main"))
675     } else {
676         already_has_main
677     };
678
679     // Don't inject `extern crate std` because it's already injected by the
680     // compiler.
681     if !already_has_extern_crate && !opts.no_crate_inject && crate_name != Some("std") {
682         if let Some(crate_name) = crate_name {
683             // Don't inject `extern crate` if the crate is never used.
684             // NOTE: this is terribly inaccurate because it doesn't actually
685             // parse the source, but only has false positives, not false
686             // negatives.
687             if s.contains(crate_name) {
688                 prog.push_str(&format!("extern crate r#{crate_name};\n"));
689                 line_offset += 1;
690             }
691         }
692     }
693
694     // FIXME: This code cannot yet handle no_std test cases yet
695     if dont_insert_main || already_has_main || prog.contains("![no_std]") {
696         prog.push_str(everything_else);
697     } else {
698         let returns_result = everything_else.trim_end().ends_with("(())");
699         // Give each doctest main function a unique name.
700         // This is for example needed for the tooling around `-C instrument-coverage`.
701         let inner_fn_name = if let Some(test_id) = test_id {
702             format!("_doctest_main_{test_id}")
703         } else {
704             "_inner".into()
705         };
706         let inner_attr = if test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
707         let (main_pre, main_post) = if returns_result {
708             (
709                 format!(
710                     "fn main() {{ {inner_attr}fn {inner_fn_name}() -> Result<(), impl core::fmt::Debug> {{\n",
711                 ),
712                 format!("\n}} {inner_fn_name}().unwrap() }}"),
713             )
714         } else if test_id.is_some() {
715             (
716                 format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
717                 format!("\n}} {inner_fn_name}() }}"),
718             )
719         } else {
720             ("fn main() {\n".into(), "\n}".into())
721         };
722         // Note on newlines: We insert a line/newline *before*, and *after*
723         // the doctest and adjust the `line_offset` accordingly.
724         // In the case of `-C instrument-coverage`, this means that the generated
725         // inner `main` function spans from the doctest opening codeblock to the
726         // closing one. For example
727         // /// ``` <- start of the inner main
728         // /// <- code under doctest
729         // /// ``` <- end of the inner main
730         line_offset += 1;
731
732         prog.extend([&main_pre, everything_else, &main_post].iter().cloned());
733     }
734
735     debug!("final doctest:\n{prog}");
736
737     (prog, line_offset, supports_color)
738 }
739
740 fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool {
741     if source.is_empty() {
742         // Empty content so nothing to check in here...
743         return true;
744     }
745     rustc_driver::catch_fatal_errors(|| {
746         rustc_span::create_session_if_not_set_then(edition, |_| {
747             use rustc_errors::emitter::EmitterWriter;
748             use rustc_errors::Handler;
749             use rustc_span::source_map::FilePathMapping;
750
751             let filename = FileName::anon_source_code(source);
752             // Any errors in parsing should also appear when the doctest is compiled for real, so just
753             // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
754             let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
755             let fallback_bundle =
756                 rustc_errors::fallback_fluent_bundle(rustc_errors::DEFAULT_LOCALE_RESOURCES, false);
757
758             let emitter = EmitterWriter::new(
759                 Box::new(io::sink()),
760                 None,
761                 None,
762                 fallback_bundle,
763                 false,
764                 false,
765                 false,
766                 None,
767                 false,
768                 false,
769             );
770
771             let handler = Handler::with_emitter(false, None, Box::new(emitter));
772             let sess = ParseSess::with_span_handler(handler, sm);
773             let mut parser =
774                 match maybe_new_parser_from_source_str(&sess, filename, source.to_owned()) {
775                     Ok(p) => p,
776                     Err(_) => {
777                         debug!("Cannot build a parser to check mod attr so skipping...");
778                         return true;
779                     }
780                 };
781             // If a parsing error happened, it's very likely that the attribute is incomplete.
782             if let Err(e) = parser.parse_attribute(InnerAttrPolicy::Permitted) {
783                 e.cancel();
784                 return false;
785             }
786             // We now check if there is an unclosed delimiter for the attribute. To do so, we look at
787             // the `unclosed_delims` and see if the opening square bracket was closed.
788             parser
789                 .unclosed_delims()
790                 .get(0)
791                 .map(|unclosed| {
792                     unclosed.unclosed_span.map(|s| s.lo()).unwrap_or(BytePos(0)) != BytePos(2)
793                 })
794                 .unwrap_or(true)
795         })
796     })
797     .unwrap_or(false)
798 }
799
800 fn partition_source(s: &str, edition: Edition) -> (String, String, String) {
801     #[derive(Copy, Clone, PartialEq)]
802     enum PartitionState {
803         Attrs,
804         Crates,
805         Other,
806     }
807     let mut state = PartitionState::Attrs;
808     let mut before = String::new();
809     let mut crates = String::new();
810     let mut after = String::new();
811
812     let mut mod_attr_pending = String::new();
813
814     for line in s.lines() {
815         let trimline = line.trim();
816
817         // FIXME(misdreavus): if a doc comment is placed on an extern crate statement, it will be
818         // shunted into "everything else"
819         match state {
820             PartitionState::Attrs => {
821                 state = if trimline.starts_with("#![") {
822                     if !check_if_attr_is_complete(line, edition) {
823                         mod_attr_pending = line.to_owned();
824                     } else {
825                         mod_attr_pending.clear();
826                     }
827                     PartitionState::Attrs
828                 } else if trimline.chars().all(|c| c.is_whitespace())
829                     || (trimline.starts_with("//") && !trimline.starts_with("///"))
830                 {
831                     PartitionState::Attrs
832                 } else if trimline.starts_with("extern crate")
833                     || trimline.starts_with("#[macro_use] extern crate")
834                 {
835                     PartitionState::Crates
836                 } else {
837                     // First we check if the previous attribute was "complete"...
838                     if !mod_attr_pending.is_empty() {
839                         // If not, then we append the new line into the pending attribute to check
840                         // if this time it's complete...
841                         mod_attr_pending.push_str(line);
842                         if !trimline.is_empty()
843                             && check_if_attr_is_complete(&mod_attr_pending, edition)
844                         {
845                             // If it's complete, then we can clear the pending content.
846                             mod_attr_pending.clear();
847                         }
848                         // In any case, this is considered as `PartitionState::Attrs` so it's
849                         // prepended before rustdoc's inserts.
850                         PartitionState::Attrs
851                     } else {
852                         PartitionState::Other
853                     }
854                 };
855             }
856             PartitionState::Crates => {
857                 state = if trimline.starts_with("extern crate")
858                     || trimline.starts_with("#[macro_use] extern crate")
859                     || trimline.chars().all(|c| c.is_whitespace())
860                     || (trimline.starts_with("//") && !trimline.starts_with("///"))
861                 {
862                     PartitionState::Crates
863                 } else {
864                     PartitionState::Other
865                 };
866             }
867             PartitionState::Other => {}
868         }
869
870         match state {
871             PartitionState::Attrs => {
872                 before.push_str(line);
873                 before.push('\n');
874             }
875             PartitionState::Crates => {
876                 crates.push_str(line);
877                 crates.push('\n');
878             }
879             PartitionState::Other => {
880                 after.push_str(line);
881                 after.push('\n');
882             }
883         }
884     }
885
886     debug!("before:\n{before}");
887     debug!("crates:\n{crates}");
888     debug!("after:\n{after}");
889
890     (before, after, crates)
891 }
892
893 pub(crate) trait Tester {
894     fn add_test(&mut self, test: String, config: LangString, line: usize);
895     fn get_line(&self) -> usize {
896         0
897     }
898     fn register_header(&mut self, _name: &str, _level: u32) {}
899 }
900
901 pub(crate) struct Collector {
902     pub(crate) tests: Vec<test::TestDescAndFn>,
903
904     // The name of the test displayed to the user, separated by `::`.
905     //
906     // In tests from Rust source, this is the path to the item
907     // e.g., `["std", "vec", "Vec", "push"]`.
908     //
909     // In tests from a markdown file, this is the titles of all headers (h1~h6)
910     // of the sections that contain the code block, e.g., if the markdown file is
911     // written as:
912     //
913     // ``````markdown
914     // # Title
915     //
916     // ## Subtitle
917     //
918     // ```rust
919     // assert!(true);
920     // ```
921     // ``````
922     //
923     // the `names` vector of that test will be `["Title", "Subtitle"]`.
924     names: Vec<String>,
925
926     rustdoc_options: RustdocOptions,
927     use_headers: bool,
928     enable_per_target_ignores: bool,
929     crate_name: String,
930     opts: GlobalTestOptions,
931     position: Span,
932     source_map: Option<Lrc<SourceMap>>,
933     filename: Option<PathBuf>,
934     visited_tests: FxHashMap<(String, usize), usize>,
935     unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
936     compiling_test_count: AtomicUsize,
937 }
938
939 impl Collector {
940     pub(crate) fn new(
941         crate_name: String,
942         rustdoc_options: RustdocOptions,
943         use_headers: bool,
944         opts: GlobalTestOptions,
945         source_map: Option<Lrc<SourceMap>>,
946         filename: Option<PathBuf>,
947         enable_per_target_ignores: bool,
948     ) -> Collector {
949         Collector {
950             tests: Vec::new(),
951             names: Vec::new(),
952             rustdoc_options,
953             use_headers,
954             enable_per_target_ignores,
955             crate_name,
956             opts,
957             position: DUMMY_SP,
958             source_map,
959             filename,
960             visited_tests: FxHashMap::default(),
961             unused_extern_reports: Default::default(),
962             compiling_test_count: AtomicUsize::new(0),
963         }
964     }
965
966     fn generate_name(&self, line: usize, filename: &FileName) -> String {
967         let mut item_path = self.names.join("::");
968         item_path.retain(|c| c != ' ');
969         if !item_path.is_empty() {
970             item_path.push(' ');
971         }
972         format!("{} - {}(line {})", filename.prefer_local(), item_path, line)
973     }
974
975     pub(crate) fn set_position(&mut self, position: Span) {
976         self.position = position;
977     }
978
979     fn get_filename(&self) -> FileName {
980         if let Some(ref source_map) = self.source_map {
981             let filename = source_map.span_to_filename(self.position);
982             if let FileName::Real(ref filename) = filename {
983                 if let Ok(cur_dir) = env::current_dir() {
984                     if let Some(local_path) = filename.local_path() {
985                         if let Ok(path) = local_path.strip_prefix(&cur_dir) {
986                             return path.to_owned().into();
987                         }
988                     }
989                 }
990             }
991             filename
992         } else if let Some(ref filename) = self.filename {
993             filename.clone().into()
994         } else {
995             FileName::Custom("input".to_owned())
996         }
997     }
998 }
999
1000 impl Tester for Collector {
1001     fn add_test(&mut self, test: String, config: LangString, line: usize) {
1002         let filename = self.get_filename();
1003         let name = self.generate_name(line, &filename);
1004         let crate_name = self.crate_name.clone();
1005         let opts = self.opts.clone();
1006         let edition = config.edition.unwrap_or(self.rustdoc_options.edition);
1007         let rustdoc_options = self.rustdoc_options.clone();
1008         let runtool = self.rustdoc_options.runtool.clone();
1009         let runtool_args = self.rustdoc_options.runtool_args.clone();
1010         let target = self.rustdoc_options.target.clone();
1011         let target_str = target.to_string();
1012         let unused_externs = self.unused_extern_reports.clone();
1013         let no_run = config.no_run || rustdoc_options.no_run;
1014         if !config.compile_fail {
1015             self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
1016         }
1017
1018         let path = match &filename {
1019             FileName::Real(path) => {
1020                 if let Some(local_path) = path.local_path() {
1021                     local_path.to_path_buf()
1022                 } else {
1023                     // Somehow we got the filename from the metadata of another crate, should never happen
1024                     unreachable!("doctest from a different crate");
1025                 }
1026             }
1027             _ => PathBuf::from(r"doctest.rs"),
1028         };
1029
1030         // For example `module/file.rs` would become `module_file_rs`
1031         let file = filename
1032             .prefer_local()
1033             .to_string_lossy()
1034             .chars()
1035             .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
1036             .collect::<String>();
1037         let test_id = format!(
1038             "{file}_{line}_{number}",
1039             file = file,
1040             line = line,
1041             number = {
1042                 // Increases the current test number, if this file already
1043                 // exists or it creates a new entry with a test number of 0.
1044                 self.visited_tests.entry((file.clone(), line)).and_modify(|v| *v += 1).or_insert(0)
1045             },
1046         );
1047         let outdir = if let Some(mut path) = rustdoc_options.persist_doctests.clone() {
1048             path.push(&test_id);
1049
1050             if let Err(err) = std::fs::create_dir_all(&path) {
1051                 eprintln!("Couldn't create directory for doctest executables: {}", err);
1052                 panic::resume_unwind(Box::new(()));
1053             }
1054
1055             DirState::Perm(path)
1056         } else {
1057             DirState::Temp(
1058                 TempFileBuilder::new()
1059                     .prefix("rustdoctest")
1060                     .tempdir()
1061                     .expect("rustdoc needs a tempdir"),
1062             )
1063         };
1064
1065         debug!("creating test {name}: {test}");
1066         self.tests.push(test::TestDescAndFn {
1067             desc: test::TestDesc {
1068                 name: test::DynTestName(name),
1069                 ignore: match config.ignore {
1070                     Ignore::All => true,
1071                     Ignore::None => false,
1072                     Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
1073                 },
1074                 ignore_message: None,
1075                 // compiler failures are test failures
1076                 should_panic: test::ShouldPanic::No,
1077                 compile_fail: config.compile_fail,
1078                 no_run,
1079                 test_type: test::TestType::DocTest,
1080             },
1081             testfn: test::DynTestFn(Box::new(move || {
1082                 let report_unused_externs = |uext| {
1083                     unused_externs.lock().unwrap().push(uext);
1084                 };
1085                 let res = run_test(
1086                     &test,
1087                     &crate_name,
1088                     line,
1089                     rustdoc_options,
1090                     config,
1091                     no_run,
1092                     runtool,
1093                     runtool_args,
1094                     target,
1095                     &opts,
1096                     edition,
1097                     outdir,
1098                     path,
1099                     &test_id,
1100                     report_unused_externs,
1101                 );
1102
1103                 if let Err(err) = res {
1104                     match err {
1105                         TestFailure::CompileError => {
1106                             eprint!("Couldn't compile the test.");
1107                         }
1108                         TestFailure::UnexpectedCompilePass => {
1109                             eprint!("Test compiled successfully, but it's marked `compile_fail`.");
1110                         }
1111                         TestFailure::UnexpectedRunPass => {
1112                             eprint!("Test executable succeeded, but it's marked `should_panic`.");
1113                         }
1114                         TestFailure::MissingErrorCodes(codes) => {
1115                             eprint!("Some expected error codes were not found: {:?}", codes);
1116                         }
1117                         TestFailure::ExecutionError(err) => {
1118                             eprint!("Couldn't run the test: {err}");
1119                             if err.kind() == io::ErrorKind::PermissionDenied {
1120                                 eprint!(" - maybe your tempdir is mounted with noexec?");
1121                             }
1122                         }
1123                         TestFailure::ExecutionFailure(out) => {
1124                             eprintln!("Test executable failed ({reason}).", reason = out.status);
1125
1126                             // FIXME(#12309): An unfortunate side-effect of capturing the test
1127                             // executable's output is that the relative ordering between the test's
1128                             // stdout and stderr is lost. However, this is better than the
1129                             // alternative: if the test executable inherited the parent's I/O
1130                             // handles the output wouldn't be captured at all, even on success.
1131                             //
1132                             // The ordering could be preserved if the test process' stderr was
1133                             // redirected to stdout, but that functionality does not exist in the
1134                             // standard library, so it may not be portable enough.
1135                             let stdout = str::from_utf8(&out.stdout).unwrap_or_default();
1136                             let stderr = str::from_utf8(&out.stderr).unwrap_or_default();
1137
1138                             if !stdout.is_empty() || !stderr.is_empty() {
1139                                 eprintln!();
1140
1141                                 if !stdout.is_empty() {
1142                                     eprintln!("stdout:\n{stdout}");
1143                                 }
1144
1145                                 if !stderr.is_empty() {
1146                                     eprintln!("stderr:\n{stderr}");
1147                                 }
1148                             }
1149                         }
1150                     }
1151
1152                     panic::resume_unwind(Box::new(()));
1153                 }
1154                 Ok(())
1155             })),
1156         });
1157     }
1158
1159     fn get_line(&self) -> usize {
1160         if let Some(ref source_map) = self.source_map {
1161             let line = self.position.lo().to_usize();
1162             let line = source_map.lookup_char_pos(BytePos(line as u32)).line;
1163             if line > 0 { line - 1 } else { line }
1164         } else {
1165             0
1166         }
1167     }
1168
1169     fn register_header(&mut self, name: &str, level: u32) {
1170         if self.use_headers {
1171             // We use these headings as test names, so it's good if
1172             // they're valid identifiers.
1173             let name = name
1174                 .chars()
1175                 .enumerate()
1176                 .map(|(i, c)| {
1177                     if (i == 0 && rustc_lexer::is_id_start(c))
1178                         || (i != 0 && rustc_lexer::is_id_continue(c))
1179                     {
1180                         c
1181                     } else {
1182                         '_'
1183                     }
1184                 })
1185                 .collect::<String>();
1186
1187             // Here we try to efficiently assemble the header titles into the
1188             // test name in the form of `h1::h2::h3::h4::h5::h6`.
1189             //
1190             // Suppose that originally `self.names` contains `[h1, h2, h3]`...
1191             let level = level as usize;
1192             if level <= self.names.len() {
1193                 // ... Consider `level == 2`. All headers in the lower levels
1194                 // are irrelevant in this new level. So we should reset
1195                 // `self.names` to contain headers until <h2>, and replace that
1196                 // slot with the new name: `[h1, name]`.
1197                 self.names.truncate(level);
1198                 self.names[level - 1] = name;
1199             } else {
1200                 // ... On the other hand, consider `level == 5`. This means we
1201                 // need to extend `self.names` to contain five headers. We fill
1202                 // in the missing level (<h4>) with `_`. Thus `self.names` will
1203                 // become `[h1, h2, h3, "_", name]`.
1204                 if level - 1 > self.names.len() {
1205                     self.names.resize(level - 1, "_".to_owned());
1206                 }
1207                 self.names.push(name);
1208             }
1209         }
1210     }
1211 }
1212
1213 struct HirCollector<'a, 'hir, 'tcx> {
1214     sess: &'a Session,
1215     collector: &'a mut Collector,
1216     map: Map<'hir>,
1217     codes: ErrorCodes,
1218     tcx: TyCtxt<'tcx>,
1219 }
1220
1221 impl<'a, 'hir, 'tcx> HirCollector<'a, 'hir, 'tcx> {
1222     fn visit_testable<F: FnOnce(&mut Self)>(
1223         &mut self,
1224         name: String,
1225         hir_id: HirId,
1226         sp: Span,
1227         nested: F,
1228     ) {
1229         let ast_attrs = self.tcx.hir().attrs(hir_id);
1230         if let Some(ref cfg) = ast_attrs.cfg(self.tcx, &FxHashSet::default()) {
1231             if !cfg.matches(&self.sess.parse_sess, Some(self.sess.features_untracked())) {
1232                 return;
1233             }
1234         }
1235
1236         let has_name = !name.is_empty();
1237         if has_name {
1238             self.collector.names.push(name);
1239         }
1240
1241         // The collapse-docs pass won't combine sugared/raw doc attributes, or included files with
1242         // anything else, this will combine them for us.
1243         let attrs = Attributes::from_ast(ast_attrs);
1244         if let Some(doc) = attrs.collapsed_doc_value() {
1245             // Use the outermost invocation, so that doctest names come from where the docs were written.
1246             let span = ast_attrs
1247                 .span()
1248                 .map(|span| span.ctxt().outer_expn().expansion_cause().unwrap_or(span))
1249                 .unwrap_or(DUMMY_SP);
1250             self.collector.set_position(span);
1251             markdown::find_testable_code(
1252                 &doc,
1253                 self.collector,
1254                 self.codes,
1255                 self.collector.enable_per_target_ignores,
1256                 Some(&crate::html::markdown::ExtraInfo::new(
1257                     self.tcx,
1258                     hir_id,
1259                     span_of_attrs(&attrs).unwrap_or(sp),
1260                 )),
1261             );
1262         }
1263
1264         nested(self);
1265
1266         if has_name {
1267             self.collector.names.pop();
1268         }
1269     }
1270 }
1271
1272 impl<'a, 'hir, 'tcx> intravisit::Visitor<'hir> for HirCollector<'a, 'hir, 'tcx> {
1273     type NestedFilter = nested_filter::All;
1274
1275     fn nested_visit_map(&mut self) -> Self::Map {
1276         self.map
1277     }
1278
1279     fn visit_item(&mut self, item: &'hir hir::Item<'_>) {
1280         let name = match &item.kind {
1281             hir::ItemKind::Impl(impl_) => {
1282                 rustc_hir_pretty::id_to_string(&self.map, impl_.self_ty.hir_id)
1283             }
1284             _ => item.ident.to_string(),
1285         };
1286
1287         self.visit_testable(name, item.hir_id(), item.span, |this| {
1288             intravisit::walk_item(this, item);
1289         });
1290     }
1291
1292     fn visit_trait_item(&mut self, item: &'hir hir::TraitItem<'_>) {
1293         self.visit_testable(item.ident.to_string(), item.hir_id(), item.span, |this| {
1294             intravisit::walk_trait_item(this, item);
1295         });
1296     }
1297
1298     fn visit_impl_item(&mut self, item: &'hir hir::ImplItem<'_>) {
1299         self.visit_testable(item.ident.to_string(), item.hir_id(), item.span, |this| {
1300             intravisit::walk_impl_item(this, item);
1301         });
1302     }
1303
1304     fn visit_foreign_item(&mut self, item: &'hir hir::ForeignItem<'_>) {
1305         self.visit_testable(item.ident.to_string(), item.hir_id(), item.span, |this| {
1306             intravisit::walk_foreign_item(this, item);
1307         });
1308     }
1309
1310     fn visit_variant(&mut self, v: &'hir hir::Variant<'_>) {
1311         self.visit_testable(v.ident.to_string(), v.hir_id, v.span, |this| {
1312             intravisit::walk_variant(this, v);
1313         });
1314     }
1315
1316     fn visit_field_def(&mut self, f: &'hir hir::FieldDef<'_>) {
1317         self.visit_testable(f.ident.to_string(), f.hir_id, f.span, |this| {
1318             intravisit::walk_field_def(this, f);
1319         });
1320     }
1321 }
1322
1323 #[cfg(test)]
1324 mod tests;