Skip to content

Commit 009c91a

Browse files
add option to calculate documentation coverage
1 parent 1999a22 commit 009c91a

File tree

6 files changed

+167
-4
lines changed

6 files changed

+167
-4
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,24 @@ $ 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+
* Since trait implementations can inherit documentation from their trait, it will count trait impl
449+
blocks separately, and show totals both with and without trait impls included.
450+
* Inherent impl blocks are not counted, even though their doc comments are displayed, because the
451+
common pattern in Rust code is to write all inherent methods into the same impl block.

src/librustdoc/config.rs

Lines changed: 12 additions & 0 deletions
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,10 @@ impl Options {
224228
for &name in passes::DEFAULT_PRIVATE_PASSES {
225229
println!("{:>20}", name);
226230
}
231+
println!("\nPasses run with `--show-coverage`:");
232+
for &name in passes::DEFAULT_COVERAGE_PASSES {
233+
println!("{:>20}", name);
234+
}
227235
return Err(0);
228236
}
229237

@@ -415,12 +423,15 @@ impl Options {
415423

416424
let default_passes = if matches.opt_present("no-defaults") {
417425
passes::DefaultPassOption::None
426+
} else if matches.opt_present("show-coverage") {
427+
passes::DefaultPassOption::Coverage
418428
} else if matches.opt_present("document-private-items") {
419429
passes::DefaultPassOption::Private
420430
} else {
421431
passes::DefaultPassOption::Default
422432
};
423433
let manual_passes = matches.opt_strs("passes");
434+
let show_coverage = matches.opt_present("show-coverage");
424435

425436
let crate_name = matches.opt_str("crate-name");
426437
let playground_url = matches.opt_str("playground-url");
@@ -463,6 +474,7 @@ impl Options {
463474
default_passes,
464475
manual_passes,
465476
display_warnings,
477+
show_coverage,
466478
crate_version,
467479
persist_doctests,
468480
render_options: RenderOptions {

src/librustdoc/core.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -605,10 +605,13 @@ pub fn run_core(options: RustdocOptions) -> (clean::Crate, RenderInfo, RenderOpt
605605

606606
info!("Executing passes");
607607

608-
for pass in &passes {
609-
match passes::find_pass(pass).map(|p| p.pass) {
610-
Some(pass) => krate = pass(krate, &ctxt),
611-
None => error!("unknown pass {}, skipping", *pass),
608+
for pass_name in &passes {
609+
match passes::find_pass(pass_name).map(|p| p.pass) {
610+
Some(pass) => {
611+
debug!("running pass {}", pass_name);
612+
krate = pass(krate, &ctxt);
613+
}
614+
None => error!("unknown pass {}, skipping", *pass_name),
612615
}
613616
}
614617

src/librustdoc/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ fn opts() -> Vec<RustcOptGroup> {
347347
"generate-redirect-pages",
348348
"Generate extra pages to support legacy URLs and tool links")
349349
}),
350+
unstable("show-coverage", |o| {
351+
o.optflag("",
352+
"show-coverage",
353+
"calculate percentage of public items with documentation")
354+
}),
350355
]
351356
}
352357

@@ -391,7 +396,14 @@ fn main_args(args: &[String]) -> isize {
391396
let diag_opts = (options.error_format,
392397
options.debugging_options.treat_err_as_bug,
393398
options.debugging_options.ui_testing);
399+
let show_coverage = options.show_coverage;
394400
rust_input(options, move |out| {
401+
if show_coverage {
402+
// if we ran coverage, bail early, we don't need to also generate docs at this point
403+
// (also we didn't load in any of the useful passes)
404+
return rustc_driver::EXIT_SUCCESS;
405+
}
406+
395407
let Output { krate, passes, renderinfo, renderopts } = out;
396408
info!("going to format");
397409
let (error_format, treat_err_as_bug, ui_testing) = diag_opts;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
8+
pub const CALCULATE_DOC_COVERAGE: Pass = Pass {
9+
name: "calculate-doc-coverage",
10+
pass: calculate_doc_coverage,
11+
description: "counts the number of items with and without documentation",
12+
};
13+
14+
fn calculate_doc_coverage(krate: clean::Crate, _: &DocContext<'_, '_, '_>) -> clean::Crate {
15+
let mut calc = CoverageCalculator::default();
16+
let krate = calc.fold_crate(krate);
17+
18+
let total_minus_traits = calc.total - calc.total_trait_impls;
19+
let docs_minus_traits = calc.with_docs - calc.trait_impls_with_docs;
20+
21+
print!("Rustdoc found {}/{} items with documentation", calc.with_docs, calc.total);
22+
println!(" ({}/{} not counting trait impls)", docs_minus_traits, total_minus_traits);
23+
24+
if calc.total > 0 {
25+
let percentage = (calc.with_docs as f64 * 100.0) / calc.total as f64;
26+
let percentage_minus_traits =
27+
(docs_minus_traits as f64 * 100.0) / total_minus_traits as f64;
28+
println!(" Score: {:.1}% ({:.1}% not counting trait impls)",
29+
percentage, percentage_minus_traits);
30+
}
31+
32+
krate
33+
}
34+
35+
#[derive(Default)]
36+
struct CoverageCalculator {
37+
total: usize,
38+
with_docs: usize,
39+
total_trait_impls: usize,
40+
trait_impls_with_docs: usize,
41+
}
42+
43+
impl fold::DocFolder for CoverageCalculator {
44+
fn fold_item(&mut self, i: clean::Item) -> Option<clean::Item> {
45+
match i.inner {
46+
clean::StrippedItem(..) => {}
47+
clean::ImplItem(ref impl_)
48+
if attr::contains_name(&i.attrs.other_attrs, "automatically_derived")
49+
|| impl_.synthetic || impl_.blanket_impl.is_some() =>
50+
{
51+
// skip counting anything inside these impl blocks
52+
// FIXME(misdreavus): need to also find items that came out of a derive macro
53+
return Some(i);
54+
}
55+
// non-local items are skipped because they can be out of the users control, especially
56+
// in the case of trait impls, which rustdoc eagerly inlines
57+
_ => if i.def_id.is_local() {
58+
let has_docs = !i.attrs.doc_strings.is_empty();
59+
60+
if let clean::ImplItem(ref i) = i.inner {
61+
if let Some(ref tr) = i.trait_ {
62+
debug!("counting impl {:#} for {:#}", tr, i.for_);
63+
64+
self.total += 1;
65+
if has_docs {
66+
self.with_docs += 1;
67+
}
68+
69+
// trait impls inherit their docs from the trait definition, so documenting
70+
// them can be considered optional
71+
72+
self.total_trait_impls += 1;
73+
if has_docs {
74+
self.trait_impls_with_docs += 1;
75+
}
76+
77+
for it in &i.items {
78+
self.total_trait_impls += 1;
79+
if !it.attrs.doc_strings.is_empty() {
80+
self.trait_impls_with_docs += 1;
81+
}
82+
}
83+
} else {
84+
// inherent impls *can* be documented, and those docs show up, but in most
85+
// cases it doesn't make sense, as all methods on a type are in one single
86+
// impl block
87+
debug!("not counting impl {:#}", i.for_);
88+
}
89+
} else {
90+
debug!("counting {} {:?}", i.type_(), i.name);
91+
self.total += 1;
92+
if has_docs {
93+
self.with_docs += 1;
94+
}
95+
}
96+
}
97+
}
98+
99+
self.fold_item_recur(i)
100+
}
101+
}

src/librustdoc/passes/mod.rs

Lines changed: 14 additions & 0 deletions
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,21 @@ 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+
97109
/// A shorthand way to refer to which set of passes to use, based on the presence of
98110
/// `--no-defaults` or `--document-private-items`.
99111
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
100112
pub enum DefaultPassOption {
101113
Default,
102114
Private,
115+
Coverage,
103116
None,
104117
}
105118

@@ -108,6 +121,7 @@ pub fn defaults(default_set: DefaultPassOption) -> &'static [&'static str] {
108121
match default_set {
109122
DefaultPassOption::Default => DEFAULT_PASSES,
110123
DefaultPassOption::Private => DEFAULT_PRIVATE_PASSES,
124+
DefaultPassOption::Coverage => DEFAULT_COVERAGE_PASSES,
111125
DefaultPassOption::None => &[],
112126
}
113127
}

0 commit comments

Comments
 (0)