Skip to content

Commit d28c9b8

Browse files
committed
Auto merge of #11430 - willcrichton:example-analyzer, r=weihanglo
Improve strategy for selecting targets to be scraped for examples ### What does this PR try to resolve? After #10343, we have identified a clear set of conditions for whether a particular target should be considered by `-Zrustdoc-scrape-examples`. These conditions are described more clearly in #11425. However, after some testing with complex Cargo workspaces (e.g. [wasmtime](https://github.com/bytecodealliance/wasmtime/)), I realized that the current approach of modifying the `CompileFilter` did not correctly implement this new specification. For example, a target with `doc = false` should not be scraped by default since it is not a documented unit, but the current approach would potentially include such a target for scraping. This PR provides a new approach which I believe correctly implements the specification: 1. `generate_targets` is called with the same parameters except the `mode` which becomes `CompileMode::Docscrape` instead of `CompileMode::Doc`. `filter_default_targets` generates the same targets for `Docscrape` as for `Doc`. 2. Inside `generate_targets`, an initial set of `Proposal`s are created. This set of proposals is extended with further proposals based on targets identified as `doc-scrape-examples = true`, or Example targets where possible. This PR subsumes #11423, and also fixes #10571. ### How should we test and review this PR? I have added another test `docscrape::only_scrape_documented_targets` to verify that only documented or explicitly-enabled targets are included for scraping. r? `@weihanglo`
2 parents 0460192 + 968caae commit d28c9b8

File tree

3 files changed

+122
-84
lines changed

3 files changed

+122
-84
lines changed

src/cargo/ops/cargo_compile/compile_filter.rs

+2-66
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
//! Filters and their rules to select which Cargo targets will be built.
22
33
use crate::core::compiler::CompileMode;
4-
use crate::core::dependency::DepKind;
5-
use crate::core::resolver::HasDevUnits;
6-
use crate::core::{Package, Target, TargetKind};
4+
5+
use crate::core::{Target, TargetKind};
76
use crate::util::restricted_names::is_glob_pattern;
87

98
#[derive(Debug, PartialEq, Eq, Clone)]
@@ -301,67 +300,4 @@ impl CompileFilter {
301300
}
302301
}
303302
}
304-
305-
/// Generate a CompileFilter that represents the maximal set of targets
306-
/// that should be considered for scraped examples.
307-
pub(super) fn refine_for_docscrape(
308-
&self,
309-
to_builds: &[&Package],
310-
has_dev_units: HasDevUnits,
311-
) -> CompileFilter {
312-
// In general, the goal is to scrape examples from (a) whatever targets
313-
// the user is documenting, and (b) Example targets. However, if the user
314-
// is documenting a library with dev-dependencies, those dev-deps are not
315-
// needed for the library, while dev-deps are needed for the examples.
316-
//
317-
// If scrape-examples caused `cargo doc` to start requiring dev-deps, this
318-
// would be a breaking change to crates whose dev-deps don't compile.
319-
// Therefore we ONLY want to scrape Example targets if either:
320-
// (1) No package has dev-dependencies, so this is a moot issue, OR
321-
// (2) The provided CompileFilter requires dev-dependencies anyway.
322-
//
323-
// The next two variables represent these two conditions.
324-
325-
let no_pkg_has_dev_deps = to_builds.iter().all(|pkg| {
326-
pkg.summary()
327-
.dependencies()
328-
.iter()
329-
.all(|dep| !matches!(dep.kind(), DepKind::Development))
330-
});
331-
332-
let reqs_dev_deps = matches!(has_dev_units, HasDevUnits::Yes);
333-
334-
let example_filter = if no_pkg_has_dev_deps || reqs_dev_deps {
335-
FilterRule::All
336-
} else {
337-
FilterRule::none()
338-
};
339-
340-
match self {
341-
CompileFilter::Only {
342-
all_targets,
343-
lib,
344-
bins,
345-
tests,
346-
benches,
347-
..
348-
} => CompileFilter::Only {
349-
all_targets: *all_targets,
350-
lib: lib.clone(),
351-
bins: bins.clone(),
352-
examples: example_filter,
353-
tests: tests.clone(),
354-
benches: benches.clone(),
355-
},
356-
357-
CompileFilter::Default { .. } => CompileFilter::Only {
358-
all_targets: false,
359-
lib: LibRule::Default,
360-
bins: FilterRule::none(),
361-
examples: example_filter,
362-
tests: FilterRule::none(),
363-
benches: FilterRule::none(),
364-
},
365-
}
366-
}
367303
}

