Skip to content

Commit 2e791cf

Browse files
Add new failed_merged_doctest_compilation rustdoc lint
1 parent c40c51f commit 2e791cf

17 files changed

Lines changed: 187 additions & 148 deletions

src/librustdoc/doctest.rs

Lines changed: 94 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,15 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
211211
Err(error) => return crate::wrap_return(dcx, Err(error)),
212212
};
213213
let args_path = temp_dir.path().join("rustdoc-cfgs");
214+
let temp_dir_path = temp_dir.path().to_path_buf();
214215
crate::wrap_return(dcx, generate_args_file(&args_path, &options));
215216

216217
let extract_doctests = options.output_format == OutputFormat::Doctest;
217218
let save_temps = options.codegen_options.save_temps;
218219
let result = interface::run_compiler(config, |compiler| {
219220
let krate = rustc_interface::passes::parse(&compiler.sess);
220221

221-
let collector = rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| {
222+
rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| {
222223
let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
223224
let opts = scrape_test_config(tcx, crate_name, args_path);
224225

@@ -227,6 +228,8 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
227228
tcx,
228229
);
229230
let tests = hir_collector.collect_crate();
231+
tcx.dcx().abort_if_errors();
232+
230233
if extract_doctests {
231234
let mut collector = extracted::ExtractedDocTests::new();
232235
tests.into_iter().for_each(|t| collector.add_test(t, &opts, &options));
@@ -235,93 +238,90 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
235238
let mut stdout = stdout.lock();
236239
if let Err(error) = serde_json::ser::to_writer(&mut stdout, &collector) {
237240
eprintln!();
238-
Err(format!("Failed to generate JSON output for doctests: {error:?}"))
241+
return Err(format!("Failed to generate JSON output for doctests: {error:?}"));
239242
} else {
240-
Ok(None)
243+
return Ok(());
241244
}
242-
} else {
243-
let mut collector = CreateRunnableDocTests::new(options, opts);
244-
tests.into_iter().for_each(|t| collector.add_test(t, Some(compiler.sess.dcx())));
245-
246-
Ok(Some(collector))
247245
}
248-
});
249-
compiler.sess.dcx().abort_if_errors();
246+
let mut collector = CreateRunnableDocTests::new(options, opts);
247+
tests.into_iter().for_each(|t| collector.add_test(t, Some(compiler.sess.dcx())));
250248

251-
collector
252-
});
249+
let CreateRunnableDocTests {
250+
standalone_tests,
251+
mergeable_tests,
252+
rustdoc_options,
253+
opts,
254+
unused_extern_reports,
255+
compiling_test_count,
256+
..
257+
} = collector;
253258

