Skip to content

Commit 5a7ea1d

Browse files
committed
Count unsafe operations only towards the innermost unsafe block
Also, macro calls are counted as at most one unsafe operation.
1 parent 24e16f9 commit 5a7ea1d

File tree

3 files changed

+390
-66
lines changed

3 files changed

+390
-66
lines changed

clippy_lints/src/multiple_unsafe_ops_per_block.rs

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ use clippy_utils::diagnostics::span_lint_and_then;
33
use hir::def::{DefKind, Res};
44
use hir::{BlockCheckMode, ExprKind, QPath, UnOp};
55
use rustc_ast::{BorrowKind, Mutability};
6+
use rustc_data_structures::fx::FxHashMap;
67
use rustc_hir as hir;
78
use rustc_hir::intravisit::{Visitor, walk_body, walk_expr};
89
use rustc_lint::{LateContext, LateLintPass};
910
use rustc_middle::hir::nested_filter;
1011
use rustc_middle::ty::{self, TyCtxt, TypeckResults};
1112
use rustc_session::declare_lint_pass;
12-
use rustc_span::{DesugaringKind, Span};
13+
use rustc_span::Span;
1314

1415
declare_clippy_lint! {
1516
/// ### What it does
@@ -56,12 +57,16 @@ declare_clippy_lint! {
5657
/// }
5758
/// ```
5859
///
59-
/// ### Note
60+
/// ### Notes
6061
///
61-
/// Taking a raw pointer to a union field is always safe and will
62-
/// not be considered unsafe by this lint, even when linting code written
63-
/// with a specified Rust version of 1.91 or earlier (which required
64-
/// using an `unsafe` block).
62+
/// - Unsafe operations only count towards the total for the innermost
63+
/// enclosing `unsafe` block.
64+
/// - Each call to a macro expanding to unsafe operations count for one
65+
/// unsafe operation.
66+
/// - Taking a raw pointer to a union field is always safe and will
67+
/// not be considered unsafe by this lint, even when linting code written
68+
/// with a specified Rust version of 1.91 or earlier (which required
69+
/// using an `unsafe` block).
6570
#[clippy::version = "1.69.0"]
6671
pub MULTIPLE_UNSAFE_OPS_PER_BLOCK,
6772
restriction,
@@ -71,10 +76,7 @@ declare_lint_pass!(MultipleUnsafeOpsPerBlock => [MULTIPLE_UNSAFE_OPS_PER_BLOCK])
7176

7277
impl<'tcx> LateLintPass<'tcx> for MultipleUnsafeOpsPerBlock {
7378
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) {
74-
if !matches!(block.rules, BlockCheckMode::UnsafeBlock(_))
75-
|| block.span.in_external_macro(cx.tcx.sess.source_map())
76-
|| block.span.is_desugaring(DesugaringKind::Await)
77-
{
79+
if !matches!(block.rules, BlockCheckMode::UnsafeBlock(_)) || block.span.from_expansion() {
7880
return;
7981
}
8082
let unsafe_ops = UnsafeExprCollector::collect_unsafe_exprs(cx, block);
@@ -100,18 +102,41 @@ impl<'tcx> LateLintPass<'tcx> for MultipleUnsafeOpsPerBlock {
100102
struct UnsafeExprCollector<'tcx> {
101103
tcx: TyCtxt<'tcx>,
102104
typeck_results: &'tcx TypeckResults<'tcx>,
103-
unsafe_ops: Vec<(&'static str, Span)>,
105+
unsafe_ops: FxHashMap<Span, &'static str>,
104106
}
105107

106108
impl<'tcx> UnsafeExprCollector<'tcx> {
107109
fn collect_unsafe_exprs(cx: &LateContext<'tcx>, block: &'tcx hir::Block<'tcx>) -> Vec<(&'static str, Span)> {
108110
let mut collector = Self {
109111
tcx: cx.tcx,
110112
typeck_results: cx.typeck_results(),
111-
unsafe_ops: vec![],
113+
unsafe_ops: FxHashMap::default(),
112114
};
113115
collector.visit_block(block);
114-
collector.unsafe_ops
116+
#[allow(
117+
rustc::potential_query_instability,
118+
reason = "span ordering only needed inside the one expression being walked"
119+
)]
120+
let mut unsafe_ops = collector
121+
.unsafe_ops
122+
.into_iter()
123+
.map(|(span, msg)| (msg, span))
124+
.collect::<Vec<_>>();
125+
unsafe_ops.sort_unstable();
126+
unsafe_ops
127+
}
128+
}
129+
130+
impl UnsafeExprCollector<'_> {
131+
fn insert_span(&mut self, span: Span, message: &'static str) {
132+
if span.from_expansion() {
133+
self.unsafe_ops.insert(
134+
span.source_callsite(),
135+
"this macro call expands into one or more unsafe operations",
136+
);
137+
} else {
138+
self.unsafe_ops.insert(span, message);
139+
}
115140
}
116141
}
117142

@@ -126,7 +151,10 @@ impl<'tcx> Visitor<'tcx> for UnsafeExprCollector<'tcx> {
126151
return self.visit_expr(e);
127152
},
128153

129-
ExprKind::InlineAsm(_) => self.unsafe_ops.push(("inline assembly used here", expr.span)),
154+
// Do not recurse inside an inner `unsafe` block, it will be checked on its own
155+
ExprKind::Block(block, _) if matches!(block.rules, BlockCheckMode::UnsafeBlock(_)) => return,
156+
157+
ExprKind::InlineAsm(_) => self.insert_span(expr.span, "inline assembly used here"),
130158

131159
ExprKind::AddrOf(BorrowKind::Raw, _, mut inner) => {
132160
while let ExprKind::Field(prefix, _) = inner.kind
@@ -139,7 +167,7 @@ impl<'tcx> Visitor<'tcx> for UnsafeExprCollector<'tcx> {
139167

140168
ExprKind::Field(e, _) => {
141169
if self.typeck_results.expr_ty(e).is_union() {
142-
self.unsafe_ops.push(("union field access occurs here", expr.span));
170+
self.insert_span(expr.span, "union field access occurs here");
143171
}
144172
},
145173

@@ -157,12 +185,11 @@ impl<'tcx> Visitor<'tcx> for UnsafeExprCollector<'tcx> {
157185
..
158186
},
159187
)) => {
160-
self.unsafe_ops
161-
.push(("access of a mutable static occurs here", expr.span));
188+
self.insert_span(expr.span, "access of a mutable static occurs here");
162189
},
163190

