@@ -290,7 +290,7 @@ impl<'tcx> LateLintPass<'tcx> for Write {
290
290
}
291
291
292
292
let Some ( args) = format_args. args ( cx) else { return } ;
293
- check_literal ( cx, & args , name , format_args . is_raw ( cx ) ) ;
293
+ check_literal ( cx, & format_args , & args , name ) ;
294
294
295
295
if !self . in_debug_impl {
296
296
for arg in args {
@@ -426,7 +426,7 @@ fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, ma
426
426
}
427
427
}
428
428
429
- fn check_literal ( cx : & LateContext < ' _ > , args : & [ FormatArgsArg < ' _ > ] , name : & str , raw : bool ) {
429
+ fn check_literal ( cx : & LateContext < ' _ > , format_args : & FormatArgsExpn < ' _ > , args : & [ FormatArgsArg < ' _ > ] , name : & str ) {
430
430
let mut counts = HirIdMap :: < usize > :: default ( ) ;
431
431
for arg in args {
432
432
* counts. entry ( arg. value . hir_id ) . or_default ( ) += 1 ;
@@ -436,14 +436,24 @@ fn check_literal(cx: &LateContext<'_>, args: &[FormatArgsArg<'_>], name: &str, r
436
436
if_chain ! {
437
437
if counts[ & arg. value. hir_id] == 1 ;
438
438
if arg. format_trait == sym:: Display ;
439
+ if let ExprKind :: Lit ( lit) = & arg. value. kind;
439
440
if !arg. has_primitive_formatting( ) ;
440
441
if !arg. value. span. from_expansion( ) ;
441
- if let ExprKind :: Lit ( lit) = & arg. value. kind;
442
+ if let Some ( value_string) = snippet_opt( cx, arg. value. span) ;
443
+ if let Some ( format_string) = snippet_opt( cx, format_args. format_string_span) ;
442
444
then {
443
- let replacement = match lit. node {
444
- LitKind :: Str ( s, _) => s. to_string( ) ,
445
- LitKind :: Char ( c) => c. to_string( ) ,
446
- LitKind :: Bool ( b) => b. to_string( ) ,
445
+ let ( replacement, replace_raw) = match lit. node {
446
+ LitKind :: Str ( ..) => extract_str_literal( & value_string) ,
447
+ LitKind :: Char ( ch) => (
448
+ match ch {
449
+ '"' => "\\ \" " ,
450
+ '\'' => "'" ,
451
+ _ => & value_string[ 1 ..value_string. len( ) - 1 ] ,
452
+ }
453
+ . to_string( ) ,
454
+ false ,
455
+ ) ,
456
+ LitKind :: Bool ( b) => ( b. to_string( ) , false ) ,
447
457
_ => continue ,
448
458
} ;
449
459
@@ -453,40 +463,95 @@ fn check_literal(cx: &LateContext<'_>, args: &[FormatArgsArg<'_>], name: &str, r
453
463
PRINT_LITERAL
454
464
} ;
455
465
466
+ let replacement = match ( format_string. starts_with( 'r' ) , replace_raw) {
467
+ ( false , false ) => Some ( replacement) ,
468
+ ( false , true ) => Some ( replacement. replace( '"' , "\\ \" " ) . replace( '\\' , "\\ \\ " ) ) ,
469
+ ( true , false ) => match conservative_unescape( & replacement) {
470
+ Ok ( unescaped) => Some ( unescaped) ,
471
+ Err ( UnescapeErr :: Lint ) => None ,
472
+ Err ( UnescapeErr :: Ignore ) => continue ,
473
+ } ,
474
+ ( true , true ) => {
475
+ if replacement. contains( [ '#' , '"' ] ) {
476
+ None
477
+ } else {
478
+ Some ( replacement)
479
+ }
480
+ } ,
481
+ } ;
482
+
456
483
span_lint_and_then( cx, lint, arg. value. span, "literal with an empty format string" , |diag| {
457
- if raw && replacement. contains( & [ '"' , '#' ] ) {
458
- return ;
459
- }
484
+ if let Some ( replacement) = replacement {
485
+ // `format!("{}", "a")`, `format!("{named}", named = "b")
486
+ // ~~~~~ ~~~~~~~~~~~~~
487
+ let value_span = expand_past_previous_comma( cx, arg. value. span) ;
460
488
461
- let backslash = if raw {
462
- r"\"
463
- } else {
464
- r"\\"
465
- } ;
466
- let replacement = replacement
467
- . replace( '{' , "{{" )
468
- . replace( '}' , "}}" )
469
- . replace( '"' , "\\ \" " )
470
- . replace( '\\' , backslash) ;
471
-
472
- // `format!("{}", "a")`, `format!("{named}", named = "b")
473
- // ~~~~~ ~~~~~~~~~~~~~
474
- let value_span = expand_past_previous_comma( cx, arg. value. span) ;
475
-
476
- diag. multipart_suggestion(
477
- "try this" ,
478
- vec![
479
- ( arg. span, replacement) ,
480
- ( value_span, String :: new( ) ) ,
481
- ] ,
482
- Applicability :: MachineApplicable ,
483
- ) ;
489
+ let replacement = replacement. replace( '{' , "{{" ) . replace( '}' , "}}" ) ;
490
+ diag. multipart_suggestion(
491
+ "try this" ,
492
+ vec![
493
+ ( arg. span, replacement) ,
494
+ ( value_span, String :: new( ) ) ,
495
+ ] ,
496
+ Applicability :: MachineApplicable ,
497
+ ) ;
498
+ }
484
499
} ) ;
485
500
}
486
501
}
487
502
}
488
503
}
489
504
505
+ /// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
506
+ ///
507
+ /// `r#"a"#` -> (`a`, true)
508
+ ///
509
+ /// `"b"` -> (`b`, false)
510
+ fn extract_str_literal ( literal : & str ) -> ( String , bool ) {
511
+ let ( literal, raw) = match literal. strip_prefix ( 'r' ) {
512
+ Some ( stripped) => ( stripped. trim_matches ( '#' ) , true ) ,
513
+ None => ( literal, false ) ,
514
+ } ;
515
+
516
+ ( literal[ 1 ..literal. len ( ) - 1 ] . to_string ( ) , raw)
517
+ }
518
+
519
+ enum UnescapeErr {
520
+ /// Should still be linted, can be manually resolved by author, e.g.
521
+ ///
522
+ /// ```ignore
523
+ /// print!(r"{}", '"');
524
+ /// ```
525
+ Lint ,
526
+ /// Should not be linted, e.g.
527
+ ///
528
+ /// ```ignore
529
+ /// print!(r"{}", '\r');
530
+ /// ```
531
+ Ignore ,
532
+ }
533
+
534
+ /// Unescape a normal string into a raw string
535
+ fn conservative_unescape ( literal : & str ) -> Result < String , UnescapeErr > {
536
+ let mut unescaped = String :: with_capacity ( literal. len ( ) ) ;
537
+ let mut chars = literal. chars ( ) ;
538
+ let mut err = false ;
539
+
540
+ while let Some ( ch) = chars. next ( ) {
541
+ match ch {
542
+ '#' => err = true ,
543
+ '\\' => match chars. next ( ) {
544
+ Some ( '\\' ) => unescaped. push ( '\\' ) ,
545
+ Some ( '"' ) => err = true ,
546
+ _ => return Err ( UnescapeErr :: Ignore ) ,
547
+ } ,
548
+ _ => unescaped. push ( ch) ,
549
+ }
550
+ }
551
+
552
+ if err { Err ( UnescapeErr :: Lint ) } else { Ok ( unescaped) }
553
+ }
554
+
490
555
// Expand from `writeln!(o, "")` to `writeln!(o, "")`
491
556
// ^^ ^^^^
492
557
fn expand_past_previous_comma ( cx : & LateContext < ' _ > , span : Span ) -> Span {
0 commit comments