254-
let CreateRunnableDocTests {
255-
standalone_tests,
256-
mergeable_tests,
257-
rustdoc_options,
258-
opts,
259-
unused_extern_reports,
260-
compiling_test_count,
261-
..
262-
} = match result {
263-
Ok(Some(collector)) => collector,
264-
Ok(None) => return,
265-
Err(error) => {
266-
eprintln!("{error}");
267-
// Since some files in the temporary folder are still owned and alive, we need
268-
// to manually remove the folder.
269-
if !save_temps {
270-
let _ = std::fs::remove_dir_all(temp_dir.path());
259+
run_tests(
260+
compiler.sess.dcx(),
261+
opts,
262+
&rustdoc_options,
263+
&unused_extern_reports,
264+
standalone_tests,
265+
mergeable_tests,
266+
Some(temp_dir),
267+
Some(tcx),
268+
);
269+
270+
let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
271+
272+
// Collect and warn about unused externs, but only if we've gotten
273+
// reports for each doctest
274+
if json_unused_externs.is_enabled() {
275+
let unused_extern_reports: Vec<_> =
276+
std::mem::take(&mut unused_extern_reports.lock().unwrap());
277+
if unused_extern_reports.len() == compiling_test_count {
278+
let extern_names =
279+
externs.iter().map(|(name, _)| name).collect::<FxIndexSet<&String>>();
280+
let mut unused_extern_names = unused_extern_reports
281+
.iter()
282+
.map(|uexts| {
283+
uexts.unused_extern_names.iter().collect::<FxIndexSet<&String>>()
284+
})
285+
.fold(extern_names, |uextsa, uextsb| {
286+
uextsa.intersection(&uextsb).copied().collect::<FxIndexSet<&String>>()
287+
})
288+
.iter()
289+
.map(|v| (*v).clone())
290+
.collect::<Vec<String>>();
291+
unused_extern_names.sort();
292+
// Take the most severe lint level
293+
let lint_level = unused_extern_reports
294+
.iter()
295+
.map(|uexts| uexts.lint_level.as_str())
296+
.max_by_key(|v| match *v {
297+
"warn" => 1,
298+
"deny" => 2,
299+
"forbid" => 3,
300+
// The allow lint level is not expected,
301+
// as if allow is specified, no message
302+
// is to be emitted.
303+
v => unreachable!("Invalid lint level '{v}'"),
304+
})
305+
.unwrap_or("warn")
306+
.to_string();
307+
let uext = UnusedExterns { lint_level, unused_extern_names };
308+
let unused_extern_json = serde_json::to_string(&uext).unwrap();
309+
eprintln!("{unused_extern_json}");
310+
}
271311
}
272-
std::process::exit(1);
273-
}
274-
};
275312

276-
run_tests(
277-
dcx,
278-
opts,
279-
&rustdoc_options,
280-
&unused_extern_reports,
281-
standalone_tests,
282-
mergeable_tests,
283-
Some(temp_dir),
284-
);
313+
Ok(())
314+
})
315+
});
285316

286-
let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
287-
288-
// Collect and warn about unused externs, but only if we've gotten
289-
// reports for each doctest
290-
if json_unused_externs.is_enabled() {
291-
let unused_extern_reports: Vec<_> =
292-
std::mem::take(&mut unused_extern_reports.lock().unwrap());
293-
if unused_extern_reports.len() == compiling_test_count {
294-
let extern_names =
295-
externs.iter().map(|(name, _)| name).collect::<FxIndexSet<&String>>();
296-
let mut unused_extern_names = unused_extern_reports
297-
.iter()
298-
.map(|uexts| uexts.unused_extern_names.iter().collect::<FxIndexSet<&String>>())
299-
.fold(extern_names, |uextsa, uextsb| {
300-
uextsa.intersection(&uextsb).copied().collect::<FxIndexSet<&String>>()
301-
})
302-
.iter()
303-
.map(|v| (*v).clone())
304-
.collect::<Vec<String>>();
305-
unused_extern_names.sort();
306-
// Take the most severe lint level
307-
let lint_level = unused_extern_reports
308-
.iter()
309-
.map(|uexts| uexts.lint_level.as_str())
310-
.max_by_key(|v| match *v {
311-
"warn" => 1,
312-
"deny" => 2,
313-
"forbid" => 3,
314-
// The allow lint level is not expected,
315-
// as if allow is specified, no message
316-
// is to be emitted.
317-
v => unreachable!("Invalid lint level '{v}'"),
318-
})
319-
.unwrap_or("warn")
320-
.to_string();
321-
let uext = UnusedExterns { lint_level, unused_extern_names };
322-
let unused_extern_json = serde_json::to_string(&uext).unwrap();
323-
eprintln!("{unused_extern_json}");
317+
if let Err(error) = result {
318+
eprintln!("{error}");
319+
// Since some files in the temporary folder are still owned and alive, we need
320+
// to manually remove the folder.
321+
if !save_temps {
322+
let _ = std::fs::remove_dir_all(temp_dir_path);
324323
}
324+
std::process::exit(1);
325325
}
326326
}
327327

@@ -334,6 +334,7 @@ pub(crate) fn run_tests(
334334
mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>,
335335
// We pass this argument so we can drop it manually before using `exit`.
336336
mut temp_dir: Option<TempDir>,
337+
tcx: Option<TyCtxt<'_>>,
337338
) {
338339
let mut test_args = Vec::with_capacity(rustdoc_options.test_args.len() + 1);
339340
test_args.insert(0, "rustdoctest".to_string());
@@ -386,6 +387,20 @@ pub(crate) fn run_tests(
386387
diag.emit();
387388
}
388389

390+
if let Some(tcx) = tcx {
391+
tcx.node_span_lint(
392+
crate::lint::FAILED_MERGED_DOCTEST_COMPILATION,
393+
CRATE_HIR_ID,
394+
tcx.hir_span(CRATE_HIR_ID),
395+
|lint| {
396+
lint.primary_message(format!(
397+
"failed to compile merged doctests for edition {edition}. \
398+
Reverting to standalone doctests."
399+
));
400+
},
401+
);
402+
}
403+
389404
// We failed to compile all compatible tests as one so we push them into the
390405
// `standalone_tests` doctests.
391406
debug!("Failed to compile compatible doctests for edition {} all at once", edition);

src/librustdoc/doctest/markdown.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ pub(crate) fn test(input: &Input, options: Options, dcx: DiagCtxtHandle<'_>) ->
131131
standalone_tests,
132132
mergeable_tests,
133133
None,
134+
None,
134135
);
135136
Ok(())
136137
}

src/librustdoc/lint.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,17 @@ declare_rustdoc_lint! {
196196
"detects redundant explicit links in doc comments"
197197
}
198198

199+
declare_rustdoc_lint! {
200+
/// This lint is **warn-by-default**. It warns when merged doctests fail to compile
201+
/// when running doctests. This is a `rustdoc` only lint, see the documentation in
202+
/// the [rustdoc book].
203+
///
204+
/// [rustdoc book]: ../../../rustdoc/lints.html#failed_merged_doctest_compilation
205+
FAILED_MERGED_DOCTEST_COMPILATION,
206+
Warn,
207+
"warns when merged doctest fail to compile when running doctests"
208+
}
209+
199210
pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
200211
vec![
201212
BROKEN_INTRA_DOC_LINKS,
@@ -209,6 +220,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
209220
MISSING_CRATE_LEVEL_DOCS,
210221
UNESCAPED_BACKTICKS,
211222
REDUNDANT_EXPLICIT_LINKS,
223+
FAILED_MERGED_DOCTEST_COMPILATION,
212224
]
213225
});
214226

