@@ -4,15 +4,20 @@ use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_the
44use clippy_utils:: msrvs:: { self , Msrv } ;
55use clippy_utils:: source:: { SpanRangeExt , snippet, snippet_with_applicability} ;
66use 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 ;
813use rustc_ast:: ast:: RangeLimits ;
914use 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 } ;
1318use rustc_session:: impl_lint_pass;
14- use rustc_span:: Span ;
1519use rustc_span:: source_map:: Spanned ;
20+ use rustc_span:: { Span , sym} ;
1621use std:: cmp:: Ordering ;
1722
1823declare_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