]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/ranges.rs
4b514bbd42ca7d0df4c0b5ebb5853974dec1f31a
[rust.git] / clippy_lints / src / ranges.rs
1 use crate::consts::{constant, Constant};
2 use if_chain::if_chain;
3 use rustc_ast::ast::RangeLimits;
4 use rustc_errors::Applicability;
5 use rustc_hir::{BinOpKind, Expr, ExprKind, PathSegment, QPath};
6 use rustc_lint::{LateContext, LateLintPass};
7 use rustc_middle::ty;
8 use rustc_session::{declare_lint_pass, declare_tool_lint};
9 use rustc_span::source_map::{Span, Spanned};
10 use rustc_span::sym;
11 use rustc_span::symbol::Ident;
12 use std::cmp::Ordering;
13
14 use crate::utils::sugg::Sugg;
15 use crate::utils::{
16     get_parent_expr, is_integer_const, single_segment_path, snippet, snippet_opt, snippet_with_applicability,
17     span_lint, span_lint_and_sugg, span_lint_and_then,
18 };
19 use crate::utils::{higher, SpanlessEq};
20
21 declare_clippy_lint! {
22     /// **What it does:** Checks for zipping a collection with the range of
23     /// `0.._.len()`.
24     ///
25     /// **Why is this bad?** The code is better expressed with `.enumerate()`.
26     ///
27     /// **Known problems:** None.
28     ///
29     /// **Example:**
30     /// ```rust
31     /// # let x = vec![1];
32     /// x.iter().zip(0..x.len());
33     /// ```
34     /// Could be written as
35     /// ```rust
36     /// # let x = vec![1];
37     /// x.iter().enumerate();
38     /// ```
39     pub RANGE_ZIP_WITH_LEN,
40     complexity,
41     "zipping iterator with a range when `enumerate()` would do"
42 }
43
44 declare_clippy_lint! {
45     /// **What it does:** Checks for exclusive ranges where 1 is added to the
46     /// upper bound, e.g., `x..(y+1)`.
47     ///
48     /// **Why is this bad?** The code is more readable with an inclusive range
49     /// like `x..=y`.
50     ///
51     /// **Known problems:** Will add unnecessary pair of parentheses when the
52     /// expression is not wrapped in a pair but starts with a opening parenthesis
53     /// and ends with a closing one.
54     /// I.e., `let _ = (f()+1)..(f()+1)` results in `let _ = ((f()+1)..=f())`.
55     ///
56     /// Also in many cases, inclusive ranges are still slower to run than
57     /// exclusive ranges, because they essentially add an extra branch that
58     /// LLVM may fail to hoist out of the loop.
59     ///
60     /// This will cause a warning that cannot be fixed if the consumer of the
61     /// range only accepts a specific range type, instead of the generic
62     /// `RangeBounds` trait
63     /// ([#3307](https://github.com/rust-lang/rust-clippy/issues/3307)).
64     ///
65     /// **Example:**
66     /// ```rust,ignore
67     /// for x..(y+1) { .. }
68     /// ```
69     /// Could be written as
70     /// ```rust,ignore
71     /// for x..=y { .. }
72     /// ```
73     pub RANGE_PLUS_ONE,
74     pedantic,
75     "`x..(y+1)` reads better as `x..=y`"
76 }
77
78 declare_clippy_lint! {
79     /// **What it does:** Checks for inclusive ranges where 1 is subtracted from
80     /// the upper bound, e.g., `x..=(y-1)`.
81     ///
82     /// **Why is this bad?** The code is more readable with an exclusive range
83     /// like `x..y`.
84     ///
85     /// **Known problems:** This will cause a warning that cannot be fixed if
86     /// the consumer of the range only accepts a specific range type, instead of
87     /// the generic `RangeBounds` trait
88     /// ([#3307](https://github.com/rust-lang/rust-clippy/issues/3307)).
89     ///
90     /// **Example:**
91     /// ```rust,ignore
92     /// for x..=(y-1) { .. }
93     /// ```
94     /// Could be written as
95     /// ```rust,ignore
96     /// for x..y { .. }
97     /// ```
98     pub RANGE_MINUS_ONE,
99     pedantic,
100     "`x..=(y-1)` reads better as `x..y`"
101 }
102
103 declare_clippy_lint! {
104     /// **What it does:** Checks for range expressions `x..y` where both `x` and `y`
105     /// are constant and `x` is greater or equal to `y`.
106     ///
107     /// **Why is this bad?** Empty ranges yield no values so iterating them is a no-op.
108     /// Moreover, trying to use a reversed range to index a slice will panic at run-time.
109     ///
110     /// **Known problems:** None.
111     ///
112     /// **Example:**
113     ///
114     /// ```rust,no_run
115     /// fn main() {
116     ///     (10..=0).for_each(|x| println!("{}", x));
117     ///
118     ///     let arr = [1, 2, 3, 4, 5];
119     ///     let sub = &arr[3..1];
120     /// }
121     /// ```
122     /// Use instead:
123     /// ```rust
124     /// fn main() {
125     ///     (0..=10).rev().for_each(|x| println!("{}", x));
126     ///
127     ///     let arr = [1, 2, 3, 4, 5];
128     ///     let sub = &arr[1..3];
129     /// }
130     /// ```
131     pub REVERSED_EMPTY_RANGES,
132     correctness,
133     "reversing the limits of range expressions, resulting in empty ranges"
134 }
135
136 declare_clippy_lint! {
137     /// **What it does:** Checks for expressions like `x >= 3 && x < 8` that could
138     /// be more readably expressed as `(3..8).contains(x)`.
139     ///
140     /// **Why is this bad?** `contains` expresses the intent better and has less
141     /// failure modes (such as fencepost errors or using `||` instead of `&&`).
142     ///
143     /// **Known problems:** None.
144     ///
145     /// **Example:**
146     ///
147     /// ```rust
148     /// // given
149     /// let x = 6;
150     ///
151     /// assert!(x >= 3 && x < 8);
152     /// ```
153     /// Use instead:
154     /// ```rust
155     ///# let x = 6;
156     /// assert!((3..8).contains(&x));
157     /// ```
158     pub MANUAL_RANGE_CONTAINS,
159     style,
160     "manually reimplementing {`Range`, `RangeInclusive`}`::contains`"
161 }
162
163 declare_lint_pass!(Ranges => [
164     RANGE_ZIP_WITH_LEN,
165     RANGE_PLUS_ONE,
166     RANGE_MINUS_ONE,
167     REVERSED_EMPTY_RANGES,
168     MANUAL_RANGE_CONTAINS,
169 ]);
170
171 impl<'tcx> LateLintPass<'tcx> for Ranges {
172     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
173         match expr.kind {
174             ExprKind::MethodCall(ref path, _, ref args, _) => {
175                 check_range_zip_with_len(cx, path, args, expr.span);
176             },
177             ExprKind::Binary(ref op, ref l, ref r) => {
178                 check_possible_range_contains(cx, op.node, l, r, expr.span);
179             },
180             _ => {},
181         }
182
183         check_exclusive_range_plus_one(cx, expr);
184         check_inclusive_range_minus_one(cx, expr);
185         check_reversed_empty_range(cx, expr);
186     }
187 }
188
189 fn check_possible_range_contains(cx: &LateContext<'_>, op: BinOpKind, l: &Expr<'_>, r: &Expr<'_>, span: Span) {
190     let combine_and = match op {
191         BinOpKind::And | BinOpKind::BitAnd => true,
192         BinOpKind::Or | BinOpKind::BitOr => false,
193         _ => return,
194     };
195     // value, name, order (higher/lower), inclusiveness
196     if let (Some((lval, lname, name_span, lval_span, lord, linc)), Some((rval, rname, _, rval_span, rord, rinc))) =
197         (check_range_bounds(cx, l), check_range_bounds(cx, r))
198     {
199         // we only lint comparisons on the same name and with different
200         // direction
201         if lname != rname || lord == rord {
202             return;
203         }
204         let ord = Constant::partial_cmp(cx.tcx, cx.typeck_results().expr_ty(l), &lval, &rval);
205         if combine_and && ord == Some(rord) {
206             // order lower bound and upper bound
207             let (l_span, u_span, l_inc, u_inc) = if rord == Ordering::Less {
208                 (lval_span, rval_span, linc, rinc)
209             } else {
210                 (rval_span, lval_span, rinc, linc)
211             };
212             // we only lint inclusive lower bounds
213             if !l_inc {
214                 return;
215             }
216             let (range_type, range_op) = if u_inc {
217                 ("RangeInclusive", "..=")
218             } else {
219                 ("Range", "..")
220             };
221             let mut applicability = Applicability::MachineApplicable;
222             let name = snippet_with_applicability(cx, name_span, "_", &mut applicability);
223             let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability);
224             let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability);
225             let space = if lo.ends_with('.') { " " } else { "" };
226             span_lint_and_sugg(
227                 cx,
228                 MANUAL_RANGE_CONTAINS,
229                 span,
230                 &format!("manual `{}::contains` implementation", range_type),
231                 "use",
232                 format!("({}{}{}{}).contains(&{})", lo, space, range_op, hi, name),
233                 applicability,
234             );
235         } else if !combine_and && ord == Some(lord) {
236             // `!_.contains(_)`
237             // order lower bound and upper bound
238             let (l_span, u_span, l_inc, u_inc) = if lord == Ordering::Less {
239                 (lval_span, rval_span, linc, rinc)
240             } else {
241                 (rval_span, lval_span, rinc, linc)
242             };
243             if l_inc {
244                 return;
245             }
246             let (range_type, range_op) = if u_inc {
247                 ("Range", "..")
248             } else {
249                 ("RangeInclusive", "..=")
250             };
251             let mut applicability = Applicability::MachineApplicable;
252             let name = snippet_with_applicability(cx, name_span, "_", &mut applicability);
253             let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability);
254             let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability);
255             let space = if lo.ends_with('.') { " " } else { "" };
256             span_lint_and_sugg(
257                 cx,
258                 MANUAL_RANGE_CONTAINS,
259                 span,
260                 &format!("manual `!{}::contains` implementation", range_type),
261                 "use",
262                 format!("!({}{}{}{}).contains(&{})", lo, space, range_op, hi, name),
263                 applicability,
264             );
265         }
266     }
267 }
268
269 fn check_range_bounds(cx: &LateContext<'_>, ex: &Expr<'_>) -> Option<(Constant, Ident, Span, Span, Ordering, bool)> {
270     if let ExprKind::Binary(ref op, ref l, ref r) = ex.kind {
271         let (inclusive, ordering) = match op.node {
272             BinOpKind::Gt => (false, Ordering::Greater),
273             BinOpKind::Ge => (true, Ordering::Greater),
274             BinOpKind::Lt => (false, Ordering::Less),
275             BinOpKind::Le => (true, Ordering::Less),
276             _ => return None,
277         };
278         if let Some(id) = match_ident(l) {
279             if let Some((c, _)) = constant(cx, cx.typeck_results(), r) {
280                 return Some((c, id, l.span, r.span, ordering, inclusive));
281             }
282         } else if let Some(id) = match_ident(r) {
283             if let Some((c, _)) = constant(cx, cx.typeck_results(), l) {
284                 return Some((c, id, r.span, l.span, ordering.reverse(), inclusive));
285             }
286         }
287     }
288     None
289 }
290
291 fn match_ident(e: &Expr<'_>) -> Option<Ident> {
292     if let ExprKind::Path(ref qpath) = e.kind {
293         if let Some(seg) = single_segment_path(qpath) {
294             if seg.args.is_none() {
295                 return Some(seg.ident);
296             }
297         }
298     }
299     None
300 }
301
302 fn check_range_zip_with_len(cx: &LateContext<'_>, path: &PathSegment<'_>, args: &[Expr<'_>], span: Span) {
303     let name = path.ident.as_str();
304     if name == "zip" && args.len() == 2 {
305         let iter = &args[0].kind;
306         let zip_arg = &args[1];
307         if_chain! {
308             // `.iter()` call
309             if let ExprKind::MethodCall(ref iter_path, _, ref iter_args, _) = *iter;
310             if iter_path.ident.name == sym::iter;
311             // range expression in `.zip()` call: `0..x.len()`
312             if let Some(higher::Range { start: Some(start), end: Some(end), .. }) = higher::range(zip_arg);
313             if is_integer_const(cx, start, 0);
314             // `.len()` call
315             if let ExprKind::MethodCall(ref len_path, _, ref len_args, _) = end.kind;
316             if len_path.ident.name == sym!(len) && len_args.len() == 1;
317             // `.iter()` and `.len()` called on same `Path`
318             if let ExprKind::Path(QPath::Resolved(_, ref iter_path)) = iter_args[0].kind;
319             if let ExprKind::Path(QPath::Resolved(_, ref len_path)) = len_args[0].kind;
320             if SpanlessEq::new(cx).eq_path_segments(&iter_path.segments, &len_path.segments);
321             then {
322                 span_lint(cx,
323                     RANGE_ZIP_WITH_LEN,
324                     span,
325                     &format!("it is more idiomatic to use `{}.iter().enumerate()`",
326                         snippet(cx, iter_args[0].span, "_"))
327                 );
328             }
329         }
330     }
331 }
332
333 // exclusive range plus one: `x..(y+1)`
334 fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
335     if_chain! {
336         if let Some(higher::Range {
337             start,
338             end: Some(end),
339             limits: RangeLimits::HalfOpen
340         }) = higher::range(expr);
341         if let Some(y) = y_plus_one(cx, end);
342         then {
343             let span = if expr.span.from_expansion() {
344                 expr.span
345                     .ctxt()
346                     .outer_expn_data()
347                     .call_site
348             } else {
349                 expr.span
350             };
351             span_lint_and_then(
352                 cx,
353                 RANGE_PLUS_ONE,
354                 span,
355                 "an inclusive range would be more readable",
356                 |diag| {
357                     let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").to_string());
358                     let end = Sugg::hir(cx, y, "y");
359                     if let Some(is_wrapped) = &snippet_opt(cx, span) {
360                         if is_wrapped.starts_with('(') && is_wrapped.ends_with(')') {
361                             diag.span_suggestion(
362                                 span,
363                                 "use",
364                                 format!("({}..={})", start, end),
365                                 Applicability::MaybeIncorrect,
366                             );
367                         } else {
368                             diag.span_suggestion(
369                                 span,
370                                 "use",
371                                 format!("{}..={}", start, end),
372                                 Applicability::MachineApplicable, // snippet
373                             );
374                         }
375                     }
376                 },
377             );
378         }
379     }
380 }
381
382 // inclusive range minus one: `x..=(y-1)`
383 fn check_inclusive_range_minus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
384     if_chain! {
385         if let Some(higher::Range { start, end: Some(end), limits: RangeLimits::Closed }) = higher::range(expr);
386         if let Some(y) = y_minus_one(cx, end);
387         then {
388             span_lint_and_then(
389                 cx,
390                 RANGE_MINUS_ONE,
391                 expr.span,
392                 "an exclusive range would be more readable",
393                 |diag| {
394                     let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").to_string());
395                     let end = Sugg::hir(cx, y, "y");
396                     diag.span_suggestion(
397                         expr.span,
398                         "use",
399                         format!("{}..{}", start, end),
400                         Applicability::MachineApplicable, // snippet
401                     );
402                 },
403             );
404         }
405     }
406 }
407
408 fn check_reversed_empty_range(cx: &LateContext<'_>, expr: &Expr<'_>) {
409     fn inside_indexing_expr(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
410         matches!(
411             get_parent_expr(cx, expr),
412             Some(Expr {
413                 kind: ExprKind::Index(..),
414                 ..
415             })
416         )
417     }
418
419     fn is_for_loop_arg(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
420         let mut cur_expr = expr;
421         while let Some(parent_expr) = get_parent_expr(cx, cur_expr) {
422             match higher::for_loop(parent_expr) {
423                 Some((_, args, _)) if args.hir_id == expr.hir_id => return true,
424                 _ => cur_expr = parent_expr,
425             }
426         }
427
428         false
429     }
430
431     fn is_empty_range(limits: RangeLimits, ordering: Ordering) -> bool {
432         match limits {
433             RangeLimits::HalfOpen => ordering != Ordering::Less,
434             RangeLimits::Closed => ordering == Ordering::Greater,
435         }
436     }
437
438     if_chain! {
439         if let Some(higher::Range { start: Some(start), end: Some(end), limits }) = higher::range(expr);
440         let ty = cx.typeck_results().expr_ty(start);
441         if let ty::Int(_) | ty::Uint(_) = ty.kind();
442         if let Some((start_idx, _)) = constant(cx, cx.typeck_results(), start);
443         if let Some((end_idx, _)) = constant(cx, cx.typeck_results(), end);
444         if let Some(ordering) = Constant::partial_cmp(cx.tcx, ty, &start_idx, &end_idx);
445         if is_empty_range(limits, ordering);
446         then {
447             if inside_indexing_expr(cx, expr) {
448                 // Avoid linting `N..N` as it has proven to be useful, see #5689 and #5628 ...
449                 if ordering != Ordering::Equal {
450                     span_lint(
451                         cx,
452                         REVERSED_EMPTY_RANGES,
453                         expr.span,
454                         "this range is reversed and using it to index a slice will panic at run-time",
455                     );
456                 }
457             // ... except in for loop arguments for backwards compatibility with `reverse_range_loop`
458             } else if ordering != Ordering::Equal || is_for_loop_arg(cx, expr) {
459                 span_lint_and_then(
460                     cx,
461                     REVERSED_EMPTY_RANGES,
462                     expr.span,
463                     "this range is empty so it will yield no values",
464                     |diag| {
465                         if ordering != Ordering::Equal {
466                             let start_snippet = snippet(cx, start.span, "_");
467                             let end_snippet = snippet(cx, end.span, "_");
468                             let dots = match limits {
469                                 RangeLimits::HalfOpen => "..",
470                                 RangeLimits::Closed => "..="
471                             };
472
473                             diag.span_suggestion(
474                                 expr.span,
475                                 "consider using the following if you are attempting to iterate over this \
476                                  range in reverse",
477                                 format!("({}{}{}).rev()", end_snippet, dots, start_snippet),
478                                 Applicability::MaybeIncorrect,
479                             );
480                         }
481                     },
482                 );
483             }
484         }
485     }
486 }
487
488 fn y_plus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> {
489     match expr.kind {
490         ExprKind::Binary(
491             Spanned {
492                 node: BinOpKind::Add, ..
493             },
494             ref lhs,
495             ref rhs,
496         ) => {
497             if is_integer_const(cx, lhs, 1) {
498                 Some(rhs)
499             } else if is_integer_const(cx, rhs, 1) {
500                 Some(lhs)
501             } else {
502                 None
503             }
504         },
505         _ => None,
506     }
507 }
508
509 fn y_minus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> {
510     match expr.kind {
511         ExprKind::Binary(
512             Spanned {
513                 node: BinOpKind::Sub, ..
514             },
515             ref lhs,
516             ref rhs,
517         ) if is_integer_const(cx, rhs, 1) => Some(lhs),
518         _ => None,
519     }
520 }