Skip to content

Commit 8f8689f

Browse files
committed
Improve unused_unsafe lint
Main motivation: Fixes some issues with the current behavior. This PR is more-or-less completely re-implementing the unused_unsafe lint; it’s also only done in the MIR-version of the lint, the set of tests for the `-Zthir-unsafeck` version no longer succeeds (and is thus disabled, see `lint-unused-unsafe.rs`). On current nightly, ```rs unsafe fn unsf() {} fn inner_ignored() { unsafe { #[allow(unused_unsafe)] unsafe { unsf() } } } ``` doesn’t create any warnings. This situation is not unrealistic to come by, the inner `unsafe` block could e.g. come from a macro. Actually, this PR even includes removal of one unused `unsafe` in the standard library that was missed in a similar situation. (The inner `unsafe` coming from an external macro hides the warning, too.) The reason behind this problem is how the check currently works: * While generating MIR, it already skips nested unsafe blocks (i.e. unsafe nested in other unsafe) so that the inner one is always the one considered unused * To differentiate the cases of no unsafe operations inside the `unsafe` vs. a surrounding `unsafe` block, there’s some ad-hoc magic walking up the HIR to look for surrounding used `unsafe` blocks. There’s a lot of problems with this approach besides the one presented above. E.g. the MIR-building uses checks for `unsafe_op_in_unsafe_fn` lint to decide early whether or not `unsafe` blocks in an `unsafe fn` are redundant and ought to be removed. ```rs unsafe fn granular_disallow_op_in_unsafe_fn() { unsafe { #[deny(unsafe_op_in_unsafe_fn)] { unsf(); } } } ``` ``` error: call to unsafe function is unsafe and requires unsafe block (error E0133) --> src/main.rs:13:13 | 13 | unsf(); | ^^^^^^ call to unsafe function | note: the lint level is defined here --> src/main.rs:11:16 | 11 | #[deny(unsafe_op_in_unsafe_fn)] | ^^^^^^^^^^^^^^^^^^^^^^ = note: consult the function's documentation for information on how to avoid undefined behavior warning: unnecessary `unsafe` block --> src/main.rs:10:5 | 9 | unsafe fn granular_disallow_op_in_unsafe_fn() { | --------------------------------------------- because it's nested under this `unsafe` fn 10 | unsafe { | ^^^^^^ unnecessary `unsafe` block | = note: `#[warn(unused_unsafe)]` on by default ``` Here, the intermediate `unsafe` was ignored, even though it contains a unsafe operation that is not allowed to happen in an `unsafe fn` without an additional `unsafe` block. Also closures were problematic and the workaround/algorithms used on current nightly didn’t work properly. (I skipped trying to fully understand what it was supposed to do, because this PR uses a completely different approach.) ```rs fn nested() { unsafe { unsafe { unsf() } } } ``` ``` warning: unnecessary `unsafe` block --> src/main.rs:10:9 | 9 | unsafe { | ------ because it's nested under this `unsafe` block 10 | unsafe { unsf() } | ^^^^^^ unnecessary `unsafe` block | = note: `#[warn(unused_unsafe)]` on by default ``` vs ```rs fn nested() { let _ = || unsafe { let _ = || unsafe { unsf() }; }; } ``` ``` warning: unnecessary `unsafe` block --> src/main.rs:9:16 | 9 | let _ = || unsafe { | ^^^^^^ unnecessary `unsafe` block | = note: `#[warn(unused_unsafe)]` on by default warning: unnecessary `unsafe` block --> src/main.rs:10:20 | 10 | let _ = || unsafe { unsf() }; | ^^^^^^ unnecessary `unsafe` block ``` *note that this warning kind-of suggests that **both** unsafe blocks are redundant* -------------------------------------------------------------------------------- I also dislike the fact that it always suggests keeping the outermost `unsafe`. E.g. for ```rs fn granularity() { unsafe { unsafe { unsf() } unsafe { unsf() } unsafe { unsf() } } } ``` I prefer if `rustc` suggests removing the more-course outer-level `unsafe` instead of the fine-grained inner `unsafe` blocks, which it currently does on nightly: ``` warning: unnecessary `unsafe` block --> src/main.rs:10:9 | 9 | unsafe { | ------ because it's nested under this `unsafe` block 10 | unsafe { unsf() } | ^^^^^^ unnecessary `unsafe` block | = note: `#[warn(unused_unsafe)]` on by default warning: unnecessary `unsafe` block --> src/main.rs:11:9 | 9 | unsafe { | ------ because it's nested under this `unsafe` block 10 | unsafe { unsf() } 11 | unsafe { unsf() } | ^^^^^^ unnecessary `unsafe` block warning: unnecessary `unsafe` block --> src/main.rs:12:9 | 9 | unsafe { | ------ because it's nested under this `unsafe` block ... 12 | unsafe { unsf() } | ^^^^^^ unnecessary `unsafe` block ``` -------------------------------------------------------------------------------- Needless to say, this PR addresses all these points. For context, as far as my understanding goes, the main advantage of skipping inner unsafe blocks was that a test case like ```rs fn top_level_used() { unsafe { unsf(); unsafe { unsf() } unsafe { unsf() } unsafe { unsf() } } } ``` should generate some warning because there’s redundant nested `unsafe`, however every single `unsafe` block _does_ contain some statement that uses it. Of course this PR doesn’t aim change the warnings on this kind of code example, because the current behavior, warning on all the inner `unsafe` blocks, makes sense in this case. As mentioned, during MIR building all the unsafe blocks *are* kept now, and usage is attributed to them. The way to still generate a warning like ``` warning: unnecessary `unsafe` block --> src/main.rs:11:9 | 9 | unsafe { | ------ because it's nested under this `unsafe` block 10 | unsf(); 11 | unsafe { unsf() } | ^^^^^^ unnecessary `unsafe` block | = note: `#[warn(unused_unsafe)]` on by default warning: unnecessary `unsafe` block --> src/main.rs:12:9 | 9 | unsafe { | ------ because it's nested under this `unsafe` block ... 12 | unsafe { unsf() } | ^^^^^^ unnecessary `unsafe` block warning: unnecessary `unsafe` block --> src/main.rs:13:9 | 9 | unsafe { | ------ because it's nested under this `unsafe` block ... 13 | unsafe { unsf() } | ^^^^^^ unnecessary `unsafe` block ``` in this case is by emitting a `unused_unsafe` warning for all of the `unsafe` blocks that are _within a **used** unsafe block_. The previous code had a little HIR traversal already anyways to collect a set of all the unsafe blocks (in order to afterwards determine which ones are unused afterwards). This PR uses such a traversal to do additional things including logic like _always_ warn for an `unsafe` block that’s inside of another **used** unsafe block. The traversal is expanded to include nested closures in the same go, this simplifies a lot of things. The whole logic around `unsafe_op_in_unsafe_fn` is a little complicated, there’s some test cases of corner-cases in this PR. (The implementation involves differentiating between whether a used unsafe block was used exclusively by operations where `allow(unsafe_op_in_unsafe_fn)` was active.) The main goal was to make sure that code should compile successfully if all the `unused_unsafe`-warnings are addressed _simultaneously_ (by removing the respective `unsafe` blocks) no matter how complicated the patterns of `unsafe_op_in_unsafe_fn` being disallowed and allowed throughout the function are. -------------------------------------------------------------------------------- One noteworthy design decision I took here: An `unsafe` block with `allow(unused_unsafe)` **is considered used** for the purposes of linting about redundant contained unsafe blocks. So while ```rs fn granularity() { unsafe { //~ ERROR: unnecessary `unsafe` block unsafe { unsf() } unsafe { unsf() } unsafe { unsf() } } } ``` warns for the outer `unsafe` block, ```rs fn top_level_ignored() { #[allow(unused_unsafe)] unsafe { #[deny(unused_unsafe)] { unsafe { unsf() } //~ ERROR: unnecessary `unsafe` block unsafe { unsf() } //~ ERROR: unnecessary `unsafe` block unsafe { unsf() } //~ ERROR: unnecessary `unsafe` block } } } ``` warns on the inner ones.
1 parent c1aa854 commit 8f8689f

