Skip to content

Commit c94ef81

Browse files
committed
feat: extend const_is_empty with many kinds of constants
1 parent 67db984 commit c94ef81

File tree

4 files changed

+233
-90
lines changed

4 files changed

+233
-90
lines changed

clippy_lints/src/methods/is_empty.rs

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
use clippy_utils::diagnostics::span_lint_and_note;
1+
use clippy_utils::consts::constant_is_empty;
2+
use clippy_utils::diagnostics::span_lint;
23
use clippy_utils::expr_or_init;
3-
use rustc_ast::LitKind;
4-
use rustc_hir::{Expr, ExprKind};
4+
use rustc_hir::Expr;
55
use rustc_lint::{LateContext, LintContext};
66
use rustc_middle::lint::in_external_macro;
7-
use rustc_span::source_map::Spanned;
87

98
use super::CONST_IS_EMPTY;
109

@@ -13,28 +12,12 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &'_ Expr<'_>, receiver: &Expr<'_
1312
return;
1413
}
1514
let init_expr = expr_or_init(cx, receiver);
16-
if let Some(init_is_empty) = is_empty(init_expr)
17-
&& init_expr.span.eq_ctxt(receiver.span)
18-
{
19-
span_lint_and_note(
15+
if let Some(init_is_empty) = constant_is_empty(cx, init_expr) {
16+
span_lint(
2017
cx,
2118
CONST_IS_EMPTY,
2219
expr.span,
2320
&format!("this expression always evaluates to {init_is_empty:?}"),
24-
Some(init_expr.span),
25-
"because its initialization value is constant",
2621
);
2722
}
2823
}
29-
30-
fn is_empty(expr: &'_ rustc_hir::Expr<'_>) -> Option<bool> {
31-
if let ExprKind::Lit(Spanned { node, .. }) = expr.kind {
32-
match node {
33-
LitKind::Str(sym, _) => Some(sym.is_empty()),
34-
LitKind::ByteStr(value, _) | LitKind::CStr(value, _) => Some(value.is_empty()),
35-
_ => None,
36-
}
37-
} else {
38-
None
39-
}
40-
}

clippy_utils/src/consts.rs

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ 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};
1516
use rustc_span::symbol::{Ident, Symbol};
@@ -303,6 +304,12 @@ impl ConstantSource {
303304
}
304305
}
305306

307+
/// Attempts to check whether the expression is a constant representing an empty slice, str, array,
308+
/// etc…
309+
pub fn constant_is_empty(lcx: &LateContext<'_>, e: &Expr<'_>) -> Option<bool> {
310+
ConstEvalLateContext::new(lcx, lcx.typeck_results()).expr_is_empty(e)
311+
}
312+
306313
/// Attempts to evaluate the expression as a constant.
307314
pub fn constant<'tcx>(
308315
lcx: &LateContext<'tcx>,
@@ -402,7 +409,13 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
402409
match e.kind {
403410
ExprKind::ConstBlock(ConstBlock { body, .. }) => self.expr(self.lcx.tcx.hir().body(body).value),
404411
ExprKind::DropTemps(e) => self.expr(e),
405-
ExprKind::Path(ref qpath) => self.fetch_path(qpath, e.hir_id, self.typeck_results.expr_ty(e)),
412+
ExprKind::Path(ref qpath) => {
413+
self.fetch_path_and_apply(qpath, e.hir_id, self.typeck_results.expr_ty(e), |this, result| {
414+
let result = mir_to_const(this.lcx, result)?;
415+
this.source = ConstantSource::Constant;
416+
Some(result)
417+
})
418+
},
406419
ExprKind::Block(block, _) => self.block(block),
407420
ExprKind::Lit(lit) => {
408421
if is_direct_expn_of(e.span, "cfg").is_some() {
@@ -468,6 +481,39 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
468481
}
469482
}
470483

484+
/// Simple constant folding to determine if an expression is an empty slice, str, array, …
485+
pub fn expr_is_empty(&mut self, e: &Expr<'_>) -> Option<bool> {
486+
match e.kind {
487+
ExprKind::ConstBlock(ConstBlock { body, .. }) => self.expr_is_empty(self.lcx.tcx.hir().body(body).value),
488+
ExprKind::DropTemps(e) => self.expr_is_empty(e),
489+
ExprKind::Path(ref qpath) => {
490+
self.fetch_path_and_apply(qpath, e.hir_id, self.typeck_results.expr_ty(e), |this, result| {
491+
mir_is_empty(this.lcx, result)
492+
})
493+
},
494+
ExprKind::Lit(lit) => {
495+
if is_direct_expn_of(e.span, "cfg").is_some() {
496+
None
497+
} else {
498+
match &lit.node {
499+
LitKind::Str(is, _) => Some(is.is_empty()),
500+
LitKind::ByteStr(s, _) | LitKind::CStr(s, _) => Some(s.is_empty()),
501+
_ => None,
502+
}
503+
}
504+
},
505+
ExprKind::Array(vec) => self.multi(vec).map(|v| v.is_empty()),
506+
ExprKind::Repeat(..) => {
507+
if let ty::Array(_, n) = self.typeck_results.expr_ty(e).kind() {
508+
Some(n.try_eval_target_usize(self.lcx.tcx, self.lcx.param_env)? == 0)
509+
} else {
510+
span_bug!(e.span, "typeck error");
511+
}
512+
},
513+
_ => None,
514+
}
515+
}
516+
471517
#[expect(clippy::cast_possible_wrap)]
472518
fn constant_not(&self, o: &Constant<'tcx>, ty: Ty<'_>) -> Option<Constant<'tcx>> {
473519
use self::Constant::{Bool, Int};
@@ -515,8 +561,11 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
515561
vec.iter().map(|elem| self.expr(elem)).collect::<Option<_>>()
516562
}
517563

