Skip to content

Commit de56c12

Browse files
committed
Auto merge of #10343 - willcrichton:example-analyzer, r=weihanglo
Change rustdoc-scrape-examples to be a target-level configuration This PR addresses issues raised in #9525. Specifically: 1. It enables examples to be scraped from `#[test]` functions, by passing additional flags to Rustdoc to ensure that these functions aren't ignored by rustc. 2. It moves the `arg` from `-Z rustdoc-scrape-examples={arg}` into a target-level configuration that can be added to Cargo.toml. The added test `scrape_examples_configure_target` shows a concrete example. In short, examples will be default scraped from Example and Lib targets. Then the user can enable or disable scraping like so: ```toml [lib] doc-scrape-examples = false [[test]] name = "my_test" doc-scrape-examples = true ```
2 parents 6a0f0cb + 183425f commit de56c12

File tree

18 files changed

+832
-365
lines changed

18 files changed

+832
-365
lines changed

crates/cargo-test-support/src/compare.rs

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ fn substitute_macros(input: &str) -> String {
168168
("[NOTE]", "note:"),
169169
("[HELP]", "help:"),
170170
("[DOCUMENTING]", " Documenting"),
171+
("[SCRAPING]", " Scraping"),
171172
("[FRESH]", " Fresh"),
172173
("[UPDATING]", " Updating"),
173174
("[ADDING]", " Adding"),

src/cargo/core/compiler/context/compilation_files.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,11 @@ impl<'a, 'cfg: 'a> CompilationFiles<'a, 'cfg> {
451451
vec![]
452452
}
453453
CompileMode::Docscrape => {
454-
let path = self
455-
.deps_dir(unit)
456-
.join(format!("{}.examples", unit.buildkey()));
454+
// The file name needs to be stable across Cargo sessions.
455+
// This originally used unit.buildkey(), but that isn't stable,
456+
// so we use metadata instead (prefixed with name for debugging).
457+
let file_name = format!("{}-{}.examples", unit.pkg.name(), self.metadata(unit));
458+
let path = self.deps_dir(unit).join(file_name);
457459
vec![OutputFile {
458460
path,
459461
hardlink: None,

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

+6
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ pub struct Context<'a, 'cfg> {
7777
/// Map of Doc/Docscrape units to metadata for their -Cmetadata flag.
7878
/// See Context::find_metadata_units for more details.
7979
pub metadata_for_doc_units: HashMap<Unit, Metadata>,
80+
81+
/// Set of metadata of Docscrape units that fail before completion, e.g.
82+
/// because the target has a type error. This is in an Arc<Mutex<..>>
83+
/// because it is continuously updated as the job progresses.
84+
pub failed_scrape_units: Arc<Mutex<HashSet<Metadata>>>,
8085
}
8186

8287
impl<'a, 'cfg> Context<'a, 'cfg> {
@@ -115,6 +120,7 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
115120
rustc_clients: HashMap::new(),
116121
lto: HashMap::new(),
117122
metadata_for_doc_units: HashMap::new(),
123+
failed_scrape_units: Arc::new(Mutex::new(HashSet::new())),
118124
})
119125
}
120126

src/cargo/core/compiler/fingerprint.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1275,7 +1275,7 @@ fn calculate_normal(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Finger
12751275

12761276
// Afterwards calculate our own fingerprint information.
12771277
let target_root = target_root(cx);
1278-
let local = if unit.mode.is_doc() {
1278+
let local = if unit.mode.is_doc() || unit.mode.is_doc_scrape() {
12791279
// rustdoc does not have dep-info files.
12801280
let fingerprint = pkg_fingerprint(cx.bcx, &unit.pkg).with_context(|| {
12811281
format!(
@@ -1302,7 +1302,7 @@ fn calculate_normal(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Finger
13021302
// Fill out a bunch more information that we'll be tracking typically
13031303
// hashed to take up less space on disk as we just need to know when things
13041304
// change.
1305-
let extra_flags = if unit.mode.is_doc() {
1305+
let extra_flags = if unit.mode.is_doc() || unit.mode.is_doc_scrape() {
13061306
cx.bcx.rustdocflags_args(unit)
13071307
} else {
13081308
cx.bcx.rustflags_args(unit)

src/cargo/core/compiler/job_queue.rs

+47-1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ struct DrainState<'cfg> {
134134
active: HashMap<JobId, Unit>,
135135
compiled: HashSet<PackageId>,
136136
documented: HashSet<PackageId>,
137+
scraped: HashSet<PackageId>,
137138
counts: HashMap<PackageId, usize>,
138139
progress: Progress<'cfg>,
139140
next_id: u32,
@@ -347,17 +348,29 @@ enum Message {
347348
BuildPlanMsg(String, ProcessBuilder, Arc<Vec<OutputFile>>),
348349
Stdout(String),
349350
Stderr(String),
351+
352+
// This is for general stderr output from subprocesses
350353
Diagnostic {
351354
id: JobId,
352355
level: String,
353356
diag: String,
354357
fixable: bool,
355358
},
359+
// This handles duplicate output that is suppressed, for showing
360+
// only a count of duplicate messages instead
356361
WarningCount {
357362
id: JobId,
358363
emitted: bool,
359364
fixable: bool,
360365
},
366+
// This is for warnings generated by Cargo's interpretation of the
367+
// subprocess output, e.g. scrape-examples prints a warning if a
368+
// unit fails to be scraped
369+
Warning {
370+
id: JobId,
371+
warning: String,
372+
},
373+
361374
FixDiagnostic(diagnostic_server::Message),
362375
Token(io::Result<Acquired>),
363376
Finish(JobId, Artifact, CargoResult<()>),
@@ -405,6 +418,7 @@ impl<'a, 'cfg> JobState<'a, 'cfg> {
405418
Ok(())
406419
}
407420

421+
/// See [`Message::Diagnostic`] and [`Message::WarningCount`].
408422
pub fn emit_diag(&self, level: String, diag: String, fixable: bool) -> CargoResult<()> {
409423
if let Some(dedupe) = self.output {
410424
let emitted = dedupe.emit_diag(&diag)?;
@@ -426,6 +440,15 @@ impl<'a, 'cfg> JobState<'a, 'cfg> {
426440
Ok(())
427441
}
428442

443+
/// See [`Message::Warning`].
444+
pub fn warning(&self, warning: String) -> CargoResult<()> {
445+
self.messages.push_bounded(Message::Warning {
446+
id: self.id,
447+
warning,
448+
});
449+
Ok(())
450+
}
451+
429452
/// A method used to signal to the coordinator thread that the rmeta file
430453
/// for an rlib has been produced. This is only called for some rmeta
431454
/// builds when required, and can be called at any time before a job ends.
@@ -475,8 +498,10 @@ impl<'cfg> JobQueue<'cfg> {
475498
.filter(|dep| {
476499
// Binaries aren't actually needed to *compile* tests, just to run
477500
// them, so we don't include this dependency edge in the job graph.
501+
// But we shouldn't filter out dependencies being scraped for Rustdoc.
478502
(!dep.unit.target.is_test() && !dep.unit.target.is_bin())
479503
|| dep.unit.artifact.is_true()
504+
|| dep.unit.mode.is_doc_scrape()
480505
})
481506
.map(|dep| {
482507
// Handle the case here where our `unit -> dep` dependency may
@@ -563,6 +588,7 @@ impl<'cfg> JobQueue<'cfg> {
563588
active: HashMap::new(),
564589
compiled: HashSet::new(),
565590
documented: HashSet::new(),
591+
scraped: HashSet::new(),
566592
counts: self.counts,
567593
progress,
568594
next_id: 0,
@@ -739,6 +765,10 @@ impl<'cfg> DrainState<'cfg> {
739765
cnts.disallow_fixable();
740766
}
741767
}
768+
Message::Warning { id, warning } => {
769+
cx.bcx.config.shell().warn(warning)?;
770+
self.bump_warning_count(id, true, false);
771+
}
742772
Message::WarningCount {
743773
id,
744774
emitted,
@@ -782,6 +812,16 @@ impl<'cfg> DrainState<'cfg> {
782812
debug!("end ({:?}): {:?}", unit, result);
783813
match result {
784814
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+
{
819+
cx.failed_scrape_units
820+
.lock()
821+
.unwrap()
822+
.insert(cx.files().metadata(&unit));
823+
self.queue.finish(&unit, &artifact);
824+
}
785825
Err(error) => {
786826
let msg = "The following warnings were emitted during compilation:";
787827
self.emit_warnings(Some(msg), &unit, cx)?;
@@ -1303,8 +1343,11 @@ impl<'cfg> DrainState<'cfg> {
13031343
unit: &Unit,
13041344
fresh: Freshness,
13051345
) -> CargoResult<()> {
1306-
if (self.compiled.contains(&unit.pkg.package_id()) && !unit.mode.is_doc())
1346+
if (self.compiled.contains(&unit.pkg.package_id())
1347+
&& !unit.mode.is_doc()
1348+
&& !unit.mode.is_doc_scrape())
13071349
|| (self.documented.contains(&unit.pkg.package_id()) && unit.mode.is_doc())
1350+
|| (self.scraped.contains(&unit.pkg.package_id()) && unit.mode.is_doc_scrape())
13081351
{
13091352
return Ok(());
13101353
}
@@ -1318,6 +1361,9 @@ impl<'cfg> DrainState<'cfg> {
13181361
config.shell().status("Documenting", &unit.pkg)?;
13191362
} else if unit.mode.is_doc_test() {
13201363
// Skip doc test.
1364+
} else if unit.mode.is_doc_scrape() {
1365+
self.scraped.insert(unit.pkg.package_id());
1366+
config.shell().status("Scraping", &unit.pkg)?;
13211367
} else {
13221368
self.compiled.insert(unit.pkg.package_id());
13231369
if unit.mode.is_check() {

src/cargo/core/compiler/mod.rs

+66-16
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ mod unit;
2222
pub mod unit_dependencies;
2323
pub mod unit_graph;
2424

25-
use std::collections::HashSet;
25+
use std::collections::{HashMap, HashSet};
2626
use std::env;
2727
use std::ffi::{OsStr, OsString};
2828
use std::fs::{self, File};
@@ -55,7 +55,7 @@ use crate::core::compiler::future_incompat::FutureIncompatReport;
5555
pub use crate::core::compiler::unit::{Unit, UnitInterner};
5656
use crate::core::manifest::TargetSourcePath;
5757
use crate::core::profiles::{PanicStrategy, Profile, Strip};
58-
use crate::core::{Feature, PackageId, Target};
58+
use crate::core::{Feature, PackageId, Target, Verbosity};
5959
use crate::util::errors::{CargoResult, VerboseError};
6060
use crate::util::interning::InternedString;
6161
use crate::util::machine_message::{self, Message};
@@ -654,13 +654,16 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
654654
rustdoc.arg("-C").arg(format!("metadata={}", metadata));
655655

656656
let scrape_output_path = |unit: &Unit| -> CargoResult<PathBuf> {
657-
let output_dir = cx.files().deps_dir(unit);
658-
Ok(output_dir.join(format!("{}.examples", unit.buildkey())))
657+
cx.outputs(unit).map(|outputs| outputs[0].path.clone())
659658
};
660659

661660
if unit.mode.is_doc_scrape() {
662661
debug_assert!(cx.bcx.scrape_units.contains(unit));
663662

663+
if unit.target.is_test() {
664+
rustdoc.arg("--scrape-tests");
665+
}
666+
664667
rustdoc.arg("-Zunstable-options");
665668

666669
rustdoc
@@ -678,18 +681,23 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
678681
rustdoc.arg("--scrape-examples-target-crate").arg(name);
679682
}
680683
}
681-
} else if cx.bcx.scrape_units.len() > 0 && cx.bcx.ws.unit_needs_doc_scrape(unit) {
682-
// We only pass scraped examples to packages in the workspace
683-
// since examples are only coming from reverse-dependencies of workspace packages
684+
}
684685

686+
let should_include_scrape_units = unit.mode.is_doc()
687+
&& cx.bcx.scrape_units.len() > 0
688+
&& cx.bcx.ws.unit_needs_doc_scrape(unit);
689+
let scrape_outputs = if should_include_scrape_units {
685690
rustdoc.arg("-Zunstable-options");
686-
687-
for scrape_unit in &cx.bcx.scrape_units {
688-
rustdoc
689-
.arg("--with-examples")
690-
.arg(scrape_output_path(scrape_unit)?);
691-
}
692-
}
691+
Some(
692+
cx.bcx
693+
.scrape_units
694+
.iter()
695+
.map(|unit| Ok((cx.files().metadata(unit), scrape_output_path(unit)?)))
696+
.collect::<CargoResult<HashMap<_, _>>>()?,
697+
)
698+
} else {
699+
None
700+
};
693701

694702
build_deps_args(&mut rustdoc, cx, unit)?;
695703
rustdoc::add_root_urls(cx, unit, &mut rustdoc)?;
@@ -700,19 +708,45 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
700708
append_crate_version_flag(unit, &mut rustdoc);
701709
}
702710

711+
let target_desc = unit.target.description_named();
703712
let name = unit.pkg.name().to_string();
704713
let build_script_outputs = Arc::clone(&cx.build_script_outputs);
705714
let package_id = unit.pkg.package_id();
706715
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();
707720
let target = Target::clone(&unit.target);
708721
let mut output_options = OutputOptions::new(cx, unit);
709722
let script_metadata = cx.find_build_script_metadata(unit);
723+
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()
726+
&& !matches!(cx.bcx.config.shell().verbosity(), Verbosity::Verbose);
727+
if hide_diagnostics_for_scrape_unit {
728+
output_options.show_diagnostics = false;
729+
}
710730
Ok(Work::new(move |state| {
711731
add_custom_flags(
712732
&mut rustdoc,
713733
&build_script_outputs.lock().unwrap(),
714734
script_metadata,
715735
)?;
736+
737+
// Add the output of scraped examples to the rustdoc command.
738+
// This action must happen after the unit's dependencies have finished,
739+
// because some of those deps may be Docscrape units which have failed.
740+
// So we dynamically determine which `--with-examples` flags to pass here.
741+
if let Some(scrape_outputs) = scrape_outputs {
742+
let failed_scrape_units = failed_scrape_units.lock().unwrap();
743+
for (metadata, output_path) in &scrape_outputs {
744+
if !failed_scrape_units.contains(metadata) {
745+
rustdoc.arg("--with-examples").arg(output_path);
746+
}
747+
}
748+
}
749+
716750
let crate_dir = doc_dir.join(&crate_name);
717751
if crate_dir.exists() {
718752
// Remove output from a previous build. This ensures that stale
@@ -722,7 +756,7 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
722756
}
723757
state.running(&rustdoc);
724758

725-
rustdoc
759+
let result = rustdoc
726760
.exec_with_streaming(
727761
&mut |line| on_stdout_line(state, line, package_id, &target),
728762
&mut |line| {
@@ -737,7 +771,23 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
737771
},
738772
false,
739773
)
740-
.with_context(|| format!("could not document `{}`", name))?;
774+
.with_context(|| format!("could not document `{}`", name));
775+
776+
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)?;
786+
}
787+
788+
return Err(e);
789+
}
790+
741791
Ok(())
742792
}))
743793
}

src/cargo/core/compiler/rustdoc.rs

+19
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,22 @@ pub fn add_root_urls(
190190
}
191191
Ok(())
192192
}
193+
194+
/// Indicates whether a target should have examples scraped from it
195+
/// by rustdoc. Configured within Cargo.toml.
196+
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Copy)]
197+
pub enum RustdocScrapeExamples {
198+
Enabled,
199+
Disabled,
200+
Unset,
201+
}
202+
203+
impl RustdocScrapeExamples {
204+
pub fn is_enabled(&self) -> bool {
205+
matches!(self, RustdocScrapeExamples::Enabled)
206+
}
207+
208+
pub fn is_unset(&self) -> bool {
209+
matches!(self, RustdocScrapeExamples::Unset)
210+
}
211+
}

src/cargo/core/compiler/unit_dependencies.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -718,13 +718,14 @@ fn compute_deps_doc(
718718
// Add all units being scraped for examples as a dependency of top-level Doc units.
719719
if state.ws.unit_needs_doc_scrape(unit) {
720720
for scrape_unit in state.scrape_units.iter() {
721-
deps_of(scrape_unit, state, unit_for)?;
721+
let scrape_unit_for = UnitFor::new_normal(scrape_unit.kind);
722+
deps_of(scrape_unit, state, scrape_unit_for)?;
722723
ret.push(new_unit_dep(
723724
state,
724725
scrape_unit,
725726
&scrape_unit.pkg,
726727
&scrape_unit.target,
727-
unit_for,
728+
scrape_unit_for,
728729
scrape_unit.kind,
729730
scrape_unit.mode,
730731
IS_NO_ARTIFACT_DEP,
@@ -1088,7 +1089,6 @@ impl<'a, 'cfg> State<'a, 'cfg> {
10881089
if !dep.is_transitive()
10891090
&& !unit.target.is_test()
10901091
&& !unit.target.is_example()
1091-
&& !unit.mode.is_doc_scrape()
10921092
&& !unit.mode.is_any_test()
10931093
{
10941094
return false;

0 commit comments

Comments
 (0)