Skip to content

Commit 4af1bef

Browse files
committed
feat(missing_asserts_for_indexing): support comparing to const values
1 parent 80fce9b commit 4af1bef

File tree

4 files changed

+220
-45
lines changed

4 files changed

+220
-45
lines changed

clippy_lints/src/missing_asserts_for_indexing.rs

Lines changed: 143 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::mem;
22
use std::ops::ControlFlow;
33

44
use clippy_utils::comparisons::{Rel, normalize_comparison};
5+
use clippy_utils::consts::{ConstEvalCtxt, Constant};
56
use clippy_utils::diagnostics::span_lint_and_then;
67
use clippy_utils::higher::{If, Range};
78
use clippy_utils::macros::{find_assert_eq_args, first_node_macro_backtrace, root_macro_call};
@@ -91,35 +92,133 @@ enum LengthComparison {
9192
/// `v.len() == 5`
9293
LengthEqualInt,
9394
}
95+
#[derive(Copy, Clone, Debug)]
96+
struct EvaluatedIntExpr<'hir> {
97+
expr: &'hir Expr<'hir>,
98+
value: usize,
99+
}
100+
#[derive(Copy, Clone, Debug)]
101+
enum AssertionSide<'hir> {
102+
/// `v.len()` in `v.len() > 5`
103+
SliceLen {
104+
/// `v` in `v.len()`
105+
slice: &'hir Expr<'hir>,
106+
},
107+
/// `5` in `v.len() > 5`
108+
AssertedLen(EvaluatedIntExpr<'hir>),
109+
}
110+
impl<'hir> AssertionSide<'hir> {
111+
pub fn from_expr(cx: &LateContext<'_>, expr: &'hir Expr<'hir>) -> Option<Self> {
112+
Self::asserted_len_from_int_lit_expr(expr)
113+
.or_else(|| Self::slice_len_from_expr(cx, expr))
114+
.or_else(|| Self::asserted_len_from_possibly_const_expr(cx, expr))
115+
}
116+
pub fn slice_len_from_expr(cx: &LateContext<'_>, expr: &'hir Expr<'hir>) -> Option<Self> {
117+
if let ExprKind::MethodCall(method, recv, [], _) = expr.kind
118+
// checking method name first rather than receiver's type could improve performance
119+
&& method.ident.name == sym::len
120+
&& cx.typeck_results().expr_ty_adjusted(recv).peel_refs().is_slice()
121+
{
122+
Some(Self::SliceLen { slice: recv })
123+
} else {
124+
None
125+
}
126+
}
127+
pub fn asserted_len_from_expr(cx: &LateContext<'_>, expr: &'hir Expr<'hir>) -> Option<Self> {
128+
Self::asserted_len_from_int_lit_expr(expr).or_else(|| Self::asserted_len_from_possibly_const_expr(cx, expr))
129+
}
130+
pub fn asserted_len_from_int_lit_expr(expr: &'hir Expr<'hir>) -> Option<Self> {
131+
if let ExprKind::Lit(Spanned {
132+
node: LitKind::Int(Pu128(x), _),
133+
..
134+
}) = expr.kind
135+
{
136+
Some(Self::AssertedLen(EvaluatedIntExpr {
137+
expr,
138+
value: x as usize,
139+
}))
140+
} else {
141+
None
142+
}
143+
}
144+
pub fn asserted_len_from_possibly_const_expr(cx: &LateContext<'_>, expr: &'hir Expr<'hir>) -> Option<Self> {
145+
if let Some(Constant::Int(x)) = ConstEvalCtxt::new(cx).eval(expr) {
146+
Some(Self::AssertedLen(EvaluatedIntExpr {
147+
expr,
148+
value: x as usize,
149+
}))
150+
} else {
151+
None
152+
}
153+
}
154+
}
94155

