]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/passes/calculate_doc_coverage.rs
0a836f46c0eb85bc96bc861e94f3af109e58eb1c
[rust.git] / src / librustdoc / passes / calculate_doc_coverage.rs
1 use crate::clean;
2 use crate::config::OutputFormat;
3 use crate::core::DocContext;
4 use crate::fold::{self, DocFolder};
5 use crate::html::markdown::{find_testable_code, ErrorCodes};
6 use crate::passes::doc_test_lints::Tests;
7 use crate::passes::Pass;
8 use rustc_span::symbol::sym;
9 use rustc_span::FileName;
10 use serde::Serialize;
11
12 use std::collections::BTreeMap;
13 use std::ops;
14
15 pub const CALCULATE_DOC_COVERAGE: Pass = Pass {
16     name: "calculate-doc-coverage",
17     run: calculate_doc_coverage,
18     description: "counts the number of items with and without documentation",
19 };
20
21 fn calculate_doc_coverage(krate: clean::Crate, ctx: &DocContext<'_>) -> clean::Crate {
22     let mut calc = CoverageCalculator::new();
23     let krate = calc.fold_crate(krate);
24
25     calc.print_results(ctx.renderinfo.borrow().output_format);
26
27     krate
28 }
29
30 #[derive(Default, Copy, Clone, Serialize)]
31 struct ItemCount {
32     total: u64,
33     with_docs: u64,
34     with_examples: u64,
35 }
36
37 impl ItemCount {
38     fn count_item(&mut self, has_docs: bool, has_doc_example: bool) {
39         self.total += 1;
40
41         if has_docs {
42             self.with_docs += 1;
43         }
44         if has_doc_example {
45             self.with_examples += 1;
46         }
47     }
48
49     fn percentage(&self) -> Option<f64> {
50         if self.total > 0 {
51             Some((self.with_docs as f64 * 100.0) / self.total as f64)
52         } else {
53             None
54         }
55     }
56
57     fn examples_percentage(&self) -> Option<f64> {
58         if self.total > 0 {
59             Some((self.with_examples as f64 * 100.0) / self.total as f64)
60         } else {
61             None
62         }
63     }
64 }
65
66 impl ops::Sub for ItemCount {
67     type Output = Self;
68
69     fn sub(self, rhs: Self) -> Self {
70         ItemCount {
71             total: self.total - rhs.total,
72             with_docs: self.with_docs - rhs.with_docs,
73             with_examples: self.with_examples - rhs.with_examples,
74         }
75     }
76 }
77
78 impl ops::AddAssign for ItemCount {
79     fn add_assign(&mut self, rhs: Self) {
80         self.total += rhs.total;
81         self.with_docs += rhs.with_docs;
82         self.with_examples += rhs.with_examples;
83     }
84 }
85
86 struct CoverageCalculator {
87     items: BTreeMap<FileName, ItemCount>,
88 }
89
90 fn limit_filename_len(filename: String) -> String {
91     let nb_chars = filename.chars().count();
92     if nb_chars > 35 {
93         "...".to_string()
94             + &filename[filename.char_indices().nth(nb_chars - 32).map(|x| x.0).unwrap_or(0)..]
95     } else {
96         filename
97     }
98 }
99
100 impl CoverageCalculator {
101     fn new() -> CoverageCalculator {
102         CoverageCalculator { items: Default::default() }
103     }
104
105     fn to_json(&self) -> String {
106         serde_json::to_string(
107             &self
108                 .items
109                 .iter()
110                 .map(|(k, v)| (k.to_string(), v))
111                 .collect::<BTreeMap<String, &ItemCount>>(),
112         )
113         .expect("failed to convert JSON data to string")
114     }
115
116     fn print_results(&self, output_format: Option<OutputFormat>) {
117         if output_format.map(|o| o.is_json()).unwrap_or_else(|| false) {
118             println!("{}", self.to_json());
119             return;
120         }
121         let mut total = ItemCount::default();
122
123         fn print_table_line() {
124             println!("+-{0:->35}-+-{0:->10}-+-{0:->10}-+-{0:->10}-+-{0:->10}-+-{0:->10}-+", "");
125         }
126
127         fn print_table_record(
128             name: &str,
129             count: ItemCount,
130             percentage: f64,
131             examples_percentage: f64,
132         ) {
133             println!(
134                 "| {:<35} | {:>10} | {:>10} | {:>9.1}% | {:>10} | {:>9.1}% |",
135                 name,
136                 count.with_docs,
137                 count.total,
138                 percentage,
139                 count.with_examples,
140                 examples_percentage,
141             );
142         }
143
144         print_table_line();
145         println!(
146             "| {:<35} | {:>10} | {:>10} | {:>10} | {:>10} | {:>10} |",
147             "File", "Documented", "Total", "Percentage", "Examples", "Percentage",
148         );
149         print_table_line();
150
151         for (file, &count) in &self.items {
152             if let (Some(percentage), Some(examples_percentage)) =
153                 (count.percentage(), count.examples_percentage())
154             {
155                 print_table_record(
156                     &limit_filename_len(file.to_string()),
157                     count,
158                     percentage,
159                     examples_percentage,
160                 );
161
162                 total += count;
163             }
164         }
165
166         print_table_line();
167         print_table_record(
168             "Total",
169             total,
170             total.percentage().unwrap_or(0.0),
171             total.examples_percentage().unwrap_or(0.0),
172         );
173         print_table_line();
174     }
175 }
176
177 impl fold::DocFolder for CoverageCalculator {
178     fn fold_item(&mut self, i: clean::Item) -> Option<clean::Item> {
179         let has_docs = !i.attrs.doc_strings.is_empty();
180         let mut tests = Tests { found_tests: 0 };
181
182         find_testable_code(
183             &i.attrs.doc_strings.iter().map(|d| d.as_str()).collect::<Vec<_>>().join("\n"),
184             &mut tests,
185             ErrorCodes::No,
186             false,
187             None,
188         );
189
190         let has_doc_example = tests.found_tests != 0;
191
192         match i.inner {
193             _ if !i.def_id.is_local() => {
194                 // non-local items are skipped because they can be out of the users control,
195                 // especially in the case of trait impls, which rustdoc eagerly inlines
196                 return Some(i);
197             }
198             clean::StrippedItem(..) => {
199                 // don't count items in stripped modules
200                 return Some(i);
201             }
202             clean::ImportItem(..) | clean::ExternCrateItem(..) => {
203                 // docs on `use` and `extern crate` statements are not displayed, so they're not
204                 // worth counting
205                 return Some(i);
206             }
207             clean::ImplItem(ref impl_)
208                 if i.attrs
209                     .other_attrs
210                     .iter()
211                     .any(|item| item.has_name(sym::automatically_derived))
212                     || impl_.synthetic
213                     || impl_.blanket_impl.is_some() =>
214             {
215                 // built-in derives get the `#[automatically_derived]` attribute, and
216                 // synthetic/blanket impls are made up by rustdoc and can't be documented
217                 // FIXME(misdreavus): need to also find items that came out of a derive macro
218                 return Some(i);
219             }
220             clean::ImplItem(ref impl_) => {
221                 if let Some(ref tr) = impl_.trait_ {
222                     debug!(
223                         "impl {:#} for {:#} in {}",
224                         tr.print(),
225                         impl_.for_.print(),
226                         i.source.filename
227                     );
228
229                     // don't count trait impls, the missing-docs lint doesn't so we shouldn't
230                     // either
231                     return Some(i);
232                 } else {
233                     // inherent impls *can* be documented, and those docs show up, but in most
234                     // cases it doesn't make sense, as all methods on a type are in one single
235                     // impl block
236                     debug!("impl {:#} in {}", impl_.for_.print(), i.source.filename);
237                 }
238             }
239             _ => {
240                 debug!("counting {:?} {:?} in {}", i.type_(), i.name, i.source.filename);
241                 self.items
242                     .entry(i.source.filename.clone())
243                     .or_default()
244                     .count_item(has_docs, has_doc_example);
245             }
246         }
247
248         self.fold_item_recur(i)
249     }
250 }