Skip to content

Commit 0a98b1d

Browse files
committed
Auto merge of #9525 - willcrichton:example-analyzer, r=alexcrichton
Scrape code examples from examples/ directory for Rustdoc Adds support for the functionality described in rust-lang/rfcs#3123 Matching changes to rustdoc are here: rust-lang/rust#85833
2 parents 6c1bc24 + 33718c7 commit 0a98b1d

File tree

16 files changed

+435
-28
lines changed

16 files changed

+435
-28
lines changed

src/bin/cargo/commands/doc.rs

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::command_prelude::*;
22

3-
use cargo::ops::{self, DocOptions};
3+
use anyhow::anyhow;
4+
use cargo::ops::{self, CompileFilter, DocOptions, FilterRule, LibRule};
45

56
pub fn cli() -> App {
67
subcommand("doc")
@@ -19,6 +20,13 @@ pub fn cli() -> App {
1920
)
2021
.arg(opt("no-deps", "Don't build documentation for dependencies"))
2122
.arg(opt("document-private-items", "Document private items"))
23+
.arg(
24+
opt(
25+
"scrape-examples",
26+
"Scrape examples to include as function documentation",
27+
)
28+
.value_name("FLAGS"),
29+
)
2230
.arg_jobs()
2331
.arg_targets_lib_bin_example(
2432
"Document only this package's library",
@@ -48,6 +56,33 @@ pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult {
4856
args.compile_options(config, mode, Some(&ws), ProfileChecking::Custom)?;
4957
compile_opts.rustdoc_document_private_items = args.is_present("document-private-items");
5058

59+
// TODO(wcrichto): move scrape example configuration into Cargo.toml before stabilization
60+
// See: https://github.com/rust-lang/cargo/pull/9525#discussion_r728470927
61+
compile_opts.rustdoc_scrape_examples = match args.value_of("scrape-examples") {
62+
Some(s) => Some(match s {
63+
"all" => CompileFilter::new_all_targets(),
64+
"examples" => CompileFilter::new(
65+
LibRule::False,
66+
FilterRule::none(),
67+
FilterRule::none(),
68+
FilterRule::All,
69+
FilterRule::none(),
70+
),
71+
_ => {
72+
return Err(CliError::from(anyhow!(
73+
r#"--scrape-examples must take "all" or "examples" as an argument"#
74+
)));
75+
}
76+
}),
77+
None => None,
78+
};
79+
80+
if compile_opts.rustdoc_scrape_examples.is_some() {
81+
config
82+
.cli_unstable()
83+
.fail_if_stable_opt("--scrape-examples", 9910)?;
84+
}
85+
5186
let doc_opts = DocOptions {
5287
open_result: args.is_present("open"),
5388
compile_opts,

src/cargo/core/compiler/build_config.rs

+8
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ pub enum CompileMode {
149149
Doc { deps: bool },
150150
/// A target that will be tested with `rustdoc`.
151151
Doctest,
152+
/// An example or library that will be scraped for function calls by `rustdoc`.
153+
Docscrape,
152154
/// A marker for Units that represent the execution of a `build.rs` script.
153155
RunCustomBuild,
154156
}
@@ -166,6 +168,7 @@ impl ser::Serialize for CompileMode {
166168
Bench => "bench".serialize(s),
167169
Doc { .. } => "doc".serialize(s),
168170
Doctest => "doctest".serialize(s),
171+
Docscrape => "docscrape".serialize(s),
169172
RunCustomBuild => "run-custom-build".serialize(s),
170173
}
171174
}
@@ -187,6 +190,11 @@ impl CompileMode {
187190
self == CompileMode::Doctest
188191
}
189192

193+
/// Returns `true` if this is scraping examples for documentation.
194+
pub fn is_doc_scrape(self) -> bool {
195+
self == CompileMode::Docscrape
196+
}
197+
190198
/// Returns `true` if this is any type of test (test, benchmark, doc test, or
191199
/// check test).
192200
pub fn is_any_test(self) -> bool {

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

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ pub struct BuildContext<'a, 'cfg> {
4747
/// The dependency graph of units to compile.
4848
pub unit_graph: UnitGraph,
4949

50+
/// Reverse-dependencies of documented units, used by the rustdoc --scrape-examples flag.
51+
pub scrape_units: Vec<Unit>,
52+
5053
/// The list of all kinds that are involved in this build
5154
pub all_kinds: HashSet<CompileKind>,
5255
}
@@ -61,6 +64,7 @@ impl<'a, 'cfg> BuildContext<'a, 'cfg> {
6164
target_data: RustcTargetData<'cfg>,
6265
roots: Vec<Unit>,
6366
unit_graph: UnitGraph,
67+
scrape_units: Vec<Unit>,
6468
) -> CargoResult<BuildContext<'a, 'cfg>> {
6569
let all_kinds = unit_graph
6670
.keys()
@@ -79,6 +83,7 @@ impl<'a, 'cfg> BuildContext<'a, 'cfg> {
7983
target_data,
8084
roots,
8185
unit_graph,
86+
scrape_units,
8287
all_kinds,
8388
})
8489
}

src/cargo/core/compiler/build_context/target_info.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,10 @@ impl TargetInfo {
452452
}
453453
}
454454
CompileMode::Check { .. } => Ok((vec![FileType::new_rmeta()], Vec::new())),
455-
CompileMode::Doc { .. } | CompileMode::Doctest | CompileMode::RunCustomBuild => {
455+
CompileMode::Doc { .. }
456+
| CompileMode::Doctest
457+
| CompileMode::Docscrape
458+
| CompileMode::RunCustomBuild => {
456459
panic!("asked for rustc output for non-rustc mode")
457460
}
458461
}

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ impl<'a, 'cfg: 'a> CompilationFiles<'a, 'cfg> {
191191
/// Returns the directory where the artifacts for the given unit are
192192
/// initially created.
193193
pub fn out_dir(&self, unit: &Unit) -> PathBuf {
194-
if unit.mode.is_doc() {
194+
// Docscrape units need to have doc/ set as the out_dir so sources for reverse-dependencies
195+
// will be put into doc/ and not into deps/ where the *.examples files are stored.
196+
if unit.mode.is_doc() || unit.mode.is_doc_scrape() {
195197
self.layout(unit.kind).doc().to_path_buf()
196198
} else if unit.mode.is_doc_test() {
197199
panic!("doc tests do not have an out dir");
@@ -417,6 +419,17 @@ impl<'a, 'cfg: 'a> CompilationFiles<'a, 'cfg> {
417419
// but Cargo does not know about that.
418420
vec![]
419421
}
422+
CompileMode::Docscrape => {
423+
let path = self
424+
.deps_dir(unit)
425+
.join(format!("{}.examples", unit.buildkey()));
426+
vec![OutputFile {
427+
path,
428+
hardlink: None,
429+
export_path: None,
430+
flavor: FileFlavor::Normal,
431+
}]
432+
}
420433
CompileMode::Test
421434
| CompileMode::Build
422435
| CompileMode::Bench

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

+42
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ pub struct Context<'a, 'cfg> {
8080
/// compilation is happening (only object, only bitcode, both, etc), and is
8181
/// precalculated early on.
8282
pub lto: HashMap<Unit, Lto>,
83+
84+
/// Map of Doc/Docscrape units to metadata for their -Cmetadata flag.
85+
/// See Context::find_metadata_units for more details.
86+
pub metadata_for_doc_units: HashMap<Unit, Metadata>,
8387
}
8488

8589
impl<'a, 'cfg> Context<'a, 'cfg> {
@@ -120,6 +124,7 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
120124
rustc_clients: HashMap::new(),
121125
pipelining,
122126
lto: HashMap::new(),
127+
metadata_for_doc_units: HashMap::new(),
123128
})
124129
}
125130

@@ -134,6 +139,7 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
134139
self.prepare()?;
135140
custom_build::build_map(&mut self)?;
136141
self.check_collisions()?;
142+
self.compute_metadata_for_doc_units();
137143

138144
// We need to make sure that if there were any previous docs
139145
// already compiled, they were compiled with the same Rustc version that we're currently
@@ -620,4 +626,40 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
620626

621627
Ok(client)
622628
}
629+
630+
/// Finds metadata for Doc/Docscrape units.
631+
///
632+
/// rustdoc needs a -Cmetadata flag in order to recognize StableCrateIds that refer to
633+
/// items in the crate being documented. The -Cmetadata flag used by reverse-dependencies
634+
/// will be the metadata of the Cargo unit that generated the current library's rmeta file,
635+
/// which should be a Check unit.
636+
///
637+
/// If the current crate has reverse-dependencies, such a Check unit should exist, and so
638+
/// we use that crate's metadata. If not, we use the crate's Doc unit so at least examples
639+
/// scraped from the current crate can be used when documenting the current crate.
640+
pub fn compute_metadata_for_doc_units(&mut self) {
641+
for unit in self.bcx.unit_graph.keys() {
642+
if !unit.mode.is_doc() && !unit.mode.is_doc_scrape() {
643+
continue;
644+
}
645+
646+
let matching_units = self
647+
.bcx
648+
.unit_graph
649+
.keys()
650+
.filter(|other| {
651+
unit.pkg == other.pkg
652+
&& unit.target == other.target
653+
&& !other.mode.is_doc_scrape()
654+
})
655+
.collect::<Vec<_>>();
656+
let metadata_unit = matching_units
657+
.iter()
658+
.find(|other| other.mode.is_check())
659+
.or_else(|| matching_units.iter().find(|other| other.mode.is_doc()))
660+
.unwrap_or(&unit);
661+
self.metadata_for_doc_units
662+
.insert(unit.clone(), self.files().metadata(metadata_unit));
663+
}
664+
}
623665
}

