@@ -3,8 +3,9 @@ use std::iter;
3
3
use std:: ops:: { Deref , Range } ;
4
4
5
5
use 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} ;
7
7
use rustc_ast:: ast:: { Expr , ExprKind , Impl , Item , ItemKind , MacCall , Path , StrLit , StrStyle } ;
8
+ use rustc_ast:: ptr:: P ;
8
9
use rustc_ast:: token:: { self , LitKind } ;
9
10
use rustc_ast:: tokenstream:: TokenStream ;
10
11
use rustc_errors:: { Applicability , DiagnosticBuilder } ;
@@ -256,6 +257,28 @@ declare_clippy_lint! {
256
257
"writing a literal with a format string"
257
258
}
258
259
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
+
259
282
#[ derive( Default ) ]
260
283
pub struct Write {
261
284
in_debug_impl : bool ,
@@ -270,7 +293,8 @@ impl_lint_pass!(Write => [
270
293
PRINT_LITERAL ,
271
294
WRITE_WITH_NEWLINE ,
272
295
WRITELN_EMPTY_STRING ,
273
- WRITE_LITERAL
296
+ WRITE_LITERAL ,
297
+ POSITIONAL_NAMED_FORMAT_PARAMETERS ,
274
298
] ) ;
275
299
276
300
impl EarlyLintPass for Write {
@@ -408,6 +432,7 @@ fn newline_span(fmtstr: &StrLit) -> (Span, bool) {
408
432
#[ derive( Default ) ]
409
433
struct SimpleFormatArgs {
410
434
unnamed : Vec < Vec < Span > > ,
435
+ complex_unnamed : Vec < Vec < Span > > ,
411
436
named : Vec < ( Symbol , Vec < Span > ) > ,
412
437
}
413
438
impl SimpleFormatArgs {
@@ -419,6 +444,10 @@ impl SimpleFormatArgs {
419
444
} )
420
445
}
421
446
447
+ fn get_complex_unnamed ( & self ) -> impl Iterator < Item = & [ Span ] > {
448
+ self . complex_unnamed . iter ( ) . map ( Vec :: as_slice)
449
+ }
450
+
422
451
fn get_named ( & self , n : & Path ) -> & [ Span ] {
423
452
self . named . iter ( ) . find ( |x| * n == x. 0 ) . map_or ( & [ ] , |x| x. 1 . as_slice ( ) )
424
453
}
@@ -479,6 +508,61 @@ impl SimpleFormatArgs {
479
508
} ,
480
509
} ;
481
510
}
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
+ }
482
566
}
483
567
484
568
impl Write {
@@ -511,8 +595,8 @@ impl Write {
511
595
// FIXME: modify rustc's fmt string parser to give us the current span
512
596
span_lint ( cx, USE_DEBUG , span, "use of `Debug`-based formatting" ) ;
513
597
}
514
-
515
598
args. push ( arg, span) ;
599
+ args. push_complex ( cx, arg, str_lit. span , span) ;
516
600
}
517
601
518
602
parser. errors . is_empty ( ) . then_some ( args)
@@ -566,6 +650,7 @@ impl Write {
566
650
567
651
let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL } ;
568
652
let mut unnamed_args = args. get_unnamed ( ) ;
653
+ let mut complex_unnamed_args = args. get_complex_unnamed ( ) ;
569
654
loop {
570
655
if !parser. eat ( & token:: Comma ) {
571
656
return ( Some ( fmtstr) , expr) ;
@@ -577,11 +662,20 @@ impl Write {
577
662
} else {
578
663
return ( Some ( fmtstr) , None ) ;
579
664
} ;
665
+ let complex_unnamed_arg = complex_unnamed_args. next ( ) ;
666
+
580
667
let ( fmt_spans, lit) = match & token_expr. kind {
581
668
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
+ }
585
679
} ,
586
680
_ => {
587
681
unnamed_args. next ( ) ;
@@ -637,6 +731,29 @@ impl Write {
637
731
}
638
732
}
639
733
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
+
640
757
fn lint_println_empty_string ( & self , cx : & EarlyContext < ' _ > , mac : & MacCall ) {
641
758
if let ( Some ( fmt_str) , _) = self . check_tts ( cx, mac. args . inner_tokens ( ) , false ) {
642
759
if fmt_str. symbol == kw:: Empty {
0 commit comments