File tree

11 files changed

+3209
-244
lines changed

11 files changed

+3209
-244
lines changed

compiler/rustc_codegen_llvm/src/abi.rs

+5-7
Original file line numberDiff line numberDiff line change
@@ -599,13 +599,11 @@ impl<'ll, 'tcx> FnAbiLlvmExt<'ll, 'tcx> for FnAbi<'tcx, Ty<'tcx>> {
599599
if self.conv == Conv::CCmseNonSecureCall {
600600
// This will probably get ignored on all targets but those supporting the TrustZone-M
601601
// extension (thumbv8m targets).
602-
unsafe {
603-
llvm::AddCallSiteAttrString(
604-
callsite,
605-
llvm::AttributePlace::Function,
606-
cstr::cstr!("cmse_nonsecure_call"),
607-
);
608-
}
602+
llvm::AddCallSiteAttrString(
603+
callsite,
604+
llvm::AttributePlace::Function,
605+
cstr::cstr!("cmse_nonsecure_call"),
606+
);
609607
}
610608
}
611609
}

compiler/rustc_middle/src/hir/nested_filter.rs

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ use rustc_hir::intravisit::nested_filter::NestedFilter;
33
/// Do not visit nested item-like things, but visit nested things
44
/// that are inside of an item-like.
55
///
6+
/// Notably, possible occurrences of bodies in non-item-like things
7+
/// include: closures/generators, inline `const {}` blocks, and
8+
/// constant arguments of types, e.g. in `let _: [(); /* HERE */];`.
9+
///
610
/// **This is the most common choice.** A very common pattern is
711
/// to use `visit_all_item_likes()` as an outer loop,
812
/// and to have the visitor that visits the contents of each item

