@@ -3,8 +3,9 @@ use std::iter;
33use  std:: ops:: { Deref ,  Range } ; 
44
55use  clippy_utils:: diagnostics:: { span_lint,  span_lint_and_sugg,  span_lint_and_then} ; 
6- use  clippy_utils:: source:: { snippet_opt,  snippet_with_applicability} ; 
6+ use  clippy_utils:: source:: { snippet ,   snippet_opt,  snippet_with_applicability} ; 
77use  rustc_ast:: ast:: { Expr ,  ExprKind ,  Impl ,  Item ,  ItemKind ,  MacCall ,  Path ,  StrLit ,  StrStyle } ; 
8+ use  rustc_ast:: ptr:: P ; 
89use  rustc_ast:: token:: { self ,  LitKind } ; 
910use  rustc_ast:: tokenstream:: TokenStream ; 
1011use  rustc_errors:: { Applicability ,  DiagnosticBuilder } ; 
@@ -256,6 +257,28 @@ declare_clippy_lint! {
256257    "writing a literal with a format string" 
257258} 
258259
260+ declare_clippy_lint !  { 
261+     /// ### What it does 
262+ /// This lint warns when a named parameter in a format string is used as a positional one. 
263+ /// 
264+ /// ### Why is this bad? 
265+ /// It may be confused for an assignment and obfuscates which parameter is being used. 
266+ /// 
267+ /// ### Example 
268+ /// ```rust 
269+ /// println!("{}", x = 10); 
270+ /// ``` 
271+ /// 
272+ /// Use instead: 
273+ /// ```rust 
274+ /// println!("{x}", x = 10); 
275+ /// ``` 
276+ [ clippy:: version = "1.63.0" ] 
277+     pub  POSITIONAL_NAMED_FORMAT_PARAMETERS , 
278+     suspicious, 
279+     "named parameter in a format string is used positionally" 
280+ } 
281+ 
259282#[ derive( Default ) ]  
260283pub  struct  Write  { 
261284    in_debug_impl :  bool , 
@@ -270,7 +293,8 @@ impl_lint_pass!(Write => [
270293    PRINT_LITERAL , 
271294    WRITE_WITH_NEWLINE , 
272295    WRITELN_EMPTY_STRING , 
273-     WRITE_LITERAL 
296+     WRITE_LITERAL , 
297+     POSITIONAL_NAMED_FORMAT_PARAMETERS , 
274298] ) ; 
275299
276300impl  EarlyLintPass  for  Write  { 
@@ -408,6 +432,7 @@ fn newline_span(fmtstr: &StrLit) -> (Span, bool) {
408432#[ derive( Default ) ]  
409433struct  SimpleFormatArgs  { 
410434    unnamed :  Vec < Vec < Span > > , 
435+     complex_unnamed :  Vec < Vec < Span > > , 
411436    named :  Vec < ( Symbol ,  Vec < Span > ) > , 
412437} 
413438impl  SimpleFormatArgs  { 
@@ -419,6 +444,10 @@ impl SimpleFormatArgs {
419444        } ) 
420445    } 
421446
447+     fn  get_complex_unnamed ( & self )  -> impl  Iterator < Item  = & [ Span ] >  { 
448+         self . complex_unnamed . iter ( ) . map ( Vec :: as_slice) 
449+     } 
450+ 
422451    fn  get_named ( & self ,  n :  & Path )  -> & [ Span ]  { 
423452        self . named . iter ( ) . find ( |x| * n == x. 0 ) . map_or ( & [ ] ,  |x| x. 1 . as_slice ( ) ) 
424453    } 
@@ -479,6 +508,61 @@ impl SimpleFormatArgs {
479508            } , 
480509        } ; 
481510    } 
511+ 
512+     fn  push_to_complex ( & mut  self ,  span :  Span ,  position :  usize )  { 
513+         if  self . complex_unnamed . len ( )  <= position { 
514+             self . complex_unnamed . resize_with ( position,  Vec :: new) ; 
515+             self . complex_unnamed . push ( vec ! [ span] ) ; 
516+         }  else  { 
517+             let  args:  & mut  Vec < Span >  = & mut  self . complex_unnamed [ position] ; 
518+             args. push ( span) ; 
519+         } 
520+     } 
521+ 
522+     fn  push_complex ( 
523+         & mut  self , 
524+         cx :  & EarlyContext < ' _ > , 
525+         arg :  rustc_parse_format:: Argument < ' _ > , 
526+         str_lit_span :  Span , 
527+         fmt_span :  Span , 
528+     )  { 
529+         use  rustc_parse_format:: { ArgumentImplicitlyIs ,  ArgumentIs ,  CountIsParam } ; 
530+ 
531+         let  snippet = snippet_opt ( cx,  fmt_span) ; 
532+ 
533+         let  end = snippet
534+             . as_ref ( ) 
535+             . and_then ( |s| s. find ( ':' ) ) 
536+             . or_else ( || fmt_span. hi ( ) . 0 . checked_sub ( fmt_span. lo ( ) . 0  + 1 ) . map ( |u| u as  usize ) ) ; 
537+ 
538+         if  let  ( ArgumentIs ( n)  | ArgumentImplicitlyIs ( n) ,  Some ( end) )  = ( arg. position ,  end)  { 
539+             let  span = fmt_span. from_inner ( InnerSpan :: new ( 1 ,  end) ) ; 
540+             self . push_to_complex ( span,  n) ; 
541+         } ; 
542+ 
543+         if  let  ( CountIsParam ( n) ,  Some ( span) )  = ( arg. format . precision ,  arg. format . precision_span )  { 
544+             // We need to do this hack as precision spans should be converted from .* to .foo$ 
545+             let  hack = if  snippet. as_ref ( ) . and_then ( |s| s. find ( '*' ) ) . is_some ( )  { 
546+                 0 
547+             }  else  { 
548+                 1 
549+             } ; 
550+ 
551+             let  span = str_lit_span. from_inner ( InnerSpan  { 
552+                 start :  span. start  + 1 , 
553+                 end :  span. end  - hack, 
554+             } ) ; 
555+             self . push_to_complex ( span,  n) ; 
556+         } ; 
557+ 
558+         if  let  ( CountIsParam ( n) ,  Some ( span) )  = ( arg. format . width ,  arg. format . width_span )  { 
559+             let  span = str_lit_span. from_inner ( InnerSpan  { 
560+                 start :  span. start , 
561+                 end :  span. end  - 1 , 
562+             } ) ; 
563+             self . push_to_complex ( span,  n) ; 
564+         } ; 
565+     } 
482566} 
483567
484568impl  Write  { 
@@ -511,8 +595,8 @@ impl Write {
511595                // FIXME: modify rustc's fmt string parser to give us the current span 
512596                span_lint ( cx,  USE_DEBUG ,  span,  "use of `Debug`-based formatting" ) ; 
513597            } 
514- 
515598            args. push ( arg,  span) ; 
599+             args. push_complex ( cx,  arg,  str_lit. span ,  span) ; 
516600        } 
517601
518602        parser. errors . is_empty ( ) . then_some ( args) 
@@ -566,6 +650,7 @@ impl Write {
566650
567651        let  lint = if  is_write {  WRITE_LITERAL  }  else  {  PRINT_LITERAL  } ; 
568652        let  mut  unnamed_args = args. get_unnamed ( ) ; 
653+         let  mut  complex_unnamed_args = args. get_complex_unnamed ( ) ; 
569654        loop  { 
570655            if  !parser. eat ( & token:: Comma )  { 
571656                return  ( Some ( fmtstr) ,  expr) ; 
@@ -577,11 +662,20 @@ impl Write {
577662            }  else  { 
578663                return  ( Some ( fmtstr) ,  None ) ; 
579664            } ; 
665+             let  complex_unnamed_arg = complex_unnamed_args. next ( ) ; 
666+ 
580667            let  ( fmt_spans,  lit)  = match  & token_expr. kind  { 
581668                ExprKind :: Lit ( lit)  => ( unnamed_args. next ( ) . unwrap_or ( & [ ] ) ,  lit) , 
582-                 ExprKind :: Assign ( lhs,  rhs,  _)  => match  ( & lhs. kind ,  & rhs. kind )  { 
583-                     ( ExprKind :: Path ( _,  p) ,  ExprKind :: Lit ( lit) )  => ( args. get_named ( p) ,  lit) , 
584-                     _ => continue , 
669+                 ExprKind :: Assign ( lhs,  rhs,  _)  => { 
670+                     if  let  Some ( span)  = complex_unnamed_arg { 
671+                         for  x in  span { 
672+                             Self :: report_positional_named_param ( cx,  * x,  lhs,  rhs) ; 
673+                         } 
674+                     } 
675+                     match  ( & lhs. kind ,  & rhs. kind )  { 
676+                         ( ExprKind :: Path ( _,  p) ,  ExprKind :: Lit ( lit) )  => ( args. get_named ( p) ,  lit) , 
677+                         _ => continue , 
678+                     } 
585679                } , 
586680                _ => { 
587681                    unnamed_args. next ( ) ; 
@@ -637,6 +731,29 @@ impl Write {
637731        } 
638732    } 
639733
734+     fn  report_positional_named_param ( cx :  & EarlyContext < ' _ > ,  span :  Span ,  lhs :  & P < Expr > ,  _rhs :  & P < Expr > )  { 
735+         if  let  ExprKind :: Path ( _,  _p)  = & lhs. kind  { 
736+             let  mut  applicability = Applicability :: MachineApplicable ; 
737+             let  name = snippet_with_applicability ( cx,  lhs. span ,  "name" ,  & mut  applicability) ; 
738+             // We need to do this hack as precision spans should be converted from .* to .foo$ 
739+             let  hack = snippet ( cx,  span,  "" ) . contains ( '*' ) ; 
740+ 
741+             span_lint_and_sugg ( 
742+                 cx, 
743+                 POSITIONAL_NAMED_FORMAT_PARAMETERS , 
744+                 span, 
745+                 & format ! ( "named parameter {} is used as a positional parameter" ,  name) , 
746+                 "replace it with" , 
747+                 if  hack { 
748+                     format ! ( "{}$" ,  name) 
749+                 }  else  { 
750+                     format ! ( "{}" ,  name) 
751+                 } , 
752+                 applicability, 
753+             ) ; 
754+         } ; 
755+     } 
756+ 
640757    fn  lint_println_empty_string ( & self ,  cx :  & EarlyContext < ' _ > ,  mac :  & MacCall )  { 
641758        if  let  ( Some ( fmt_str) ,  _)  = self . check_tts ( cx,  mac. args . inner_tokens ( ) ,  false )  { 
642759            if  fmt_str. symbol  == kw:: Empty  { 
0 commit comments