Skip to content

Commit 0b4b684

Browse files
committed
Auto merge of #12433 - J-ZhengLi:issue12197, r=dswij
fix [`missing_docs_in_private_items`] on some proc macros fixes: #12197 --- changelog: [`missing_docs_in_private_items`] support manually search for docs as fallback method
2 parents 93f0a9a + adc91e4 commit 0b4b684

File tree

4 files changed

+108
-24
lines changed

4 files changed

+108
-24
lines changed

clippy_lints/src/missing_doc.rs

+59-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use clippy_utils::attrs::is_doc_hidden;
99
use clippy_utils::diagnostics::span_lint;
1010
use clippy_utils::is_from_proc_macro;
11+
use clippy_utils::source::snippet_opt;
1112
use rustc_ast::ast::{self, MetaItem, MetaItemKind};
1213
use rustc_hir as hir;
1314
use rustc_hir::def_id::LocalDefId;
@@ -32,13 +33,22 @@ declare_clippy_lint! {
3233
"detects missing documentation for private members"
3334
}
3435

36+
macro_rules! note_prev_span_then_ret {
37+
($prev_span:expr, $span:expr) => {{
38+
$prev_span = Some($span);
39+
return;
40+
}};
41+
}
42+
3543
pub struct MissingDoc {
3644
/// Whether to **only** check for missing documentation in items visible within the current
3745
/// crate. For example, `pub(crate)` items.
3846
crate_items_only: bool,
3947
/// Stack of whether #[doc(hidden)] is set
4048
/// at each level which has lint attributes.
4149
doc_hidden_stack: Vec<bool>,
50+
/// Used to keep tracking of the previous item, field or variants etc, to get the search span.
51+
prev_span: Option<Span>,
4252
}
4353

