]>
Commit | Line | Data |
---|---|---|
74b04a01 | 1 | use rustc_ast::ast; |
0531ce1d | 2 | use rustc_data_structures::sync::Lrc; |
ba9703b0 | 3 | use rustc_errors::ErrorReported; |
60c5eb7d | 4 | use rustc_feature::UnstableFeatures; |
dfeec247 XL |
5 | use rustc_hir as hir; |
6 | use rustc_hir::intravisit; | |
f9f354fc | 7 | use rustc_hir::{HirId, CRATE_HIR_ID}; |
532ac7d7 | 8 | use rustc_interface::interface; |
ba9703b0 | 9 | use rustc_middle::hir::map::Map; |
f9f354fc XL |
10 | use rustc_middle::ty::TyCtxt; |
11 | use rustc_session::config::{self, CrateType}; | |
12 | use rustc_session::{lint, DiagnosticOutput, Session}; | |
dfeec247 XL |
13 | use rustc_span::edition::Edition; |
14 | use rustc_span::source_map::SourceMap; | |
15 | use rustc_span::symbol::sym; | |
16 | use rustc_span::{BytePos, FileName, Pos, Span, DUMMY_SP}; | |
e1599b0c | 17 | use rustc_target::spec::TargetTriple; |
f9f354fc XL |
18 | use tempfile::Builder as TempFileBuilder; |
19 | ||
ba9703b0 | 20 | use std::collections::HashMap; |
0731742a | 21 | use std::env; |
e1599b0c XL |
22 | use std::io::{self, Write}; |
23 | use std::panic; | |
532ac7d7 | 24 | use std::path::PathBuf; |
e1599b0c | 25 | use std::process::{self, Command, Stdio}; |
0731742a | 26 | use std::str; |
1a4d82fc | 27 | |
9fa01778 XL |
28 | use crate::clean::Attributes; |
29 | use crate::config::Options; | |
f9f354fc | 30 | use crate::core::init_lints; |
dfeec247 | 31 | use crate::html::markdown::{self, ErrorCodes, Ignore, LangString}; |
f9f354fc | 32 | use crate::passes::span_of_attrs; |
1a4d82fc | 33 | |
9346a6ac AL |
34 | #[derive(Clone, Default)] |
35 | pub struct TestOptions { | |
83c7162d | 36 | /// Whether to disable the default `extern crate my_crate;` when creating doctests. |
9346a6ac | 37 | pub no_crate_inject: bool, |
83c7162d XL |
38 | /// Whether to emit compilation warnings when compiling doctests. Setting this will suppress |
39 | /// the default `#![allow(unused)]`. | |
40 | pub display_warnings: bool, | |
41 | /// Additional crate-level attributes to add to doctests. | |
9346a6ac AL |
42 | pub attrs: Vec<String>, |
43 | } | |
44 | ||
f9f354fc | 45 | pub fn run(options: Options) -> Result<(), String> { |
a1dfa0c6 | 46 | let input = config::Input::File(options.input.clone()); |
1a4d82fc | 47 | |
f9f354fc XL |
48 | let invalid_codeblock_attribute_name = rustc_lint::builtin::INVALID_CODEBLOCK_ATTRIBUTES.name; |
49 | ||
f035d41b | 50 | // In addition to those specific lints, we also need to allow those given through |
f9f354fc | 51 | // command line, otherwise they'll get ignored and we don't want that. |
f035d41b | 52 | let allowed_lints = vec![invalid_codeblock_attribute_name.to_owned()]; |
f9f354fc | 53 | |
f035d41b | 54 | let (lint_opts, lint_caps) = init_lints(allowed_lints, options.lint_opts.clone(), |lint| { |
f9f354fc XL |
55 | if lint.name == invalid_codeblock_attribute_name { |
56 | None | |
57 | } else { | |
58 | Some((lint.name_lower(), lint::Allow)) | |
59 | } | |
60 | }); | |
61 | ||
62 | let crate_types = | |
63 | if options.proc_macro_crate { vec![CrateType::ProcMacro] } else { vec![CrateType::Rlib] }; | |
e1599b0c | 64 | |
1a4d82fc | 65 | let sessopts = config::Options { |
dc9dc135 | 66 | maybe_sysroot: options.maybe_sysroot.clone(), |
a1dfa0c6 | 67 | search_paths: options.libs.clone(), |
e1599b0c | 68 | crate_types, |
f9f354fc XL |
69 | lint_opts: if !options.display_warnings { lint_opts } else { vec![] }, |
70 | lint_cap: Some(options.lint_cap.clone().unwrap_or_else(|| lint::Forbid)), | |
a1dfa0c6 XL |
71 | cg: options.codegen_options.clone(), |
72 | externs: options.externs.clone(), | |
9e0c209e | 73 | unstable_features: UnstableFeatures::from_environment(), |
32a655c1 | 74 | actually_rustdoc: true, |
dfeec247 | 75 | debugging_opts: config::DebuggingOptions { ..config::basic_debugging_options() }, |
a1dfa0c6 | 76 | edition: options.edition, |
e1599b0c | 77 | target_triple: options.target.clone(), |
b7449926 | 78 | ..config::Options::default() |
1a4d82fc | 79 | }; |
94b46f34 | 80 | |
e74abb32 | 81 | let mut cfgs = options.cfgs.clone(); |
60c5eb7d | 82 | cfgs.push("doc".to_owned()); |
e74abb32 | 83 | cfgs.push("doctest".to_owned()); |
532ac7d7 XL |
84 | let config = interface::Config { |
85 | opts: sessopts, | |
e74abb32 | 86 | crate_cfg: interface::parse_cfgspecs(cfgs), |
532ac7d7 XL |
87 | input, |
88 | input_path: None, | |
89 | output_file: None, | |
90 | output_dir: None, | |
91 | file_loader: None, | |
92 | diagnostic_output: DiagnosticOutput::Default, | |
93 | stderr: None, | |
94 | crate_name: options.crate_name.clone(), | |
f9f354fc | 95 | lint_caps, |
e74abb32 | 96 | register_lints: None, |
60c5eb7d XL |
97 | override_queries: None, |
98 | registry: rustc_driver::diagnostics_registry(), | |
532ac7d7 XL |
99 | }; |
100 | ||
101 | let mut test_args = options.test_args.clone(); | |
102 | let display_warnings = options.display_warnings; | |
103 | ||
dfeec247 XL |
104 | let tests = interface::run_compiler(config, |compiler| { |
105 | compiler.enter(|queries| { | |
106 | let lower_to_hir = queries.lower_to_hir()?; | |
107 | ||
74b04a01 | 108 | let mut opts = scrape_test_config(lower_to_hir.peek().0); |
dfeec247 XL |
109 | opts.display_warnings |= options.display_warnings; |
110 | let enable_per_target_ignores = options.enable_per_target_ignores; | |
111 | let mut collector = Collector::new( | |
112 | queries.crate_name()?.peek().to_string(), | |
113 | options, | |
114 | false, | |
115 | opts, | |
f9f354fc | 116 | Some(compiler.session().parse_sess.clone_source_map()), |
dfeec247 XL |
117 | None, |
118 | enable_per_target_ignores, | |
119 | ); | |
120 | ||
121 | let mut global_ctxt = queries.global_ctxt()?.take(); | |
122 | ||
123 | global_ctxt.enter(|tcx| { | |
124 | let krate = tcx.hir().krate(); | |
f9f354fc | 125 | |
dfeec247 XL |
126 | let mut hir_collector = HirCollector { |
127 | sess: compiler.session(), | |
128 | collector: &mut collector, | |
ba9703b0 | 129 | map: tcx.hir(), |
dfeec247 XL |
130 | codes: ErrorCodes::from( |
131 | compiler.session().opts.unstable_features.is_nightly_build(), | |
132 | ), | |
f9f354fc | 133 | tcx, |
dfeec247 | 134 | }; |
f9f354fc XL |
135 | hir_collector.visit_testable( |
136 | "".to_string(), | |
137 | &krate.item.attrs, | |
138 | CRATE_HIR_ID, | |
139 | krate.item.span, | |
140 | |this| { | |
141 | intravisit::walk_crate(this, krate); | |
142 | }, | |
143 | ); | |
94b46f34 | 144 | }); |
dfeec247 | 145 | compiler.session().abort_if_errors(); |
1a4d82fc | 146 | |
dfeec247 XL |
147 | let ret: Result<_, ErrorReported> = Ok(collector.tests); |
148 | ret | |
149 | }) | |
150 | }); | |
151 | let tests = match tests { | |
152 | Ok(tests) => tests, | |
f9f354fc | 153 | Err(ErrorReported) => return Err(String::new()), |
dfeec247 | 154 | }; |
1a4d82fc | 155 | |
532ac7d7 XL |
156 | test_args.insert(0, "rustdoctest".to_string()); |
157 | ||
158 | testing::test_main( | |
159 | &test_args, | |
160 | tests, | |
dfeec247 | 161 | Some(testing::Options::new().display_output(display_warnings)), |
532ac7d7 XL |
162 | ); |
163 | ||
f9f354fc | 164 | Ok(()) |
1a4d82fc JJ |
165 | } |
166 | ||
0731742a | 167 | // Look for `#![doc(test(no_crate_inject))]`, used by crates in the std facade. |
f035d41b | 168 | fn scrape_test_config(krate: &::rustc_hir::Crate<'_>) -> TestOptions { |
74b04a01 | 169 | use rustc_ast_pretty::pprust; |
c34b1796 | 170 | |
dfeec247 XL |
171 | let mut opts = |
172 | TestOptions { no_crate_inject: false, display_warnings: false, attrs: Vec::new() }; | |
9346a6ac | 173 | |
dfeec247 | 174 | let test_attrs: Vec<_> = krate |
ba9703b0 | 175 | .item |
dfeec247 XL |
176 | .attrs |
177 | .iter() | |
48663c56 | 178 | .filter(|a| a.check_name(sym::doc)) |
cc61c64b | 179 | .flat_map(|a| a.meta_item_list().unwrap_or_else(Vec::new)) |
48663c56 | 180 | .filter(|a| a.check_name(sym::test)) |
cc61c64b XL |
181 | .collect(); |
182 | let attrs = test_attrs.iter().flat_map(|a| a.meta_item_list().unwrap_or(&[])); | |
183 | ||
9346a6ac | 184 | for attr in attrs { |
48663c56 | 185 | if attr.check_name(sym::no_crate_inject) { |
9346a6ac AL |
186 | opts.no_crate_inject = true; |
187 | } | |
48663c56 | 188 | if attr.check_name(sym::attr) { |
9346a6ac AL |
189 | if let Some(l) = attr.meta_item_list() { |
190 | for item in l { | |
9e0c209e | 191 | opts.attrs.push(pprust::meta_list_item_to_string(item)); |
c34b1796 AL |
192 | } |
193 | } | |
194 | } | |
195 | } | |
196 | ||
c30ab7b3 | 197 | opts |
c34b1796 AL |
198 | } |
199 | ||
dc9dc135 XL |
200 | /// Documentation test failure modes. |
201 | enum TestFailure { | |
202 | /// The test failed to compile. | |
203 | CompileError, | |
204 | /// The test is marked `compile_fail` but compiled successfully. | |
205 | UnexpectedCompilePass, | |
206 | /// The test failed to compile (as expected) but the compiler output did not contain all | |
207 | /// expected error codes. | |
208 | MissingErrorCodes(Vec<String>), | |
209 | /// The test binary was unable to be executed. | |
210 | ExecutionError(io::Error), | |
211 | /// The test binary exited with a non-zero exit code. | |
212 | /// | |
213 | /// This typically means an assertion in the test failed or another form of panic occurred. | |
214 | ExecutionFailure(process::Output), | |
215 | /// The test is marked `should_panic` but the test binary executed successfully. | |
216 | UnexpectedRunPass, | |
217 | } | |
218 | ||
ba9703b0 XL |
219 | enum DirState { |
220 | Temp(tempfile::TempDir), | |
221 | Perm(PathBuf), | |
222 | } | |
223 | ||
224 | impl DirState { | |
225 | fn path(&self) -> &std::path::Path { | |
226 | match self { | |
227 | DirState::Temp(t) => t.path(), | |
228 | DirState::Perm(p) => p.as_path(), | |
229 | } | |
230 | } | |
231 | } | |
232 | ||
dc9dc135 XL |
233 | fn run_test( |
234 | test: &str, | |
235 | cratename: &str, | |
dc9dc135 | 236 | line: usize, |
e1599b0c | 237 | options: Options, |
dc9dc135 XL |
238 | should_panic: bool, |
239 | no_run: bool, | |
240 | as_test_harness: bool, | |
e1599b0c XL |
241 | runtool: Option<String>, |
242 | runtool_args: Vec<String>, | |
243 | target: TargetTriple, | |
dc9dc135 XL |
244 | compile_fail: bool, |
245 | mut error_codes: Vec<String>, | |
246 | opts: &TestOptions, | |
dc9dc135 | 247 | edition: Edition, |
ba9703b0 XL |
248 | outdir: DirState, |
249 | path: PathBuf, | |
dc9dc135 | 250 | ) -> Result<(), TestFailure> { |
dfeec247 | 251 | let (test, line_offset) = make_test(test, Some(cratename), as_test_harness, opts, edition); |
48663c56 | 252 | |
532ac7d7 XL |
253 | let output_file = outdir.path().join("rust_out"); |
254 | ||
dfeec247 XL |
255 | let rustc_binary = options |
256 | .test_builder | |
f9f354fc | 257 | .as_deref() |
dfeec247 | 258 | .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc")); |
e1599b0c XL |
259 | let mut compiler = Command::new(&rustc_binary); |
260 | compiler.arg("--crate-type").arg("bin"); | |
261 | for cfg in &options.cfgs { | |
262 | compiler.arg("--cfg").arg(&cfg); | |
263 | } | |
264 | if let Some(sysroot) = options.maybe_sysroot { | |
265 | compiler.arg("--sysroot").arg(sysroot); | |
266 | } | |
267 | compiler.arg("--edition").arg(&edition.to_string()); | |
268 | compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", path); | |
dfeec247 | 269 | compiler.env("UNSTABLE_RUSTDOC_TEST_LINE", format!("{}", line as isize - line_offset as isize)); |
e1599b0c XL |
270 | compiler.arg("-o").arg(&output_file); |
271 | if as_test_harness { | |
272 | compiler.arg("--test"); | |
273 | } | |
274 | for lib_str in &options.lib_strs { | |
275 | compiler.arg("-L").arg(&lib_str); | |
276 | } | |
277 | for extern_str in &options.extern_strs { | |
278 | compiler.arg("--extern").arg(&extern_str); | |
279 | } | |
280 | compiler.arg("-Ccodegen-units=1"); | |
281 | for codegen_options_str in &options.codegen_options_strs { | |
282 | compiler.arg("-C").arg(&codegen_options_str); | |
283 | } | |
e74abb32 XL |
284 | for debugging_option_str in &options.debugging_options_strs { |
285 | compiler.arg("-Z").arg(&debugging_option_str); | |
286 | } | |
74b04a01 | 287 | if no_run && !compile_fail { |
e1599b0c XL |
288 | compiler.arg("--emit=metadata"); |
289 | } | |
ba9703b0 XL |
290 | compiler.arg("--target").arg(match target { |
291 | TargetTriple::TargetTriple(s) => s, | |
292 | TargetTriple::TargetPath(path) => { | |
293 | path.to_str().expect("target path must be valid unicode").to_string() | |
294 | } | |
295 | }); | |
041b39d2 | 296 | |
e1599b0c XL |
297 | compiler.arg("-"); |
298 | compiler.stdin(Stdio::piped()); | |
299 | compiler.stderr(Stdio::piped()); | |
74d20737 | 300 | |
e1599b0c XL |
301 | let mut child = compiler.spawn().expect("Failed to spawn rustc process"); |
302 | { | |
303 | let stdin = child.stdin.as_mut().expect("Failed to open stdin"); | |
304 | stdin.write_all(test.as_bytes()).expect("could write out test sources"); | |
305 | } | |
306 | let output = child.wait_with_output().expect("Failed to read stdout"); | |
307 | ||
308 | struct Bomb<'a>(&'a str); | |
309 | impl Drop for Bomb<'_> { | |
310 | fn drop(&mut self) { | |
dfeec247 | 311 | eprint!("{}", self.0); |
e1599b0c XL |
312 | } |
313 | } | |
e1599b0c XL |
314 | let out = str::from_utf8(&output.stderr).unwrap(); |
315 | let _bomb = Bomb(&out); | |
316 | match (output.status.success(), compile_fail) { | |
317 | (true, true) => { | |
dc9dc135 | 318 | return Err(TestFailure::UnexpectedCompilePass); |
74d20737 | 319 | } |
e1599b0c XL |
320 | (true, false) => {} |
321 | (false, true) => { | |
dc9dc135 | 322 | if !error_codes.is_empty() { |
dfeec247 | 323 | error_codes.retain(|err| !out.contains(&format!("error[{}]: ", err))); |
dc9dc135 XL |
324 | |
325 | if !error_codes.is_empty() { | |
326 | return Err(TestFailure::MissingErrorCodes(error_codes)); | |
327 | } | |
3157f602 XL |
328 | } |
329 | } | |
e1599b0c | 330 | (false, false) => { |
dc9dc135 | 331 | return Err(TestFailure::CompileError); |
041b39d2 | 332 | } |
74d20737 | 333 | } |
3157f602 | 334 | |
dc9dc135 XL |
335 | if no_run { |
336 | return Ok(()); | |
74d20737 | 337 | } |
1a4d82fc | 338 | |
1a4d82fc | 339 | // Run the code! |
e1599b0c XL |
340 | let mut cmd; |
341 | ||
342 | if let Some(tool) = runtool { | |
343 | cmd = Command::new(tool); | |
e1599b0c | 344 | cmd.args(runtool_args); |
ba9703b0 | 345 | cmd.arg(output_file); |
e1599b0c XL |
346 | } else { |
347 | cmd = Command::new(output_file); | |
348 | } | |
532ac7d7 | 349 | |
1a4d82fc | 350 | match cmd.output() { |
dc9dc135 | 351 | Err(e) => return Err(TestFailure::ExecutionError(e)), |
1a4d82fc | 352 | Ok(out) => { |
c34b1796 | 353 | if should_panic && out.status.success() { |
dc9dc135 | 354 | return Err(TestFailure::UnexpectedRunPass); |
c34b1796 | 355 | } else if !should_panic && !out.status.success() { |
dc9dc135 | 356 | return Err(TestFailure::ExecutionFailure(out)); |
1a4d82fc JJ |
357 | } |
358 | } | |
359 | } | |
dc9dc135 XL |
360 | |
361 | Ok(()) | |
1a4d82fc JJ |
362 | } |
363 | ||
48663c56 XL |
364 | /// Transforms a test into code that can be compiled into a Rust binary, and returns the number of |
365 | /// lines before the test code begins. | |
dfeec247 XL |
366 | pub fn make_test( |
367 | s: &str, | |
368 | cratename: Option<&str>, | |
369 | dont_insert_main: bool, | |
370 | opts: &TestOptions, | |
371 | edition: Edition, | |
372 | ) -> (String, usize) { | |
a1dfa0c6 | 373 | let (crate_attrs, everything_else, crates) = partition_source(s); |
0531ce1d | 374 | let everything_else = everything_else.trim(); |
2c00a5a8 | 375 | let mut line_offset = 0; |
1a4d82fc | 376 | let mut prog = String::new(); |
c34b1796 | 377 | |
83c7162d | 378 | if opts.attrs.is_empty() && !opts.display_warnings { |
abe05a73 XL |
379 | // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some |
380 | // lints that are commonly triggered in doctests. The crate-level test attributes are | |
381 | // commonly used to make tests fail in case they trigger warnings, so having this there in | |
382 | // that case may cause some tests to pass when they shouldn't have. | |
383 | prog.push_str("#![allow(unused)]\n"); | |
2c00a5a8 | 384 | line_offset += 1; |
abe05a73 | 385 | } |
c34b1796 | 386 | |
abe05a73 | 387 | // Next, any attributes that came from the crate root via #![doc(test(attr(...)))]. |
9346a6ac AL |
388 | for attr in &opts.attrs { |
389 | prog.push_str(&format!("#![{}]\n", attr)); | |
2c00a5a8 | 390 | line_offset += 1; |
1a4d82fc JJ |
391 | } |
392 | ||
abe05a73 XL |
393 | // Now push any outer attributes from the example, assuming they |
394 | // are intended to be crate attributes. | |
395 | prog.push_str(&crate_attrs); | |
0731742a | 396 | prog.push_str(&crates); |
abe05a73 | 397 | |
74b04a01 | 398 | // Uses librustc_ast to parse the doctest and find if there's a main fn and the extern |
a1dfa0c6 | 399 | // crate already is included. |
dfeec247 | 400 | let result = rustc_driver::catch_fatal_errors(|| { |
f035d41b | 401 | rustc_ast::with_session_globals(edition, || { |
dfeec247 XL |
402 | use rustc_errors::emitter::EmitterWriter; |
403 | use rustc_errors::Handler; | |
404 | use rustc_parse::maybe_new_parser_from_source_str; | |
74b04a01 | 405 | use rustc_session::parse::ParseSess; |
dfeec247 | 406 | use rustc_span::source_map::FilePathMapping; |
dfeec247 XL |
407 | |
408 | let filename = FileName::anon_source_code(s); | |
74b04a01 | 409 | let source = crates + everything_else; |
dfeec247 XL |
410 | |
411 | // Any errors in parsing should also appear when the doctest is compiled for real, so just | |
74b04a01 XL |
412 | // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr. |
413 | let sm = Lrc::new(SourceMap::new(FilePathMapping::empty())); | |
dfeec247 XL |
414 | let emitter = |
415 | EmitterWriter::new(box io::sink(), None, false, false, false, None, false); | |
416 | // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser | |
417 | let handler = Handler::with_emitter(false, None, box emitter); | |
74b04a01 | 418 | let sess = ParseSess::with_span_handler(handler, sm); |
dfeec247 XL |
419 | |
420 | let mut found_main = false; | |
421 | let mut found_extern_crate = cratename.is_none(); | |
422 | let mut found_macro = false; | |
423 | ||
424 | let mut parser = match maybe_new_parser_from_source_str(&sess, filename, source) { | |
425 | Ok(p) => p, | |
426 | Err(errs) => { | |
427 | for mut err in errs { | |
428 | err.cancel(); | |
429 | } | |
a1dfa0c6 | 430 | |
dfeec247 XL |
431 | return (found_main, found_extern_crate, found_macro); |
432 | } | |
433 | }; | |
a1dfa0c6 | 434 | |
dfeec247 XL |
435 | loop { |
436 | match parser.parse_item() { | |
437 | Ok(Some(item)) => { | |
438 | if !found_main { | |
439 | if let ast::ItemKind::Fn(..) = item.kind { | |
440 | if item.ident.name == sym::main { | |
441 | found_main = true; | |
442 | } | |
a1dfa0c6 XL |
443 | } |
444 | } | |
a1dfa0c6 | 445 | |
dfeec247 XL |
446 | if !found_extern_crate { |
447 | if let ast::ItemKind::ExternCrate(original) = item.kind { | |
448 | // This code will never be reached if `cratename` is none because | |
449 | // `found_extern_crate` is initialized to `true` if it is none. | |
450 | let cratename = cratename.unwrap(); | |
a1dfa0c6 | 451 | |
dfeec247 XL |
452 | match original { |
453 | Some(name) => found_extern_crate = name.as_str() == cratename, | |
454 | None => found_extern_crate = item.ident.as_str() == cratename, | |
455 | } | |
a1dfa0c6 XL |
456 | } |
457 | } | |
a1dfa0c6 | 458 | |
dfeec247 | 459 | if !found_macro { |
ba9703b0 | 460 | if let ast::ItemKind::MacCall(..) = item.kind { |
dfeec247 XL |
461 | found_macro = true; |
462 | } | |
69743fb6 | 463 | } |
69743fb6 | 464 | |
dfeec247 XL |
465 | if found_main && found_extern_crate { |
466 | break; | |
467 | } | |
468 | } | |
469 | Ok(None) => break, | |
470 | Err(mut e) => { | |
471 | e.cancel(); | |
a1dfa0c6 XL |
472 | break; |
473 | } | |
474 | } | |
a1dfa0c6 | 475 | } |
a1dfa0c6 | 476 | |
dfeec247 XL |
477 | (found_main, found_extern_crate, found_macro) |
478 | }) | |
a1dfa0c6 | 479 | }); |
dfeec247 XL |
480 | let (already_has_main, already_has_extern_crate, found_macro) = match result { |
481 | Ok(result) => result, | |
482 | Err(ErrorReported) => { | |
483 | // If the parser panicked due to a fatal error, pass the test code through unchanged. | |
484 | // The error will be reported during compilation. | |
485 | return (s.to_owned(), 0); | |
486 | } | |
487 | }; | |
a1dfa0c6 | 488 | |
69743fb6 XL |
489 | // If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't |
490 | // see it. In that case, run the old text-based scan to see if they at least have a main | |
491 | // function written inside a macro invocation. See | |
492 | // https://github.com/rust-lang/rust/issues/56898 | |
493 | let already_has_main = if found_macro && !already_has_main { | |
494 | s.lines() | |
495 | .map(|line| { | |
496 | let comment = line.find("//"); | |
dfeec247 | 497 | if let Some(comment_begins) = comment { &line[0..comment_begins] } else { line } |
69743fb6 XL |
498 | }) |
499 | .any(|code| code.contains("fn main")) | |
500 | } else { | |
501 | already_has_main | |
502 | }; | |
503 | ||
1a4d82fc JJ |
504 | // Don't inject `extern crate std` because it's already injected by the |
505 | // compiler. | |
a1dfa0c6 | 506 | if !already_has_extern_crate && !opts.no_crate_inject && cratename != Some("std") { |
54a0048b | 507 | if let Some(cratename) = cratename { |
a1dfa0c6 | 508 | // Make sure its actually used if not included. |
54a0048b SL |
509 | if s.contains(cratename) { |
510 | prog.push_str(&format!("extern crate {};\n", cratename)); | |
2c00a5a8 | 511 | line_offset += 1; |
1a4d82fc | 512 | } |
1a4d82fc JJ |
513 | } |
514 | } | |
ea8adc8c | 515 | |
9fa01778 XL |
516 | // FIXME: This code cannot yet handle no_std test cases yet |
517 | if dont_insert_main || already_has_main || prog.contains("![no_std]") { | |
0531ce1d | 518 | prog.push_str(everything_else); |
1a4d82fc | 519 | } else { |
9fa01778 XL |
520 | let returns_result = everything_else.trim_end().ends_with("(())"); |
521 | let (main_pre, main_post) = if returns_result { | |
dfeec247 XL |
522 | ( |
523 | "fn main() { fn _inner() -> Result<(), impl core::fmt::Debug> {", | |
524 | "}\n_inner().unwrap() }", | |
525 | ) | |
9fa01778 XL |
526 | } else { |
527 | ("fn main() {\n", "\n}") | |
528 | }; | |
529 | prog.extend([main_pre, everything_else, main_post].iter().cloned()); | |
2c00a5a8 | 530 | line_offset += 1; |
1a4d82fc JJ |
531 | } |
532 | ||
0731742a | 533 | debug!("final doctest:\n{}", prog); |
c34b1796 | 534 | |
2c00a5a8 | 535 | (prog, line_offset) |
1a4d82fc JJ |
536 | } |
537 | ||
8bb4bdeb | 538 | // FIXME(aburka): use a real parser to deal with multiline attributes |
a1dfa0c6 | 539 | fn partition_source(s: &str) -> (String, String, String) { |
0731742a XL |
540 | #[derive(Copy, Clone, PartialEq)] |
541 | enum PartitionState { | |
542 | Attrs, | |
543 | Crates, | |
544 | Other, | |
545 | } | |
546 | let mut state = PartitionState::Attrs; | |
c34b1796 | 547 | let mut before = String::new(); |
a1dfa0c6 | 548 | let mut crates = String::new(); |
c34b1796 AL |
549 | let mut after = String::new(); |
550 | ||
551 | for line in s.lines() { | |
552 | let trimline = line.trim(); | |
0731742a XL |
553 | |
554 | // FIXME(misdreavus): if a doc comment is placed on an extern crate statement, it will be | |
555 | // shunted into "everything else" | |
556 | match state { | |
557 | PartitionState::Attrs => { | |
dfeec247 XL |
558 | state = if trimline.starts_with("#![") |
559 | || trimline.chars().all(|c| c.is_whitespace()) | |
560 | || (trimline.starts_with("//") && !trimline.starts_with("///")) | |
0731742a XL |
561 | { |
562 | PartitionState::Attrs | |
dfeec247 XL |
563 | } else if trimline.starts_with("extern crate") |
564 | || trimline.starts_with("#[macro_use] extern crate") | |
0731742a XL |
565 | { |
566 | PartitionState::Crates | |
567 | } else { | |
568 | PartitionState::Other | |
569 | }; | |
570 | } | |
571 | PartitionState::Crates => { | |
dfeec247 XL |
572 | state = if trimline.starts_with("extern crate") |
573 | || trimline.starts_with("#[macro_use] extern crate") | |
574 | || trimline.chars().all(|c| c.is_whitespace()) | |
575 | || (trimline.starts_with("//") && !trimline.starts_with("///")) | |
0731742a XL |
576 | { |
577 | PartitionState::Crates | |
578 | } else { | |
579 | PartitionState::Other | |
580 | }; | |
581 | } | |
582 | PartitionState::Other => {} | |
583 | } | |
584 | ||
585 | match state { | |
586 | PartitionState::Attrs => { | |
587 | before.push_str(line); | |
588 | before.push_str("\n"); | |
589 | } | |
590 | PartitionState::Crates => { | |
a1dfa0c6 XL |
591 | crates.push_str(line); |
592 | crates.push_str("\n"); | |
593 | } | |
0731742a XL |
594 | PartitionState::Other => { |
595 | after.push_str(line); | |
596 | after.push_str("\n"); | |
597 | } | |
c34b1796 AL |
598 | } |
599 | } | |
600 | ||
0731742a XL |
601 | debug!("before:\n{}", before); |
602 | debug!("crates:\n{}", crates); | |
603 | debug!("after:\n{}", after); | |
604 | ||
a1dfa0c6 | 605 | (before, after, crates) |
c34b1796 AL |
606 | } |
607 | ||
0bf4aa26 XL |
608 | pub trait Tester { |
609 | fn add_test(&mut self, test: String, config: LangString, line: usize); | |
610 | fn get_line(&self) -> usize { | |
611 | 0 | |
612 | } | |
613 | fn register_header(&mut self, _name: &str, _level: u32) {} | |
614 | } | |
615 | ||
1a4d82fc JJ |
616 | pub struct Collector { |
617 | pub tests: Vec<testing::TestDescAndFn>, | |
abe05a73 XL |
618 | |
619 | // The name of the test displayed to the user, separated by `::`. | |
620 | // | |
621 | // In tests from Rust source, this is the path to the item | |
0731742a | 622 | // e.g., `["std", "vec", "Vec", "push"]`. |
abe05a73 XL |
623 | // |
624 | // In tests from a markdown file, this is the titles of all headers (h1~h6) | |
0731742a | 625 | // of the sections that contain the code block, e.g., if the markdown file is |
abe05a73 XL |
626 | // written as: |
627 | // | |
628 | // ``````markdown | |
629 | // # Title | |
630 | // | |
631 | // ## Subtitle | |
632 | // | |
633 | // ```rust | |
634 | // assert!(true); | |
635 | // ``` | |
636 | // `````` | |
637 | // | |
638 | // the `names` vector of that test will be `["Title", "Subtitle"]`. | |
1a4d82fc | 639 | names: Vec<String>, |
abe05a73 | 640 | |
e1599b0c | 641 | options: Options, |
1a4d82fc | 642 | use_headers: bool, |
e1599b0c | 643 | enable_per_target_ignores: bool, |
1a4d82fc | 644 | cratename: String, |
9346a6ac | 645 | opts: TestOptions, |
8bb4bdeb | 646 | position: Span, |
b7449926 | 647 | source_map: Option<Lrc<SourceMap>>, |
ff7c6d11 | 648 | filename: Option<PathBuf>, |
ba9703b0 | 649 | visited_tests: HashMap<(String, usize), usize>, |
1a4d82fc JJ |
650 | } |
651 | ||
652 | impl Collector { | |
dfeec247 XL |
653 | pub fn new( |
654 | cratename: String, | |
655 | options: Options, | |
656 | use_headers: bool, | |
657 | opts: TestOptions, | |
658 | source_map: Option<Lrc<SourceMap>>, | |
659 | filename: Option<PathBuf>, | |
660 | enable_per_target_ignores: bool, | |
661 | ) -> Collector { | |
1a4d82fc JJ |
662 | Collector { |
663 | tests: Vec::new(), | |
664 | names: Vec::new(), | |
e1599b0c | 665 | options, |
3b2f2976 | 666 | use_headers, |
e1599b0c | 667 | enable_per_target_ignores, |
3b2f2976 XL |
668 | cratename, |
669 | opts, | |
8bb4bdeb | 670 | position: DUMMY_SP, |
b7449926 | 671 | source_map, |
3b2f2976 | 672 | filename, |
ba9703b0 | 673 | visited_tests: HashMap::new(), |
1a4d82fc JJ |
674 | } |
675 | } | |
676 | ||
ff7c6d11 | 677 | fn generate_name(&self, line: usize, filename: &FileName) -> String { |
f035d41b XL |
678 | let mut item_path = self.names.join("::"); |
679 | if !item_path.is_empty() { | |
680 | item_path.push(' '); | |
681 | } | |
682 | format!("{} - {}(line {})", filename, item_path, line) | |
cc61c64b XL |
683 | } |
684 | ||
0bf4aa26 XL |
685 | pub fn set_position(&mut self, position: Span) { |
686 | self.position = position; | |
687 | } | |
688 | ||
689 | fn get_filename(&self) -> FileName { | |
690 | if let Some(ref source_map) = self.source_map { | |
691 | let filename = source_map.span_to_filename(self.position); | |
692 | if let FileName::Real(ref filename) = filename { | |
693 | if let Ok(cur_dir) = env::current_dir() { | |
ba9703b0 | 694 | if let Ok(path) = filename.local_path().strip_prefix(&cur_dir) { |
0bf4aa26 XL |
695 | return path.to_owned().into(); |
696 | } | |
697 | } | |
698 | } | |
699 | filename | |
700 | } else if let Some(ref filename) = self.filename { | |
701 | filename.clone().into() | |
702 | } else { | |
703 | FileName::Custom("input".to_owned()) | |
704 | } | |
705 | } | |
706 | } | |
707 | ||
708 | impl Tester for Collector { | |
709 | fn add_test(&mut self, test: String, config: LangString, line: usize) { | |
b7449926 | 710 | let filename = self.get_filename(); |
cc61c64b | 711 | let name = self.generate_name(line, &filename); |
1a4d82fc | 712 | let cratename = self.cratename.to_string(); |
9346a6ac | 713 | let opts = self.opts.clone(); |
ba9703b0 | 714 | let edition = config.edition.unwrap_or(self.options.edition); |
e1599b0c XL |
715 | let options = self.options.clone(); |
716 | let runtool = self.options.runtool.clone(); | |
717 | let runtool_args = self.options.runtool_args.clone(); | |
718 | let target = self.options.target.clone(); | |
719 | let target_str = target.to_string(); | |
9fa01778 | 720 | |
ba9703b0 XL |
721 | // FIXME(#44940): if doctests ever support path remapping, then this filename |
722 | // needs to be the result of `SourceMap::span_to_unmapped_path`. | |
723 | let path = match &filename { | |
724 | FileName::Real(path) => path.local_path().to_path_buf(), | |
725 | _ => PathBuf::from(r"doctest.rs"), | |
726 | }; | |
727 | ||
728 | let outdir = if let Some(mut path) = options.persist_doctests.clone() { | |
729 | // For example `module/file.rs` would become `module_file_rs` | |
730 | let folder_name = filename | |
731 | .to_string() | |
732 | .chars() | |
733 | .map(|c| if c == '/' || c == '.' { '_' } else { c }) | |
734 | .collect::<String>(); | |
735 | ||
736 | path.push(format!( | |
737 | "{name}_{line}_{number}", | |
738 | name = folder_name, | |
739 | number = { | |
740 | // Increases the current test number, if this file already | |
741 | // exists or it creates a new entry with a test number of 0. | |
742 | self.visited_tests | |
743 | .entry((folder_name.clone(), line)) | |
744 | .and_modify(|v| *v += 1) | |
745 | .or_insert(0) | |
746 | }, | |
747 | line = line, | |
748 | )); | |
749 | ||
750 | std::fs::create_dir_all(&path) | |
751 | .expect("Couldn't create directory for doctest executables"); | |
752 | ||
753 | DirState::Perm(path) | |
754 | } else { | |
755 | DirState::Temp( | |
756 | TempFileBuilder::new() | |
757 | .prefix("rustdoctest") | |
758 | .tempdir() | |
759 | .expect("rustdoc needs a tempdir"), | |
760 | ) | |
761 | }; | |
762 | ||
416331ca | 763 | debug!("creating test {}: {}", name, test); |
1a4d82fc JJ |
764 | self.tests.push(testing::TestDescAndFn { |
765 | desc: testing::TestDesc { | |
dfeec247 | 766 | name: testing::DynTestName(name), |
e1599b0c XL |
767 | ignore: match config.ignore { |
768 | Ignore::All => true, | |
769 | Ignore::None => false, | |
dfeec247 | 770 | Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), |
e1599b0c | 771 | }, |
9346a6ac AL |
772 | // compiler failures are test failures |
773 | should_panic: testing::ShouldPanic::No, | |
b7449926 | 774 | allow_fail: config.allow_fail, |
e74abb32 | 775 | test_type: testing::TestType::DocTest, |
1a4d82fc | 776 | }, |
ff7c6d11 | 777 | testfn: testing::DynTestFn(box move || { |
dc9dc135 | 778 | let res = run_test( |
532ac7d7 XL |
779 | &test, |
780 | &cratename, | |
532ac7d7 | 781 | line, |
e1599b0c | 782 | options, |
532ac7d7 XL |
783 | config.should_panic, |
784 | config.no_run, | |
785 | config.test_harness, | |
e1599b0c XL |
786 | runtool, |
787 | runtool_args, | |
788 | target, | |
532ac7d7 XL |
789 | config.compile_fail, |
790 | config.error_codes, | |
791 | &opts, | |
532ac7d7 | 792 | edition, |
ba9703b0 XL |
793 | outdir, |
794 | path, | |
dc9dc135 XL |
795 | ); |
796 | ||
797 | if let Err(err) = res { | |
798 | match err { | |
799 | TestFailure::CompileError => { | |
800 | eprint!("Couldn't compile the test."); | |
801 | } | |
802 | TestFailure::UnexpectedCompilePass => { | |
803 | eprint!("Test compiled successfully, but it's marked `compile_fail`."); | |
804 | } | |
805 | TestFailure::UnexpectedRunPass => { | |
806 | eprint!("Test executable succeeded, but it's marked `should_panic`."); | |
807 | } | |
808 | TestFailure::MissingErrorCodes(codes) => { | |
809 | eprint!("Some expected error codes were not found: {:?}", codes); | |
810 | } | |
811 | TestFailure::ExecutionError(err) => { | |
812 | eprint!("Couldn't run the test: {}", err); | |
813 | if err.kind() == io::ErrorKind::PermissionDenied { | |
814 | eprint!(" - maybe your tempdir is mounted with noexec?"); | |
815 | } | |
816 | } | |
817 | TestFailure::ExecutionFailure(out) => { | |
818 | let reason = if let Some(code) = out.status.code() { | |
819 | format!("exit code {}", code) | |
820 | } else { | |
821 | String::from("terminated by signal") | |
822 | }; | |
823 | ||
824 | eprintln!("Test executable failed ({}).", reason); | |
825 | ||
826 | // FIXME(#12309): An unfortunate side-effect of capturing the test | |
827 | // executable's output is that the relative ordering between the test's | |
828 | // stdout and stderr is lost. However, this is better than the | |
829 | // alternative: if the test executable inherited the parent's I/O | |
830 | // handles the output wouldn't be captured at all, even on success. | |
831 | // | |
832 | // The ordering could be preserved if the test process' stderr was | |
833 | // redirected to stdout, but that functionality does not exist in the | |
834 | // standard library, so it may not be portable enough. | |
835 | let stdout = str::from_utf8(&out.stdout).unwrap_or_default(); | |
836 | let stderr = str::from_utf8(&out.stderr).unwrap_or_default(); | |
837 | ||
838 | if !stdout.is_empty() || !stderr.is_empty() { | |
839 | eprintln!(); | |
840 | ||
841 | if !stdout.is_empty() { | |
842 | eprintln!("stdout:\n{}", stdout); | |
843 | } | |
844 | ||
845 | if !stderr.is_empty() { | |
846 | eprintln!("stderr:\n{}", stderr); | |
847 | } | |
848 | } | |
849 | } | |
850 | } | |
851 | ||
852 | panic::resume_unwind(box ()); | |
853 | } | |
32a655c1 | 854 | }), |
1a4d82fc JJ |
855 | }); |
856 | } | |
857 | ||
0bf4aa26 | 858 | fn get_line(&self) -> usize { |
b7449926 | 859 | if let Some(ref source_map) = self.source_map { |
ea8adc8c | 860 | let line = self.position.lo().to_usize(); |
b7449926 | 861 | let line = source_map.lookup_char_pos(BytePos(line as u32)).line; |
8bb4bdeb XL |
862 | if line > 0 { line - 1 } else { line } |
863 | } else { | |
864 | 0 | |
865 | } | |
866 | } | |
867 | ||
0bf4aa26 | 868 | fn register_header(&mut self, name: &str, level: u32) { |
abe05a73 | 869 | if self.use_headers { |
0731742a | 870 | // We use these headings as test names, so it's good if |
1a4d82fc | 871 | // they're valid identifiers. |
dfeec247 XL |
872 | let name = name |
873 | .chars() | |
874 | .enumerate() | |
875 | .map(|(i, c)| { | |
876 | if (i == 0 && rustc_lexer::is_id_start(c)) | |
877 | || (i != 0 && rustc_lexer::is_id_continue(c)) | |
878 | { | |
1a4d82fc JJ |
879 | c |
880 | } else { | |
881 | '_' | |
882 | } | |
dfeec247 XL |
883 | }) |
884 | .collect::<String>(); | |
1a4d82fc | 885 | |
abe05a73 XL |
886 | // Here we try to efficiently assemble the header titles into the |
887 | // test name in the form of `h1::h2::h3::h4::h5::h6`. | |
888 | // | |
0731742a | 889 | // Suppose that originally `self.names` contains `[h1, h2, h3]`... |
abe05a73 XL |
890 | let level = level as usize; |
891 | if level <= self.names.len() { | |
892 | // ... Consider `level == 2`. All headers in the lower levels | |
893 | // are irrelevant in this new level. So we should reset | |
894 | // `self.names` to contain headers until <h2>, and replace that | |
895 | // slot with the new name: `[h1, name]`. | |
896 | self.names.truncate(level); | |
897 | self.names[level - 1] = name; | |
898 | } else { | |
899 | // ... On the other hand, consider `level == 5`. This means we | |
900 | // need to extend `self.names` to contain five headers. We fill | |
901 | // in the missing level (<h4>) with `_`. Thus `self.names` will | |
902 | // become `[h1, h2, h3, "_", name]`. | |
903 | if level - 1 > self.names.len() { | |
904 | self.names.resize(level - 1, "_".to_owned()); | |
905 | } | |
906 | self.names.push(name); | |
907 | } | |
1a4d82fc JJ |
908 | } |
909 | } | |
910 | } | |
911 | ||
f9f354fc | 912 | struct HirCollector<'a, 'hir, 'tcx> { |
ba9703b0 | 913 | sess: &'a Session, |
476ff2be | 914 | collector: &'a mut Collector, |
ba9703b0 | 915 | map: Map<'hir>, |
b7449926 | 916 | codes: ErrorCodes, |
f9f354fc | 917 | tcx: TyCtxt<'tcx>, |
476ff2be | 918 | } |
92a42be0 | 919 | |
f9f354fc | 920 | impl<'a, 'hir, 'tcx> HirCollector<'a, 'hir, 'tcx> { |
dfeec247 XL |
921 | fn visit_testable<F: FnOnce(&mut Self)>( |
922 | &mut self, | |
923 | name: String, | |
924 | attrs: &[ast::Attribute], | |
f9f354fc XL |
925 | hir_id: HirId, |
926 | sp: Span, | |
dfeec247 XL |
927 | nested: F, |
928 | ) { | |
3b2f2976 XL |
929 | let mut attrs = Attributes::from_ast(self.sess.diagnostic(), attrs); |
930 | if let Some(ref cfg) = attrs.cfg { | |
0531ce1d | 931 | if !cfg.matches(&self.sess.parse_sess, Some(&self.sess.features_untracked())) { |
3b2f2976 XL |
932 | return; |
933 | } | |
934 | } | |
935 | ||
476ff2be SL |
936 | let has_name = !name.is_empty(); |
937 | if has_name { | |
938 | self.collector.names.push(name); | |
939 | } | |
92a42be0 | 940 | |
476ff2be SL |
941 | attrs.collapse_doc_comments(); |
942 | attrs.unindent_doc_comments(); | |
0731742a XL |
943 | // The collapse-docs pass won't combine sugared/raw doc attributes, or included files with |
944 | // anything else, this will combine them for us. | |
ff7c6d11 | 945 | if let Some(doc) = attrs.collapsed_doc_value() { |
b7449926 | 946 | self.collector.set_position(attrs.span.unwrap_or(DUMMY_SP)); |
dfeec247 XL |
947 | markdown::find_testable_code( |
948 | &doc, | |
949 | self.collector, | |
950 | self.codes, | |
951 | self.collector.enable_per_target_ignores, | |
f9f354fc XL |
952 | Some(&crate::html::markdown::ExtraInfo::new( |
953 | &self.tcx, | |
954 | hir_id, | |
955 | span_of_attrs(&attrs).unwrap_or(sp), | |
956 | )), | |
dfeec247 | 957 | ); |
1a4d82fc | 958 | } |
92a42be0 | 959 | |
476ff2be SL |
960 | nested(self); |
961 | ||
962 | if has_name { | |
963 | self.collector.names.pop(); | |
1a4d82fc | 964 | } |
476ff2be SL |
965 | } |
966 | } | |
92a42be0 | 967 | |
f9f354fc | 968 | impl<'a, 'hir, 'tcx> intravisit::Visitor<'hir> for HirCollector<'a, 'hir, 'tcx> { |
dfeec247 XL |
969 | type Map = Map<'hir>; |
970 | ||
ba9703b0 XL |
971 | fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap<Self::Map> { |
972 | intravisit::NestedVisitorMap::All(self.map) | |
476ff2be | 973 | } |
92a42be0 | 974 | |
f035d41b | 975 | fn visit_item(&mut self, item: &'hir hir::Item<'_>) { |
dfeec247 | 976 | let name = if let hir::ItemKind::Impl { ref self_ty, .. } = item.kind { |
ba9703b0 | 977 | rustc_hir_pretty::id_to_string(&self.map, self_ty.hir_id) |
476ff2be | 978 | } else { |
0731742a | 979 | item.ident.to_string() |
476ff2be | 980 | }; |
92a42be0 | 981 | |
f9f354fc | 982 | self.visit_testable(name, &item.attrs, item.hir_id, item.span, |this| { |
476ff2be SL |
983 | intravisit::walk_item(this, item); |
984 | }); | |
985 | } | |
92a42be0 | 986 | |
f035d41b | 987 | fn visit_trait_item(&mut self, item: &'hir hir::TraitItem<'_>) { |
f9f354fc | 988 | self.visit_testable(item.ident.to_string(), &item.attrs, item.hir_id, item.span, |this| { |
476ff2be SL |
989 | intravisit::walk_trait_item(this, item); |
990 | }); | |
991 | } | |
92a42be0 | 992 | |
f035d41b | 993 | fn visit_impl_item(&mut self, item: &'hir hir::ImplItem<'_>) { |
f9f354fc | 994 | self.visit_testable(item.ident.to_string(), &item.attrs, item.hir_id, item.span, |this| { |
476ff2be SL |
995 | intravisit::walk_impl_item(this, item); |
996 | }); | |
997 | } | |
998 | ||
f035d41b | 999 | fn visit_foreign_item(&mut self, item: &'hir hir::ForeignItem<'_>) { |
f9f354fc | 1000 | self.visit_testable(item.ident.to_string(), &item.attrs, item.hir_id, item.span, |this| { |
476ff2be SL |
1001 | intravisit::walk_foreign_item(this, item); |
1002 | }); | |
1003 | } | |
1004 | ||
dfeec247 XL |
1005 | fn visit_variant( |
1006 | &mut self, | |
f035d41b XL |
1007 | v: &'hir hir::Variant<'_>, |
1008 | g: &'hir hir::Generics<'_>, | |
dfeec247 XL |
1009 | item_id: hir::HirId, |
1010 | ) { | |
f9f354fc | 1011 | self.visit_testable(v.ident.to_string(), &v.attrs, v.id, v.span, |this| { |
476ff2be SL |
1012 | intravisit::walk_variant(this, v, g, item_id); |
1013 | }); | |
1014 | } | |
1015 | ||
f035d41b | 1016 | fn visit_struct_field(&mut self, f: &'hir hir::StructField<'_>) { |
f9f354fc | 1017 | self.visit_testable(f.ident.to_string(), &f.attrs, f.hir_id, f.span, |this| { |
476ff2be SL |
1018 | intravisit::walk_struct_field(this, f); |
1019 | }); | |
1020 | } | |
1021 | ||
f035d41b | 1022 | fn visit_macro_def(&mut self, macro_def: &'hir hir::MacroDef<'_>) { |
f9f354fc XL |
1023 | self.visit_testable( |
1024 | macro_def.ident.to_string(), | |
1025 | ¯o_def.attrs, | |
1026 | macro_def.hir_id, | |
1027 | macro_def.span, | |
1028 | |_| (), | |
1029 | ); | |
1a4d82fc JJ |
1030 | } |
1031 | } | |
0531ce1d XL |
1032 | |
1033 | #[cfg(test)] | |
dc9dc135 | 1034 | mod tests; |