@@ -4,27 +4,40 @@ use std::collections::BTreeMap;
44use clippy_utils:: diagnostics:: span_lint_hir_and_then;
55use clippy_utils:: is_lint_allowed;
66use itertools:: Itertools ;
7+ use rustc_hir:: def_id:: LocalDefId ;
78use rustc_hir:: intravisit:: { walk_block, walk_expr, walk_stmt, Visitor } ;
89use rustc_hir:: { BlockCheckMode , Expr , ExprKind , HirId , Stmt , UnsafeSource } ;
910use rustc_lint:: { LateContext , LateLintPass } ;
1011use rustc_session:: impl_lint_pass;
11- use rustc_span:: def_id:: DefId ;
1212use rustc_span:: { sym, Span , SyntaxContext } ;
1313
1414declare_clippy_lint ! {
1515 /// ### What it does
1616 /// Looks for macros that expand metavariables in an unsafe block.
1717 ///
1818 /// ### 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 }`.
2128 ///
2229 /// ### Known limitations
2330 /// Due to how macros are represented in the compiler at the time Clippy runs its lints,
2431 /// it's not possible to look for metavariables in macro definitions directly.
2532 ///
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.
2841 ///
2942 /// ### Example
3043 /// ```no_run
@@ -65,19 +78,21 @@ declare_clippy_lint! {
6578 /// assert_eq!(*first!(std::hint::unreachable_unchecked() as &[i32]), 1);
6679 /// ```
6780 #[ 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"
7184}
85+ impl_lint_pass ! ( ExprMetavarsInUnsafe => [ MACRO_METAVARS_IN_UNSAFE ] ) ;
7286
7387#[ derive( Clone , Debug ) ]
74- enum MetavarState {
88+ pub enum MetavarState {
7589 ReferencedInUnsafe { unsafe_blocks : Vec < HirId > } ,
7690 ReferencedInSafe ,
7791}
7892
7993#[ derive( Default ) ]
8094pub struct ExprMetavarsInUnsafe {
95+ pub warn_unsafe_macro_metavars_in_private_macros : bool ,
8196 /// A metavariable can be expanded more than once, potentially across multiple bodies, so it
8297 /// requires some state kept across HIR nodes to make it possible to delay a warning
8398 /// and later undo:
@@ -91,21 +106,27 @@ pub struct ExprMetavarsInUnsafe {
91106 /// }
92107 /// }
93108 /// ```
94- metavar_expns : BTreeMap < Span , MetavarState > ,
109+ pub metavar_expns : BTreeMap < Span , MetavarState > ,
95110}
96- impl_lint_pass ! ( ExprMetavarsInUnsafe => [ EXPR_METAVARS_IN_UNSAFE ] ) ;
97111
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.
100115 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.
104119 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)
106127}
107128
108- impl < ' a , ' tcx > Visitor < ' tcx > for BodyVisitor < ' a > {
129+ impl < ' a , ' tcx > Visitor < ' tcx > for BodyVisitor < ' a , ' tcx > {
109130 fn visit_stmt ( & mut self , s : & ' tcx Stmt < ' tcx > ) {
110131 let from_expn = s. span . from_expansion ( ) ;
111132 if from_expn {
@@ -123,15 +144,17 @@ impl<'a, 'tcx> Visitor<'tcx> for BodyVisitor<'a> {
123144 if let ExprKind :: Block ( block, _) = e. kind
124145 && let BlockCheckMode :: UnsafeBlock ( UnsafeSource :: UserProvided ) = block. rules
125146 && !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) )
127150 {
128151 self . macro_unsafe_blocks . push ( block. hir_id ) ;
129152 walk_block ( self , block) ;
130153 self . macro_unsafe_blocks . pop ( ) ;
131154 } else if ctxt. is_root ( ) && self . expn_depth > 0 {
132155 let unsafe_block = self . macro_unsafe_blocks . last ( ) . copied ( ) ;
133156
134- match ( self . metavar_map . entry ( e. span ) , unsafe_block) {
157+ match ( self . lint . metavar_expns . entry ( e. span ) , unsafe_block) {
135158 ( Entry :: Vacant ( e) , None ) => {
136159 e. insert ( MetavarState :: ReferencedInSafe ) ;
137160 } ,
@@ -164,7 +187,7 @@ impl<'a, 'tcx> Visitor<'tcx> for BodyVisitor<'a> {
164187
165188impl < ' tcx > LateLintPass < ' tcx > for ExprMetavarsInUnsafe {
166189 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 ) {
168191 return ;
169192 }
170193
@@ -175,7 +198,8 @@ impl<'tcx> LateLintPass<'tcx> for ExprMetavarsInUnsafe {
175198 #[ expect( clippy:: bool_to_int_with_if) ] // obfuscates the meaning
176199 expn_depth : if body. value . span . from_expansion ( ) { 1 } else { 0 } ,
177200 macro_unsafe_blocks : Vec :: new ( ) ,
178- metavar_map : & mut self . metavar_expns ,
201+ lint : self ,
202+ cx
179203 } ;
180204 vis. visit_body ( body) ;
181205 }
@@ -205,37 +229,28 @@ impl<'tcx> LateLintPass<'tcx> for ExprMetavarsInUnsafe {
205229 // The invocation doesn't matter. Also we want to dedupe by the unsafe block and not by anything
206230 // related to the callsite.
207231 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 ) )
214234 } )
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 (
231247 "consider expanding any metavariables outside of this block, e.g. by storing them in a variable" ,
232248 ) ;
233- diag. help (
249+ diag. help (
234250 "... or also expand referenced metavariables in a safe context to require an unsafe block at callsite" ,
235251 ) ;
236- } ,
237- ) ;
238- }
252+ } ,
253+ ) ;
239254 }
240255 }
241256}
0 commit comments