@@ -34,6 +34,40 @@ declare_clippy_lint! {
34
34
"presence of `_`, `::` or camel-case outside backticks in documentation"
35
35
}
36
36
37
+ declare_clippy_lint ! {
38
+ /// **What it does:** Checks for the doc comments of publicly visible
39
+ /// unsafe functions and warns if there is no `# Safety` section.
40
+ ///
41
+ /// **Why is this bad?** Unsafe functions should document their safety
42
+ /// preconditions, so that users can be sure they are using them safely.
43
+ ///
44
+ /// **Known problems:** None.
45
+ ///
46
+ /// **Examples**:
47
+ /// ```rust
48
+ ///# type Universe = ();
49
+ /// /// This function should really be documented
50
+ /// pub unsafe fn start_apocalypse(u: &mut Universe) {
51
+ /// unimplemented!();
52
+ /// }
53
+ /// ```
54
+ ///
55
+ /// At least write a line about safety:
56
+ ///
57
+ /// ```rust
58
+ ///# type Universe = ();
59
+ /// /// # Safety
60
+ /// ///
61
+ /// /// This function should not be called before the horsemen are ready.
62
+ /// pub unsafe fn start_apocalypse(u: &mut Universe) {
63
+ /// unimplemented!();
64
+ /// }
65
+ /// ```
66
+ pub MISSING_SAFETY_DOC ,
67
+ style,
68
+ "`pub unsafe fn` without `# Safety` docs"
69
+ }
70
+
37
71
#[ allow( clippy:: module_name_repetitions) ]
38
72
#[ derive( Clone ) ]
39
73
pub struct DocMarkdown {
@@ -46,15 +80,28 @@ impl DocMarkdown {
46
80
}
47
81
}
48
82
49
- impl_lint_pass ! ( DocMarkdown => [ DOC_MARKDOWN ] ) ;
83
+ impl_lint_pass ! ( DocMarkdown => [ DOC_MARKDOWN , MISSING_SAFETY_DOC ] ) ;
50
84
51
85
impl EarlyLintPass for DocMarkdown {
52
86
fn check_crate ( & mut self , cx : & EarlyContext < ' _ > , krate : & ast:: Crate ) {
53
87
check_attrs ( cx, & self . valid_idents , & krate. attrs ) ;
54
88
}
55
89
56
90
fn check_item ( & mut self , cx : & EarlyContext < ' _ > , item : & ast:: Item ) {
57
- check_attrs ( cx, & self . valid_idents , & item. attrs ) ;
91
+ if check_attrs ( cx, & self . valid_idents , & item. attrs ) {
92
+ return ;
93
+ }
94
+ // no safety header
95
+ if let ast:: ItemKind :: Fn ( _, ref header, ..) = item. node {
96
+ if item. vis . node . is_pub ( ) && header. unsafety == ast:: Unsafety :: Unsafe {
97
+ span_lint (
98
+ cx,
99
+ MISSING_SAFETY_DOC ,
100
+ item. span ,
101
+ "unsafe function's docs miss `# Safety` section" ,
102
+ ) ;
103
+ }
104
+ }
58
105
}
59
106
}
60
107
@@ -115,7 +162,7 @@ pub fn strip_doc_comment_decoration(comment: &str, span: Span) -> (String, Vec<(
115
162
panic ! ( "not a doc-comment: {}" , comment) ;
116
163
}
117
164
118
- pub fn check_attrs < ' a > ( cx : & EarlyContext < ' _ > , valid_idents : & FxHashSet < String > , attrs : & ' a [ ast:: Attribute ] ) {
165
+ pub fn check_attrs < ' a > ( cx : & EarlyContext < ' _ > , valid_idents : & FxHashSet < String > , attrs : & ' a [ ast:: Attribute ] ) -> bool {
119
166
let mut doc = String :: new ( ) ;
120
167
let mut spans = vec ! [ ] ;
121
168
@@ -129,7 +176,7 @@ pub fn check_attrs<'a>(cx: &EarlyContext<'_>, valid_idents: &FxHashSet<String>,
129
176
}
130
177
} else if attr. check_name ( sym ! ( doc) ) {
131
178
// ignore mix of sugared and non-sugared doc
132
- return ;
179
+ return true ; // don't trigger the safety check
133
180
}
134
181
}
135
182
@@ -140,57 +187,64 @@ pub fn check_attrs<'a>(cx: &EarlyContext<'_>, valid_idents: &FxHashSet<String>,
140
187
current += offset_copy;
141
188
}
142
189
143
- if !doc. is_empty ( ) {
144
- let parser = pulldown_cmark:: Parser :: new ( & doc) . into_offset_iter ( ) ;
145
- // Iterate over all `Events` and combine consecutive events into one
146
- let events = parser. coalesce ( |previous, current| {
147
- use pulldown_cmark:: Event :: * ;
148
-
149
- let previous_range = previous. 1 ;
150
- let current_range = current. 1 ;
151
-
152
- match ( previous. 0 , current. 0 ) {
153
- ( Text ( previous) , Text ( current) ) => {
154
- let mut previous = previous. to_string ( ) ;
155
- previous. push_str ( & current) ;
156
- Ok ( ( Text ( previous. into ( ) ) , previous_range) )
157
- } ,
158
- ( previous, current) => Err ( ( ( previous, previous_range) , ( current, current_range) ) ) ,
159
- }
160
- } ) ;
161
- check_doc ( cx, valid_idents, events, & spans) ;
190
+ if doc. is_empty ( ) {
191
+ return false ;
162
192
}
193
+
194
+ let parser = pulldown_cmark:: Parser :: new ( & doc) . into_offset_iter ( ) ;
195
+ // Iterate over all `Events` and combine consecutive events into one
196
+ let events = parser. coalesce ( |previous, current| {
197
+ use pulldown_cmark:: Event :: * ;
198
+
199
+ let previous_range = previous. 1 ;
200
+ let current_range = current. 1 ;
201
+
202
+ match ( previous. 0 , current. 0 ) {
203
+ ( Text ( previous) , Text ( current) ) => {
204
+ let mut previous = previous. to_string ( ) ;
205
+ previous. push_str ( & current) ;
206
+ Ok ( ( Text ( previous. into ( ) ) , previous_range) )
207
+ } ,
208
+ ( previous, current) => Err ( ( ( previous, previous_range) , ( current, current_range) ) ) ,
209
+ }
210
+ } ) ;
211
+ check_doc ( cx, valid_idents, events, & spans)
163
212
}
164
213
165
214
fn check_doc < ' a , Events : Iterator < Item = ( pulldown_cmark:: Event < ' a > , Range < usize > ) > > (
166
215
cx : & EarlyContext < ' _ > ,
167
216
valid_idents : & FxHashSet < String > ,
168
217
events : Events ,
169
218
spans : & [ ( usize , Span ) ] ,
170
- ) {
219
+ ) -> bool {
220
+ // true if a safety header was found
171
221
use pulldown_cmark:: Event :: * ;
172
222
use pulldown_cmark:: Tag :: * ;
173
223
224
+ let mut safety_header = false ;
174
225
let mut in_code = false ;
175
226
let mut in_link = None ;
227
+ let mut in_heading = false ;
176
228
177
229
for ( event, range) in events {
178
230
match event {
179
231
Start ( CodeBlock ( _) ) => in_code = true ,
180
232
End ( CodeBlock ( _) ) => in_code = false ,
181
233
Start ( Link ( _, url, _) ) => in_link = Some ( url) ,
182
234
End ( Link ( ..) ) => in_link = None ,
183
- Start ( _tag) | End ( _tag) => ( ) , // We don't care about other tags
184
- Html ( _html) | InlineHtml ( _html) => ( ) , // HTML is weird, just ignore it
185
- SoftBreak | HardBreak | TaskListMarker ( _) | Code ( _) => ( ) ,
235
+ Start ( Heading ( _) ) => in_heading = true ,
236
+ End ( Heading ( _) ) => in_heading = false ,
237
+ Start ( _tag) | End ( _tag) => ( ) , // We don't care about other tags
238
+ Html ( _html) => ( ) , // HTML is weird, just ignore it
239
+ SoftBreak | HardBreak | TaskListMarker ( _) | Code ( _) | Rule => ( ) ,
186
240
FootnoteReference ( text) | Text ( text) => {
187
241
if Some ( & text) == in_link. as_ref ( ) {
188
242
// Probably a link of the form `<http://example.com>`
189
243
// Which are represented as a link to "http://example.com" with
190
244
// text "http://example.com" by pulldown-cmark
191
245
continue ;
192
246
}
193
-
247
+ safety_header |= in_heading && text . trim ( ) == "Safety" ;
194
248
if !in_code {
195
249
let index = match spans. binary_search_by ( |c| c. 0 . cmp ( & range. start ) ) {
196
250
Ok ( o) => o,
@@ -207,6 +261,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
207
261
} ,
208
262
}
209
263
}
264
+ safety_header
210
265
}
211
266
212
267
fn check_text ( cx : & EarlyContext < ' _ > , valid_idents : & FxHashSet < String > , text : & str , span : Span ) {
0 commit comments