-
Notifications
You must be signed in to change notification settings - Fork 13.4k
rustdoc: add option to calculate "documentation coverage" #58626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
009c91a
9e98a25
fc94593
95500c0
5eb1ab5
a3a2559
3ce19b4
63bdd29
80b4919
1b63543
74cf1ad
515dbe7
e28cf74
3df0b89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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") | ||
}), | ||
] | ||
} | ||
|
||
|
@@ -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| { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it necessary to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems a bit overkill but let's go along for now... |
||
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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
use crate::clean; | ||
use crate::core::DocContext; | ||
use crate::fold::{self, DocFolder}; | ||
use crate::passes::Pass; | ||
|
||
use syntax::attr; | ||
use syntax_pos::FileName; | ||
|
||
use std::collections::BTreeMap; | ||
use std::fmt; | ||
use std::ops; | ||
|
||
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); | ||
|
||
calc.print_results(); | ||
|
||
krate | ||
} | ||
|
||
#[derive(Default, Copy, Clone)] | ||
struct ItemCount { | ||
total: u64, | ||
with_docs: u64, | ||
} | ||
|
||
impl ItemCount { | ||
fn count_item(&mut self, has_docs: bool) { | ||
self.total += 1; | ||
|
||
if has_docs { | ||
self.with_docs += 1; | ||
} | ||
} | ||
|
||
fn percentage(&self) -> Option<f64> { | ||
if self.total > 0 { | ||
Some((self.with_docs as f64 * 100.0) / self.total as f64) | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
impl ops::Sub for ItemCount { | ||
type Output = Self; | ||
|
||
fn sub(self, rhs: Self) -> Self { | ||
ItemCount { | ||
total: self.total - rhs.total, | ||
with_docs: self.with_docs - rhs.with_docs, | ||
} | ||
} | ||
} | ||
|
||
impl ops::AddAssign for ItemCount { | ||
fn add_assign(&mut self, rhs: Self) { | ||
self.total += rhs.total; | ||
self.with_docs += rhs.with_docs; | ||
} | ||
} | ||
|
||
impl fmt::Display for ItemCount { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "{}/{}", self.with_docs, self.total) | ||
QuietMisdreavus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
#[derive(Default)] | ||
struct CoverageCalculator { | ||
items: BTreeMap<FileName, ItemCount>, | ||
} | ||
|
||
impl CoverageCalculator { | ||
fn print_results(&self) { | ||
let mut total = ItemCount::default(); | ||
|
||
fn print_table_line() { | ||
println!("+-{0:->35}-+-{0:->10}-+-{0:->10}-+-{0:->10}-+", ""); | ||
} | ||
|
||
fn print_table_record(name: &str, count: ItemCount, percentage: f64) { | ||
println!("| {:<35} | {:>10} | {:>10} | {:>9.1}% |", | ||
name, count.with_docs, count.total, percentage); | ||
} | ||
|
||
print_table_line(); | ||
println!("| {:<35} | {:>10} | {:>10} | {:>10} |", | ||
"File", "Documented", "Total", "Percentage"); | ||
print_table_line(); | ||
|
||
for (file, &count) in &self.items { | ||
if let Some(percentage) = count.percentage() { | ||
let mut name = file.to_string(); | ||
// if a filename is too long, shorten it so we don't blow out the table | ||
// FIXME(misdreavus): this needs to count graphemes, and probably also track | ||
// double-wide characters... | ||
if name.len() > 35 { | ||
name = "...".to_string() + &name[name.len()-32..]; | ||
} | ||
|
||
print_table_record(&name, count, percentage); | ||
|
||
total += count; | ||
} | ||
} | ||
|
||
print_table_line(); | ||
print_table_record("Total", total, total.percentage().unwrap_or(0.0)); | ||
print_table_line(); | ||
} | ||
} | ||
|
||
impl fold::DocFolder for CoverageCalculator { | ||
fn fold_item(&mut self, i: clean::Item) -> Option<clean::Item> { | ||
let has_docs = !i.attrs.doc_strings.is_empty(); | ||
|
||
match i.inner { | ||
_ if !i.def_id.is_local() => { | ||
// 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 | ||
return Some(i); | ||
} | ||
clean::StrippedItem(..) => { | ||
// don't count items in stripped modules | ||
return Some(i); | ||
} | ||
clean::ImportItem(..) | clean::ExternCrateItem(..) => { | ||
// docs on `use` and `extern crate` statements are not displayed, so they're not | ||
// worth counting | ||
return Some(i); | ||
} | ||
clean::ImplItem(ref impl_) | ||
if attr::contains_name(&i.attrs.other_attrs, "automatically_derived") | ||
|| impl_.synthetic || impl_.blanket_impl.is_some() => | ||
{ | ||
// built-in derives get the `#[automatically_derived]` attribute, and | ||
// synthetic/blanket impls are made up by rustdoc and can't be documented | ||
// FIXME(misdreavus): need to also find items that came out of a derive macro | ||
return Some(i); | ||
} | ||
clean::ImplItem(ref impl_) => { | ||
if let Some(ref tr) = impl_.trait_ { | ||
debug!("impl {:#} for {:#} in {}", tr, impl_.for_, i.source.filename); | ||
|
||
// don't count trait impls, the missing-docs lint doesn't so we shouldn't | ||
// either | ||
return Some(i); | ||
} 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!("impl {:#} in {}", impl_.for_, i.source.filename); | ||
} | ||
} | ||
_ => { | ||
debug!("counting {} {:?} in {}", i.type_(), i.name, i.source.filename); | ||
self.items.entry(i.source.filename.clone()) | ||
.or_default() | ||
.count_item(has_docs); | ||
} | ||
} | ||
|
||
self.fold_item_recur(i) | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.