95156
/// Extracts parts out of a length comparison expression.
96157
///
97158
/// E.g. for `v.len() > 5` this returns `Some((LengthComparison::IntLessThanLength, 5, v.len()))`
98159
fn len_comparison<'hir>(
160+
cx: &LateContext<'_>,
99161
bin_op: BinOpKind,
100-
left: &'hir Expr<'hir>,
101-
right: &'hir Expr<'hir>,
102-
) -> Option<(LengthComparison, usize, &'hir Expr<'hir>)> {
103-
macro_rules! int_lit_pat {
104-
($id:ident) => {
105-
ExprKind::Lit(Spanned {
106-
node: LitKind::Int(Pu128($id), _),
107-
..
108-
})
109-
};
162+
left_expr: &'hir Expr<'hir>,
163+
right_expr: &'hir Expr<'hir>,
164+
) -> Option<(LengthComparison, EvaluatedIntExpr<'hir>, &'hir Expr<'hir>)> {
165+
fn sniff_operands<'hir>(
166+
cx: &LateContext<'_>,
167+
left: &'hir Expr<'hir>,
168+
right: &'hir Expr<'hir>,
169+
) -> Option<(AssertionSide<'hir>, AssertionSide<'hir>)> {
170+
// sniff as cheap as possible
171+
if let Some(left) = AssertionSide::asserted_len_from_int_lit_expr(left) {
172+
Some((left, AssertionSide::from_expr(cx, right)?))
173+
} else if let Some(left) = AssertionSide::slice_len_from_expr(cx, left) {
174+
Some((left, AssertionSide::asserted_len_from_expr(cx, right)?))
175+
} else {
176+
Some((
177+
AssertionSide::asserted_len_from_possibly_const_expr(cx, left)?,
178+
AssertionSide::slice_len_from_expr(cx, right)?,
179+
))
180+
}
110181
}
111182

183+
type Side<'hir> = AssertionSide<'hir>;
184+
112185
// normalize comparison, `v.len() > 4` becomes `4 < v.len()`
113186
// this simplifies the logic a bit
114-
let (op, left, right) = normalize_comparison(bin_op, left, right)?;
115-
match (op, left.kind, right.kind) {
116-
(Rel::Lt, int_lit_pat!(left), _) => Some((LengthComparison::IntLessThanLength, left as usize, right)),
117-
(Rel::Lt, _, int_lit_pat!(right)) => Some((LengthComparison::LengthLessThanInt, right as usize, left)),
118-
(Rel::Le, int_lit_pat!(left), _) => Some((LengthComparison::IntLessThanOrEqualLength, left as usize, right)),
119-
(Rel::Le, _, int_lit_pat!(right)) => Some((LengthComparison::LengthLessThanOrEqualInt, right as usize, left)),
120-
(Rel::Eq, int_lit_pat!(left), _) => Some((LengthComparison::LengthEqualInt, left as usize, right)),
121-
(Rel::Eq, _, int_lit_pat!(right)) => Some((LengthComparison::LengthEqualInt, right as usize, left)),
122-
_ => None,
187+
let (op, left_expr, right_expr) = normalize_comparison(bin_op, left_expr, right_expr)?;
188+
189+
let (left, right) = sniff_operands(cx, left_expr, right_expr)?;
190+
let (swapped, asserted_len, slice) = match (left, right) {
191+
// `A > B` (e.g. `5 > 4`)
192+
| (Side::AssertedLen(_), Side::AssertedLen(_))
193+
// `v.len() > w.len()`
194+
| (Side::SliceLen { .. }, Side::SliceLen { .. }) => return None,
195+
(Side::AssertedLen(asserted_len), Side::SliceLen { slice }) => {
196+
(false, asserted_len, slice)
197+
},
198+
(Side::SliceLen { slice }, Side::AssertedLen(asserted_len)) => {
199+
(true, asserted_len, slice)
200+
},
201+
};
202+
203+
match op {
204+
Rel::Lt => {
205+
let cmp = if swapped {
206+
LengthComparison::LengthLessThanInt
207+
} else {
208+
LengthComparison::IntLessThanLength
209+
};
210+
Some((cmp, asserted_len, slice))
211+
},
212+
Rel::Le => {
213+
let cmp = if swapped {
214+
LengthComparison::LengthLessThanOrEqualInt
215+
} else {
216+
LengthComparison::IntLessThanOrEqualLength
217+
};
218+
Some((cmp, asserted_len, slice))
219+
},
220+
Rel::Eq => Some((LengthComparison::LengthEqualInt, asserted_len, slice)),
221+
Rel::Ne => None,
123222
}
124223
}
125224

@@ -132,15 +231,15 @@ fn len_comparison<'hir>(
132231
fn assert_len_expr<'hir>(
133232
cx: &LateContext<'_>,
134233
expr: &'hir Expr<'hir>,
135-
) -> Option<(LengthComparison, usize, &'hir Expr<'hir>, Symbol)> {
136-
let ((cmp, asserted_len, slice_len), macro_call) = if let Some(If { cond, then, .. }) = If::hir(expr)
234+
) -> Option<(LengthComparison, EvaluatedIntExpr<'hir>, &'hir Expr<'hir>, Symbol)> {
235+
let ((cmp, asserted_len, slice), macro_call) = if let Some(If { cond, then, .. }) = If::hir(expr)
137236
&& let ExprKind::Unary(UnOp::Not, condition) = &cond.kind
138237
&& let ExprKind::Binary(bin_op, left, right) = &condition.kind
139238
// check if `then` block has a never type expression
140239
&& let ExprKind::Block(Block { expr: Some(then_expr), .. }, _) = then.kind
141240
&& cx.typeck_results().expr_ty(then_expr).is_never()
142241
{
143-
(len_comparison(bin_op.node, left, right)?, sym::assert_macro)
242+
(len_comparison(cx, bin_op.node, left, right)?, sym::assert_macro)
144243
} else if let Some((macro_call, bin_op)) = first_node_macro_backtrace(cx, expr).find_map(|macro_call| {
145244
match cx.tcx.get_diagnostic_name(macro_call.def_id) {
146245
Some(sym::assert_eq_macro) => Some((macro_call, BinOpKind::Eq)),
@@ -150,7 +249,7 @@ fn assert_len_expr<'hir>(
150249
}) && let Some((left, right, _)) = find_assert_eq_args(cx, expr, macro_call.expn)
151250
{
152251
(
153-
len_comparison(bin_op, left, right)?,
252+
len_comparison(cx, bin_op, left, right)?,
154253
root_macro_call(expr.span)
155254
.and_then(|macro_call| cx.tcx.get_diagnostic_name(macro_call.def_id))
156255
.unwrap_or(sym::assert_macro),
@@ -159,21 +258,14 @@ fn assert_len_expr<'hir>(
159258
return None;
160259
};
161260

162-
if let ExprKind::MethodCall(method, recv, [], _) = &slice_len.kind
163-
&& cx.typeck_results().expr_ty_adjusted(recv).peel_refs().is_slice()
164-
&& method.ident.name == sym::len
165-
{
166-
Some((cmp, asserted_len, recv, macro_call))
167-
} else {
168-
None
169-
}
261+
Some((cmp, asserted_len, slice, macro_call))
170262
}
171263

172264
#[derive(Debug)]
173265
enum IndexEntry<'hir> {
174266
/// `assert!` without any indexing (so far)
175267
StrayAssert {
176-
asserted_len: usize,
268+
asserted_len: EvaluatedIntExpr<'hir>,
177269
comparison: LengthComparison,
178270
assert_span: Span,
179271
slice: &'hir Expr<'hir>,
@@ -186,7 +278,7 @@ enum IndexEntry<'hir> {
186278
AssertWithIndex {
187279
highest_index: usize,
188280
is_first_highest: bool,
189-
asserted_len: usize,
281+
asserted_len: EvaluatedIntExpr<'hir>,
190282
assert_span: Span,
191283
slice: &'hir Expr<'hir>,
192284
indexes: Vec<Span>,
@@ -367,22 +459,29 @@ fn report_indexes(cx: &LateContext<'_>, map: UnindexMap<u64, Vec<IndexEntry<'_>>
367459
Some(format!("assert!({slice_str}.len() > {highest_index})",))
368460
},
369461
// `5 < v.len()` == `v.len() > 5`
370-
LengthComparison::IntLessThanLength if asserted_len < highest_index => {
462+
LengthComparison::IntLessThanLength if asserted_len.value < highest_index => {
371463
Some(format!("assert!({slice_str}.len() > {highest_index})",))
372464
},
373465
// `5 <= v.len() == `v.len() >= 5`
374-
LengthComparison::IntLessThanOrEqualLength if asserted_len <= highest_index => {
375-
Some(format!("assert!({slice_str}.len() > {highest_index})",))
466+
LengthComparison::IntLessThanOrEqualLength if asserted_len.value == highest_index => {
467+
let asserted_len_str =
468+
snippet_with_applicability(cx, asserted_len.expr.span, "_", &mut app);
469+
Some(format!("assert!({slice_str}.len() > {asserted_len_str})",))
470+
},
471+
LengthComparison::IntLessThanOrEqualLength if asserted_len.value < highest_index => {
472+
Some(format!("assert!({slice_str}.len() >= {})", highest_index + 1))
376473
},
377474
// `highest_index` here is rather a length, so we need to add 1 to it
378-
LengthComparison::LengthEqualInt if asserted_len < highest_index + 1 => match macro_call {
379-
sym::assert_eq_macro => {
380-
Some(format!("assert_eq!({slice_str}.len(), {})", highest_index + 1))
381-
},
382-
sym::debug_assert_eq_macro => {
383-
Some(format!("debug_assert_eq!({slice_str}.len(), {})", highest_index + 1))
384-
},
385-
_ => Some(format!("assert!({slice_str}.len() == {})", highest_index + 1)),
475+
LengthComparison::LengthEqualInt if asserted_len.value < highest_index + 1 => {
476+
match macro_call {
477+
sym::assert_eq_macro => {
478+
Some(format!("assert_eq!({slice_str}.len(), {})", highest_index + 1))
479+
},
480+
sym::debug_assert_eq_macro => {
481+
Some(format!("debug_assert_eq!({slice_str}.len(), {})", highest_index + 1))
482+
},
483+
_ => Some(format!("assert!({slice_str}.len() == {})", highest_index + 1)),
484+
}
386485
},
387486
_ => None,
388487
};

tests/ui/missing_asserts_for_indexing.fixed

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,24 @@ mod issue15988 {
180180
}
181181
}
182182

183+
fn assert_cmp_to_const(v1: &[u8], v2: &[u8], v3: &[u8], v4: &[u8]) {
184+
const N: usize = 2;
185+
186+
assert!(v1.len() >= N);
187+
assert!(v2.len() > N);
188+
assert!(v3.len() >= 4);
189+
assert!(v4.len() > 3);
190+
191+
let _ = v1[0] + v1[1];
192+
193+
let _ = v2[0] + v2[1] + v2[2];
194+
//~^ missing_asserts_for_indexing
195+
196+
let _ = v3[0] + v3[1] + v3[2] + v3[3];
197+
//~^ missing_asserts_for_indexing
198+
199+
let _ = v4[0] + v4[1] + v4[2] + v4[3];
200+
//~^ missing_asserts_for_indexing
201+
}
202+
183203
fn main() {}

tests/ui/missing_asserts_for_indexing.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,24 @@ mod issue15988 {
180180
}
181181
}
182182

183+
fn assert_cmp_to_const(v1: &[u8], v2: &[u8], v3: &[u8], v4: &[u8]) {
184+
const N: usize = 2;
185+
186+
assert!(v1.len() >= N);
187+
assert!(v2.len() >= N);
188+
assert!(v3.len() >= N);
189+
assert!(v4.len() > N);
190+
191+
let _ = v1[0] + v1[1];
192+
193+
let _ = v2[0] + v2[1] + v2[2];
194+
//~^ missing_asserts_for_indexing
195+
196+
let _ = v3[0] + v3[1] + v3[2] + v3[3];
197+
//~^ missing_asserts_for_indexing
198+
199+
let _ = v4[0] + v4[1] + v4[2] + v4[3];
200+
//~^ missing_asserts_for_indexing
201+
}
202+
183203
fn main() {}

tests/ui/missing_asserts_for_indexing.stderr

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,5 +187,41 @@ LL - debug_assert_eq!(v.len(), 2);
187187
LL + debug_assert_eq!(v.len(), 3);
188188
|
189189

190-
error: aborting due to 15 previous errors
190+
error: indexing into a slice multiple times with an `assert` that does not cover the highest index
191+
--> tests/ui/missing_asserts_for_indexing.rs:193:13
192+
|
193+
LL | let _ = v2[0] + v2[1] + v2[2];
194+
| ^^^^^ ^^^^^ ^^^^^
195+
|
196+
help: provide the highest index that is indexed with
197+
|
198+
LL - assert!(v2.len() >= N);
199+
LL + assert!(v2.len() > N);
200+
|
201+
202+
error: indexing into a slice multiple times with an `assert` that does not cover the highest index
203+
--> tests/ui/missing_asserts_for_indexing.rs:196:13
204+
|
205+
LL | let _ = v3[0] + v3[1] + v3[2] + v3[3];
206+
| ^^^^^ ^^^^^ ^^^^^ ^^^^^
207+
|
208+
help: provide the highest index that is indexed with
209+
|
210+
LL - assert!(v3.len() >= N);
211+
LL + assert!(v3.len() >= 4);
212+
|
213+
214+
error: indexing into a slice multiple times with an `assert` that does not cover the highest index
215+
--> tests/ui/missing_asserts_for_indexing.rs:199:13
216+
|
217+
LL | let _ = v4[0] + v4[1] + v4[2] + v4[3];
218+
| ^^^^^ ^^^^^ ^^^^^ ^^^^^
219+
|
220+
help: provide the highest index that is indexed with
221+
|
222+
LL - assert!(v4.len() > N);
223+
LL + assert!(v4.len() > 3);
224+
|
225+
226+
error: aborting due to 18 previous errors
191227

0 commit comments

Comments
 (0)