1
- use clippy_utils:: diagnostics:: { span_lint_and_help, span_lint_and_sugg } ;
1
+ use clippy_utils:: diagnostics:: span_lint_and_help;
2
2
use 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 } ;
9
7
use 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 ;
14
11
15
12
declare_clippy_lint ! {
16
13
/// ### What it does
17
14
/// Checks for `unsafe` blocks without a `// SAFETY: ` comment
18
15
/// explaining why the unsafe operations performed inside
19
16
/// the block are safe.
20
17
///
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
+ ///
21
36
/// ### Why is this bad?
22
37
/// Undocumented unsafe blocks can make it difficult to
23
38
/// read and maintain code, as well as uncover unsoundness
@@ -44,179 +59,139 @@ declare_clippy_lint! {
44
59
"creating an unsafe block without explaining why it is safe"
45
60
}
46
61
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 ] ) ;
61
63
62
64
impl LateLintPass < ' _ > for UndocumentedUnsafeBlocks {
63
65
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
+ } ;
78
78
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
+ ) ;
102
87
}
103
88
}
89
+ }
104
90
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" ) )
113
100
}
114
101
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
120
131
}
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
121
150
}
122
151
}
123
152
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 ,
183
177
}
184
-
185
- pos += token. len ;
186
178
}
187
-
188
- Some ( false )
189
179
}
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 ) ;
196
191
}
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 ,
220
195
}
221
196
}
222
197
}
0 commit comments