@@ -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