1use std::path::PathBuf;
4
5use anyhow::Result;
6use syn::spanned::Spanned;
7
8use crate::files::collect_rust_files_with_exclusions;
9
10#[derive(Debug)]
11pub struct DocCoverageReport {
12 pub missing_items: Vec<MissingDoc>,
13 pub total_items: usize,
14 pub total_fields: usize,
15 pub missing_item_count: usize,
16 pub missing_field_count: usize,
17}
18
19#[derive(Debug)]
20pub struct MissingDoc {
21 pub file_path: String,
22 pub line: usize,
23 pub label: String, }
25
26pub fn run(paths: &[PathBuf], check_fields: bool, exclude: &[String]) -> Result<DocCoverageReport> {
27 let files = collect_rust_files_with_exclusions(paths, exclude)?;
28
29 let mut missing_items: Vec<MissingDoc> = Vec::new();
30 let mut total_items = 0usize;
31 let mut total_fields = 0usize;
32 let mut missing_item_count = 0usize;
33 let mut missing_field_count = 0usize;
34
35 for file in &files {
36 let content = match std::fs::read_to_string(file) {
37 Ok(c) => c,
38 Err(e) => {
39 eprintln!("⚠️ Skipping {}: {}", file.display(), e);
40 continue;
41 }
42 };
43 let syntax = match syn::parse_file(&content) {
44 Ok(s) => s,
45 Err(e) => {
46 eprintln!("⚠️ Skipping {} (parse error): {}", file.display(), e);
47 continue;
48 }
49 };
50
51 let fp = file.to_string_lossy().to_string();
52
53 for item in &syntax.items {
54 process_item(
55 item,
56 &fp,
57 check_fields,
58 &mut missing_items,
59 &mut total_items,
60 &mut total_fields,
61 &mut missing_item_count,
62 &mut missing_field_count,
63 );
64 }
65 }
66
67 Ok(DocCoverageReport {
68 missing_items,
69 total_items,
70 total_fields,
71 missing_item_count,
72 missing_field_count,
73 })
74}
75
76pub fn render(report: &DocCoverageReport) {
77 println!("Missing docs (items): {}", report.missing_item_count);
78 println!("Missing docs (fields): {}", report.missing_field_count);
79
80 if report.missing_items.is_empty() {
81 println!("All public items are documented.");
82 return;
83 }
84
85 let mut offenders: Vec<&MissingDoc> = report.missing_items.iter().collect();
87 offenders.sort_by(|a, b| a.file_path.cmp(&b.file_path).then(a.line.cmp(&b.line)));
88
89 let top: Vec<_> = offenders.iter().take(10).collect();
90 println!("\nTop offenders:");
91 for doc in top {
92 println!(" {}:{}: {}", doc.file_path, doc.line, doc.label);
93 }
94 if offenders.len() > 10 {
95 println!(" ... and {} more", offenders.len() - 10);
96 }
97}
98
99fn has_doc(attrs: &[syn::Attribute]) -> bool {
102 attrs.iter().any(|attr| attr.path().is_ident("doc"))
103}
104
105fn line_of(span: proc_macro2::Span) -> usize {
106 span.start().line
107}
108
109#[allow(clippy::too_many_arguments)]
110fn process_item(
111 item: &syn::Item,
112 file_path: &str,
113 check_fields: bool,
114 missing: &mut Vec<MissingDoc>,
115 total_items: &mut usize,
116 total_fields: &mut usize,
117 missing_item_count: &mut usize,
118 missing_field_count: &mut usize,
119) {
120 use syn::Item;
121
122 match item {
123 Item::Struct(s) => {
124 *total_items += 1;
125 if !has_doc(&s.attrs) {
126 *missing_item_count += 1;
127 missing.push(MissingDoc {
128 file_path: file_path.to_string(),
129 line: line_of(s.struct_token.span),
130 label: format!("{} (struct)", s.ident),
131 });
132 }
133 if check_fields && let syn::Fields::Named(ref named) = s.fields {
134 for field in &named.named {
135 *total_fields += 1;
136 if !has_doc(&field.attrs) {
137 *missing_field_count += 1;
138 let field_name = field
139 .ident
140 .as_ref()
141 .map(|i| i.to_string())
142 .unwrap_or_default();
143 missing.push(MissingDoc {
144 file_path: file_path.to_string(),
145 line: line_of(field.span()),
146 label: format!("{}::{} (field)", s.ident, field_name),
147 });
148 }
149 }
150 }
151 }
152 Item::Enum(e) => {
153 *total_items += 1;
154 if !has_doc(&e.attrs) {
155 *missing_item_count += 1;
156 missing.push(MissingDoc {
157 file_path: file_path.to_string(),
158 line: line_of(e.enum_token.span),
159 label: format!("{} (enum)", e.ident),
160 });
161 }
162 if check_fields {
163 for variant in &e.variants {
164 *total_fields += 1;
165 if !has_doc(&variant.attrs) {
166 *missing_field_count += 1;
167 missing.push(MissingDoc {
168 file_path: file_path.to_string(),
169 line: line_of(variant.ident.span()),
170 label: format!("{}::{} (variant)", e.ident, variant.ident),
171 });
172 }
173 }
174 }
175 }
176 Item::Fn(f) => {
177 *total_items += 1;
178 if !has_doc(&f.attrs) {
179 *missing_item_count += 1;
180 missing.push(MissingDoc {
181 file_path: file_path.to_string(),
182 line: line_of(f.sig.fn_token.span),
183 label: format!("{} (fn)", f.sig.ident),
184 });
185 }
186 }
187 Item::Trait(t) => {
188 *total_items += 1;
189 if !has_doc(&t.attrs) {
190 *missing_item_count += 1;
191 missing.push(MissingDoc {
192 file_path: file_path.to_string(),
193 line: line_of(t.trait_token.span),
194 label: format!("{} (trait)", t.ident),
195 });
196 }
197 if check_fields {
198 for ti in &t.items {
199 if let syn::TraitItem::Fn(tf) = ti {
200 *total_fields += 1;
201 if !has_doc(&tf.attrs) {
202 *missing_field_count += 1;
203 missing.push(MissingDoc {
204 file_path: file_path.to_string(),
205 line: line_of(tf.sig.fn_token.span),
206 label: format!("{}::{} (trait method)", t.ident, tf.sig.ident),
207 });
208 }
209 }
210 }
211 }
212 }
213 Item::Mod(m) if m.content.is_some() => {
214 *total_items += 1;
215 if !has_doc(&m.attrs) {
216 *missing_item_count += 1;
217 missing.push(MissingDoc {
218 file_path: file_path.to_string(),
219 line: line_of(m.mod_token.span),
220 label: format!("{} (mod)", m.ident),
221 });
222 }
223 }
224 Item::Type(t) => {
225 *total_items += 1;
226 if !has_doc(&t.attrs) {
227 *missing_item_count += 1;
228 missing.push(MissingDoc {
229 file_path: file_path.to_string(),
230 line: line_of(t.type_token.span),
231 label: format!("{} (type alias)", t.ident),
232 });
233 }
234 }
235 Item::Const(c) => {
236 *total_items += 1;
237 if !has_doc(&c.attrs) {
238 *missing_item_count += 1;
239 missing.push(MissingDoc {
240 file_path: file_path.to_string(),
241 line: line_of(c.const_token.span),
242 label: format!("{} (const)", c.ident),
243 });
244 }
245 }
246 Item::Static(s) => {
247 *total_items += 1;
248 if !has_doc(&s.attrs) {
249 *missing_item_count += 1;
250 missing.push(MissingDoc {
251 file_path: file_path.to_string(),
252 line: line_of(s.static_token.span),
253 label: format!("{} (static)", s.ident),
254 });
255 }
256 }
257 Item::Impl(impl_block) if check_fields => {
258 for impl_item in &impl_block.items {
260 if let syn::ImplItem::Fn(f) = impl_item {
261 *total_fields += 1;
262 if !has_doc(&f.attrs) {
263 *missing_field_count += 1;
264 let type_name = match &*impl_block.self_ty {
265 syn::Type::Path(tp) => tp
266 .path
267 .segments
268 .last()
269 .map(|s| s.ident.to_string())
270 .unwrap_or_default(),
271 _ => String::new(),
272 };
273 missing.push(MissingDoc {
274 file_path: file_path.to_string(),
275 line: line_of(f.sig.fn_token.span),
276 label: format!("{}::{} (impl method)", type_name, f.sig.ident),
277 });
278 }
279 }
280 }
281 }
282 _ => {}
285 }
286}