Skip to content

Commit 8ffa0bf

Browse files
committed
New lint: reversed_empty_ranges
1 parent b20a9cd commit 8ffa0bf

8 files changed

+258
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,7 @@ Released 2018-09-13
15461546
[`result_map_unwrap_or_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#result_map_unwrap_or_else
15471547
[`result_unwrap_used`]: https://rust-lang.github.io/rust-clippy/master/index.html#result_unwrap_used
15481548
[`reverse_range_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#reverse_range_loop
1549+
[`reversed_empty_ranges`]: https://rust-lang.github.io/rust-clippy/master/index.html#reversed_empty_ranges
15491550
[`same_functions_in_if_condition`]: https://rust-lang.github.io/rust-clippy/master/index.html#same_functions_in_if_condition
15501551
[`search_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#search_is_some
15511552
[`serde_api_misuse`]: https://rust-lang.github.io/rust-clippy/master/index.html#serde_api_misuse

clippy_lints/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
770770
&ranges::RANGE_MINUS_ONE,
771771
&ranges::RANGE_PLUS_ONE,
772772
&ranges::RANGE_ZIP_WITH_LEN,
773+
&ranges::REVERSED_EMPTY_RANGES,
773774
&redundant_clone::REDUNDANT_CLONE,
774775
&redundant_field_names::REDUNDANT_FIELD_NAMES,
775776
&redundant_pattern_matching::REDUNDANT_PATTERN_MATCHING,
@@ -1384,6 +1385,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13841385
LintId::of(&question_mark::QUESTION_MARK),
13851386
LintId::of(&ranges::RANGE_MINUS_ONE),
13861387
LintId::of(&ranges::RANGE_ZIP_WITH_LEN),
1388+
LintId::of(&ranges::REVERSED_EMPTY_RANGES),
13871389
LintId::of(&redundant_clone::REDUNDANT_CLONE),
13881390
LintId::of(&redundant_field_names::REDUNDANT_FIELD_NAMES),
13891391
LintId::of(&redundant_pattern_matching::REDUNDANT_PATTERN_MATCHING),
@@ -1675,6 +1677,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16751677
LintId::of(&open_options::NONSENSICAL_OPEN_OPTIONS),
16761678
LintId::of(&option_env_unwrap::OPTION_ENV_UNWRAP),
16771679
LintId::of(&ptr::MUT_FROM_REF),
1680+
LintId::of(&ranges::REVERSED_EMPTY_RANGES),
16781681
LintId::of(&regex::INVALID_REGEX),
16791682
LintId::of(&serde_api::SERDE_API_MISUSE),
16801683
LintId::of(&suspicious_trait_impl::SUSPICIOUS_ARITHMETIC_IMPL),

clippy_lints/src/ranges.rs

+110-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
use crate::consts::{constant, Constant};
12
use if_chain::if_chain;
23
use rustc_ast::ast::RangeLimits;
34
use rustc_errors::Applicability;
45
use rustc_hir::{BinOpKind, Expr, ExprKind, QPath};
56
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_middle::ty;
68
use rustc_session::{declare_lint_pass, declare_tool_lint};
79
use rustc_span::source_map::Spanned;
10+
use std::cmp::Ordering;
811

912
use crate::utils::sugg::Sugg;
13+
use crate::utils::{get_parent_expr, is_integer_const, snippet, snippet_opt, span_lint, span_lint_and_then};
1014
use crate::utils::{higher, SpanlessEq};
11-
use crate::utils::{is_integer_const, snippet, snippet_opt, span_lint, span_lint_and_then};
1215

1316
declare_clippy_lint! {
1417
/// **What it does:** Checks for zipping a collection with the range of
@@ -84,10 +87,44 @@ declare_clippy_lint! {
8487
"`x..=(y-1)` reads better as `x..y`"
8588
}
8689

90+
declare_clippy_lint! {
91+
/// **What it does:** Checks for range expressions `x..y` where both `x` and `y`
92+
/// are constant and `x` is greater or equal to `y`.
93+
///
94+
/// **Why is this bad?** Empty ranges yield no values so iterating them is a no-op.
95+
/// Moreover, trying to use a reversed range to index a slice will panic at run-time.
96+
///
97+
/// **Known problems:** None.
98+
///
99+
/// **Example:**
100+
///
101+
/// ```rust
102+
/// fn main() {
103+
/// (10..=0).for_each(|x| println!("{}", x));
104+
///
105+
/// let arr = [1, 2, 3, 4, 5];
106+
/// let sub = &arr[3..1];
107+
/// }
108+
/// ```
109+
/// Use instead:
110+
/// ```rust
111+
/// fn main() {
112+
/// (0..=10).rev().for_each(|x| println!("{}", x));
113+
///
114+
/// let arr = [1, 2, 3, 4, 5];
115+
/// let sub = &arr[1..3];
116+
/// }
117+
/// ```
118+
pub REVERSED_EMPTY_RANGES,
119+
correctness,
120+
"reversing the limits of range expressions, resulting in empty ranges"
121+
}
122+
87123
declare_lint_pass!(Ranges => [
88124
RANGE_ZIP_WITH_LEN,
89125
RANGE_PLUS_ONE,
90-
RANGE_MINUS_ONE
126+
RANGE_MINUS_ONE,
127+
REVERSED_EMPTY_RANGES,
91128
]);
92129

93130
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for Ranges {
@@ -124,6 +161,7 @@ impl<'a, 'tcx> LateLintPass<'a, 'tcx> for Ranges {
124161

125162
check_exclusive_range_plus_one(cx, expr);
126163
check_inclusive_range_minus_one(cx, expr);
164+
check_reversed_empty_range(cx, expr);
127165
}
128166
}
129167

@@ -202,6 +240,76 @@ fn check_inclusive_range_minus_one(cx: &LateContext<'_, '_>, expr: &Expr<'_>) {
202240
}
203241
}
204242

