@@ -3,12 +3,15 @@ use clippy_utils::macros::{find_format_args, format_arg_removal_span, root_macro
33use clippy_utils:: source:: { expand_past_previous_comma, snippet_opt} ;
44use clippy_utils:: { is_in_cfg_test, is_in_test_function} ;
55use rustc_ast:: token:: LitKind ;
6- use rustc_ast:: { FormatArgPosition , FormatArgs , FormatArgsPiece , FormatOptions , FormatPlaceholder , FormatTrait } ;
6+ use rustc_ast:: {
7+ FormatArgPosition , FormatArgPositionKind , FormatArgs , FormatArgsPiece , FormatOptions , FormatPlaceholder ,
8+ FormatTrait ,
9+ } ;
710use rustc_errors:: Applicability ;
811use rustc_hir:: { Expr , Impl , Item , ItemKind } ;
912use rustc_lint:: { LateContext , LateLintPass , LintContext } ;
1013use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
11- use rustc_span:: { sym, BytePos } ;
14+ use rustc_span:: { sym, BytePos , Span } ;
1215
1316declare_clippy_lint ! {
1417 /// ### What it does
@@ -450,13 +453,25 @@ fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call
450453fn check_literal ( cx : & LateContext < ' _ > , format_args : & FormatArgs , name : & str ) {
451454 let arg_index = |argument : & FormatArgPosition | argument. index . unwrap_or_else ( |pos| pos) ;
452455
456+ let lint_name = if name. starts_with ( "write" ) {
457+ WRITE_LITERAL
458+ } else {
459+ PRINT_LITERAL
460+ } ;
461+
453462 let mut counts = vec ! [ 0u32 ; format_args. arguments. all_args( ) . len( ) ] ;
454463 for piece in & format_args. template {
455464 if let FormatArgsPiece :: Placeholder ( placeholder) = piece {
456465 counts[ arg_index ( & placeholder. argument ) ] += 1 ;
457466 }
458467 }
459468
469+ let mut suggestion: Vec < ( Span , String ) > = vec ! [ ] ;
470+ // holds index of replaced positional arguments; used to decrement the index of the remaining
471+ // positional arguments.
472+ let mut replaced_position: Vec < usize > = vec ! [ ] ;
473+ let mut sug_span: Option < Span > = None ;
474+
460475 for piece in & format_args. template {
461476 if let FormatArgsPiece :: Placeholder ( FormatPlaceholder {
462477 argument,
@@ -493,12 +508,6 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
493508 _ => continue ,
494509 } ;
495510
496- let lint = if name. starts_with ( "write" ) {
497- WRITE_LITERAL
498- } else {
499- PRINT_LITERAL
500- } ;
501-
502511 let Some ( format_string_snippet) = snippet_opt ( cx, format_args. span ) else { continue } ;
503512 let format_string_is_raw = format_string_snippet. starts_with ( 'r' ) ;
504513
@@ -519,29 +528,58 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
519528 } ,
520529 } ;
521530
522- span_lint_and_then (
523- cx,
524- lint,
525- arg. expr . span ,
526- "literal with an empty format string" ,
527- |diag| {
528- if let Some ( replacement) = replacement
529- // `format!("{}", "a")`, `format!("{named}", named = "b")
530- // ~~~~~ ~~~~~~~~~~~~~
531- && let Some ( removal_span) = format_arg_removal_span ( format_args, index)
532- {
533- let replacement = replacement. replace ( '{' , "{{" ) . replace ( '}' , "}}" ) ;
534- diag. multipart_suggestion (
535- "try" ,
536- vec ! [ ( * placeholder_span, replacement) , ( removal_span, String :: new( ) ) ] ,
537- Applicability :: MachineApplicable ,
538- ) ;
539- }
540- } ,
541- ) ;
531+ sug_span = Some ( sug_span. unwrap_or ( arg. expr . span ) . to ( arg. expr . span ) ) ;
542532
533+ if let Some ( ( _, index) ) = positional_arg_piece_span ( piece) {
534+ replaced_position. push ( index) ;
535+ }
536+
537+ if let Some ( replacement) = replacement
538+ // `format!("{}", "a")`, `format!("{named}", named = "b")
539+ // ~~~~~ ~~~~~~~~~~~~~
540+ && let Some ( removal_span) = format_arg_removal_span ( format_args, index) {
541+ let replacement = replacement. replace ( '{' , "{{" ) . replace ( '}' , "}}" ) ;
542+ suggestion. push ( ( * placeholder_span, replacement) ) ;
543+ suggestion. push ( ( removal_span, String :: new ( ) ) ) ;
544+ }
543545 }
544546 }
547+
548+ // Decrement the index of the remaining by the number of replaced positional arguments
549+ if !suggestion. is_empty ( ) {
550+ for piece in & format_args. template {
551+ if let Some ( ( span, index) ) = positional_arg_piece_span ( piece)
552+ && suggestion. iter ( ) . all ( |( s, _) | * s != span) {
553+ let decrement = replaced_position. iter ( ) . filter ( |i| * * i < index) . count ( ) ;
554+ suggestion. push ( ( span, format ! ( "{{{}}}" , index. saturating_sub( decrement) ) ) ) ;
555+ }
556+ }
557+ }
558+
559+ if let Some ( span) = sug_span {
560+ span_lint_and_then ( cx, lint_name, span, "literal with an empty format string" , |diag| {
561+ if !suggestion. is_empty ( ) {
562+ diag. multipart_suggestion ( "try" , suggestion, Applicability :: MachineApplicable ) ;
563+ }
564+ } ) ;
565+ }
566+ }
567+
568+ /// Extract Span and its index from the given `piece`, iff it's positional argument.
569+ fn positional_arg_piece_span ( piece : & FormatArgsPiece ) -> Option < ( Span , usize ) > {
570+ match piece {
571+ FormatArgsPiece :: Placeholder ( FormatPlaceholder {
572+ argument :
573+ FormatArgPosition {
574+ index : Ok ( index) ,
575+ kind : FormatArgPositionKind :: Number ,
576+ ..
577+ } ,
578+ span : Some ( span) ,
579+ ..
580+ } ) => Some ( ( * span, * index) ) ,
581+ _ => None ,
582+ }
545583}
546584
547585/// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
0 commit comments