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::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 return;
220 }
221
222 match i.kind {
223 ItemKind::Stripped(..) => return,
225 ItemKind::PlaceholderImpl => return,
227 ItemKind::Import(..) | ItemKind::ExternCrate { .. } => {}
230 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 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 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}