Skip to content

Commit

Permalink
add option to calculate documentation coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
QuietMisdreavus committed Feb 28, 2019
1 parent 1999a22 commit 009c91a
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 4 deletions.
21 changes: 21 additions & 0 deletions src/doc/rustdoc/src/unstable-features.md
Expand Up @@ -428,3 +428,24 @@ $ rustdoc src/lib.rs --test -Z unstable-options --persist-doctests target/rustdo
This flag allows you to keep doctest executables around after they're compiled or run.
Usually, rustdoc will immediately discard a compiled doctest after it's been tested, but
with this option, you can keep those binaries around for farther testing.

### `--show-coverage`: calculate the percentage of items with documentation

Using this flag looks like this:

```bash
$ rustdoc src/lib.rs -Z unstable-options --show-coverage
```

If you want to determine how many items in your crate are documented, pass this flag to rustdoc.
When it receives this flag, it will count the public items in your crate that have documentation,
and print out the counts and a percentage instead of generating docs.

Some methodology notes about what rustdoc counts in this metric:

* Rustdoc will only count items from your crate (i.e. items re-exported from other crates don't
count).
* Since trait implementations can inherit documentation from their trait, it will count trait impl
blocks separately, and show totals both with and without trait impls included.
* Inherent impl blocks are not counted, even though their doc comments are displayed, because the
common pattern in Rust code is to write all inherent methods into the same impl block.
12 changes: 12 additions & 0 deletions src/librustdoc/config.rs
Expand Up @@ -85,6 +85,9 @@ pub struct Options {
/// Whether to display warnings during doc generation or while gathering doctests. By default,
/// all non-rustdoc-specific lints are allowed when generating docs.
pub display_warnings: bool,
/// Whether to run the `calculate-doc-coverage` pass, which counts the number of public items
/// with and without documentation.
pub show_coverage: bool,

// Options that alter generated documentation pages

Expand Down Expand Up @@ -128,6 +131,7 @@ impl fmt::Debug for Options {
.field("default_passes", &self.default_passes)
.field("manual_passes", &self.manual_passes)
.field("display_warnings", &self.display_warnings)
.field("show_coverage", &self.show_coverage)
.field("crate_version", &self.crate_version)
.field("render_options", &self.render_options)
.finish()
Expand Down Expand Up @@ -224,6 +228,10 @@ impl Options {
for &name in passes::DEFAULT_PRIVATE_PASSES {
println!("{:>20}", name);
}
println!("\nPasses run with `--show-coverage`:");
for &name in passes::DEFAULT_COVERAGE_PASSES {
println!("{:>20}", name);
}
return Err(0);
}

Expand Down Expand Up @@ -415,12 +423,15 @@ impl Options {

let default_passes = if matches.opt_present("no-defaults") {
passes::DefaultPassOption::None
} else if matches.opt_present("show-coverage") {
passes::DefaultPassOption::Coverage
} else if matches.opt_present("document-private-items") {
passes::DefaultPassOption::Private
} else {
passes::DefaultPassOption::Default
};
let manual_passes = matches.opt_strs("passes");
let show_coverage = matches.opt_present("show-coverage");

let crate_name = matches.opt_str("crate-name");
let playground_url = matches.opt_str("playground-url");
Expand Down Expand Up @@ -463,6 +474,7 @@ impl Options {
default_passes,
manual_passes,
display_warnings,
show_coverage,
crate_version,
persist_doctests,
render_options: RenderOptions {
Expand Down
11 changes: 7 additions & 4 deletions src/librustdoc/core.rs
Expand Up @@ -605,10 +605,13 @@ pub fn run_core(options: RustdocOptions) -> (clean::Crate, RenderInfo, RenderOpt

info!("Executing passes");

for pass in &passes {
match passes::find_pass(pass).map(|p| p.pass) {
Some(pass) => krate = pass(krate, &ctxt),
None => error!("unknown pass {}, skipping", *pass),
for pass_name in &passes {
match passes::find_pass(pass_name).map(|p| p.pass) {
Some(pass) => {
debug!("running pass {}", pass_name);
krate = pass(krate, &ctxt);
}
None => error!("unknown pass {}, skipping", *pass_name),
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/librustdoc/lib.rs
Expand Up @@ -347,6 +347,11 @@ fn opts() -> Vec<RustcOptGroup> {
"generate-redirect-pages",
"Generate extra pages to support legacy URLs and tool links")
}),
unstable("show-coverage", |o| {
o.optflag("",
"show-coverage",
"calculate percentage of public items with documentation")
}),
]
}

Expand Down Expand Up @@ -391,7 +396,14 @@ fn main_args(args: &[String]) -> isize {
let diag_opts = (options.error_format,
options.debugging_options.treat_err_as_bug,
options.debugging_options.ui_testing);
let show_coverage = options.show_coverage;
rust_input(options, move |out| {
if show_coverage {
// if we ran coverage, bail early, we don't need to also generate docs at this point
// (also we didn't load in any of the useful passes)
return rustc_driver::EXIT_SUCCESS;
}

let Output { krate, passes, renderinfo, renderopts } = out;
info!("going to format");
let (error_format, treat_err_as_bug, ui_testing) = diag_opts;
Expand Down
101 changes: 101 additions & 0 deletions src/librustdoc/passes/calculate_doc_coverage.rs
@@ -0,0 +1,101 @@
use crate::clean;
use crate::core::DocContext;
use crate::fold::{self, DocFolder};
use crate::passes::Pass;

use syntax::attr;

pub const CALCULATE_DOC_COVERAGE: Pass = Pass {
name: "calculate-doc-coverage",
pass: calculate_doc_coverage,
description: "counts the number of items with and without documentation",
};

fn calculate_doc_coverage(krate: clean::Crate, _: &DocContext<'_, '_, '_>) -> clean::Crate {
let mut calc = CoverageCalculator::default();
let krate = calc.fold_crate(krate);

let total_minus_traits = calc.total - calc.total_trait_impls;
let docs_minus_traits = calc.with_docs - calc.trait_impls_with_docs;

print!("Rustdoc found {}/{} items with documentation", calc.with_docs, calc.total);
println!(" ({}/{} not counting trait impls)", docs_minus_traits, total_minus_traits);

if calc.total > 0 {
let percentage = (calc.with_docs as f64 * 100.0) / calc.total as f64;
let percentage_minus_traits =
(docs_minus_traits as f64 * 100.0) / total_minus_traits as f64;
println!(" Score: {:.1}% ({:.1}% not counting trait impls)",
percentage, percentage_minus_traits);
}

krate
}

#[derive(Default)]
struct CoverageCalculator {
total: usize,
with_docs: usize,
total_trait_impls: usize,
trait_impls_with_docs: usize,
}

impl fold::DocFolder for CoverageCalculator {
fn fold_item(&mut self, i: clean::Item) -> Option<clean::Item> {
match i.inner {
clean::StrippedItem(..) => {}
clean::ImplItem(ref impl_)
if attr::contains_name(&i.attrs.other_attrs, "automatically_derived")
|| impl_.synthetic || impl_.blanket_impl.is_some() =>
{
// skip counting anything inside these impl blocks
// FIXME(misdreavus): need to also find items that came out of a derive macro
return Some(i);
}
// non-local items are skipped because they can be out of the users control, especially
// in the case of trait impls, which rustdoc eagerly inlines
_ => if i.def_id.is_local() {
let has_docs = !i.attrs.doc_strings.is_empty();

if let clean::ImplItem(ref i) = i.inner {
if let Some(ref tr) = i.trait_ {
debug!("counting impl {:#} for {:#}", tr, i.for_);

self.total += 1;
if has_docs {
self.with_docs += 1;
}

// trait impls inherit their docs from the trait definition, so documenting
// them can be considered optional

self.total_trait_impls += 1;
if has_docs {
self.trait_impls_with_docs += 1;
}

for it in &i.items {
self.total_trait_impls += 1;
if !it.attrs.doc_strings.is_empty() {
self.trait_impls_with_docs += 1;
}
}
} else {
// inherent impls *can* be documented, and those docs show up, but in most
// cases it doesn't make sense, as all methods on a type are in one single
// impl block
debug!("not counting impl {:#}", i.for_);
}
} else {
debug!("counting {} {:?}", i.type_(), i.name);
self.total += 1;
if has_docs {
self.with_docs += 1;
}
}
}
}

self.fold_item_recur(i)
}
}
14 changes: 14 additions & 0 deletions src/librustdoc/passes/mod.rs
Expand Up @@ -45,6 +45,9 @@ pub use self::collect_trait_impls::COLLECT_TRAIT_IMPLS;
mod check_code_block_syntax;
pub use self::check_code_block_syntax::CHECK_CODE_BLOCK_SYNTAX;

mod calculate_doc_coverage;
pub use self::calculate_doc_coverage::CALCULATE_DOC_COVERAGE;

/// A single pass over the cleaned documentation.
///
/// Runs in the compiler context, so it has access to types and traits and the like.
Expand All @@ -67,6 +70,7 @@ pub const PASSES: &'static [Pass] = &[
COLLECT_INTRA_DOC_LINKS,
CHECK_CODE_BLOCK_SYNTAX,
COLLECT_TRAIT_IMPLS,
CALCULATE_DOC_COVERAGE,
];

/// The list of passes run by default.
Expand Down Expand Up @@ -94,12 +98,21 @@ pub const DEFAULT_PRIVATE_PASSES: &[&str] = &[
"propagate-doc-cfg",
];

/// The list of default passes run when `--doc-coverage` is passed to rustdoc.
pub const DEFAULT_COVERAGE_PASSES: &'static [&'static str] = &[
"collect-trait-impls",
"strip-hidden",
"strip-private",
"calculate-doc-coverage",
];

/// A shorthand way to refer to which set of passes to use, based on the presence of
/// `--no-defaults` or `--document-private-items`.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum DefaultPassOption {
Default,
Private,
Coverage,
None,
}

Expand All @@ -108,6 +121,7 @@ pub fn defaults(default_set: DefaultPassOption) -> &'static [&'static str] {
match default_set {
DefaultPassOption::Default => DEFAULT_PASSES,
DefaultPassOption::Private => DEFAULT_PRIVATE_PASSES,
DefaultPassOption::Coverage => DEFAULT_COVERAGE_PASSES,
DefaultPassOption::None => &[],
}
}
Expand Down

0 comments on commit 009c91a

Please sign in to comment.