1
1
use std:: borrow:: Cow ;
2
- use std:: ops:: Range ;
2
+ use std:: iter;
3
+ use std:: ops:: { Deref , Range } ;
3
4
4
5
use 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 } ;
9
9
use rustc_ast:: tokenstream:: TokenStream ;
10
10
use rustc_errors:: Applicability ;
11
11
use rustc_lexer:: unescape:: { self , EscapeError } ;
12
12
use rustc_lint:: { EarlyContext , EarlyLintPass } ;
13
13
use rustc_parse:: parser;
14
14
use 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 } ;
17
17
18
18
declare_clippy_lint ! {
19
19
/// **What it does:** This lint warns when you use `println!("")` to
@@ -354,7 +354,120 @@ fn newline_span(fmtstr: &StrLit) -> Span {
354
354
sp. with_lo ( newline_sp_hi - newline_sp_len) . with_hi ( newline_sp_hi)
355
355
}
356
356
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
+
357
434
impl 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
+
358
471
/// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
359
472
/// `Option`s. The first `Option` of the tuple is the macro's format string. It includes
360
473
/// 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 {
376
489
/// ```
377
490
#[ allow( clippy:: too_many_lines) ]
378
491
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
-
384
492
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 ( ) ) ,
394
503
}
395
- }
504
+ } else {
505
+ None
506
+ } ;
396
507
397
508
let fmtstr = match parser. parse_str_lit ( ) {
398
509
Ok ( fmtstr) => fmtstr,
399
510
Err ( _) => return ( None , expr) ,
400
511
} ;
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
+
416
518
let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL } ;
417
- let mut idx = 0 ;
519
+ let mut unnamed_args = args . get_unnamed ( ) ;
418
520
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
- } ;
430
521
if !parser. eat ( & token:: Comma ) {
431
522
return ( Some ( fmtstr) , expr) ;
432
523
}
524
+
525
+ let comma_span = parser. prev_token . span ;
433
526
let token_expr = if let Ok ( expr) = parser. parse_expr ( ) . map_err ( |mut err| err. cancel ( ) ) {
434
527
expr
435
528
} else {
436
529
return ( Some ( fmtstr) , None ) ;
437
530
} ;
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 ,
457
536
} ,
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 ( "}" , "}}" )
482
547
} ,
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
+ ) ;
484
582
}
485
583
}
486
584
}
0 commit comments