diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py index 3ee96bd225..75818e741b 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py @@ -102,3 +102,8 @@ deque("abc") # OK deque(b"abc") # OK deque(f"" "a") # OK deque(f"{x}" "") # OK + +# https://github.com/astral-sh/ruff/issues/19951 +deque(t"") +deque(t"" t"") +deque(t"{""}") # OK diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs index 51561e4e1f..88374119c8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs @@ -103,6 +103,7 @@ pub(crate) fn unnecessary_literal_within_deque_call(checker: &Checker, deque: &a Expr::StringLiteral(string) => string.value.is_empty(), Expr::BytesLiteral(bytes) => bytes.value.is_empty(), Expr::FString(fstring) => fstring.value.is_empty_literal(), + Expr::TString(tstring) => tstring.value.is_empty_iterable(), _ => false, }; if !is_empty_literal { diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap index f396239316..97b71fd4a5 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap @@ -383,3 +383,42 @@ help: Replace with `deque()` 101 101 | deque("abc") # OK 102 102 | deque(b"abc") # 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 diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 3e241b8252..6de571a86e 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -515,7 +515,7 @@ impl FStringValue { /// 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 /// f-string, not whether the f-string has 0 parts inside it. pub fn is_empty_literal(&self) -> bool { @@ -681,6 +681,22 @@ impl TStringValue { pub fn elements(&self) -> impl Iterator { 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 { @@ -1182,6 +1198,10 @@ impl TString { pub fn quote_style(&self) -> Quote { self.flags.quote_style() } + + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } } impl From for Expr {