tests/rustdoc-ui/doctest/dead-code-2024.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME"
99
//@ failure-status: 101
1010

11+
#![allow(rustdoc::failed_merged_doctest_compilation)]
1112
#![doc(test(attr(allow(unused_variables), deny(warnings))))]
1213

1314
/// Example

tests/rustdoc-ui/doctest/dead-code-2024.stdout

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11

22
running 1 test
3-
test $DIR/dead-code-2024.rs - f (line 15) - compile ... FAILED
3+
test $DIR/dead-code-2024.rs - f (line 16) - compile ... FAILED
44

55
failures:
66

7-
---- $DIR/dead-code-2024.rs - f (line 15) stdout ----
7+
---- $DIR/dead-code-2024.rs - f (line 16) stdout ----
88
error: trait `T` is never used
9-
--> $DIR/dead-code-2024.rs:16:7
9+
--> $DIR/dead-code-2024.rs:17:7
1010
|
1111
LL | trait T { fn f(); }
1212
| ^
1313
|
1414
note: the lint level is defined here
15-
--> $DIR/dead-code-2024.rs:14:9
15+
--> $DIR/dead-code-2024.rs:15:9
1616
|
1717
LL | #![deny(warnings)]
1818
| ^^^^^^^^
@@ -23,7 +23,7 @@ error: aborting due to 1 previous error
2323
Couldn't compile the test.
2424

2525
failures:
26-
$DIR/dead-code-2024.rs - f (line 15)
26+
$DIR/dead-code-2024.rs - f (line 16)
2727

2828
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
2929

tests/rustdoc-ui/doctest/dead-code-items.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//@ failure-status: 101
1010

1111
#![doc(test(attr(deny(warnings))))]
12+
#![allow(rustdoc::failed_merged_doctest_compilation)]
1213

1314
#[doc(test(attr(allow(dead_code))))]
1415
/// Example

0 commit comments

Comments
 (0)