@@ -34,6 +34,40 @@ declare_clippy_lint! {
3434 "presence of `_`, `::` or camel-case outside backticks in documentation"
3535}
3636
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+
3771#[ allow( clippy:: module_name_repetitions) ]
3872#[ derive( Clone ) ]
3973pub struct DocMarkdown {
@@ -46,15 +80,28 @@ impl DocMarkdown {
4680 }
4781}
4882
49- impl_lint_pass ! ( DocMarkdown => [ DOC_MARKDOWN ] ) ;
83+ impl_lint_pass ! ( DocMarkdown => [ DOC_MARKDOWN , MISSING_SAFETY_DOC ] ) ;
5084
5185impl EarlyLintPass for DocMarkdown {
5286 fn check_crate ( & mut self , cx : & EarlyContext < ' _ > , krate : & ast:: Crate ) {
5387 check_attrs ( cx, & self . valid_idents , & krate. attrs ) ;
5488 }
5589
5690 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+ }
58105 }
59106}
60107
@@ -115,7 +162,7 @@ pub fn strip_doc_comment_decoration(comment: &str, span: Span) -> (String, Vec<(
115162 panic ! ( "not a doc-comment: {}" , comment) ;
116163}
117164
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 {
119166 let mut doc = String :: new ( ) ;
120167 let mut spans = vec ! [ ] ;
121168
@@ -129,7 +176,7 @@ pub fn check_attrs<'a>(cx: &EarlyContext<'_>, valid_idents: &FxHashSet<String>,
129176 }
130177 } else if attr. check_name ( sym ! ( doc) ) {
131178 // ignore mix of sugared and non-sugared doc
132- return ;
179+ return true ; // don't trigger the safety check
133180 }
134181 }
135182
@@ -140,57 +187,64 @@ pub fn check_attrs<'a>(cx: &EarlyContext<'_>, valid_idents: &FxHashSet<String>,
140187 current += offset_copy;
141188 }
142189
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 ;
162192 }
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)
163212}
164213
165214fn check_doc < ' a , Events : Iterator < Item = ( pulldown_cmark:: Event < ' a > , Range < usize > ) > > (
166215 cx : & EarlyContext < ' _ > ,
167216 valid_idents : & FxHashSet < String > ,
168217 events : Events ,
169218 spans : & [ ( usize , Span ) ] ,
170- ) {
219+ ) -> bool {
220+ // true if a safety header was found
171221 use pulldown_cmark:: Event :: * ;
172222 use pulldown_cmark:: Tag :: * ;
173223
224+ let mut safety_header = false ;
174225 let mut in_code = false ;
175226 let mut in_link = None ;
227+ let mut in_heading = false ;
176228
177229 for ( event, range) in events {
178230 match event {
179231 Start ( CodeBlock ( _) ) => in_code = true ,
180232 End ( CodeBlock ( _) ) => in_code = false ,
181233 Start ( Link ( _, url, _) ) => in_link = Some ( url) ,
182234 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 => ( ) ,
186240 FootnoteReference ( text) | Text ( text) => {
187241 if Some ( & text) == in_link. as_ref ( ) {
188242 // Probably a link of the form `<http://example.com>`
189243 // Which are represented as a link to "http://example.com" with
190244 // text "http://example.com" by pulldown-cmark
191245 continue ;
192246 }
193-
247+ safety_header |= in_heading && text . trim ( ) == "Safety" ;
194248 if !in_code {
195249 let index = match spans. binary_search_by ( |c| c. 0 . cmp ( & range. start ) ) {
196250 Ok ( o) => o,
@@ -207,6 +261,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
207261 } ,
208262 }
209263 }
264+ safety_header
210265}
211266
212267fn check_text ( cx : & EarlyContext < ' _ > , valid_idents : & FxHashSet < String > , text : & str , span : Span ) {
0 commit comments