Skip to content

Commit 90cb25d

Browse files
committed
Auto merge of #6177 - rust-lang:manual-range-contains, r=flip1995
New lint: manual-range-contains This fixes #1110, at least for the contains-suggesting part. - \[x] Followed [lint naming conventions][lint_naming] - \[x] Added passing UI tests (including committed `.stderr` file) - \[x] `cargo test` passes locally - \[x] Executed `cargo dev update_lints` - \[x] Added lint documentation - \[x] Run `cargo dev fmt` --- changelog: new lint: manual-range-contains
2 parents 399732b + c693de3 commit 90cb25d

File tree

7 files changed

+354
-31
lines changed

7 files changed

+354
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,6 +1795,7 @@ Released 2018-09-13
17951795
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
17961796
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
17971797
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
1798+
[`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains
17981799
[`manual_saturating_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_saturating_arithmetic
17991800
[`manual_strip`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_strip
18001801
[`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
@@ -792,6 +792,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
792792
&ptr_eq::PTR_EQ,
793793
&ptr_offset_with_cast::PTR_OFFSET_WITH_CAST,
794794
&question_mark::QUESTION_MARK,
795+
&ranges::MANUAL_RANGE_CONTAINS,
795796
&ranges::RANGE_MINUS_ONE,
796797
&ranges::RANGE_PLUS_ONE,
797798
&ranges::RANGE_ZIP_WITH_LEN,
@@ -1483,6 +1484,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
14831484
LintId::of(&ptr_eq::PTR_EQ),
14841485
LintId::of(&ptr_offset_with_cast::PTR_OFFSET_WITH_CAST),
14851486
LintId::of(&question_mark::QUESTION_MARK),
1487+
LintId::of(&ranges::MANUAL_RANGE_CONTAINS),
14861488
LintId::of(&ranges::RANGE_ZIP_WITH_LEN),
14871489
LintId::of(&ranges::REVERSED_EMPTY_RANGES),
14881490
LintId::of(&redundant_clone::REDUNDANT_CLONE),
@@ -1640,6 +1642,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16401642
LintId::of(&ptr::PTR_ARG),
16411643
LintId::of(&ptr_eq::PTR_EQ),
16421644
LintId::of(&question_mark::QUESTION_MARK),
1645+
LintId::of(&ranges::MANUAL_RANGE_CONTAINS),
16431646
LintId::of(&redundant_field_names::REDUNDANT_FIELD_NAMES),
16441647
LintId::of(&redundant_static_lifetimes::REDUNDANT_STATIC_LIFETIMES),
16451648
LintId::of(&regex::TRIVIAL_REGEX),

clippy_lints/src/ranges.rs

Lines changed: 185 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_sugg, 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,148 @@ 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+
let mut applicability = Applicability::MachineApplicable;
221+
let name = snippet_with_applicability(cx, name_span, "_", &mut applicability);
222+
let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability);
223+
let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability);
224+
span_lint_and_sugg(
225+
cx,
226+
MANUAL_RANGE_CONTAINS,
227+
span,
228+
&format!("manual `{}::contains` implementation", range_type),
229+
"use",
230+
format!("({}{}{}).contains(&{})", lo, range_op, hi, name),
231+
applicability,
232+
);
233+
} else if !combine_and && ord == Some(lord) {
234+
// `!_.contains(_)`
235+
// order lower bound and upper bound
236+
let (l_span, u_span, l_inc, u_inc) = if lord == Ordering::Less {
237+
(lval_span, rval_span, linc, rinc)
238+
} else {
239+
(rval_span, lval_span, rinc, linc)
240+
};
241+
if l_inc {
242+
return;
243+
}
244+
let (range_type, range_op) = if u_inc {
245+
("Range", "..")
246+
} else {
247+
("RangeInclusive", "..=")
248+
};
249+
let mut applicability = Applicability::MachineApplicable;
250+
let name = snippet_with_applicability(cx, name_span, "_", &mut applicability);
251+
let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability);
252+
let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability);
253+
span_lint_and_sugg(
254+
cx,
255+
MANUAL_RANGE_CONTAINS,
256+
span,
257+
&format!("manual `!{}::contains` implementation", range_type),
258+
"use",
259+
format!("!({}{}{}).contains(&{})", lo, range_op, hi, name),
260+
applicability,
261+
);
262+
}
263+
}
264+
}
265+
266+
fn check_range_bounds(cx: &LateContext<'_>, ex: &Expr<'_>) -> Option<(Constant, Ident, Span, Span, Ordering, bool)> {
267+
if let ExprKind::Binary(ref op, ref l, ref r) = ex.kind {
268+
let (inclusive, ordering) = match op.node {
269+
BinOpKind::Gt => (false, Ordering::Greater),
270+
BinOpKind::Ge => (true, Ordering::Greater),
271+
BinOpKind::Lt => (false, Ordering::Less),
272+
BinOpKind::Le => (true, Ordering::Less),
273+
_ => return None,
274+
};
275+
if let Some(id) = match_ident(l) {
276+
if let Some((c, _)) = constant(cx, cx.typeck_results(), r) {
277+
return Some((c, id, l.span, r.span, ordering, inclusive));
278+
}
279+
} else if let Some(id) = match_ident(r) {
280+
if let Some((c, _)) = constant(cx, cx.typeck_results(), l) {
281+
return Some((c, id, r.span, l.span, ordering.reverse(), inclusive));
282+
}
283+
}
284+
}
285+
None
286+
}
287+
288+
fn match_ident(e: &Expr<'_>) -> Option<Ident> {
289+
if let ExprKind::Path(ref qpath) = e.kind {
290+
if let Some(seg) = single_segment_path(qpath) {
291+
if seg.args.is_none() {
292+
return Some(seg.ident);
293+
}
294+
}
295+
}
296+
None
297+
}
298+
299+
fn check_range_zip_with_len(cx: &LateContext<'_>, path: &PathSegment<'_>, args: &[Expr<'_>], span: Span) {
300+
let name = path.ident.as_str();
301+
if name == "zip" && args.len() == 2 {
302+
let iter = &args[0].kind;
303+
let zip_arg = &args[1];
304+
if_chain! {
305+
// `.iter()` call
306+
if let ExprKind::MethodCall(ref iter_path, _, ref iter_args, _) = *iter;
307+
if iter_path.ident.name == sym!(iter);
308+
// range expression in `.zip()` call: `0..x.len()`
309+
if let Some(higher::Range { start: Some(start), end: Some(end), .. }) = higher::range(zip_arg);
310+
if is_integer_const(cx, start, 0);
311+
// `.len()` call
312+
if let ExprKind::MethodCall(ref len_path, _, ref len_args, _) = end.kind;
313+
if len_path.ident.name == sym!(len) && len_args.len() == 1;
314+
// `.iter()` and `.len()` called on same `Path`
315+
if let ExprKind::Path(QPath::Resolved(_, ref iter_path)) = iter_args[0].kind;
316+
if let ExprKind::Path(QPath::Resolved(_, ref len_path)) = len_args[0].kind;
317+
if SpanlessEq::new(cx).eq_path_segments(&iter_path.segments, &len_path.segments);
318+
then {
319+
span_lint(cx,
320+
RANGE_ZIP_WITH_LEN,
321+
span,
322+
&format!("it is more idiomatic to use `{}.iter().enumerate()`",
323+
snippet(cx, iter_args[0].span, "_"))
324+
);
325+
}
326+
}
327+
}
328+
}
329+
176330
// exclusive range plus one: `x..(y+1)`
177331
fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
178332
if_chain! {

src/lintlist/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,13 @@ vec![
11731173
deprecation: None,
11741174
module: "manual_non_exhaustive",
11751175
},
1176+
Lint {
1177+
name: "manual_range_contains",
1178+
group: "style",
1179+
desc: "manually reimplementing {`Range`, `RangeInclusive`}`::contains`",
1180+
deprecation: None,
1181+
module: "ranges",
1182+
},
11761183
Lint {
11771184
name: "manual_saturating_arithmetic",
11781185
group: "style",

tests/ui/range_contains.fixed

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// run-rustfix
2+
3+
#[warn(clippy::manual_range_contains)]
4+
#[allow(unused)]
5+
#[allow(clippy::no_effect)]
6+
#[allow(clippy::short_circuit_statement)]
7+
#[allow(clippy::unnecessary_operation)]
8+
fn main() {
9+
let x = 9_u32;
10+
11+
// order shouldn't matter
12+
(8..12).contains(&x);
13+
(21..42).contains(&x);
14+
(1..100).contains(&x);
15+
16+
// also with inclusive ranges
17+
(9..=99).contains(&x);
18+
(1..=33).contains(&x);
19+
(1..=999).contains(&x);
20+
21+
// and the outside
22+
!(8..12).contains(&x);
23+
!(21..42).contains(&x);
24+
!(1..100).contains(&x);
25+
26+
// also with the outside of inclusive ranges
27+
!(9..=99).contains(&x);
28+
!(1..=33).contains(&x);
29+
!(1..=999).contains(&x);
30+
31+
// not a range.contains
32+
x > 8 && x < 12; // lower bound not inclusive
33+
x < 8 && x <= 12; // same direction
34+
x >= 12 && 12 >= x; // same bounds
35+
x < 8 && x > 12; // wrong direction
36+
37+
x <= 8 || x >= 12;
38+
x >= 8 || x >= 12;
39+
x < 12 || 12 < x;
40+
x >= 8 || x <= 12;
41+
}

tests/ui/range_contains.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// run-rustfix
2+
3+
#[warn(clippy::manual_range_contains)]
4+
#[allow(unused)]
5+
#[allow(clippy::no_effect)]
6+
#[allow(clippy::short_circuit_statement)]
7+
#[allow(clippy::unnecessary_operation)]
8+
fn main() {
9+
let x = 9_u32;
10+
11+
// order shouldn't matter
12+
x >= 8 && x < 12;
13+
x < 42 && x >= 21;
14+
100 > x && 1 <= x;
15+
16+
// also with inclusive ranges
17+
x >= 9 && x <= 99;
18+
x <= 33 && x >= 1;
19+
999 >= x && 1 <= x;
20+
21+
// and the outside
22+
x < 8 || x >= 12;
23+
x >= 42 || x < 21;
24+
100 <= x || 1 > x;
25+
26+
// also with the outside of inclusive ranges
27+
x < 9 || x > 99;
28+
x > 33 || x < 1;
29+
999 < x || 1 > x;
30+
31+
// not a range.contains
32+
x > 8 && x < 12; // lower bound not inclusive
33+
x < 8 && x <= 12; // same direction
34+
x >= 12 && 12 >= x; // same bounds
35+
x < 8 && x > 12; // wrong direction
36+
37+
x <= 8 || x >= 12;
38+
x >= 8 || x >= 12;
39+
x < 12 || 12 < x;
40+
x >= 8 || x <= 12;
41+
}

0 commit comments

Comments
 (0)