Skip to content

Commit fa2f840

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 da7b678 commit fa2f840

File tree

7 files changed

+359
-57
lines changed

7 files changed

+359
-57
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
@@ -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

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: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ 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+
fn_def_id, get_parent_expr, higher, is_in_const_context, is_integer_const, is_path_lang_item, path_to_local,
10+
};
11+
use rustc_ast::Mutability;
812
use rustc_ast::ast::RangeLimits;
913
use rustc_errors::Applicability;
10-
use rustc_hir::{BinOpKind, Expr, ExprKind, HirId};
14+
use rustc_hir::{BinOpKind, Expr, ExprKind, HirId, LangItem};
1115
use rustc_lint::{LateContext, LateLintPass};
12-
use rustc_middle::ty;
16+
use rustc_middle::ty::{self, ClauseKind, GenericArgKind, PredicatePolarity, Ty};
1317
use rustc_session::impl_lint_pass;
14-
use rustc_span::Span;
1518
use rustc_span::source_map::Spanned;
19+
use rustc_span::{Span, sym};
1620
use std::cmp::Ordering;
1721

1822
declare_clippy_lint! {
@@ -24,6 +28,12 @@ declare_clippy_lint! {
2428
/// The code is more readable with an inclusive range
2529
/// like `x..=y`.
2630
///
31+
/// ### Limitations
32+
/// The lint is conservative and will trigger only when switching
33+
/// from an exclusive to an inclusive range is provably safe from
34+
/// a typing point of view. This corresponds to situations where
35+
/// the range is used as an iterator, or for indexing.
36+
///
2737
/// ### Known problems
2838
/// Will add unnecessary pair of parentheses when the
2939
/// expression is not wrapped in a pair but starts with an opening parenthesis
@@ -34,11 +44,6 @@ declare_clippy_lint! {
3444
/// exclusive ranges, because they essentially add an extra branch that
3545
/// LLVM may fail to hoist out of the loop.
3646
///
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-
///
4247
/// ### Example
4348
/// ```no_run
4449
/// # let x = 0;
@@ -71,11 +76,11 @@ declare_clippy_lint! {
7176
/// The code is more readable with an exclusive range
7277
/// like `x..y`.
7378
///
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)).
79+
/// ### Limitations
80+
/// The lint is conservative and will trigger only when switching
81+
/// from an inclusive to an exclusive range is provably safe from
82+
/// a typing point of view. This corresponds to situations where
83+
/// the range is used as an iterator, or for indexing.
7984
///
8085
/// ### Example
8186
/// ```no_run
@@ -344,6 +349,115 @@ fn check_range_bounds<'a, 'tcx>(cx: &'a LateContext<'tcx>, ex: &'a Expr<'_>) ->
344349
None
345350
}
346351

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

tests/ui/range_plus_minus_one.fixed

Lines changed: 97 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,90 @@ 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+
// Do not lint, as the same type is used for both parameters
99+
take_args(10..20 + 1, 10..21);
100+
101+
// Do not lint, as the range type is also used indirectly in second parameter
102+
take_arg_and_struct(10..20 + 1, S { t: 1..2 });
103+
104+
// As target of `IndexMut`
105+
let mut a = [10, 20, 30];
106+
a[0..=2][0] = 1;
107+
//~^ range_plus_one
108+
}
109+
110+
fn take_arg<T: Iterator<Item = u32>>(_: T) {}
111+
fn take_args<T: Iterator<Item = u32>>(_: T, _: T) {}
112+
113+
struct S<T> {
114+
t: T,
115+
}
116+
fn take_arg_and_struct<T: Iterator<Item = u32>>(_: T, _: S<T>) {}
117+
118+
fn no_index_by_range_inclusive(a: usize) {
119+
struct S;
120+
121+
impl Index<Range<usize>> for S {
122+
type Output = [u32];
123+
fn index(&self, _: Range<usize>) -> &Self::Output {
124+
&[]
125+
}
126+
}
127+
128+
_ = &S[0..a + 1];
129+
}
130+
131+
fn no_index_mut_with_switched_range(a: usize) {
132+
struct S(u32);
133+
134+
impl Index<Range<usize>> for S {
135+
type Output = u32;
136+
fn index(&self, _: Range<usize>) -> &Self::Output {
137+
&self.0
138+
}
139+
}
140+
141+
impl IndexMut<Range<usize>> for S {
142+
fn index_mut(&mut self, _: Range<usize>) -> &mut Self::Output {
143+
&mut self.0
144+
}
145+
}
146+
147+
impl Index<RangeInclusive<usize>> for S {
148+
type Output = u32;
149+
fn index(&self, _: RangeInclusive<usize>) -> &Self::Output {
150+
&self.0
151+
}
152+
}
153+
154+
S(2)[0..a + 1] = 3;
68155
}

0 commit comments

Comments
 (0)