Skip to content

Commit 541f1f4

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

File tree

4 files changed

+201
-46
lines changed

4 files changed

+201
-46
lines changed

clippy_lints/src/missing_asserts_for_indexing.rs

Lines changed: 124 additions & 45 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,113 @@ 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 slice_len_from_expr(cx: &LateContext<'_>, expr: &'hir Expr<'hir>) -> Option<Self> {
112+
if let ExprKind::MethodCall(method, recv, [], _) = expr.kind
113+
// checking method name first rather than receiver's type could improve performance
114+
&& method.ident.name == sym::len
115+
&& cx.typeck_results().expr_ty_adjusted(recv).peel_refs().is_slice()
116+
{
117+
Some(Self::SliceLen { slice: recv })
118+
} else {
119+
None
120+
}
121+
}
122+
pub fn asserted_len_from_expr(cx: &LateContext<'_>, expr: &'hir Expr<'hir>) -> Option<Self> {
123+
if let ExprKind::Lit(Spanned { node, .. }) = expr.kind {
124+
// cases like `b'A'` or `5.0` would be extremely rare so
125+
// ignore those and consider only integers
126+
if let LitKind::Int(Pu128(x), _) = node {
127+
// integer literals
128+
Some(Self::AssertedLen(EvaluatedIntExpr {
129+
expr,
130+
value: x as usize,
131+
}))
132+
} else {
133+
None
134+
}
135+
} else if let Some(Constant::Int(x)) = ConstEvalCtxt::new(cx).eval(expr) {
136+
Some(Self::AssertedLen(EvaluatedIntExpr {
137+
expr,
138+
value: x as usize,
139+
}))
140+
} else {
141+
None
142+
}
143+
}
144+
}
94145

95146
/// Extracts parts out of a length comparison expression.
96147
///
97148
/// E.g. for `v.len() > 5` this returns `Some((LengthComparison::IntLessThanLength, 5, v.len()))`
98149
fn len_comparison<'hir>(
150+
cx: &LateContext<'_>,
99151
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-
};
110-
}
152+
left_expr: &'hir Expr<'hir>,
153+
right_expr: &'hir Expr<'hir>,
154+
) -> Option<(LengthComparison, EvaluatedIntExpr<'hir>, &'hir Expr<'hir>)> {
155+
type Side<'hir> = AssertionSide<'hir>;
111156

112157
// normalize comparison, `v.len() > 4` becomes `4 < v.len()`
113158
// 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,
159+
let (op, left_expr, right_expr) = normalize_comparison(bin_op, left_expr, right_expr)?;
160+
161+
// sniff operands as cheap as possible
162+
// eliminate possibility of both sides being `v.len()` variant
163+
let (left, right) = match AssertionSide::slice_len_from_expr(cx, left_expr) {
164+
Some(left) => (left, AssertionSide::asserted_len_from_expr(cx, right_expr)?),
165+
None => (
166+
AssertionSide::asserted_len_from_expr(cx, left_expr)?,
167+
AssertionSide::slice_len_from_expr(cx, right_expr)?,
168+
),
169+
};
170+
let (swapped, asserted_len, slice) = match (left, right) {
171+
// `A > B` (e.g. `5 > 4`)
172+
| (Side::AssertedLen(_), Side::AssertedLen(_))
173+
// `v.len() > w.len()`
174+
| (Side::SliceLen { .. }, Side::SliceLen { .. }) => return None,
175+
(Side::AssertedLen(asserted_len), Side::SliceLen { slice }) => {
176+
(false, asserted_len, slice)
177+
},
178+
(Side::SliceLen { slice }, Side::AssertedLen(asserted_len)) => {
179+
(true, asserted_len, slice)
180+
},
181+
};
182+
183+
match op {
184+
Rel::Lt => {
185+
let cmp = if swapped {
186+
LengthComparison::LengthLessThanInt
187+
} else {
188+
LengthComparison::IntLessThanLength
189+
};
190+
Some((cmp, asserted_len, slice))
191+
},
192+
Rel::Le => {
193+
let cmp = if swapped {
194+
LengthComparison::LengthLessThanOrEqualInt
195+
} else {
196+
LengthComparison::IntLessThanOrEqualLength
197+
};
198+
Some((cmp, asserted_len, slice))
199+
},
200+
Rel::Eq => Some((LengthComparison::LengthEqualInt, asserted_len, slice)),
201+
Rel::Ne => None,
123202
}
124203
}
125204

