1 use clippy_utils::consts::{constant, Constant};
2 use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then};
3 use clippy_utils::source::{snippet, snippet_opt, snippet_with_applicability};
4 use clippy_utils::sugg::Sugg;
5 use clippy_utils::{get_parent_expr, in_constant, is_integer_const, meets_msrv, msrvs, path_to_local};
6 use clippy_utils::{higher, SpanlessEq};
7 use if_chain::if_chain;
8 use rustc_ast::ast::RangeLimits;
9 use rustc_errors::Applicability;
10 use rustc_hir::{BinOpKind, Expr, ExprKind, HirId, PathSegment, QPath};
11 use rustc_lint::{LateContext, LateLintPass};
13 use rustc_semver::RustcVersion;
14 use rustc_session::{declare_tool_lint, impl_lint_pass};
15 use rustc_span::source_map::{Span, Spanned};
17 use std::cmp::Ordering;
19 declare_clippy_lint! {
21 /// Checks for zipping a collection with the range of
24 /// ### Why is this bad?
25 /// The code is better expressed with `.enumerate()`.
29 /// # let x = vec![1];
31 /// x.iter().zip(0..x.len());
36 /// # let x = vec![1];
38 /// x.iter().enumerate();
40 #[clippy::version = "pre 1.29.0"]
41 pub RANGE_ZIP_WITH_LEN,
43 "zipping iterator with a range when `enumerate()` would do"
46 declare_clippy_lint! {
48 /// Checks for exclusive ranges where 1 is added to the
49 /// upper bound, e.g., `x..(y+1)`.
51 /// ### Why is this bad?
52 /// The code is more readable with an inclusive range
55 /// ### Known problems
56 /// Will add unnecessary pair of parentheses when the
57 /// expression is not wrapped in a pair but starts with an opening parenthesis
58 /// and ends with a closing one.
59 /// I.e., `let _ = (f()+1)..(f()+1)` results in `let _ = ((f()+1)..=f())`.
61 /// Also in many cases, inclusive ranges are still slower to run than
62 /// exclusive ranges, because they essentially add an extra branch that
63 /// LLVM may fail to hoist out of the loop.
65 /// This will cause a warning that cannot be fixed if the consumer of the
66 /// range only accepts a specific range type, instead of the generic
67 /// `RangeBounds` trait
68 /// ([#3307](https://github.com/rust-lang/rust-clippy/issues/3307)).
74 /// for i in x..(y+1) {
87 #[clippy::version = "pre 1.29.0"]
90 "`x..(y+1)` reads better as `x..=y`"
93 declare_clippy_lint! {
95 /// Checks for inclusive ranges where 1 is subtracted from
96 /// the upper bound, e.g., `x..=(y-1)`.
98 /// ### Why is this bad?
99 /// The code is more readable with an exclusive range
102 /// ### Known problems
103 /// This will cause a warning that cannot be fixed if
104 /// the consumer of the range only accepts a specific range type, instead of
105 /// the generic `RangeBounds` trait
106 /// ([#3307](https://github.com/rust-lang/rust-clippy/issues/3307)).
112 /// for i in x..=(y-1) {
125 #[clippy::version = "pre 1.29.0"]
128 "`x..=(y-1)` reads better as `x..y`"
131 declare_clippy_lint! {
133 /// Checks for range expressions `x..y` where both `x` and `y`
134 /// are constant and `x` is greater or equal to `y`.
136 /// ### Why is this bad?
137 /// Empty ranges yield no values so iterating them is a no-op.
138 /// Moreover, trying to use a reversed range to index a slice will panic at run-time.
143 /// (10..=0).for_each(|x| println!("{}", x));
145 /// let arr = [1, 2, 3, 4, 5];
146 /// let sub = &arr[3..1];
152 /// (0..=10).rev().for_each(|x| println!("{}", x));
154 /// let arr = [1, 2, 3, 4, 5];
155 /// let sub = &arr[1..3];
158 #[clippy::version = "1.45.0"]
159 pub REVERSED_EMPTY_RANGES,
161 "reversing the limits of range expressions, resulting in empty ranges"
164 declare_clippy_lint! {
166 /// Checks for expressions like `x >= 3 && x < 8` that could
167 /// be more readably expressed as `(3..8).contains(x)`.
169 /// ### Why is this bad?
170 /// `contains` expresses the intent better and has less
171 /// failure modes (such as fencepost errors or using `||` instead of `&&`).
178 /// assert!(x >= 3 && x < 8);
183 /// assert!((3..8).contains(&x));
185 #[clippy::version = "1.49.0"]
186 pub MANUAL_RANGE_CONTAINS,
188 "manually reimplementing {`Range`, `RangeInclusive`}`::contains`"
192 msrv: Option<RustcVersion>,
197 pub fn new(msrv: Option<RustcVersion>) -> Self {
202 impl_lint_pass!(Ranges => [
206 REVERSED_EMPTY_RANGES,
207 MANUAL_RANGE_CONTAINS,
210 impl<'tcx> LateLintPass<'tcx> for Ranges {
211 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
213 ExprKind::MethodCall(path, args, _) => {
214 check_range_zip_with_len(cx, path, args, expr.span);
216 ExprKind::Binary(ref op, l, r) => {
217 if meets_msrv(self.msrv, msrvs::RANGE_CONTAINS) {
218 check_possible_range_contains(cx, op.node, l, r, expr, expr.span);
224 check_exclusive_range_plus_one(cx, expr);
225 check_inclusive_range_minus_one(cx, expr);
226 check_reversed_empty_range(cx, expr);
228 extract_msrv_attr!(LateContext);
231 fn check_possible_range_contains(
232 cx: &LateContext<'_>,
239 if in_constant(cx, expr.hir_id) {
243 let combine_and = match op {
244 BinOpKind::And | BinOpKind::BitAnd => true,
245 BinOpKind::Or | BinOpKind::BitOr => false,
248 // value, name, order (higher/lower), inclusiveness
249 if let (Some(l), Some(r)) = (check_range_bounds(cx, left), check_range_bounds(cx, right)) {
250 // we only lint comparisons on the same name and with different
252 if l.id != r.id || l.ord == r.ord {
255 let ord = Constant::partial_cmp(cx.tcx, cx.typeck_results().expr_ty(l.expr), &l.val, &r.val);
256 if combine_and && ord == Some(r.ord) {
257 // order lower bound and upper bound
258 let (l_span, u_span, l_inc, u_inc) = if r.ord == Ordering::Less {
259 (l.val_span, r.val_span, l.inc, r.inc)
261 (r.val_span, l.val_span, r.inc, l.inc)
263 // we only lint inclusive lower bounds
267 let (range_type, range_op) = if u_inc {
268 ("RangeInclusive", "..=")
272 let mut applicability = Applicability::MachineApplicable;
273 let name = snippet_with_applicability(cx, l.name_span, "_", &mut applicability);
274 let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability);
275 let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability);
276 let space = if lo.ends_with('.') { " " } else { "" };
279 MANUAL_RANGE_CONTAINS,
281 &format!("manual `{}::contains` implementation", range_type),
283 format!("({}{}{}{}).contains(&{})", lo, space, range_op, hi, name),
286 } else if !combine_and && ord == Some(l.ord) {
288 // order lower bound and upper bound
289 let (l_span, u_span, l_inc, u_inc) = if l.ord == Ordering::Less {
290 (l.val_span, r.val_span, l.inc, r.inc)
292 (r.val_span, l.val_span, r.inc, l.inc)
297 let (range_type, range_op) = if u_inc {
300 ("RangeInclusive", "..=")
302 let mut applicability = Applicability::MachineApplicable;
303 let name = snippet_with_applicability(cx, l.name_span, "_", &mut applicability);
304 let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability);
305 let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability);
306 let space = if lo.ends_with('.') { " " } else { "" };
309 MANUAL_RANGE_CONTAINS,
311 &format!("manual `!{}::contains` implementation", range_type),
313 format!("!({}{}{}{}).contains(&{})", lo, space, range_op, hi, name),
319 // If the LHS is the same operator, we have to recurse to get the "real" RHS, since they have
320 // the same operator precedence
322 if let ExprKind::Binary(ref lhs_op, _left, new_lhs) = left.kind;
323 if op == lhs_op.node;
324 let new_span = Span::new(new_lhs.span.lo(), right.span.hi(), expr.span.ctxt(), expr.span.parent());
325 if let Some(snip) = &snippet_opt(cx, new_span);
326 // Do not continue if we have mismatched number of parens, otherwise the suggestion is wrong
327 if snip.matches('(').count() == snip.matches(')').count();
329 check_possible_range_contains(cx, op, new_lhs, right, expr, new_span);
334 struct RangeBounds<'a> {
344 // Takes a binary expression such as x <= 2 as input
345 // Breaks apart into various pieces, such as the value of the number,
346 // hir id of the variable, and direction/inclusiveness of the operator
347 fn check_range_bounds<'a>(cx: &'a LateContext<'_>, ex: &'a Expr<'_>) -> Option<RangeBounds<'a>> {
348 if let ExprKind::Binary(ref op, l, r) = ex.kind {
349 let (inclusive, ordering) = match op.node {
350 BinOpKind::Gt => (false, Ordering::Greater),
351 BinOpKind::Ge => (true, Ordering::Greater),
352 BinOpKind::Lt => (false, Ordering::Less),
353 BinOpKind::Le => (true, Ordering::Less),
356 if let Some(id) = path_to_local(l) {
357 if let Some((c, _)) = constant(cx, cx.typeck_results(), r) {
358 return Some(RangeBounds {
368 } else if let Some(id) = path_to_local(r) {
369 if let Some((c, _)) = constant(cx, cx.typeck_results(), l) {
370 return Some(RangeBounds {
376 ord: ordering.reverse(),
385 fn check_range_zip_with_len(cx: &LateContext<'_>, path: &PathSegment<'_>, args: &[Expr<'_>], span: Span) {
387 if path.ident.as_str() == "zip";
388 if let [iter, zip_arg] = args;
390 if let ExprKind::MethodCall(iter_path, iter_args, _) = iter.kind;
391 if iter_path.ident.name == sym::iter;
392 // range expression in `.zip()` call: `0..x.len()`
393 if let Some(higher::Range { start: Some(start), end: Some(end), .. }) = higher::Range::hir(zip_arg);
394 if is_integer_const(cx, start, 0);
396 if let ExprKind::MethodCall(len_path, len_args, _) = end.kind;
397 if len_path.ident.name == sym::len && len_args.len() == 1;
398 // `.iter()` and `.len()` called on same `Path`
399 if let ExprKind::Path(QPath::Resolved(_, iter_path)) = iter_args[0].kind;
400 if let ExprKind::Path(QPath::Resolved(_, len_path)) = len_args[0].kind;
401 if SpanlessEq::new(cx).eq_path_segments(iter_path.segments, len_path.segments);
406 &format!("it is more idiomatic to use `{}.iter().enumerate()`",
407 snippet(cx, iter_args[0].span, "_"))
413 // exclusive range plus one: `x..(y+1)`
414 fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
416 if let Some(higher::Range {
419 limits: RangeLimits::HalfOpen
420 }) = higher::Range::hir(expr);
421 if let Some(y) = y_plus_one(cx, end);
423 let span = if expr.span.from_expansion() {
435 "an inclusive range would be more readable",
437 let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").maybe_par().to_string());
438 let end = Sugg::hir(cx, y, "y").maybe_par();
439 if let Some(is_wrapped) = &snippet_opt(cx, span) {
440 if is_wrapped.starts_with('(') && is_wrapped.ends_with(')') {
441 diag.span_suggestion(
444 format!("({}..={})", start, end),
445 Applicability::MaybeIncorrect,
448 diag.span_suggestion(
451 format!("{}..={}", start, end),
452 Applicability::MachineApplicable, // snippet
462 // inclusive range minus one: `x..=(y-1)`
463 fn check_inclusive_range_minus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
465 if let Some(higher::Range { start, end: Some(end), limits: RangeLimits::Closed }) = higher::Range::hir(expr);
466 if let Some(y) = y_minus_one(cx, end);
472 "an exclusive range would be more readable",
474 let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").maybe_par().to_string());
475 let end = Sugg::hir(cx, y, "y").maybe_par();
476 diag.span_suggestion(
479 format!("{}..{}", start, end),
480 Applicability::MachineApplicable, // snippet
488 fn check_reversed_empty_range(cx: &LateContext<'_>, expr: &Expr<'_>) {
489 fn inside_indexing_expr(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
491 get_parent_expr(cx, expr),
493 kind: ExprKind::Index(..),
499 fn is_for_loop_arg(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
500 let mut cur_expr = expr;
501 while let Some(parent_expr) = get_parent_expr(cx, cur_expr) {
502 match higher::ForLoop::hir(parent_expr) {
503 Some(higher::ForLoop { arg, .. }) if arg.hir_id == expr.hir_id => return true,
504 _ => cur_expr = parent_expr,
511 fn is_empty_range(limits: RangeLimits, ordering: Ordering) -> bool {
513 RangeLimits::HalfOpen => ordering != Ordering::Less,
514 RangeLimits::Closed => ordering == Ordering::Greater,
519 if let Some(higher::Range { start: Some(start), end: Some(end), limits }) = higher::Range::hir(expr);
520 let ty = cx.typeck_results().expr_ty(start);
521 if let ty::Int(_) | ty::Uint(_) = ty.kind();
522 if let Some((start_idx, _)) = constant(cx, cx.typeck_results(), start);
523 if let Some((end_idx, _)) = constant(cx, cx.typeck_results(), end);
524 if let Some(ordering) = Constant::partial_cmp(cx.tcx, ty, &start_idx, &end_idx);
525 if is_empty_range(limits, ordering);
527 if inside_indexing_expr(cx, expr) {
528 // Avoid linting `N..N` as it has proven to be useful, see #5689 and #5628 ...
529 if ordering != Ordering::Equal {
532 REVERSED_EMPTY_RANGES,
534 "this range is reversed and using it to index a slice will panic at run-time",
537 // ... except in for loop arguments for backwards compatibility with `reverse_range_loop`
538 } else if ordering != Ordering::Equal || is_for_loop_arg(cx, expr) {
541 REVERSED_EMPTY_RANGES,
543 "this range is empty so it will yield no values",
545 if ordering != Ordering::Equal {
546 let start_snippet = snippet(cx, start.span, "_");
547 let end_snippet = snippet(cx, end.span, "_");
548 let dots = match limits {
549 RangeLimits::HalfOpen => "..",
550 RangeLimits::Closed => "..="
553 diag.span_suggestion(
555 "consider using the following if you are attempting to iterate over this \
557 format!("({}{}{}).rev()", end_snippet, dots, start_snippet),
558 Applicability::MaybeIncorrect,
568 fn y_plus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> {
572 node: BinOpKind::Add, ..
577 if is_integer_const(cx, lhs, 1) {
579 } else if is_integer_const(cx, rhs, 1) {
589 fn y_minus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> {
593 node: BinOpKind::Sub, ..
597 ) if is_integer_const(cx, rhs, 1) => Some(lhs),