Skip to content

Commit 4a8d17e

Browse files
committedDec 11, 2022
Auto merge of #11450 - willcrichton:example-analyzer, r=weihanglo
Allow Check targets needed for optional doc-scraping to fail without killing the build ### What does this PR try to resolve? In doing a Crater run of -Zrustdoc-scrape-examples, I found that the only remaining regressions are cases where: * A library does not type-check * The library has examples * Cargo tries to scrape the examples, which requires checking the library * The Check unit for the library fails, crashing the build The core issue is that the Check unit should be able to fail without killing the build. This PR fixes this issue by checking for this condition, and then allowing the unit to fail. Specifically, I added a new method `BuildContext::unit_can_fail_for_docscraping` that determines the conditions for whether a unit is allowed to fail. This method is used both in `JobQueue` to interpret process failure, and in the `rustc`/`rustdoc` functions to emit a warning upon failure. I modified `rustc` to handle the case of failure similar to `rustdoc`, but with a slightly different diagnostic. ### How should we test and review this PR? The unit test `no_fail_bad_lib` has been extended with example files to test this case. r? `@weihanglo`
2 parents 9c8e8a9 + 29407d0 commit 4a8d17e

File tree

4 files changed

+153
-55
lines changed

4 files changed

+153
-55
lines changed
 

‎src/cargo/core/compiler/job_queue.rs