243+
fn check_reversed_empty_range(cx: &LateContext<'_, '_>, expr: &Expr<'_>) {
244+
fn inside_indexing_expr(cx: &LateContext<'_, '_>, expr: &Expr<'_>) -> bool {
245+
matches!(
246+
get_parent_expr(cx, expr),
247+
Some(Expr {
248+
kind: ExprKind::Index(..),
249+
..
250+
})
251+
)
252+
}
253+
254+
fn is_empty_range(limits: RangeLimits, ordering: Ordering) -> bool {
255+
match limits {
256+
RangeLimits::HalfOpen => ordering != Ordering::Less,
257+
RangeLimits::Closed => ordering == Ordering::Greater,
258+
}
259+
}
260+
261+
if_chain! {
262+
if let Some(higher::Range { start: Some(start), end: Some(end), limits }) = higher::range(cx, expr);
263+
let ty = cx.tables.expr_ty(start);
264+
if let ty::Int(_) | ty::Uint(_) = ty.kind;
265+
if let Some((start_idx, _)) = constant(cx, cx.tables, start);
266+
if let Some((end_idx, _)) = constant(cx, cx.tables, end);
267+
if let Some(ordering) = Constant::partial_cmp(cx.tcx, ty, &start_idx, &end_idx);
268+
if is_empty_range(limits, ordering);
269+
then {
270+
if inside_indexing_expr(cx, expr) {
271+
let (reason, outcome) = if ordering == Ordering::Equal {
272+
("empty", "always yield an empty slice")
273+
} else {
274+
("reversed", "panic at run-time")
275+
};
276+
277+
span_lint(
278+
cx,
279+
REVERSED_EMPTY_RANGES,
280+
expr.span,
281+
&format!("this range is {} and using it to index a slice will {}", reason, outcome),
282+
);
283+
} else {
284+
span_lint_and_then(
285+
cx,
286+
REVERSED_EMPTY_RANGES,
287+
expr.span,
288+
"this range is empty so it will yield no values",
289+
|diag| {
290+
if ordering != Ordering::Equal {
291+
let start_snippet = snippet(cx, start.span, "_");
292+
let end_snippet = snippet(cx, end.span, "_");
293+
let dots = match limits {
294+
RangeLimits::HalfOpen => "..",
295+
RangeLimits::Closed => "..="
296+
};
297+
298+
diag.span_suggestion(
299+
expr.span,
300+
"consider using the following if you are attempting to iterate over this \
301+
range in reverse",
302+
format!("({}{}{}).rev()", end_snippet, dots, start_snippet),
303+
Applicability::MaybeIncorrect,
304+
);
305+
}
306+
},
307+
);
308+
}
309+
}
310+
}
311+
}
312+
205313
fn y_plus_one<'t>(cx: &LateContext<'_, '_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> {
206314
match expr.kind {
207315
ExprKind::Binary(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// run-rustfix
2+
#![warn(clippy::reversed_empty_ranges)]
3+
4+
const ANSWER: i32 = 42;
5+
6+
fn main() {
7+
(21..=42).rev().for_each(|x| println!("{}", x));
8+
let _ = (21..ANSWER).rev().filter(|x| x % 2 == 0).take(10).collect::<Vec<_>>();
9+
10+
for _ in (-42..=-21).rev() {}
11+
for _ in (21u32..42u32).rev() {}
12+
13+
// These should be ignored as they are not empty ranges:
14+
15+
(21..=42).for_each(|x| println!("{}", x));
16+
(21..42).for_each(|x| println!("{}", x));
17+
18+
let arr = [1, 2, 3, 4, 5];
19+
let _ = &arr[1..=3];
20+
let _ = &arr[1..3];
21+
22+
for _ in 21..=42 {}
23+
for _ in 21..42 {}
24+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// run-rustfix
2+
#![warn(clippy::reversed_empty_ranges)]
3+
4+
const ANSWER: i32 = 42;
5+
6+
fn main() {
7+
(42..=21).for_each(|x| println!("{}", x));
8+
let _ = (ANSWER..21).filter(|x| x % 2 == 0).take(10).collect::<Vec<_>>();
9+
10+
for _ in -21..=-42 {}
11+
for _ in 42u32..21u32 {}
12+
13+
// These should be ignored as they are not empty ranges:
14+
15+
(21..=42).for_each(|x| println!("{}", x));
16+
(21..42).for_each(|x| println!("{}", x));
17+
18+
let arr = [1, 2, 3, 4, 5];
19+
let _ = &arr[1..=3];
20+
let _ = &arr[1..3];
21+
22+
for _ in 21..=42 {}
23+
for _ in 21..42 {}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
error: this range is empty so it will yield no values
2+
--> $DIR/reversed_empty_ranges_fixable.rs:7:5
3+
|
4+
LL | (42..=21).for_each(|x| println!("{}", x));
5+
| ^^^^^^^^^
6+
|
7+
= note: `-D clippy::reversed-empty-ranges` implied by `-D warnings`
8+
help: consider using the following if you are attempting to iterate over this range in reverse
9+
|
10+
LL | (21..=42).rev().for_each(|x| println!("{}", x));
11+
| ^^^^^^^^^^^^^^^
12+
13+
error: this range is empty so it will yield no values
14+
--> $DIR/reversed_empty_ranges_fixable.rs:8:13
15+
|
16+
LL | let _ = (ANSWER..21).filter(|x| x % 2 == 0).take(10).collect::<Vec<_>>();
17+
| ^^^^^^^^^^^^
18+
|
19+
help: consider using the following if you are attempting to iterate over this range in reverse
20+
|
21+
LL | let _ = (21..ANSWER).rev().filter(|x| x % 2 == 0).take(10).collect::<Vec<_>>();
22+
| ^^^^^^^^^^^^^^^^^^
23+
24+
error: this range is empty so it will yield no values
25+
--> $DIR/reversed_empty_ranges_fixable.rs:10:14
26+
|
27+
LL | for _ in -21..=-42 {}
28+
| ^^^^^^^^^
29+
|
30+
help: consider using the following if you are attempting to iterate over this range in reverse
31+
|
32+
LL | for _ in (-42..=-21).rev() {}
33+
| ^^^^^^^^^^^^^^^^^
34+
35+
error: this range is empty so it will yield no values
36+
--> $DIR/reversed_empty_ranges_fixable.rs:11:14
37+
|
38+
LL | for _ in 42u32..21u32 {}
39+
| ^^^^^^^^^^^^
40+
|
41+
help: consider using the following if you are attempting to iterate over this range in reverse
42+
|
43+
LL | for _ in (21u32..42u32).rev() {}
44+
| ^^^^^^^^^^^^^^^^^^^^
45+
46+
error: aborting due to 4 previous errors
47+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#![warn(clippy::reversed_empty_ranges)]
2+
3+
const ANSWER: i32 = 42;
4+
const SOME_NUM: usize = 3;
5+
6+
fn main() {
7+
let _ = (42 + 10..42 + 10).map(|x| x / 2).find(|&x| x == 21);
8+
9+
let arr = [1, 2, 3, 4, 5];
10+
let _ = &arr[3usize..=1usize];
11+
let _ = &arr[SOME_NUM..1];
12+
let _ = &arr[3..3];
13+
14+
for _ in ANSWER..ANSWER {}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
error: this range is empty so it will yield no values
2+
--> $DIR/reversed_empty_ranges_unfixable.rs:7:13
3+
|
4+
LL | let _ = (42 + 10..42 + 10).map(|x| x / 2).find(|&x| x == 21);
5+
| ^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: `-D clippy::reversed-empty-ranges` implied by `-D warnings`
8+
9+
error: this range is reversed and using it to index a slice will panic at run-time
10+
--> $DIR/reversed_empty_ranges_unfixable.rs:10:18
11+
|
12+
LL | let _ = &arr[3usize..=1usize];
13+
| ^^^^^^^^^^^^^^^
14+
15+
error: this range is reversed and using it to index a slice will panic at run-time
16+
--> $DIR/reversed_empty_ranges_unfixable.rs:11:18
17+
|
18+
LL | let _ = &arr[SOME_NUM..1];
19+
| ^^^^^^^^^^^
20+
21+
error: this range is empty and using it to index a slice will always yield an empty slice
22+
--> $DIR/reversed_empty_ranges_unfixable.rs:12:18
23+
|
24+
LL | let _ = &arr[3..3];
25+
| ^^^^
26+
27+
error: this range is empty so it will yield no values
28+
--> $DIR/reversed_empty_ranges_unfixable.rs:14:14
29+
|
30+
LL | for _ in ANSWER..ANSWER {}
31+
| ^^^^^^^^^^^^^^
32+
33+
error: aborting due to 5 previous errors
34+

0 commit comments

Comments
 (0)