164191
ExprKind::Unary(UnOp::Deref, e) if self.typeck_results.expr_ty(e).is_raw_ptr() => {
165-
self.unsafe_ops.push(("raw pointer dereference occurs here", expr.span));
192+
self.insert_span(expr.span, "raw pointer dereference occurs here");
166193
},
167194

168195
ExprKind::Call(path_expr, _) => {
@@ -172,7 +199,7 @@ impl<'tcx> Visitor<'tcx> for UnsafeExprCollector<'tcx> {
172199
_ => None,
173200
};
174201
if opt_sig.is_some_and(|sig| sig.safety().is_unsafe()) {
175-
self.unsafe_ops.push(("unsafe function call occurs here", expr.span));
202+
self.insert_span(expr.span, "unsafe function call occurs here");
176203
}
177204
},
178205

@@ -182,7 +209,7 @@ impl<'tcx> Visitor<'tcx> for UnsafeExprCollector<'tcx> {
182209
.type_dependent_def_id(expr.hir_id)
183210
.map(|def_id| self.tcx.fn_sig(def_id));
184211
if opt_sig.is_some_and(|sig| sig.skip_binder().safety().is_unsafe()) {
185-
self.unsafe_ops.push(("unsafe method call occurs here", expr.span));
212+
self.insert_span(expr.span, "unsafe method call occurs here");
186213
}
187214
},
188215

@@ -203,8 +230,7 @@ impl<'tcx> Visitor<'tcx> for UnsafeExprCollector<'tcx> {
203230
}
204231
))
205232
) {
206-
self.unsafe_ops
207-
.push(("modification of a mutable static occurs here", expr.span));
233+
self.insert_span(expr.span, "modification of a mutable static occurs here");
208234
return self.visit_expr(rhs);
209235
}
210236
},

tests/ui/multiple_unsafe_ops_per_block.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,137 @@ fn check_closures() {
312312
}
313313
}
314314

