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