Skip to content

Commit 453242c

Browse files
committed
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
2 parents 0b4b684 + 898ed88 commit 453242c

18 files changed

+566
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5110,6 +5110,7 @@ Released 2018-09-13
51105110
[`collection_is_never_read`]: https://rust-lang.github.io/rust-clippy/master/index.html#collection_is_never_read
51115111
[`comparison_chain`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_chain
51125112
[`comparison_to_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
5113+
[`const_is_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_is_empty
51135114
[`const_static_lifetime`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_static_lifetime
51145115
[`copy_iterator`]: https://rust-lang.github.io/rust-clippy/master/index.html#copy_iterator
51155116
[`crate_in_macro_def`]: https://rust-lang.github.io/rust-clippy/master/index.html#crate_in_macro_def

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
353353
crate::methods::CLONE_ON_COPY_INFO,
354354
crate::methods::CLONE_ON_REF_PTR_INFO,
355355
crate::methods::COLLAPSIBLE_STR_REPLACE_INFO,
356+
crate::methods::CONST_IS_EMPTY_INFO,
356357
crate::methods::DRAIN_COLLECT_INFO,
357358
crate::methods::ERR_EXPECT_INFO,
358359
crate::methods::EXPECT_FUN_CALL_INFO,

clippy_lints/src/methods/is_empty.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use clippy_utils::consts::constant_is_empty;
2+
use clippy_utils::diagnostics::span_lint;
3+
use clippy_utils::{find_binding_init, path_to_local};
4+
use rustc_hir::{Expr, HirId};
5+
use rustc_lint::{LateContext, LintContext};
6+
use rustc_middle::lint::in_external_macro;
7+
use rustc_span::sym;
8+
9+
use super::CONST_IS_EMPTY;
10+
11+
/// Expression whose initialization depend on a constant conditioned by a `#[cfg(…)]` directive will
12+
/// not trigger the lint.
13+
pub(super) fn check(cx: &LateContext<'_>, expr: &'_ Expr<'_>, receiver: &Expr<'_>) {
14+
if in_external_macro(cx.sess(), expr.span) || !receiver.span.eq_ctxt(expr.span) {
15+
return;
16+
}
17+
let init_expr = expr_or_init(cx, receiver);
18+
if !receiver.span.eq_ctxt(init_expr.span) {
19+
return;
20+
}
21+
if let Some(init_is_empty) = constant_is_empty(cx, init_expr) {
22+
span_lint(
23+
cx,
24+
CONST_IS_EMPTY,
25+
expr.span,
26+
&format!("this expression always evaluates to {init_is_empty:?}"),
27+
);
28+
}
29+
}
30+
31+
fn is_under_cfg(cx: &LateContext<'_>, id: HirId) -> bool {
32+
cx.tcx
33+
.hir()
34+
.parent_id_iter(id)
35+
.any(|id| cx.tcx.hir().attrs(id).iter().any(|attr| attr.has_name(sym::cfg)))
36+
}
37+
38+
/// Similar to [`clippy_utils::expr_or_init`], but does not go up the chain if the initialization
39+
/// value depends on a `#[cfg(…)]` directive.
40+
fn expr_or_init<'a, 'b, 'tcx: 'b>(cx: &LateContext<'tcx>, mut expr: &'a Expr<'b>) -> &'a Expr<'b> {
41+
while let Some(init) = path_to_local(expr)
42+
.and_then(|id| find_binding_init(cx, id))
43+
.filter(|init| cx.typeck_results().expr_adjustments(init).is_empty())
44+
.filter(|init| !is_under_cfg(cx, init.hir_id))
45+
{
46+
expr = init;
47+
}
48+
expr
49+
}

clippy_lints/src/methods/mod.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ mod inefficient_to_string;
3636
mod inspect_for_each;
3737
mod into_iter_on_ref;
3838
mod is_digit_ascii_radix;
39+
mod is_empty;
3940
mod iter_cloned_collect;
4041
mod iter_count;
4142
mod iter_filter;
@@ -4044,6 +4045,31 @@ declare_clippy_lint! {
40444045
"calling `.get().is_some()` or `.get().is_none()` instead of `.contains()` or `.contains_key()`"
40454046
}
40464047

4048+
declare_clippy_lint! {
4049+
/// ### What it does
4050+
/// It identifies calls to `.is_empty()` on constant values.
4051+
///
4052+
/// ### Why is this bad?
4053+
/// String literals and constant values are known at compile time. Checking if they
4054+
/// are empty will always return the same value. This might not be the intention of
4055+
/// the expression.
4056+
///
4057+
/// ### Example
4058+
/// ```no_run
4059+
/// let value = "";
4060+
/// if value.is_empty() {
4061+
/// println!("the string is empty");
4062+
/// }
4063+
/// ```
4064+
/// Use instead:
4065+
/// ```no_run
4066+
/// println!("the string is empty");
4067+
/// ```
4068+
#[clippy::version = "1.78.0"]
4069+
pub CONST_IS_EMPTY,
4070+
suspicious,
4071+
"is_empty() called on strings known at compile time"
4072+
}
40474073
pub struct Methods {
40484074
avoid_breaking_exported_api: bool,
40494075
msrv: Msrv,
@@ -4092,6 +4118,7 @@ impl_lint_pass!(Methods => [
40924118
CLONE_ON_COPY,
40934119
CLONE_ON_REF_PTR,
40944120
COLLAPSIBLE_STR_REPLACE,
4121+
CONST_IS_EMPTY,
40954122
ITER_OVEREAGER_CLONED,
40964123
CLONED_INSTEAD_OF_COPIED,
40974124
FLAT_MAP_OPTION,
@@ -4445,7 +4472,7 @@ impl Methods {
44454472
("as_deref" | "as_deref_mut", []) => {
44464473
needless_option_as_deref::check(cx, expr, recv, name);
44474474
},
4448-
("as_bytes" | "is_empty", []) => {
4475+
("as_bytes", []) => {
44494476
if let Some(("as_str", recv, [], as_str_span, _)) = method_call(recv) {
44504477
redundant_as_str::check(cx, expr, recv, as_str_span, span);
44514478
}
@@ -4619,6 +4646,12 @@ impl Methods {
46194646
("hash", [arg]) => {
46204647
unit_hash::check(cx, expr, recv, arg);
46214648
},
4649+
("is_empty", []) => {
4650+
if let Some(("as_str", recv, [], as_str_span, _)) = method_call(recv) {
4651+
redundant_as_str::check(cx, expr, recv, as_str_span, span);
4652+
}
4653+
is_empty::check(cx, expr, recv);
4654+
},
46224655
("is_file", []) => filetype_is_file::check(cx, expr, recv),
46234656
("is_digit", [radix]) => is_digit_ascii_radix::check(cx, expr, recv, radix, &self.msrv),
46244657
("is_none", []) => check_is_some_is_none(cx, expr, recv, call_span, false),

clippy_utils/src/consts.rs

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ use rustc_hir::{BinOp, BinOpKind, Block, ConstBlock, Expr, ExprKind, HirId, Item
1010
use rustc_lexer::tokenize;
1111
use rustc_lint::LateContext;
1212
use rustc_middle::mir::interpret::{alloc_range, Scalar};
13+
use rustc_middle::mir::ConstValue;
1314
use rustc_middle::ty::{self, EarlyBinder, FloatTy, GenericArgsRef, IntTy, List, ScalarInt, Ty, TyCtxt, UintTy};
1415
use rustc_middle::{bug, mir, span_bug};
16+
use rustc_span::def_id::DefId;
1517
use rustc_span::symbol::{Ident, Symbol};
1618
use rustc_span::SyntaxContext;
1719
use rustc_target::abi::Size;
@@ -307,6 +309,12 @@ impl ConstantSource {
307309
}
308310
}
309311

312+
/// Attempts to check whether the expression is a constant representing an empty slice, str, array,
313+
/// etc…
314+
pub fn constant_is_empty(lcx: &LateContext<'_>, e: &Expr<'_>) -> Option<bool> {
315+
ConstEvalLateContext::new(lcx, lcx.typeck_results()).expr_is_empty(e)
316+
}
317+
310318
/// Attempts to evaluate the expression as a constant.
311319
pub fn constant<'tcx>(
312320
lcx: &LateContext<'tcx>,
@@ -406,7 +414,13 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
406414
match e.kind {
407415
ExprKind::ConstBlock(ConstBlock { body, .. }) => self.expr(self.lcx.tcx.hir().body(body).value),
408416
ExprKind::DropTemps(e) => self.expr(e),
409-
ExprKind::Path(ref qpath) => self.fetch_path(qpath, e.hir_id, self.typeck_results.expr_ty(e)),
417+
ExprKind::Path(ref qpath) => {
418+
self.fetch_path_and_apply(qpath, e.hir_id, self.typeck_results.expr_ty(e), |this, result| {
419+
let result = mir_to_const(this.lcx, result)?;
420+
this.source = ConstantSource::Constant;
421+
Some(result)
422+
})
423+
},
410424
ExprKind::Block(block, _) => self.block(block),
411425
ExprKind::Lit(lit) => {
412426
if is_direct_expn_of(e.span, "cfg").is_some() {
@@ -472,6 +486,49 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
472486
}
473487
}
474488

489+
/// Simple constant folding to determine if an expression is an empty slice, str, array, …
490+
/// `None` will be returned if the constness cannot be determined, or if the resolution
491+
/// leaves the local crate.
492+
pub fn expr_is_empty(&mut self, e: &Expr<'_>) -> Option<bool> {
493+
match e.kind {
494+
ExprKind::ConstBlock(ConstBlock { body, .. }) => self.expr_is_empty(self.lcx.tcx.hir().body(body).value),
495+
ExprKind::DropTemps(e) => self.expr_is_empty(e),
496+
ExprKind::Path(ref qpath) => {
497+
if !self
498+
.typeck_results
499+
.qpath_res(qpath, e.hir_id)
500+
.opt_def_id()
501+
.is_some_and(DefId::is_local)
502+
{
503+
return None;
504+
}
505+
self.fetch_path_and_apply(qpath, e.hir_id, self.typeck_results.expr_ty(e), |this, result| {
506+
mir_is_empty(this.lcx, result)
507+
})
508+
},
509+
ExprKind::Lit(lit) => {
510+
if is_direct_expn_of(e.span, "cfg").is_some() {
511+
None
512+
} else {
513+
match &lit.node {
514+
LitKind::Str(is, _) => Some(is.is_empty()),
515+
LitKind::ByteStr(s, _) | LitKind::CStr(s, _) => Some(s.is_empty()),
516+
_ => None,
517+
}
518+
}
519+
},
520+
ExprKind::Array(vec) => self.multi(vec).map(|v| v.is_empty()),
521+
ExprKind::Repeat(..) => {
522+
if let ty::Array(_, n) = self.typeck_results.expr_ty(e).kind() {
523+
Some(n.try_eval_target_usize(self.lcx.tcx, self.lcx.param_env)? == 0)
524+
} else {
525+
span_bug!(e.span, "typeck error");
526+
}
527+
},
528+
_ => None,
529+
}
530+
}
531+
475532
#[expect(clippy::cast_possible_wrap)]
476533
fn constant_not(&self, o: &Constant<'tcx>, ty: Ty<'_>) -> Option<Constant<'tcx>> {
477534
use self::Constant::{Bool, Int};
@@ -519,8 +576,11 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
519576
vec.iter().map(|elem| self.expr(elem)).collect::<Option<_>>()
520577
}
521578

522-
/// Lookup a possibly constant expression from an `ExprKind::Path`.
523-
fn fetch_path(&mut self, qpath: &QPath<'_>, id: HirId, ty: Ty<'tcx>) -> Option<Constant<'tcx>> {
579+
/// Lookup a possibly constant expression from an `ExprKind::Path` and apply a function on it.
580+
fn fetch_path_and_apply<T, F>(&mut self, qpath: &QPath<'_>, id: HirId, ty: Ty<'tcx>, f: F) -> Option<T>
581+
where
582+
F: FnOnce(&mut Self, rustc_middle::mir::Const<'tcx>) -> Option<T>,
583+
{
524584
let res = self.typeck_results.qpath_res(qpath, id);
525585
match res {
526586
Res::Def(DefKind::Const | DefKind::AssocConst, def_id) => {
@@ -553,9 +613,7 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
553613
.const_eval_resolve(self.param_env, mir::UnevaluatedConst::new(def_id, args), None)
554614
.ok()
555615
.map(|val| rustc_middle::mir::Const::from_value(val, ty))?;
556-
let result = mir_to_const(self.lcx, result)?;
557-
self.source = ConstantSource::Constant;
558-
Some(result)
616+
f(self, result)
559617
},
560618
_ => None,
561619
}
@@ -746,7 +804,6 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
746804
}
747805

748806
pub fn mir_to_const<'tcx>(lcx: &LateContext<'tcx>, result: mir::Const<'tcx>) -> Option<Constant<'tcx>> {
749-
use rustc_middle::mir::ConstValue;
750807
let mir::Const::Val(val, _) = result else {
751808
// We only work on evaluated consts.
752809
return None;
@@ -794,6 +851,42 @@ pub fn mir_to_const<'tcx>(lcx: &LateContext<'tcx>, result: mir::Const<'tcx>) ->
794851
}
795852
}
796853

854+
fn mir_is_empty<'tcx>(lcx: &LateContext<'tcx>, result: mir::Const<'tcx>) -> Option<bool> {
855+
let mir::Const::Val(val, _) = result else {
856+
// We only work on evaluated consts.
857+
return None;
858+
};
859+
match (val, result.ty().kind()) {
860+
(_, ty::Ref(_, inner_ty, _)) => match inner_ty.kind() {
861+
ty::Str | ty::Slice(_) => {
862+
if let ConstValue::Indirect { alloc_id, offset } = val {
863+
// Get the length from the slice, using the same formula as
864+
// [`ConstValue::try_get_slice_bytes_for_diagnostics`].
865+
let a = lcx.tcx.global_alloc(alloc_id).unwrap_memory().inner();
866+
let ptr_size = lcx.tcx.data_layout.pointer_size;
867+
if a.size() < offset + 2 * ptr_size {
868+
// (partially) dangling reference
869+
return None;
870+
}
871+
let len = a
872+
.read_scalar(&lcx.tcx, alloc_range(offset + ptr_size, ptr_size), false)
873+
.ok()?
874+
.to_target_usize(&lcx.tcx)
875+
.ok()?;
876+
Some(len == 0)
877+
} else {
878+
None
879+
}
880+
},
881+
ty::Array(_, len) => Some(len.try_to_target_usize(lcx.tcx)? == 0),
882+
_ => None,
883+
},
884+
(ConstValue::Indirect { .. }, ty::Array(_, len)) => Some(len.try_to_target_usize(lcx.tcx)? == 0),
885+
(ConstValue::ZeroSized, _) => Some(true),
886+
_ => None,
887+
}
888+
}
889+
797890
fn field_of_struct<'tcx>(
798891
adt_def: ty::AdtDef<'tcx>,
799892
lcx: &LateContext<'tcx>,

tests/ui/bool_assert_comparison.fixed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![allow(unused, clippy::assertions_on_constants)]
1+
#![allow(unused, clippy::assertions_on_constants, clippy::const_is_empty)]
22
#![warn(clippy::bool_assert_comparison)]
33

44
use std::ops::Not;

tests/ui/bool_assert_comparison.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![allow(unused, clippy::assertions_on_constants)]
1+
#![allow(unused, clippy::assertions_on_constants, clippy::const_is_empty)]
22
#![warn(clippy::bool_assert_comparison)]
33

44
use std::ops::Not;

0 commit comments

Comments
 (0)