Skip to content

Commit e113e66

Browse files
authored
Propose to exchange ranges only when it is safe to do so (rust-lang#14432)
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. In a second commit, the `range_plus_one` and `range_minus_one` logic are unified, in order to properly emit parentheses around the suggestion when needed. Fix rust-lang/rust-clippy#3307 Fix rust-lang/rust-clippy#9908 changelog: [`range_plus_one`, `range_minus_one`]: restrict lint to cases where it is safe to switch the range type *Edit:* as a consequence, this led to the removal of three `#[expect(clippy::range_plus_one)]` in the Clippy sources to avoid those false positives.
2 parents 2a4c83d + 15fc993 commit e113e66

File tree

7 files changed

+492
-113
lines changed

7 files changed

+492
-113
lines changed

clippy_lints/src/cognitive_complexity.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ impl CognitiveComplexity {
110110
FnKind::ItemFn(ident, _, _) | FnKind::Method(ident, _) => ident.span,
111111
FnKind::Closure => {
112112
let header_span = body_span.with_hi(decl.output.span().lo());
113-
#[expect(clippy::range_plus_one)]
114113
if let Some(range) = header_span.map_range(cx, |_, src, range| {
115114
let mut idxs = src.get(range.clone())?.match_indices('|');
116115
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: 194 additions & 70 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};
11-
use rustc_lint::{LateContext, LateLintPass};
12-
use rustc_middle::ty;
15+
use rustc_hir::{BinOpKind, Expr, ExprKind, HirId, LangItem, Node};
16+
use rustc_lint::{LateContext, LateLintPass, Lint};
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,70 +350,188 @@ fn check_range_bounds<'a, 'tcx>(cx: &'a LateContext<'tcx>, ex: &'a Expr<'_>) ->
344350
None
345351
}
346352

347-
// exclusive range plus one: `x..(y+1)`
348-
fn check_exclusive_range_plus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
349-
if expr.span.can_be_used_for_suggestions()
350-
&& let Some(higher::Range {
351-
start,
352-
end: Some(end),
353-
limits: RangeLimits::HalfOpen,
354-
}) = higher::Range::hir(expr)
355-
&& let Some(y) = y_plus_one(cx, end)
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)
356374
{
357-
let span = expr.span;
358-
span_lint_and_then(
359-
cx,
360-
RANGE_PLUS_ONE,
361-
span,
362-
"an inclusive range would be more readable",
363-
|diag| {
364-
let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").maybe_paren().to_string());
365-
let end = Sugg::hir(cx, y, "y").maybe_paren();
366-
match span.with_source_text(cx, |src| src.starts_with('(') && src.ends_with(')')) {
367-
Some(true) => {
368-
diag.span_suggestion(span, "use", format!("({start}..={end})"), Applicability::MaybeIncorrect);
369-
},
370-
Some(false) => {
371-
diag.span_suggestion(
372-
span,
373-
"use",
374-
format!("{start}..={end}"),
375-
Applicability::MachineApplicable, // snippet
376-
);
377-
},
378-
None => {},
379-
}
380-
},
381-
);
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;
382464
}
465+
466+
false
467+
}
468+
469+
// exclusive range plus one: `x..(y+1)`
470+
fn check_exclusive_range_plus_one<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
471+
check_range_switch(
472+
cx,
473+
expr,
474+
RangeLimits::HalfOpen,
475+
y_plus_one,
476+
RANGE_PLUS_ONE,
477+
"an inclusive range would be more readable",
478+
"..=",
479+
);
383480
}
384481

385482
// inclusive range minus one: `x..=(y-1)`
386-
fn check_inclusive_range_minus_one(cx: &LateContext<'_>, expr: &Expr<'_>) {
483+
fn check_inclusive_range_minus_one<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
484+
check_range_switch(
485+
cx,
486+
expr,
487+
RangeLimits::Closed,
488+
y_minus_one,
489+
RANGE_MINUS_ONE,
490+
"an exclusive range would be more readable",
491+
"..",
492+
);
493+
}
494+
495+
/// Check for a `kind` of range in `expr`, check for `predicate` on the end,
496+
/// and emit the `lint` with `msg` and the `operator`.
497+
fn check_range_switch<'tcx>(
498+
cx: &LateContext<'tcx>,
499+
expr: &'tcx Expr<'_>,
500+
kind: RangeLimits,
501+
predicate: impl for<'hir> FnOnce(&LateContext<'_>, &Expr<'hir>) -> Option<&'hir Expr<'hir>>,
502+
lint: &'static Lint,
503+
msg: &'static str,
504+
operator: &str,
505+
) {
387506
if expr.span.can_be_used_for_suggestions()
388507
&& let Some(higher::Range {
389508
start,
390509
end: Some(end),
391-
limits: RangeLimits::Closed,
510+
limits,
392511
}) = higher::Range::hir(expr)
393-
&& let Some(y) = y_minus_one(cx, end)
512+
&& limits == kind
513+
&& let Some(y) = predicate(cx, end)
514+
&& can_switch_ranges(cx, expr, kind, cx.typeck_results().expr_ty(y))
394515
{
395-
span_lint_and_then(
396-
cx,
397-
RANGE_MINUS_ONE,
398-
expr.span,
399-
"an exclusive range would be more readable",
400-
|diag| {
401-
let start = start.map_or(String::new(), |x| Sugg::hir(cx, x, "x").maybe_paren().to_string());
402-
let end = Sugg::hir(cx, y, "y").maybe_paren();
403-
diag.span_suggestion(
404-
expr.span,
405-
"use",
406-
format!("{start}..{end}"),
407-
Applicability::MachineApplicable, // snippet
408-
);
409-
},
410-
);
516+
let span = expr.span;
517+
span_lint_and_then(cx, lint, span, msg, |diag| {
518+
let mut app = Applicability::MachineApplicable;
519+
let start = start.map_or(String::new(), |x| {
520+
Sugg::hir_with_applicability(cx, x, "<x>", &mut app)
521+
.maybe_paren()
522+
.to_string()
523+
});
524+
let end = Sugg::hir_with_applicability(cx, y, "<y>", &mut app).maybe_paren();
525+
match span.with_source_text(cx, |src| src.starts_with('(') && src.ends_with(')')) {
526+
Some(true) => {
527+
diag.span_suggestion(span, "use", format!("({start}{operator}{end})"), app);
528+
},
529+
Some(false) => {
530+
diag.span_suggestion(span, "use", format!("{start}{operator}{end}"), app);
531+
},
532+
None => {},
533+
}
534+
});
411535
}
412536
}
413537

@@ -494,7 +618,7 @@ fn check_reversed_empty_range(cx: &LateContext<'_>, expr: &Expr<'_>) {
494618
}
495619
}
496620

497-
fn y_plus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> {
621+
fn y_plus_one<'tcx>(cx: &LateContext<'_>, expr: &Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
498622
match expr.kind {
499623
ExprKind::Binary(
500624
Spanned {
@@ -515,7 +639,7 @@ fn y_plus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'
515639
}
516640
}
517641

518-
fn y_minus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'t>> {
642+
fn y_minus_one<'tcx>(cx: &LateContext<'_>, expr: &Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
519643
match expr.kind {
520644
ExprKind::Binary(
521645
Spanned {

0 commit comments

Comments
 (0)