Clippy lints / Suspicious
Clippy lint · Suspicious · checked with Clippy 0.1.99
clippy::macro_metavars_in_unsafe: Looks for macros that expand metavariables in an unsafe block
What it meansLooks for macros that expand metavariables in an unsafe block. It is in the suspicious group and warns by default.
Does it matter?
This hides an unsafe block and allows the user of the macro to write unsafe code without an explicit unsafe block at callsite, making it possible to perform unsafe operations in seemingly safe code. The macro should be restructured so that these metavariables are referenced outside of unsafe blocks and that the usual unsafety checks apply to the macro argument. This is usually done by binding it to a variable outside of the unsafe block and then using that variable inside of the block as shown in the example, or by referencing it a second time in a safe context, e.g. if false { $expr }.
Code that is most likely wrong or useless. Warns by default.
How to fix it
Clippy does not fix this one automatically. Change the code by hand:
Flagged
/// Gets the first element of a slice
macro_rules! first {
($slice:expr) => {
unsafe {
let slice = $slice; // ⚠️ expansion inside of `unsafe {}`
assert!(!slice.is_empty());
// SAFETY: slice is checked to have at least one element
slice.first().unwrap_unchecked()
}
}
}
assert_eq!(*first!(&[1i32]), 1);
// This will compile as a consequence (note the lack of `unsafe {}`)
assert_eq!(*first!(std::hint::unreachable_unchecked() as &[i32]), 1);
Write this instead
macro_rules! first {
($slice:expr) => {{
let slice = $slice; // ✅ outside of `unsafe {}`
unsafe {
assert!(!slice.is_empty());
// SAFETY: slice is checked to have at least one element
slice.first().unwrap_unchecked()
}
}}
}
assert_eq!(*first!(&[1]), 1);
// This won't compile:
assert_eq!(*first!(std::hint::unreachable_unchecked() as &[i32]), 1);
Turn it off, or on
Allow it where it does not fit your code, as narrowly as you can:
On one item
#[allow(clippy::macro_metavars_in_unsafe)]
fn my_function() { /* ... */ }Whole crate (Cargo.toml)
[lints.clippy]
macro_metavars_in_unsafe = "allow"One run
cargo clippy -- -A clippy::macro-metavars-in-unsafeThe [lints] table needs Rust 1.74 or newer. In a workspace, put it under [workspace.lints.clippy] and add [lints] workspace = true to each member.
Settings in clippy.toml
warn-unsafe-macro-metavars-in-private-macros: Whether to also emit warnings for unsafe blocks with metavariable expansions in private macros.
Clippy's own documentation
What it does
Looks for macros that expand metavariables in an unsafe block.
Why is this bad?
This hides an unsafe block and allows the user of the macro to write unsafe code without an explicit unsafe block at callsite, making it possible to perform unsafe operations in seemingly safe code.
The macro should be restructured so that these metavariables are referenced outside of unsafe blocks and that the usual unsafety checks apply to the macro argument.
This is usually done by binding it to a variable outside of the unsafe block
and then using that variable inside of the block as shown in the example, or by referencing it a second time
in a safe context, e.g. if false { $expr }.
Known limitations
Due to how macros are represented in the compiler at the time Clippy runs its lints, it’s not possible to look for metavariables in macro definitions directly.
Instead, this lint looks at expansions of macros. This leads to false negatives for macros that are never actually invoked.
By default, this lint is rather conservative and will only emit warnings on publicly-exported
macros from the same crate, because oftentimes private internal macros are one-off macros where
this lint would just be noise (e.g. macros that generate impl blocks).
The default behavior should help with preventing a high number of such false positives,
however it can be configured to also emit warnings in private macros if desired.
Example
/// Gets the first element of a slice
macro_rules! first {
($slice:expr) => {
unsafe {
let slice = $slice; // ⚠️ expansion inside of `unsafe {}`
assert!(!slice.is_empty());
// SAFETY: slice is checked to have at least one element
slice.first().unwrap_unchecked()
}
}
}
assert_eq!(*first!(&[1i32]), 1);
// This will compile as a consequence (note the lack of `unsafe {}`)
assert_eq!(*first!(std::hint::unreachable_unchecked() as &[i32]), 1);
Use instead:
macro_rules! first {
($slice:expr) => {{
let slice = $slice; // ✅ outside of `unsafe {}`
unsafe {
assert!(!slice.is_empty());
// SAFETY: slice is checked to have at least one element
slice.first().unwrap_unchecked()
}
}}
}
assert_eq!(*first!(&[1]), 1);
// This won't compile:
assert_eq!(*first!(std::hint::unreachable_unchecked() as &[i32]), 1);
Configuration
-
warn-unsafe-macro-metavars-in-private-macros: Whether to also emit warnings for unsafe blocks with metavariable expansions in private macros.(default:
false)
From the Clippy lint list (0.1.99), part of the Rust project, MIT or Apache-2.0. Official entry.
Related lints
- join_absolute_pathsChecks for calls to Path::join that start with a path separator (\\ or /)
- let_underscore_futurenon-binding `let` on a future
- lines_filter_map_okChecks for usage of lines.filter_map(Result::ok) or lines.flat_map(Result::ok) when lines has type std::io::Lines
- manual_unwrap_or_defaultmatch can be simplified with `.unwrap_or_default()`
- mismatched_bit_width_typepossible buggy implementation of `bit_width`
- misnamed_gettersgetter function appears to return the wrong field
All 847 Clippy lints · Rust compiler errors · Cargo.toml checker
Up next
Related lintclippy::join_absolute_pathsChecks for calls to Path::join that start with a path separator (\\ or /)Read it next →Did this answer your question?