Skip to content

Commit 776c868

Browse files
committed
New index_refutable_slice lint
* Finding pattern slices for `avoidable_slice_indexing` * `avoidable_slice_indexing` analysing slice usage * Add configuration to `avoidable_slice_indexing` * Emitting `avoidable_slice_indexing` with suggestions * Dogfooding and fixing bugs * Add ui-toml test for `avoidable_slice_indexing` * Correctly suggest `ref` keywords for `avoidable_slice_indexing` * Test and document `mut` for `avoid_slice_indexing` * Handle macros with `avoidable_slice_indexing` lint * Ignore slices with sub patterns in `avoidable_slice_indexing` * Update lint description for `avoidable_slice_indexing` * Move `avoidable_slice_indexing` to nursery * Added more tests for `avoidable_slice_indexing` * Update documentation and message for `avoidable_slice_indexing` * Teach `avoidable_slice_indexing` about `HirId`s and `Visitors` * Rename lint to `index_refutable_slice` and connected config
1 parent 4d26c5c commit 776c868

File tree

17 files changed

+731
-5
lines changed

17 files changed

+731
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2904,6 +2904,7 @@ Released 2018-09-13
29042904
[`imprecise_flops`]: https://rust-lang.github.io/rust-clippy/master/index.html#imprecise_flops
29052905
[`inconsistent_digit_grouping`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_digit_grouping
29062906
[`inconsistent_struct_constructor`]: https://rust-lang.github.io/rust-clippy/master/index.html#inconsistent_struct_constructor
2907+
[`index_refutable_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#index_refutable_slice
29072908
[`indexing_slicing`]: https://rust-lang.github.io/rust-clippy/master/index.html#indexing_slicing
29082909
[`ineffective_bit_mask`]: https://rust-lang.github.io/rust-clippy/master/index.html#ineffective_bit_mask
29092910
[`inefficient_to_string`]: https://rust-lang.github.io/rust-clippy/master/index.html#inefficient_to_string
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
use clippy_utils::consts::{constant, Constant};
2+
use clippy_utils::diagnostics::span_lint_and_then;
3+
use clippy_utils::higher::IfLet;
4+
use clippy_utils::ty::implements_trait;
5+
use clippy_utils::{in_macro, is_expn_of, is_lint_allowed, meets_msrv, msrvs, path_to_local};
6+
use if_chain::if_chain;
7+
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
8+
use rustc_errors::Applicability;
9+
use rustc_hir as hir;
10+
use rustc_hir::intravisit::{self, NestedVisitorMap, Visitor};
11+
use rustc_lint::{LateContext, LateLintPass, LintContext};
12+
use rustc_middle::hir::map::Map;
13+
use rustc_middle::ty;
14+
use rustc_semver::RustcVersion;
15+
use rustc_session::{declare_tool_lint, impl_lint_pass};
16+
use rustc_span::{symbol::Ident, Span};
17+
use std::convert::TryInto;
18+
19+
declare_clippy_lint! {
20+
/// ### What it does
21+
/// The lint checks for slice bindings in patterns that are only used to
22+
/// access individual slice values.
23+
///
24+
/// ### Why is this bad?
25+
/// Accessing slice values using indices can lead to panics. Using refutable
26+
/// patterns can avoid these. Binding to individual values also improves the
27+
/// readability as they can be named.
28+
///
29+
/// ### Limitations
30+
/// This lint currently only checks for immutable access inside `if let`
31+
/// patterns.
32+
///
33+
/// ### Example
34+
/// ```rust
35+
/// let slice: Option<&[u32]> = Some(&[1, 2, 3]);
36+
///
37+
/// if let Some(slice) = slice {
38+
/// println!("{}", slice[0]);
39+
/// }
40+
/// ```
41+
/// Use instead:
42+
/// ```rust
43+
/// let slice: Option<&[u32]> = Some(&[1, 2, 3]);
44+
///
45+
/// if let Some(&[first, ..]) = slice {
46+
/// println!("{}", first);
47+
/// }
48+
/// ```
49+
pub INDEX_REFUTABLE_SLICE,
50+
nursery,
51+
"avoid indexing on slices which could be destructed"
52+
}
53+
54+
#[derive(Copy, Clone)]
55+
pub struct IndexRefutableSlice {
56+
indexing_limit: u64,
57+
msrv: Option<RustcVersion>,
58+
}
59+
60+
impl IndexRefutableSlice {
61+
pub fn new(max_suggested_slice_pattern_length: u64, msrv: Option<RustcVersion>) -> Self {
62+
Self {
63+
indexing_limit: max_suggested_slice_pattern_length - 1,
64+
msrv,
65+
}
66+
}
67+
}
68+
69+
impl_lint_pass!(IndexRefutableSlice => [INDEX_REFUTABLE_SLICE]);
70+
71+
impl LateLintPass<'_> for IndexRefutableSlice {
72+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
73+
if_chain! {
74+
if !in_macro(expr.span) || is_expn_of(expr.span, "if_chain").is_some();
75+
if let Some(IfLet {let_pat, if_then, ..}) = IfLet::hir(cx, expr);
76+
if !is_lint_allowed(cx, INDEX_REFUTABLE_SLICE, expr.hir_id);
77+
if meets_msrv(self.msrv.as_ref(), &msrvs::SLICE_PATTERNS);
78+
79+
let found_slices = find_slice_values(cx, let_pat);
80+
if !found_slices.is_empty();
81+
let filtered_slices = filter_lintable_slices(cx, found_slices, self.indexing_limit, if_then);
82+
if !filtered_slices.is_empty();
83+
then {
84+
for slice in filtered_slices.values() {
85+
lint_slice(cx, slice);
86+
}
87+
}
88+
}
89+
}
90+
91+
extract_msrv_attr!(LateContext);
92+
}
93+
94+
fn find_slice_values(cx: &LateContext<'_>, pat: &hir::Pat<'_>) -> FxHashMap<hir::HirId, SliceLintInformation> {
95+
let mut removed_pat: FxHashSet<hir::HirId> = FxHashSet::default();
96+
let mut slices: FxHashMap<hir::HirId, SliceLintInformation> = FxHashMap::default();
97+
pat.walk_always(|pat| {
98+
if let hir::PatKind::Binding(binding, value_hir_id, ident, sub_pat) = pat.kind {
99+
// We'll just ignore mut and ref mut for simplicity sake right now
100+
if let hir::BindingAnnotation::Mutable | hir::BindingAnnotation::RefMut = binding {
101+
return;
102+
}
103+
104+
// This block catches bindings with sub patterns. It would be hard to build a correct suggestion
105+
// for them and it's likely that the user knows what they are doing in such a case.
106+
if removed_pat.contains(&value_hir_id) {
107+
return;
108+
}
109+
if sub_pat.is_some() {
110+
removed_pat.insert(value_hir_id);
111+
slices.remove(&value_hir_id);
112+
return;
113+
}
114+
115+
let bound_ty = cx.typeck_results().node_type(pat.hir_id);
116+
if let ty::Slice(inner_ty) | ty::Array(inner_ty, _) = bound_ty.peel_refs().kind() {
117+
// The values need to use the `ref` keyword if they can't be copied.
118+
// This will need to be adjusted if the lint want to support multable access in the future
119+
let src_is_ref = bound_ty.is_ref() && binding != hir::BindingAnnotation::Ref;
120+
let is_copy = cx
121+
.tcx
122+
.lang_items()
123+
.copy_trait()
124+
.map_or(false, |trait_id| implements_trait(cx, inner_ty, trait_id, &[]));
125+
let needs_ref = !(src_is_ref || is_copy);
126+
127+
let slice_info = slices
128+
.entry(value_hir_id)
129+
.or_insert_with(|| SliceLintInformation::new(ident, needs_ref));
130+
slice_info.pattern_spans.push(pat.span);
131+
}
132+
}
133+
});
134+
135+
slices
136+
}
137+
138+
fn lint_slice(cx: &LateContext<'_>, slice: &SliceLintInformation) {
139+
let used_indices = slice
140+
.index_use
141+
.iter()
142+
.map(|(index, _)| *index)
143+
.collect::<FxHashSet<_>>();
144+
145+
let value_name = |index| format!("{}_{}", slice.ident.name, index);
146+
147+
if let Some(max_index) = used_indices.iter().max() {
148+
let opt_ref = if slice.needs_ref { "ref " } else { "" };
149+
let pat_sugg_idents = (0..=*max_index)
150+
.map(|index| {
151+
if used_indices.contains(&index) {
152+
format!("{}{}", opt_ref, value_name(index))
153+
} else {
154+
"_".to_string()
155+
}
156+
})
157+
.collect::<Vec<_>>();
158+
let pat_sugg = format!("[{}, ..]", pat_sugg_idents.join(", "));
159+
160+
span_lint_and_then(
161+
cx,
162+
INDEX_REFUTABLE_SLICE,
163+
slice.ident.span,
164+
"this binding can be a slice pattern to avoid indexing",
165+
|diag| {
166+
diag.multipart_suggestion(
167+
"try using a slice pattern here",
168+
slice
169+
.pattern_spans
170+
.iter()
171+
.map(|span| (*span, pat_sugg.clone()))
172+
.collect(),
173+
Applicability::MaybeIncorrect,
174+
);
175+
176+
diag.multipart_suggestion(
177+
"and replace the index expressions here",
178+
slice
179+
.index_use
180+
.iter()
181+
.map(|(index, span)| (*span, value_name(*index)))
182+
.collect(),
183+
Applicability::MaybeIncorrect,
184+
);
185+
186+
// I thought about adding a note to the lint message to inform the user that these
187+
// refactorings will remove the index expression. However, I decided against this,
188+
// as `filter_lintable_slices` will only return slices where all access indices are
189+
// known at compile time. The removal should therefore not have any side effects.
190+
},
191+
);
192+
}
193+
}
194+
195+
#[derive(Debug)]
196+
struct SliceLintInformation {
197+
ident: Ident,
198+
needs_ref: bool,
199+
pattern_spans: Vec<Span>,
200+
index_use: Vec<(u64, Span)>,
201+
}
202+
203+
impl SliceLintInformation {
204+
fn new(ident: Ident, needs_ref: bool) -> Self {
205+
Self {
206+
ident,
207+
needs_ref,
208+
pattern_spans: Vec::new(),
209+
index_use: Vec::new(),
210+
}
211+
}
212+
}
213+
214+
fn filter_lintable_slices<'a, 'tcx>(
215+
cx: &'a LateContext<'tcx>,
216+
slice_lint_info: FxHashMap<hir::HirId, SliceLintInformation>,
217+
index_limit: u64,
218+
scope: &'tcx hir::Expr<'tcx>,
219+
) -> FxHashMap<hir::HirId, SliceLintInformation> {
220+
let mut visitor = SliceIndexLintingVisitor {
221+
cx,
222+
slice_lint_info,
223+
index_limit,
224+
};
225+
226+
intravisit::walk_expr(&mut visitor, scope);
227+
228+
visitor
229+
.slice_lint_info
230+
.retain(|_key, value| !value.index_use.is_empty());
231+
visitor.slice_lint_info
232+
}
233+
234+
struct SliceIndexLintingVisitor<'a, 'tcx> {
235+
cx: &'a LateContext<'tcx>,
236+
slice_lint_info: FxHashMap<hir::HirId, SliceLintInformation>,
237+
index_limit: u64,
238+
}
239+
240+
impl<'a, 'tcx> Visitor<'tcx> for SliceIndexLintingVisitor<'a, 'tcx> {
241+
type Map = Map<'tcx>;
242+
243+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
244+
NestedVisitorMap::OnlyBodies(self.cx.tcx.hir())
245+
}
246+
247+
fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) {
248+
if let Some(local_id) = path_to_local(expr) {
249+
let Self {
250+
cx,
251+
ref mut slice_lint_info,
252+
index_limit,
253+
} = *self;
254+
255+
if_chain! {
256+
// Check if this is even a local we're interested in
257+
if let Some(use_info) = slice_lint_info.get_mut(&local_id);
258+
259+
let map = cx.tcx.hir();
260+
261+
// Checking for slice indexing
262+
let parent_id = map.get_parent_node(expr.hir_id);
263+
if let Some(hir::Node::Expr(parent_expr)) = map.find(parent_id);
264+
if let hir::ExprKind::Index(_, index_expr) = parent_expr.kind;
265+
if let Some((Constant::Int(index_value), _)) = constant(cx, cx.typeck_results(), index_expr);
266+
if let Ok(index_value) = index_value.try_into();
267+
if index_value <= index_limit;
268+
269+
// Make sure that this slice index is read only
270+
let maybe_addrof_id = map.get_parent_node(parent_id);
271+
if let Some(hir::Node::Expr(maybe_addrof_expr)) = map.find(maybe_addrof_id);
272+
if let hir::ExprKind::AddrOf(_kind, hir::Mutability::Not, _inner_expr) = maybe_addrof_expr.kind;
273+
then {
274+
use_info.index_use.push((index_value, map.span(parent_expr.hir_id)));
275+
return;
276+
}
277+
}
278+
279+
// The slice was used for something other than indexing
280+
self.slice_lint_info.remove(&local_id);
281+
}
282+
intravisit::walk_expr(self, expr);
283+
}
284+
}

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ store.register_lints(&[
164164
implicit_return::IMPLICIT_RETURN,
165165
implicit_saturating_sub::IMPLICIT_SATURATING_SUB,
166166
inconsistent_struct_constructor::INCONSISTENT_STRUCT_CONSTRUCTOR,
167+
index_refutable_slice::INDEX_REFUTABLE_SLICE,
167168
indexing_slicing::INDEXING_SLICING,
168169
indexing_slicing::OUT_OF_BOUNDS_INDEXING,
169170
infinite_iter::INFINITE_ITER,

clippy_lints/src/lib.register_nursery.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ store.register_group(true, "clippy::nursery", Some("clippy_nursery"), vec![
1313
LintId::of(floating_point_arithmetic::IMPRECISE_FLOPS),
1414
LintId::of(floating_point_arithmetic::SUBOPTIMAL_FLOPS),
1515
LintId::of(future_not_send::FUTURE_NOT_SEND),
16+
LintId::of(index_refutable_slice::INDEX_REFUTABLE_SLICE),
1617
LintId::of(let_if_seq::USELESS_LET_IF_SEQ),
1718
LintId::of(missing_const_for_fn::MISSING_CONST_FOR_FN),
1819
LintId::of(mutable_debug_assertion::DEBUG_ASSERT_WITH_MUT_CALL),

clippy_lints/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ mod implicit_hasher;
233233
mod implicit_return;
234234
mod implicit_saturating_sub;
235235
mod inconsistent_struct_constructor;
236+
mod index_refutable_slice;
236237
mod indexing_slicing;
237238
mod infinite_iter;
238239
mod inherent_impl;
@@ -560,6 +561,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
560561

561562
store.register_late_pass(|| Box::new(size_of_in_element_count::SizeOfInElementCount));
562563
store.register_late_pass(|| Box::new(same_name_method::SameNameMethod));
564+
let max_suggested_slice_pattern_length = conf.max_suggested_slice_pattern_length;
565+
store.register_late_pass(move || Box::new(index_refutable_slice::IndexRefutableSlice::new(max_suggested_slice_pattern_length, msrv)));
563566
store.register_late_pass(|| Box::new(map_clone::MapClone));
564567
store.register_late_pass(|| Box::new(map_err_ignore::MapErrIgnore));
565568
store.register_late_pass(|| Box::new(shadow::Shadow::default()));

clippy_lints/src/unwrap.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ impl<'a, 'tcx> Visitor<'tcx> for UnwrappableVariablesVisitor<'a, 'tcx> {
229229
} else {
230230
// find `unwrap[_err]()` calls:
231231
if_chain! {
232-
if let ExprKind::MethodCall(method_name, _, args, _) = expr.kind;
233-
if let Some(id) = path_to_local(&args[0]);
232+
if let ExprKind::MethodCall(method_name, _, [self_arg, ..], _) = expr.kind;
233+
if let Some(id) = path_to_local(self_arg);
234234
if [sym::unwrap, sym::expect, sym!(unwrap_err)].contains(&method_name.ident.name);
235235
let call_to_unwrap = [sym::unwrap, sym::expect].contains(&method_name.ident.name);
236236
if let Some(unwrappable) = self.unwrappables.iter()

clippy_lints/src/utils/conf.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ define_Conf! {
148148
///
149149
/// Suppress lints whenever the suggested change would cause breakage for other crates.
150150
(avoid_breaking_exported_api: bool = true),
151-
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT.
151+
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, INDEX_REFUTABLE_SLICE.
152152
///
153153
/// The minimum rust version that the project supports
154154
(msrv: Option<String> = None),
@@ -296,6 +296,12 @@ define_Conf! {
296296
///
297297
/// Whether to apply the raw pointer heuristic to determine if a type is `Send`.
298298
(enable_raw_pointer_heuristic_for_send: bool = true),
299+
/// Lint: INDEX_REFUTABLE_SLICE.
300+
///
301+
/// The limit after which slice indexing will not be linted.
302+
///
303+
/// As an example, the default limit of `3` would allow `slice[3]` but lint `slice[2]`.
304+
(max_suggested_slice_pattern_length: u64 = 3),
299305
}
300306

301307
/// Search for the configuration file.

clippy_utils/src/msrvs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ msrv_aliases! {
1919
1,46,0 { CONST_IF_MATCH }
2020
1,45,0 { STR_STRIP_PREFIX }
2121
1,43,0 { LOG2_10, LOG10_2 }
22-
1,42,0 { MATCHES_MACRO }
22+
1,42,0 { MATCHES_MACRO, SLICE_PATTERNS }
2323
1,41,0 { RE_REBALANCING_COHERENCE, RESULT_MAP_OR_ELSE }
2424
1,40,0 { MEM_TAKE, NON_EXHAUSTIVE, OPTION_AS_DEREF }
2525
1,38,0 { POINTER_CAST }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#![deny(clippy::index_refutable_slice)]
2+
3+
fn below_limit() {
4+
let slice: Option<&[u32]> = Some(&[1, 2, 3]);
5+
if let Some(slice) = slice {
6+
// This would usually not be linted but is included now due to the
7+
// index limit in the config gile
8+
println!("{}", slice[7]);
9+
}
10+
}
11+
12+
fn above_limit() {
13+
let slice: Option<&[u32]> = Some(&[1, 2, 3]);
14+
if let Some(slice) = slice {
15+
// This will not be linted as 8 is above the limit
16+
println!("{}", slice[8]);
17+
}
18+
}
19+
20+
fn main() {
21+
below_limit();
22+
above_limit();
23+
}

0 commit comments

Comments
 (0)