518-
/// Lookup a possibly constant expression from an `ExprKind::Path`.
519-
fn fetch_path(&mut self, qpath: &QPath<'_>, id: HirId, ty: Ty<'tcx>) -> Option<Constant<'tcx>> {
564+
/// Lookup a possibly constant expression from an `ExprKind::Path` and apply a function on it.
565+
fn fetch_path_and_apply<T, F>(&mut self, qpath: &QPath<'_>, id: HirId, ty: Ty<'tcx>, f: F) -> Option<T>
566+
where
567+
F: FnOnce(&mut Self, rustc_middle::mir::Const<'tcx>) -> Option<T>,
568+
{
520569
let res = self.typeck_results.qpath_res(qpath, id);
521570
match res {
522571
Res::Def(DefKind::Const | DefKind::AssocConst, def_id) => {
@@ -549,9 +598,7 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
549598
.const_eval_resolve(self.param_env, mir::UnevaluatedConst::new(def_id, args), None)
550599
.ok()
551600
.map(|val| rustc_middle::mir::Const::from_value(val, ty))?;
552-
let result = mir_to_const(self.lcx, result)?;
553-
self.source = ConstantSource::Constant;
554-
Some(result)
601+
f(self, result)
555602
},
556603
_ => None,
557604
}
@@ -742,7 +789,6 @@ impl<'a, 'tcx> ConstEvalLateContext<'a, 'tcx> {
742789
}
743790

744791
pub fn mir_to_const<'tcx>(lcx: &LateContext<'tcx>, result: mir::Const<'tcx>) -> Option<Constant<'tcx>> {
745-
use rustc_middle::mir::ConstValue;
746792
let mir::Const::Val(val, _) = result else {
747793
// We only work on evaluated consts.
748794
return None;
@@ -788,6 +834,40 @@ pub fn mir_to_const<'tcx>(lcx: &LateContext<'tcx>, result: mir::Const<'tcx>) ->
788834
}
789835
}
790836

837+
fn mir_is_empty<'tcx>(lcx: &LateContext<'tcx>, result: mir::Const<'tcx>) -> Option<bool> {
838+
let mir::Const::Val(val, _) = result else {
839+
// We only work on evaluated consts.
840+
return None;
841+
};
842+
match (val, result.ty().kind()) {
843+
(_, ty::Ref(_, inner_ty, _)) => match inner_ty.kind() {
844+
ty::Str | ty::Slice(_) => {
845+
if let ConstValue::Indirect { alloc_id, offset } = val {
846+
let a = lcx.tcx.global_alloc(alloc_id).unwrap_memory().inner();
847+
let ptr_size = lcx.tcx.data_layout.pointer_size;
848+
if a.size() < offset + 2 * ptr_size {
849+
// (partially) dangling reference
850+
return None;
851+
}
852+
let len = a
853+
.read_scalar(&lcx.tcx, alloc_range(offset + ptr_size, ptr_size), false)
854+
.ok()?
855+
.to_target_usize(&lcx.tcx)
856+
.ok()?;
857+
Some(len == 0)
858+
} else {
859+
None
860+
}
861+
},
862+
ty::Array(_, len) => Some(len.try_to_target_usize(lcx.tcx)? == 0),
863+
_ => None,
864+
},
865+
(ConstValue::Indirect { .. }, ty::Array(_, len)) => Some(len.try_to_target_usize(lcx.tcx)? == 0),
866+
(ConstValue::ZeroSized, _) => Some(true),
867+
_ => None,
868+
}
869+
}
870+
791871
fn field_of_struct<'tcx>(
792872
adt_def: ty::AdtDef<'tcx>,
793873
lcx: &LateContext<'tcx>,

tests/ui/const_is_empty.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,55 @@ fn test_propagated() {
3838
}
3939
}
4040

