Skip to content

Commit e1842b0

Browse files
committed
Auto merge of rust-lang#5583 - ebroto:reversed_empty_ranges, r=yaahc,flip1995
Reversed empty ranges This lint checks range expressions with inverted limits which result in empty ranges. This includes also the ranges used to index slices. The lint reverse_range_loop was covering iteration of reversed ranges in a for loop, which is a subset of what this new lint covers, so it has been removed. I'm not sure if that's the best choice. It would be doable to check in the new lint that we are not in the arguments of a for loop; I went for removing it because the logic was too similar to keep them separated. changelog: Added reversed_empty_ranges lint that checks for ranges where the limits have been inverted, resulting in empty ranges. Removed reverse_range_loop which was covering a subset of the new lint. Closes rust-lang#4192 Closes rust-lang#96
2 parents 7147068 + 671c1e3 commit e1842b0

21 files changed

+500
-313
lines changed

Diff for: CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1545,7 +1545,7 @@ Released 2018-09-13
15451545
[`result_map_unit_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#result_map_unit_fn
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
1548-
[`reverse_range_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#reverse_range_loop
1548+
[`reversed_empty_ranges`]: https://rust-lang.github.io/rust-clippy/master/index.html#reversed_empty_ranges
15491549
[`same_functions_in_if_condition`]: https://rust-lang.github.io/rust-clippy/master/index.html#same_functions_in_if_condition
15501550
[`search_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#search_is_some
15511551
[`serde_api_misuse`]: https://rust-lang.github.io/rust-clippy/master/index.html#serde_api_misuse

Diff for: clippy_lints/src/lib.rs

+7-3
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
624624
&loops::NEEDLESS_COLLECT,
625625
&loops::NEEDLESS_RANGE_LOOP,
626626
&loops::NEVER_LOOP,
627-
&loops::REVERSE_RANGE_LOOP,
628627
&loops::WHILE_IMMUTABLE_CONDITION,
629628
&loops::WHILE_LET_LOOP,
630629
&loops::WHILE_LET_ON_ITERATOR,
@@ -770,6 +769,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
770769
&ranges::RANGE_MINUS_ONE,
771770
&ranges::RANGE_PLUS_ONE,
772771
&ranges::RANGE_ZIP_WITH_LEN,
772+
&ranges::REVERSED_EMPTY_RANGES,
773773
&redundant_clone::REDUNDANT_CLONE,
774774
&redundant_field_names::REDUNDANT_FIELD_NAMES,
775775
&redundant_pattern_matching::REDUNDANT_PATTERN_MATCHING,
@@ -1283,7 +1283,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
12831283
LintId::of(&loops::NEEDLESS_COLLECT),
12841284
LintId::of(&loops::NEEDLESS_RANGE_LOOP),
12851285
LintId::of(&loops::NEVER_LOOP),
1286-
LintId::of(&loops::REVERSE_RANGE_LOOP),
12871286
LintId::of(&loops::WHILE_IMMUTABLE_CONDITION),
12881287
LintId::of(&loops::WHILE_LET_LOOP),
12891288
LintId::of(&loops::WHILE_LET_ON_ITERATOR),
@@ -1384,6 +1383,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13841383
LintId::of(&question_mark::QUESTION_MARK),
13851384
LintId::of(&ranges::RANGE_MINUS_ONE),
13861385
LintId::of(&ranges::RANGE_ZIP_WITH_LEN),
1386+
LintId::of(&ranges::REVERSED_EMPTY_RANGES),
13871387
LintId::of(&redundant_clone::REDUNDANT_CLONE),
13881388
LintId::of(&redundant_field_names::REDUNDANT_FIELD_NAMES),
13891389
LintId::of(&redundant_pattern_matching::REDUNDANT_PATTERN_MATCHING),
@@ -1656,7 +1656,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16561656
LintId::of(&loops::FOR_LOOP_OVER_RESULT),
16571657
LintId::of(&loops::ITER_NEXT_LOOP),
16581658
LintId::of(&loops::NEVER_LOOP),
1659-
LintId::of(&loops::REVERSE_RANGE_LOOP),
16601659
LintId::of(&loops::WHILE_IMMUTABLE_CONDITION),
16611660
LintId::of(&mem_discriminant::MEM_DISCRIMINANT_NON_ENUM),
16621661
LintId::of(&mem_replace::MEM_REPLACE_WITH_UNINIT),
@@ -1675,6 +1674,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16751674
LintId::of(&open_options::NONSENSICAL_OPEN_OPTIONS),
16761675
LintId::of(&option_env_unwrap::OPTION_ENV_UNWRAP),
16771676
LintId::of(&ptr::MUT_FROM_REF),
1677+
LintId::of(&ranges::REVERSED_EMPTY_RANGES),
16781678
LintId::of(&regex::INVALID_REGEX),
16791679
LintId::of(&serde_api::SERDE_API_MISUSE),
16801680
LintId::of(&suspicious_trait_impl::SUSPICIOUS_ARITHMETIC_IMPL),
@@ -1785,6 +1785,10 @@ fn register_removed_non_tool_lints(store: &mut rustc_lint::LintStore) {
17851785
"unsafe_vector_initialization",
17861786
"the replacement suggested by this lint had substantially different behavior",
17871787
);
1788+
store.register_removed(
1789+
"reverse_range_loop",
1790+
"this lint is now included in reversed_empty_ranges",
1791+
);
17881792
}
17891793

17901794
/// Register renamed lints.

Diff for: clippy_lints/src/loops.rs

+2-100
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::consts::{constant, Constant};
1+
use crate::consts::constant;
22
use crate::reexport::Name;
33
use crate::utils::paths;
44
use crate::utils::usage::{is_unused, mutated_variables};
@@ -8,7 +8,7 @@ use crate::utils::{
88
multispan_sugg, snippet, snippet_opt, snippet_with_applicability, span_lint, span_lint_and_help,
99
span_lint_and_sugg, span_lint_and_then, SpanlessEq,
1010
};
11-
use crate::utils::{is_type_diagnostic_item, qpath_res, same_tys, sext, sugg};
11+
use crate::utils::{is_type_diagnostic_item, qpath_res, same_tys, sugg};
1212
use if_chain::if_chain;
1313
use rustc_ast::ast;
1414
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
@@ -270,30 +270,6 @@ declare_clippy_lint! {
270270
"collecting an iterator when collect is not needed"
271271
}
272272

273-
declare_clippy_lint! {
274-
/// **What it does:** Checks for loops over ranges `x..y` where both `x` and `y`
275-
/// are constant and `x` is greater or equal to `y`, unless the range is
276-
/// reversed or has a negative `.step_by(_)`.
277-
///
278-
/// **Why is it bad?** Such loops will either be skipped or loop until
279-
/// wrap-around (in debug code, this may `panic!()`). Both options are probably
280-
/// not intended.
281-
///
282-
/// **Known problems:** The lint cannot catch loops over dynamically defined
283-
/// ranges. Doing this would require simulating all possible inputs and code
284-
/// paths through the program, which would be complex and error-prone.
285-
///
286-
/// **Example:**
287-
/// ```ignore
288-
/// for x in 5..10 - 5 {
289-
/// ..
290-
/// } // oops, stray `-`
291-
/// ```
292-
pub REVERSE_RANGE_LOOP,
293-
correctness,
294-
"iteration over an empty range, such as `10..0` or `5..5`"
295-
}
296-
297273
declare_clippy_lint! {
298274
/// **What it does:** Checks `for` loops over slices with an explicit counter
299275
/// and suggests the use of `.enumerate()`.
@@ -463,7 +439,6 @@ declare_lint_pass!(Loops => [
463439
FOR_LOOP_OVER_OPTION,
464440
WHILE_LET_LOOP,
465441
NEEDLESS_COLLECT,
466-
REVERSE_RANGE_LOOP,
467442
EXPLICIT_COUNTER_LOOP,
468443
EMPTY_LOOP,
469444
WHILE_LET_ON_ITERATOR,
@@ -761,7 +736,6 @@ fn check_for_loop<'a, 'tcx>(
761736
expr: &'tcx Expr<'_>,
762737
) {
763738
check_for_loop_range(cx, pat, arg, body, expr);
764-
check_for_loop_reverse_range(cx, arg, expr);
765739
check_for_loop_arg(cx, pat, arg, expr);
766740
check_for_loop_explicit_counter(cx, pat, arg, body, expr);
767741
check_for_loop_over_map_kv(cx, pat, arg, body, expr);
@@ -1248,78 +1222,6 @@ fn is_end_eq_array_len<'tcx>(
12481222
false
12491223
}
12501224

1251-
fn check_for_loop_reverse_range<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, arg: &'tcx Expr<'_>, expr: &'tcx Expr<'_>) {
1252-
// if this for loop is iterating over a two-sided range...
1253-
if let Some(higher::Range {
1254-
start: Some(start),
1255-
end: Some(end),
1256-
limits,
1257-
}) = higher::range(cx, arg)
1258-
{
1259-
// ...and both sides are compile-time constant integers...
1260-
if let Some((start_idx, _)) = constant(cx, cx.tables, start) {
1261-
if let Some((end_idx, _)) = constant(cx, cx.tables, end) {
1262-
// ...and the start index is greater than the end index,
1263-
// this loop will never run. This is often confusing for developers
1264-
// who think that this will iterate from the larger value to the
1265-
// smaller value.
1266-
let ty = cx.tables.expr_ty(start);
1267-
let (sup, eq) = match (start_idx, end_idx) {
1268-
(Constant::Int(start_idx), Constant::Int(end_idx)) => (
1269-
match ty.kind {
1270-
ty::Int(ity) => sext(cx.tcx, start_idx, ity) > sext(cx.tcx, end_idx, ity),
1271-
ty::Uint(_) => start_idx > end_idx,
1272-
_ => false,
1273-
},
1274-
start_idx == end_idx,
1275-
),
1276-
_ => (false, false),
1277-
};
1278-
1279-
if sup {
1280-
let start_snippet = snippet(cx, start.span, "_");
1281-
let end_snippet = snippet(cx, end.span, "_");
1282-
let dots = if limits == ast::RangeLimits::Closed {
1283-
"..="
1284-
} else {
1285-
".."
1286-
};
1287-
1288-
span_lint_and_then(
1289-
cx,
1290-
REVERSE_RANGE_LOOP,
1291-
expr.span,
1292-
"this range is empty so this for loop will never run",
1293-
|diag| {
1294-
diag.span_suggestion(
1295-
arg.span,
1296-
"consider using the following if you are attempting to iterate over this \
1297-
range in reverse",
1298-
format!(
1299-
"({end}{dots}{start}).rev()",
1300-
end = end_snippet,
1301-
dots = dots,
1302-
start = start_snippet
1303-
),
1304-
Applicability::MaybeIncorrect,
1305-
);
1306-
},
1307-
);
1308-
} else if eq && limits != ast::RangeLimits::Closed {
1309-
// if they are equal, it's also problematic - this loop
1310-
// will never run.
1311-
span_lint(
1312-
cx,
1313-
REVERSE_RANGE_LOOP,
1314-
expr.span,
1315-
"this range is empty so this for loop will never run",
1316-
);
1317-
}
1318-
}
1319-
}
1320-
}
1321-
}
1322-
13231225
fn lint_iter_method(cx: &LateContext<'_, '_>, args: &[Expr<'_>], arg: &Expr<'_>, method_name: &str) {
13241226
let mut applicability = Applicability::MachineApplicable;
13251227
let object = snippet_with_applicability(cx, args[0].span, "_", &mut applicability);

Diff for: 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,no_run
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(

Diff for: src/lintlist/mod.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1922,11 +1922,11 @@ pub static ref ALL_LINTS: Vec<Lint> = vec![
19221922
module: "methods",
19231923
},
19241924
Lint {
1925-
name: "reverse_range_loop",
1925+
name: "reversed_empty_ranges",
19261926
group: "correctness",
1927-
desc: "iteration over an empty range, such as `10..0` or `5..5`",
1927+
desc: "reversing the limits of range expressions, resulting in empty ranges",
19281928
deprecation: None,
1929-
module: "loops",
1929+
module: "ranges",
19301930
},
19311931
Lint {
19321932
name: "same_functions_in_if_condition",

0 commit comments

Comments
 (0)