Skip to content

Commit 1812e5b

Browse files
authored
Rollup merge of rust-lang#58626 - QuietMisdreavus:doc-coverage, r=GuillaumeGomez
rustdoc: add option to calculate "documentation coverage" This PR adds a new flag to rustdoc, `--show-coverage`. When passed, this flag will make rustdoc count the number of items in a crate with documentation instead of generating docs. This count will be output as a table of each file in the crate, like this (when run on my crate `egg-mode`): ``` +-------------------------------------+------------+------------+------------+ | File | Documented | Total | Percentage | +-------------------------------------+------------+------------+------------+ | src/auth.rs | 16 | 16 | 100.0% | | src/common/mod.rs | 1 | 1 | 100.0% | | src/common/response.rs | 9 | 9 | 100.0% | | src/cursor.rs | 24 | 24 | 100.0% | | src/direct/fun.rs | 6 | 6 | 100.0% | | src/direct/mod.rs | 41 | 41 | 100.0% | | src/entities.rs | 50 | 50 | 100.0% | | src/error.rs | 27 | 27 | 100.0% | | src/lib.rs | 1 | 1 | 100.0% | | src/list/fun.rs | 19 | 19 | 100.0% | | src/list/mod.rs | 22 | 22 | 100.0% | | src/media/mod.rs | 27 | 27 | 100.0% | | src/place/fun.rs | 8 | 8 | 100.0% | | src/place/mod.rs | 35 | 35 | 100.0% | | src/search.rs | 26 | 26 | 100.0% | | src/service.rs | 74 | 74 | 100.0% | | src/stream/mod.rs | 49 | 49 | 100.0% | | src/tweet/fun.rs | 15 | 15 | 100.0% | | src/tweet/mod.rs | 73 | 73 | 100.0% | | src/user/fun.rs | 24 | 24 | 100.0% | | src/user/mod.rs | 87 | 87 | 100.0% | +-------------------------------------+------------+------------+------------+ | Total | 634 | 634 | 100.0% | +-------------------------------------+------------+------------+------------+ ``` Trait implementations are not counted because by default they "inherit" the docs from the trait, even though an impl can override those docs. Similarly, inherent impl blocks are not counted at all, because for the majority of cases such docs are not useful. (The usual pattern for inherent impl blocks is to throw all the methods on a type into a single impl block. Any docs you would put on that block would be better served on the type itself.) In addition, `--show-coverage` can be combined with `--document-private-items` to get the coverage counts for everything in the crate, not just public items. The coverage calculation is implemented as a late pass and two new sets of passes which strip out most of the work that rustdoc otherwise does when generating docs. The is because after the new pass is executed, rustdoc immediately closes instead of going on to generate documentation. Many examples of coverage calculations have been included as `rustdoc-ui` tests. r? @rust-lang/rustdoc
2 parents d6234ba + 3df0b89 commit 1812e5b

21 files changed

+487
-10
lines changed

src/doc/rustdoc/src/unstable-features.md

+31-4
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ For example, in the following code:
5353
```rust
5454
/// Does the thing.
5555
pub fn do_the_thing(_: SomeType) {
56-
println!("Let's do the thing!");
56+
println!("Let's do the thing!");
5757
}
5858

5959
/// Token you use to [`do_the_thing`].
@@ -66,15 +66,15 @@ target out also works:
6666

6767
```rust
6868
pub mod some_module {
69-
/// Token you use to do the thing.
70-
pub struct SomeStruct;
69+
/// Token you use to do the thing.
70+
pub struct SomeStruct;
7171
}
7272

