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