[refurb] Detect empty f-strings (FURB105) (#21348)

## Summary

Fixes FURB105 (`print-empty-string`) to detect empty f-strings in
addition to regular empty strings. Previously, the rule only flagged
`print("")` but missed `print(f"")`. This fix ensures both cases are
detected and can be automatically fixed.

Fixes #21346

## Problem Analysis

The FURB105 rule checks for unnecessary empty strings passed to
`print()` calls. The `is_empty_string` helper function was only checking
for `Expr::StringLiteral` with empty values, but did not handle
`Expr::FString` (f-strings). As a result, `print(f"")` was not being
flagged as a violation, even though it's semantically equivalent to
`print("")` and should be simplified to `print()`.

The issue occurred because the function used a `matches!` macro that
only checked for string literals:

```rust
fn is_empty_string(expr: &Expr) -> bool {
    matches!(
        expr,
        Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) if value.is_empty()
    )
}
```

## Approach

1. **Import the helper function**: Added `is_empty_f_string` to the
imports from `ruff_python_ast::helpers`, which already provides logic to
detect empty f-strings.

2. **Update `is_empty_string` function**: Changed the implementation
from a `matches!` macro to a `match` expression that handles both string
literals and f-strings:

   ```rust
   fn is_empty_string(expr: &Expr) -> bool {
       match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) =>
value.is_empty(),
           Expr::FString(f_string) => is_empty_f_string(f_string),
           _ => false,
       }
   }
   ```

The fix leverages the existing `is_empty_f_string` helper function which
properly handles the complexity of f-strings, including nested f-strings
and interpolated expressions. This ensures the detection is accurate and
consistent with how empty strings are detected elsewhere in the
codebase.
This commit is contained in:
Dan Parizher 2025-11-10 12:41:44 -05:00 committed by GitHub
parent 1d188476b6
commit e4dc406a3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 78 additions and 16 deletions

View file

@ -19,6 +19,9 @@ print("", *args, sep="")
print("", **kwargs)
print(sep="\t")
print(sep=print(1))
print(f"")
print(f"", sep=",")
print(f"", end="bar")
# OK.
@ -33,3 +36,4 @@ print("foo", "", sep=",")
print("foo", "", "bar", "", sep=",")
print("", "", **kwargs)
print(*args, sep=",")
print(f"foo")

View file

@ -1,5 +1,5 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::helpers::{contains_effect, is_empty_f_string};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_codegen::Generator;
use ruff_python_semantic::SemanticModel;
@ -194,13 +194,11 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) {
/// Check if an expression is a constant empty string.
fn is_empty_string(expr: &Expr) -> bool {
matches!(
expr,
Expr::StringLiteral(ast::ExprStringLiteral {
value,
..
}) if value.is_empty()
)
match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.is_empty(),
Expr::FString(f_string) => is_empty_f_string(f_string),
_ => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View file

@ -317,7 +317,7 @@ help: Remove empty string
19 + print(**kwargs)
20 | print(sep="\t")
21 | print(sep=print(1))
22 |
22 | print(f"")
FURB105 [*] Unnecessary separator passed to `print`
--> FURB105.py:20:1
@ -327,6 +327,7 @@ FURB105 [*] Unnecessary separator passed to `print`
20 | print(sep="\t")
| ^^^^^^^^^^^^^^^
21 | print(sep=print(1))
22 | print(f"")
|
help: Remove separator
17 | print("", *args)
@ -335,8 +336,8 @@ help: Remove separator
- print(sep="\t")
20 + print()
21 | print(sep=print(1))
22 |
23 | # OK.
22 | print(f"")
23 | print(f"", sep=",")
FURB105 [*] Unnecessary separator passed to `print`
--> FURB105.py:21:1
@ -345,8 +346,8 @@ FURB105 [*] Unnecessary separator passed to `print`
20 | print(sep="\t")
21 | print(sep=print(1))
| ^^^^^^^^^^^^^^^^^^^
22 |
23 | # OK.
22 | print(f"")
23 | print(f"", sep=",")
|
help: Remove separator
18 | print("", *args, sep="")
@ -354,7 +355,66 @@ help: Remove separator
20 | print(sep="\t")
- print(sep=print(1))
21 + print()
22 |
23 | # OK.
24 |
22 | print(f"")
23 | print(f"", sep=",")
24 | print(f"", end="bar")
note: This is an unsafe fix and may change runtime behavior
FURB105 [*] Unnecessary empty string passed to `print`
--> FURB105.py:22:1
|
20 | print(sep="\t")
21 | print(sep=print(1))
22 | print(f"")
| ^^^^^^^^^^
23 | print(f"", sep=",")
24 | print(f"", end="bar")
|
help: Remove empty string
19 | print("", **kwargs)
20 | print(sep="\t")
21 | print(sep=print(1))
- print(f"")
22 + print()
23 | print(f"", sep=",")
24 | print(f"", end="bar")
25 |
FURB105 [*] Unnecessary empty string and separator passed to `print`
--> FURB105.py:23:1
|
21 | print(sep=print(1))
22 | print(f"")
23 | print(f"", sep=",")
| ^^^^^^^^^^^^^^^^^^^
24 | print(f"", end="bar")
|
help: Remove empty string and separator
20 | print(sep="\t")
21 | print(sep=print(1))
22 | print(f"")
- print(f"", sep=",")
23 + print()
24 | print(f"", end="bar")
25 |
26 | # OK.
FURB105 [*] Unnecessary empty string passed to `print`
--> FURB105.py:24:1
|
22 | print(f"")
23 | print(f"", sep=",")
24 | print(f"", end="bar")
| ^^^^^^^^^^^^^^^^^^^^^
25 |
26 | # OK.
|
help: Remove empty string
21 | print(sep=print(1))
22 | print(f"")
23 | print(f"", sep=",")
- print(f"", end="bar")
24 + print(end="bar")
25 |
26 | # OK.
27 |