Skip to content

Commit a77d6ee

Browse files
committed
New lint: manual-range-contains
1 parent 0cba5e6 commit a77d6ee

File tree

6 files changed

+250
-32
lines changed

6 files changed

+250
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,6 +1793,7 @@ Released 2018-09-13
17931793
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
17941794
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
17951795
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
1796+
[`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains
17961797
[`manual_saturating_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_saturating_arithmetic
17971798
[`manual_strip`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_strip
17981799
[`manual_swap`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_swap

clippy_lints/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
783783
&ptr_eq::PTR_EQ,
784784
&ptr_offset_with_cast::PTR_OFFSET_WITH_CAST,
785785
&question_mark::QUESTION_MARK,
786+
&ranges::MANUAL_RANGE_CONTAINS,
786787
&ranges::RANGE_MINUS_ONE,
787788
&ranges::RANGE_PLUS_ONE,
788789
&ranges::RANGE_ZIP_WITH_LEN,
@@ -1465,6 +1466,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
14651466
LintId::of(&ptr_eq::PTR_EQ),
14661467
LintId::of(&ptr_offset_with_cast::PTR_OFFSET_WITH_CAST),
14671468
LintId::of(&question_mark::QUESTION_MARK),
1469+
LintId::of(&ranges::MANUAL_RANGE_CONTAINS),
14681470
LintId::of(&ranges::RANGE_ZIP_WITH_LEN),
14691471
LintId::of(&ranges::REVERSED_EMPTY_RANGES),
14701472
LintId::of(&redundant_clone::REDUNDANT_CLONE),
@@ -1620,6 +1622,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16201622
LintId::of(&ptr::PTR_ARG),
16211623
LintId::of(&ptr_eq::PTR_EQ),
16221624
LintId::of(&question_mark::QUESTION_MARK),
1625+
LintId::of(&ranges::MANUAL_RANGE_CONTAINS),
16231626
LintId::of(&redundant_field_names::REDUNDANT_FIELD_NAMES),
16241627
LintId::of(&redundant_static_lifetimes::REDUNDANT_STATIC_LIFETIMES),
16251628
LintId::of(&regex::TRIVIAL_REGEX),

clippy_lints/src/ranges.rs

Lines changed: 161 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ use crate::consts::{constant, Constant};
22
use if_chain::if_chain;
33
use rustc_ast::ast::RangeLimits;
44
use rustc_errors::Applicability;
5-
use rustc_hir::{BinOpKind, Expr, ExprKind, QPath};
5+
use rustc_hir::{BinOpKind, Expr, ExprKind, PathSegment, QPath};
66
use rustc_lint::{LateContext, LateLintPass};
77
use rustc_middle::ty;
88
use rustc_session::{declare_lint_pass, declare_tool_lint};
9-
use rustc_span::source_map::Spanned;
9+
use rustc_span::source_map::{Span, Spanned};
10+
use rustc_span::symbol::Ident;
1011
use std::cmp::Ordering;
1112

1213
use crate::utils::sugg::Sugg;
13-
use crate::utils::{get_parent_expr, is_integer_const, snippet, snippet_opt, span_lint, span_lint_and_then};
14+
use crate::utils::{
15+
get_parent_expr, is_integer_const, single_segment_path, snippet, snippet_opt, snippet_with_applicability,
16+
span_lint, span_lint_and_then,
17+
};
1418
use crate::utils::{higher, SpanlessEq};
1519

1620
declare_clippy_lint! {
@@ -128,43 +132,51 @@ declare_clippy_lint! {
128132
"reversing the limits of range expressions, resulting in empty ranges"
129133
}
130134

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+
131162
declare_lint_pass!(Ranges => [
132163
RANGE_ZIP_WITH_LEN,
133164
RANGE_PLUS_ONE,
134165
RANGE_MINUS_ONE,
135166
REVERSED_EMPTY_RANGES,
167+
MANUAL_RANGE_CONTAINS,
136168
]);
137169

138170
impl<'tcx> LateLintPass<'tcx> for Ranges {
139171
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
140-
if let ExprKind::MethodCall(ref path, _, ref args, _) = expr.kind {
141-
let name = path.ident.as_str();
142-
if name == "zip" && args.len() == 2 {
143-
let iter = &args[0].kind;
144-
let zip_arg = &args[1];
145-
if_chain! {
146-
// `.iter()` call
147-
if let ExprKind::MethodCall(ref iter_path, _, ref iter_args , _) = *iter;
148-
if iter_path.ident.name == sym!(iter);
149-
// range expression in `.zip()` call: `0..x.len()`
150-
if let Some(higher::Range { start: Some(start), end: Some(end), .. }) = higher::range(zip_arg);
151-
if is_integer_const(cx, start, 0);
152-
// `.len()` call
153-
if let ExprKind::MethodCall(ref len_path, _, ref len_args, _) = end.kind;
154-
if len_path.ident.name == sym!(len) && len_args.len() == 1;
155-
// `.iter()` and `.len()` called on same `Path`
156-
if let ExprKind::Path(QPath::Resolved(_, ref iter_path)) = iter_args[0].kind;
157-
if let ExprKind::Path(QPath::Resolved(_, ref len_path)) = len_args[0].kind;
158-
if SpanlessEq::new(cx).eq_path_segments(&iter_path.segments, &len_path.segments);
159-
then {
160-
span_lint(cx,
161-
RANGE_ZIP_WITH_LEN,
162-
expr.span,
163-
&format!("it is more idiomatic to use `{}.iter().enumerate()`",
164-
snippet(cx, iter_args[0].span, "_")));
165-
}
166-
}
167-
}
172+
match expr.kind {
173+
ExprKind::MethodCall(ref path, _, ref args, _) => {
174+
check_range_zip_with_len(cx, path, args, expr.span);
175+
},
176+
ExprKind::Binary(ref op, ref l, ref r) => {
177+
check_possible_range_contains(cx, op.node, l, r, expr.span);
178+
},
179+
_ => {},
168180
}
169181

170182
check_exclusive_range_plus_one(cx, expr);
@@ -173,6 +185,124 @@ impl<'tcx> LateLintPass<'tcx> for Ranges {
173185
}
174186
}
175187

188+
fn check_possible_range_contains(cx: &LateContext<'_>, op: BinOpKind, l: &Expr<'_>, r: &Expr<'_>, span: Span) {
189+
let combine_and = match op {
190+
BinOpKind::And | BinOpKind::BitAnd => true,
191+
BinOpKind::Or | BinOpKind::BitOr => false,
192+
_ => return,
193+
};
194+
// value, name, order (higher/lower), inclusiveness
195+
if let (Some((lval, lname, name_span, lval_span, lord, linc)), Some((rval, rname, _, rval_span, rord, rinc))) =
196+
(check_range_bounds(cx, l), check_range_bounds(cx, r))
197+
{
198+
// we only lint comparisons on the same name and with different
199+
// direction
200+
if lname != rname || lord == rord {
201+
return;
202+
}
203+
let ord = Constant::partial_cmp(cx.tcx, cx.typeck_results().expr_ty(l), &lval, &rval);
204+
if combine_and && ord == Some(rord) {
205+
// order lower bound and upper bound
206+
let (l_span, u_span, l_inc, u_inc) = if rord == Ordering::Less {
207+
(lval_span, rval_span, linc, rinc)
208+
} else {
209+
(rval_span, lval_span, rinc, linc)
210+
};
211+
// we only lint inclusive lower bounds
212+
if !l_inc {
213+
return;
214+
}
215+
let (range_type, range_op) = if u_inc {
216+
("RangeInclusive", "..=")
217+
} else {
218+
("Range", "..")
219+
};
220+
span_lint_and_then(
221+
cx,
222+
MANUAL_RANGE_CONTAINS,
223+
span,
224+
&format!("manual `{}::contains` implementation", range_type),
225+
|diag| {
226+
let mut applicability = Applicability::MachineApplicable;
227+
let name = snippet_with_applicability(cx, name_span, "_", &mut applicability);
228+
let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability);
229+
let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability);
230+
diag.span_suggestion(
231+
span,
232+
"use",
233+
format!("({}{}{}).contains({})", lo, range_op, hi, name),
234+
applicability,
235+
);
236+
},
237+
);
238+
}
239+
}
240+
//TODO: lint !(..).contains
241+
}
242+
243+
fn check_range_bounds(cx: &LateContext<'_>, ex: &Expr<'_>) -> Option<(Constant, Ident, Span, Span, Ordering, bool)> {
244+
if let ExprKind::Binary(ref op, ref l, ref r) = ex.kind {
245+
let (inclusive, ordering) = match op.node {
246+
BinOpKind::Gt => (false, Ordering::Greater),
247+
BinOpKind::Ge => (true, Ordering::Greater),
248+
BinOpKind::Lt => (false, Ordering::Less),
249+
BinOpKind::Le => (true, Ordering::Less),
250+
_ => return None,
251+
};
252+
if let Some(id) = match_ident(l) {
253+
if let Some((c, _)) = constant(cx, cx.typeck_results(), r) {
254+
return Some((c, id, l.span, r.span, ordering, inclusive));
255+
}
256+
} else if let Some(id) = match_ident(r) {
257+
if let Some((c, _)) = constant(cx, cx.typeck_results(), l) {
258+
return Some((c, id, r.span, l.span, ordering.reverse(), inclusive));
259+
}
260+
}
261+
}
262+
None
263+
}
264+
265+
fn match_ident(e: &Expr<'_>) -> Option<Ident> {
266+
if let ExprKind::Path(ref qpath) = e.kind {
267+
if let Some(seg) = single_segment_path(qpath) {
268+
if seg.args.is_none() {
269+
return Some(seg.ident);
270+
}
271+
}
272+
}
273+
None
274+
}
275+
276+
fn check_range_zip_with_len(cx: &LateContext<'_>, path: &PathSegment<'_>, args: &[Expr<'_>], span: Span) {
277+
let name = path.ident.as_str();
278+
if name == "zip" && args.len() == 2 {
279+
let iter = &args[0].kind;
280+
let zip_arg = &args[1];
281+
if_chain! {
282+
// `.iter()` call
283+
if let ExprKind::MethodCall(ref iter_path, _, ref iter_args, _) = *iter;
284+
if iter_path.ident.name == sym!(iter);
285+
// range expression in `.zip()` call: `0..x.len()`
286+
if let Some(higher::Range { start: Some(start), end: Some(end), .. }) = higher::range(zip_arg);
287+
if is_integer_const(cx, start, 0);
288+
// `.len()` call
289+
if let ExprKind::MethodCall(ref len_path, _, ref len_args, _) = end.kind;
290+
if len_path.ident.name == sym!(len) && len_args.len() == 1;
291+
// `.iter()` and `.len()` called on same `Path`
292+
if let ExprKind::Path(QPath::Resolved(_, ref iter_path)) = iter_args[0].kind;
293+
if let ExprKind::Path(QPath::Resolved(_, ref len_path)) = len_args[0].kind;
294+
if SpanlessEq::new(cx).eq_path_segments(&iter_path.segments, &len_path.segments);
295+
then {
296+
span_lint(cx,
297+
RANGE_ZIP_WITH_LEN,
298+
span,
299+
&format!("it is more idiomatic to use `{}.iter().enumerate()`",
300+
snippet(cx, iter_args[0].span, "_")));
301+
}
302+
}
303+
}
304+
}
305+
176306
// exclusive range plus one: `x..(y+1)`
177307
fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
178308
if_chain! {

src/lintlist/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,13 @@ vec![
11591159
deprecation: None,
11601160
module: "manual_non_exhaustive",
11611161
},
1162+
Lint {
1163+
name: "manual_range_contains",
1164+
group: "style",
1165+
desc: "manually reimplementing {`Range`, `RangeInclusive`}`::contains`",
1166+
deprecation: None,
1167+
module: "ranges",
1168+
},
11621169
Lint {
11631170
name: "manual_saturating_arithmetic",
11641171
group: "style",

tests/ui/range.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,25 @@ fn no_panic_with_fake_range_types() {
1414

1515
let _ = Range { foo: 0 };
1616
}
17+
18+
#[warn(clippy::manual_range_contains)]
19+
#[allow(unused)]
20+
#[allow(clippy::no_effect)]
21+
#[allow(clippy::short_circuit_statement)]
22+
fn manual_range_contains(x: u32) {
23+
// order shouldn't matter
24+
x >= 8 && x < 12;
25+
x < 42 && x >= 21;
26+
100 > x && 0 <= x;
27+
28+
// also with inclusive ranges
29+
x >= 9 && x <= 99;
30+
x <= 33 && x >= 1;
31+
999 >= x && 1 <= x;
32+
33+
// not a range.contains
34+
x > 8 && x < 12; // lower bound not inclusive
35+
x < 8 && x <= 12; // same direction
36+
x >= 12 && 12 >= x; // same bounds
37+
x < 8 && x > 12; // wrong direction
38+
}

tests/ui/range.stderr

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,60 @@ LL | let _x = v1.iter().zip(0..v1.len());
66
|
77
= note: `-D clippy::range-zip-with-len` implied by `-D warnings`
88

9-
error: aborting due to previous error
9+
error: manual `Range::contains` implementation
10+
--> $DIR/range.rs:24:5
11+
|
12+
LL | x >= 8 && x < 12;
13+
| ^^^^^^^^^^^^^^^^ help: use: `(8..12).contains(x)`
14+
|
15+
= note: `-D clippy::manual-range-contains` implied by `-D warnings`
16+
17+
error: manual `Range::contains` implementation
18+
--> $DIR/range.rs:25:5
19+
|
20+
LL | x < 42 && x >= 21;
21+
| ^^^^^^^^^^^^^^^^^ help: use: `(21..42).contains(x)`
22+
23+
error: manual `Range::contains` implementation
24+
--> $DIR/range.rs:26:5
25+
|
26+
LL | 100 > x && 0 <= x;
27+
| ^^^^^^^^^^^^^^^^^ help: use: `(0..100).contains(x)`
28+
29+
error: this comparison involving the minimum or maximum element for this type contains a case that is always true or always false
30+
--> $DIR/range.rs:26:16
31+
|
32+
LL | 100 > x && 0 <= x;
33+
| ^^^^^^
34+
|
35+
= note: `#[deny(clippy::absurd_extreme_comparisons)]` on by default
36+
= help: because `0` is the minimum value for this type, this comparison is always true
37+
38+
error: manual `RangeInclusive::contains` implementation
39+
--> $DIR/range.rs:29:5
40+
|
41+
LL | x >= 9 && x <= 99;
42+
| ^^^^^^^^^^^^^^^^^ help: use: `(9..=99).contains(x)`
43+
44+
error: manual `RangeInclusive::contains` implementation
45+
--> $DIR/range.rs:30:5
46+
|
47+
LL | x <= 33 && x >= 1;
48+
| ^^^^^^^^^^^^^^^^^ help: use: `(1..=33).contains(x)`
49+
50+
error: manual `RangeInclusive::contains` implementation
51+
--> $DIR/range.rs:31:5
52+
|
53+
LL | 999 >= x && 1 <= x;
54+
| ^^^^^^^^^^^^^^^^^^ help: use: `(1..=999).contains(x)`
55+
56+
error: comparison is useless due to type limits
57+
--> $DIR/range.rs:26:16
58+
|
59+
LL | 100 > x && 0 <= x;
60+
| ^^^^^^
61+
|
62+
= note: `-D unused-comparisons` implied by `-D warnings`
63+
64+
error: aborting due to 9 previous errors
1065

0 commit comments

Comments
 (0)