7373
/// Does the thing. Requires one [`SomeStruct`] for the thing to work.
7474
///
7575
/// [`SomeStruct`]: some_module::SomeStruct
7676
pub fn do_the_thing(_: some_module::SomeStruct) {
77-
println!("Let's do the thing!");
77+
println!("Let's do the thing!");
7878
}
7979
```
8080

@@ -428,3 +428,30 @@ $ rustdoc src/lib.rs --test -Z unstable-options --persist-doctests target/rustdo
428428
This flag allows you to keep doctest executables around after they're compiled or run.
429429
Usually, rustdoc will immediately discard a compiled doctest after it's been tested, but
430430
with this option, you can keep those binaries around for farther testing.
431+
432+
### `--show-coverage`: calculate the percentage of items with documentation
433+
434+
Using this flag looks like this:
435+
436+
```bash
437+
$ rustdoc src/lib.rs -Z unstable-options --show-coverage
438+
```
439+
440+
If you want to determine how many items in your crate are documented, pass this flag to rustdoc.
441+
When it receives this flag, it will count the public items in your crate that have documentation,
442+
and print out the counts and a percentage instead of generating docs.
443+
444+
Some methodology notes about what rustdoc counts in this metric:
445+
446+
* Rustdoc will only count items from your crate (i.e. items re-exported from other crates don't
447+
count).
448+
* Docs written directly onto inherent impl blocks are not counted, even though their doc comments
449+
are displayed, because the common pattern in Rust code is to write all inherent methods into the
450+
same impl block.
451+
* Items in a trait implementation are not counted, as those impls will inherit any docs from the
452+
trait itself.
453+
* By default, only public items are counted. To count private items as well, pass
454+
`--document-private-items` at the same time.
455+
456+
Public items that are not documented can be seen with the built-in `missing_docs` lint. Private
457+
items that are not documented can be seen with Clippy's `missing_docs_in_private_items` lint.

src/librustdoc/config.rs

+25-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ pub struct Options {
8585
/// Whether to display warnings during doc generation or while gathering doctests. By default,
8686
/// all non-rustdoc-specific lints are allowed when generating docs.
8787
pub display_warnings: bool,
88+
/// Whether to run the `calculate-doc-coverage` pass, which counts the number of public items
89+
/// with and without documentation.
90+
pub show_coverage: bool,
8891

8992
// Options that alter generated documentation pages
9093

@@ -128,6 +131,7 @@ impl fmt::Debug for Options {
128131
.field("default_passes", &self.default_passes)
129132
.field("manual_passes", &self.manual_passes)
130133
.field("display_warnings", &self.display_warnings)
134+
.field("show_coverage", &self.show_coverage)
131135
.field("crate_version", &self.crate_version)
132136
.field("render_options", &self.render_options)
133137
.finish()
@@ -224,6 +228,18 @@ impl Options {
224228
for &name in passes::DEFAULT_PRIVATE_PASSES {
225229
println!("{:>20}", name);
226230
}
231+
232+
if nightly_options::is_nightly_build() {
233+
println!("\nPasses run with `--show-coverage`:");
234+
for &name in passes::DEFAULT_COVERAGE_PASSES {
235+
println!("{:>20}", name);
236+
}
237+
println!("\nPasses run with `--show-coverage --document-private-items`:");
238+
for &name in passes::PRIVATE_COVERAGE_PASSES {
239+
println!("{:>20}", name);
240+
}
241+
}
242+
227243
return Err(0);
228244
}
229245

@@ -413,9 +429,16 @@ impl Options {
413429
}
414430
});
415431

432+
let show_coverage = matches.opt_present("show-coverage");
433+
let document_private = matches.opt_present("document-private-items");
434+
416435
let default_passes = if matches.opt_present("no-defaults") {
417436
passes::DefaultPassOption::None
418-
} else if matches.opt_present("document-private-items") {
437+
} else if show_coverage && document_private {
438+
passes::DefaultPassOption::PrivateCoverage
439+
} else if show_coverage {
440+
passes::DefaultPassOption::Coverage
441+
} else if document_private {
419442
passes::DefaultPassOption::Private
420443
} else {
421444
passes::DefaultPassOption::Default
@@ -463,6 +486,7 @@ impl Options {
463486
default_passes,
464487
manual_passes,
465488
display_warnings,
489+
show_coverage,
466490
crate_version,
467491
persist_doctests,
468492
render_options: RenderOptions {

src/librustdoc/core.rs

+7-4
Original file line numberDiff line numberDiff line change
@@ -617,10 +617,13 @@ pub fn run_core(options: RustdocOptions) -> (clean::Crate, RenderInfo, RenderOpt
617617

618618
info!("Executing passes");
619619

620-
for pass in &passes {
621-
match passes::find_pass(pass).map(|p| p.pass) {
622-
Some(pass) => krate = pass(krate, &ctxt),
623-
None => error!("unknown pass {}, skipping", *pass),
620+
for pass_name in &passes {
621+
match passes::find_pass(pass_name).map(|p| p.pass) {
622+
Some(pass) => {
623+
debug!("running pass {}", pass_name);
624+
krate = pass(krate, &ctxt);
625+
}
626+
None => error!("unknown pass {}, skipping", *pass_name),
624627
}
625628
}
626629

src/librustdoc/html/item_type.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::clean;
1515
/// module headings. If you are adding to this enum and want to ensure that the sidebar also prints
1616
/// a heading, edit the listing in `html/render.rs`, function `sidebar_module`. This uses an
1717
/// ordering based on a helper function inside `item_module`, in the same file.
18-
#[derive(Copy, PartialEq, Clone, Debug)]
18+
#[derive(Copy, PartialEq, Eq, Clone, Debug, PartialOrd, Ord)]
1919
pub enum ItemType {
2020
Module = 0,
2121
ExternCrate = 1,

src/librustdoc/lib.rs

+12
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ fn opts() -> Vec<RustcOptGroup> {
348348
"generate-redirect-pages",
349349
"Generate extra pages to support legacy URLs and tool links")
350350
}),
351+
unstable("show-coverage", |o| {
352+
o.optflag("",
353+
"show-coverage",
354+
"calculate percentage of public items with documentation")
355+
}),
351356
]
352357
}
353358

@@ -392,7 +397,14 @@ fn main_args(args: &[String]) -> isize {
392397
let diag_opts = (options.error_format,
393398
options.debugging_options.treat_err_as_bug,
394399
options.debugging_options.ui_testing);
400+
let show_coverage = options.show_coverage;
395401
rust_input(options, move |out| {
402+
if show_coverage {
403+
// if we ran coverage, bail early, we don't need to also generate docs at this point
404+
// (also we didn't load in any of the useful passes)
405+
return rustc_driver::EXIT_SUCCESS;
406+
}
407+
396408
let Output { krate, passes, renderinfo, renderopts } = out;
397409
info!("going to format");
398410
let (error_format, treat_err_as_bug, ui_testing) = diag_opts;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
use crate::clean;
2+
use crate::core::DocContext;
3+
use crate::fold::{self, DocFolder};
4+
use crate::passes::Pass;
5+
6+
use syntax::attr;
7+
use syntax_pos::FileName;
8+
9+
use std::collections::BTreeMap;
10+
use std::ops;
11+
12+
pub const CALCULATE_DOC_COVERAGE: Pass = Pass {
13+
name: "calculate-doc-coverage",
14+
pass: calculate_doc_coverage,
15+
description: "counts the number of items with and without documentation",
16+
};
17+
18+
fn calculate_doc_coverage(krate: clean::Crate, _: &DocContext<'_, '_, '_>) -> clean::Crate {
19+
let mut calc = CoverageCalculator::default();
20+
let krate = calc.fold_crate(krate);
21+
22+
calc.print_results();
23+
24+
krate
25+
}
26+
27+
#[derive(Default, Copy, Clone)]
28+
struct ItemCount {
29+
total: u64,
30+
with_docs: u64,
31+
}
32+
33+
impl ItemCount {
34+
fn count_item(&mut self, has_docs: bool) {
35+
self.total += 1;
36+
37+
if has_docs {
38+
self.with_docs += 1;
39+
}
40+
}
41+
42+
fn percentage(&self) -> Option<f64> {
43+
if self.total > 0 {
44+
Some((self.with_docs as f64 * 100.0) / self.total as f64)
45+
} else {
46+
None
47+
}
48+
}
49+
}
50+
51+
impl ops::Sub for ItemCount {
52+
type Output = Self;
53+
54+
fn sub(self, rhs: Self) -> Self {
55+
ItemCount {
56+
total: self.total - rhs.total,
57+
with_docs: self.with_docs - rhs.with_docs,
58+
}
59+
}
60+
}
61+
62+
impl ops::AddAssign for ItemCount {
63+
fn add_assign(&mut self, rhs: Self) {
64+
self.total += rhs.total;
65+
self.with_docs += rhs.with_docs;
66+
}
67+
}
68+
69+
#[derive(Default)]
70+
struct CoverageCalculator {
71+
items: BTreeMap<FileName, ItemCount>,
72+
}
73+
74+
impl CoverageCalculator {
75+
fn print_results(&self) {
76+
let mut total = ItemCount::default();
77+
78+
fn print_table_line() {
79+
println!("+-{0:->35}-+-{0:->10}-+-{0:->10}-+-{0:->10}-+", "");
80+
}
81+
82+
fn print_table_record(name: &str, count: ItemCount, percentage: f64) {
83+
println!("| {:<35} | {:>10} | {:>10} | {:>9.1}% |",
84+
name, count.with_docs, count.total, percentage);
85+
}
86+
87+
print_table_line();
88+
println!("| {:<35} | {:>10} | {:>10} | {:>10} |",
89+
"File", "Documented", "Total", "Percentage");
90+
print_table_line();
91+
92+
for (file, &count) in &self.items {
93+
if let Some(percentage) = count.percentage() {
94+
let mut name = file.to_string();
95+
// if a filename is too long, shorten it so we don't blow out the table
96+
// FIXME(misdreavus): this needs to count graphemes, and probably also track
97+
// double-wide characters...
98+
if name.len() > 35 {
99+
name = "...".to_string() + &name[name.len()-32..];
100+
}
101+
102+
print_table_record(&name, count, percentage);
103+
104+
total += count;
105+
}
106+
}
107+
108+
print_table_line();
109+
print_table_record("Total", total, total.percentage().unwrap_or(0.0));
110+
print_table_line();
111+
}
112+
}
113+
114+
impl fold::DocFolder for CoverageCalculator {
115+
fn fold_item(&mut self, i: clean::Item) -> Option<clean::Item> {
116+
let has_docs = !i.attrs.doc_strings.is_empty();
117+
118+
match i.inner {
119+
_ if !i.def_id.is_local() => {
120+
// non-local items are skipped because they can be out of the users control,
121+
// especially in the case of trait impls, which rustdoc eagerly inlines
122+
return Some(i);
123+
}
124+
clean::StrippedItem(..) => {
125+
// don't count items in stripped modules
126+
return Some(i);
127+
}
128+
clean::ImportItem(..) | clean::ExternCrateItem(..) => {
129+
// docs on `use` and `extern crate` statements are not displayed, so they're not
130+
// worth counting
131+
return Some(i);
132+
}
133+
clean::ImplItem(ref impl_)
134+
if attr::contains_name(&i.attrs.other_attrs, "automatically_derived")
135+
|| impl_.synthetic || impl_.blanket_impl.is_some() =>
136+
{
137+
// built-in derives get the `#[automatically_derived]` attribute, and
138+
// synthetic/blanket impls are made up by rustdoc and can't be documented
139+
// FIXME(misdreavus): need to also find items that came out of a derive macro
140+
return Some(i);
141+
}
142+
clean::ImplItem(ref impl_) => {
143+
if let Some(ref tr) = impl_.trait_ {
144+
debug!("impl {:#} for {:#} in {}", tr, impl_.for_, i.source.filename);
145+
146+
// don't count trait impls, the missing-docs lint doesn't so we shouldn't
147+
// either
148+
return Some(i);
149+
} else {
150+
// inherent impls *can* be documented, and those docs show up, but in most
151+
// cases it doesn't make sense, as all methods on a type are in one single
152+
// impl block
153+
debug!("impl {:#} in {}", impl_.for_, i.source.filename);
154+
}
155+
}
156+
_ => {
157+
debug!("counting {} {:?} in {}", i.type_(), i.name, i.source.filename);
158+
self.items.entry(i.source.filename.clone())
159+
.or_default()
160+
.count_item(has_docs);
161+
}
162+
}
163+
164+
self.fold_item_recur(i)
165+
}
166+
}