315+
fn issue16116() {
316+
unsafe fn foo() -> u32 {
317+
0
318+
}
319+
320+
// Do not lint even though `format!` expansion
321+
// contains unsafe calls.
322+
unsafe {
323+
let _ = format!("{}", foo());
324+
}
325+
326+
unsafe {
327+
//~^ multiple_unsafe_ops_per_block
328+
let _ = format!("{}", foo());
329+
let _ = format!("{}", foo());
330+
}
331+
332+
// Do not lint: only one `assert!()` argument is unsafe
333+
unsafe {
334+
assert_eq!(foo(), 0, "{}", 1 + 2);
335+
}
336+
337+
// Each argument of a macro call may count as an unsafe operation.
338+
unsafe {
339+
//~^ multiple_unsafe_ops_per_block
340+
assert_eq!(foo(), 0, "{}", foo()); // One unsafe operation
341+
}
342+
343+
macro_rules! twice {
344+
($e:expr) => {{
345+
$e;
346+
$e;
347+
}};
348+
}
349+
350+
// Do not lint, a repeated argument used twice by a macro counts
351+
// as at most one unsafe operation.
352+
unsafe {
353+
twice!(foo());
354+
}
355+
356+
unsafe {
357+
//~^ multiple_unsafe_ops_per_block
358+
twice!(foo());
359+
twice!(foo());
360+
}
361+
362+
unsafe {
363+
//~^ multiple_unsafe_ops_per_block
364+
assert_eq!(foo(), 0, "{}", 1 + 2);
365+
assert_eq!(foo(), 0, "{}", 1 + 2);
366+
}
367+
368+
macro_rules! unsafe_twice {
369+
($e:expr) => {
370+
unsafe {
371+
$e;
372+
$e;
373+
}
374+
};
375+
};
376+
377+
// A macro whose expansion contains unsafe blocks will not
378+
// check inside the blocks.
379+
unsafe {
380+
unsafe_twice!(foo());
381+
}
382+
383+
macro_rules! double_non_arg_unsafe {
384+
() => {{
385+
_ = str::from_utf8_unchecked(&[]);
386+
_ = str::from_utf8_unchecked(&[]);
387+
}};
388+
}
389+
390+
// Do not lint: each unsafe expression contained in the
391+
// macro expansion will count towards the macro call.
392+
unsafe {
393+
double_non_arg_unsafe!();
394+
}
395+
396+
unsafe {
397+
//~^ multiple_unsafe_ops_per_block
398+
double_non_arg_unsafe!();
399+
double_non_arg_unsafe!();
400+
}
401+
402+
// Do not lint: the inner macro call counts as one unsafe op.
403+
unsafe {
404+
assert_eq!(double_non_arg_unsafe!(), ());
405+
}
406+
407+
unsafe {
408+
//~^ multiple_unsafe_ops_per_block
409+
assert_eq!(double_non_arg_unsafe!(), ());
410+
assert_eq!(double_non_arg_unsafe!(), ());
411+
}
412+
413+
unsafe {
414+
//~^ multiple_unsafe_ops_per_block
415+
assert_eq!((double_non_arg_unsafe!(), double_non_arg_unsafe!()), ((), ()));
416+
}
417+
418+
macro_rules! unsafe_with_arg {
419+
($e:expr) => {{
420+
_ = str::from_utf8_unchecked(&[]);
421+
$e;
422+
}};
423+
}
424+
425+
// A confusing situation: the macro call counts towards two unsafe calls,
426+
// one coming from inside the macro itself, and one coming from its argument.
427+
// The error message may seem a bit strange as both the macro call and its
428+
// argument will be marked as counting as unsafe ops, but a short investigation
429+
// in those rare situations should sort it out easily.
430+
unsafe {
431+
//~^ multiple_unsafe_ops_per_block
432+
unsafe_with_arg!(foo());
433+
}
434+
435+
macro_rules! ignore {
436+
($e: expr) => {};
437+
}
438+
439+
// Another surprising case is when the macro argument is not
440+
// used in the expansion, but in this case we won't see the
441+
// unsafe operation at all.
442+
unsafe {
443+
ignore!(foo());
444+
ignore!(foo());
445+
}
446+
}
447+
315448
fn main() {}

0 commit comments

Comments
 (0)