Skip to content

Commit 184736f

Browse files
committed
Propose to exchange ranges only when it is safe to do so
To avoid false positives, the `range_plus_one` and `range_minus_one` lints will restrict themselves to situations where the iterator types can be easily switched from exclusive to inclusive or vice-versa. This includes situations where the range is used as an iterator, or is used for indexing. On the other hand, assignments of the range to variables, including automatically typed ones or wildcards, will no longer trigger the lint. However, the cases where such an assignment would benefit from the lint are probably rare.
1 parent b96fecf commit 184736f

File tree

7 files changed

+118
-48
lines changed

7 files changed

+118
-48
lines changed

clippy_lints/src/cognitive_complexity.rs

-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ impl CognitiveComplexity {
9797
FnKind::ItemFn(ident, _, _) | FnKind::Method(ident, _) => ident.span,
9898
FnKind::Closure => {
9999
let header_span = body_span.with_hi(decl.output.span().lo());
100-
#[expect(clippy::range_plus_one)]
101100
if let Some(range) = header_span.map_range(cx, |src, range| {
102101
let mut idxs = src.get(range.clone())?.match_indices('|');
103102
Some(range.start + idxs.next()?.0..range.start + idxs.next()?.0 + 1)

clippy_lints/src/doc/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -1159,7 +1159,6 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
11591159
headers
11601160
}
11611161

1162-
#[expect(clippy::range_plus_one)] // inclusive ranges aren't the same type
11631162
fn looks_like_refdef(doc: &str, range: Range<usize>) -> Option<Range<usize>> {
11641163
if range.end < range.start {
11651164
return None;

clippy_lints/src/methods/manual_inspect.rs

-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ pub(crate) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, name:
100100
match x {
101101
UseKind::Return(s) => edits.push((s.with_leading_whitespace(cx).with_ctxt(s.ctxt()), String::new())),
102102
UseKind::Borrowed(s) => {
103-
#[expect(clippy::range_plus_one)]
104103
let range = s.map_range(cx, |src, range| {
105104
let src = src.get(range.clone())?;
106105
let trimmed = src.trim_start_matches([' ', '\t', '\n', '\r', '(']);

clippy_lints/src/ranges.rs

+51-13
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_the
44
use clippy_utils::msrvs::{self, Msrv};
55
use clippy_utils::source::{SpanRangeExt, snippet, snippet_with_applicability};
66
use clippy_utils::sugg::Sugg;
7-
use clippy_utils::{get_parent_expr, higher, is_in_const_context, is_integer_const, path_to_local};
7+
use clippy_utils::{get_parent_expr, higher, is_in_const_context, is_integer_const, is_path_lang_item, path_to_local};
88
use rustc_ast::ast::RangeLimits;
99
use rustc_errors::Applicability;
10-
use rustc_hir::{BinOpKind, Expr, ExprKind, HirId};
10+
use rustc_hir::{BinOpKind, Expr, ExprKind, HirId, LangItem};
1111
use rustc_lint::{LateContext, LateLintPass};
1212
use rustc_middle::ty;
1313
use rustc_session::impl_lint_pass;
14-
use rustc_span::Span;
1514
use rustc_span::source_map::Spanned;
15+
use rustc_span::{Span, sym};
1616
use std::cmp::Ordering;
1717

1818
declare_clippy_lint! {
@@ -24,6 +24,12 @@ declare_clippy_lint! {
2424
/// The code is more readable with an inclusive range
2525
/// like `x..=y`.
2626
///
27+
/// ### Limitations
28+
/// The lint is conservative and will trigger only when switching
29+
/// from an exclusive to an inclusive range is provably safe from
30+
/// a typing point of view. This corresponds to situations where
31+
/// the range is used as an iterator, or for indexing.
32+
///
2733
/// ### Known problems
2834
/// Will add unnecessary pair of parentheses when the
2935
/// expression is not wrapped in a pair but starts with an opening parenthesis
@@ -34,11 +40,6 @@ declare_clippy_lint! {
3440
/// exclusive ranges, because they essentially add an extra branch that
3541
/// LLVM may fail to hoist out of the loop.
3642
///
37-
/// This will cause a warning that cannot be fixed if the consumer of the
38-
/// range only accepts a specific range type, instead of the generic
39-
/// `RangeBounds` trait
40-
/// ([#3307](https://github.com/rust-lang/rust-clippy/issues/3307)).
41-
///
4243
/// ### Example
4344
/// ```no_run
4445
/// # let x = 0;
@@ -71,11 +72,11 @@ declare_clippy_lint! {
7172
/// The code is more readable with an exclusive range
7273
/// like `x..y`.
7374
///
74-
/// ### Known problems
75-
/// This will cause a warning that cannot be fixed if
76-
/// the consumer of the range only accepts a specific range type, instead of
77-
/// the generic `RangeBounds` trait
78-
/// ([#3307](https://github.com/rust-lang/rust-clippy/issues/3307)).
75+
/// ### Limitations
76+
/// The lint is conservative and will trigger only when switching
77+
/// from an inclusive to an exclusive range is provably safe from
78+
/// a typing point of view. This corresponds to situations where
79+
/// the range is used as an iterator, or for indexing.
7980
///
8081
/// ### Example
8182
/// ```no_run
@@ -344,6 +345,41 @@ fn check_range_bounds<'a, 'tcx>(cx: &'a LateContext<'tcx>, ex: &'a Expr<'_>) ->
344345
None
345346
}
346347

348+
/// Check whether `expr` could switch range types without breaking the typing requirements. This is
349+
/// the case when `expr` is used as an iterator for example, or as a slice or `&str` index.
350+
fn can_switch_ranges(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
351+
let Some(parent_expr) = get_parent_expr(cx, expr) else {
352+
return false;
353+
};
354+
355+
if let ExprKind::Call(func, [arg]) = parent_expr.kind
356+
&& arg.hir_id == expr.hir_id
357+
&& is_path_lang_item(cx, func, LangItem::IntoIterIntoIter)
358+
{
359+
return true;
360+
}
361+
362+
if let ExprKind::MethodCall(_, receiver, _, _) = parent_expr.kind
363+
&& receiver.hir_id == expr.hir_id
364+
&& let Some(method_did) = cx.typeck_results().type_dependent_def_id(parent_expr.hir_id)
365+
&& let Some(trait_did) = cx.tcx.trait_of_item(method_did)
366+
&& matches!(
367+
cx.tcx.get_diagnostic_name(trait_did),
368+
Some(sym::Iterator | sym::IntoIterator | sym::RangeBounds)
369+
)
370+
{
371+
return true;
372+
}
373+
374+
if let ExprKind::Index(_, index, _) = parent_expr.kind
375+
&& index.hir_id == expr.hir_id
376+
{
377+
return true;
378+
}
379+
380+
false
381+
}
382+
347383
// exclusive range plus one: `x..(y+1)`
348384
fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
349385
if expr.span.can_be_used_for_suggestions()
@@ -353,6 +389,7 @@ fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
353389
limits: RangeLimits::HalfOpen,
354390
}) = higher::Range::hir(expr)
355391
&& let Some(y) = y_plus_one(cx, end)
392+
&& can_switch_ranges(cx, expr)
356393
{
357394
let span = expr.span;
358395
span_lint_and_then(
@@ -391,6 +428,7 @@ fn check_inclusive_range_minus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
391428
limits: RangeLimits::Closed,
392429
}) = higher::Range::hir(expr)
393430
&& let Some(y) = y_minus_one(cx, end)
431+
&& can_switch_ranges(cx, expr)
394432
{
395433
span_lint_and_then(
396434
cx,

tests/ui/range_plus_minus_one.fixed

+27-8
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,13 @@ fn main() {
4545
//~^ range_plus_one
4646
for _ in 0..=(1 + f()) {}
4747

48+
// Those are not linted, as in the general case we cannot be sure that the exact type won't be
49+
// important.
4850
let _ = ..11 - 1;
49-
let _ = ..11;
50-
//~^ range_minus_one
51-
let _ = ..11;
52-
//~^ range_minus_one
53-
let _ = (1..=11);
54-
//~^ range_plus_one
55-
let _ = ((f() + 1)..=f());
56-
//~^ range_plus_one
51+
let _ = ..=11 - 1;
52+
let _ = ..=(11 - 1);
53+
let _ = (1..11 + 1);
54+
let _ = (f() + 1)..(f() + 1);
5755

5856
const ONE: usize = 1;
5957
// integer consts are linted, too
@@ -65,4 +63,25 @@ fn main() {
6563

6664
macro_plus_one!(5);
6765
macro_minus_one!(5);
66+
67+
// As an instance of `Iterator`
68+
(1..=10).for_each(|_| {});
69+
//~^ range_plus_one
70+
71+
// As an instance of `IntoIterator`
72+
#[allow(clippy::useless_conversion)]
73+
(1..=10).into_iter().for_each(|_| {});
74+
//~^ range_plus_one
75+
76+
// As an instance of `RangeBounds`
77+
{
78+
use std::ops::RangeBounds;
79+
let _ = (1..=10).start_bound();
80+
//~^ range_plus_one
81+
}
82+
83+
// As a `SliceIndex`
84+
let a = [10, 20, 30];
85+
let _ = &a[1..=1];
86+
//~^ range_plus_one
6887
}

tests/ui/range_plus_minus_one.rs

+23-4
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,13 @@ fn main() {
4545
//~^ range_plus_one
4646
for _ in 0..=(1 + f()) {}
4747

48+
// Those are not linted, as in the general case we cannot be sure that the exact type won't be
49+
// important.
4850
let _ = ..11 - 1;
4951
let _ = ..=11 - 1;
50-
//~^ range_minus_one
5152
let _ = ..=(11 - 1);
52-
//~^ range_minus_one
5353
let _ = (1..11 + 1);
54-
//~^ range_plus_one
5554
let _ = (f() + 1)..(f() + 1);
56-
//~^ range_plus_one
5755

5856
const ONE: usize = 1;
5957
// integer consts are linted, too
@@ -65,4 +63,25 @@ fn main() {
6563

6664
macro_plus_one!(5);
6765
macro_minus_one!(5);
66+
67+
// As an instance of `Iterator`
68+
(1..10 + 1).for_each(|_| {});
69+
//~^ range_plus_one
70+
71+
// As an instance of `IntoIterator`
72+
#[allow(clippy::useless_conversion)]
73+
(1..10 + 1).into_iter().for_each(|_| {});
74+
//~^ range_plus_one
75+
76+
// As an instance of `RangeBounds`
77+
{
78+
use std::ops::RangeBounds;
79+
let _ = (1..10 + 1).start_bound();
80+
//~^ range_plus_one
81+
}
82+
83+
// As a `SliceIndex`
84+
let a = [10, 20, 30];
85+
let _ = &a[1..1 + 1];
86+
//~^ range_plus_one
6887
}

tests/ui/range_plus_minus_one.stderr

+17-20
Original file line numberDiff line numberDiff line change
@@ -25,38 +25,35 @@ error: an inclusive range would be more readable
2525
LL | for _ in 0..(1 + f()) {}
2626
| ^^^^^^^^^^^^ help: use: `0..=f()`
2727

28-
error: an exclusive range would be more readable
29-
--> tests/ui/range_plus_minus_one.rs:49:13
30-
|
31-
LL | let _ = ..=11 - 1;
32-
| ^^^^^^^^^ help: use: `..11`
28+
error: an inclusive range would be more readable
29+
--> tests/ui/range_plus_minus_one.rs:58:14
3330
|
34-
= note: `-D clippy::range-minus-one` implied by `-D warnings`
35-
= help: to override `-D warnings` add `#[allow(clippy::range_minus_one)]`
31+
LL | for _ in 1..ONE + ONE {}
32+
| ^^^^^^^^^^^^ help: use: `1..=ONE`
3633

37-
error: an exclusive range would be more readable
38-
--> tests/ui/range_plus_minus_one.rs:51:13
34+
error: an inclusive range would be more readable
35+
--> tests/ui/range_plus_minus_one.rs:68:5
3936
|
40-
LL | let _ = ..=(11 - 1);
41-
| ^^^^^^^^^^^ help: use: `..11`
37+
LL | (1..10 + 1).for_each(|_| {});
38+
| ^^^^^^^^^^^ help: use: `(1..=10)`
4239

4340
error: an inclusive range would be more readable
44-
--> tests/ui/range_plus_minus_one.rs:53:13
41+
--> tests/ui/range_plus_minus_one.rs:73:5
4542
|
46-
LL | let _ = (1..11 + 1);
47-
| ^^^^^^^^^^^ help: use: `(1..=11)`
43+
LL | (1..10 + 1).into_iter().for_each(|_| {});
44+
| ^^^^^^^^^^^ help: use: `(1..=10)`
4845

4946
error: an inclusive range would be more readable
50-
--> tests/ui/range_plus_minus_one.rs:55:13
47+
--> tests/ui/range_plus_minus_one.rs:79:17
5148
|
52-
LL | let _ = (f() + 1)..(f() + 1);
53-
| ^^^^^^^^^^^^^^^^^^^^ help: use: `((f() + 1)..=f())`
49+
LL | let _ = (1..10 + 1).start_bound();
50+
| ^^^^^^^^^^^ help: use: `(1..=10)`
5451

5552
error: an inclusive range would be more readable
56-
--> tests/ui/range_plus_minus_one.rs:60:14
53+
--> tests/ui/range_plus_minus_one.rs:85:16
5754
|
58-
LL | for _ in 1..ONE + ONE {}
59-
| ^^^^^^^^^^^^ help: use: `1..=ONE`
55+
LL | let _ = &a[1..1 + 1];
56+
| ^^^^^^^^ help: use: `1..=1`
6057

6158
error: aborting due to 9 previous errors
6259

0 commit comments

Comments
 (0)