Skip to content

Commit 08718ae

Browse files
committed
new lint: missing_assert_for_indexing
1 parent 3594d55 commit 08718ae

File tree

6 files changed

+741
-0
lines changed

6 files changed

+741
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4829,6 +4829,7 @@ Released 2018-09-13
48294829
[`mismatching_type_param_order`]: https://rust-lang.github.io/rust-clippy/master/index.html#mismatching_type_param_order
48304830
[`misnamed_getters`]: https://rust-lang.github.io/rust-clippy/master/index.html#misnamed_getters
48314831
[`misrefactored_assign_op`]: https://rust-lang.github.io/rust-clippy/master/index.html#misrefactored_assign_op
4832+
[`missing_assert_for_indexing`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_assert_for_indexing
48324833
[`missing_assert_message`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_assert_message
48334834
[`missing_const_for_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn
48344835
[`missing_docs_in_private_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_docs_in_private_items

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
425425
crate::misc_early::UNSEPARATED_LITERAL_SUFFIX_INFO,
426426
crate::misc_early::ZERO_PREFIXED_LITERAL_INFO,
427427
crate::mismatching_type_param_order::MISMATCHING_TYPE_PARAM_ORDER_INFO,
428+
crate::missing_assert_for_indexing::MISSING_ASSERT_FOR_INDEXING_INFO,
428429
crate::missing_assert_message::MISSING_ASSERT_MESSAGE_INFO,
429430
crate::missing_const_for_fn::MISSING_CONST_FOR_FN_INFO,
430431
crate::missing_doc::MISSING_DOCS_IN_PRIVATE_ITEMS_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ mod minmax;
198198
mod misc;
199199
mod misc_early;
200200
mod mismatching_type_param_order;
201+
mod missing_assert_for_indexing;
201202
mod missing_assert_message;
202203
mod missing_const_for_fn;
203204
mod missing_doc;
@@ -970,6 +971,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
970971
store.register_late_pass(|_| Box::new(manual_slice_size_calculation::ManualSliceSizeCalculation));
971972
store.register_early_pass(|| Box::new(suspicious_doc_comments::SuspiciousDocComments));
972973
store.register_late_pass(|_| Box::new(items_after_test_module::ItemsAfterTestModule));
974+
store.register_late_pass(|_| Box::new(missing_assert_for_indexing::MissingAssertsForIndexing));
973975
// add lints here, do not remove this comment, it's used in `new_lint`
974976
}
975977

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
use std::ops::ControlFlow;
2+
use std::{collections::hash_map::Entry, mem};
3+
4+
use clippy_utils::source::snippet;
5+
use clippy_utils::{
6+
comparisons::{normalize_comparison, Rel},
7+
diagnostics::span_lint_and_then,
8+
hash_expr, higher,
9+
visitors::for_each_expr,
10+
};
11+
use rustc_ast::{LitKind, RangeLimits};
12+
use rustc_data_structures::fx::FxHashMap;
13+
use rustc_errors::{Applicability, Diagnostic};
14+
use rustc_hir::{BinOp, Block, Expr, ExprKind, UnOp};
15+
use rustc_lint::{LateContext, LateLintPass};
16+
use rustc_session::{declare_lint_pass, declare_tool_lint};
17+
use rustc_span::source_map::Spanned;
18+
use rustc_span::{sym, Span};
19+
20+
declare_clippy_lint! {
21+
/// ### What it does
22+
/// Checks for repeated slice indexing without asserting beforehand that the length
23+
/// is greater than the largest index used to index into the slice.
24+
///
25+
/// ### Why is this bad?
26+
/// In the general case where the compiler does not have a lot of information
27+
/// about the length of a slice, indexing it repeatedly will generate a bounds check
28+
/// for every single index.
29+
///
30+
/// Asserting that the length of the slice is at least as large as the largest value
31+
/// to index beforehand gives the compiler enough information to elide the bounds checks,
32+
/// effectively reducing the number of bounds checks from however many times
33+
/// the slice was indexed to just one (the assert).
34+
///
35+
/// ### Drawbacks
36+
/// False positives. It is, in general, very difficult to predict how well
37+
/// the optimizer will be able to elide bounds checks and it very much depends on
38+
/// the surrounding code. For example, indexing into the slice yielded by the
39+
/// [`slice::chunks_exact`](https://doc.rust-lang.org/stable/std/primitive.slice.html#method.chunks_exact)
40+
/// iterator will likely have all of the bounds checks elided even without an assert
41+
/// if the `chunk_size` is a constant.
42+
///
43+
/// Asserts are not tracked across function calls. Asserting the length of a slice
44+
/// in a different function likely gives the optimizer enough information
45+
/// about the length of a slice, but this lint will not detect that.
46+
///
47+
/// ### Example
48+
/// ```rust
49+
/// fn sum(v: &[u8]) -> u8 {
50+
/// // 4 bounds checks
51+
/// v[0] + v[1] + v[2] + v[3]
52+
/// }
53+
/// ```
54+
/// Use instead:
55+
/// ```rust
56+
/// fn sum(v: &[u8]) -> u8 {
57+
/// assert!(v.len() > 4);
58+
/// // no bounds checks
59+
/// v[0] + v[1] + v[2] + v[3]
60+
/// }
61+
/// ```
62+
#[clippy::version = "1.70.0"]
63+
pub MISSING_ASSERT_FOR_INDEXING,
64+
nursery,
65+
"indexing into a slice multiple times without an `assert`"
66+
}
67+
declare_lint_pass!(MissingAssertsForIndexing => [MISSING_ASSERT_FOR_INDEXING]);
68+
69+
fn report_lint<F>(cx: &LateContext<'_>, full_span: Span, msg: &str, indexes: &[Span], f: F)
70+
where
71+
F: FnOnce(&mut Diagnostic),
72+
{
73+
span_lint_and_then(cx, MISSING_ASSERT_FOR_INDEXING, full_span, msg, |diag| {
74+
f(diag);
75+
for span in indexes {
76+
diag.span_note(*span, "slice indexed here");
77+
}
78+
diag.note("asserting the length before indexing will elide bounds checks");
79+
});
80+
}
81+
82+
#[derive(Copy, Clone, Debug)]
83+
enum LengthComparison {
84+
/// `v.len() < 5`
85+
LengthLessThanInt,
86+
/// `5 < v.len()`
87+
IntLessThanLength,
88+
/// `v.len() <= 5`
89+
LengthLessThanOrEqualInt,
90+
/// `5 <= v.len()`
91+
IntLessThanOrEqualLength,
92+
}
93+
94+
fn len_comparison<'hir>(
95+
bin_op: BinOp,
96+
left: &'hir Expr<'hir>,
97+
right: &'hir Expr<'hir>,
98+
) -> Option<(LengthComparison, usize, &'hir Expr<'hir>)> {
99+
macro_rules! int_lit_pat {
100+
($id:ident) => {
101+
ExprKind::Lit(Spanned {
102+
node: LitKind::Int($id, _),
103+
..
104+
})
105+
};
106+
}
107+
108+
// normalize comparison, `v.len() > 4` becomes `4 <= v.len()`
109+
// this simplifies the logic a bit
110+
let (op, left, right) = normalize_comparison(bin_op.node, left, right)?;
111+
match (op, &left.kind, &right.kind) {
112+
(Rel::Lt, int_lit_pat!(left), _) => Some((LengthComparison::IntLessThanLength, *left as usize, right)),
113+
(Rel::Lt, _, int_lit_pat!(right)) => Some((LengthComparison::LengthLessThanInt, *right as usize, left)),
114+
(Rel::Le, int_lit_pat!(left), _) => Some((LengthComparison::IntLessThanOrEqualLength, *left as usize, right)),
115+
(Rel::Le, _, int_lit_pat!(right)) => Some((LengthComparison::LengthLessThanOrEqualInt, *right as usize, left)),
116+
_ => None,
117+
}
118+
}
119+
120+
fn assert_len_expr<'hir>(
121+
cx: &LateContext<'_>,
122+
expr: &'hir Expr<'hir>,
123+
) -> Option<(LengthComparison, usize, &'hir Expr<'hir>)> {
124+
if let Some(higher::If { cond, then, .. }) = higher::If::hir(expr)
125+
&& let ExprKind::Unary(UnOp::Not, condition) = &cond.kind
126+
&& let ExprKind::Binary(bin_op, left, right) = &condition.kind
127+
128+
&& let Some((cmp, asserted_len, slice_len)) = len_comparison(*bin_op, left, right)
129+
&& let ExprKind::MethodCall(method, recv, ..) = &slice_len.kind
130+
&& cx.typeck_results().expr_ty(recv).peel_refs().is_slice()
131+
&& method.ident.name == sym::len
132+
133+
// check if `then` block has a never type expression
134+
&& let ExprKind::Block(Block { expr: Some(then_expr), .. }, _) = then.kind
135+
&& cx.typeck_results().expr_ty(then_expr).is_never()
136+
{
137+
Some((cmp, asserted_len, recv))
138+
} else {
139+
None
140+
}
141+
}
142+
143+
#[derive(Debug)]
144+
enum IndexEntry {
145+
StrayAssert {
146+
asserted_len: usize,
147+
cmp: LengthComparison,
148+
assert_span: Span,
149+
slice_span: Span,
150+
},
151+
AssertWithIndex {
152+
highest_index: usize,
153+
asserted_len: usize,
154+
assert_span: Span,
155+
slice_span: Span,
156+
indexes: Vec<Span>,
157+
cmp: LengthComparison,
158+
},
159+
AssertWithoutIndex {
160+
highest_index: usize,
161+
indexes: Vec<Span>,
162+
slice_span: Span,
163+
},
164+
}
165+
166+
impl IndexEntry {
167+
pub fn spans(&self) -> Option<&[Span]> {
168+
match self {
169+
IndexEntry::StrayAssert { .. } => None,
170+
IndexEntry::AssertWithIndex { indexes, .. } | IndexEntry::AssertWithoutIndex { indexes, .. } => {
171+
Some(indexes)
172+
},
173+
}
174+
}
175+
}
176+
177+
fn upper_index_expr(expr: &Expr<'_>) -> Option<usize> {
178+
if let ExprKind::Lit(lit) = &expr.kind && let LitKind::Int(index, _) = lit.node {
179+
Some(index as usize)
180+
} else if let Some(higher::Range { end: Some(end), limits, .. }) = higher::Range::hir(expr)
181+
&& let ExprKind::Lit(lit) = &end.kind
182+
&& let LitKind::Int(index @ 1.., _) = lit.node
183+
{
184+
match limits {
185+
RangeLimits::HalfOpen => Some(index as usize - 1),
186+
RangeLimits::Closed => Some(index as usize),
187+
}
188+
} else {
189+
None
190+
}
191+
}
192+
193+
fn check_index(cx: &LateContext<'_>, expr: &Expr<'_>, indexes: &mut FxHashMap<u64, IndexEntry>) {
194+
if let ExprKind::Index(target, index_lit) = expr.kind
195+
&& cx.typeck_results().expr_ty(target).peel_refs().is_slice()
196+
&& let Some(index) = upper_index_expr(index_lit)
197+
{
198+
let hash = hash_expr(cx, target);
199+
let entry = indexes.entry(hash)
200+
.or_insert_with(|| IndexEntry::AssertWithoutIndex {
201+
highest_index: index,
202+
slice_span: target.span,
203+
indexes: Vec::new(),
204+
});
205+
206+
match entry {
207+
IndexEntry::StrayAssert { asserted_len, cmp, assert_span, slice_span: receiver_span } => {
208+
let asserted_len = *asserted_len;
209+
let cmp = *cmp;
210+
let assert_span = *assert_span;
211+
let receiver_span = *receiver_span;
212+
*entry = IndexEntry::AssertWithIndex {
213+
highest_index: index,
214+
asserted_len,
215+
indexes: vec![expr.span],
216+
assert_span,
217+
cmp,
218+
slice_span: receiver_span
219+
};
220+
}
221+
IndexEntry::AssertWithoutIndex { highest_index, indexes, .. }
222+
| IndexEntry::AssertWithIndex { highest_index, indexes, .. } => {
223+
indexes.push(expr.span);
224+
*highest_index = (*highest_index).max(index);
225+
}
226+
}
227+
}
228+
}
229+
230+
fn check_assert(cx: &LateContext<'_>, expr: &Expr<'_>, indexes: &mut FxHashMap<u64, IndexEntry>) {
231+
if let Some((cmp, asserted_len, slice)) = assert_len_expr(cx, expr) {
232+
let hash = hash_expr(cx, slice);
233+
match indexes.entry(hash) {
234+
Entry::Vacant(entry) => {
235+
entry.insert(IndexEntry::StrayAssert {
236+
asserted_len,
237+
cmp,
238+
assert_span: expr.span,
239+
slice_span: slice.span,
240+
});
241+
},
242+
Entry::Occupied(mut entry) => {
243+
if let IndexEntry::AssertWithoutIndex {
244+
highest_index, indexes, ..
245+
} = entry.get_mut()
246+
{
247+
let indexes = mem::take(indexes);
248+
let highest_index = *highest_index;
249+
250+
*entry.get_mut() = IndexEntry::AssertWithIndex {
251+
highest_index,
252+
asserted_len,
253+
indexes,
254+
cmp,
255+
assert_span: expr.span,
256+
slice_span: slice.span,
257+
};
258+
}
259+
},
260+
}
261+
}
262+
}
263+
264+
fn report_indexes(cx: &LateContext<'_>, indexes: &mut FxHashMap<u64, IndexEntry>) {
265+
for entry in indexes.values() {
266+
let Some(full_span) = entry
267+
.spans()
268+
.and_then(|spans| spans.first().zip(spans.last()))
269+
.map(|(low, &high)| low.to(high)) else {
270+
continue;
271+
};
272+
273+
match entry {
274+
IndexEntry::AssertWithIndex {
275+
highest_index,
276+
asserted_len,
277+
indexes,
278+
cmp,
279+
assert_span,
280+
slice_span: receiver_span,
281+
} if indexes.len() > 1 => {
282+
// if we have found an `assert!`, let's also check that it's actually right
283+
// and convers the highest index and if not, suggest the correct length
284+
let sugg = match cmp {
285+
// `v.len() < 5` and `v.len() <= 5` does nothing in terms of bounds checks.
286+
// The user probably meant `v.len() > 5`
287+
LengthComparison::LengthLessThanInt | LengthComparison::LengthLessThanOrEqualInt => Some(format!(
288+
"assert!({}.len() > {highest_index})",
289+
snippet(cx, *receiver_span, "..")
290+
)),
291+
// `5 < v.len()` == `v.len() > 5`
292+
LengthComparison::IntLessThanLength if asserted_len < highest_index => Some(format!(
293+
"assert!({}.len() > {highest_index})",
294+
snippet(cx, *receiver_span, "..")
295+
)),
296+
// `5 <= v.len() == `v.len() >= 5`
297+
LengthComparison::IntLessThanOrEqualLength if asserted_len <= highest_index => Some(format!(
298+
"assert!({}.len() > {highest_index})",
299+
snippet(cx, *receiver_span, "..")
300+
)),
301+
_ => None,
302+
};
303+
304+
if let Some(sugg) = sugg {
305+
report_lint(
306+
cx,
307+
full_span,
308+
"indexing into a slice multiple times with an `assert` that does not cover the highest index",
309+
indexes,
310+
|diag| {
311+
diag.span_suggestion(
312+
*assert_span,
313+
"provide the highest index that is indexed with",
314+
sugg,
315+
Applicability::MachineApplicable,
316+
);
317+
},
318+
);
319+
}
320+
},
321+
IndexEntry::AssertWithoutIndex {
322+
indexes,
323+
highest_index,
324+
slice_span: receiver_span,
325+
} if indexes.len() > 1 => {
326+
report_lint(
327+
cx,
328+
full_span,
329+
"indexing into a slice multiple times without an `assert`",
330+
indexes,
331+
|diag| {
332+
diag.help(format!(
333+
"consider asserting the length before indexing: `assert!({}.len() > {highest_index});`",
334+
snippet(cx, *receiver_span, "..")
335+
));
336+
},
337+
);
338+
},
339+
_ => {},
340+
}
341+
}
342+
}
343+
344+
impl LateLintPass<'_> for MissingAssertsForIndexing {
345+
fn check_block(&mut self, cx: &LateContext<'_>, block: &Block<'_>) {
346+
let mut indexes = FxHashMap::default();
347+
348+
for_each_expr(block, |expr| {
349+
check_index(cx, expr, &mut indexes);
350+
check_assert(cx, expr, &mut indexes);
351+
ControlFlow::<!, ()>::Continue(())
352+
});
353+
354+
report_indexes(cx, &mut indexes);
355+
}
356+
}

0 commit comments

Comments
 (0)