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