From ca7933804e31330f533544cea7e372e15673e1bf Mon Sep 17 00:00:00 2001 From: Victor Hugo Gomes Date: Tue, 24 Jun 2025 03:26:28 -0300 Subject: [PATCH] [`ruff`] Trigger `RUF037` for empty string and byte strings (#18862) --- .../resources/test/fixtures/ruff/RUF037.py | 13 ++- .../unnecessary_literal_within_deque_call.rs | 3 + ..._rules__ruff__tests__RUF037_RUF037.py.snap | 109 ++++++++++++++++++ crates/ruff_python_ast/src/nodes.rs | 21 ++++ 4 files changed, 145 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py index 18807d4794..3ee96bd225 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF037.py @@ -1,5 +1,5 @@ -from collections import deque import collections +from collections import deque def f(): @@ -91,3 +91,14 @@ def f(): def f(): deque([], iterable=[]) + +# https://github.com/astral-sh/ruff/issues/18854 +deque("") +deque(b"") +deque(f"") +deque(f"" "") +deque(f"" f"") +deque("abc") # OK +deque(b"abc") # OK +deque(f"" "a") # OK +deque(f"{x}" "") # 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 846dd38da8..51561e4e1f 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 @@ -100,6 +100,9 @@ pub(crate) fn unnecessary_literal_within_deque_call(checker: &Checker, deque: &a }) && call.arguments.is_empty() } + Expr::StringLiteral(string) => string.value.is_empty(), + Expr::BytesLiteral(bytes) => bytes.value.is_empty(), + Expr::FString(fstring) => fstring.value.is_empty_literal(), _ => 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 3b8f271e28..caf3f33b3d 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 @@ -246,6 +246,8 @@ RUF037.py:93:5: RUF037 [*] Unnecessary empty iterable within a deque call 92 | def f(): 93 | deque([], iterable=[]) | ^^^^^^^^^^^^^^^^^^^^^^ RUF037 +94 | +95 | # https://github.com/astral-sh/ruff/issues/18854 | = help: Replace with `deque()` @@ -255,3 +257,110 @@ RUF037.py:93:5: RUF037 [*] Unnecessary empty iterable within a deque call 92 92 | def f(): 93 |- deque([], iterable=[]) 93 |+ deque([]) +94 94 | +95 95 | # https://github.com/astral-sh/ruff/issues/18854 +96 96 | deque("") + +RUF037.py:96:1: RUF037 [*] Unnecessary empty iterable within a deque call + | +95 | # https://github.com/astral-sh/ruff/issues/18854 +96 | deque("") + | ^^^^^^^^^ RUF037 +97 | deque(b"") +98 | deque(f"") + | + = help: Replace with `deque()` + +ℹ Safe fix +93 93 | deque([], iterable=[]) +94 94 | +95 95 | # https://github.com/astral-sh/ruff/issues/18854 +96 |-deque("") + 96 |+deque() +97 97 | deque(b"") +98 98 | deque(f"") +99 99 | deque(f"" "") + +RUF037.py:97:1: RUF037 [*] Unnecessary empty iterable within a deque call + | +95 | # https://github.com/astral-sh/ruff/issues/18854 +96 | deque("") +97 | deque(b"") + | ^^^^^^^^^^ RUF037 +98 | deque(f"") +99 | deque(f"" "") + | + = help: Replace with `deque()` + +ℹ Safe fix +94 94 | +95 95 | # https://github.com/astral-sh/ruff/issues/18854 +96 96 | deque("") +97 |-deque(b"") + 97 |+deque() +98 98 | deque(f"") +99 99 | deque(f"" "") +100 100 | deque(f"" f"") + +RUF037.py:98:1: RUF037 [*] Unnecessary empty iterable within a deque call + | + 96 | deque("") + 97 | deque(b"") + 98 | deque(f"") + | ^^^^^^^^^^ RUF037 + 99 | deque(f"" "") +100 | deque(f"" f"") + | + = help: Replace with `deque()` + +ℹ Safe fix +95 95 | # https://github.com/astral-sh/ruff/issues/18854 +96 96 | deque("") +97 97 | deque(b"") +98 |-deque(f"") + 98 |+deque() +99 99 | deque(f"" "") +100 100 | deque(f"" f"") +101 101 | deque("abc") # OK + +RUF037.py:99:1: RUF037 [*] Unnecessary empty iterable within a deque call + | + 97 | deque(b"") + 98 | deque(f"") + 99 | deque(f"" "") + | ^^^^^^^^^^^^^ RUF037 +100 | deque(f"" f"") +101 | deque("abc") # OK + | + = help: Replace with `deque()` + +ℹ Safe fix +96 96 | deque("") +97 97 | deque(b"") +98 98 | deque(f"") +99 |-deque(f"" "") + 99 |+deque() +100 100 | deque(f"" f"") +101 101 | deque("abc") # OK +102 102 | deque(b"abc") # OK + +RUF037.py:100:1: RUF037 [*] Unnecessary empty iterable within a deque call + | + 98 | deque(f"") + 99 | deque(f"" "") +100 | deque(f"" f"") + | ^^^^^^^^^^^^^^ RUF037 +101 | deque("abc") # OK +102 | deque(b"abc") # OK + | + = help: Replace with `deque()` + +ℹ Safe fix +97 97 | deque(b"") +98 98 | deque(f"") +99 99 | deque(f"" "") +100 |-deque(f"" f"") + 100 |+deque() +101 101 | deque("abc") # OK +102 102 | deque(b"abc") # OK +103 103 | deque(f"" "a") # OK diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 9520765197..6217b27720 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -504,6 +504,20 @@ impl FStringValue { pub fn elements(&self) -> impl Iterator { self.f_strings().flat_map(|fstring| fstring.elements.iter()) } + + /// Returns `true` if the node represents an empty f-string literal. + /// + /// Noteh 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 { + match &self.inner { + FStringValueInner::Single(fstring_part) => fstring_part.is_empty_literal(), + FStringValueInner::Concatenated(fstring_parts) => { + fstring_parts.iter().all(FStringPart::is_empty_literal) + } + } + } } impl<'a> IntoIterator for &'a FStringValue { @@ -550,6 +564,13 @@ impl FStringPart { Self::FString(f_string) => f_string.flags.quote_style(), } } + + pub fn is_empty_literal(&self) -> bool { + match &self { + FStringPart::Literal(string_literal) => string_literal.value.is_empty(), + FStringPart::FString(f_string) => f_string.elements.is_empty(), + } + } } impl Ranged for FStringPart {