compiler/rustc_middle/src/lint.rs

+73-62
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,77 @@ impl<'a> LintDiagnosticBuilder<'a> {
202202
}
203203
}
204204

205+
pub fn explain_lint_level_source<'s>(
206+
sess: &'s Session,
207+
lint: &'static Lint,
208+
level: Level,
209+
src: LintLevelSource,
210+
err: &mut DiagnosticBuilder<'s>,
211+
) {
212+
let name = lint.name_lower();
213+
match src {
214+
LintLevelSource::Default => {
215+
sess.diag_note_once(
216+
err,
217+
DiagnosticMessageId::from(lint),
218+
&format!("`#[{}({})]` on by default", level.as_str(), name),
219+
);
220+
}
221+
LintLevelSource::CommandLine(lint_flag_val, orig_level) => {
222+
let flag = match orig_level {
223+
Level::Warn => "-W",
224+
Level::Deny => "-D",
225+
Level::Forbid => "-F",
226+
Level::Allow => "-A",
227+
Level::ForceWarn => "--force-warn",
228+
};
229+
let hyphen_case_lint_name = name.replace('_', "-");
230+
if lint_flag_val.as_str() == name {
231+
sess.diag_note_once(
232+
err,
233+
DiagnosticMessageId::from(lint),
234+
&format!(
235+
"requested on the command line with `{} {}`",
236+
flag, hyphen_case_lint_name
237+
),
238+
);
239+
} else {
240+
let hyphen_case_flag_val = lint_flag_val.as_str().replace('_', "-");
241+
sess.diag_note_once(
242+
err,
243+
DiagnosticMessageId::from(lint),
244+
&format!(
245+
"`{} {}` implied by `{} {}`",
246+
flag, hyphen_case_lint_name, flag, hyphen_case_flag_val
247+
),
248+
);
249+
}
250+
}
251+
LintLevelSource::Node(lint_attr_name, src, reason) => {
252+
if let Some(rationale) = reason {
253+
err.note(rationale.as_str());
254+
}
255+
sess.diag_span_note_once(
256+
err,
257+
DiagnosticMessageId::from(lint),
258+
src,
259+
"the lint level is defined here",
260+
);
261+
if lint_attr_name.as_str() != name {
262+
let level_str = level.as_str();
263+
sess.diag_note_once(
264+
err,
265+
DiagnosticMessageId::from(lint),
266+
&format!(
267+
"`#[{}({})]` implied by `#[{}({})]`",
268+
level_str, name, level_str, lint_attr_name
269+
),
270+
);
271+
}
272+
}
273+
}
274+
}
275+
205276
pub fn struct_lint_level<'s, 'd>(
206277
sess: &'s Session,
207278
lint: &'static Lint,
@@ -277,69 +348,9 @@ pub fn struct_lint_level<'s, 'd>(
277348
}
278349
}
279350