@@ -132,15 +211,15 @@ fn len_comparison<'hir>(
132211
fn assert_len_expr<'hir>(
133212
cx: &LateContext<'_>,
134213
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)
214+
) -> Option<(LengthComparison, EvaluatedIntExpr<'hir>, &'hir Expr<'hir>, Symbol)> {
215+
let ((cmp, asserted_len, slice), macro_call) = if let Some(If { cond, then, .. }) = If::hir(expr)
137216
&& let ExprKind::Unary(UnOp::Not, condition) = &cond.kind
138217
&& let ExprKind::Binary(bin_op, left, right) = &condition.kind
139218
// check if `then` block has a never type expression
140219
&& let ExprKind::Block(Block { expr: Some(then_expr), .. }, _) = then.kind
141220
&& cx.typeck_results().expr_ty(then_expr).is_never()
142221
{
143-
(len_comparison(bin_op.node, left, right)?, sym::assert_macro)
222+
(len_comparison(cx, bin_op.node, left, right)?, sym::assert_macro)
144223
} else if let Some((macro_call, bin_op)) = first_node_macro_backtrace(cx, expr).find_map(|macro_call| {
145224
match cx.tcx.get_diagnostic_name(macro_call.def_id) {
146225
Some(sym::assert_eq_macro) => Some((macro_call, BinOpKind::Eq)),
@@ -150,7 +229,7 @@ fn assert_len_expr<'hir>(
150229
}) && let Some((left, right, _)) = find_assert_eq_args(cx, expr, macro_call.expn)
151230
{
152231
(
153-
len_comparison(bin_op, left, right)?,
232+
len_comparison(cx, bin_op, left, right)?,
154233
root_macro_call(expr.span)
155234
.and_then(|macro_call| cx.tcx.get_diagnostic_name(macro_call.def_id))
156235
.unwrap_or(sym::assert_macro),
@@ -159,21 +238,14 @@ fn assert_len_expr<'hir>(
159238
return None;
160239
};
161240

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-
}
241+
Some((cmp, asserted_len, slice, macro_call))
170242
}
171243

172244
#[derive(Debug)]
173245
enum IndexEntry<'hir> {
174246
/// `assert!` without any indexing (so far)
175247
StrayAssert {
176-
asserted_len: usize,
248+
asserted_len: EvaluatedIntExpr<'hir>,
177249
comparison: LengthComparison,
178250
assert_span: Span,
179251
slice: &'hir Expr<'hir>,
@@ -186,7 +258,7 @@ enum IndexEntry<'hir> {
186258
AssertWithIndex {
187259
highest_index: usize,
188260
is_first_highest: bool,
189-
asserted_len: usize,
261+
asserted_len: EvaluatedIntExpr<'hir>,
190262
assert_span: Span,
191263
slice: &'hir Expr<'hir>,
192264
indexes: Vec<Span>,
@@ -367,22 +439,29 @@ fn report_indexes(cx: &LateContext<'_>, map: UnindexMap<u64, Vec<IndexEntry<'_>>
367439
Some(format!("assert!({slice_str}.len() > {highest_index})",))
368440
},
369441
// `5 < v.len()` == `v.len() > 5`
370-
LengthComparison::IntLessThanLength if asserted_len < highest_index => {
442+
LengthComparison::IntLessThanLength if asserted_len.value < highest_index => {
371443
Some(format!("assert!({slice_str}.len() > {highest_index})",))
372444
},
373445
// `5 <= v.len() == `v.len() >= 5`
374-
LengthComparison::IntLessThanOrEqualLength if asserted_len <= highest_index => {
375-
Some(format!("assert!({slice_str}.len() > {highest_index})",))
446+
LengthComparison::IntLessThanOrEqualLength if asserted_len.value == highest_index => {
447+
let asserted_len_str =
448+
snippet_with_applicability(cx, asserted_len.expr.span, "_", &mut app);
449+
Some(format!("assert!({slice_str}.len() > {asserted_len_str})",))
450+
},
451+
LengthComparison::IntLessThanOrEqualLength if asserted_len.value < highest_index => {
452+
Some(format!("assert!({slice_str}.len() >= {})", highest_index + 1))
376453
},
377454
// `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)),
455+
LengthComparison::LengthEqualInt if asserted_len.value < highest_index + 1 => {
456+
match macro_call {
457+
sym::assert_eq_macro => {
458+
Some(format!("assert_eq!({slice_str}.len(), {})", highest_index + 1))
459+
},
460+
sym::debug_assert_eq_macro => {
461+
Some(format!("debug_assert_eq!({slice_str}.len(), {})", highest_index + 1))
462+
},
463+
_ => Some(format!("assert!({slice_str}.len() == {})", highest_index + 1)),
464+
}
386465
},
387466
_ => None,
388467
};

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)