-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add new lint macros_hiding_unsafe_code
#7469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
use clippy_utils::{diagnostics::span_lint_and_sugg, in_macro, is_lint_allowed, source::snippet_with_applicability}; | ||
use rustc_errors::Applicability; | ||
use rustc_hir::{intravisit::FnKind, Block, BlockCheckMode, Body, FnDecl, FnSig, HirId, UnsafeSource, Unsafety}; | ||
use rustc_lint::{ | ||
builtin::{UNSAFE_OP_IN_UNSAFE_FN, UNUSED_UNSAFE}, | ||
LateContext, LateLintPass, LintContext, | ||
}; | ||
use rustc_middle::lint::in_external_macro; | ||
use rustc_session::{declare_tool_lint, impl_lint_pass}; | ||
use rustc_span::Span; | ||
|
||
declare_clippy_lint! { | ||
/// **What it does:** Checks for macro calls inside an `unsafe` function which expansion | ||
/// contains an `unsafe` block, while the macro call is not wrapped in an `unsafe` block | ||
/// itself. This lint only triggers in functions where the `unsafe_op_in_unsafe_fn` lint is | ||
/// enabled. | ||
/// | ||
/// **Why is this bad?** This hides an unsafe operation inside a macro call. This is against | ||
/// the intention of the `unsafe_op_in_unsafe_fn` lint, which is meant to make unsafe code more | ||
/// visible by requiring `unsafe` blocks also in `unsafe` functions. | ||
/// | ||
/// **Known problems:** | ||
/// - In some cases the intention of the macro is to actually hide the unsafety, because the | ||
/// macro itself ensures the safety of the `unsafe` operation. | ||
/// - For local macros, either an `#[allow(unused_unsafe)]` has to be added to the new unsafe | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO, suggesting introducing an unused unsafe block is not a good thing to do; and suggesting Imagine the following chain of actions:
There is a good chance that this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I also think this isn't nice. But I don't really see how else to handle this, without rendering the lint completely useless. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When implementing the lint and noticing, that this is necessary, I also thought about whether we should allow lints that contradict |
||
/// block or the unsafe block inside the macro has to be removed. | ||
/// | ||
/// **Example:** | ||
/// | ||
/// ```rust | ||
/// macro_rules! unsafe_macro { | ||
/// ($e:expr) => { | ||
/// unsafe { $e }; | ||
/// }; | ||
/// } | ||
/// | ||
/// #[warn(unsafe_op_in_unsafe_fn)] | ||
/// unsafe fn foo(x: *const usize) { | ||
/// unsafe_macro!(std::ptr::read(x)); | ||
/// } | ||
/// ``` | ||
/// Use instead: | ||
/// ```rust | ||
/// # macro_rules! unsafe_macro { | ||
/// # ($e:expr) => { | ||
/// # unsafe { $e }; | ||
/// # }; | ||
/// # } | ||
/// #[warn(unsafe_op_in_unsafe_fn)] | ||
/// unsafe fn foo(x: *const usize) { | ||
/// #[allow(unused_unsafe)] unsafe { unsafe_macro!(std::ptr::read(x)) }; | ||
/// } | ||
/// ``` | ||
pub MACROS_HIDING_UNSAFE_CODE, | ||
restriction, | ||
"macros hiding unsafe code, while `unsafe_op_in_unsafe_fn` is enabled" | ||
} | ||
|
||
#[derive(Clone, Copy, Default)] | ||
pub struct MacrosHidingUnsafeCode { | ||
in_unsafe_fn: bool, | ||
in_unsafe_block: usize, | ||
} | ||
|
||
impl_lint_pass!(MacrosHidingUnsafeCode => [MACROS_HIDING_UNSAFE_CODE]); | ||
|
||
impl LateLintPass<'_> for MacrosHidingUnsafeCode { | ||
fn check_block(&mut self, cx: &LateContext<'_>, block: &Block<'_>) { | ||
if let BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) = block.rules { | ||
if in_macro(block.span) { | ||
if self.in_unsafe_fn | ||
&& self.in_unsafe_block == 0 | ||
&& !is_lint_allowed(cx, UNSAFE_OP_IN_UNSAFE_FN, block.hir_id) | ||
{ | ||
let macro_call_span = block.span.source_callsite(); | ||
let unused_unsafe_sugg = if !in_external_macro(cx.sess(), block.span) | ||
&& !is_lint_allowed(cx, UNUSED_UNSAFE, block.hir_id) | ||
{ | ||
"#[allow(unused_unsafe)] " | ||
} else { | ||
"" | ||
}; | ||
let mut applicability = Applicability::MaybeIncorrect; | ||
span_lint_and_sugg( | ||
cx, | ||
MACROS_HIDING_UNSAFE_CODE, | ||
macro_call_span, | ||
"this macro call hides unsafe code", | ||
"wrap it in an `unsafe` block", | ||
format!( | ||
"{}unsafe {{ {} }}", | ||
unused_unsafe_sugg, | ||
snippet_with_applicability(cx, macro_call_span, "...", &mut applicability), | ||
), | ||
applicability, | ||
); | ||
} | ||
} else { | ||
self.in_unsafe_block = self.in_unsafe_block.saturating_add(1); | ||
} | ||
} | ||
} | ||
|
||
fn check_block_post(&mut self, _: &LateContext<'_>, block: &Block<'_>) { | ||
if let BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) = block.rules { | ||
if !in_macro(block.span) { | ||
self.in_unsafe_block = self.in_unsafe_block.saturating_sub(1); | ||
} | ||
} | ||
} | ||
|
||
fn check_fn(&mut self, _: &LateContext<'_>, fn_kind: FnKind<'_>, _: &FnDecl<'_>, _: &Body<'_>, _: Span, _: HirId) { | ||
if is_unsafe_fn(fn_kind) { | ||
self.in_unsafe_fn = true; | ||
} | ||
} | ||
|
||
fn check_fn_post( | ||
&mut self, | ||
_: &LateContext<'_>, | ||
fn_kind: FnKind<'_>, | ||
_: &FnDecl<'_>, | ||
_: &Body<'_>, | ||
_: Span, | ||
_: HirId, | ||
) { | ||
if is_unsafe_fn(fn_kind) { | ||
self.in_unsafe_fn = false; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this needs to be adjusted for nested functions. unsafe fn f() {
fn g() {}
unsafe_macro!();
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah right. Your example shouldn't break this code, but unsafe fn f() {
unsafe fn g() {}
unsafe_macro!();
} will. |
||
} | ||
} | ||
|
||
fn is_unsafe_fn(fn_kind: FnKind<'_>) -> bool { | ||
match fn_kind { | ||
FnKind::ItemFn(_, _, header, _) | FnKind::Method(_, &FnSig { header, .. }, _) => { | ||
matches!(header.unsafety, Unsafety::Unsafe) | ||
}, | ||
FnKind::Closure => false, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// run-rustfix | ||
// aux-build: macro_rules.rs | ||
|
||
#![warn(clippy::macros_hiding_unsafe_code)] | ||
// compiletest allows unused code by default. But we want to explicitely test | ||
// for this lint here. | ||
#![warn(unused_unsafe)] | ||
|
||
extern crate macro_rules; | ||
|
||
use macro_rules::unsafe_external_macro; | ||
|
||
macro_rules! unsafe_macro { | ||
($e:expr) => { | ||
unsafe { $e } | ||
}; | ||
(NESTED $e:expr) => { | ||
unsafe { unsafe_function($e) } | ||
}; | ||
} | ||
|
||
unsafe fn unsafe_function(x: *const usize) { | ||
let _ = std::ptr::read(x); | ||
} | ||
|
||
mod no_unsafe_op_in_unsafe_fn { | ||
use super::*; | ||
|
||
#[allow(unused_unsafe)] | ||
pub unsafe fn ok(x: *const usize) { | ||
unsafe_macro!(std::ptr::read(x)); | ||
unsafe_external_macro!(std::ptr::read(x)); | ||
} | ||
} | ||
|
||
mod unsafe_op_in_unsafe_fn { | ||
#![warn(unsafe_op_in_unsafe_fn)] | ||
|
||
use super::*; | ||
|
||
pub unsafe fn bad(x: *const usize) { | ||
#[allow(unused_unsafe)] unsafe { unsafe_macro!(std::ptr::read(x)); } | ||
#[allow(unused_unsafe)] unsafe { unsafe_macro!(NESTED std::ptr::read(&x)); } | ||
unsafe { unsafe_external_macro!(std::ptr::read(x)); } | ||
} | ||
|
||
pub unsafe fn ok(x: *const usize) { | ||
#[allow(unused_unsafe)] | ||
unsafe { | ||
unsafe_macro!(std::ptr::read(x)); | ||
unsafe_external_macro!(std::ptr::read(x)); | ||
} | ||
} | ||
|
||
#[allow(unused_unsafe)] | ||
pub unsafe fn unused_unsafe_disabled(x: *const usize) { | ||
unsafe { unsafe_macro!(std::ptr::read(x)); } | ||
unsafe { unsafe_external_macro!(std::ptr::read(x)); } | ||
} | ||
} | ||
|
||
fn main() { | ||
unsafe { | ||
no_unsafe_op_in_unsafe_fn::ok(std::ptr::null()); | ||
unsafe_op_in_unsafe_fn::bad(std::ptr::null()); | ||
unsafe_op_in_unsafe_fn::ok(std::ptr::null()); | ||
unsafe_op_in_unsafe_fn::unused_unsafe_disabled(std::ptr::null()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// run-rustfix | ||
// aux-build: macro_rules.rs | ||
|
||
#![warn(clippy::macros_hiding_unsafe_code)] | ||
// compiletest allows unused code by default. But we want to explicitely test | ||
// for this lint here. | ||
#![warn(unused_unsafe)] | ||
|
||
extern crate macro_rules; | ||
|
||
use macro_rules::unsafe_external_macro; | ||
|
||
macro_rules! unsafe_macro { | ||
($e:expr) => { | ||
unsafe { $e } | ||
}; | ||
(NESTED $e:expr) => { | ||
unsafe { unsafe_function($e) } | ||
}; | ||
} | ||
|
||
unsafe fn unsafe_function(x: *const usize) { | ||
let _ = std::ptr::read(x); | ||
} | ||
|
||
mod no_unsafe_op_in_unsafe_fn { | ||
use super::*; | ||
|
||
#[allow(unused_unsafe)] | ||
pub unsafe fn ok(x: *const usize) { | ||
unsafe_macro!(std::ptr::read(x)); | ||
unsafe_external_macro!(std::ptr::read(x)); | ||
} | ||
} | ||
|
||
mod unsafe_op_in_unsafe_fn { | ||
#![warn(unsafe_op_in_unsafe_fn)] | ||
|
||
use super::*; | ||
|
||
pub unsafe fn bad(x: *const usize) { | ||
unsafe_macro!(std::ptr::read(x)); | ||
unsafe_macro!(NESTED std::ptr::read(&x)); | ||
unsafe_external_macro!(std::ptr::read(x)); | ||
} | ||
|
||
pub unsafe fn ok(x: *const usize) { | ||
#[allow(unused_unsafe)] | ||
unsafe { | ||
unsafe_macro!(std::ptr::read(x)); | ||
unsafe_external_macro!(std::ptr::read(x)); | ||
} | ||
} | ||
|
||
#[allow(unused_unsafe)] | ||
pub unsafe fn unused_unsafe_disabled(x: *const usize) { | ||
unsafe_macro!(std::ptr::read(x)); | ||
unsafe_external_macro!(std::ptr::read(x)); | ||
} | ||
} | ||
|
||
fn main() { | ||
unsafe { | ||
no_unsafe_op_in_unsafe_fn::ok(std::ptr::null()); | ||
unsafe_op_in_unsafe_fn::bad(std::ptr::null()); | ||
unsafe_op_in_unsafe_fn::ok(std::ptr::null()); | ||
unsafe_op_in_unsafe_fn::unused_unsafe_disabled(std::ptr::null()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
error: this macro call hides unsafe code | ||
--> $DIR/macros_hiding_unsafe_code.rs:42:9 | ||
| | ||
LL | unsafe_macro!(std::ptr::read(x)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `#[allow(unused_unsafe)] unsafe { unsafe_macro!(std::ptr::read(x)); }` | ||
| | ||
= note: `-D clippy::macros-hiding-unsafe-code` implied by `-D warnings` | ||
|
||
error: this macro call hides unsafe code | ||
--> $DIR/macros_hiding_unsafe_code.rs:43:9 | ||
| | ||
LL | unsafe_macro!(NESTED std::ptr::read(&x)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `#[allow(unused_unsafe)] unsafe { unsafe_macro!(NESTED std::ptr::read(&x)); }` | ||
|
||
error: this macro call hides unsafe code | ||
--> $DIR/macros_hiding_unsafe_code.rs:44:9 | ||
| | ||
LL | unsafe_external_macro!(std::ptr::read(x)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `unsafe { unsafe_external_macro!(std::ptr::read(x)); }` | ||
|
||
error: this macro call hides unsafe code | ||
--> $DIR/macros_hiding_unsafe_code.rs:57:9 | ||
| | ||
LL | unsafe_macro!(std::ptr::read(x)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `unsafe { unsafe_macro!(std::ptr::read(x)); }` | ||
|
||
error: this macro call hides unsafe code | ||
--> $DIR/macros_hiding_unsafe_code.rs:58:9 | ||
| | ||
LL | unsafe_external_macro!(std::ptr::read(x)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: wrap it in an `unsafe` block: `unsafe { unsafe_external_macro!(std::ptr::read(x)); }` | ||
|
||
error: aborting due to 5 previous errors | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that being against the intention of lint is a strong motivation since it's the nature of the lint to catch what it's supposed to catch. Like, chicken and egg problem.
I'd emphasize that the macro containing an unsafe can hide the fact that it uses unsafe, and thus makes it easier to use in the context where required invariants are not held.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I'll word this differently.