src/librustdoc/passes/mod.rs

+23
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ pub use self::collect_trait_impls::COLLECT_TRAIT_IMPLS;
4545
mod check_code_block_syntax;
4646
pub use self::check_code_block_syntax::CHECK_CODE_BLOCK_SYNTAX;
4747

48+
mod calculate_doc_coverage;
49+
pub use self::calculate_doc_coverage::CALCULATE_DOC_COVERAGE;
50+
4851
/// A single pass over the cleaned documentation.
4952
///
5053
/// Runs in the compiler context, so it has access to types and traits and the like.
@@ -67,6 +70,7 @@ pub const PASSES: &'static [Pass] = &[
6770
COLLECT_INTRA_DOC_LINKS,
6871
CHECK_CODE_BLOCK_SYNTAX,
6972
COLLECT_TRAIT_IMPLS,
73+
CALCULATE_DOC_COVERAGE,
7074
];
7175

7276
/// The list of passes run by default.
@@ -94,12 +98,29 @@ pub const DEFAULT_PRIVATE_PASSES: &[&str] = &[
9498
"propagate-doc-cfg",
9599
];
96100

101+
/// The list of default passes run when `--doc-coverage` is passed to rustdoc.
102+
pub const DEFAULT_COVERAGE_PASSES: &'static [&'static str] = &[
103+
"collect-trait-impls",
104+
"strip-hidden",
105+
"strip-private",
106+
"calculate-doc-coverage",
107+
];
108+
109+
/// The list of default passes run when `--doc-coverage --document-private-items` is passed to
110+
/// rustdoc.
111+
pub const PRIVATE_COVERAGE_PASSES: &'static [&'static str] = &[
112+
"collect-trait-impls",
113+
"calculate-doc-coverage",
114+
];
115+
97116
/// A shorthand way to refer to which set of passes to use, based on the presence of
98117
/// `--no-defaults` or `--document-private-items`.
99118
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
100119
pub enum DefaultPassOption {
101120
Default,
102121
Private,
122+
Coverage,
123+
PrivateCoverage,
103124
None,
104125
}
105126

@@ -108,6 +129,8 @@ pub fn defaults(default_set: DefaultPassOption) -> &'static [&'static str] {
108129
match default_set {
109130
DefaultPassOption::Default => DEFAULT_PASSES,
110131
DefaultPassOption::Private => DEFAULT_PRIVATE_PASSES,
132+
DefaultPassOption::Coverage => DEFAULT_COVERAGE_PASSES,
133+
DefaultPassOption::PrivateCoverage => PRIVATE_COVERAGE_PASSES,
111134
DefaultPassOption::None => &[],
112135
}
113136
}

0 commit comments

Comments
 (0)