@@ -4,27 +4,40 @@ use std::collections::BTreeMap;
4
4
use clippy_utils:: diagnostics:: span_lint_hir_and_then;
5
5
use clippy_utils:: is_lint_allowed;
6
6
use itertools:: Itertools ;
7
+ use rustc_hir:: def_id:: LocalDefId ;
7
8
use rustc_hir:: intravisit:: { walk_block, walk_expr, walk_stmt, Visitor } ;
8
9
use rustc_hir:: { BlockCheckMode , Expr , ExprKind , HirId , Stmt , UnsafeSource } ;
9
10
use rustc_lint:: { LateContext , LateLintPass } ;
10
11
use rustc_session:: impl_lint_pass;
11
- use rustc_span:: def_id:: DefId ;
12
12
use rustc_span:: { sym, Span , SyntaxContext } ;
13
13
14
14
declare_clippy_lint ! {
15
15
/// ### What it does
16
16
/// Looks for macros that expand metavariables in an unsafe block.
17
17
///
18
18
/// ### Why is this bad?
19
- /// This is unsound: it allows the user of the macro to write unsafe code outside of an
20
- /// unsafe block at callsite, potentially invoking undefined behavior in safe code.
19
+ /// This hides an unsafe block and allows the user of the macro to write unsafe code without an explicit
20
+ /// unsafe block at callsite, making it possible to perform unsafe operations in seemingly safe code.
21
+ ///
22
+ /// The macro should be restructured so that these metavariables are referenced outside of unsafe blocks
23
+ /// and that the usual unsafety checks apply to the macro argument.
24
+ ///
25
+ /// This is usually done by binding it to a variable outside of the unsafe block
26
+ /// and then using that variable inside of the block as shown in the example, or by referencing it a second time
27
+ /// in a safe context, e.g. `if false { $expr }`.
21
28
///
22
29
/// ### Known limitations
23
30
/// Due to how macros are represented in the compiler at the time Clippy runs its lints,
24
31
/// it's not possible to look for metavariables in macro definitions directly.
25
32
///
26
- /// Instead, this lint looks at expansions of macros defined in the same crate.
27
- /// This leads to false negatives when a macro is never actually invoked.
33
+ /// Instead, this lint looks at expansions of macros.
34
+ /// This leads to false negatives for macros that are never actually invoked.
35
+ ///
36
+ /// By default, this lint is rather conservative and will only emit warnings on publicly-exported
37
+ /// macros from the same crate, because oftentimes private internal macros are one-off macros where
38
+ /// this lint would just be noise (e.g. macros that generate `impl` blocks).
39
+ /// The default behavior should help with preventing a high number of such false positives,
40
+ /// however it can be configured to also emit warnings in private macros if desired.
28
41
///
29
42
/// ### Example
30
43
/// ```no_run
@@ -65,19 +78,21 @@ declare_clippy_lint! {
65
78
/// assert_eq!(*first!(std::hint::unreachable_unchecked() as &[i32]), 1);
66
79
/// ```
67
80
#[ clippy:: version = "1.77.0" ]
68
- pub EXPR_METAVARS_IN_UNSAFE ,
69
- nursery ,
70
- "expanding expr metavariables in an unsafe block"
81
+ pub MACRO_METAVARS_IN_UNSAFE ,
82
+ suspicious ,
83
+ "expanding macro metavariables in an unsafe block"
71
84
}
85
+ impl_lint_pass ! ( ExprMetavarsInUnsafe => [ MACRO_METAVARS_IN_UNSAFE ] ) ;
72
86
73
87
#[ derive( Clone , Debug ) ]
74
- enum MetavarState {
88
+ pub enum MetavarState {
75
89
ReferencedInUnsafe { unsafe_blocks : Vec < HirId > } ,
76
90
ReferencedInSafe ,
77
91
}
78
92
79
93
#[ derive( Default ) ]
80
94
pub struct ExprMetavarsInUnsafe {
95
+ pub warn_unsafe_macro_metavars_in_private_macros : bool ,
81
96
/// A metavariable can be expanded more than once, potentially across multiple bodies, so it
82
97
/// requires some state kept across HIR nodes to make it possible to delay a warning
83
98
/// and later undo:
@@ -91,21 +106,27 @@ pub struct ExprMetavarsInUnsafe {
91
106
/// }
92
107
/// }
93
108
/// ```
94
- metavar_expns : BTreeMap < Span , MetavarState > ,
109
+ pub metavar_expns : BTreeMap < Span , MetavarState > ,
95
110
}
96
- impl_lint_pass ! ( ExprMetavarsInUnsafe => [ EXPR_METAVARS_IN_UNSAFE ] ) ;
97
111
98
- struct BodyVisitor < ' a > {
99
- /// The top item always represents the last seen unsafe block
112
+ struct BodyVisitor < ' a , ' tcx > {
113
+ /// Stack of unsafe blocks -- the top item always represents the last seen unsafe block from
114
+ /// within a relevant macro.
100
115
macro_unsafe_blocks : Vec < HirId > ,
101
- /// When this is >0, it means that the node in the visitor currently being visited is "within" a
102
- /// macro definition. This helps reduce the number of spans we need to insert into the map,
103
- /// since only spans from macros are relevant.
116
+ /// When this is >0, it means that the node currently being visited is "within" a
117
+ /// macro definition. This is not necessary for correctness, it merely helps reduce the number
118
+ /// of spans we need to insert into the map, since only spans from macros are relevant.
104
119
expn_depth : u32 ,
105
- metavar_map : & ' a mut BTreeMap < Span , MetavarState > ,
120
+ cx : & ' a LateContext < ' tcx > ,
121
+ lint : & ' a mut ExprMetavarsInUnsafe ,
122
+ }
123
+
124
+ fn is_public_macro ( cx : & LateContext < ' _ > , def_id : LocalDefId ) -> bool {
125
+ ( cx. effective_visibilities . is_exported ( def_id) || cx. tcx . has_attr ( def_id, sym:: macro_export) )
126
+ && !cx. tcx . is_doc_hidden ( def_id)
106
127
}
107
128
108
- impl < ' a , ' tcx > Visitor < ' tcx > for BodyVisitor < ' a > {
129
+ impl < ' a , ' tcx > Visitor < ' tcx > for BodyVisitor < ' a , ' tcx > {
109
130
fn visit_stmt ( & mut self , s : & ' tcx Stmt < ' tcx > ) {
110
131
let from_expn = s. span . from_expansion ( ) ;
111
132
if from_expn {
@@ -123,15 +144,17 @@ impl<'a, 'tcx> Visitor<'tcx> for BodyVisitor<'a> {
123
144
if let ExprKind :: Block ( block, _) = e. kind
124
145
&& let BlockCheckMode :: UnsafeBlock ( UnsafeSource :: UserProvided ) = block. rules
125
146
&& !ctxt. is_root ( )
126
- && ctxt. outer_expn_data ( ) . macro_def_id . is_some_and ( DefId :: is_local)
147
+ && let Some ( macro_def_id) = ctxt. outer_expn_data ( ) . macro_def_id
148
+ && let Some ( macro_def_id) = macro_def_id. as_local ( )
149
+ && ( self . lint . warn_unsafe_macro_metavars_in_private_macros || is_public_macro ( self . cx , macro_def_id) )
127
150
{
128
151
self . macro_unsafe_blocks . push ( block. hir_id ) ;
129
152
walk_block ( self , block) ;
130
153
self . macro_unsafe_blocks . pop ( ) ;
131
154
} else if ctxt. is_root ( ) && self . expn_depth > 0 {
132
155
let unsafe_block = self . macro_unsafe_blocks . last ( ) . copied ( ) ;
133
156
134
- match ( self . metavar_map . entry ( e. span ) , unsafe_block) {
157
+ match ( self . lint . metavar_expns . entry ( e. span ) , unsafe_block) {
135
158
( Entry :: Vacant ( e) , None ) => {
136
159
e. insert ( MetavarState :: ReferencedInSafe ) ;
137
160
} ,
@@ -164,7 +187,7 @@ impl<'a, 'tcx> Visitor<'tcx> for BodyVisitor<'a> {
164
187
165
188
impl < ' tcx > LateLintPass < ' tcx > for ExprMetavarsInUnsafe {
166
189
fn check_body ( & mut self , cx : & LateContext < ' tcx > , body : & ' tcx rustc_hir:: Body < ' tcx > ) {
167
- if is_lint_allowed ( cx, EXPR_METAVARS_IN_UNSAFE , body. value . hir_id ) {
190
+ if is_lint_allowed ( cx, MACRO_METAVARS_IN_UNSAFE , body. value . hir_id ) {
168
191
return ;
169
192
}
170
193
@@ -175,7 +198,8 @@ impl<'tcx> LateLintPass<'tcx> for ExprMetavarsInUnsafe {
175
198
#[ expect( clippy:: bool_to_int_with_if) ] // obfuscates the meaning
176
199
expn_depth : if body. value . span . from_expansion ( ) { 1 } else { 0 } ,
177
200
macro_unsafe_blocks : Vec :: new ( ) ,
178
- metavar_map : & mut self . metavar_expns ,
201
+ lint : self ,
202
+ cx
179
203
} ;
180
204
vis. visit_body ( body) ;
181
205
}
@@ -205,37 +229,28 @@ impl<'tcx> LateLintPass<'tcx> for ExprMetavarsInUnsafe {
205
229
// The invocation doesn't matter. Also we want to dedupe by the unsafe block and not by anything
206
230
// related to the callsite.
207
231
let span = cx. tcx . hir ( ) . span ( id) ;
208
- let macro_def_id = span. ctxt ( ) . outer_expn_data ( ) . macro_def_id . and_then ( DefId :: as_local) ;
209
- (
210
- id,
211
- Span :: new ( span. lo ( ) , span. hi ( ) , SyntaxContext :: root ( ) , None ) ,
212
- macro_def_id,
213
- )
232
+
233
+ ( id, Span :: new ( span. lo ( ) , span. hi ( ) , SyntaxContext :: root ( ) , None ) )
214
234
} )
215
- . dedup_by ( |( _, a, _) , ( _, b, _) | a == b) ;
216
-
217
- for ( id, span, def_id) in bad_unsafe_blocks {
218
- if let Some ( def_id) = def_id
219
- && ( cx. effective_visibilities . is_exported ( def_id) || cx. tcx . has_attr ( def_id, sym:: macro_export) )
220
- && !cx. tcx . is_doc_hidden ( def_id)
221
- {
222
- span_lint_hir_and_then (
223
- cx,
224
- EXPR_METAVARS_IN_UNSAFE ,
225
- id,
226
- span,
227
- "this unsafe block in a macro expands `expr` metavariables" ,
228
- |diag| {
229
- diag. note ( "this allows the user of the macro to write unsafe code outside of an unsafe block" ) ;
230
- diag. help (
235
+ . dedup_by ( |& ( _, a) , & ( _, b) | a == b) ;
236
+
237
+ for ( id, span) in bad_unsafe_blocks {
238
+ span_lint_hir_and_then (
239
+ cx,
240
+ MACRO_METAVARS_IN_UNSAFE ,
241
+ id,
242
+ span,
243
+ "this unsafe block in a macro expands metavariables" ,
244
+ |diag| {
245
+ diag. note ( "this allows the user of the macro to write unsafe code outside of an unsafe block" ) ;
246
+ diag. help (
231
247
"consider expanding any metavariables outside of this block, e.g. by storing them in a variable" ,
232
248
) ;
233
- diag. help (
249
+ diag. help (
234
250
"... or also expand referenced metavariables in a safe context to require an unsafe block at callsite" ,
235
251
) ;
236
- } ,
237
- ) ;
238
- }
252
+ } ,
253
+ ) ;
239
254
}
240
255
}
241
256
}
0 commit comments