1use 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 return;
219 }
220
221 match i.kind {
222 clean::StrippedItem(..) => {
223 return;
225 }
226 clean::PlaceholderImplItem => {
227 return;
229 }
230 clean::ImportItem(..) | clean::ExternCrateItem { .. } => {}
233 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 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 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}