+1-4
Original file line numberDiff line numberDiff line change
@@ -812,10 +812,7 @@ impl<'cfg> DrainState<'cfg> {
812812
debug!("end ({:?}): {:?}", unit, result);
813813
match result {
814814
Ok(()) => self.finish(id, &unit, artifact, cx)?,
815-
Err(_)
816-
if unit.mode.is_doc_scrape()
817-
&& unit.target.doc_scrape_examples().is_unset() =>
818-
{
815+
Err(_) if cx.bcx.unit_can_fail_for_docscraping(&unit) => {
819816
cx.failed_scrape_units
820817
.lock()
821818
.unwrap()

‎src/cargo/core/compiler/mod.rs

+96-47
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub mod unit_graph;
2525
use std::collections::{HashMap, HashSet};
2626
use std::env;
2727
use std::ffi::{OsStr, OsString};
28+
use std::fmt::Display;
2829
use std::fs::{self, File};
2930
use std::io::{BufRead, Write};
3031
use std::path::{Path, PathBuf};
@@ -207,6 +208,27 @@ fn compile<'cfg>(
207208
Ok(())
208209
}
209210

211+
/// Generates the warning message used when fallible doc-scrape units fail,
212+
/// either for rustdoc or rustc.
213+
fn make_failed_scrape_diagnostic(
214+
cx: &Context<'_, '_>,
215+
unit: &Unit,
216+
top_line: impl Display,
217+
) -> String {
218+
let manifest_path = unit.pkg.manifest_path();
219+
let relative_manifest_path = manifest_path
220+
.strip_prefix(cx.bcx.ws.root())
221+
.unwrap_or(&manifest_path);
222+
223+
format!(
224+
"\
225+
{top_line}
226+
Try running with `--verbose` to see the error message.
227+
If an example or library should not be scanned, then consider adding `doc-scrape-examples = false` to its `[[example]]` or `[lib]` definition in {}",
228+
relative_manifest_path.display()
229+
)
230+
}
231+
210232
fn rustc(cx: &mut Context<'_, '_>, unit: &Unit, exec: &Arc<dyn Executor>) -> CargoResult<Work> {
211233
let mut rustc = prepare_rustc(cx, &unit.target.rustc_crate_types(), unit)?;
212234
let build_plan = cx.bcx.build_config.build_plan;
@@ -265,6 +287,26 @@ fn rustc(cx: &mut Context<'_, '_>, unit: &Unit, exec: &Arc<dyn Executor>) -> Car
265287
let is_local = unit.is_local();
266288
let artifact = unit.artifact;
267289

290+
let hide_diagnostics_for_scrape_unit = cx.bcx.unit_can_fail_for_docscraping(unit)
291+
&& !matches!(cx.bcx.config.shell().verbosity(), Verbosity::Verbose);
292+
let failed_scrape_diagnostic = hide_diagnostics_for_scrape_unit.then(|| {
293+
// If this unit is needed for doc-scraping, then we generate a diagnostic that
294+
// describes the set of reverse-dependencies that cause the unit to be needed.
295+
let target_desc = unit.target.description_named();
296+
let mut for_scrape_units = cx
297+
.bcx
298+
.scrape_units_have_dep_on(unit)
299+
.into_iter()
300+
.map(|unit| unit.target.description_named())
301+
.collect::<Vec<_>>();
302+
for_scrape_units.sort();
303+
let for_scrape_units = for_scrape_units.join(", ");
304+
make_failed_scrape_diagnostic(cx, unit, format_args!("failed to check {target_desc} in package `{name}` as a prerequisite for scraping examples from: {for_scrape_units}"))
305+
});
306+
if hide_diagnostics_for_scrape_unit {
307+
output_options.show_diagnostics = false;
308+
}
309+
268310
return Ok(Work::new(move |state| {
269311
// Artifacts are in a different location than typical units,
270312
// hence we must assure the crate- and target-dependent
@@ -339,38 +381,48 @@ fn rustc(cx: &mut Context<'_, '_>, unit: &Unit, exec: &Arc<dyn Executor>) -> Car
339381
if build_plan {
340382
state.build_plan(buildkey, rustc.clone(), outputs.clone());
341383
} else {
342-
exec.exec(
343-
&rustc,
344-
package_id,
345-
&target,
346-
mode,
347-
&mut |line| on_stdout_line(state, line, package_id, &target),
348-
&mut |line| {
349-
on_stderr_line(
350-
state,
351-
line,
352-
package_id,
353-
&manifest_path,
354-
&target,
355-
&mut output_options,
356-
)
357-
},
358-
)
359-
.map_err(verbose_if_simple_exit_code)
360-
.with_context(|| {
361-
// adapted from rustc_errors/src/lib.rs
362-
let warnings = match output_options.warnings_seen {
363-
0 => String::new(),
364-
1 => "; 1 warning emitted".to_string(),
365-
count => format!("; {} warnings emitted", count),
366-
};
367-
let errors = match output_options.errors_seen {
368-
0 => String::new(),
369-
1 => " due to previous error".to_string(),
370-
count => format!(" due to {} previous errors", count),
371-
};
372-
format!("could not compile `{}`{}{}", name, errors, warnings)
373-
})?;
384+
let result = exec
385+
.exec(
386+
&rustc,
387+
package_id,
388+
&target,
389+
mode,
390+
&mut |line| on_stdout_line(state, line, package_id, &target),
391+
&mut |line| {
392+
on_stderr_line(
393+
state,
394+
line,
395+
package_id,
396+
&manifest_path,
397+
&target,
398+
&mut output_options,
399+
)
400+
},
401+
)
402+
.map_err(verbose_if_simple_exit_code)
403+
.with_context(|| {
404+
// adapted from rustc_errors/src/lib.rs
405+
let warnings = match output_options.warnings_seen {
406+
0 => String::new(),
407+
1 => "; 1 warning emitted".to_string(),
408+
count => format!("; {} warnings emitted", count),
409+
};
410+
let errors = match output_options.errors_seen {
411+
0 => String::new(),
412+
1 => " due to previous error".to_string(),
413+
count => format!(" due to {} previous errors", count),
414+
};
415+
format!("could not compile `{}`{}{}", name, errors, warnings)
416+
});
417+
418+
if let Err(e) = result {
419+
if let Some(diagnostic) = failed_scrape_diagnostic {
420+
state.warning(diagnostic)?;
421+
}
422+
423+
return Err(e);
424+
}
425+
374426
// Exec should never return with success *and* generate an error.
375427
debug_assert_eq!(output_options.errors_seen, 0);
376428
}
@@ -713,20 +765,24 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
713765
let build_script_outputs = Arc::clone(&cx.build_script_outputs);
714766
let package_id = unit.pkg.package_id();
715767
let manifest_path = PathBuf::from(unit.pkg.manifest_path());
716-
let relative_manifest_path = manifest_path
717-
.strip_prefix(cx.bcx.ws.root())
718-
.unwrap_or(&manifest_path)
719-
.to_owned();
720768
let target = Target::clone(&unit.target);
721769
let mut output_options = OutputOptions::new(cx, unit);
722770
let script_metadata = cx.find_build_script_metadata(unit);
771+
723772
let failed_scrape_units = Arc::clone(&cx.failed_scrape_units);
724-
let hide_diagnostics_for_scrape_unit = unit.mode.is_doc_scrape()
725-
&& unit.target.doc_scrape_examples().is_unset()
773+
let hide_diagnostics_for_scrape_unit = cx.bcx.unit_can_fail_for_docscraping(unit)
726774
&& !matches!(cx.bcx.config.shell().verbosity(), Verbosity::Verbose);
775+
let failed_scrape_diagnostic = hide_diagnostics_for_scrape_unit.then(|| {
776+
make_failed_scrape_diagnostic(
777+
cx,
778+
unit,
779+
format_args!("failed to scan {target_desc} in package `{name}` for example code usage"),
780+
)
781+
});
727782
if hide_diagnostics_for_scrape_unit {
728783
output_options.show_diagnostics = false;
729784
}
785+
730786
Ok(Work::new(move |state| {
731787
add_custom_flags(
732788
&mut rustdoc,
@@ -774,15 +830,8 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
774830
.with_context(|| format!("could not document `{}`", name));
775831

776832
if let Err(e) = result {
777-
if hide_diagnostics_for_scrape_unit {
778-
let diag = format!(
779-
"\
780-
failed to scan {target_desc} in package `{name}` for example code usage
781-
Try running with `--verbose` to see the error message.
782-
If this example should not be scanned, consider adding `doc-scrape-examples = false` to the `[[example]]` definition in {}",
783-
relative_manifest_path.display()
784-
);
785-
state.warning(diag)?;
833+
if let Some(diagnostic) = failed_scrape_diagnostic {
834+
state.warning(diagnostic)?;
786835
}
787836

788837
return Err(e);

‎src/cargo/core/compiler/rustdoc.rs

+38-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::core::compiler::context::Context;
44
use crate::core::compiler::unit::Unit;
5-
use crate::core::compiler::CompileKind;
5+
use crate::core::compiler::{BuildContext, CompileKind};
66
use crate::sources::CRATES_IO_REGISTRY;
77
use crate::util::errors::{internal, CargoResult};
88
use cargo_util::ProcessBuilder;
@@ -209,3 +209,40 @@ impl RustdocScrapeExamples {
209209
matches!(self, RustdocScrapeExamples::Unset)
210210
}
211211
}
212+
213+
impl BuildContext<'_, '_> {
214+
/// Returns the set of Docscrape units that have a direct dependency on `unit`
215+
pub fn scrape_units_have_dep_on<'a>(&'a self, unit: &'a Unit) -> Vec<&'a Unit> {
216+
self.scrape_units
217+
.iter()
218+
.filter(|scrape_unit| {
219+
self.unit_graph[scrape_unit]
220+
.iter()
221+
.any(|dep| &dep.unit == unit)
222+
})
223+
.collect::<Vec<_>>()
224+
}
225+
226+
/// Returns true if this unit is needed for doing doc-scraping and is also
227+
/// allowed to fail without killing the build.
228+
pub fn unit_can_fail_for_docscraping(&self, unit: &Unit) -> bool {
229+
// If the unit is not a Docscrape unit, e.g. a Lib target that is
230+
// checked to scrape an Example target, then we need to get the doc-scrape-examples
231+
// configuration for the reverse-dependent Example target.
232+
let for_scrape_units = if unit.mode.is_doc_scrape() {
233+
vec![unit]
234+
} else {
235+
self.scrape_units_have_dep_on(unit)
236+
};
237+
238+
if for_scrape_units.is_empty() {
239+
false
240+
} else {
241+
// All Docscrape units must have doc-scrape-examples unset. If any are true,
242+
// then the unit is not allowed to fail.
243+
for_scrape_units
244+
.iter()
245+
.all(|unit| unit.target.doc_scrape_examples().is_unset())
246+
}
247+
}
248+
}

‎tests/testsuite/docscrape.rs

+18-3
Original file line numberDiff line numberDiff line change
@@ -332,17 +332,32 @@ fn no_fail_bad_lib() {
332332
"#,
333333
)
334334
.file("src/lib.rs", "pub fn foo() { CRASH_THE_BUILD() }")
335+
.file("examples/ex.rs", "fn main() { foo::foo(); }")
336+
.file("examples/ex2.rs", "fn main() { foo::foo(); }")
335337
.build();
336338

337339
p.cargo("doc -Zunstable-options -Z rustdoc-scrape-examples")
338340
.masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"])
339-
.with_stderr(
341+
.with_stderr_unordered(
340342
"\
343+
[CHECKING] foo v0.0.1 ([CWD])
341344
[SCRAPING] foo v0.0.1 ([CWD])
342345
warning: failed to scan lib in package `foo` for example code usage
343346
Try running with `--verbose` to see the error message.
344-
If this example should not be scanned, consider adding `doc-scrape-examples = false` to the `[[example]]` definition in Cargo.toml
347+
If an example or library should not be scanned, then consider adding `doc-scrape-examples = false` to its `[[example]]` or `[lib]` definition in Cargo.toml
345348
warning: `foo` (lib) generated 1 warning
349+
warning: failed to check lib in package `foo` as a prerequisite for scraping examples from: example \"ex\", example \"ex2\"
350+
Try running with `--verbose` to see the error message.
351+
If an example or library should not be scanned, then consider adding `doc-scrape-examples = false` to its `[[example]]` or `[lib]` definition in Cargo.toml
352+
warning: `foo` (lib) generated 1 warning
353+
warning: failed to scan example \"ex\" in package `foo` for example code usage
354+
Try running with `--verbose` to see the error message.
355+
If an example or library should not be scanned, then consider adding `doc-scrape-examples = false` to its `[[example]]` or `[lib]` definition in Cargo.toml
356+
warning: `foo` (example \"ex\") generated 1 warning
357+
warning: failed to scan example \"ex2\" in package `foo` for example code usage
358+
Try running with `--verbose` to see the error message.
359+
If an example or library should not be scanned, then consider adding `doc-scrape-examples = false` to its `[[example]]` or `[lib]` definition in Cargo.toml
360+
warning: `foo` (example \"ex2\") generated 1 warning
346361
[DOCUMENTING] foo v0.0.1 ([CWD])
347362
[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]",
348363
)
@@ -374,7 +389,7 @@ fn no_fail_bad_example() {
374389
[SCRAPING] foo v0.0.1 ([CWD])
375390
warning: failed to scan example \"ex1\" in package `foo` for example code usage
376391
Try running with `--verbose` to see the error message.
377-
If this example should not be scanned, consider adding `doc-scrape-examples = false` to the `[[example]]` definition in Cargo.toml
392+
If an example or library should not be scanned, then consider adding `doc-scrape-examples = false` to its `[[example]]` or `[lib]` definition in Cargo.toml
378393
warning: `foo` (example \"ex1\") generated 1 warning
379394
[DOCUMENTING] foo v0.0.1 ([CWD])
380395
[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]",

0 commit comments

Comments
 (0)