]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/ranges.rs
Auto merge of #6284 - camsteffen:rustc-sym, r=flip1995
[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             span_lint_and_sugg(
226                 cx,
227                 MANUAL_RANGE_CONTAINS,
228                 span,
229                 &format!("manual `{}::contains` implementation", range_type),
230                 "use",
231                 format!("({}{}{}).contains(&{})", lo, range_op, hi, name),
232                 applicability,
233             );
234         } else if !combine_and && ord == Some(lord) {
235             // `!_.contains(_)`
236             // order lower bound and upper bound
237             let (l_span, u_span, l_inc, u_inc) = if lord == Ordering::Less {
238                 (lval_span, rval_span, linc, rinc)
239             } else {
240                 (rval_span, lval_span, rinc, linc)
241             };
242             if l_inc {
243                 return;
244             }
245             let (range_type, range_op) = if u_inc {
246                 ("Range", "..")
247             } else {
248                 ("RangeInclusive", "..=")
249             };
250             let mut applicability = Applicability::MachineApplicable;
251             let name = snippet_with_applicability(cx, name_span, "_", &mut applicability);
252             let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability);
253             let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability);
254             span_lint_and_sugg(
255                 cx,
256                 MANUAL_RANGE_CONTAINS,
257                 span,
258                 &format!("manual `!{}::contains` implementation", range_type),
259                 "use",
260                 format!("!({}{}{}).contains(&{})", lo, range_op, hi, name),
261                 applicability,
262             );
263         }
264     }
265 }
266
267 fn check_range_bounds(cx: &LateContext<'_>, ex: &Expr<'_>) -> Option<(Constant, Ident, Span, Span, Ordering, bool)> {
268     if let ExprKind::Binary(ref op, ref l, ref r) = ex.kind {
269         let (inclusive, ordering) = match op.node {
270             BinOpKind::Gt => (false, Ordering::Greater),
271             BinOpKind::Ge => (true, Ordering::Greater),
272             BinOpKind::Lt => (false, Ordering::Less),
273             BinOpKind::Le => (true, Ordering::Less),
274             _ => return None,
275         };
276         if let Some(id) = match_ident(l) {
277             if let Some((c, _)) = constant(cx, cx.typeck_results(), r) {
278                 return Some((c, id, l.span, r.span, ordering, inclusive));
279             }
280         } else if let Some(id) = match_ident(r) {
281             if let Some((c, _)) = constant(cx, cx.typeck_results(), l) {
282                 return Some((c, id, r.span, l.span, ordering.reverse(), inclusive));
283             }
284         }
285     }
286     None
287 }
288
289 fn match_ident(e: &Expr<'_>) -> Option<Ident> {
290     if let ExprKind::Path(ref qpath) = e.kind {
291         if let Some(seg) = single_segment_path(qpath) {
292             if seg.args.is_none() {
293                 return Some(seg.ident);
294             }
295         }
296     }
297     None
298 }
299
300 fn check_range_zip_with_len(cx: &LateContext<'_>, path: &PathSegment<'_>, args: &[Expr<'_>], span: Span) {
301     let name = path.ident.as_str();
302     if name == "zip" && args.len() == 2 {
303         let iter = &args[0].kind;
304         let zip_arg = &args[1];
305         if_chain! {
306             // `.iter()` call
307             if let ExprKind::MethodCall(ref iter_path, _, ref iter_args, _) = *iter;
308             if iter_path.ident.name == sym::iter;
309             // range expression in `.zip()` call: `0..x.len()`
310             if let Some(higher::Range { start: Some(start), end: Some(end), .. }) = higher::range(zip_arg);
311             if is_integer_const(cx, start, 0);
312             // `.len()` call
313             if let ExprKind::MethodCall(ref len_path, _, ref len_args, _) = end.kind;
314             if len_path.ident.name == sym!(len) && len_args.len() == 1;
315             // `.iter()` and `.len()` called on same `Path`
316             if let ExprKind::Path(QPath::Resolved(_, ref iter_path)) = iter_args[0].kind;
317             if let ExprKind::Path(QPath::Resolved(_, ref len_path)) = len_args[0].kind;
318             if SpanlessEq::new(cx).eq_path_segments(&iter_path.segments, &len_path.segments);
319             then {
320                 span_lint(cx,
321                     RANGE_ZIP_WITH_LEN,
322                     span,
323                     &format!("it is more idiomatic to use `{}.iter().enumerate()`",
324                         snippet(cx, iter_args[0].span, "_"))
325                 );
326             }
327         }
328     }
329 }
330
331 // exclusive range plus one: `x..(y+1)`
332 fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
333     if_chain! {
334         if let Some(higher::Range {
335             start,
336             end: Some(end),
337             limits: RangeLimits::HalfOpen
338         }) = higher::range(expr);
339         if let Some(y) = y_plus_one(cx, end);
340         then {
341             let span = if expr.span.from_expansion() {
342                 expr.span
343                     .ctxt()
344                     .outer_expn_data()
345                     .call_site
346             } else {
347                 expr.span
348             };
349             span_lint_and_then(
350                 cx,
351                 RANGE_PLUS_ONE,
352                 span,
353                 "an inclusive range would be more readable",
354                 |diag| {
355                     let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").to_string());
356                     let end = Sugg::hir(cx, y, "y");
357                     if let Some(is_wrapped) = &snippet_opt(cx, span) {
358                         if is_wrapped.starts_with('(') && is_wrapped.ends_with(')') {
359                             diag.span_suggestion(
360                                 span,
361                                 "use",
362                                 format!("({}..={})", start, end),
363                                 Applicability::MaybeIncorrect,
364                             );
365                         } else {
366                             diag.span_suggestion(
367                                 span,
368                                 "use",
369                                 format!("{}..={}", start, end),
370                                 Applicability::MachineApplicable, // snippet
371                             );
372                         }
373                     }
374                 },
375             );
376         }
377     }
378 }
379
380 // inclusive range minus one: `x..=(y-1)`
381 fn check_inclusive_range_minus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
382     if_chain! {
383         if let Some(higher::Range { start, end: Some(end), limits: RangeLimits::Closed }) = higher::range(expr);
384         if let Some(y) = y_minus_one(cx, end);
385         then {
386             span_lint_and_then(
387                 cx,
388                 RANGE_MINUS_ONE,
389                 expr.span,
390                 "an exclusive range would be more readable",
391                 |diag| {
392                     let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").to_string());
393                     let end = Sugg::hir(cx, y, "y");
394                     diag.span_suggestion(
395                         expr.span,
396                         "use",
397                         format!("{}..{}", start, end),
398                         Applicability::MachineApplicable, // snippet
399                     );
400                 },
401             );
402         }
403     }
404 }
405
406 fn check_reversed_empty_range(cx: &LateContext<'_>, expr: &Expr<'_>) {
407     fn inside_indexing_expr(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
408         matches!(
409             get_parent_expr(cx, expr),
410             Some(Expr {
411                 kind: ExprKind::Index(..),
412                 ..
413             })
414         )
415     }
416
417     fn is_for_loop_arg(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
418         let mut cur_expr = expr;
419         while let Some(parent_expr) = get_parent_expr(cx, cur_expr) {
420             match higher::for_loop(parent_expr) {
421                 Some((_, args, _)) if args.hir_id == expr.hir_id => return true,
422                 _ => cur_expr = parent_expr,
423             }
424         }
425
426         false
427     }
428
429     fn is_empty_range(limits: RangeLimits, ordering: Ordering) -> bool {
430         match limits {
431             RangeLimits::HalfOpen => ordering != Ordering::Less,
432             RangeLimits::Closed => ordering == Ordering::Greater,
433         }
434     }
435
436     if_chain! {
437         if let Some(higher::Range { start: Some(start), end: Some(end), limits }) = higher::range(expr);
438         let ty = cx.typeck_results().expr_ty(start);
439         if let ty::Int(_) | ty::Uint(_) = ty.kind();
440         if let Some((start_idx, _)) = constant(cx, cx.typeck_results(), start);
441         if let Some((end_idx, _)) = constant(cx, cx.typeck_results(), end);
442         if let Some(ordering) = Constant::partial_cmp(cx.tcx, ty, &start_idx, &end_idx);
443         if is_empty_range(limits, ordering);
444         then {
445             if inside_indexing_expr(cx, expr) {
446                 // Avoid linting `N..N` as it has proven to be useful, see #5689 and #5628 ...
447                 if ordering != Ordering::Equal {
448                     span_lint(
449                         cx,
450                         REVERSED_EMPTY_RANGES,
451                         expr.span,
452                         "this range is reversed and using it to index a slice will panic at run-time",
453                     );
454                 }
455             // ... except in for loop arguments for backwards compatibility with `reverse_range_loop`
456             } else if ordering != Ordering::Equal || is_for_loop_arg(cx, expr) {
457                 span_lint_and_then(
458                     cx,
459                     REVERSED_EMPTY_RANGES,
460                     expr.span,
461                     "this range is empty so it will yield no values",
462                     |diag| {
463                         if ordering != Ordering::Equal {
464                             let start_snippet = snippet(cx, start.span, "_");
465                             let end_snippet = snippet(cx, end.span, "_");
466                             let dots = match limits {
467                                 RangeLimits::HalfOpen => "..",
468                                 RangeLimits::Closed => "..="
469                             };
470
471                             diag.span_suggestion(
472                                 expr.span,
473                                 "consider using the following if you are attempting to iterate over this \
474                                  range in reverse",
475                                 format!("({}{}{}).rev()", end_snippet, dots, start_snippet),
476                                 Applicability::MaybeIncorrect,
477                             );
478                         }
479                     },
480                 );
481             }
482         }
483     }
484 }
485
486 fn y_plus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> {
487     match expr.kind {
488         ExprKind::Binary(
489             Spanned {
490                 node: BinOpKind::Add, ..
491             },
492             ref lhs,
493             ref rhs,
494         ) => {
495             if is_integer_const(cx, lhs, 1) {
496                 Some(rhs)
497             } else if is_integer_const(cx, rhs, 1) {
498                 Some(lhs)
499             } else {
500                 None
501             }
502         },
503         _ => None,
504     }
505 }
506
507 fn y_minus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> {
508     match expr.kind {
509         ExprKind::Binary(
510             Spanned {
511                 node: BinOpKind::Sub, ..
512             },
513             ref lhs,
514             ref rhs,
515         ) if is_integer_const(cx, rhs, 1) => Some(lhs),
516         _ => None,
517     }
518 }