Repository: rust-lang/rust-clippy (Rust)

Issue:
`let_and_return` false positive with conditionally compiled code
### Summary

I use `metrics` to gather metrics about the run of a program, but only when a (non-default) feature flag is enabled, for performance reasons.

I thus have a bunch of macros conditionally defined, that either wrap the corresponding macro from the `metrics` crate if the feature is enabled, or do nothing otherwise, for example:

```
#[cfg(feature = "metrics")]
macro_rules! sample {
    ( $counter:expr, $value:expr ) => {
        ::metrics::histogram!($counter).record($value)
    };
}

#[cfg(not(feature = "metrics"))]
macro_rules! sample {
    ( $( $args:expr ),+ ) => {};
}
```

Now, clippy reports the following warning (and suggests a fix):

```
warning: returning the result of a `let` binding from a block
  --> core/src/environment.rs:83:9
   |
77 | /         let r = self.iter_layers().find_map(|hmap| {
78 | |             sample!("Environment.hashmap_size_get", hmap.len() as f64);
79 | |             layer_count += 1;
80 | |             hmap.get(key)
81 | |         });
   | |___________- unnecessary `let` binding
82 |           sample!("Environment.get_layers_traversed", layer_count as f64);
83 |           r
   |           ^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#let_and_return
   = note: `#[warn(clippy::let_and_return)]` on by default
help: return the expression directly
   |
77 ~         
78 |         sample!("Environment.get_layers_traversed", layer_count as f64);
79 ~         self.iter_layers().find_map(|hmap| {
80 +             sample!("Environment.hashmap_size_get", hmap.len() as f64);
81 +             layer_count += 1;
82 +             hmap.get(key)
83 +         })
```

Of course, this is "correct" when the feature is disabled, because the macro `sample!` is just a no-op. However, accepting the suggestion would make the semantics of the version with `metrics` enabled wrong, since there both the macro and the original RHS of the let-binding are effectful,and thus I can't re-order them (we wouldn't measure the same thing). It's sneaky because the suggestion still compiles (hopefully I double checked the result of `cargo clippy --fix`, or that could have gone under the radar).

I understand that the way clippy works might make the problem of conditional compilation difficult, but it's still a dangerous behavior to me, as a end-user.

### Lint Name

let_and_return

### Reproducer

I tried this code:

```rust
#[cfg(feature = "foo")]
macro_rules! do_stuff {
    ( $value:expr ) => {
        eprintln!("{}", $value)
    };
}

#[cfg(not(feature = "foo"))]
macro_rules! do_stuff {
    ( $value:expr ) => {};
}

fn use_foo() -> i32 {
    let block = {
        eprintln!("tick");
        1 + 1
    };

    do_stuff!(block);

    block
 }


fn main() {
    use_foo();
}
```

I saw this happen:

```
warning: returning the result of a `let` binding from a block
  --> src/main.rs:21:5
   |
14 | /     let block = {
15 | |         eprintln!("tick");
16 | |         1 + 1
17 | |     };
   | |______- unnecessary `let` binding
...
21 |       block
   |       ^^^^^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#let_and_return
   = note: `#[warn(clippy::let_and_return)]` on by default
help: return the expression directly
   |
14 ~     
15 |
16 |     do_stuff!(block);
17 |
18 ~     {
19 +         eprintln!("tick");
20 +         1 + 1
21 +     }
```

I expected to see this happen: not having clippy suggest or apply as an auto-fix to re-order the macro call with the let-bound block.

### Version

```text
rustc 1.88.0 (6b00bc388 2025-06-23)
binary: rustc
commit-hash: 6b00bc3880198600130e1cf62b8f8a93494488cc
commit-date: 2025-06-23
host: x86_64-unknown-linux-gnu
release: 1.88.0
LLVM version: 20.1.5
```

### Additional Labels

_No response_

<!-- TRIAGEBOT_START -->

<!-- TRIAGEBOT_ASSIGN_START -->

<!-- TRIAGEBOT_ASSIGN_END -->
<!-- TRIAGEBOT_END -->


Discussion:
@rustbot claim



Patch:
diff --git a/clippy_lints/src/returns/let_and_return.rs b/clippy_lints/src/returns/let_and_return.rs
index f54a26a77620..0a00981e15be 100644
--- a/clippy_lints/src/returns/let_and_return.rs
+++ b/clippy_lints/src/returns/let_and_return.rs
@@ -3,7 +3,7 @@ use clippy_utils::res::MaybeResPath;
 use clippy_utils::source::SpanRangeExt;
 use clippy_utils::sugg::has_enclosing_paren;
 use clippy_utils::visitors::for_each_expr;
-use clippy_utils::{binary_expr_needs_parentheses, fn_def_id, span_contains_cfg};
+use clippy_utils::{binary_expr_needs_parentheses, fn_def_id, span_contains_non_whitespace};
 use core::ops::ControlFlow;
 use rustc_errors::Applicability;
 use rustc_hir::{Block, Expr, PatKind, StmtKind};
@@ -27,7 +27,7 @@ pub(super) fn check_block<'tcx>(cx: &LateContext<'tcx>, block: &'tcx Block<'_>)
         && !initexpr.span.in_external_macro(cx.sess().source_map())
         && !retexpr.span.in_external_macro(cx.sess().source_map())
         && !local.span.from_expansion()
-        && !span_contains_cfg(cx, stmt.span.between(retexpr.span))
+        && !span_contains_non_whitespace(cx, stmt.span.between(retexpr.span), true)
     {
         span_lint_hir_and_then(
             cx,


Test patch:
diff --git a/tests/ui/let_and_return.edition2021.fixed b/tests/ui/let_and_return.edition2021.fixed
index 70d503018e0f..e89e4476bf82 100644
--- a/tests/ui/let_and_return.edition2021.fixed
+++ b/tests/ui/let_and_return.edition2021.fixed
@@ -261,4 +261,14 @@ fn issue14164() -> Result<u32, ()> {
     //~[edition2024]^ let_and_return
 }
 
+fn issue15987() -> i32 {
+    macro_rules! sample {
+        ( $( $args:expr ),+ ) => {};
+    }
+
+    let r = 5;
+    sample!(r);
+    r
+}
+
 fn main() {}
diff --git a/tests/ui/let_and_return.edition2024.fixed b/tests/ui/let_and_return.edition2024.fixed
index 9990c3b71205..d2c76673ca03 100644
--- a/tests/ui/let_and_return.edition2024.fixed
+++ b/tests/ui/let_and_return.edition2024.fixed
@@ -261,4 +261,14 @@ fn issue14164() -> Result<u32, ()> {
     //~[edition2024]^ let_and_return
 }
 
+fn issue15987() -> i32 {
+    macro_rules! sample {
+        ( $( $args:expr ),+ ) => {};
+    }
+
+    let r = 5;
+    sample!(r);
+    r
+}
+
 fn main() {}
diff --git a/tests/ui/let_and_return.rs b/tests/ui/let_and_return.rs
index 48c20cdd60db..1af5f8ba5c16 100644
--- a/tests/ui/let_and_return.rs
+++ b/tests/ui/let_and_return.rs
@@ -261,4 +261,14 @@ fn issue14164() -> Result<u32, ()> {
     //~[edition2024]^ let_and_return
 }
 
+fn issue15987() -> i32 {
+    macro_rules! sample {
+        ( $( $args:expr ),+ ) => {};
+    }
+
+    let r = 5;
+    sample!(r);
+    r
+}
+
 fn main() {}