From 25f753bd9ea35ef705c273cb634d10ce2e1e2b0e Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 6 Nov 2025 23:34:10 -0500 Subject: [PATCH] fix-21295 --- .../fixtures/flake8_logging_format/G004.py | 9 +++ .../rules/logging_call.rs | 30 +++++++++- ...flake8_logging_format__tests__G004.py.snap | 35 ++++++++++++ ..._format__tests__preview__G004_G004.py.snap | 55 +++++++++++++++++++ 4 files changed, 127 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004.py b/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004.py index fcfa953a32..86110041d9 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004.py @@ -64,3 +64,12 @@ import logging x = 1 logging.error(f"{x} -> %s", x) + +# Test cases for control characters in f-strings +bar = "bar" +logging.getLogger().error(f"Lorem ipsum \n {bar}") + +value = "test" +logging.info(f"Tab\t{value}") + +logging.info(f"CR\r{value}") diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs b/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs index 23d842efeb..8ead0f9176 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs +++ b/crates/ruff_linter/src/rules/flake8_logging_format/rules/logging_call.rs @@ -14,6 +14,26 @@ use crate::rules::flake8_logging_format::violations::{ }; use crate::{Edit, Fix}; +/// Escape control characters and quotes for use in a Python string literal. +/// Escapes newlines, tabs, carriage returns, backslashes, and quotes that match the quote style. +fn escape_string_for_literal(text: &str, quote_char: char) -> String { + let mut escaped = String::with_capacity(text.len() * 2); + for ch in text.chars() { + match ch { + '\n' => escaped.push_str("\\n"), + '\t' => escaped.push_str("\\t"), + '\r' => escaped.push_str("\\r"), + '\\' => escaped.push_str("\\\\"), + ch if ch == quote_char => { + escaped.push('\\'); + escaped.push(ch); + } + _ => escaped.push(ch), + } + } + escaped +} + fn logging_f_string( checker: &Checker, msg: &Expr, @@ -50,6 +70,9 @@ fn logging_f_string( .next() .unwrap_or("\""); + // Extract the quote character for escaping + let quote_char = quote_str.chars().next().unwrap_or('"'); + for part in &f_string.value { match part { ast::FStringPart::Literal(literal) => { @@ -57,7 +80,7 @@ fn logging_f_string( if literal_text.contains('%') { return; } - format_string.push_str(literal_text); + format_string.push_str(&escape_string_for_literal(literal_text, quote_char)); } ast::FStringPart::FString(f) => { for element in &f.elements { @@ -69,7 +92,10 @@ fn logging_f_string( if lit.value.as_ref().contains('%') { return; } - format_string.push_str(lit.value.as_ref()); + format_string.push_str(&escape_string_for_literal( + lit.value.as_ref(), + quote_char, + )); } InterpolatedStringElement::Interpolation(interpolated) => { if interpolated.format_spec.is_some() diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap index f02d019ad5..9d3b242eb4 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap @@ -217,5 +217,40 @@ G004 Logging statement uses f-string 65 | x = 1 66 | logging.error(f"{x} -> %s", x) | ^^^^^^^^^^^^ +67 | +68 | # Test cases for control characters in f-strings + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:70:27 + | +68 | # Test cases for control characters in f-strings +69 | bar = "bar" +70 | logging.getLogger().error(f"Lorem ipsum \n {bar}") + | ^^^^^^^^^^^^^^^^^^^^^^^ +71 | +72 | value = "test" + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:73:14 + | +72 | value = "test" +73 | logging.info(f"Tab\t{value}") + | ^^^^^^^^^^^^^^^ +74 | +75 | logging.info(f"CR\r{value}") + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:75:14 + | +73 | logging.info(f"Tab\t{value}") +74 | +75 | logging.info(f"CR\r{value}") + | ^^^^^^^^^^^^^^ | help: Convert to lazy `%` formatting diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap index 92a9ff3139..2ae17f3f39 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap @@ -273,5 +273,60 @@ G004 Logging statement uses f-string 65 | x = 1 66 | logging.error(f"{x} -> %s", x) | ^^^^^^^^^^^^ +67 | +68 | # Test cases for control characters in f-strings | help: Convert to lazy `%` formatting + +G004 [*] Logging statement uses f-string + --> G004.py:70:27 + | +68 | # Test cases for control characters in f-strings +69 | bar = "bar" +70 | logging.getLogger().error(f"Lorem ipsum \n {bar}") + | ^^^^^^^^^^^^^^^^^^^^^^^ +71 | +72 | value = "test" + | +help: Convert to lazy `%` formatting +67 | +68 | # Test cases for control characters in f-strings +69 | bar = "bar" + - logging.getLogger().error(f"Lorem ipsum \n {bar}") +70 + logging.getLogger().error("Lorem ipsum \n %s", bar) +71 | +72 | value = "test" +73 | logging.info(f"Tab\t{value}") + +G004 [*] Logging statement uses f-string + --> G004.py:73:14 + | +72 | value = "test" +73 | logging.info(f"Tab\t{value}") + | ^^^^^^^^^^^^^^^ +74 | +75 | logging.info(f"CR\r{value}") + | +help: Convert to lazy `%` formatting +70 | logging.getLogger().error(f"Lorem ipsum \n {bar}") +71 | +72 | value = "test" + - logging.info(f"Tab\t{value}") +73 + logging.info("Tab\t%s", value) +74 | +75 | logging.info(f"CR\r{value}") + +G004 [*] Logging statement uses f-string + --> G004.py:75:14 + | +73 | logging.info(f"Tab\t{value}") +74 | +75 | logging.info(f"CR\r{value}") + | ^^^^^^^^^^^^^^ + | +help: Convert to lazy `%` formatting +72 | value = "test" +73 | logging.info(f"Tab\t{value}") +74 | + - logging.info(f"CR\r{value}") +75 + logging.info("CR\r%s", value)