[ruff] Handle empty t-strings in unnecessary-empty-iterable-within-deque-call (RUF037) (#20045)

Adds a method to `TStringValue` to detect whether the t-string is empty
_as an iterable_. Note the subtlety here that, unlike f-strings, an
empty t-string is still truthy (i.e. `bool(t"")==True`).

Closes #19951
This commit is contained in:
Dylan 2025-08-22 10:23:49 -05:00 committed by GitHub
parent 0e9d77e43a
commit 0b6ce1c788
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 66 additions and 1 deletions

View file

@ -102,3 +102,8 @@ deque("abc") # OK
deque(b"abc") # OK deque(b"abc") # OK
deque(f"" "a") # OK deque(f"" "a") # OK
deque(f"{x}" "") # OK deque(f"{x}" "") # OK
# https://github.com/astral-sh/ruff/issues/19951
deque(t"")
deque(t"" t"")
deque(t"{""}") # OK

View file

@ -103,6 +103,7 @@ pub(crate) fn unnecessary_literal_within_deque_call(checker: &Checker, deque: &a
Expr::StringLiteral(string) => string.value.is_empty(), Expr::StringLiteral(string) => string.value.is_empty(),
Expr::BytesLiteral(bytes) => bytes.value.is_empty(), Expr::BytesLiteral(bytes) => bytes.value.is_empty(),
Expr::FString(fstring) => fstring.value.is_empty_literal(), Expr::FString(fstring) => fstring.value.is_empty_literal(),
Expr::TString(tstring) => tstring.value.is_empty_iterable(),
_ => false, _ => false,
}; };
if !is_empty_literal { if !is_empty_literal {

View file

@ -383,3 +383,42 @@ help: Replace with `deque()`
101 101 | deque("abc") # OK 101 101 | deque("abc") # OK
102 102 | deque(b"abc") # OK 102 102 | deque(b"abc") # OK
103 103 | deque(f"" "a") # OK 103 103 | deque(f"" "a") # OK
RUF037 [*] Unnecessary empty iterable within a deque call
--> RUF037.py:107:1
|
106 | # https://github.com/astral-sh/ruff/issues/19951
107 | deque(t"")
| ^^^^^^^^^^
108 | deque(t"" t"")
109 | deque(t"{""}") # OK
|
help: Replace with `deque()`
Safe fix
104 104 | deque(f"{x}" "") # OK
105 105 |
106 106 | # https://github.com/astral-sh/ruff/issues/19951
107 |-deque(t"")
107 |+deque()
108 108 | deque(t"" t"")
109 109 | deque(t"{""}") # OK
RUF037 [*] Unnecessary empty iterable within a deque call
--> RUF037.py:108:1
|
106 | # https://github.com/astral-sh/ruff/issues/19951
107 | deque(t"")
108 | deque(t"" t"")
| ^^^^^^^^^^^^^^^
109 | deque(t"{""}") # OK
|
help: Replace with `deque()`
Safe fix
105 105 |
106 106 | # https://github.com/astral-sh/ruff/issues/19951
107 107 | deque(t"")
108 |-deque(t"" t"")
108 |+deque()
109 109 | deque(t"{""}") # OK

View file

@ -515,7 +515,7 @@ impl FStringValue {
/// Returns `true` if the node represents an empty f-string literal. /// Returns `true` if the node represents an empty f-string literal.
/// ///
/// Noteh that a [`FStringValue`] node will always have >= 1 [`FStringPart`]s inside it. /// Note that a [`FStringValue`] node will always have >= 1 [`FStringPart`]s inside it.
/// This method checks whether the value of the concatenated parts is equal to the empty /// This method checks whether the value of the concatenated parts is equal to the empty
/// f-string, not whether the f-string has 0 parts inside it. /// f-string, not whether the f-string has 0 parts inside it.
pub fn is_empty_literal(&self) -> bool { pub fn is_empty_literal(&self) -> bool {
@ -681,6 +681,22 @@ impl TStringValue {
pub fn elements(&self) -> impl Iterator<Item = &InterpolatedStringElement> { pub fn elements(&self) -> impl Iterator<Item = &InterpolatedStringElement> {
self.iter().flat_map(|tstring| tstring.elements.iter()) self.iter().flat_map(|tstring| tstring.elements.iter())
} }
/// Returns `true` if the node represents an empty t-string in the
/// sense that `__iter__` returns an empty iterable.
///
/// Beware that empty t-strings are still truthy, i.e. `bool(t"") == True`.
///
/// Note that a [`TStringValue`] node will always contain at least one
/// [`TString`] node. This method checks whether each of the constituent
/// t-strings (in an implicitly concatenated t-string) are empty
/// in the above sense.
pub fn is_empty_iterable(&self) -> bool {
match &self.inner {
TStringValueInner::Single(tstring) => tstring.is_empty(),
TStringValueInner::Concatenated(tstrings) => tstrings.iter().all(TString::is_empty),
}
}
} }
impl<'a> IntoIterator for &'a TStringValue { impl<'a> IntoIterator for &'a TStringValue {
@ -1182,6 +1198,10 @@ impl TString {
pub fn quote_style(&self) -> Quote { pub fn quote_style(&self) -> Quote {
self.flags.quote_style() self.flags.quote_style()
} }
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
} }
impl From<TString> for Expr { impl From<TString> for Expr {