rust-lang / rust-lang/rust-clippy

`bad_bit_mask` triggers on expressions containing consts defined as `const _: _ = if cfg!(_) { 0 } else { 1 };`

Open
#15,664 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C-bug I-false-positive
Dominant language
Rust
Stars
13.5k
Forks
2.2k
Avg merge
2d 10h
Merged PRs (30d)
32

Description

Summary

The lint looks for constants which have compile-time-known value of 0 to warn when they are used as a mask in &. But in this case, it doesn't notice that the value of the constant actually depends on cfg!().

This has the same root cause as https://github.com/rust-lang/rust-clippy/issues/7597, and the fix for that, https://github.com/rust-lang/rust-clippy/pull/8614, introduced a check for the following pattern:

const C: u8 = cfg!(foo);

assert_eq!(C, 0); 

by looking whether the initializer (cfg!(foo)) comes from a (direct) expansion of cfg.

My attempt at a solution

One could of course add a check for if cfg!() {} else {} expressions in there, but I think a more future-proof approach would be to call ConstEvalCtxt::eval on the initializer, because that would handle all the ways an expr can be non-const:

diff --git i/clippy_utils/src/consts.rs w/clippy_utils/src/consts.rs
index ecd88daa6..2c07b0d7c 100644
--- i/clippy_utils/src/consts.rs
+++ w/clippy_utils/src/consts.rs
@@ -645,12 +645,8 @@ impl<'tcx> ConstEvalCtxt<'tcx> {
                         kind: ItemKind::Const(.., body_id),
                         ..
                     }) = node
-                    && let Node::Expr(Expr {
-                        kind: ExprKind::Lit(_),
-                        span,
-                        ..
-                    }) = self.tcx.hir_node(body_id.hir_id)
-                    && is_direct_expn_of(*span, sym::cfg).is_some()
+                    && let Node::Expr(expr) = self.tcx.hir_node(body_id.hir_id)
+                    && self.expr(expr).is_none()
                 {
                     return None;
                 }

And that by itself works just fine:

  1. self.expr(expr) goes to the ExprKind::If case
  2. that calls self.ifthenelse(cond, then, otherwise) with cond = cfg!(not(test))
  3. that calls self.expr(cond), which goes to ExprKind::Lit case
  4. that checks is_direct_expn_of(cond.span, sym::cfg).is_some(), which comes out true
  5. so we go all the way back up and return None for the const

Unfortunately, it causes an ICE in the following, more complicated case:

const CONDITIONALLY_ZERO: u8 = if cfg!(not(test)) { 0 } else { 1 };
const X: u8 = 0b00001111;

fn foo(x: u8) -> bool {
    CONDITIONALLY_ZERO & X == 0
}

and the ICE actually happens when checking X:

  1. we arrive at the const item node, as before
  2. we call self.expr(expr) with expr = 0b00001111, which goes to the ExprKind::Lit case
  3. that calls is_direct_expn_of(expr.span, sym::cfg).is_some(), which comes out false
  4. so we go to the other case, and call lit_to_mir_constant(&lit.node, self.typeck.expr_ty_opt(expr)))
  5. and self.typeck.expr_ty_opt(expr) is the part that ends up failing. The reason for that is that self.typeck belongs to the Owner of const X: u8 = 0b00001111;, which is the surrounding block/module, and so it doesn't accept expr whose Owner is the const itself.
Lint Name

bad_bit_mask

Reproducer

I tried this code:

#![allow(unused)]
#![warn(clippy::bad_bit_mask)]

const CONDITIONALLY_ZERO: u8 = if cfg!(not(test)) { 0 } else { 1 };

fn foo(x: u8) -> bool {
    CONDITIONALLY_ZERO & x == 0
}

I saw this happen:

warning: &-masking with zero
 --> src/lib.rs:8:5
  |
8 |     CONDITIONALLY_ZERO & x == 0
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bad_bit_mask

I expected to see this happen:
No lint, because CONDITIONALLY_ZERO is only sometimes 0, depending on the cfg

Version
(playground, 1.91.0-nightly (2025-09-11) and 1.89.0 stable)
Additional Labels

@rustbot label I-false-positive

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in clippy_utils/src/consts.rs, especially ConstEvalCtxt::eval and the initializer handling described in the issue. Reproduce the bad_bit_mask example with CONDITIONALLY_ZERO and the more complicated case involving X, then trace the owner mismatch during constant evaluation. Done means cfg-dependent constants no longer trigger a false-positive lint and the evaluation path does not ICE.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.