Skip to content

Commit a0c2257

Browse files
committed
New lint: manual-range-contains
1 parent 4e83a38 commit a0c2257

File tree

6 files changed

+317
-32
lines changed

6 files changed

+317
-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
@@ -785,6 +785,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
785785
&ptr_eq::PTR_EQ,
786786
&ptr_offset_with_cast::PTR_OFFSET_WITH_CAST,
787787
&question_mark::QUESTION_MARK,
788+
&ranges::MANUAL_RANGE_CONTAINS,
788789
&ranges::RANGE_MINUS_ONE,
789790
&ranges::RANGE_PLUS_ONE,
790791
&ranges::RANGE_ZIP_WITH_LEN,
@@ -1469,6 +1470,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
14691470
LintId::of(&ptr_eq::PTR_EQ),
14701471
LintId::of(&ptr_offset_with_cast::PTR_OFFSET_WITH_CAST),
14711472
LintId::of(&question_mark::QUESTION_MARK),
1473+
LintId::of(&ranges::MANUAL_RANGE_CONTAINS),
14721474
LintId::of(&ranges::RANGE_ZIP_WITH_LEN),
14731475
LintId::of(&ranges::REVERSED_EMPTY_RANGES),
14741476
LintId::of(&redundant_clone::REDUNDANT_CLONE),
@@ -1624,6 +1626,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16241626
LintId::of(&ptr::PTR_ARG),
16251627
LintId::of(&ptr_eq::PTR_EQ),
16261628
LintId::of(&question_mark::QUESTION_MARK),
1629+
LintId::of(&ranges::MANUAL_RANGE_CONTAINS),
16271630
LintId::of(&redundant_field_names::REDUNDANT_FIELD_NAMES),
16281631
LintId::of(&redundant_static_lifetimes::REDUNDANT_STATIC_LIFETIMES),
16291632
LintId::of(&regex::TRIVIAL_REGEX),

clippy_lints/src/ranges.rs

Lines changed: 194 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,157 @@ 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+
} else if !combine_and && ord == Some(lord) {
239+
// `!_.contains(_)`
240+
// order lower bound and upper bound
241+
let (l_span, u_span, l_inc, u_inc) = if lord == Ordering::Less {
242+
(lval_span, rval_span, linc, rinc)
243+
} else {
244+
(rval_span, lval_span, rinc, linc)
245+
};
246+
if l_inc {
247+
return;
248+
}
249+
let (range_type, range_op) = if u_inc {
250+
("Range", "..")
251+
} else {
252+
("RangeInclusive", "..=")
253+
};
254+
span_lint_and_then(
255+
cx,
256+
MANUAL_RANGE_CONTAINS,
257+
span,
258+
&format!("manual `!{}::contains` implementation", range_type),
259+
|diag| {
260+
let mut applicability = Applicability::MachineApplicable;
261+
let name = snippet_with_applicability(cx, name_span, "_", &mut applicability);
262+
let lo = snippet_with_applicability(cx, l_span, "_", &mut applicability);
263+
let hi = snippet_with_applicability(cx, u_span, "_", &mut applicability);
264+
diag.span_suggestion(
265+
span,
266+
"use",
267+
format!("!({}{}{}).contains({})", lo, range_op, hi, name),
268+
applicability,
269+
);
270+
},
271+
);
272+
}
273+
}
274+
}
275+
276+
fn check_range_bounds(cx: &LateContext<'_>, ex: &Expr<'_>) -> Option<(Constant, Ident, Span, Span, Ordering, bool)> {
277+
if let ExprKind::Binary(ref op, ref l, ref r) = ex.kind {
278+
let (inclusive, ordering) = match op.node {
279+
BinOpKind::Gt => (false, Ordering::Greater),
280+
BinOpKind::Ge => (true, Ordering::Greater),
281+
BinOpKind::Lt => (false, Ordering::Less),
282+
BinOpKind::Le => (true, Ordering::Less),
283+
_ => return None,
284+
};
285+
if let Some(id) = match_ident(l) {
286+
if let Some((c, _)) = constant(cx, cx.typeck_results(), r) {
287+
return Some((c, id, l.span, r.span, ordering, inclusive));
288+
}
289+
} else if let Some(id) = match_ident(r) {
290+
if let Some((c, _)) = constant(cx, cx.typeck_results(), l) {
291+
return Some((c, id, r.span, l.span, ordering.reverse(), inclusive));
292+
}
293+
}
294+
}
295+
None
296+
}
297+
298+
fn match_ident(e: &Expr<'_>) -> Option<Ident> {
299+
if let ExprKind::Path(ref qpath) = e.kind {
300+
if let Some(seg) = single_segment_path(qpath) {
301+
if seg.args.is_none() {
302+
return Some(seg.ident);
303+
}
304+
}
305+
}
306+
None
307+
}
308+
309+
fn check_range_zip_with_len(cx: &LateContext<'_>, path: &PathSegment<'_>, args: &[Expr<'_>], span: Span) {
310+
let name = path.ident.as_str();
311+
if name == "zip" && args.len() == 2 {
312+
let iter = &args[0].kind;
313+
let zip_arg = &args[1];
314+
if_chain! {
315+
// `.iter()` call
316+
if let ExprKind::MethodCall(ref iter_path, _, ref iter_args, _) = *iter;
317+
if iter_path.ident.name == sym!(iter);
318+
// range expression in `.zip()` call: `0..x.len()`
319+
if let Some(higher::Range { start: Some(start), end: Some(end), .. }) = higher::range(zip_arg);
320+
if is_integer_const(cx, start, 0);
321+
// `.len()` call
322+
if let ExprKind::MethodCall(ref len_path, _, ref len_args, _) = end.kind;
323+
if len_path.ident.name == sym!(len) && len_args.len() == 1;
324+
// `.iter()` and `.len()` called on same `Path`
325+
if let ExprKind::Path(QPath::Resolved(_, ref iter_path)) = iter_args[0].kind;
326+
if let ExprKind::Path(QPath::Resolved(_, ref len_path)) = len_args[0].kind;
327+
if SpanlessEq::new(cx).eq_path_segments(&iter_path.segments, &len_path.segments);
328+
then {
329+
span_lint(cx,
330+
RANGE_ZIP_WITH_LEN,
331+
span,
332+
&format!("it is more idiomatic to use `{}.iter().enumerate()`",
333+
snippet(cx, iter_args[0].span, "_")));
334+
}
335+
}
336+
}
337+
}
338+
176339
// exclusive range plus one: `x..(y+1)`
177340
fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
178341
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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,40 @@ 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 && 1 <= x;
27+
28+
// also with inclusive ranges
29+
x >= 9 && x <= 99;
30+
x <= 33 && x >= 1;
31+
999 >= x && 1 <= x;
32+
33+
// and the outside
34+
x < 8 || x >= 12;
35+
x >= 42 || x < 21;
36+
100 <= x || 1 > x;
37+
38+
// also with the outside of inclusive ranges
39+
x < 9 || x > 99;
40+
x > 33 || x < 1;
41+
999 < x || 1 > x;
42+
43+
// not a range.contains
44+
x > 8 && x < 12; // lower bound not inclusive
45+
x < 8 && x <= 12; // same direction
46+
x >= 12 && 12 >= x; // same bounds
47+
x < 8 && x > 12; // wrong direction
48+
49+
x <= 8 || x >= 12;
50+
x >= 8 || x >= 12;
51+
x < 12 || 12 < x;
52+
x >= 8 || x <= 12;
53+
}

0 commit comments

Comments
 (0)