4454
impl Default for MissingDoc {
@@ -54,6 +64,7 @@ impl MissingDoc {
5464
Self {
5565
crate_items_only,
5666
doc_hidden_stack: vec![false],
67+
prev_span: None,
5768
}
5869
}
5970

@@ -108,7 +119,8 @@ impl MissingDoc {
108119

109120
let has_doc = attrs
110121
.iter()
111-
.any(|a| a.doc_str().is_some() || Self::has_include(a.meta()));
122+
.any(|a| a.doc_str().is_some() || Self::has_include(a.meta()))
123+
|| matches!(self.search_span(sp), Some(span) if span_to_snippet_contains_docs(cx, span));
112124

113125
if !has_doc {
114126
span_lint(
@@ -119,6 +131,32 @@ impl MissingDoc {
119131
);
120132
}
121133
}
134+
135+
/// Return a span to search for doc comments manually.
136+
///
137+
/// # Example
138+
/// ```ignore
139+
/// fn foo() { ... }
140+
/// ^^^^^^^^^^^^^^^^ prev_span
141+
/// ↑
142+
/// | search_span |
143+
/// ↓
144+
/// fn bar() { ... }
145+
/// ^^^^^^^^^^^^^^^^ cur_span
146+
/// ```
147+
fn search_span(&self, cur_span: Span) -> Option<Span> {
148+
let prev_span = self.prev_span?;
149+
let start_pos = if prev_span.contains(cur_span) {
150+
// In case when the prev_span is an entire struct, or enum,
151+
// and the current span is a field, or variant, we need to search from
152+
// the starting pos of the previous span.
153+
prev_span.lo()
154+
} else {
155+
prev_span.hi()
156+
};
157+
let search_span = cur_span.with_lo(start_pos).with_hi(cur_span.lo());
158+
Some(search_span)
159+
}
122160
}
123161

124162
impl_lint_pass!(MissingDoc => [MISSING_DOCS_IN_PRIVATE_ITEMS]);
@@ -138,14 +176,18 @@ impl<'tcx> LateLintPass<'tcx> for MissingDoc {
138176
self.check_missing_docs_attrs(cx, CRATE_DEF_ID, attrs, cx.tcx.def_span(CRATE_DEF_ID), "the", "crate");
139177
}
140178

179+
fn check_crate_post(&mut self, _: &LateContext<'tcx>) {
180+
self.prev_span = None;
181+
}
182+
141183
fn check_item(&mut self, cx: &LateContext<'tcx>, it: &'tcx hir::Item<'_>) {
142184
match it.kind {
143185
hir::ItemKind::Fn(..) => {
144186
// ignore main()
145187
if it.ident.name == sym::main {
146188
let at_root = cx.tcx.local_parent(it.owner_id.def_id) == CRATE_DEF_ID;
147189
if at_root {
148-
return;
190+
note_prev_span_then_ret!(self.prev_span, it.span);
149191
}
150192
}
151193
},
@@ -164,7 +206,7 @@ impl<'tcx> LateLintPass<'tcx> for MissingDoc {
164206
| hir::ItemKind::ForeignMod { .. }
165207
| hir::ItemKind::GlobalAsm(..)
166208
| hir::ItemKind::Impl { .. }
167-
| hir::ItemKind::Use(..) => return,
209+
| hir::ItemKind::Use(..) => note_prev_span_then_ret!(self.prev_span, it.span),
168210
};
169211

170212
let (article, desc) = cx.tcx.article_and_description(it.owner_id.to_def_id());
@@ -173,6 +215,7 @@ impl<'tcx> LateLintPass<'tcx> for MissingDoc {
173215
if !is_from_proc_macro(cx, it) {
174216
self.check_missing_docs_attrs(cx, it.owner_id.def_id, attrs, it.span, article, desc);
175217
}
218+
self.prev_span = Some(it.span);
176219
}
177220

178221
fn check_trait_item(&mut self, cx: &LateContext<'tcx>, trait_item: &'tcx hir::TraitItem<'_>) {
@@ -182,23 +225,25 @@ impl<'tcx> LateLintPass<'tcx> for MissingDoc {
182225
if !is_from_proc_macro(cx, trait_item) {
183226
self.check_missing_docs_attrs(cx, trait_item.owner_id.def_id, attrs, trait_item.span, article, desc);
184227
}
228+
self.prev_span = Some(trait_item.span);
185229
}
186230

187231
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx hir::ImplItem<'_>) {
188232
// If the method is an impl for a trait, don't doc.
189233
if let Some(cid) = cx.tcx.associated_item(impl_item.owner_id).impl_container(cx.tcx) {
190234
if cx.tcx.impl_trait_ref(cid).is_some() {
191-
return;
235+
note_prev_span_then_ret!(self.prev_span, impl_item.span);
192236
}
193237
} else {
194-
return;
238+
note_prev_span_then_ret!(self.prev_span, impl_item.span);
195239
}
196240

197241
let (article, desc) = cx.tcx.article_and_description(impl_item.owner_id.to_def_id());
198242
let attrs = cx.tcx.hir().attrs(impl_item.hir_id());
199243
if !is_from_proc_macro(cx, impl_item) {
200244
self.check_missing_docs_attrs(cx, impl_item.owner_id.def_id, attrs, impl_item.span, article, desc);
201245
}
246+
self.prev_span = Some(impl_item.span);
202247
}
203248

204249
fn check_field_def(&mut self, cx: &LateContext<'tcx>, sf: &'tcx hir::FieldDef<'_>) {
@@ -208,12 +253,21 @@ impl<'tcx> LateLintPass<'tcx> for MissingDoc {
208253
self.check_missing_docs_attrs(cx, sf.def_id, attrs, sf.span, "a", "struct field");
209254
}
210255
}
256+
self.prev_span = Some(sf.span);
211257
}
212258

213259
fn check_variant(&mut self, cx: &LateContext<'tcx>, v: &'tcx hir::Variant<'_>) {
214260
let attrs = cx.tcx.hir().attrs(v.hir_id);
215261
if !is_from_proc_macro(cx, v) {
216262
self.check_missing_docs_attrs(cx, v.def_id, attrs, v.span, "a", "variant");
217263
}
264+
self.prev_span = Some(v.span);
218265
}
219266
}
267+
268+
fn span_to_snippet_contains_docs(cx: &LateContext<'_>, search_span: Span) -> bool {
269+
let Some(snippet) = snippet_opt(cx, search_span) else {
270+
return false;
271+
};
272+
snippet.lines().rev().any(|line| line.trim().starts_with("///"))
273+
}

tests/ui/auxiliary/proc_macro_attr.rs

+24-6
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use quote::{quote, quote_spanned};
1111
use syn::spanned::Spanned;
1212
use syn::token::Star;
1313
use syn::{
14-
parse_macro_input, parse_quote, FnArg, ImplItem, ItemFn, ItemImpl, ItemTrait, Lifetime, Pat, PatIdent, PatType,
15-
Signature, TraitItem, Type,
14+
parse_macro_input, parse_quote, FnArg, ImplItem, ItemFn, ItemImpl, ItemStruct, ItemTrait, Lifetime, Pat, PatIdent,
15+
PatType, Signature, TraitItem, Type, Visibility,
1616
};
1717

1818
#[proc_macro_attribute]
@@ -101,9 +101,7 @@ pub fn fake_main(_attr: TokenStream, item: TokenStream) -> TokenStream {
101101
let mut item = parse_macro_input!(item as ItemFn);
102102
let span = item.block.brace_token.span;
103103

104-
if item.sig.asyncness.is_some() {
105-
item.sig.asyncness = None;
106-
}
104+
item.sig.asyncness = None;
107105

108106
let crate_name = quote! { fake_crate };
109107
let block = item.block;
@@ -128,7 +126,7 @@ pub fn fake_main(_attr: TokenStream, item: TokenStream) -> TokenStream {
128126

129127
#[proc_macro_attribute]
130128
pub fn fake_desugar_await(_args: TokenStream, input: TokenStream) -> TokenStream {
131-
let mut async_fn = syn::parse_macro_input!(input as syn::ItemFn);
129+
let mut async_fn = parse_macro_input!(input as syn::ItemFn);
132130

133131
for stmt in &mut async_fn.block.stmts {
134132
if let syn::Stmt::Expr(syn::Expr::Match(syn::ExprMatch { expr: scrutinee, .. }), _) = stmt {
@@ -145,3 +143,23 @@ pub fn fake_desugar_await(_args: TokenStream, input: TokenStream) -> TokenStream
145143

146144
quote!(#async_fn).into()
147145
}
146+
147+
#[proc_macro_attribute]
148+
pub fn rewrite_struct(_args: TokenStream, input: TokenStream) -> TokenStream {
149+
let mut item_struct = parse_macro_input!(input as syn::ItemStruct);
150+
// remove struct attributes including doc comments.
151+
item_struct.attrs = vec![];
152+
if let Visibility::Public(token) = item_struct.vis {
153+
// set vis to `pub(crate)` to trigger `missing_docs_in_private_items` lint.
154+
let new_vis: Visibility = syn::parse_quote_spanned!(token.span() => pub(crate));
155+
item_struct.vis = new_vis;
156+
}
157+
if let syn::Fields::Named(fields) = &mut item_struct.fields {
158+
for field in &mut fields.named {
159+
// remove all attributes from fields as well.
160+
field.attrs = vec![];
161+
}
162+
}
163+
164+
quote!(#item_struct).into()
165+
}

tests/ui/missing_doc.rs

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//@needs-asm-support
22
//@aux-build: proc_macros.rs
3+
//@aux-build: proc_macro_attr.rs
34

45
#![warn(clippy::missing_docs_in_private_items)]
56
// When denying at the crate level, be sure to not get random warnings from the
@@ -8,6 +9,8 @@
89
//! Some garbage docs for the crate here
910
#![doc = "More garbage"]
1011

12+
#[macro_use]
13+
extern crate proc_macro_attr;
1114
extern crate proc_macros;
1215

1316
use proc_macros::with_span;
@@ -112,3 +115,12 @@ with_span!(span pub enum FooPm3 { A, B(u32), C { field: u32 }});
112115
with_span!(span pub fn foo_pm() {});
113116
with_span!(span pub static FOO_PM: u32 = 0;);
114117
with_span!(span pub const FOO2_PM: u32 = 0;);
118+
119+
// issue #12197
120+
// Undocumented field originated inside of spanned proc-macro attribute
121+
/// Some dox for struct.
122+
#[rewrite_struct]
123+
pub struct Test {
124+
/// Dox
125+
a: u8,
126+
}

tests/ui/missing_doc.stderr

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: missing documentation for a type alias
2-
--> tests/ui/missing_doc.rs:16:1
2+
--> tests/ui/missing_doc.rs:19:1
33
|
44
LL | type Typedef = String;
55
| ^^^^^^^^^^^^^^^^^^^^^^
@@ -8,19 +8,19 @@ LL | type Typedef = String;
88
= help: to override `-D warnings` add `#[allow(clippy::missing_docs_in_private_items)]`
99

1010
error: missing documentation for a module
11-
--> tests/ui/missing_doc.rs:19:1
11+
--> tests/ui/missing_doc.rs:22:1
1212
|
1313
LL | mod module_no_dox {}
1414
| ^^^^^^^^^^^^^^^^^^^^
1515

1616
error: missing documentation for a function
17-
--> tests/ui/missing_doc.rs:25:1
17+
--> tests/ui/missing_doc.rs:28:1
1818
|
1919
LL | fn foo3() {}
2020
| ^^^^^^^^^^^^
2121

2222
error: missing documentation for an enum
23-
--> tests/ui/missing_doc.rs:39:1
23+
--> tests/ui/missing_doc.rs:42:1
2424
|
2525
LL | / enum Baz {
2626
LL | | BazA { a: isize, b: isize },
@@ -29,43 +29,43 @@ LL | | }
2929
| |_^
3030

3131
error: missing documentation for a variant
32-
--> tests/ui/missing_doc.rs:40:5
32+
--> tests/ui/missing_doc.rs:43:5
3333
|
3434
LL | BazA { a: isize, b: isize },
3535
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
3636

3737
error: missing documentation for a struct field
38-
--> tests/ui/missing_doc.rs:40:12
38+
--> tests/ui/missing_doc.rs:43:12
3939
|
4040
LL | BazA { a: isize, b: isize },
4141
| ^^^^^^^^
4242

4343
error: missing documentation for a struct field
44-
--> tests/ui/missing_doc.rs:40:22
44+
--> tests/ui/missing_doc.rs:43:22
4545
|
4646
LL | BazA { a: isize, b: isize },
4747
| ^^^^^^^^
4848

4949
error: missing documentation for a variant
50-
--> tests/ui/missing_doc.rs:41:5
50+
--> tests/ui/missing_doc.rs:44:5
5151
|
5252
LL | BarB,
5353
| ^^^^
5454

5555
error: missing documentation for a constant
56-
--> tests/ui/missing_doc.rs:65:1
56+
--> tests/ui/missing_doc.rs:68:1
5757
|
5858
LL | const FOO: u32 = 0;
5959
| ^^^^^^^^^^^^^^^^^^^
6060

6161
error: missing documentation for a static
62-
--> tests/ui/missing_doc.rs:74:1
62+
--> tests/ui/missing_doc.rs:77:1
6363
|
6464
LL | static BAR: u32 = 0;
6565
| ^^^^^^^^^^^^^^^^^^^^
6666

6767
error: missing documentation for a module
68-
--> tests/ui/missing_doc.rs:83:1
68+
--> tests/ui/missing_doc.rs:86:1
6969
|
7070
LL | / mod internal_impl {
7171
LL | | /// dox
@@ -77,13 +77,13 @@ LL | | }
7777
| |_^
7878

7979
error: missing documentation for a function
80-
--> tests/ui/missing_doc.rs:88:5
80+
--> tests/ui/missing_doc.rs:91:5
8181
|
8282
LL | fn undocumented3() {}
8383
| ^^^^^^^^^^^^^^^^^^^^^
8484

8585
error: missing documentation for a function
86-
--> tests/ui/missing_doc.rs:94:9
86+
--> tests/ui/missing_doc.rs:97:9
8787
|
8888
LL | fn also_undocumented2() {}
8989
| ^^^^^^^^^^^^^^^^^^^^^^^^^^

0 commit comments

Comments
 (0)