src/cargo/ops/cargo_compile/mod.rs

+58-17
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ use crate::core::compiler::{standard_lib, CrateType, TargetInfo};
4141
use crate::core::compiler::{BuildConfig, BuildContext, Compilation, Context};
4242
use crate::core::compiler::{CompileKind, CompileMode, CompileTarget, RustcTargetData, Unit};
4343
use crate::core::compiler::{DefaultExecutor, Executor, UnitInterner};
44+
use crate::core::dependency::DepKind;
4445
use crate::core::profiles::{Profiles, UnitFor};
4546
use crate::core::resolver::features::{self, CliFeatures, FeaturesFor};
4647
use crate::core::resolver::{HasDevUnits, Resolve};
@@ -361,6 +362,7 @@ pub fn create_bcx<'a, 'cfg>(
361362
&pkg_set,
362363
&profiles,
363364
interner,
365+
has_dev_units,
364366
)?;
365367

366368
if let Some(args) = target_rustc_crate_types {
@@ -369,11 +371,10 @@ pub fn create_bcx<'a, 'cfg>(
369371

370372
let should_scrape = build_config.mode.is_doc() && config.cli_unstable().rustdoc_scrape_examples;
371373
let mut scrape_units = if should_scrape {
372-
let scrape_filter = filter.refine_for_docscrape(&to_builds, has_dev_units);
373374
let all_units = generate_targets(
374375
ws,
375376
&to_builds,
376-
&scrape_filter,
377+
&filter,
377378
&build_config.requested_kinds,
378379
explicit_host_kind,
379380
CompileMode::Docscrape,
@@ -383,24 +384,17 @@ pub fn create_bcx<'a, 'cfg>(
383384
&pkg_set,
384385
&profiles,
385386
interner,
387+
has_dev_units,
386388
)?;
387389

388-
// The set of scraped targets should be a strict subset of the set of documented targets,
389-
// except in the special case of examples targets.
390-
if cfg!(debug_assertions) {
391-
let valid_targets = units.iter().map(|u| &u.target).collect::<HashSet<_>>();
392-
for unit in &all_units {
393-
assert!(unit.target.is_example() || valid_targets.contains(&unit.target));
394-
}
395-
}
396-
397390
let valid_units = all_units
398391
.into_iter()
399392
.filter(|unit| {
400-
!matches!(
401-
unit.target.doc_scrape_examples(),
402-
RustdocScrapeExamples::Disabled
403-
)
393+
ws.unit_needs_doc_scrape(unit)
394+
&& !matches!(
395+
unit.target.doc_scrape_examples(),
396+
RustdocScrapeExamples::Disabled
397+
)
404398
})
405399
.collect::<Vec<_>>();
406400
valid_units
@@ -597,6 +591,7 @@ fn generate_targets(
597591
package_set: &PackageSet<'_>,
598592
profiles: &Profiles,
599593
interner: &UnitInterner,
594+
has_dev_units: HasDevUnits,
600595
) -> CargoResult<Vec<Unit>> {
601596
let config = ws.config();
602597
// Helper for creating a list of `Unit` structures
@@ -828,6 +823,52 @@ fn generate_targets(
828823
}
829824
}
830825

826+
if mode.is_doc_scrape() {
827+
// In general, the goal is to scrape examples from (a) whatever targets
828+
// the user is documenting, and (b) Example targets. However, if the user
829+
// is documenting a library with dev-dependencies, those dev-deps are not
830+
// needed for the library, while dev-deps are needed for the examples.
831+
//
832+
// If scrape-examples caused `cargo doc` to start requiring dev-deps, this
833+
// would be a breaking change to crates whose dev-deps don't compile.
834+
// Therefore we ONLY want to scrape Example targets if either:
835+
// (1) No package has dev-dependencies, so this is a moot issue, OR
836+
// (2) The provided CompileFilter requires dev-dependencies anyway.
837+
//
838+
// The next two variables represent these two conditions.
839+
let no_pkg_has_dev_deps = packages.iter().all(|pkg| {
840+
pkg.summary()
841+
.dependencies()
842+
.iter()
843+
.all(|dep| !matches!(dep.kind(), DepKind::Development))
844+
});
845+
let reqs_dev_deps = matches!(has_dev_units, HasDevUnits::Yes);
846+
let safe_to_scrape_example_targets = no_pkg_has_dev_deps || reqs_dev_deps;
847+
848+
let proposed_targets: HashSet<&Target> = proposals.iter().map(|p| p.target).collect();
849+
let can_scrape = |target: &Target| {
850+
let not_redundant = !proposed_targets.contains(target);
851+
not_redundant
852+
&& match (target.doc_scrape_examples(), target.is_example()) {
853+
// Targets configured by the user to not be scraped should never be scraped
854+
(RustdocScrapeExamples::Disabled, _) => false,
855+
// Targets configured by the user to be scraped should always be scraped
856+
(RustdocScrapeExamples::Enabled, _) => true,
857+
// Example targets with no configuration should be conditionally scraped if
858+
// it's guaranteed not to break the build
859+
(RustdocScrapeExamples::Unset, true) => safe_to_scrape_example_targets,
860+
// All other targets are ignored for now. This may change in the future!
861+
(RustdocScrapeExamples::Unset, false) => false,
862+
}
863+
};
864+
proposals.extend(filter_targets(
865+
packages,
866+
can_scrape,
867+
false,
868+
CompileMode::Docscrape,
869+
));
870+
}
871+
831872
// Only include targets that are libraries or have all required
832873
// features available.
833874
//
@@ -1074,7 +1115,7 @@ fn filter_default_targets(targets: &[Target], mode: CompileMode) -> Vec<&Target>
10741115
.iter()
10751116
.filter(|t| t.is_bin() || t.is_lib())
10761117
.collect(),
1077-
CompileMode::Doc { .. } => {
1118+
CompileMode::Doc { .. } | CompileMode::Docscrape => {
10781119
// `doc` does lib and bins (bin with same name as lib is skipped).
10791120
targets
10801121
.iter()
@@ -1085,7 +1126,7 @@ fn filter_default_targets(targets: &[Target], mode: CompileMode) -> Vec<&Target>
10851126
})
10861127
.collect()
10871128
}
1088-
CompileMode::Doctest | CompileMode::Docscrape | CompileMode::RunCustomBuild => {
1129+
CompileMode::Doctest | CompileMode::RunCustomBuild => {
10891130
panic!("Invalid mode {:?}", mode)
10901131
}
10911132
}

tests/testsuite/docscrape.rs

+62-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ fn configure_target() {
196196
)
197197
.build();
198198

199-
p.cargo("doc --lib --bins -Zunstable-options -Zrustdoc-scrape-examples")
199+
p.cargo("doc -Zunstable-options -Zrustdoc-scrape-examples")
200200
.masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"])
201201
.run();
202202

@@ -519,3 +519,64 @@ fn use_dev_deps_if_explicitly_enabled() {
519519
)
520520
.run();
521521
}
522+
523+
#[cargo_test(nightly, reason = "rustdoc scrape examples flags are unstable")]
524+
fn only_scrape_documented_targets() {
525+
// package bar has doc = false and should not be eligible for documtation.
526+
let run_with_config = |config: &str, should_scrape: bool| {
527+
let p = project()
528+
.file(
529+
"Cargo.toml",
530+
&format!(
531+
r#"
532+
[package]
533+
name = "bar"
534+
version = "0.0.1"
535+
authors = []
536+
537+
[lib]
538+
{config}
539+
540+
[workspace]
541+
members = ["foo"]
542+
543+
[dependencies]
544+
foo = {{ path = "foo" }}
545+
"#
546+
),
547+
)
548+
.file("src/lib.rs", "pub fn bar() { foo::foo(); }")
549+
.file(
550+
"foo/Cargo.toml",
551+
r#"
552+
[package]
553+
name = "foo"
554+
version = "0.0.1"
555+
authors = []
556+
"#,
557+
)
558+
.file("foo/src/lib.rs", "pub fn foo() {}")
559+
.build();
560+
561+
p.cargo("doc --workspace -Zunstable-options -Zrustdoc-scrape-examples")
562+
.masquerade_as_nightly_cargo(&["rustdoc-scrape-examples"])
563+
.run();
564+
565+
let doc_html = p.read_file("target/doc/foo/fn.foo.html");
566+
let example_found = doc_html.contains("Examples found in repository");
567+
if should_scrape {
568+
assert!(example_found);
569+
} else {
570+
assert!(!example_found);
571+
}
572+
};
573+
574+
// By default, bar should be scraped.
575+
run_with_config("", true);
576+
// If bar isn't supposed to be documented, then it is not eligible
577+
// for scraping.
578+
run_with_config("doc = false", false);
579+
// But if the user explicitly says bar should be scraped, then it should
580+
// be scraped.
581+
run_with_config("doc = false\ndoc-scrape-examples = true", true);
582+
}

0 commit comments

Comments
 (0)