Skip to content

Commit d66e4f6

Browse files
committed
Restrict the cases where Clippy proposes to switch range types
To limit 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. Also, when the range is used as a function or method call argument and the parameter type is generic over traits implemented by both kind of ranges, the lint will trigger. 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. This fix doesn't prevent false positives from happening. A more complete fix would check if the obligations can be satisfied by the new proposed range type.
1 parent 19c1c70 commit d66e4f6

File tree

7 files changed

+389
-59
lines changed

7 files changed

+389
-59
lines changed

clippy_lints/src/cognitive_complexity.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ impl CognitiveComplexity {
103103
FnKind::ItemFn(ident, _, _) | FnKind::Method(ident, _) => ident.span,
104104
FnKind::Closure => {
105105
let header_span = body_span.with_hi(decl.output.span().lo());
106-
#[expect(clippy::range_plus_one)]
107106
if let Some(range) = header_span.map_range(cx, |_, src, range| {
108107
let mut idxs = src.get(range.clone())?.match_indices('|');
109108
Some(range.start + idxs.next()?.0..range.start + idxs.next()?.0 + 1)

clippy_lints/src/doc/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,6 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
12321232
headers
12331233
}
12341234

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

clippy_lints/src/methods/manual_inspect.rs

Lines changed: 0 additions & 1 deletion
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

Lines changed: 140 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@ 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::ty::implements_trait;
8+
use clippy_utils::{
9+
expr_use_ctxt, fn_def_id, get_parent_expr, higher, is_in_const_context, is_integer_const, is_path_lang_item,
10+
path_to_local,
11+
};
12+
use rustc_ast::Mutability;
813
use rustc_ast::ast::RangeLimits;
914
use rustc_errors::Applicability;
10-
use rustc_hir::{BinOpKind, Expr, ExprKind, HirId};
15+
use rustc_hir::{BinOpKind, Expr, ExprKind, HirId, LangItem, Node};
1116
use rustc_lint::{LateContext, LateLintPass};
12-
use rustc_middle::ty;
17+
use rustc_middle::ty::{self, ClauseKind, GenericArgKind, PredicatePolarity, Ty};
1318
use rustc_session::impl_lint_pass;
14-
use rustc_span::Span;
1519
use rustc_span::source_map::Spanned;
20+
use rustc_span::{Span, sym};
1621
use std::cmp::Ordering;
1722

1823
declare_clippy_lint! {
@@ -24,6 +29,12 @@ declare_clippy_lint! {
2429
/// The code is more readable with an inclusive range
2530
/// like `x..=y`.
2631
///
32+
/// ### Limitations
33+
/// The lint is conservative and will trigger only when switching
34+
/// from an exclusive to an inclusive range is provably safe from
35+
/// a typing point of view. This corresponds to situations where
36+
/// the range is used as an iterator, or for indexing.
37+
///
2738
/// ### Known problems
2839
/// Will add unnecessary pair of parentheses when the
2940
/// expression is not wrapped in a pair but starts with an opening parenthesis
@@ -34,11 +45,6 @@ declare_clippy_lint! {
3445
/// exclusive ranges, because they essentially add an extra branch that
3546
/// LLVM may fail to hoist out of the loop.
3647
///
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-
///
4248
/// ### Example
4349
/// ```no_run
4450
/// # let x = 0;
@@ -71,11 +77,11 @@ declare_clippy_lint! {
7177
/// The code is more readable with an exclusive range
7278
/// like `x..y`.
7379
///
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)).
80+
/// ### Limitations
81+
/// The lint is conservative and will trigger only when switching
82+
/// from an inclusive to an exclusive range is provably safe from
83+
/// a typing point of view. This corresponds to situations where
84+
/// the range is used as an iterator, or for indexing.
7985
///
8086
/// ### Example
8187
/// ```no_run
@@ -344,15 +350,132 @@ fn check_range_bounds<'a, 'tcx>(cx: &'a LateContext<'tcx>, ex: &'a Expr<'_>) ->
344350
None
345351
}
346352

353+
/// Check whether `expr` could switch range types without breaking the typing requirements. This is
354+
/// generally the case when `expr` is used as an iterator for example, or as a slice or `&str`
355+
/// index.
356+
///
357+
/// FIXME: Note that the current implementation may still return false positives. A proper fix would
358+
/// check that the obligations are still satisfied after switching the range type.
359+
fn can_switch_ranges<'tcx>(
360+
cx: &LateContext<'tcx>,
361+
expr: &'tcx Expr<'_>,
362+
original: RangeLimits,
363+
inner_ty: Ty<'tcx>,
364+
) -> bool {
365+
let use_ctxt = expr_use_ctxt(cx, expr);
366+
let (Node::Expr(parent_expr), false) = (use_ctxt.node, use_ctxt.is_ty_unified) else {
367+
return false;
368+
};
369+
370+
// Check if `expr` is the argument of a compiler-generated `IntoIter::into_iter(expr)`
371+
if let ExprKind::Call(func, [arg]) = parent_expr.kind
372+
&& arg.hir_id == use_ctxt.child_id
373+
&& is_path_lang_item(cx, func, LangItem::IntoIterIntoIter)
374+
{
375+
return true;
376+
}
377+
378+
// Check if `expr` is used as the receiver of a method of the `Iterator`, `IntoIterator`,
379+
// or `RangeBounds` traits.
380+
if let ExprKind::MethodCall(_, receiver, _, _) = parent_expr.kind
381+
&& receiver.hir_id == use_ctxt.child_id
382+
&& let Some(method_did) = cx.typeck_results().type_dependent_def_id(parent_expr.hir_id)
383+
&& let Some(trait_did) = cx.tcx.trait_of_item(method_did)
384+
&& matches!(
385+
cx.tcx.get_diagnostic_name(trait_did),
386+
Some(sym::Iterator | sym::IntoIterator | sym::RangeBounds)
387+
)
388+
{
389+
return true;
390+
}
391+
392+
// Check if `expr` is an argument of a call which requires an `Iterator`, `IntoIterator`,
393+
// or `RangeBounds` trait.
394+
if let ExprKind::Call(_, args) | ExprKind::MethodCall(_, _, args, _) = parent_expr.kind
395+
&& let Some(id) = fn_def_id(cx, parent_expr)
396+
&& let Some(arg_idx) = args.iter().position(|e| e.hir_id == use_ctxt.child_id)
397+
{
398+
let input_idx = if matches!(parent_expr.kind, ExprKind::MethodCall(..)) {
399+
arg_idx + 1
400+
} else {
401+
arg_idx
402+
};
403+
let inputs = cx
404+
.tcx
405+
.liberate_late_bound_regions(id, cx.tcx.fn_sig(id).instantiate_identity())
406+
.inputs();
407+
let expr_ty = inputs[input_idx];
408+
// Check that the `expr` type is present only once, otherwise modifying just one of them might be
409+
// risky if they are referenced using the same generic type for example.
410+
if inputs.iter().enumerate().all(|(n, ty)|
411+
n == input_idx
412+
|| !ty.walk().any(|arg| matches!(arg.kind(),
413+
GenericArgKind::Type(ty) if ty == expr_ty)))
414+
// Look for a clause requiring `Iterator`, `IntoIterator`, or `RangeBounds`, and resolving to `expr_type`.
415+
&& cx
416+
.tcx
417+
.param_env(id)
418+
.caller_bounds()
419+
.into_iter()
420+
.any(|p| {
421+
if let ClauseKind::Trait(t) = p.kind().skip_binder()
422+
&& t.polarity == PredicatePolarity::Positive
423+
&& matches!(
424+
cx.tcx.get_diagnostic_name(t.trait_ref.def_id),
425+
Some(sym::Iterator | sym::IntoIterator | sym::RangeBounds)
426+
)
427+
{
428+
t.self_ty() == expr_ty
429+
} else {
430+
false
431+
}
432+
})
433+
{
434+
return true;
435+
}
436+
}
437+
438+
// Check if `expr` is used for indexing, and if the switched range type could be used
439+
// as well.
440+
if let ExprKind::Index(outer_expr, index, _) = parent_expr.kind
441+
&& index.hir_id == expr.hir_id
442+
// Build the switched range type (for example `RangeInclusive<usize>`).
443+
&& let Some(switched_range_def_id) = match original {
444+
RangeLimits::HalfOpen => cx.tcx.lang_items().range_inclusive_struct(),
445+
RangeLimits::Closed => cx.tcx.lang_items().range_struct(),
446+
}
447+
&& let switched_range_ty = cx
448+
.tcx
449+
.type_of(switched_range_def_id)
450+
.instantiate(cx.tcx, &[inner_ty.into()])
451+
// Check that the switched range type can be used for indexing the original expression
452+
// through the `Index` or `IndexMut` trait.
453+
&& let ty::Ref(_, outer_ty, mutability) = cx.typeck_results().expr_ty_adjusted(outer_expr).kind()
454+
&& let Some(index_def_id) = match mutability {
455+
Mutability::Not => cx.tcx.lang_items().index_trait(),
456+
Mutability::Mut => cx.tcx.lang_items().index_mut_trait(),
457+
}
458+
&& implements_trait(cx, *outer_ty, index_def_id, &[switched_range_ty.into()])
459+
// We could also check that the associated item of the `index_def_id` trait with the switched range type
460+
// return the same type, but it is reasonable to expect so. We can't check that the result is identical
461+
// in both `Index<Range<…>>` and `Index<RangeInclusive<…>>` anyway.
462+
{
463+
return true;
464+
}
465+
466+
false
467+
}
468+
347469
// exclusive range plus one: `x..(y+1)`
348-
fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
470+
fn check_exclusive_range_plus_one<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
349471
if expr.span.can_be_used_for_suggestions()
350472
&& let Some(higher::Range {
351473
start,
352474
end: Some(end),
353475
limits: RangeLimits::HalfOpen,
354476
}) = higher::Range::hir(expr)
355477
&& let Some(y) = y_plus_one(cx, end)
478+
&& can_switch_ranges(cx, expr, RangeLimits::HalfOpen, cx.typeck_results().expr_ty(y))
356479
{
357480
let span = expr.span;
358481
span_lint_and_then(
@@ -383,14 +506,15 @@ fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
383506
}
384507

385508
// inclusive range minus one: `x..=(y-1)`
386-
fn check_inclusive_range_minus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
509+
fn check_inclusive_range_minus_one<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
387510
if expr.span.can_be_used_for_suggestions()
388511
&& let Some(higher::Range {
389512
start,
390513
end: Some(end),
391514
limits: RangeLimits::Closed,
392515
}) = higher::Range::hir(expr)
393516
&& let Some(y) = y_minus_one(cx, end)
517+
&& can_switch_ranges(cx, expr, RangeLimits::Closed, cx.typeck_results().expr_ty(y))
394518
{
395519
span_lint_and_then(
396520
cx,

tests/ui/range_plus_minus_one.fixed

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
#![warn(clippy::range_minus_one)]
2+
#![warn(clippy::range_plus_one)]
13
#![allow(unused_parens)]
24
#![allow(clippy::iter_with_drain)]
5+
6+
use std::ops::{Index, IndexMut, Range, RangeBounds, RangeInclusive};
7+
38
fn f() -> usize {
49
42
510
}
@@ -20,8 +25,6 @@ macro_rules! macro_minus_one {
2025
};
2126
}
2227

23-
#[warn(clippy::range_plus_one)]
24-
#[warn(clippy::range_minus_one)]
2528
fn main() {
2629
for _ in 0..2 {}
2730
for _ in 0..=2 {}
@@ -45,15 +48,13 @@ fn main() {
4548
//~^ range_plus_one
4649
for _ in 0..=(1 + f()) {}
4750

51+
// Those are not linted, as in the general case we cannot be sure that the exact type won't be
52+
// important.
4853
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
54+
let _ = ..=11 - 1;
55+
let _ = ..=(11 - 1);
56+
let _ = (1..11 + 1);
57+
let _ = (f() + 1)..(f() + 1);
5758

5859
const ONE: usize = 1;
5960
// integer consts are linted, too
@@ -65,4 +66,97 @@ fn main() {
6566

6667
macro_plus_one!(5);
6768
macro_minus_one!(5);
69+
70+
// As an instance of `Iterator`
71+
(1..=10).for_each(|_| {});
72+
//~^ range_plus_one
73+
74+
// As an instance of `IntoIterator`
75+
#[allow(clippy::useless_conversion)]
76+
(1..=10).into_iter().for_each(|_| {});
77+
//~^ range_plus_one
78+
79+
// As an instance of `RangeBounds`
80+
{
81+
let _ = (1..=10).start_bound();
82+
//~^ range_plus_one
83+
}
84+
85+
// As a `SliceIndex`
86+
let a = [10, 20, 30];
87+
let _ = &a[1..=1];
88+
//~^ range_plus_one
89+
90+
// As method call argument
91+
vec.drain(2..=3);
92+
//~^ range_plus_one
93+
94+
// As function call argument
95+
take_arg(10..=20);
96+
//~^ range_plus_one
97+
98+
// As function call argument inside a block
99+
take_arg({ 10..=20 });
100+
//~^ range_plus_one
101+
102+
// Do not lint in case types are unified
103+
take_arg(if true { 10..20 } else { 10..20 + 1 });
104+
105+
// Do not lint, as the same type is used for both parameters
106+
take_args(10..20 + 1, 10..21);
107+
108+
// Do not lint, as the range type is also used indirectly in second parameter
109+
take_arg_and_struct(10..20 + 1, S { t: 1..2 });
110+
111+
// As target of `IndexMut`
112+
let mut a = [10, 20, 30];
113+
a[0..=2][0] = 1;
114+
//~^ range_plus_one
115+
}
116+
117+
fn take_arg<T: Iterator<Item = u32>>(_: T) {}
118+
fn take_args<T: Iterator<Item = u32>>(_: T, _: T) {}
119+
120+
struct S<T> {
121+
t: T,
122+
}
123+
fn take_arg_and_struct<T: Iterator<Item = u32>>(_: T, _: S<T>) {}
124+
125+
fn no_index_by_range_inclusive(a: usize) {
126+
struct S;
127+
128+
impl Index<Range<usize>> for S {
129+
type Output = [u32];
130+
fn index(&self, _: Range<usize>) -> &Self::Output {
131+
&[]
132+
}
133+
}
134+
135+
_ = &S[0..a + 1];
136+
}
137+
138+
fn no_index_mut_with_switched_range(a: usize) {
139+
struct S(u32);
140+
141+
impl Index<Range<usize>> for S {
142+
type Output = u32;
143+
fn index(&self, _: Range<usize>) -> &Self::Output {
144+
&self.0
145+
}
146+
}
147+
148+
impl IndexMut<Range<usize>> for S {
149+
fn index_mut(&mut self, _: Range<usize>) -> &mut Self::Output {
150+
&mut self.0
151+
}
152+
}
153+
154+
impl Index<RangeInclusive<usize>> for S {
155+
type Output = u32;
156+
fn index(&self, _: RangeInclusive<usize>) -> &Self::Output {
157+
&self.0
158+
}
159+
}
160+
161+
S(2)[0..a + 1] = 3;
68162
}

0 commit comments

Comments
 (0)