Email me

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-unsafe

The [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.
Suspiciousgroup
warndefault level
nofixed by cargo clippy --fix
1.80.0added in Rust

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

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?

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Popular in the shop

All 124 tools →

Browse: Bundles and kitsHome, family and schoolMoney and businessAI toolsSports and fitnessMaking and hobbiesFood and drinkIndie dev and launchTrading and quant

Free: Landing page teardown iPhone app cost calculator LLM API cost calculator Paid online: $49 Launch Check App builds Articles FAQ