From 875e36f7e459776d3e51b7c089ab89b0821b6122 Mon Sep 17 00:00:00 2001 From: Niki4tap Date: Sun, 8 Jan 2023 06:52:22 +0300 Subject: [PATCH] Add `multiple_unsafe_ops_per_block` lint --- CHANGELOG.md | 1 + clippy_lints/src/declared_lints.rs | 1 + clippy_lints/src/lib.rs | 2 + .../src/multiple_unsafe_ops_per_block.rs | 185 ++++++++++++++++++ tests/ui/multiple_unsafe_ops_per_block.rs | 110 +++++++++++ tests/ui/multiple_unsafe_ops_per_block.stderr | 129 ++++++++++++ 6 files changed, 428 insertions(+) create mode 100644 clippy_lints/src/multiple_unsafe_ops_per_block.rs create mode 100644 tests/ui/multiple_unsafe_ops_per_block.rs create mode 100644 tests/ui/multiple_unsafe_ops_per_block.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e31e8f0d98..84f4654f34e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4383,6 +4383,7 @@ Released 2018-09-13 [`multi_assignments`]: https://rust-lang.github.io/rust-clippy/master/index.html#multi_assignments [`multiple_crate_versions`]: https://rust-lang.github.io/rust-clippy/master/index.html#multiple_crate_versions [`multiple_inherent_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#multiple_inherent_impl +[`multiple_unsafe_ops_per_block`]: https://rust-lang.github.io/rust-clippy/master/index.html#multiple_unsafe_ops_per_block [`must_use_candidate`]: https://rust-lang.github.io/rust-clippy/master/index.html#must_use_candidate [`must_use_unit`]: https://rust-lang.github.io/rust-clippy/master/index.html#must_use_unit [`mut_from_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#mut_from_ref diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 91ca73633f0..36a366fc974 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -422,6 +422,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::module_style::MOD_MODULE_FILES_INFO, crate::module_style::SELF_NAMED_MODULE_FILES_INFO, crate::multi_assignments::MULTI_ASSIGNMENTS_INFO, + crate::multiple_unsafe_ops_per_block::MULTIPLE_UNSAFE_OPS_PER_BLOCK_INFO, crate::mut_key::MUTABLE_KEY_TYPE_INFO, crate::mut_mut::MUT_MUT_INFO, crate::mut_reference::UNNECESSARY_MUT_PASSED_INFO, diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index d8e2ae02c5a..5c4b6041044 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -198,6 +198,7 @@ mod missing_trait_methods; mod mixed_read_write_in_expression; mod module_style; mod multi_assignments; +mod multiple_unsafe_ops_per_block; mod mut_key; mod mut_mut; mod mut_reference; @@ -908,6 +909,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(|_| Box::new(fn_null_check::FnNullCheck)); store.register_late_pass(|_| Box::new(permissions_set_readonly_false::PermissionsSetReadonlyFalse)); store.register_late_pass(|_| Box::new(size_of_ref::SizeOfRef)); + store.register_late_pass(|_| Box::new(multiple_unsafe_ops_per_block::MultipleUnsafeOpsPerBlock)); // add lints here, do not remove this comment, it's used in `new_lint` } diff --git a/clippy_lints/src/multiple_unsafe_ops_per_block.rs b/clippy_lints/src/multiple_unsafe_ops_per_block.rs new file mode 100644 index 00000000000..18e61c75eec --- /dev/null +++ b/clippy_lints/src/multiple_unsafe_ops_per_block.rs @@ -0,0 +1,185 @@ +use clippy_utils::{ + diagnostics::span_lint_and_then, + visitors::{for_each_expr_with_closures, Descend, Visitable}, +}; +use core::ops::ControlFlow::Continue; +use hir::{ + def::{DefKind, Res}, + BlockCheckMode, ExprKind, QPath, UnOp, Unsafety, +}; +use rustc_ast::Mutability; +use rustc_hir as hir; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for `unsafe` blocks that contain more than one unsafe operation. + /// + /// ### Why is this bad? + /// Combined with `undocumented_unsafe_blocks`, + /// this lint ensures that each unsafe operation must be independently justified. + /// Combined with `unused_unsafe`, this lint also ensures + /// elimination of unnecessary unsafe blocks through refactoring. + /// + /// ### Example + /// ```rust + /// /// Reads a `char` from the given pointer. + /// /// + /// /// # Safety + /// /// + /// /// `ptr` must point to four consecutive, initialized bytes which + /// /// form a valid `char` when interpreted in the native byte order. + /// fn read_char(ptr: *const u8) -> char { + /// // SAFETY: The caller has guaranteed that the value pointed + /// // to by `bytes` is a valid `char`. + /// unsafe { char::from_u32_unchecked(*ptr.cast::()) } + /// } + /// ``` + /// Use instead: + /// ```rust + /// /// Reads a `char` from the given pointer. + /// /// + /// /// # Safety + /// /// + /// /// - `ptr` must be 4-byte aligned, point to four consecutive + /// /// initialized bytes, and be valid for reads of 4 bytes. + /// /// - The bytes pointed to by `ptr` must represent a valid + /// /// `char` when interpreted in the native byte order. + /// fn read_char(ptr: *const u8) -> char { + /// // SAFETY: `ptr` is 4-byte aligned, points to four consecutive + /// // initialized bytes, and is valid for reads of 4 bytes. + /// let int_value = unsafe { *ptr.cast::() }; + /// + /// // SAFETY: The caller has guaranteed that the four bytes + /// // pointed to by `bytes` represent a valid `char`. + /// unsafe { char::from_u32_unchecked(int_value) } + /// } + /// ``` + #[clippy::version = "1.68.0"] + pub MULTIPLE_UNSAFE_OPS_PER_BLOCK, + restriction, + "more than one unsafe operation per `unsafe` block" +} +declare_lint_pass!(MultipleUnsafeOpsPerBlock => [MULTIPLE_UNSAFE_OPS_PER_BLOCK]); + +impl<'tcx> LateLintPass<'tcx> for MultipleUnsafeOpsPerBlock { + fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) { + if !matches!(block.rules, BlockCheckMode::UnsafeBlock(_)) { + return; + } + let mut unsafe_ops = vec![]; + collect_unsafe_exprs(cx, block, &mut unsafe_ops); + if unsafe_ops.len() > 1 { + span_lint_and_then( + cx, + MULTIPLE_UNSAFE_OPS_PER_BLOCK, + block.span, + &format!( + "this `unsafe` block contains {} unsafe operations, expected only one", + unsafe_ops.len() + ), + |diag| { + for (msg, span) in unsafe_ops { + diag.span_note(span, msg); + } + }, + ); + } + } +} + +fn collect_unsafe_exprs<'tcx>( + cx: &LateContext<'tcx>, + node: impl Visitable<'tcx>, + unsafe_ops: &mut Vec<(&'static str, Span)>, +) { + for_each_expr_with_closures(cx, node, |expr| { + match expr.kind { + ExprKind::InlineAsm(_) => unsafe_ops.push(("inline assembly used here", expr.span)), + + ExprKind::Field(e, _) => { + if cx.typeck_results().expr_ty(e).is_union() { + unsafe_ops.push(("union field access occurs here", expr.span)); + } + }, + + ExprKind::Path(QPath::Resolved( + _, + hir::Path { + res: Res::Def(DefKind::Static(Mutability::Mut), _), + .. + }, + )) => { + unsafe_ops.push(("access of a mutable static occurs here", expr.span)); + }, + + ExprKind::Unary(UnOp::Deref, e) if cx.typeck_results().expr_ty_adjusted(e).is_unsafe_ptr() => { + unsafe_ops.push(("raw pointer dereference occurs here", expr.span)); + }, + + ExprKind::Call(path_expr, _) => match path_expr.kind { + ExprKind::Path(QPath::Resolved( + _, + hir::Path { + res: Res::Def(kind, def_id), + .. + }, + )) if kind.is_fn_like() => { + let sig = cx.tcx.bound_fn_sig(*def_id); + if sig.0.unsafety() == Unsafety::Unsafe { + unsafe_ops.push(("unsafe function call occurs here", expr.span)); + } + }, + + ExprKind::Path(QPath::TypeRelative(..)) => { + if let Some(sig) = cx + .typeck_results() + .type_dependent_def_id(path_expr.hir_id) + .map(|def_id| cx.tcx.bound_fn_sig(def_id)) + { + if sig.0.unsafety() == Unsafety::Unsafe { + unsafe_ops.push(("unsafe function call occurs here", expr.span)); + } + } + }, + + _ => {}, + }, + + ExprKind::MethodCall(..) => { + if let Some(sig) = cx + .typeck_results() + .type_dependent_def_id(expr.hir_id) + .map(|def_id| cx.tcx.bound_fn_sig(def_id)) + { + if sig.0.unsafety() == Unsafety::Unsafe { + unsafe_ops.push(("unsafe method call occurs here", expr.span)); + } + } + }, + + ExprKind::AssignOp(_, lhs, rhs) | ExprKind::Assign(lhs, rhs, _) => { + if matches!( + lhs.kind, + ExprKind::Path(QPath::Resolved( + _, + hir::Path { + res: Res::Def(DefKind::Static(Mutability::Mut), _), + .. + } + )) + ) { + unsafe_ops.push(("modification of a mutable static occurs here", expr.span)); + collect_unsafe_exprs(cx, rhs, unsafe_ops); + return Continue(Descend::No); + } + }, + + _ => {}, + }; + + Continue::<(), _>(Descend::Yes) + }); +} diff --git a/tests/ui/multiple_unsafe_ops_per_block.rs b/tests/ui/multiple_unsafe_ops_per_block.rs new file mode 100644 index 00000000000..41263535df6 --- /dev/null +++ b/tests/ui/multiple_unsafe_ops_per_block.rs @@ -0,0 +1,110 @@ +#![allow(unused)] +#![allow(deref_nullptr)] +#![allow(clippy::unnecessary_operation)] +#![allow(clippy::drop_copy)] +#![warn(clippy::multiple_unsafe_ops_per_block)] + +use core::arch::asm; + +fn raw_ptr() -> *const () { + core::ptr::null() +} + +unsafe fn not_very_safe() {} + +struct Sample; + +impl Sample { + unsafe fn not_very_safe(&self) {} +} + +#[allow(non_upper_case_globals)] +const sample: Sample = Sample; + +union U { + i: i32, + u: u32, +} + +static mut STATIC: i32 = 0; + +fn test1() { + unsafe { + STATIC += 1; + not_very_safe(); + } +} + +fn test2() { + let u = U { i: 0 }; + + unsafe { + drop(u.u); + *raw_ptr(); + } +} + +fn test3() { + unsafe { + asm!("nop"); + sample.not_very_safe(); + STATIC = 0; + } +} + +fn test_all() { + let u = U { i: 0 }; + unsafe { + drop(u.u); + drop(STATIC); + sample.not_very_safe(); + not_very_safe(); + *raw_ptr(); + asm!("nop"); + } +} + +// no lint +fn correct1() { + unsafe { + STATIC += 1; + } +} + +// no lint +fn correct2() { + unsafe { + STATIC += 1; + } + + unsafe { + *raw_ptr(); + } +} + +// no lint +fn correct3() { + let u = U { u: 0 }; + + unsafe { + not_very_safe(); + } + + unsafe { + drop(u.i); + } +} + +// tests from the issue (https://github.com/rust-lang/rust-clippy/issues/10064) + +unsafe fn read_char_bad(ptr: *const u8) -> char { + unsafe { char::from_u32_unchecked(*ptr.cast::()) } +} + +// no lint +unsafe fn read_char_good(ptr: *const u8) -> char { + let int_value = unsafe { *ptr.cast::() }; + unsafe { core::char::from_u32_unchecked(int_value) } +} + +fn main() {} diff --git a/tests/ui/multiple_unsafe_ops_per_block.stderr b/tests/ui/multiple_unsafe_ops_per_block.stderr new file mode 100644 index 00000000000..f6b8341795d --- /dev/null +++ b/tests/ui/multiple_unsafe_ops_per_block.stderr @@ -0,0 +1,129 @@ +error: this `unsafe` block contains 2 unsafe operations, expected only one + --> $DIR/multiple_unsafe_ops_per_block.rs:32:5 + | +LL | / unsafe { +LL | | STATIC += 1; +LL | | not_very_safe(); +LL | | } + | |_____^ + | +note: modification of a mutable static occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:33:9 + | +LL | STATIC += 1; + | ^^^^^^^^^^^ +note: unsafe function call occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:34:9 + | +LL | not_very_safe(); + | ^^^^^^^^^^^^^^^ + = note: `-D clippy::multiple-unsafe-ops-per-block` implied by `-D warnings` + +error: this `unsafe` block contains 2 unsafe operations, expected only one + --> $DIR/multiple_unsafe_ops_per_block.rs:41:5 + | +LL | / unsafe { +LL | | drop(u.u); +LL | | *raw_ptr(); +LL | | } + | |_____^ + | +note: union field access occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:42:14 + | +LL | drop(u.u); + | ^^^ +note: raw pointer dereference occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:43:9 + | +LL | *raw_ptr(); + | ^^^^^^^^^^ + +error: this `unsafe` block contains 3 unsafe operations, expected only one + --> $DIR/multiple_unsafe_ops_per_block.rs:48:5 + | +LL | / unsafe { +LL | | asm!("nop"); +LL | | sample.not_very_safe(); +LL | | STATIC = 0; +LL | | } + | |_____^ + | +note: inline assembly used here + --> $DIR/multiple_unsafe_ops_per_block.rs:49:9 + | +LL | asm!("nop"); + | ^^^^^^^^^^^ +note: unsafe method call occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:50:9 + | +LL | sample.not_very_safe(); + | ^^^^^^^^^^^^^^^^^^^^^^ +note: modification of a mutable static occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:51:9 + | +LL | STATIC = 0; + | ^^^^^^^^^^ + +error: this `unsafe` block contains 6 unsafe operations, expected only one + --> $DIR/multiple_unsafe_ops_per_block.rs:57:5 + | +LL | / unsafe { +LL | | drop(u.u); +LL | | drop(STATIC); +LL | | sample.not_very_safe(); +... | +LL | | asm!("nop"); +LL | | } + | |_____^ + | +note: union field access occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:58:14 + | +LL | drop(u.u); + | ^^^ +note: access of a mutable static occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:59:14 + | +LL | drop(STATIC); + | ^^^^^^ +note: unsafe method call occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:60:9 + | +LL | sample.not_very_safe(); + | ^^^^^^^^^^^^^^^^^^^^^^ +note: unsafe function call occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:61:9 + | +LL | not_very_safe(); + | ^^^^^^^^^^^^^^^ +note: raw pointer dereference occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:62:9 + | +LL | *raw_ptr(); + | ^^^^^^^^^^ +note: inline assembly used here + --> $DIR/multiple_unsafe_ops_per_block.rs:63:9 + | +LL | asm!("nop"); + | ^^^^^^^^^^^ + +error: this `unsafe` block contains 2 unsafe operations, expected only one + --> $DIR/multiple_unsafe_ops_per_block.rs:101:5 + | +LL | unsafe { char::from_u32_unchecked(*ptr.cast::()) } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +note: unsafe function call occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:101:14 + | +LL | unsafe { char::from_u32_unchecked(*ptr.cast::()) } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: raw pointer dereference occurs here + --> $DIR/multiple_unsafe_ops_per_block.rs:101:39 + | +LL | unsafe { char::from_u32_unchecked(*ptr.cast::()) } + | ^^^^^^^^^^^^^^^^^^ + +error: aborting due to 5 previous errors +