Skip to main content

rustdoc/
calculate_doc_coverage.rs

1//! Calculates information used for the `--show-coverage` flag.
2//!
3//! More specifically, it counts the number of items with documentation, ones with
4//! "examples" (i.e., non-ignored Rust code blocks) and various totals.
5
6use std::collections::BTreeMap;
7use std::fs::{File, create_dir_all};
8use std::io::{self, BufWriter, Write, stdout};
9use std::ops;
10
11use rustc_hir as hir;
12use rustc_lint::builtin::MISSING_DOCS;
13use rustc_middle::lint::LintLevelSource;
14use rustc_span::{FileName, RemapPathScopeComponents};
15use serde::Serialize;
16use tracing::debug;
17
18use crate::config::{OutputFormat, RenderOptions};
19use crate::core::DocContext;
20use crate::docfs::PathError;
21use crate::error::Error;
22use crate::html::markdown::{ErrorCodes, find_testable_code};
23use crate::passes::check_doc_test_visibility::{Tests, should_have_doc_example};
24use crate::visit::DocVisitor;
25use crate::{clean, try_err};
26
27pub(crate) fn run(
28    krate: &clean::Crate,
29    ctx: &mut DocContext<'_>,
30    options: &RenderOptions,
31) -> Result<(), Error> {
32    let is_json = ctx.output_format == OutputFormat::CoverageJson;
33    let tcx = ctx.tcx;
34    let mut calc = CoverageCalculator { items: Default::default(), ctx };
35    calc.visit_crate(&krate);
36
37    if options.output_to_stdout {
38        calc.print_results(BufWriter::new(stdout().lock()))
39            .map_err(|error| Error::new(error, "<stdout>"))
40    } else {
41        let out_dir = &options.output;
42        try_err!(create_dir_all(out_dir), out_dir);
43        let name = krate.name(tcx);
44        let mut out_file = out_dir.join(name.as_str());
45        out_file.set_extension(if is_json { "json" } else { "txt" });
46        let buf = try_err!(File::create_buffered(&out_file), out_file);
47        calc.print_results(buf).map_err(|error| Error::new(error, &out_file))?;
48        println!("Generated output into {out_file:?}");
49        Ok(())
50    }
51}
52
53#[derive(Default, Copy, Clone, Serialize, Debug)]
54struct ItemCount {
55    total: u64,
56    with_docs: u64,
57    total_examples: u64,
58    with_examples: u64,
59}
60
61impl ItemCount {
62    fn count_item(
63        &mut self,
64        has_docs: bool,
65        has_doc_example: bool,
66        should_have_doc_examples: bool,
67        should_have_docs: bool,
68    ) {
69        if has_docs || should_have_docs {
70            self.total += 1;
71        }
72
73        if has_docs {
74            self.with_docs += 1;
75        }
76        if should_have_doc_examples || has_doc_example {
77            self.total_examples += 1;
78        }
79        if has_doc_example {
80            self.with_examples += 1;
81        }
82    }
83
84    fn percentage(&self) -> Option<f64> {
85        if self.total > 0 {
86            Some((self.with_docs as f64 * 100.0) / self.total as f64)
87        } else {
88            None
89        }
90    }
91
92    fn examples_percentage(&self) -> Option<f64> {
93        if self.total_examples > 0 {
94            Some((self.with_examples as f64 * 100.0) / self.total_examples as f64)
95        } else {
96            None
97        }
98    }
99}
100
101impl ops::Sub for ItemCount {
102    type Output = Self;
103
104    fn sub(self, rhs: Self) -> Self {
105        ItemCount {
106            total: self.total - rhs.total,
107            with_docs: self.with_docs - rhs.with_docs,
108            total_examples: self.total_examples - rhs.total_examples,
109            with_examples: self.with_examples - rhs.with_examples,
110        }
111    }
112}
113
114impl ops::AddAssign for ItemCount {
115    fn add_assign(&mut self, rhs: Self) {
116        self.total += rhs.total;
117        self.with_docs += rhs.with_docs;
118        self.total_examples += rhs.total_examples;
119        self.with_examples += rhs.with_examples;
120    }
121}
122
123struct CoverageCalculator<'a, 'b> {
124    items: BTreeMap<FileName, ItemCount>,
125    ctx: &'a mut DocContext<'b>,
126}
127
128fn limit_filename_len(filename: String) -> String {
129    let nb_chars = filename.chars().count();
130    if nb_chars > 35 {
131        "...".to_string()
132            + &filename[filename.char_indices().nth(nb_chars - 32).map(|x| x.0).unwrap_or(0)..]
133    } else {
134        filename
135    }
136}
137
138impl CoverageCalculator<'_, '_> {
139    fn to_json(&self) -> String {
140        serde_json::to_string(
141            &self
142                .items
143                .iter()
144                .map(|(k, v)| (k.display(RemapPathScopeComponents::COVERAGE).to_string(), v))
145                .collect::<BTreeMap<String, &ItemCount>>(),
146        )
147        .expect("failed to convert JSON data to string")
148    }
149
150    fn print_results(&self, mut buf: impl Write) -> io::Result<()> {
151        let output_format = self.ctx.output_format;
152        if output_format == OutputFormat::CoverageJson {
153            return writeln!(buf, "{}", self.to_json());
154        }
155        let mut total = ItemCount::default();
156
157        fn print_table_line(buf: &mut impl Write) -> io::Result<()> {
158            writeln!(buf, "+-{0:->35}-+-{0:->10}-+-{0:->10}-+-{0:->10}-+-{0:->10}-+", "")
159        }
160
161        fn print_table_record(
162            buf: &mut impl Write,
163            name: &str,
164            count: ItemCount,
165            percentage: f64,
166            examples_percentage: f64,
167        ) -> io::Result<()> {
168            writeln!(
169                buf,
170                "| {name:<35} | {with_docs:>10} | {percentage:>9.1}% | {with_examples:>10} | \
171                 {examples_percentage:>9.1}% |",
172                with_docs = count.with_docs,
173                with_examples = count.with_examples,
174            )
175        }
176
177        print_table_line(&mut buf)?;
178        writeln!(
179            buf,
180            "| {:<35} | {:>10} | {:>10} | {:>10} | {:>10} |",
181            "File", "Documented", "Percentage", "Examples", "Percentage",
182        )?;
183        print_table_line(&mut buf)?;
184
185        for (file, &count) in &self.items {
186            if let Some(percentage) = count.percentage() {
187                print_table_record(
188                    &mut buf,
189                    &limit_filename_len(
190                        file.display(RemapPathScopeComponents::COVERAGE).to_string(),
191                    ),
192                    count,
193                    percentage,
194                    count.examples_percentage().unwrap_or(0.),
195                )?;
196
197                total += count;
198            }
199        }
200
201        print_table_line(&mut buf)?;
202        print_table_record(
203            &mut buf,
204            "Total",
205            total,
206            total.percentage().unwrap_or(0.0),
207            total.examples_percentage().unwrap_or(0.0),
208        )?;
209        print_table_line(&mut buf)
210    }
211}
212
213impl DocVisitor<'_> for CoverageCalculator<'_, '_> {
214    fn visit_item(&mut self, i: &clean::Item) {
215        if !i.item_id.is_local() {
216            // non-local items are skipped because they can be out of the users control,
217            // especially in the case of trait impls, which rustdoc eagerly inlines
218            return;
219        }
220
221        match i.kind {
222            clean::StrippedItem(..) => {
223                // don't count items in stripped modules
224                return;
225            }
226            clean::PlaceholderImplItem => {
227                // The "real" impl items are handled below.
228                return;
229            }
230            // docs on `use` and `extern crate` statements are not displayed, so they're not
231            // worth counting
232            clean::ImportItem(..) | clean::ExternCrateItem { .. } => {}
233            // Don't count trait impls, the missing-docs lint doesn't so we shouldn't either.
234            // Inherent impls *can* be documented, and those docs show up, but in most cases it
235            // doesn't make sense, as all methods on a type are in one single impl block
236            clean::ImplItem(_) => {}
237            _ => {
238                let has_docs = !i.attrs.doc_strings.is_empty();
239                let mut tests = Tests { found_tests: 0 };
240
241                find_testable_code(&i.doc_value(), &mut tests, ErrorCodes::No, None);
242
243                let has_doc_example = tests.found_tests != 0;
244                let hir_id = DocContext::as_local_hir_id(self.ctx.tcx, i.item_id).unwrap();
245                let level_spec = self.ctx.tcx.lint_level_spec_at_node(MISSING_DOCS, hir_id);
246
247                // In case we have:
248                //
249                // ```
250                // enum Foo { Bar(u32) }
251                // // or:
252                // struct Bar(u32);
253                // ```
254                //
255                // there is no need to require documentation on the fields of tuple variants and
256                // tuple structs.
257                let should_be_ignored = i
258                    .item_id
259                    .as_def_id()
260                    .and_then(|def_id| self.ctx.tcx.opt_parent(def_id))
261                    .and_then(|def_id| self.ctx.tcx.hir_get_if_local(def_id))
262                    .map(|node| {
263                        matches!(
264                            node,
265                            hir::Node::Variant(hir::Variant {
266                                data: hir::VariantData::Tuple(_, _, _),
267                                ..
268                            }) | hir::Node::Item(hir::Item {
269                                kind: hir::ItemKind::Struct(_, _, hir::VariantData::Tuple(_, _, _)),
270                                ..
271                            })
272                        )
273                    })
274                    .unwrap_or(false);
275
276                // `missing_docs` is allow-by-default, so don't treat this as ignoring the item
277                // unless the user had an explicit `allow`.
278                //
279                let should_have_docs = !should_be_ignored
280                    && (!level_spec.is_allow()
281                        || matches!(level_spec.src, LintLevelSource::Default));
282
283                if let Some(span) = i.span(self.ctx.tcx) {
284                    let filename = span.filename(self.ctx.sess());
285                    debug!("counting {:?} {:?} in {filename:?}", i.type_(), i.name);
286                    self.items.entry(filename).or_default().count_item(
287                        has_docs,
288                        has_doc_example,
289                        should_have_doc_example(self.ctx, i),
290                        should_have_docs,
291                    );
292                }
293            }
294        }
295
296        self.visit_item_recur(i)
297    }
298}