src/cargo/core/compiler/mod.rs

+37-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ fn compile<'cfg>(
165165
let force = exec.force_rebuild(unit) || force_rebuild;
166166
let mut job = fingerprint::prepare_target(cx, unit, force)?;
167167
job.before(if job.freshness() == Freshness::Dirty {
168-
let work = if unit.mode.is_doc() {
168+
let work = if unit.mode.is_doc() || unit.mode.is_doc_scrape() {
169169
rustdoc(cx, unit)?
170170
} else {
171171
rustc(cx, unit, exec)?
@@ -647,6 +647,42 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Work> {
647647
rustdoc.args(args);
648648
}
649649

650+
let metadata = cx.metadata_for_doc_units[&unit];
651+
rustdoc.arg("-C").arg(format!("metadata={}", metadata));
652+
653+
let scrape_output_path = |unit: &Unit| -> CargoResult<PathBuf> {
654+
let output_dir = cx.files().deps_dir(unit);
655+
Ok(output_dir.join(format!("{}.examples", unit.buildkey())))
656+
};
657+
658+
if unit.mode.is_doc_scrape() {
659+
debug_assert!(cx.bcx.scrape_units.contains(unit));
660+
661+
rustdoc.arg("-Zunstable-options");
662+
663+
rustdoc
664+
.arg("--scrape-examples-output-path")
665+
.arg(scrape_output_path(unit)?);
666+
667+
// Only scrape example for items from crates in the workspace, to reduce generated file size
668+
for pkg in cx.bcx.ws.members() {
669+
rustdoc
670+
.arg("--scrape-examples-target-crate")
671+
.arg(pkg.name());
672+
}
673+
} else if cx.bcx.scrape_units.len() > 0 && cx.bcx.ws.is_member(&unit.pkg) {
674+
// We only pass scraped examples to packages in the workspace
675+
// since examples are only coming from reverse-dependencies of workspace packages
676+
677+
rustdoc.arg("-Zunstable-options");
678+
679+
for scrape_unit in &cx.bcx.scrape_units {
680+
rustdoc
681+
.arg("--with-examples")
682+
.arg(scrape_output_path(scrape_unit)?);
683+
}
684+
}
685+
650686
build_deps_args(&mut rustdoc, cx, unit)?;
651687
rustdoc::add_root_urls(cx, unit, &mut rustdoc)?;
652688

src/cargo/core/compiler/timings.rs

+1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ impl<'cfg> Timings<'cfg> {
176176
CompileMode::Bench => target.push_str(" (bench)"),
177177
CompileMode::Doc { .. } => target.push_str(" (doc)"),
178178
CompileMode::Doctest => target.push_str(" (doc test)"),
179+
CompileMode::Docscrape => target.push_str(" (doc scrape)"),
179180
CompileMode::RunCustomBuild => target.push_str(" (run)"),
180181
}
181182
let unit_time = UnitTime {

src/cargo/core/compiler/unit_dependencies.rs

+32-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ struct State<'a, 'cfg> {
4747
target_data: &'a RustcTargetData<'cfg>,
4848
profiles: &'a Profiles,
4949
interner: &'a UnitInterner,
50+
scrape_units: &'a [Unit],
5051

5152
/// A set of edges in `unit_dependencies` where (a, b) means that the
5253
/// dependency from a to b was added purely because it was a dev-dependency.
@@ -61,6 +62,7 @@ pub fn build_unit_dependencies<'a, 'cfg>(
6162
features: &'a ResolvedFeatures,
6263
std_resolve: Option<&'a (Resolve, ResolvedFeatures)>,
6364
roots: &[Unit],
65+
scrape_units: &[Unit],
6466
std_roots: &HashMap<CompileKind, Vec<Unit>>,
6567
global_mode: CompileMode,
6668
target_data: &'a RustcTargetData<'cfg>,
@@ -91,6 +93,7 @@ pub fn build_unit_dependencies<'a, 'cfg>(
9193
target_data,
9294
profiles,
9395
interner,
96+
scrape_units,
9497
dev_dependency_edges: HashSet::new(),
9598
};
9699

@@ -253,6 +256,7 @@ fn compute_deps(
253256
if !dep.is_transitive()
254257
&& !unit.target.is_test()
255258
&& !unit.target.is_example()
259+
&& !unit.mode.is_doc_scrape()
256260
&& !unit.mode.is_any_test()
257261
{
258262
return false;
@@ -467,6 +471,25 @@ fn compute_deps_doc(
467471
if unit.target.is_bin() || unit.target.is_example() {
468472
ret.extend(maybe_lib(unit, state, unit_for)?);
469473
}
474+
475+
// Add all units being scraped for examples as a dependency of Doc units.
476+
if state.ws.is_member(&unit.pkg) {
477+
for scrape_unit in state.scrape_units.iter() {
478+
// This needs to match the FeaturesFor used in cargo_compile::generate_targets.
479+
let unit_for = UnitFor::new_host(scrape_unit.target.proc_macro());
480+
deps_of(scrape_unit, state, unit_for)?;
481+
ret.push(new_unit_dep(
482+
state,
483+
scrape_unit,
484+
&scrape_unit.pkg,
485+
&scrape_unit.target,
486+
unit_for,
487+
scrape_unit.kind,
488+
scrape_unit.mode,
489+
)?);
490+
}
491+
}
492+
470493
Ok(ret)
471494
}
472495

@@ -558,7 +581,7 @@ fn dep_build_script(
558581
/// Choose the correct mode for dependencies.
559582
fn check_or_build_mode(mode: CompileMode, target: &Target) -> CompileMode {
560583
match mode {
561-
CompileMode::Check { .. } | CompileMode::Doc { .. } => {
584+
CompileMode::Check { .. } | CompileMode::Doc { .. } | CompileMode::Docscrape => {
562585
if target.for_host() {
563586
// Plugin and proc macro targets should be compiled like
564587
// normal.
@@ -695,6 +718,14 @@ fn connect_run_custom_build_deps(state: &mut State<'_, '_>) {
695718
&& other.unit.target.is_linkable()
696719
&& other.unit.pkg.manifest().links().is_some()
697720
})
721+
// Avoid cycles when using the doc --scrape-examples feature:
722+
// Say a workspace has crates A and B where A has a build-dependency on B.
723+
// The Doc units for A and B will have a dependency on the Docscrape for both A and B.
724+
// So this would add a dependency from B-build to A-build, causing a cycle:
725+
// B (build) -> A (build) -> B(build)
726+
// See the test scrape_examples_avoid_build_script_cycle for a concrete example.
727+
// To avoid this cycle, we filter out the B -> A (docscrape) dependency.
728+
.filter(|(_parent, other)| !other.unit.mode.is_doc_scrape())
698729
// Skip dependencies induced via dev-dependencies since
699730
// connections between `links` and build scripts only happens
700731
// via normal dependencies. Otherwise since dev-dependencies can

src/cargo/core/profiles.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,9 @@ impl Profiles {
323323
(InternedString::new("dev"), None)
324324
}
325325
}
326-
CompileMode::Doc { .. } => (InternedString::new("doc"), None),
326+
CompileMode::Doc { .. } | CompileMode::Docscrape => {
327+
(InternedString::new("doc"), None)
328+
}
327329
}
328330
} else {
329331
(self.requested_profile, None)

0 commit comments

Comments
 (0)