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 {
134 if let syn::Fields::Named(ref named) = s.fields {
135 for field in &named.named {
136 *total_fields += 1;
137 if !has_doc(&field.attrs) {
138 *missing_field_count += 1;
139 let field_name = field
140 .ident
141 .as_ref()
142 .map(|i| i.to_string())
143 .unwrap_or_default();
144 missing.push(MissingDoc {
145 file_path: file_path.to_string(),
146 line: line_of(field.span()),
147 label: format!("{}::{} (field)", s.ident, field_name),
148 });
149 }
150 }
151 }
152 }
153 }
154 Item::Enum(e) => {
155 *total_items += 1;
156 if !has_doc(&e.attrs) {
157 *missing_item_count += 1;
158 missing.push(MissingDoc {
159 file_path: file_path.to_string(),
160 line: line_of(e.enum_token.span),
161 label: format!("{} (enum)", e.ident),
162 });
163 }
164 if check_fields {
165 for variant in &e.variants {
166 *total_fields += 1;
167 if !has_doc(&variant.attrs) {
168 *missing_field_count += 1;
169 missing.push(MissingDoc {
170 file_path: file_path.to_string(),
171 line: line_of(variant.ident.span()),
172 label: format!("{}::{} (variant)", e.ident, variant.ident),
173 });
174 }
175 }
176 }
177 }
178 Item::Fn(f) => {
179 *total_items += 1;
180 if !has_doc(&f.attrs) {
181 *missing_item_count += 1;
182 missing.push(MissingDoc {
183 file_path: file_path.to_string(),
184 line: line_of(f.sig.fn_token.span),
185 label: format!("{} (fn)", f.sig.ident),
186 });
187 }
188 }
189 Item::Trait(t) => {
190 *total_items += 1;
191 if !has_doc(&t.attrs) {
192 *missing_item_count += 1;
193 missing.push(MissingDoc {
194 file_path: file_path.to_string(),
195 line: line_of(t.trait_token.span),
196 label: format!("{} (trait)", t.ident),
197 });
198 }
199 if check_fields {
200 for ti in &t.items {
201 if let syn::TraitItem::Fn(tf) = ti {
202 *total_fields += 1;
203 if !has_doc(&tf.attrs) {
204 *missing_field_count += 1;
205 missing.push(MissingDoc {
206 file_path: file_path.to_string(),
207 line: line_of(tf.sig.fn_token.span),
208 label: format!("{}::{} (trait method)", t.ident, tf.sig.ident),
209 });
210 }
211 }
212 }
213 }
214 }
215 Item::Mod(m) if m.content.is_some() => {
216 *total_items += 1;
217 if !has_doc(&m.attrs) {
218 *missing_item_count += 1;
219 missing.push(MissingDoc {
220 file_path: file_path.to_string(),
221 line: line_of(m.mod_token.span),
222 label: format!("{} (mod)", m.ident),
223 });
224 }
225 }
226 Item::Type(t) => {
227 *total_items += 1;
228 if !has_doc(&t.attrs) {
229 *missing_item_count += 1;
230 missing.push(MissingDoc {
231 file_path: file_path.to_string(),
232 line: line_of(t.type_token.span),
233 label: format!("{} (type alias)", t.ident),
234 });
235 }
236 }
237 Item::Const(c) => {
238 *total_items += 1;
239 if !has_doc(&c.attrs) {
240 *missing_item_count += 1;
241 missing.push(MissingDoc {
242 file_path: file_path.to_string(),
243 line: line_of(c.const_token.span),
244 label: format!("{} (const)", c.ident),
245 });
246 }
247 }
248 Item::Static(s) => {
249 *total_items += 1;
250 if !has_doc(&s.attrs) {
251 *missing_item_count += 1;
252 missing.push(MissingDoc {
253 file_path: file_path.to_string(),
254 line: line_of(s.static_token.span),
255 label: format!("{} (static)", s.ident),
256 });
257 }
258 }
259 Item::Impl(impl_block) if check_fields => {
260 for impl_item in &impl_block.items {
262 if let syn::ImplItem::Fn(f) = impl_item {
263 *total_fields += 1;
264 if !has_doc(&f.attrs) {
265 *missing_field_count += 1;
266 let type_name = match &*impl_block.self_ty {
267 syn::Type::Path(tp) => tp
268 .path
269 .segments
270 .last()
271 .map(|s| s.ident.to_string())
272 .unwrap_or_default(),
273 _ => String::new(),
274 };
275 missing.push(MissingDoc {
276 file_path: file_path.to_string(),
277 line: line_of(f.sig.fn_token.span),
278 label: format!("{}::{} (impl method)", type_name, f.sig.ident),
279 });
280 }
281 }
282 }
283 }
284 _ => {}
286 }
287}
288