41+
const EMPTY_STR: &str = "";
42+
const NON_EMPTY_STR: &str = "foo";
43+
const EMPTY_BSTR: &[u8] = b"";
44+
const NON_EMPTY_BSTR: &[u8] = b"foo";
45+
const EMPTY_U8_SLICE: &[u8] = &[];
46+
const NON_EMPTY_U8_SLICE: &[u8] = &[1, 2];
47+
const EMPTY_SLICE: &[u32] = &[];
48+
const NON_EMPTY_SLICE: &[u32] = &[1, 2];
49+
const NON_EMPTY_SLICE_REPEAT: &[u32] = &[1; 2];
50+
const EMPTY_ARRAY: [u32; 0] = [];
51+
const EMPTY_ARRAY_REPEAT: [u32; 0] = [1; 0];
52+
const NON_EMPTY_ARRAY: [u32; 2] = [1, 2];
53+
const NON_EMPTY_ARRAY_REPEAT: [u32; 2] = [1; 2];
54+
const EMPTY_REF_ARRAY: &[u32; 0] = &[];
55+
const NON_EMPTY_REF_ARRAY: &[u32; 3] = &[1, 2, 3];
56+
57+
fn test_from_const() {
58+
let _ = EMPTY_STR.is_empty();
59+
//~^ ERROR: this expression always evaluates to true
60+
let _ = NON_EMPTY_STR.is_empty();
61+
//~^ ERROR: this expression always evaluates to false
62+
let _ = EMPTY_BSTR.is_empty();
63+
//~^ ERROR: this expression always evaluates to true
64+
let _ = NON_EMPTY_BSTR.is_empty();
65+
//~^ ERROR: this expression always evaluates to false
66+
let _ = EMPTY_ARRAY.is_empty();
67+
//~^ ERROR: this expression always evaluates to true
68+
let _ = EMPTY_ARRAY_REPEAT.is_empty();
69+
//~^ ERROR: this expression always evaluates to true
70+
let _ = EMPTY_U8_SLICE.is_empty();
71+
//~^ ERROR: this expression always evaluates to true
72+
let _ = NON_EMPTY_U8_SLICE.is_empty();
73+
//~^ ERROR: this expression always evaluates to false
74+
let _ = NON_EMPTY_ARRAY.is_empty();
75+
//~^ ERROR: this expression always evaluates to false
76+
let _ = NON_EMPTY_ARRAY_REPEAT.is_empty();
77+
//~^ ERROR: this expression always evaluates to false
78+
let _ = EMPTY_REF_ARRAY.is_empty();
79+
//~^ ERROR: this expression always evaluates to true
80+
let _ = NON_EMPTY_REF_ARRAY.is_empty();
81+
//~^ ERROR: this expression always evaluates to false
82+
let _ = EMPTY_SLICE.is_empty();
83+
//~^ ERROR: this expression always evaluates to true
84+
let _ = NON_EMPTY_SLICE.is_empty();
85+
//~^ ERROR: this expression always evaluates to false
86+
let _ = NON_EMPTY_SLICE_REPEAT.is_empty();
87+
//~^ ERROR: this expression always evaluates to false
88+
}
89+
4190
fn main() {
4291
let value = "foobar";
4392
let _ = value.is_empty();

0 commit comments

Comments
 (0)