1515//! The first delimiter, up to the `=>`, is the rule itself, and we look for
1616//! any `$ident:expr` metavariable declarations (implemented in
1717//! [`UnsafeMetavariableFinder::collect_expr_metavariables_in_rule`]).
18+ //! All metavariables start out as [`MetavarState::Unreferenced`] at this point.
1819//!
1920//! Once we have those, we know the next token after the delimited rule is the `=>` and after that
2021//! we have the delimited macro rule body.
2425//! The metavariable finder stores a stack of [`EnclosingNode`]. The top always tells us if we're
2526//! within an unsafe block, and we push `EnclosingNode::UnsafeBlock` when we encounter `unsafe {`
2627//! (and pop from it at the right time when leaving the delimited block).
27- //! So, if we find `$x` anywhere in the body and the last item is an unsafe block, emit a lint.
2828//!
29- //! Note that it's possible to be inside of an unsafe block syntactically, but not semantically:
29+ //! If we find a `$x` anywhere in the body and the last item is an unsafe block, this tells us that
30+ //! this is likely something we want to lint, however we don't emit a warning right away,
31+ //! but update the metavar to [`MetavarState::Referenced`].
32+ //! Only at the end of the lint will we emit warnings for each `MetavarState::Referenced`.
33+ //!
34+ //! The reason for that is that it's possible that we might come across the same metavariable twice:
35+ //! Once in an unsafe block, and then outside of one. There's no reason to lint that because the
36+ //! user *does* need an unsafe block at call site to write unsafe code.
37+ //! So if we do come across a metavar reference outside of an unsafe block, we have to update it to
38+ //! `MetavarState::Suppressed` (indicating that this should never be linted or updated).
39+ //!
40+ //! Note also that it's possible to be inside of an unsafe block syntactically, but not
41+ //! semantically:
3042//! ```ignore
3143//! macro_rules! x {
3244//! ($v:expr) => {
3951//! ```
4052//! The `$v` is in an unsafe block, but that is *not* considered by the unsafety checker because
4153//! it's part of another body.
42- //! This is handled by having `EnclosingNode::Body`. In the above example, `:: Body` is at the top,
54+ //! This is handled by having [ `EnclosingNode::Body`] . In the above example, `Body` is at the top,
4355//! so it's not linted.
4456//!
4557//! There are other edge cases for bodies (such as const generics, e.g. `Ty<{ $v > 3 }>`),
@@ -139,10 +151,38 @@ fn extract_metavariable<'a>(
139151 }
140152}
141153
142- struct UnsafeMetavariableFinder < ' a , ' tcx > {
154+ enum MetavarState {
155+ /// The metavariable was referenced inside of an unsafe block
156+ Referenced {
157+ /// unsafe { $v }
158+ /// ^^
159+ reference_span : Span ,
160+ /// macro_rules! x { (... $v:expr ...) => {} }
161+ /// ^^^^^^^
162+ decl_span : Span ,
163+ /// unsafe { $v }
164+ /// ^^^^^^^^
165+ unsafe_bl_span : Span ,
166+ } ,
167+
168+ /// The metavariable was not found yet, but we're still looking for it
169+ Unreferenced { decl_span : Span } ,
170+
171+ /// The metavariable was found, but was then suppressed.
172+ ///
173+ /// For example:
174+ /// ```ignore
175+ /// unsafe { $v; }
176+ /// ^^ Found reference to $v in unsafe block, set to Referenced
177+ /// $v;
178+ /// ^^ ... then later referenced outside of an `unsafe {}` block, set to Suppressed and avoid linting
179+ /// ```
180+ Suppressed ,
181+ }
182+
183+ struct UnsafeMetavariableFinder {
143184 enclosing : Vec < EnclosingNode > ,
144- cx : & ' a LateContext < ' tcx > ,
145- expr_variables : FxHashMap < Symbol , Span > ,
185+ expr_variables : FxHashMap < Symbol , MetavarState > ,
146186}
147187
148188enum EnclosingNode {
@@ -163,7 +203,7 @@ enum EnclosingNode {
163203 UnsafeBlock ( Span ) ,
164204}
165205
166- impl < ' a , ' tcx > UnsafeMetavariableFinder < ' a , ' tcx > {
206+ impl UnsafeMetavariableFinder {
167207 fn clear ( & mut self ) {
168208 self . expr_variables . clear ( ) ;
169209 self . enclosing . clear ( ) ;
@@ -174,7 +214,7 @@ impl<'a, 'tcx> UnsafeMetavariableFinder<'a, 'tcx> {
174214 /// macro_rules! x { (... $var:expr ...) => { ... } }
175215 /// ^^^^^^^^^^^^^^^^^^^
176216 /// ```
177- /// Returns the variable `var`
217+ /// Puts `var` in the variable map.
178218 fn collect_expr_metavariables_in_rule ( & mut self , tts : & TokenStream ) {
179219 let mut tts = tts. trees ( ) . peekable ( ) ;
180220 while let Some ( tt) = tts. next ( ) {
@@ -195,7 +235,12 @@ impl<'a, 'tcx> UnsafeMetavariableFinder<'a, 'tcx> {
195235 _,
196236 ) ) = tts. next ( )
197237 {
198- self . expr_variables . insert ( sym, start_span. to ( end_span) ) ;
238+ self . expr_variables . insert (
239+ sym,
240+ MetavarState :: Unreferenced {
241+ decl_span : start_span. to ( end_span) ,
242+ } ,
243+ ) ;
199244 }
200245 } ,
201246 TokenTree :: Delimited ( .., body) => self . collect_expr_metavariables_in_rule ( body) ,
@@ -325,23 +370,29 @@ impl<'a, 'tcx> UnsafeMetavariableFinder<'a, 'tcx> {
325370 self . find_metavariables_in_fn ( & mut tts) ;
326371 }
327372 } else if let Some ( ( sym, span) ) = extract_metavariable ( tt, & mut tts)
328- && let Some ( decl_span) = self . expr_variables . get ( & sym) . copied ( )
329- && let Some ( & EnclosingNode :: UnsafeBlock ( unsafe_bl_span) ) = self . enclosing . last ( )
373+ && let Some ( state) = self . expr_variables . get_mut ( & sym)
330374 {
331- span_lint_and_then (
332- self . cx ,
333- EXPR_METAVARS_IN_UNSAFE ,
334- span,
335- "expanding an expression metavariable in an unsafe block" ,
336- |diag| {
337- diag. span_note ( decl_span, "metavariable declared here" ) ;
338- diag. span_note ( unsafe_bl_span, "unsafe block entered here" ) ;
339- diag. help (
340- "consider expanding it outside of this block, e.g. by storing it in a variable" ,
341- ) ;
342- diag. note ( "this allows the user of the macro to write unsafe code without an unsafe block (at the callsite)" ) ;
375+ let unsafe_block_span = self . enclosing . last ( ) . and_then ( |v| match v {
376+ EnclosingNode :: Body => None ,
377+ EnclosingNode :: UnsafeBlock ( span) => Some ( * span) ,
378+ } ) ;
379+
380+ match ( & * state, unsafe_block_span) {
381+ ( MetavarState :: Referenced { .. } , Some ( _) ) => { } ,
382+ ( _, None ) => {
383+ // Metavar was referenced outside of an unsafe block. Set to
384+ // suppressed because we don't want to lint.
385+ * state = MetavarState :: Suppressed ;
343386 } ,
344- ) ;
387+ ( MetavarState :: Unreferenced { decl_span } , Some ( unsafe_bl_span) ) => {
388+ * state = MetavarState :: Referenced {
389+ reference_span : span,
390+ decl_span : * decl_span,
391+ unsafe_bl_span,
392+ } ;
393+ } ,
394+ ( MetavarState :: Suppressed , _) => { } ,
395+ }
345396 }
346397 } ,
347398 TokenTree :: Delimited ( .., body) => self . find_metavariables_in_body ( body) ,
@@ -361,12 +412,12 @@ impl<'tcx> LateLintPass<'tcx> for ExprMetavarsInUnsafe {
361412 if let ItemKind :: Macro ( def, MacroKind :: Bang ) = item. kind
362413 && let def_id = item. owner_id . def_id
363414 && ( cx. effective_visibilities . is_exported ( def_id) || cx. tcx . has_attr ( def_id, sym:: macro_export) )
415+ && !cx. tcx . is_doc_hidden ( def_id)
364416 {
365417 let mut tts = def. body . tokens . trees ( ) . peekable ( ) ;
366418 let mut finder = UnsafeMetavariableFinder {
367419 enclosing : Vec :: new ( ) ,
368420 expr_variables : FxHashMap :: default ( ) ,
369- cx,
370421 } ;
371422
372423 while let Some ( tt) = tts. next ( ) {
@@ -401,6 +452,30 @@ impl<'tcx> LateLintPass<'tcx> for ExprMetavarsInUnsafe {
401452 . is_some ( )
402453 { }
403454 }
455+
456+ for metavar in finder. expr_variables . values ( ) {
457+ if let & MetavarState :: Referenced {
458+ decl_span,
459+ reference_span,
460+ unsafe_bl_span,
461+ } = metavar
462+ {
463+ span_lint_and_then (
464+ cx,
465+ EXPR_METAVARS_IN_UNSAFE ,
466+ reference_span,
467+ "expanding an expression metavariable in an unsafe block" ,
468+ |diag| {
469+ diag. span_note ( decl_span, "metavariable declared here" ) ;
470+ diag. span_note ( unsafe_bl_span, "unsafe block entered here" ) ;
471+ diag. help ( "consider expanding it outside of this block, e.g. by storing it in a variable" ) ;
472+ diag. note (
473+ "this allows the user of the macro to write unsafe code without an unsafe block (at the callsite)" ,
474+ ) ;
475+ } ,
476+ ) ;
477+ }
478+ }
404479 }
405480 }
406481}
0 commit comments