From 30e903bac6c400d20ac61b215895ad23b30f7e54 Mon Sep 17 00:00:00 2001 From: Harshal Patel Date: Sat, 18 Jul 2026 02:41:27 +0530 Subject: [PATCH 1/2] Fix false positive in RUF065 with unpacked arguments RUF065 assumes a strict 1:1 positional mapping between format string specifiers and logging arguments. That breaks down when a starred expression (like `*args`) is passed. When we hit a starred argument, the linter gets misaligned and flags unrelated eager conversions. To fix this, we just bail out of the argument loop as soon as we see a starred expression. This stops false positives without breaking legitimate checks for arguments passed *before* the starred one. Fixes #26912 --- .../mdtest/ruff/logging-eager-conversion.md | 43 +++++++++++++++++++ .../resources/test/fixtures/ruff/RUF065_0.py | 1 + .../ruff/rules/logging_eager_conversion.rs | 9 +++- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md diff --git a/crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md b/crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md new file mode 100644 index 00000000000000..800d2776e64e79 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md @@ -0,0 +1,43 @@ +# `logging-eager-conversion` (`RUF065`) + +```toml +lint.preview = true +lint.select = ["RUF065"] +``` + +## Unpacked arguments + +The presence of a starred expression (`*args`) breaks the positional mapping between format string specifiers and variadic logging arguments. Ensure eager conversions *before* the starred argument are still flagged, but bail out on ambiguous cases *after* it. + +```py +import logging + +# 1. Starred before eager conversion (should not trigger for repr("5") because the mapping is broken) +logging.warning("%s%s%s%s %s", *"1234", repr("5")) + +# 2. Eager conversion before starred (should trigger for repr("1") because it maps reliably) +logging.warning("%s %s", repr("1"), *["1234"]) # snapshot: logging-eager-conversion + +# 3. Multiple starred arguments (should not trigger anywhere) +logging.warning("%s %s %s", *["1"], *["2"], repr("3")) + +# 4. Mixed specifiers and eager conversion before starred (should trigger for repr("1")) +logging.warning("%s %s %s", repr("1"), *["2", "3"]) # snapshot: logging-eager-conversion +``` + +```snapshot +error[RUF065]: Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> src/mdtest_snippet.py:7:26 + | +7 | logging.warning("%s %s", repr("1"), *["1234"]) # snapshot: logging-eager-conversion + | ^^^^^^^^^ + | + + +error[RUF065]: Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> src/mdtest_snippet.py:13:29 + | +13 | logging.warning("%s %s %s", repr("1"), *["2", "3"]) # snapshot: logging-eager-conversion + | ^^^^^^^^^ + | +``` diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF065_0.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF065_0.py index 879fb186bf543b..6cdf1011feacf9 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF065_0.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF065_0.py @@ -69,3 +69,4 @@ def str(s): return f"str = {s}" info("Hex: %s", hex(42)) log(logging.INFO, "Hex: %s", hex(255)) + diff --git a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs index 920f6c73a5e644..3d8f8ab45ddfb7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs @@ -127,7 +127,6 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall) return; }; - // Iterate over % placeholders in format string and zip with logging statement arguments for (spec, arg) in format_string .iter() .filter_map(|(_, part)| { @@ -137,7 +136,13 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall) None } }) - .zip(call.arguments.args.iter().skip(msg_pos + 1)) + .zip( + call.arguments + .args + .iter() + .skip(msg_pos + 1) + .take_while(|arg| !arg.is_starred_expr()), + ) { // Check if the argument is a call to eagerly format a value if let Expr::Call(ast::ExprCall { From de93463f73e9d8f2418dd0d09b53ee8997725f0a Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Fri, 24 Jul 2026 13:02:01 -0400 Subject: [PATCH 2/2] revert newline and preserve comment --- crates/ruff_linter/resources/test/fixtures/ruff/RUF065_0.py | 1 - .../ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF065_0.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF065_0.py index 6cdf1011feacf9..879fb186bf543b 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF065_0.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF065_0.py @@ -69,4 +69,3 @@ def str(s): return f"str = {s}" info("Hex: %s", hex(42)) log(logging.INFO, "Hex: %s", hex(255)) - diff --git a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs index 3d8f8ab45ddfb7..861ee00c3d9c02 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs @@ -127,6 +127,7 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall) return; }; + // Iterate over % placeholders in format string and zip with logging statement arguments for (spec, arg) in format_string .iter() .filter_map(|(_, part)| {