280-
let name = lint.name_lower();
281-
match src {
282-
LintLevelSource::Default => {
283-
sess.diag_note_once(
284-
&mut err,
285-
DiagnosticMessageId::from(lint),
286-
&format!("`#[{}({})]` on by default", level.as_str(), name),
287-
);
288-
}
289-
LintLevelSource::CommandLine(lint_flag_val, orig_level) => {
290-
let flag = match orig_level {
291-
Level::Warn => "-W",
292-
Level::Deny => "-D",
293-
Level::Forbid => "-F",
294-
Level::Allow => "-A",
295-
Level::ForceWarn => "--force-warn",
296-
};
297-
let hyphen_case_lint_name = name.replace('_', "-");
298-
if lint_flag_val.as_str() == name {
299-
sess.diag_note_once(
300-
&mut err,
301-
DiagnosticMessageId::from(lint),
302-
&format!(
303-
"requested on the command line with `{} {}`",
304-
flag, hyphen_case_lint_name
305-
),
306-
);
307-
} else {
308-
let hyphen_case_flag_val = lint_flag_val.as_str().replace('_', "-");
309-
sess.diag_note_once(
310-
&mut err,
311-
DiagnosticMessageId::from(lint),
312-
&format!(
313-
"`{} {}` implied by `{} {}`",
314-
flag, hyphen_case_lint_name, flag, hyphen_case_flag_val
315-
),
316-
);
317-
}
318-
}
319-
LintLevelSource::Node(lint_attr_name, src, reason) => {
320-
if let Some(rationale) = reason {
321-
err.note(rationale.as_str());
322-
}
323-
sess.diag_span_note_once(
324-
&mut err,
325-
DiagnosticMessageId::from(lint),
326-
src,
327-
"the lint level is defined here",
328-
);
329-
if lint_attr_name.as_str() != name {
330-
let level_str = level.as_str();
331-
sess.diag_note_once(
332-
&mut err,
333-
DiagnosticMessageId::from(lint),
334-
&format!(
335-
"`#[{}({})]` implied by `#[{}({})]`",
336-
level_str, name, level_str, lint_attr_name
337-
),
338-
);
339-
}
340-
}
341-
}
351+
explain_lint_level_source(sess, lint, level, src, &mut err);
342352

353+
let name = lint.name_lower();
343354
let is_force_warn = matches!(level, Level::ForceWarn);
344355
err.code(DiagnosticId::Lint { name, has_future_breakage, is_force_warn });
345356

compiler/rustc_middle/src/mir/query.rs

+37-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::mir::{Body, Promoted};
44
use crate::ty::{self, Ty, TyCtxt};
5-
use rustc_data_structures::sync::Lrc;
5+
use rustc_data_structures::stable_map::FxHashMap;
66
use rustc_data_structures::vec_map::VecMap;
77
use rustc_errors::ErrorReported;
88
use rustc_hir as hir;
@@ -114,13 +114,44 @@ pub struct UnsafetyViolation {
114114
pub details: UnsafetyViolationDetails,
115115
}
116116

