11use std:: borrow:: Cow ;
2- use std:: ops:: Range ;
2+ use std:: iter;
3+ use std:: ops:: { Deref , Range } ;
34
45use clippy_utils:: diagnostics:: { span_lint, span_lint_and_sugg, span_lint_and_then} ;
5- use clippy_utils:: source:: snippet_with_applicability;
6- use if_chain:: if_chain;
7- use rustc_ast:: ast:: { Expr , ExprKind , ImplKind , Item , ItemKind , LitKind , MacCall , StrLit , StrStyle } ;
8- use rustc_ast:: token;
6+ use clippy_utils:: source:: { snippet_opt, snippet_with_applicability} ;
7+ use rustc_ast:: ast:: { Expr , ExprKind , ImplKind , Item , ItemKind , MacCall , Path , StrLit , StrStyle } ;
8+ use rustc_ast:: token:: { self , LitKind } ;
99use rustc_ast:: tokenstream:: TokenStream ;
1010use rustc_errors:: Applicability ;
1111use rustc_lexer:: unescape:: { self , EscapeError } ;
1212use rustc_lint:: { EarlyContext , EarlyLintPass } ;
1313use rustc_parse:: parser;
1414use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
15- use rustc_span:: symbol:: kw ;
16- use rustc_span:: { sym, BytePos , Span } ;
15+ use rustc_span:: symbol:: { kw , Symbol } ;
16+ use rustc_span:: { sym, BytePos , Span , DUMMY_SP } ;
1717
1818declare_clippy_lint ! {
1919 /// **What it does:** This lint warns when you use `println!("")` to
@@ -354,7 +354,120 @@ fn newline_span(fmtstr: &StrLit) -> Span {
354354 sp. with_lo ( newline_sp_hi - newline_sp_len) . with_hi ( newline_sp_hi)
355355}
356356
357+ /// Stores a list of replacement spans for each argument, but only if all the replacements used an
358+ /// empty format string.
359+ #[ derive( Default ) ]
360+ struct SimpleFormatArgs {
361+ unnamed : Vec < Vec < Span > > ,
362+ named : Vec < ( Symbol , Vec < Span > ) > ,
363+ }
364+ impl SimpleFormatArgs {
365+ fn get_unnamed ( & self ) -> impl Iterator < Item = & [ Span ] > {
366+ self . unnamed . iter ( ) . map ( |x| match x. as_slice ( ) {
367+ // Ignore the dummy span added from out of order format arguments.
368+ [ DUMMY_SP ] => & [ ] ,
369+ x => x,
370+ } )
371+ }
372+
373+ fn get_named ( & self , n : & Path ) -> & [ Span ] {
374+ self . named . iter ( ) . find ( |x| * n == x. 0 ) . map_or ( & [ ] , |x| x. 1 . as_slice ( ) )
375+ }
376+
377+ fn push ( & mut self , arg : rustc_parse_format:: Argument < ' _ > , span : Span ) {
378+ use rustc_parse_format:: {
379+ AlignUnknown , ArgumentImplicitlyIs , ArgumentIs , ArgumentNamed , CountImplied , FormatSpec ,
380+ } ;
381+
382+ const SIMPLE : FormatSpec < ' _ > = FormatSpec {
383+ fill : None ,
384+ align : AlignUnknown ,
385+ flags : 0 ,
386+ precision : CountImplied ,
387+ precision_span : None ,
388+ width : CountImplied ,
389+ width_span : None ,
390+ ty : "" ,
391+ ty_span : None ,
392+ } ;
393+
394+ match arg. position {
395+ ArgumentIs ( n) | ArgumentImplicitlyIs ( n) => {
396+ if self . unnamed . len ( ) <= n {
397+ // Use a dummy span to mark all unseen arguments.
398+ self . unnamed . resize_with ( n, || vec ! [ DUMMY_SP ] ) ;
399+ if arg. format == SIMPLE {
400+ self . unnamed . push ( vec ! [ span] ) ;
401+ } else {
402+ self . unnamed . push ( Vec :: new ( ) ) ;
403+ }
404+ } else {
405+ let args = & mut self . unnamed [ n] ;
406+ match ( args. as_mut_slice ( ) , arg. format == SIMPLE ) {
407+ // A non-empty format string has been seen already.
408+ ( [ ] , _) => ( ) ,
409+ // Replace the dummy span, if it exists.
410+ ( [ dummy @ DUMMY_SP ] , true ) => * dummy = span,
411+ ( [ _, ..] , true ) => args. push ( span) ,
412+ ( [ _, ..] , false ) => * args = Vec :: new ( ) ,
413+ }
414+ }
415+ } ,
416+ ArgumentNamed ( n) => {
417+ if let Some ( x) = self . named . iter_mut ( ) . find ( |x| x. 0 == n) {
418+ match x. 1 . as_slice ( ) {
419+ // A non-empty format string has been seen already.
420+ [ ] => ( ) ,
421+ [ _, ..] if arg. format == SIMPLE => x. 1 . push ( span) ,
422+ [ _, ..] => x. 1 = Vec :: new ( ) ,
423+ }
424+ } else if arg. format == SIMPLE {
425+ self . named . push ( ( n, vec ! [ span] ) ) ;
426+ } else {
427+ self . named . push ( ( n, Vec :: new ( ) ) ) ;
428+ }
429+ } ,
430+ } ;
431+ }
432+ }
433+
357434impl Write {
435+ /// Parses a format string into a collection of spans for each argument. This only keeps track
436+ /// of empty format arguments. Will also lint usages of debug format strings outside of debug
437+ /// impls.
438+ fn parse_fmt_string ( & self , cx : & EarlyContext < ' _ > , str_lit : & StrLit ) -> Option < SimpleFormatArgs > {
439+ use rustc_parse_format:: { ParseMode , Parser , Piece } ;
440+
441+ let str_sym = str_lit. symbol_unescaped . as_str ( ) ;
442+ let style = match str_lit. style {
443+ StrStyle :: Cooked => None ,
444+ StrStyle :: Raw ( n) => Some ( n as usize ) ,
445+ } ;
446+
447+ let mut parser = Parser :: new ( & str_sym, style, snippet_opt ( cx, str_lit. span ) , false , ParseMode :: Format ) ;
448+ let mut args = SimpleFormatArgs :: default ( ) ;
449+
450+ while let Some ( arg) = parser. next ( ) {
451+ let arg = match arg {
452+ Piece :: String ( _) => continue ,
453+ Piece :: NextArgument ( arg) => arg,
454+ } ;
455+ let span = parser
456+ . arg_places
457+ . last ( )
458+ . map_or ( DUMMY_SP , |& x| str_lit. span . from_inner ( x) ) ;
459+
460+ if !self . in_debug_impl && arg. format . ty == "?" {
461+ // FIXME: modify rustc's fmt string parser to give us the current span
462+ span_lint ( cx, USE_DEBUG , span, "use of `Debug`-based formatting" ) ;
463+ }
464+
465+ args. push ( arg, span) ;
466+ }
467+
468+ parser. errors . is_empty ( ) . then ( move || args)
469+ }
470+
358471 /// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
359472 /// `Option`s. The first `Option` of the tuple is the macro's format string. It includes
360473 /// the contents of the string, whether it's a raw string, and the span of the literal in the
@@ -376,111 +489,96 @@ impl Write {
376489 /// ```
377490 #[ allow( clippy:: too_many_lines) ]
378491 fn check_tts < ' a > ( & self , cx : & EarlyContext < ' a > , tts : TokenStream , is_write : bool ) -> ( Option < StrLit > , Option < Expr > ) {
379- use rustc_parse_format:: {
380- AlignUnknown , ArgumentImplicitlyIs , ArgumentIs , ArgumentNamed , CountImplied , FormatSpec , ParseMode , Parser ,
381- Piece ,
382- } ;
383-
384492 let mut parser = parser:: Parser :: new ( & cx. sess . parse_sess , tts, false , None ) ;
385- let mut expr: Option < Expr > = None ;
386- if is_write {
387- expr = match parser. parse_expr ( ) . map_err ( |mut err| err. cancel ( ) ) {
388- Ok ( p) => Some ( p. into_inner ( ) ) ,
389- Err ( _) => return ( None , None ) ,
390- } ;
391- // might be `writeln!(foo)`
392- if parser. expect ( & token:: Comma ) . map_err ( |mut err| err. cancel ( ) ) . is_err ( ) {
393- return ( None , expr) ;
493+ let expr = if is_write {
494+ match parser
495+ . parse_expr ( )
496+ . map ( rustc_ast:: ptr:: P :: into_inner)
497+ . map_err ( |mut e| e. cancel ( ) )
498+ {
499+ // write!(e, ...)
500+ Ok ( p) if parser. eat ( & token:: Comma ) => Some ( p) ,
501+ // write!(e) or error
502+ e => return ( None , e. ok ( ) ) ,
394503 }
395- }
504+ } else {
505+ None
506+ } ;
396507
397508 let fmtstr = match parser. parse_str_lit ( ) {
398509 Ok ( fmtstr) => fmtstr,
399510 Err ( _) => return ( None , expr) ,
400511 } ;
401- let tmp = fmtstr. symbol . as_str ( ) ;
402- let mut args = vec ! [ ] ;
403- let mut fmt_parser = Parser :: new ( & tmp, None , None , false , ParseMode :: Format ) ;
404- while let Some ( piece) = fmt_parser. next ( ) {
405- if !fmt_parser. errors . is_empty ( ) {
406- return ( None , expr) ;
407- }
408- if let Piece :: NextArgument ( arg) = piece {
409- if !self . in_debug_impl && arg. format . ty == "?" {
410- // FIXME: modify rustc's fmt string parser to give us the current span
411- span_lint ( cx, USE_DEBUG , parser. prev_token . span , "use of `Debug`-based formatting" ) ;
412- }
413- args. push ( arg) ;
414- }
415- }
512+
513+ let args = match self . parse_fmt_string ( cx, & fmtstr) {
514+ Some ( args) => args,
515+ None => return ( Some ( fmtstr) , expr) ,
516+ } ;
517+
416518 let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL } ;
417- let mut idx = 0 ;
519+ let mut unnamed_args = args . get_unnamed ( ) ;
418520 loop {
419- const SIMPLE : FormatSpec < ' _ > = FormatSpec {
420- fill : None ,
421- align : AlignUnknown ,
422- flags : 0 ,
423- precision : CountImplied ,
424- precision_span : None ,
425- width : CountImplied ,
426- width_span : None ,
427- ty : "" ,
428- ty_span : None ,
429- } ;
430521 if !parser. eat ( & token:: Comma ) {
431522 return ( Some ( fmtstr) , expr) ;
432523 }
524+
525+ let comma_span = parser. prev_token . span ;
433526 let token_expr = if let Ok ( expr) = parser. parse_expr ( ) . map_err ( |mut err| err. cancel ( ) ) {
434527 expr
435528 } else {
436529 return ( Some ( fmtstr) , None ) ;
437530 } ;
438- match & token_expr. kind {
439- ExprKind :: Lit ( lit) if !matches ! ( lit. kind, LitKind :: Int ( ..) | LitKind :: Float ( ..) ) => {
440- let mut all_simple = true ;
441- let mut seen = false ;
442- for arg in & args {
443- match arg. position {
444- ArgumentImplicitlyIs ( n) | ArgumentIs ( n) => {
445- if n == idx {
446- all_simple &= arg. format == SIMPLE ;
447- seen = true ;
448- }
449- } ,
450- ArgumentNamed ( _) => { } ,
451- }
452- }
453- if all_simple && seen {
454- span_lint ( cx, lint, token_expr. span , "literal with an empty format string" ) ;
455- }
456- idx += 1 ;
531+ let ( fmt_spans, lit) = match & token_expr. kind {
532+ ExprKind :: Lit ( lit) => ( unnamed_args. next ( ) . unwrap_or ( & [ ] ) , lit) ,
533+ ExprKind :: Assign ( lhs, rhs, _) => match ( & lhs. kind , & rhs. kind ) {
534+ ( ExprKind :: Path ( _, p) , ExprKind :: Lit ( lit) ) => ( args. get_named ( p) , lit) ,
535+ _ => continue ,
457536 } ,
458- ExprKind :: Assign ( lhs, rhs, _) => {
459- if_chain ! {
460- if let ExprKind :: Lit ( ref lit) = rhs. kind;
461- if !matches!( lit. kind, LitKind :: Int ( ..) | LitKind :: Float ( ..) ) ;
462- if let ExprKind :: Path ( _, p) = & lhs. kind;
463- then {
464- let mut all_simple = true ;
465- let mut seen = false ;
466- for arg in & args {
467- match arg. position {
468- ArgumentImplicitlyIs ( _) | ArgumentIs ( _) => { } ,
469- ArgumentNamed ( name) => {
470- if * p == name {
471- seen = true ;
472- all_simple &= arg. format == SIMPLE ;
473- }
474- } ,
475- }
476- }
477- if all_simple && seen {
478- span_lint( cx, lint, rhs. span, "literal with an empty format string" ) ;
479- }
480- }
481- }
537+ _ => {
538+ unnamed_args. next ( ) ;
539+ continue ;
540+ } ,
541+ } ;
542+
543+ let replacement: String = match lit. token . kind {
544+ LitKind :: Integer | LitKind :: Float | LitKind :: Err => continue ,
545+ LitKind :: StrRaw ( _) | LitKind :: ByteStrRaw ( _) if matches ! ( fmtstr. style, StrStyle :: Raw ( _) ) => {
546+ lit. token . symbol . as_str ( ) . replace ( "{" , "{{" ) . replace ( "}" , "}}" )
482547 } ,
483- _ => idx += 1 ,
548+ LitKind :: Str | LitKind :: ByteStr if matches ! ( fmtstr. style, StrStyle :: Cooked ) => {
549+ lit. token . symbol . as_str ( ) . replace ( "{" , "{{" ) . replace ( "}" , "}}" )
550+ } ,
551+ LitKind :: StrRaw ( _) | LitKind :: Str | LitKind :: ByteStrRaw ( _) | LitKind :: ByteStr => continue ,
552+ LitKind :: Byte | LitKind :: Char => match & * lit. token . symbol . as_str ( ) {
553+ "\" " if matches ! ( fmtstr. style, StrStyle :: Cooked ) => "\\ \" " ,
554+ "\" " if matches ! ( fmtstr. style, StrStyle :: Raw ( 0 ) ) => continue ,
555+ "\\ \\ " if matches ! ( fmtstr. style, StrStyle :: Raw ( _) ) => "\\ " ,
556+ "\\ '" => "'" ,
557+ "{" => "{{" ,
558+ "}" => "}}" ,
559+ x if matches ! ( fmtstr. style, StrStyle :: Raw ( _) ) && x. starts_with ( '\\' ) => continue ,
560+ x => x,
561+ }
562+ . into ( ) ,
563+ LitKind :: Bool => lit. token . symbol . as_str ( ) . deref ( ) . into ( ) ,
564+ } ;
565+
566+ if !fmt_spans. is_empty ( ) {
567+ span_lint_and_then (
568+ cx,
569+ lint,
570+ token_expr. span ,
571+ "literal with an empty format string" ,
572+ |diag| {
573+ diag. multipart_suggestion (
574+ "try this" ,
575+ iter:: once ( ( comma_span. to ( token_expr. span ) , String :: new ( ) ) )
576+ . chain ( fmt_spans. iter ( ) . cloned ( ) . zip ( iter:: repeat ( replacement) ) )
577+ . collect ( ) ,
578+ Applicability :: MachineApplicable ,
579+ ) ;
580+ } ,
581+ ) ;
484582 }
485583 }
486584 }
0 commit comments