1- use clippy_utils:: diagnostics:: { span_lint_and_help, span_lint_and_sugg } ;
1+ use clippy_utils:: diagnostics:: span_lint_and_help;
22use clippy_utils:: is_lint_allowed;
3- use clippy_utils:: source:: { indent_of, reindent_multiline, snippet} ;
4- use rustc_errors:: Applicability ;
5- use rustc_hir:: intravisit:: { walk_expr, Visitor } ;
6- use rustc_hir:: { Block , BlockCheckMode , Expr , ExprKind , HirId , Local , UnsafeSource } ;
7- use rustc_lexer:: TokenKind ;
8- use rustc_lint:: { LateContext , LateLintPass } ;
3+ use clippy_utils:: source:: walk_span_to_context;
4+ use rustc_hir:: { Block , BlockCheckMode , UnsafeSource } ;
5+ use rustc_lexer:: { tokenize, TokenKind } ;
6+ use rustc_lint:: { LateContext , LateLintPass , LintContext } ;
97use rustc_middle:: lint:: in_external_macro;
10- use rustc_middle:: ty:: TyCtxt ;
11- use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
12- use rustc_span:: { BytePos , Span } ;
13- use std:: borrow:: Cow ;
8+ use rustc_session:: { declare_lint_pass, declare_tool_lint} ;
9+ use rustc_span:: { BytePos , Pos , SyntaxContext } ;
10+ use std:: rc:: Rc ;
1411
1512declare_clippy_lint ! {
1613 /// ### What it does
1714 /// Checks for `unsafe` blocks without a `// SAFETY: ` comment
1815 /// explaining why the unsafe operations performed inside
1916 /// the block are safe.
2017 ///
18+ /// Note the comment must appear on the line(s) preceding the unsafe block
19+ /// with nothing appearing in between. The following is ok:
20+ /// ```ignore
21+ /// foo(
22+ /// // SAFETY:
23+ /// // This is a valid safety comment
24+ /// unsafe { *x }
25+ /// )
26+ /// ```
27+ /// But neither of these are:
28+ /// ```ignore
29+ /// // SAFETY:
30+ /// // This is not a valid safety comment
31+ /// foo(
32+ /// /* SAFETY: Neither is this */ unsafe { *x },
33+ /// );
34+ /// ```
35+ ///
2136 /// ### Why is this bad?
2237 /// Undocumented unsafe blocks can make it difficult to
2338 /// read and maintain code, as well as uncover unsoundness
@@ -44,179 +59,139 @@ declare_clippy_lint! {
4459 "creating an unsafe block without explaining why it is safe"
4560}
4661
47- impl_lint_pass ! ( UndocumentedUnsafeBlocks => [ UNDOCUMENTED_UNSAFE_BLOCKS ] ) ;
48-
49- #[ derive( Default ) ]
50- pub struct UndocumentedUnsafeBlocks {
51- pub local_level : u32 ,
52- pub local_span : Option < Span > ,
53- // The local was already checked for an overall safety comment
54- // There is no need to continue checking the blocks in the local
55- pub local_checked : bool ,
56- // Since we can only check the blocks from expanded macros
57- // We have to omit the suggestion due to the actual definition
58- // Not being available to us
59- pub macro_expansion : bool ,
60- }
62+ declare_lint_pass ! ( UndocumentedUnsafeBlocks => [ UNDOCUMENTED_UNSAFE_BLOCKS ] ) ;
6163
6264impl LateLintPass < ' _ > for UndocumentedUnsafeBlocks {
6365 fn check_block ( & mut self , cx : & LateContext < ' _ > , block : & ' _ Block < ' _ > ) {
64- if_chain ! {
65- if !self . local_checked;
66- if !is_lint_allowed( cx, UNDOCUMENTED_UNSAFE_BLOCKS , block. hir_id) ;
67- if !in_external_macro( cx. tcx. sess, block. span) ;
68- if let BlockCheckMode :: UnsafeBlock ( UnsafeSource :: UserProvided ) = block. rules;
69- if let Some ( enclosing_scope_hir_id) = cx. tcx. hir( ) . get_enclosing_scope( block. hir_id) ;
70- if self . block_has_safety_comment( cx. tcx, enclosing_scope_hir_id, block. span) == Some ( false ) ;
71- then {
72- let mut span = block. span;
73-
74- if let Some ( local_span) = self . local_span {
75- span = local_span;
76-
77- let result = self . block_has_safety_comment( cx. tcx, enclosing_scope_hir_id, span) ;
66+ if block. rules == BlockCheckMode :: UnsafeBlock ( UnsafeSource :: UserProvided )
67+ && !in_external_macro ( cx. tcx . sess , block. span )
68+ && !is_lint_allowed ( cx, UNDOCUMENTED_UNSAFE_BLOCKS , block. hir_id )
69+ && !is_unsafe_from_proc_macro ( cx, block)
70+ && !block_has_safety_comment ( cx, block)
71+ {
72+ let source_map = cx. tcx . sess . source_map ( ) ;
73+ let span = if source_map. is_multiline ( block. span ) {
74+ source_map. span_until_char ( block. span , '\n' )
75+ } else {
76+ block. span
77+ } ;
7878
79- if result. unwrap_or( true ) {
80- self . local_checked = true ;
81- return ;
82- }
83- }
84-
85- self . lint( cx, span) ;
86- }
87- }
88- }
89-
90- fn check_local ( & mut self , cx : & LateContext < ' _ > , local : & ' _ Local < ' _ > ) {
91- if_chain ! {
92- if !is_lint_allowed( cx, UNDOCUMENTED_UNSAFE_BLOCKS , local. hir_id) ;
93- if !in_external_macro( cx. tcx. sess, local. span) ;
94- if let Some ( init) = local. init;
95- then {
96- self . visit_expr( init) ;
97-
98- if self . local_level > 0 {
99- self . local_span = Some ( local. span) ;
100- }
101- }
79+ span_lint_and_help (
80+ cx,
81+ UNDOCUMENTED_UNSAFE_BLOCKS ,
82+ span,
83+ "unsafe block missing a safety comment" ,
84+ None ,
85+ "consider adding a safety comment on the preceding line" ,
86+ ) ;
10287 }
10388 }
89+ }
10490
105- fn check_block_post ( & mut self , _: & LateContext < ' _ > , _: & ' _ Block < ' _ > ) {
106- self . local_level = self . local_level . saturating_sub ( 1 ) ;
107-
108- if self . local_level == 0 {
109- self . local_checked = false ;
110- self . local_span = None ;
111- }
112- }
91+ fn is_unsafe_from_proc_macro ( cx : & LateContext < ' _ > , block : & Block < ' _ > ) -> bool {
92+ let source_map = cx. sess ( ) . source_map ( ) ;
93+ let file_pos = source_map. lookup_byte_offset ( block. span . lo ( ) ) ;
94+ file_pos
95+ . sf
96+ . src
97+ . as_deref ( )
98+ . and_then ( |src| src. get ( file_pos. pos . to_usize ( ) ..) )
99+ . map_or ( true , |src| !src. starts_with ( "unsafe" ) )
113100}
114101
115- impl < ' v > Visitor < ' v > for UndocumentedUnsafeBlocks {
116- fn visit_expr ( & mut self , ex : & ' v Expr < ' v > ) {
117- match ex. kind {
118- ExprKind :: Block ( _, _) => self . local_level = self . local_level . saturating_add ( 1 ) ,
119- _ => walk_expr ( self , ex) ,
102+ /// Checks if the lines immediately preceding the block contain a safety comment.
103+ fn block_has_safety_comment ( cx : & LateContext < ' _ > , block : & Block < ' _ > ) -> bool {
104+ // This intentionally ignores text before the start of a function so something like:
105+ // ```
106+ // // SAFETY: reason
107+ // fn foo() { unsafe { .. } }
108+ // ```
109+ // won't work. This is to avoid dealing with where such a comment should be place relative to
110+ // attributes and doc comments.
111+
112+ let source_map = cx. sess ( ) . source_map ( ) ;
113+ let ctxt = block. span . ctxt ( ) ;
114+ if ctxt != SyntaxContext :: root ( ) {
115+ // From a macro expansion. Get the text from the start of the macro declaration to start of the unsafe block.
116+ // macro_rules! foo { () => { stuff }; (x) => { unsafe { stuff } }; }
117+ // ^--------------------------------------------^
118+ if let Ok ( unsafe_line) = source_map. lookup_line ( block. span . lo ( ) )
119+ && let Ok ( macro_line) = source_map. lookup_line ( ctxt. outer_expn_data ( ) . def_site . lo ( ) )
120+ && Rc :: ptr_eq ( & unsafe_line. sf , & macro_line. sf )
121+ && let Some ( src) = unsafe_line. sf . src . as_deref ( )
122+ {
123+ macro_line. line < unsafe_line. line && text_has_safety_comment (
124+ src,
125+ & unsafe_line. sf . lines [ macro_line. line + 1 ..=unsafe_line. line ] ,
126+ unsafe_line. sf . start_pos . to_usize ( ) ,
127+ )
128+ } else {
129+ // Problem getting source text. Pretend a comment was found.
130+ true
120131 }
132+ } else if let Ok ( unsafe_line) = source_map. lookup_line ( block. span . lo ( ) )
133+ && let Some ( body) = cx. enclosing_body
134+ && let Some ( body_span) = walk_span_to_context ( cx. tcx . hir ( ) . body ( body) . value . span , SyntaxContext :: root ( ) )
135+ && let Ok ( body_line) = source_map. lookup_line ( body_span. lo ( ) )
136+ && Rc :: ptr_eq ( & unsafe_line. sf , & body_line. sf )
137+ && let Some ( src) = unsafe_line. sf . src . as_deref ( )
138+ {
139+ // Get the text from the start of function body to the unsafe block.
140+ // fn foo() { some_stuff; unsafe { stuff }; other_stuff; }
141+ // ^-------------^
142+ body_line. line < unsafe_line. line && text_has_safety_comment (
143+ src,
144+ & unsafe_line. sf . lines [ body_line. line + 1 ..=unsafe_line. line ] ,
145+ unsafe_line. sf . start_pos . to_usize ( ) ,
146+ )
147+ } else {
148+ // Problem getting source text. Pretend a comment was found.
149+ true
121150 }
122151}
123152
124- impl UndocumentedUnsafeBlocks {
125- fn block_has_safety_comment ( & mut self , tcx : TyCtxt < ' _ > , enclosing_hir_id : HirId , block_span : Span ) -> Option < bool > {
126- let map = tcx. hir ( ) ;
127- let source_map = tcx. sess . source_map ( ) ;
128-
129- let enclosing_scope_span = map. opt_span ( enclosing_hir_id) ?;
130-
131- let between_span = if block_span. from_expansion ( ) {
132- self . macro_expansion = true ;
133- enclosing_scope_span. with_hi ( block_span. hi ( ) ) . source_callsite ( )
134- } else {
135- self . macro_expansion = false ;
136- enclosing_scope_span. to ( block_span) . source_callsite ( )
137- } ;
138-
139- let file_name = source_map. span_to_filename ( between_span) ;
140- let source_file = source_map. get_source_file ( & file_name) ?;
141-
142- let lex_start = ( between_span. lo ( ) . 0 - source_file. start_pos . 0 + 1 ) as usize ;
143- let lex_end = ( between_span. hi ( ) . 0 - source_file. start_pos . 0 ) as usize ;
144- let src_str = source_file. src . as_ref ( ) ?[ lex_start..lex_end] . to_string ( ) ;
145-
146- let source_start_pos = source_file. start_pos . 0 as usize + lex_start;
147-
148- let mut pos = 0 ;
149- let mut comment = false ;
150-
151- for token in rustc_lexer:: tokenize ( & src_str) {
152- match token. kind {
153- TokenKind :: LineComment { doc_style : None }
154- | TokenKind :: BlockComment {
155- doc_style : None ,
156- terminated : true ,
157- } => {
158- let comment_str = src_str[ pos + 2 ..pos + token. len ] . to_ascii_uppercase ( ) ;
159-
160- if comment_str. contains ( "SAFETY:" ) {
161- comment = true ;
162- }
163- } ,
164- // We need to add all whitespace to `pos` before checking the comment's line number
165- TokenKind :: Whitespace => { } ,
166- _ => {
167- if comment {
168- // Get the line number of the "comment" (really wherever the trailing whitespace ended)
169- let comment_line_num = source_file
170- . lookup_file_pos ( BytePos ( ( source_start_pos + pos) . try_into ( ) . unwrap ( ) ) )
171- . 0 ;
172- // Find the block/local's line number
173- let block_line_num = tcx. sess . source_map ( ) . lookup_char_pos ( block_span. lo ( ) ) . line ;
174-
175- // Check the comment is immediately followed by the block/local
176- if block_line_num == comment_line_num + 1 || block_line_num == comment_line_num {
177- return Some ( true ) ;
178- }
179-
180- comment = false ;
181- }
182- } ,
153+ /// Checks if the given text has a safety comment for the immediately proceeding line.
154+ fn text_has_safety_comment ( src : & str , line_starts : & [ BytePos ] , offset : usize ) -> bool {
155+ let mut lines = line_starts
156+ . array_windows :: < 2 > ( )
157+ . rev ( )
158+ . map_while ( |[ start, end] | {
159+ src. get ( start. to_usize ( ) - offset..end. to_usize ( ) - offset)
160+ . map ( |text| ( start. to_usize ( ) , text. trim_start ( ) ) )
161+ } )
162+ . filter ( |( _, text) | !text. is_empty ( ) ) ;
163+
164+ let Some ( ( line_start, line) ) = lines. next ( ) else {
165+ return false ;
166+ } ;
167+ // Check for a sequence of line comments.
168+ if line. starts_with ( "//" ) {
169+ let mut line = line;
170+ loop {
171+ if line. to_ascii_uppercase ( ) . contains ( "SAFETY:" ) {
172+ return true ;
173+ }
174+ match lines. next ( ) {
175+ Some ( ( _, x) ) if x. starts_with ( "//" ) => line = x,
176+ _ => return false ,
183177 }
184-
185- pos += token. len ;
186178 }
187-
188- Some ( false )
189179 }
190-
191- fn lint ( & self , cx : & LateContext < ' _ > , mut span : Span ) {
192- let source_map = cx. tcx . sess . source_map ( ) ;
193-
194- if source_map. is_multiline ( span) {
195- span = source_map. span_until_char ( span, '\n' ) ;
180+ // No line comments; look for the start of a block comment.
181+ // This will only find them if they are at the start of a line.
182+ let ( mut line_start, mut line) = ( line_start, line) ;
183+ loop {
184+ if line. starts_with ( "/*" ) {
185+ let src = src[ line_start..line_starts. last ( ) . unwrap ( ) . to_usize ( ) ] . trim_start ( ) ;
186+ let mut tokens = tokenize ( src) ;
187+ return src[ ..tokens. next ( ) . unwrap ( ) . len ]
188+ . to_ascii_uppercase ( )
189+ . contains ( "SAFETY:" )
190+ && tokens. all ( |t| t. kind == TokenKind :: Whitespace ) ;
196191 }
197-
198- if self . macro_expansion {
199- span_lint_and_help (
200- cx,
201- UNDOCUMENTED_UNSAFE_BLOCKS ,
202- span,
203- "unsafe block in macro expansion missing a safety comment" ,
204- None ,
205- "consider adding a safety comment in the macro definition" ,
206- ) ;
207- } else {
208- let block_indent = indent_of ( cx, span) ;
209- let suggestion = format ! ( "// SAFETY: ...\n {}" , snippet( cx, span, ".." ) ) ;
210-
211- span_lint_and_sugg (
212- cx,
213- UNDOCUMENTED_UNSAFE_BLOCKS ,
214- span,
215- "unsafe block missing a safety comment" ,
216- "consider adding a safety comment" ,
217- reindent_multiline ( Cow :: Borrowed ( & suggestion) , true , block_indent) . to_string ( ) ,
218- Applicability :: HasPlaceholders ,
219- ) ;
192+ match lines. next ( ) {
193+ Some ( x) => ( line_start, line) = x,
194+ None => return false ,
220195 }
221196 }
222197}
0 commit comments