@@ -4,15 +4,20 @@ use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_the
4
4
use clippy_utils:: msrvs:: { self , Msrv } ;
5
5
use clippy_utils:: source:: { SpanRangeExt , snippet, snippet_with_applicability} ;
6
6
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 ;
8
13
use rustc_ast:: ast:: RangeLimits ;
9
14
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 } ;
13
18
use rustc_session:: impl_lint_pass;
14
- use rustc_span:: Span ;
15
19
use rustc_span:: source_map:: Spanned ;
20
+ use rustc_span:: { Span , sym} ;
16
21
use std:: cmp:: Ordering ;
17
22
18
23
declare_clippy_lint ! {
@@ -24,6 +29,12 @@ declare_clippy_lint! {
24
29
/// The code is more readable with an inclusive range
25
30
/// like `x..=y`.
26
31
///
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
+ ///
27
38
/// ### Known problems
28
39
/// Will add unnecessary pair of parentheses when the
29
40
/// expression is not wrapped in a pair but starts with an opening parenthesis
@@ -34,11 +45,6 @@ declare_clippy_lint! {
34
45
/// exclusive ranges, because they essentially add an extra branch that
35
46
/// LLVM may fail to hoist out of the loop.
36
47
///
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
- ///
42
48
/// ### Example
43
49
/// ```no_run
44
50
/// # let x = 0;
@@ -71,11 +77,11 @@ declare_clippy_lint! {
71
77
/// The code is more readable with an exclusive range
72
78
/// like `x..y`.
73
79
///
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 .
79
85
///
80
86
/// ### Example
81
87
/// ```no_run
@@ -344,70 +350,188 @@ fn check_range_bounds<'a, 'tcx>(cx: &'a LateContext<'tcx>, ex: &'a Expr<'_>) ->
344
350
None
345
351
}
346
352
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 )
356
374
{
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 ;
382
464
}
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
+ ) ;
383
480
}
384
481
385
482
// 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
+ ) {
387
506
if expr. span . can_be_used_for_suggestions ( )
388
507
&& let Some ( higher:: Range {
389
508
start,
390
509
end : Some ( end) ,
391
- limits : RangeLimits :: Closed ,
510
+ limits,
392
511
} ) = 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) )
394
515
{
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
+ } ) ;
411
535
}
412
536
}
413
537
@@ -494,7 +618,7 @@ fn check_reversed_empty_range(cx: &LateContext<'_>, expr: &Expr<'_>) {
494
618
}
495
619
}
496
620
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 > > {
498
622
match expr. kind {
499
623
ExprKind :: Binary (
500
624
Spanned {
@@ -515,7 +639,7 @@ fn y_plus_one<'t>(cx: &LateContext<'_>, expr: &'t Expr<'_>) -> Option<&'t Expr<'
515
639
}
516
640
}
517
641
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 > > {
519
643
match expr. kind {
520
644
ExprKind :: Binary (
521
645
Spanned {
0 commit comments