117-
#[derive(Clone, TyEncodable, TyDecodable, HashStable, Debug)]
117+
#[derive(Copy, Clone, PartialEq, TyEncodable, TyDecodable, HashStable, Debug)]
118+
pub enum UnusedUnsafe {
119+
/// `unsafe` block contains no unsafe operations
120+
/// > ``unnecessary `unsafe` block``
121+
Unused,
122+
/// `unsafe` block nested under another (used) `unsafe` block
123+
/// > ``… because it's nested under this `unsafe` block``
124+
InUnsafeBlock(hir::HirId),
125+
/// `unsafe` block nested under `unsafe fn`
126+
/// > ``… because it's nested under this `unsafe fn` ``
127+
///
128+
/// the second HirId here indicates the first usage of the `unsafe` block,
129+
/// which allows retrival of the LintLevelSource for why that operation would
130+
/// have been permitted without the block
131+
InUnsafeFn(hir::HirId, hir::HirId),
132+
}
133+
134+
#[derive(Copy, Clone, PartialEq, TyEncodable, TyDecodable, HashStable, Debug)]
135+
pub enum UsedUnsafeBlockData {
136+
SomeDisallowedInUnsafeFn,
137+
// the HirId here indicates the first usage of the `unsafe` block
138+
// (i.e. the one that's first encountered in the MIR traversal of the unsafety check)
139+
AllAllowedInUnsafeFn(hir::HirId),
140+
}
141+
142+
#[derive(TyEncodable, TyDecodable, HashStable, Debug)]
118143
pub struct UnsafetyCheckResult {
119144
/// Violations that are propagated *upwards* from this function.
120-
pub violations: Lrc<[UnsafetyViolation]>,
121-
/// `unsafe` blocks in this function, along with whether they are used. This is
122-
/// used for the "unused_unsafe" lint.
123-
pub unsafe_blocks: Lrc<[(hir::HirId, bool)]>,
145+
pub violations: Vec<UnsafetyViolation>,
146+
147+
/// Used `unsafe` blocks in this function. This is used for the "unused_unsafe" lint.
148+
///
149+
/// The keys are the used `unsafe` blocks, the UnusedUnsafeKind indicates whether
150+
/// or not any of the usages happen at a place that doesn't allow `unsafe_op_in_unsafe_fn`.
151+
pub used_unsafe_blocks: FxHashMap<hir::HirId, UsedUnsafeBlockData>,
152+
153+
/// This is `Some` iff the item is not a closure.
154+
pub unused_unsafes: Option<Vec<(hir::HirId, UnusedUnsafe)>>,
124155
}
125156

126157
rustc_index::newtype_index! {

compiler/rustc_mir_build/src/build/block.rs

+5-17
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ use crate::build::ForGuard::OutsideGuard;
33
use crate::build::{BlockAnd, BlockAndExtension, BlockFrame, Builder};
44
use rustc_middle::mir::*;
55
use rustc_middle::thir::*;
6-
use rustc_session::lint::builtin::UNSAFE_OP_IN_UNSAFE_FN;
7-
use rustc_session::lint::Level;
86
use rustc_span::Span;
97

108
impl<'a, 'tcx> Builder<'a, 'tcx> {
@@ -209,28 +207,18 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
209207
block.unit()
210208
}
211209

212-
/// If we are changing the safety mode, create a new source scope
210+
/// If we are entering an unsafe block, create a new source scope
213211
fn update_source_scope_for_safety_mode(&mut self, span: Span, safety_mode: BlockSafety) {
214212
debug!("update_source_scope_for({:?}, {:?})", span, safety_mode);
215213
let new_unsafety = match safety_mode {
216-
BlockSafety::Safe => None,
217-
BlockSafety::BuiltinUnsafe => Some(Safety::BuiltinUnsafe),
214+
BlockSafety::Safe => return,
215+
BlockSafety::BuiltinUnsafe => Safety::BuiltinUnsafe,
218216
BlockSafety::ExplicitUnsafe(hir_id) => {
219-
match self.in_scope_unsafe {
220-
Safety::Safe => {}
221-
// no longer treat `unsafe fn`s as `unsafe` contexts (see RFC #2585)
222-
Safety::FnUnsafe
223-
if self.tcx.lint_level_at_node(UNSAFE_OP_IN_UNSAFE_FN, hir_id).0
224-
!= Level::Allow => {}
225-
_ => return,
226-
}
227217
self.in_scope_unsafe = Safety::ExplicitUnsafe(hir_id);
228-
Some(Safety::ExplicitUnsafe(hir_id))
218+
Safety::ExplicitUnsafe(hir_id)
229219
}
230220
};
231221

232-
if let Some(unsafety) = new_unsafety {
233-
self.source_scope = self.new_source_scope(span, LintLevel::Inherited, Some(unsafety));
234-
}
222+
self.source_scope = self.new_source_scope(span, LintLevel::Inherited, Some(new_unsafety));
235223
}
236224
}

0 commit comments

Comments
 (0)