Skip to content

Commit 7c0dc33

Browse files
committed
don't lint on doc(hidden) macros and metavars that are expanded twice
1 parent 490b25a commit 7c0dc33

File tree

1 file changed

+100
-25
lines changed

1 file changed

+100
-25
lines changed

clippy_lints/src/expr_metavars_in_unsafe.rs

+100-25
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
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.
@@ -24,9 +25,20 @@
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) => {
@@ -39,7 +51,7 @@
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

148188
enum 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

Comments
 (0)