From 136abace9241ebf42afb70e60ee760361b13e2d0 Mon Sep 17 00:00:00 2001 From: Hamir Mahal Date: Tue, 26 Aug 2025 07:51:24 -0700 Subject: [PATCH] [`flake8-logging-format`] Add auto-fix for f-string logging calls (`G004`) (#19303) Closes #19302 ## Summary This adds an auto-fix for `Logging statement uses f-string` Ruff G004, so users don't have to resolve it manually. ## Test Plan I ran the auto-fixes on a Python file locally and and it worked as expected. --------- Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../fixtures/flake8_logging_format/G004.py | 47 +++ .../flake8_logging_format/G004_arg_order.py | 10 + crates/ruff_linter/src/preview.rs | 5 + .../src/rules/flake8_logging_format/mod.rs | 21 ++ .../rules/logging_call.rs | 95 +++++- ...flake8_logging_format__tests__G004.py.snap | 158 ++++++++++ ...ging_format__tests__G004_arg_order.py.snap | 23 ++ ..._format__tests__preview__G004_G004.py.snap | 291 ++++++++++++++++++ ...ests__preview__G004_G004_arg_order.py.snap | 23 ++ .../rules/flake8_logging_format/violations.rs | 6 + 10 files changed, 674 insertions(+), 5 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004_arg_order.py create mode 100644 crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_arg_order.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_arg_order.py.snap 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 da05aba630..fcfa953a32 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 @@ -17,3 +17,50 @@ info(f"{__name__}") # Don't trigger for t-strings info(t"{name}") info(t"{__name__}") + +count = 5 +total = 9 +directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/" +logging.info(f"{count} out of {total} files in {directory_path} checked") + + + +x = 99 +fmt = "08d" +logger.info(f"{x:{'08d'}}") +logger.info(f"{x:>10} {x:{fmt}}") + +logging.info(f"") +logging.info(f"This message doesn't have any variables.") + +obj = {"key": "value"} +logging.info(f"Object: {obj!r}") + +items_count = 3 +logging.warning(f"Items: {items_count:d}") + +data = {"status": "active"} +logging.info(f"Processing {len(data)} items") +logging.info(f"Status: {data.get('status', 'unknown').upper()}") + + +result = 123 +logging.info(f"Calculated result: {result + 100}") + +temperature = 123 +logging.info(f"Temperature: {temperature:.1f}°C") + +class FilePath: + def __init__(self, name: str): + self.name = name + +logging.info(f"No changes made to {file_path.name}.") + +user = "tron" +balance = 123.45 +logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}") + +import logging + +x = 1 +logging.error(f"{x} -> %s", x) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004_arg_order.py b/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004_arg_order.py new file mode 100644 index 0000000000..44c370bd3d --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004_arg_order.py @@ -0,0 +1,10 @@ +"""Test f-string argument order.""" + +import logging + +logger = logging.getLogger(__name__) + +X = 1 +Y = 2 +logger.error(f"{X} -> %s", Y) +logger.error(f"{Y} -> %s", X) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index b7d3b5df7a..6b6a4d15e5 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -40,6 +40,11 @@ pub(crate) const fn is_bad_version_info_in_non_stub_enabled(settings: &LinterSet settings.preview.is_enabled() } +/// +pub(crate) const fn is_fix_f_string_logging_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + // https://github.com/astral-sh/ruff/pull/16719 pub(crate) const fn is_fix_manual_dict_comprehension_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/mod.rs b/crates/ruff_linter/src/rules/flake8_logging_format/mod.rs index 1dc2b536a6..cea8ce78e8 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_logging_format/mod.rs @@ -22,6 +22,7 @@ mod tests { #[test_case(Path::new("G002.py"))] #[test_case(Path::new("G003.py"))] #[test_case(Path::new("G004.py"))] + #[test_case(Path::new("G004_arg_order.py"))] #[test_case(Path::new("G010.py"))] #[test_case(Path::new("G101_1.py"))] #[test_case(Path::new("G101_2.py"))] @@ -48,4 +49,24 @@ mod tests { assert_diagnostics!(snapshot, diagnostics); Ok(()) } + + #[test_case(Rule::LoggingFString, Path::new("G004.py"))] + #[test_case(Rule::LoggingFString, Path::new("G004_arg_order.py"))] + fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "preview__{}_{}", + rule_code.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("flake8_logging_format").join(path).as_path(), + &settings::LinterSettings { + logger_objects: vec!["logging_setup.logger".to_string()], + preview: settings::types::PreviewMode::Enabled, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } } 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 3372741154..e87cf3a38d 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 @@ -1,9 +1,11 @@ -use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator}; +use ruff_python_ast::InterpolatedStringElement; +use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Operator, StringFlags}; use ruff_python_semantic::analyze::logging; use ruff_python_stdlib::logging::LoggingLevel; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::preview::is_fix_f_string_logging_enabled; use crate::registry::Rule; use crate::rules::flake8_logging_format::violations::{ LoggingExcInfo, LoggingExtraAttrClash, LoggingFString, LoggingPercentFormat, @@ -11,6 +13,87 @@ use crate::rules::flake8_logging_format::violations::{ }; use crate::{Edit, Fix}; +fn logging_f_string( + checker: &Checker, + msg: &Expr, + f_string: &ast::ExprFString, + arguments: &Arguments, + msg_pos: usize, +) { + // Report the diagnostic up-front so we can attach a fix later only when preview is enabled. + let mut diagnostic = checker.report_diagnostic(LoggingFString, msg.range()); + + // Preview gate for the automatic fix. + if !is_fix_f_string_logging_enabled(checker.settings()) { + return; + } + + // If there are existing positional arguments after the message, bail out. + // This could indicate a mistake or complex usage we shouldn't try to fix. + if arguments.args.len() > msg_pos + 1 { + return; + } + + let mut format_string = String::new(); + let mut args: Vec<&str> = Vec::new(); + + // Try to reuse the first part's quote style when building the replacement. + // Default to double quotes if we can't determine it. + let quote_str = f_string + .value + .f_strings() + .next() + .map(|f| f.flags.quote_str()) + .unwrap_or("\""); + + for f in f_string.value.f_strings() { + for element in &f.elements { + match element { + InterpolatedStringElement::Literal(lit) => { + // If the literal text contains a '%' placeholder, bail out: mixing + // f-string interpolation with '%' placeholders is ambiguous for our + // automatic conversion, so don't offer a fix for this case. + if lit.value.as_ref().contains('%') { + return; + } + format_string.push_str(lit.value.as_ref()); + } + InterpolatedStringElement::Interpolation(interpolated) => { + if interpolated.format_spec.is_some() + || !matches!( + interpolated.conversion, + ruff_python_ast::ConversionFlag::None + ) + { + return; + } + match interpolated.expression.as_ref() { + Expr::Name(name) => { + format_string.push_str("%s"); + args.push(name.id.as_str()); + } + _ => return, + } + } + } + } + } + + if args.is_empty() { + return; + } + + let replacement = format!( + "{q}{format_string}{q}, {args}", + q = quote_str, + format_string = format_string, + args = args.join(", ") + ); + + let fix = Fix::safe_edit(Edit::range_replacement(replacement, msg.range())); + diagnostic.set_fix(fix); +} + /// Returns `true` if the attribute is a reserved attribute on the `logging` module's `LogRecord` /// class. fn is_reserved_attr(attr: &str) -> bool { @@ -42,7 +125,7 @@ fn is_reserved_attr(attr: &str) -> bool { } /// Check logging messages for violations. -fn check_msg(checker: &Checker, msg: &Expr) { +fn check_msg(checker: &Checker, msg: &Expr, arguments: &Arguments, msg_pos: usize) { match msg { // Check for string concatenation and percent format. Expr::BinOp(ast::ExprBinOp { op, .. }) => match op { @@ -55,8 +138,10 @@ fn check_msg(checker: &Checker, msg: &Expr) { _ => {} }, // Check for f-strings. - Expr::FString(_) => { - checker.report_diagnostic_if_enabled(LoggingFString, msg.range()); + Expr::FString(f_string) => { + if checker.is_rule_enabled(Rule::LoggingFString) { + logging_f_string(checker, msg, f_string, arguments, msg_pos); + } } // Check for .format() calls. Expr::Call(ast::ExprCall { func, .. }) => { @@ -168,7 +253,7 @@ pub(crate) fn logging_call(checker: &Checker, call: &ast::ExprCall) { // G001, G002, G003, G004 let msg_pos = usize::from(matches!(logging_call_type, LoggingCallType::LogCall)); if let Some(format_arg) = call.arguments.find_argument_value("msg", msg_pos) { - check_msg(checker, format_arg); + check_msg(checker, format_arg, &call.arguments, msg_pos); } // G010 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 10a31007b8..f02d019ad5 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 @@ -9,6 +9,7 @@ G004 Logging statement uses f-string | ^^^^^^^^^^^^^^^ 5 | logging.log(logging.INFO, f"Hello {name}") | +help: Convert to lazy `%` formatting G004 Logging statement uses f-string --> G004.py:5:27 @@ -20,6 +21,7 @@ G004 Logging statement uses f-string 6 | 7 | _LOGGER = logging.getLogger() | +help: Convert to lazy `%` formatting G004 Logging statement uses f-string --> G004.py:8:14 @@ -30,6 +32,7 @@ G004 Logging statement uses f-string 9 | 10 | logging.getLogger().info(f"{name}") | +help: Convert to lazy `%` formatting G004 Logging statement uses f-string --> G004.py:10:26 @@ -41,6 +44,7 @@ G004 Logging statement uses f-string 11 | 12 | from logging import info | +help: Convert to lazy `%` formatting G004 Logging statement uses f-string --> G004.py:14:6 @@ -51,6 +55,7 @@ G004 Logging statement uses f-string | ^^^^^^^^^ 15 | info(f"{__name__}") | +help: Convert to lazy `%` formatting G004 Logging statement uses f-string --> G004.py:15:6 @@ -61,3 +66,156 @@ G004 Logging statement uses f-string 16 | 17 | # Don't trigger for t-strings | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:24:14 + | +22 | total = 9 +23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/" +24 | logging.info(f"{count} out of {total} files in {directory_path} checked") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:30:13 + | +28 | x = 99 +29 | fmt = "08d" +30 | logger.info(f"{x:{'08d'}}") + | ^^^^^^^^^^^^^^ +31 | logger.info(f"{x:>10} {x:{fmt}}") + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:31:13 + | +29 | fmt = "08d" +30 | logger.info(f"{x:{'08d'}}") +31 | logger.info(f"{x:>10} {x:{fmt}}") + | ^^^^^^^^^^^^^^^^^^^^ +32 | +33 | logging.info(f"") + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:33:14 + | +31 | logger.info(f"{x:>10} {x:{fmt}}") +32 | +33 | logging.info(f"") + | ^^^ +34 | logging.info(f"This message doesn't have any variables.") + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:34:14 + | +33 | logging.info(f"") +34 | logging.info(f"This message doesn't have any variables.") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +35 | +36 | obj = {"key": "value"} + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:37:14 + | +36 | obj = {"key": "value"} +37 | logging.info(f"Object: {obj!r}") + | ^^^^^^^^^^^^^^^^^^ +38 | +39 | items_count = 3 + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:40:17 + | +39 | items_count = 3 +40 | logging.warning(f"Items: {items_count:d}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +41 | +42 | data = {"status": "active"} + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:43:14 + | +42 | data = {"status": "active"} +43 | logging.info(f"Processing {len(data)} items") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}") + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:44:14 + | +42 | data = {"status": "active"} +43 | logging.info(f"Processing {len(data)} items") +44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:48:14 + | +47 | result = 123 +48 | logging.info(f"Calculated result: {result + 100}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +49 | +50 | temperature = 123 + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:51:14 + | +50 | temperature = 123 +51 | logging.info(f"Temperature: {temperature:.1f}°C") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +52 | +53 | class FilePath: + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:57:14 + | +55 | self.name = name +56 | +57 | logging.info(f"No changes made to {file_path.name}.") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +58 | +59 | user = "tron" + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:61:15 + | +59 | user = "tron" +60 | balance = 123.45 +61 | logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +62 | +63 | import logging + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:66:15 + | +65 | x = 1 +66 | logging.error(f"{x} -> %s", x) + | ^^^^^^^^^^^^ + | +help: Convert to lazy `%` formatting diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_arg_order.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_arg_order.py.snap new file mode 100644 index 0000000000..cb976e768a --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004_arg_order.py.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs +--- +G004 Logging statement uses f-string + --> G004_arg_order.py:9:14 + | + 7 | X = 1 + 8 | Y = 2 + 9 | logger.error(f"{X} -> %s", Y) + | ^^^^^^^^^^^^ +10 | logger.error(f"{Y} -> %s", X) + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004_arg_order.py:10:14 + | + 8 | Y = 2 + 9 | logger.error(f"{X} -> %s", Y) +10 | logger.error(f"{Y} -> %s", X) + | ^^^^^^^^^^^^ + | +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 new file mode 100644 index 0000000000..b74a9e2306 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004.py.snap @@ -0,0 +1,291 @@ +--- +source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs +--- +G004 [*] Logging statement uses f-string + --> G004.py:4:14 + | +3 | name = "world" +4 | logging.info(f"Hello {name}") + | ^^^^^^^^^^^^^^^ +5 | logging.log(logging.INFO, f"Hello {name}") + | +help: Convert to lazy `%` formatting + +ℹ Safe fix +1 1 | import logging +2 2 | +3 3 | name = "world" +4 |-logging.info(f"Hello {name}") + 4 |+logging.info("Hello %s", name) +5 5 | logging.log(logging.INFO, f"Hello {name}") +6 6 | +7 7 | _LOGGER = logging.getLogger() + +G004 [*] Logging statement uses f-string + --> G004.py:5:27 + | +3 | name = "world" +4 | logging.info(f"Hello {name}") +5 | logging.log(logging.INFO, f"Hello {name}") + | ^^^^^^^^^^^^^^^ +6 | +7 | _LOGGER = logging.getLogger() + | +help: Convert to lazy `%` formatting + +ℹ Safe fix +2 2 | +3 3 | name = "world" +4 4 | logging.info(f"Hello {name}") +5 |-logging.log(logging.INFO, f"Hello {name}") + 5 |+logging.log(logging.INFO, "Hello %s", name) +6 6 | +7 7 | _LOGGER = logging.getLogger() +8 8 | _LOGGER.info(f"{__name__}") + +G004 [*] Logging statement uses f-string + --> G004.py:8:14 + | + 7 | _LOGGER = logging.getLogger() + 8 | _LOGGER.info(f"{__name__}") + | ^^^^^^^^^^^^^ + 9 | +10 | logging.getLogger().info(f"{name}") + | +help: Convert to lazy `%` formatting + +ℹ Safe fix +5 5 | logging.log(logging.INFO, f"Hello {name}") +6 6 | +7 7 | _LOGGER = logging.getLogger() +8 |-_LOGGER.info(f"{__name__}") + 8 |+_LOGGER.info("%s", __name__) +9 9 | +10 10 | logging.getLogger().info(f"{name}") +11 11 | + +G004 [*] Logging statement uses f-string + --> G004.py:10:26 + | + 8 | _LOGGER.info(f"{__name__}") + 9 | +10 | logging.getLogger().info(f"{name}") + | ^^^^^^^^^ +11 | +12 | from logging import info + | +help: Convert to lazy `%` formatting + +ℹ Safe fix +7 7 | _LOGGER = logging.getLogger() +8 8 | _LOGGER.info(f"{__name__}") +9 9 | +10 |-logging.getLogger().info(f"{name}") + 10 |+logging.getLogger().info("%s", name) +11 11 | +12 12 | from logging import info +13 13 | + +G004 [*] Logging statement uses f-string + --> G004.py:14:6 + | +12 | from logging import info +13 | +14 | info(f"{name}") + | ^^^^^^^^^ +15 | info(f"{__name__}") + | +help: Convert to lazy `%` formatting + +ℹ Safe fix +11 11 | +12 12 | from logging import info +13 13 | +14 |-info(f"{name}") + 14 |+info("%s", name) +15 15 | info(f"{__name__}") +16 16 | +17 17 | # Don't trigger for t-strings + +G004 [*] Logging statement uses f-string + --> G004.py:15:6 + | +14 | info(f"{name}") +15 | info(f"{__name__}") + | ^^^^^^^^^^^^^ +16 | +17 | # Don't trigger for t-strings + | +help: Convert to lazy `%` formatting + +ℹ Safe fix +12 12 | from logging import info +13 13 | +14 14 | info(f"{name}") +15 |-info(f"{__name__}") + 15 |+info("%s", __name__) +16 16 | +17 17 | # Don't trigger for t-strings +18 18 | info(t"{name}") + +G004 [*] Logging statement uses f-string + --> G004.py:24:14 + | +22 | total = 9 +23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/" +24 | logging.info(f"{count} out of {total} files in {directory_path} checked") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Convert to lazy `%` formatting + +ℹ Safe fix +21 21 | count = 5 +22 22 | total = 9 +23 23 | directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/" +24 |-logging.info(f"{count} out of {total} files in {directory_path} checked") + 24 |+logging.info("%s out of %s files in %s checked", count, total, directory_path) +25 25 | +26 26 | +27 27 | + +G004 Logging statement uses f-string + --> G004.py:30:13 + | +28 | x = 99 +29 | fmt = "08d" +30 | logger.info(f"{x:{'08d'}}") + | ^^^^^^^^^^^^^^ +31 | logger.info(f"{x:>10} {x:{fmt}}") + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:31:13 + | +29 | fmt = "08d" +30 | logger.info(f"{x:{'08d'}}") +31 | logger.info(f"{x:>10} {x:{fmt}}") + | ^^^^^^^^^^^^^^^^^^^^ +32 | +33 | logging.info(f"") + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:33:14 + | +31 | logger.info(f"{x:>10} {x:{fmt}}") +32 | +33 | logging.info(f"") + | ^^^ +34 | logging.info(f"This message doesn't have any variables.") + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:34:14 + | +33 | logging.info(f"") +34 | logging.info(f"This message doesn't have any variables.") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +35 | +36 | obj = {"key": "value"} + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:37:14 + | +36 | obj = {"key": "value"} +37 | logging.info(f"Object: {obj!r}") + | ^^^^^^^^^^^^^^^^^^ +38 | +39 | items_count = 3 + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:40:17 + | +39 | items_count = 3 +40 | logging.warning(f"Items: {items_count:d}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +41 | +42 | data = {"status": "active"} + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:43:14 + | +42 | data = {"status": "active"} +43 | logging.info(f"Processing {len(data)} items") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}") + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:44:14 + | +42 | data = {"status": "active"} +43 | logging.info(f"Processing {len(data)} items") +44 | logging.info(f"Status: {data.get('status', 'unknown').upper()}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:48:14 + | +47 | result = 123 +48 | logging.info(f"Calculated result: {result + 100}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +49 | +50 | temperature = 123 + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:51:14 + | +50 | temperature = 123 +51 | logging.info(f"Temperature: {temperature:.1f}°C") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +52 | +53 | class FilePath: + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:57:14 + | +55 | self.name = name +56 | +57 | logging.info(f"No changes made to {file_path.name}.") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +58 | +59 | user = "tron" + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:61:15 + | +59 | user = "tron" +60 | balance = 123.45 +61 | logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +62 | +63 | import logging + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004.py:66:15 + | +65 | x = 1 +66 | logging.error(f"{x} -> %s", x) + | ^^^^^^^^^^^^ + | +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_arg_order.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_arg_order.py.snap new file mode 100644 index 0000000000..cb976e768a --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__preview__G004_G004_arg_order.py.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/rules/flake8_logging_format/mod.rs +--- +G004 Logging statement uses f-string + --> G004_arg_order.py:9:14 + | + 7 | X = 1 + 8 | Y = 2 + 9 | logger.error(f"{X} -> %s", Y) + | ^^^^^^^^^^^^ +10 | logger.error(f"{Y} -> %s", X) + | +help: Convert to lazy `%` formatting + +G004 Logging statement uses f-string + --> G004_arg_order.py:10:14 + | + 8 | Y = 2 + 9 | logger.error(f"{X} -> %s", Y) +10 | logger.error(f"{Y} -> %s", X) + | ^^^^^^^^^^^^ + | +help: Convert to lazy `%` formatting diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs b/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs index 51cc9f436f..21e22bf2f7 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs +++ b/crates/ruff_linter/src/rules/flake8_logging_format/violations.rs @@ -327,10 +327,16 @@ impl Violation for LoggingStringConcat { pub(crate) struct LoggingFString; impl Violation for LoggingFString { + const FIX_AVAILABILITY: crate::FixAvailability = crate::FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Logging statement uses f-string".to_string() } + + fn fix_title(&self) -> Option { + Some("Convert to lazy `%` formatting".to_string()) + } } /// ## What it does