@@ -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