@@ -290,7 +290,7 @@ impl<'tcx> LateLintPass<'tcx> for Write {
290290 }
291291
292292 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 ) ;
294294
295295 if !self . in_debug_impl {
296296 for arg in args {
@@ -426,7 +426,7 @@ fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, ma
426426 }
427427}
428428
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 ) {
430430 let mut counts = HirIdMap :: < usize > :: default ( ) ;
431431 for arg in args {
432432 * counts. entry ( arg. value . hir_id ) . or_default ( ) += 1 ;
@@ -436,14 +436,24 @@ fn check_literal(cx: &LateContext<'_>, args: &[FormatArgsArg<'_>], name: &str, r
436436 if_chain ! {
437437 if counts[ & arg. value. hir_id] == 1 ;
438438 if arg. format_trait == sym:: Display ;
439+ if let ExprKind :: Lit ( lit) = & arg. value. kind;
439440 if !arg. has_primitive_formatting( ) ;
440441 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) ;
442444 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 ) ,
447457 _ => continue ,
448458 } ;
449459
@@ -453,40 +463,95 @@ fn check_literal(cx: &LateContext<'_>, args: &[FormatArgsArg<'_>], name: &str, r
453463 PRINT_LITERAL
454464 } ;
455465
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+
456483 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) ;
460488
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+ }
484499 } ) ;
485500 }
486501 }
487502 }
488503}
489504
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+
490555// Expand from `writeln!(o, "")` to `writeln!(o, "")`
491556// ^^ ^^^^
492557fn expand_past_previous_comma ( cx : & LateContext < ' _ > , span : Span ) -> Span {
0 commit comments