about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-03-09 09:56:37 +0000
committerbors <bors@rust-lang.org>2024-03-09 09:56:37 +0000
commit453242cbdea7c0e9288197393da706f45ba95b0a (patch)
treed3878da56d30a37e1b47b1dccd27b793b36e9d1e
parent0b4b684b46b5b962eca2d275ad609b7ade946505 (diff)
parent898ed8825d610d4f441ccb7c84c770261ce45ded (diff)
downloadrust-453242cbdea7c0e9288197393da706f45ba95b0a.tar.gz
rust-453242cbdea7c0e9288197393da706f45ba95b0a.zip
Auto merge of #12310 - samueltardieu:issue-12307, r=xFrednet
New lint `const_is_empty`

This lint detects calls to `.is_empty()` on an entity initialized from a string literal and flag them as suspicious. To avoid triggering on macros called from generated code, it checks that the `.is_empty()` receiver, the call itself and the initialization come from the same context.

Fixes #12307

changelog: [`const_is_empty`]: new lint
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/methods/is_empty.rs49
-rw-r--r--clippy_lints/src/methods/mod.rs35
-rw-r--r--clippy_utils/src/consts.rs107
-rw-r--r--tests/ui/bool_assert_comparison.fixed2
-rw-r--r--tests/ui/bool_assert_comparison.rs2
-rw-r--r--tests/ui/const_is_empty.rs174
-rw-r--r--tests/ui/const_is_empty.stderr161
-rw-r--r--tests/ui/len_zero.fixed8
-rw-r--r--tests/ui/len_zero.rs8
-rw-r--r--tests/ui/len_zero.stderr46
-rw-r--r--tests/ui/needless_bitwise_bool.fixed1
-rw-r--r--tests/ui/needless_bitwise_bool.rs1
-rw-r--r--tests/ui/needless_bitwise_bool.stderr2
-rw-r--r--tests/ui/redundant_as_str.fixed1
-rw-r--r--tests/ui/redundant_as_str.rs1
-rw-r--r--tests/ui/redundant_as_str.stderr4
18 files changed, 566 insertions, 38 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3b2c0a7bf6..5e783b593f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5110,6 +5110,7 @@ Released 2018-09-13
 [`collection_is_never_read`]: https://rust-lang.github.io/rust-clippy/master/index.html#collection_is_never_read
 [`comparison_chain`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_chain
 [`comparison_to_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
+[`const_is_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_is_empty
 [`const_static_lifetime`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_static_lifetime
 [`copy_iterator`]: https://rust-lang.github.io/rust-clippy/master/index.html#copy_iterator
 [`crate_in_macro_def`]: https://rust-lang.github.io/rust-clippy/master/index.html#crate_in_macro_def
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 2b324f5f96e..7a0d57c7859 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -353,6 +353,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
     crate::methods::CLONE_ON_COPY_INFO,
     crate::methods::CLONE_ON_REF_PTR_INFO,
     crate::methods::COLLAPSIBLE_STR_REPLACE_INFO,
+    crate::methods::CONST_IS_EMPTY_INFO,
     crate::methods::DRAIN_COLLECT_INFO,
     crate::methods::ERR_EXPECT_INFO,
     crate::methods::EXPECT_FUN_CALL_INFO,
diff --git a/clippy_lints/src/methods/is_empty.rs b/clippy_lints/src/methods/is_empty.rs
new file mode 100644
index 00000000000..7fe66062251
--- /dev/null
+++ b/clippy_lints/src/methods/is_empty.rs
@@ -0,0 +1,49 @@
+use clippy_utils::consts::constant_is_empty;
+use clippy_utils::diagnostics::span_lint;
+use clippy_utils::{find_binding_init, path_to_local};
+use rustc_hir::{Expr, HirId};
+use rustc_lint::{LateContext, LintContext};
+use rustc_middle::lint::in_external_macro;
+use rustc_span::sym;
+
+use super::CONST_IS_EMPTY;
+
+/// Expression whose initialization depend on a constant conditioned by a `#[cfg(…)]` directive will
+/// not trigger the lint.
+pub(super) fn check(cx: &LateContext<'_>, expr: &'_ Expr<'_>, receiver: &Expr<'_>) {
+    if in_external_macro(cx.sess(), expr.span) || !receiver.span.eq_ctxt(expr.span) {
+        return;
+    }
+    let init_expr = expr_or_init(cx, receiver);
+    if !receiver.span.eq_ctxt(init_expr.span) {
+        return;
+    }
+    if let Some(init_is_empty) = constant_is_empty(cx, init_expr) {
+        span_lint(
+            cx,
+            CONST_IS_EMPTY,
+            expr.span,
+            &format!("this expression always evaluates to {init_is_empty:?}"),
+        );
+    }
+}
+
+fn is_under_cfg(cx: &LateContext<'_>, id: HirId) -> bool {
+    cx.tcx
+        .hir()
+        .parent_id_iter(id)
+        .any(|id| cx.tcx.hir().attrs(id).iter().any(|attr| attr.has_name(sym::cfg)))
+}
+
+/// Similar to [`clippy_utils::expr_or_init`], but does not go up the chain if the initialization
+/// value depends on a `#[cfg(…)]` directive.
+fn expr_or_init<'a, 'b, 'tcx: 'b>(cx: &LateContext<'tcx>, mut expr: &'a Expr<'b>) -> &'a Expr<'b> {
+    while let Some(init) = path_to_local(expr)
+        .and_then(|id| find_binding_init(cx, id))
+        .filter(|init| cx.typeck_results().expr_adjustments(init).is_empty())
+        .filter(|init| !is_under_cfg(cx, init.hir_id))
+    {
+        expr = init;
+    }
+    expr
+}
diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs
index 8a24ccea3a1..830dc28a4b8 100644
--- a/clippy_lints/src/methods/mod.rs
+++ b/clippy_lints/src/methods/mod.rs
@@ -36,6 +36,7 @@ mod inefficient_to_string;
 mod inspect_for_each;
 mod into_iter_on_ref;
 mod is_digit_ascii_radix;
+mod is_empty;
 mod iter_cloned_collect;
 mod iter_count;
 mod iter_filter;
@@ -4044,6 +4045,31 @@ declare_clippy_lint! {
     "calling `.get().is_some()` or `.get().is_none()` instead of `.contains()` or `.contains_key()`"
 }
 
+declare_clippy_lint! {
+    /// ### What it does
+    /// It identifies calls to `.is_empty()` on constant values.
+    ///
+    /// ### Why is this bad?
+    /// String literals and constant values are known at compile time. Checking if they
+    /// are empty will always return the same value. This might not be the intention of
+    /// the expression.
+    ///
+    /// ### Example
+    /// ```no_run
+    /// let value = "";
+    /// if value.is_empty() {
+    ///     println!("the string is empty");
+    /// }
+    /// ```
+    /// Use instead:
+    /// ```no_run
+    /// println!("the string is empty");
+    /// ```
+    #[clippy::version = "1.78.0"]
+    pub CONST_IS_EMPTY,
+    suspicious,
+    "is_empty() called on strings known at compile time"
+}
 pub struct Methods {
     avoid_breaking_exported_api: bool,
     msrv: Msrv,
@@ -4092,6 +4118,7 @@ impl_lint_pass!(Methods => [
     CLONE_ON_COPY,
     CLONE_ON_REF_PTR,
     COLLAPSIBLE_STR_REPLACE,
+    CONST_IS_EMPTY,
     ITER_OVEREAGER_CLONED,
     CLONED_INSTEAD_OF_COPIED,
     FLAT_MAP_OPTION,
@@ -4445,7 +4472,7 @@ impl Methods {
                 ("as_deref" | "as_deref_mut", []) => {
                     needless_option_as_deref::check(cx, expr, recv, name);
                 },
-                ("as_bytes" | "is_empty", []) => {
+                ("as_bytes", []) => {
                     if let Some(("as_str", recv, [], as_str_span, _)) = method_call(recv) {
                         redundant_as_str::check(cx, expr, recv, as_str_span, span);
                     }
@@ -4619,6 +4646,12 @@ impl Methods {
                 ("hash", [arg]) => {
                     unit_hash::check(cx, expr, recv, arg);
                 },
+                ("is_empty", []) => {
+                    if let Some(("as_str", recv, [], as_str_span, _)) = method_call(recv) {
+                        redundant_as_str::check(cx, expr, recv, as_str_span, span);
+                    }
+                    is_empty::check(cx, expr, recv);
+                },
                 ("is_file", []) => filetype_is_file::check(cx, expr, recv),
                 ("is_digit", [radix]) => is_digit_ascii_radix::check(cx, expr, recv, radix, &self.msrv),
                 ("is_none", []) => check_is_some_is_none(cx, expr, recv, call_span, false),
diff --git a/clippy_utils/src/consts.rs b/clippy_utils/src/consts.rs
index 07ed4fbbf8e..948adcb35df 100644
--- a/clippy_utils/src/consts.rs
+++ b/clippy_utils/src/consts.rs
@@ -10,8 +10,10 @@ use rustc_hir::{BinOp, BinOpKind, Block, ConstBlock, Expr, ExprKind, HirId, Item
 use rustc_lexer::tokenize;
 use rustc_lint::LateContext;
 use rustc_middle::mir::interpret::{alloc_range, Scalar};
+use rustc_middle::mir::ConstValue;
 use rustc_middle::ty::{self, EarlyBinder, FloatTy, GenericArgsRef, IntTy, List, ScalarInt, Ty, TyCtxt, UintTy};
 use rustc_middle::{bug, mir, span_bug};
+use rustc_span::def_id::DefId;
 use rustc_span::symbol::{Ident, Symbol};
 use rustc_span::SyntaxContext;
 use rustc_target::abi::Size;
@@ -307,6 +309,12 @@ impl ConstantSource {
     }
 }
 
+/// Attempts to check whether the expression is a constant representing an empty slice, str, array,
+/// etc…
+pub fn constant_is_empty(lcx: &LateContext<'_>, e: &Expr<'_>) -> Option<bool> {
+    ConstEvalLateContext::new(lcx, lcx.typeck_results()).expr_is_empty(e)
+}
+
 /// Attempts to evaluate the expression as a constant.
 pub fn constant<'tcx>(
     lcx: &LateContext<'tcx>,
@@ -406,7 +414,13 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
         match e.kind {
             ExprKind::ConstBlock(ConstBlock { body, .. }) => self.expr(self.lcx.tcx.hir().body(body).value),
             ExprKind::DropTemps(e) => self.expr(e),
-            ExprKind::Path(ref qpath) => self.fetch_path(qpath, e.hir_id, self.typeck_results.expr_ty(e)),
+            ExprKind::Path(ref qpath) => {
+                self.fetch_path_and_apply(qpath, e.hir_id, self.typeck_results.expr_ty(e), |this, result| {
+                    let result = mir_to_const(this.lcx, result)?;
+                    this.source = ConstantSource::Constant;
+                    Some(result)
+                })
+            },
             ExprKind::Block(block, _) => self.block(block),
             ExprKind::Lit(lit) => {
                 if is_direct_expn_of(e.span, "cfg").is_some() {
@@ -472,6 +486,49 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
         }
     }
 
+    /// Simple constant folding to determine if an expression is an empty slice, str, array, …
+    /// `None` will be returned if the constness cannot be determined, or if the resolution
+    /// leaves the local crate.
+    pub fn expr_is_empty(&mut self, e: &Expr<'_>) -> Option<bool> {
+        match e.kind {
+            ExprKind::ConstBlock(ConstBlock { body, .. }) => self.expr_is_empty(self.lcx.tcx.hir().body(body).value),
+            ExprKind::DropTemps(e) => self.expr_is_empty(e),
+            ExprKind::Path(ref qpath) => {
+                if !self
+                    .typeck_results
+                    .qpath_res(qpath, e.hir_id)
+                    .opt_def_id()
+                    .is_some_and(DefId::is_local)
+                {
+                    return None;
+                }
+                self.fetch_path_and_apply(qpath, e.hir_id, self.typeck_results.expr_ty(e), |this, result| {
+                    mir_is_empty(this.lcx, result)
+                })
+            },
+            ExprKind::Lit(lit) => {
+                if is_direct_expn_of(e.span, "cfg").is_some() {
+                    None
+                } else {
+                    match &lit.node {
+                        LitKind::Str(is, _) => Some(is.is_empty()),
+                        LitKind::ByteStr(s, _) | LitKind::CStr(s, _) => Some(s.is_empty()),
+                        _ => None,
+                    }
+                }
+            },
+            ExprKind::Array(vec) => self.multi(vec).map(|v| v.is_empty()),
+            ExprKind::Repeat(..) => {
+                if let ty::Array(_, n) = self.typeck_results.expr_ty(e).kind() {
+                    Some(n.try_eval_target_usize(self.lcx.tcx, self.lcx.param_env)? == 0)
+                } else {
+                    span_bug!(e.span, "typeck error");
+                }
+            },
+            _ => None,
+        }
+    }
+
     #[expect(clippy::cast_possible_wrap)]
     fn constant_not(&self, o: &Constant<'tcx>, ty: Ty<'_>) -> Option<Constant<'tcx>> {
         use self::Constant::{Bool, Int};
@@ -519,8 +576,11 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
         vec.iter().map(|elem| self.expr(elem)).collect::<Option<_>>()
     }
 
-    /// Lookup a possibly constant expression from an `ExprKind::Path`.
-    fn fetch_path(&mut self, qpath: &QPath<'_>, id: HirId, ty: Ty<'tcx>) -> Option<Constant<'tcx>> {
+    /// Lookup a possibly constant expression from an `ExprKind::Path` and apply a function on it.
+    fn fetch_path_and_apply<T, F>(&mut self, qpath: &QPath<'_>, id: HirId, ty: Ty<'tcx>, f: F) -> Option<T>
+    where
+        F: FnOnce(&mut Self, rustc_middle::mir::Const<'tcx>) -> Option<T>,
+    {
         let res = self.typeck_results.qpath_res(qpath, id);
         match res {
             Res::Def(DefKind::Const | DefKind::AssocConst, def_id) => {
@@ -553,9 +613,7 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
                     .const_eval_resolve(self.param_env, mir::UnevaluatedConst::new(def_id, args), None)
                     .ok()
                     .map(|val| rustc_middle::mir::Const::from_value(val, ty))?;
-                let result = mir_to_const(self.lcx, result)?;
-                self.source = ConstantSource::Constant;
-                Some(result)
+                f(self, result)
             },
             _ => None,
         }
@@ -746,7 +804,6 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
 }
 
 pub fn mir_to_const<'tcx>(lcx: &LateContext<'tcx>, result: mir::Const<'tcx>) -> Option<Constant<'tcx>> {
-    use rustc_middle::mir::ConstValue;
     let mir::Const::Val(val, _) = result else {
         // We only work on evaluated consts.
         return None;
@@ -794,6 +851,42 @@ pub fn mir_to_const<'tcx>(lcx: &LateContext<'tcx>, result: mir::Const<'tcx>) ->
     }
 }
 
+fn mir_is_empty<'tcx>(lcx: &LateContext<'tcx>, result: mir::Const<'tcx>) -> Option<bool> {
+    let mir::Const::Val(val, _) = result else {
+        // We only work on evaluated consts.
+        return None;
+    };
+    match (val, result.ty().kind()) {
+        (_, ty::Ref(_, inner_ty, _)) => match inner_ty.kind() {
+            ty::Str | ty::Slice(_) => {
+                if let ConstValue::Indirect { alloc_id, offset } = val {
+                    // Get the length from the slice, using the same formula as
+                    // [`ConstValue::try_get_slice_bytes_for_diagnostics`].
+                    let a = lcx.tcx.global_alloc(alloc_id).unwrap_memory().inner();
+                    let ptr_size = lcx.tcx.data_layout.pointer_size;
+                    if a.size() < offset + 2 * ptr_size {
+                        // (partially) dangling reference
+                        return None;
+                    }
+                    let len = a
+                        .read_scalar(&lcx.tcx, alloc_range(offset + ptr_size, ptr_size), false)
+                        .ok()?
+                        .to_target_usize(&lcx.tcx)
+                        .ok()?;
+                    Some(len == 0)
+                } else {
+                    None
+                }
+            },
+            ty::Array(_, len) => Some(len.try_to_target_usize(lcx.tcx)? == 0),
+            _ => None,
+        },
+        (ConstValue::Indirect { .. }, ty::Array(_, len)) => Some(len.try_to_target_usize(lcx.tcx)? == 0),
+        (ConstValue::ZeroSized, _) => Some(true),
+        _ => None,
+    }
+}
+
 fn field_of_struct<'tcx>(
     adt_def: ty::AdtDef<'tcx>,
     lcx: &LateContext<'tcx>,
diff --git a/tests/ui/bool_assert_comparison.fixed b/tests/ui/bool_assert_comparison.fixed
index 63b8e27e1c6..b05166a055e 100644
--- a/tests/ui/bool_assert_comparison.fixed
+++ b/tests/ui/bool_assert_comparison.fixed
@@ -1,4 +1,4 @@
-#![allow(unused, clippy::assertions_on_constants)]
+#![allow(unused, clippy::assertions_on_constants, clippy::const_is_empty)]
 #![warn(clippy::bool_assert_comparison)]
 
 use std::ops::Not;
diff --git a/tests/ui/bool_assert_comparison.rs b/tests/ui/bool_assert_comparison.rs
index 58f81fedb79..dc51fcf1d36 100644
--- a/tests/ui/bool_assert_comparison.rs
+++ b/tests/ui/bool_assert_comparison.rs
@@ -1,4 +1,4 @@
-#![allow(unused, clippy::assertions_on_constants)]
+#![allow(unused, clippy::assertions_on_constants, clippy::const_is_empty)]
 #![warn(clippy::bool_assert_comparison)]
 
 use std::ops::Not;
diff --git a/tests/ui/const_is_empty.rs b/tests/ui/const_is_empty.rs
new file mode 100644
index 00000000000..ae37a82e4f9
--- /dev/null
+++ b/tests/ui/const_is_empty.rs
@@ -0,0 +1,174 @@
+#![feature(inline_const)]
+#![warn(clippy::const_is_empty)]
+#![allow(clippy::needless_late_init, unused_must_use)]
+
+fn test_literal() {
+    if "".is_empty() {
+        //~^ERROR: this expression always evaluates to true
+    }
+    if "foobar".is_empty() {
+        //~^ERROR: this expression always evaluates to false
+    }
+}
+
+fn test_byte_literal() {
+    if b"".is_empty() {
+        //~^ERROR: this expression always evaluates to true
+    }
+    if b"foobar".is_empty() {
+        //~^ERROR: this expression always evaluates to false
+    }
+}
+
+fn test_no_mut() {
+    let mut empty = "";
+    if empty.is_empty() {
+        // No lint because it is mutable
+    }
+}
+
+fn test_propagated() {
+    let empty = "";
+    let non_empty = "foobar";
+    let empty2 = empty;
+    let non_empty2 = non_empty;
+    if empty2.is_empty() {
+        //~^ERROR: this expression always evaluates to true
+    }
+    if non_empty2.is_empty() {
+        //~^ERROR: this expression always evaluates to false
+    }
+}
+
+const EMPTY_STR: &str = "";
+const NON_EMPTY_STR: &str = "foo";
+const EMPTY_BSTR: &[u8] = b"";
+const NON_EMPTY_BSTR: &[u8] = b"foo";
+const EMPTY_U8_SLICE: &[u8] = &[];
+const NON_EMPTY_U8_SLICE: &[u8] = &[1, 2];
+const EMPTY_SLICE: &[u32] = &[];
+const NON_EMPTY_SLICE: &[u32] = &[1, 2];
+const NON_EMPTY_SLICE_REPEAT: &[u32] = &[1; 2];
+const EMPTY_ARRAY: [u32; 0] = [];
+const EMPTY_ARRAY_REPEAT: [u32; 0] = [1; 0];
+const NON_EMPTY_ARRAY: [u32; 2] = [1, 2];
+const NON_EMPTY_ARRAY_REPEAT: [u32; 2] = [1; 2];
+const EMPTY_REF_ARRAY: &[u32; 0] = &[];
+const NON_EMPTY_REF_ARRAY: &[u32; 3] = &[1, 2, 3];
+
+fn test_from_const() {
+    let _ = EMPTY_STR.is_empty();
+    //~^ ERROR: this expression always evaluates to true
+    let _ = NON_EMPTY_STR.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+    let _ = EMPTY_BSTR.is_empty();
+    //~^ ERROR: this expression always evaluates to true
+    let _ = NON_EMPTY_BSTR.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+    let _ = EMPTY_ARRAY.is_empty();
+    //~^ ERROR: this expression always evaluates to true
+    let _ = EMPTY_ARRAY_REPEAT.is_empty();
+    //~^ ERROR: this expression always evaluates to true
+    let _ = EMPTY_U8_SLICE.is_empty();
+    //~^ ERROR: this expression always evaluates to true
+    let _ = NON_EMPTY_U8_SLICE.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+    let _ = NON_EMPTY_ARRAY.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+    let _ = NON_EMPTY_ARRAY_REPEAT.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+    let _ = EMPTY_REF_ARRAY.is_empty();
+    //~^ ERROR: this expression always evaluates to true
+    let _ = NON_EMPTY_REF_ARRAY.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+    let _ = EMPTY_SLICE.is_empty();
+    //~^ ERROR: this expression always evaluates to true
+    let _ = NON_EMPTY_SLICE.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+    let _ = NON_EMPTY_SLICE_REPEAT.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+}
+
+fn main() {
+    let value = "foobar";
+    let _ = value.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+    let x = value;
+    let _ = x.is_empty();
+    //~^ ERROR: this expression always evaluates to false
+    let _ = "".is_empty();
+    //~^ ERROR: this expression always evaluates to true
+    let _ = b"".is_empty();
+    //~^ ERROR: this expression always evaluates to true
+}
+
+fn str_from_arg(var: &str) {
+    var.is_empty();
+    // Do not lint, we know nothiny about var
+}
+
+fn update_str() {
+    let mut value = "duck";
+    value = "penguin";
+
+    let _ = value.is_empty();
+    // Do not lint since value is mutable
+}
+
+fn macros() {
+    // Content from Macro
+    let file = include_str!("const_is_empty.rs");
+    let _ = file.is_empty();
+    // No lint because initializer comes from a macro result
+
+    let var = env!("PATH");
+    let _ = var.is_empty();
+    // No lint because initializer comes from a macro result
+}
+
+fn conditional_value() {
+    let value;
+
+    if true {
+        value = "hey";
+    } else {
+        value = "hej";
+    }
+
+    let _ = value.is_empty();
+    // Do not lint, current constant folding is too simple to detect this
+}
+
+fn cfg_conditioned() {
+    #[cfg(test)]
+    let val = "";
+    #[cfg(not(test))]
+    let val = "foo";
+
+    let _ = val.is_empty();
+    // Do not lint, value depend on a #[cfg(…)] directive
+}
+
+fn not_cfg_conditioned() {
+    let val = "";
+    #[cfg(not(target_os = "inexistent"))]
+    let _ = val.is_empty();
+    //~^ ERROR: this expression always evaluates to true
+}
+
+const fn const_rand() -> &'static str {
+    "17"
+}
+
+fn const_expressions() {
+    let _ = const { if true { "1" } else { "2" } }.is_empty();
+    // Do not lint, we do not recurse into boolean expressions
+
+    let _ = const_rand().is_empty();
+    // Do not lint, we do not recurse into functions
+}
+
+fn constant_from_external_crate() {
+    let _ = std::env::consts::EXE_EXTENSION.is_empty();
+    // Do not lint, `exe_ext` comes from the `std` crate
+}
diff --git a/tests/ui/const_is_empty.stderr b/tests/ui/const_is_empty.stderr
new file mode 100644
index 00000000000..0e09da77bb4
--- /dev/null
+++ b/tests/ui/const_is_empty.stderr
@@ -0,0 +1,161 @@
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:6:8
+   |
+LL |     if "".is_empty() {
+   |        ^^^^^^^^^^^^^
+   |
+   = note: `-D clippy::const-is-empty` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::const_is_empty)]`
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:9:8
+   |
+LL |     if "foobar".is_empty() {
+   |        ^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:15:8
+   |
+LL |     if b"".is_empty() {
+   |        ^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:18:8
+   |
+LL |     if b"foobar".is_empty() {
+   |        ^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:35:8
+   |
+LL |     if empty2.is_empty() {
+   |        ^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:38:8
+   |
+LL |     if non_empty2.is_empty() {
+   |        ^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:60:13
+   |
+LL |     let _ = EMPTY_STR.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:62:13
+   |
+LL |     let _ = NON_EMPTY_STR.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:64:13
+   |
+LL |     let _ = EMPTY_BSTR.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:66:13
+   |
+LL |     let _ = NON_EMPTY_BSTR.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:68:13
+   |
+LL |     let _ = EMPTY_ARRAY.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:70:13
+   |
+LL |     let _ = EMPTY_ARRAY_REPEAT.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:72:13
+   |
+LL |     let _ = EMPTY_U8_SLICE.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:74:13
+   |
+LL |     let _ = NON_EMPTY_U8_SLICE.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:76:13
+   |
+LL |     let _ = NON_EMPTY_ARRAY.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:78:13
+   |
+LL |     let _ = NON_EMPTY_ARRAY_REPEAT.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:80:13
+   |
+LL |     let _ = EMPTY_REF_ARRAY.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:82:13
+   |
+LL |     let _ = NON_EMPTY_REF_ARRAY.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:84:13
+   |
+LL |     let _ = EMPTY_SLICE.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:86:13
+   |
+LL |     let _ = NON_EMPTY_SLICE.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:88:13
+   |
+LL |     let _ = NON_EMPTY_SLICE_REPEAT.is_empty();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:94:13
+   |
+LL |     let _ = value.is_empty();
+   |             ^^^^^^^^^^^^^^^^
+
+error: this expression always evaluates to false
+  --> tests/ui/const_is_empty.rs:97:13
+   |
+LL |     let _ = x.is_empty();
+   |             ^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:99:13
+   |
+LL |     let _ = "".is_empty();
+   |             ^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:101:13
+   |
+LL |     let _ = b"".is_empty();
+   |             ^^^^^^^^^^^^^^
+
+error: this expression always evaluates to true
+  --> tests/ui/const_is_empty.rs:155:13
+   |
+LL |     let _ = val.is_empty();
+   |             ^^^^^^^^^^^^^^
+
+error: aborting due to 26 previous errors
+
diff --git a/tests/ui/len_zero.fixed b/tests/ui/len_zero.fixed
index 745fc7e1a8b..c16d7a26616 100644
--- a/tests/ui/len_zero.fixed
+++ b/tests/ui/len_zero.fixed
@@ -1,5 +1,11 @@
 #![warn(clippy::len_zero)]
-#![allow(dead_code, unused, clippy::needless_if, clippy::len_without_is_empty)]
+#![allow(
+    dead_code,
+    unused,
+    clippy::needless_if,
+    clippy::len_without_is_empty,
+    clippy::const_is_empty
+)]
 
 extern crate core;
 use core::ops::Deref;
diff --git a/tests/ui/len_zero.rs b/tests/ui/len_zero.rs
index 048ad2f4fd3..5c49a5abf81 100644
--- a/tests/ui/len_zero.rs
+++ b/tests/ui/len_zero.rs
@@ -1,5 +1,11 @@
 #![warn(clippy::len_zero)]
-#![allow(dead_code, unused, clippy::needless_if, clippy::len_without_is_empty)]
+#![allow(
+    dead_code,
+    unused,
+    clippy::needless_if,
+    clippy::len_without_is_empty,
+    clippy::const_is_empty
+)]
 
 extern crate core;
 use core::ops::Deref;
diff --git a/tests/ui/len_zero.stderr b/tests/ui/len_zero.stderr
index b1f04c94de6..dd07a85d62c 100644
--- a/tests/ui/len_zero.stderr
+++ b/tests/ui/len_zero.stderr
@@ -1,5 +1,5 @@
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:82:8
+  --> tests/ui/len_zero.rs:88:8
    |
 LL |     if x.len() == 0 {
    |        ^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `x.is_empty()`
@@ -8,13 +8,13 @@ LL |     if x.len() == 0 {
    = help: to override `-D warnings` add `#[allow(clippy::len_zero)]`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:86:8
+  --> tests/ui/len_zero.rs:92:8
    |
 LL |     if "".len() == 0 {}
    |        ^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `"".is_empty()`
 
 error: comparison to empty slice
-  --> tests/ui/len_zero.rs:95:20
+  --> tests/ui/len_zero.rs:101:20
    |
 LL |     println!("{}", *s1 == "");
    |                    ^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `s1.is_empty()`
@@ -23,121 +23,121 @@ LL |     println!("{}", *s1 == "");
    = help: to override `-D warnings` add `#[allow(clippy::comparison_to_empty)]`
 
 error: comparison to empty slice
-  --> tests/ui/len_zero.rs:96:20
+  --> tests/ui/len_zero.rs:102:20
    |
 LL |     println!("{}", **s2 == "");
    |                    ^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `s2.is_empty()`
 
 error: comparison to empty slice
-  --> tests/ui/len_zero.rs:97:20
+  --> tests/ui/len_zero.rs:103:20
    |
 LL |     println!("{}", ***s3 == "");
    |                    ^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `s3.is_empty()`
 
 error: comparison to empty slice
-  --> tests/ui/len_zero.rs:98:20
+  --> tests/ui/len_zero.rs:104:20
    |
 LL |     println!("{}", ****s4 == "");
    |                    ^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `s4.is_empty()`
 
 error: comparison to empty slice
-  --> tests/ui/len_zero.rs:99:20
+  --> tests/ui/len_zero.rs:105:20
    |
 LL |     println!("{}", *****s5 == "");
    |                    ^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `s5.is_empty()`
 
 error: comparison to empty slice
-  --> tests/ui/len_zero.rs:100:20
+  --> tests/ui/len_zero.rs:106:20
    |
 LL |     println!("{}", ******(s6) == "");
    |                    ^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `(s6).is_empty()`
 
 error: comparison to empty slice
-  --> tests/ui/len_zero.rs:103:20
+  --> tests/ui/len_zero.rs:109:20
    |
 LL |     println!("{}", &**d2s == "");
    |                    ^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `(**d2s).is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:118:8
+  --> tests/ui/len_zero.rs:124:8
    |
 LL |     if has_is_empty.len() == 0 {
    |        ^^^^^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `has_is_empty.is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:121:8
+  --> tests/ui/len_zero.rs:127:8
    |
 LL |     if has_is_empty.len() != 0 {
    |        ^^^^^^^^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!has_is_empty.is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:124:8
+  --> tests/ui/len_zero.rs:130:8
    |
 LL |     if has_is_empty.len() > 0 {
    |        ^^^^^^^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!has_is_empty.is_empty()`
 
 error: length comparison to one
-  --> tests/ui/len_zero.rs:127:8
+  --> tests/ui/len_zero.rs:133:8
    |
 LL |     if has_is_empty.len() < 1 {
    |        ^^^^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `has_is_empty.is_empty()`
 
 error: length comparison to one
-  --> tests/ui/len_zero.rs:130:8
+  --> tests/ui/len_zero.rs:136:8
    |
 LL |     if has_is_empty.len() >= 1 {
    |        ^^^^^^^^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!has_is_empty.is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:141:8
+  --> tests/ui/len_zero.rs:147:8
    |
 LL |     if 0 == has_is_empty.len() {
    |        ^^^^^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `has_is_empty.is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:144:8
+  --> tests/ui/len_zero.rs:150:8
    |
 LL |     if 0 != has_is_empty.len() {
    |        ^^^^^^^^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!has_is_empty.is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:147:8
+  --> tests/ui/len_zero.rs:153:8
    |
 LL |     if 0 < has_is_empty.len() {
    |        ^^^^^^^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!has_is_empty.is_empty()`
 
 error: length comparison to one
-  --> tests/ui/len_zero.rs:150:8
+  --> tests/ui/len_zero.rs:156:8
    |
 LL |     if 1 <= has_is_empty.len() {
    |        ^^^^^^^^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!has_is_empty.is_empty()`
 
 error: length comparison to one
-  --> tests/ui/len_zero.rs:153:8
+  --> tests/ui/len_zero.rs:159:8
    |
 LL |     if 1 > has_is_empty.len() {
    |        ^^^^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `has_is_empty.is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:167:8
+  --> tests/ui/len_zero.rs:173:8
    |
 LL |     if with_is_empty.len() == 0 {
    |        ^^^^^^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `with_is_empty.is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:179:6
+  --> tests/ui/len_zero.rs:185:6
    |
 LL |     (has_is_empty.len() > 0).then(|| println!("This can happen."));
    |      ^^^^^^^^^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!has_is_empty.is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:180:6
+  --> tests/ui/len_zero.rs:186:6
    |
 LL |     (has_is_empty.len() == 0).then(|| println!("Or this!"));
    |      ^^^^^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `has_is_empty.is_empty()`
 
 error: length comparison to zero
-  --> tests/ui/len_zero.rs:184:8
+  --> tests/ui/len_zero.rs:190:8
    |
 LL |     if b.len() != 0 {}
    |        ^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!b.is_empty()`
diff --git a/tests/ui/needless_bitwise_bool.fixed b/tests/ui/needless_bitwise_bool.fixed
index 201f8a4c19d..a8176618c1f 100644
--- a/tests/ui/needless_bitwise_bool.fixed
+++ b/tests/ui/needless_bitwise_bool.fixed
@@ -1,4 +1,5 @@
 #![warn(clippy::needless_bitwise_bool)]
+#![allow(clippy::const_is_empty)]
 
 fn returns_bool() -> bool {
     true
diff --git a/tests/ui/needless_bitwise_bool.rs b/tests/ui/needless_bitwise_bool.rs
index b0e5014b74b..f190eb2b76e 100644
--- a/tests/ui/needless_bitwise_bool.rs
+++ b/tests/ui/needless_bitwise_bool.rs
@@ -1,4 +1,5 @@
 #![warn(clippy::needless_bitwise_bool)]
+#![allow(clippy::const_is_empty)]
 
 fn returns_bool() -> bool {
     true
diff --git a/tests/ui/needless_bitwise_bool.stderr b/tests/ui/needless_bitwise_bool.stderr
index f29d4492540..9f14646c3e5 100644
--- a/tests/ui/needless_bitwise_bool.stderr
+++ b/tests/ui/needless_bitwise_bool.stderr
@@ -1,5 +1,5 @@
 error: use of bitwise operator instead of lazy operator between booleans
-  --> tests/ui/needless_bitwise_bool.rs:22:8
+  --> tests/ui/needless_bitwise_bool.rs:23:8
    |
 LL |     if y & !x {
    |        ^^^^^^ help: try: `y && !x`
diff --git a/tests/ui/redundant_as_str.fixed b/tests/ui/redundant_as_str.fixed
index 4185b402226..708a1cc9150 100644
--- a/tests/ui/redundant_as_str.fixed
+++ b/tests/ui/redundant_as_str.fixed
@@ -1,4 +1,5 @@
 #![warn(clippy::redundant_as_str)]
+#![allow(clippy::const_is_empty)]
 
 fn main() {
     let string = "Hello, world!".to_owned();
diff --git a/tests/ui/redundant_as_str.rs b/tests/ui/redundant_as_str.rs
index 7a74d8a55de..257af591cef 100644
--- a/tests/ui/redundant_as_str.rs
+++ b/tests/ui/redundant_as_str.rs
@@ -1,4 +1,5 @@
 #![warn(clippy::redundant_as_str)]
+#![allow(clippy::const_is_empty)]
 
 fn main() {
     let string = "Hello, world!".to_owned();
diff --git a/tests/ui/redundant_as_str.stderr b/tests/ui/redundant_as_str.stderr
index f086de5fede..f5379d701db 100644
--- a/tests/ui/redundant_as_str.stderr
+++ b/tests/ui/redundant_as_str.stderr
@@ -1,5 +1,5 @@
 error: this `as_str` is redundant and can be removed as the method immediately following exists on `String` too
-  --> tests/ui/redundant_as_str.rs:7:29
+  --> tests/ui/redundant_as_str.rs:8:29
    |
 LL |     let _redundant = string.as_str().as_bytes();
    |                             ^^^^^^^^^^^^^^^^^ help: try: `as_bytes`
@@ -8,7 +8,7 @@ LL |     let _redundant = string.as_str().as_bytes();
    = help: to override `-D warnings` add `#[allow(clippy::redundant_as_str)]`
 
 error: this `as_str` is redundant and can be removed as the method immediately following exists on `String` too
-  --> tests/ui/redundant_as_str.rs:8:29
+  --> tests/ui/redundant_as_str.rs:9:29
    |
 LL |     let _redundant = string.as_str().is_empty();
    |                             ^^^^^^^^^^^^^^^^^ help: try: `is_empty`