Implement unnecessary-if (RUF050) (#24114)

This commit is contained in:
Daniil Sivak authored and GitHub committed 2026-03-25 17:57:43 +01:00
1 parent 7e8fb8adec
commit 3a44cce48e
14 files changed
+886

No files matched your search

@@ -0,0 +1,124 @@
### Errors (condition removed entirely)
# Simple if with pass
if True:
pass
# Simple if with ellipsis
if True:
...
# Side-effect-free condition (comparison)
import sys
if sys.version_info >= (3, 11):
pass
# Side-effect-free condition (boolean operator)
if x and y:
pass
# Nested in function
def nested():
if a:
pass
# Single-line form (pass)
if True: pass
# Single-line form (ellipsis)
if True: ...
# Multiple pass statements
if True:
pass
pass
# Mixed pass and ellipsis
if True:
pass
...
# Only statement in a with block
with pytest.raises(ValueError, match=msg):
if obj1:
pass
### Errors (condition preserved as expression statement)
# Function call
if foo():
pass
# Method call
if bar.baz():
pass
# Nested call in boolean operator
if x and foo():
pass
# Walrus operator with call
if (x := foo()):
pass
# Walrus operator without call
if (x := y):
pass
# Only statement in a suite
class Foo:
if foo():
pass
### No errors
# Non-empty body
if True:
bar()
# Body with non-stub statement
if True:
pass
foo()
# Has elif clause (handled by RUF047)
if True:
pass
elif True:
pass
# Has else clause (handled by RUF047)
if True:
pass
else:
pass
# TYPE_CHECKING block (handled by TC005)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
pass
# Comment in body
if True:
# comment
pass
# Inline comment after pass
if True:
pass # comment
# Inline comment on if line
if True: # comment
pass
# Trailing comment at body indentation
if True:
pass
# trailing comment
# Trailing comment deeper than body indentation
if True:
pass
# deeper trailing comment
@@ -0,0 +1,18 @@
# Reproduces the scenario from issue #9472:
# F401 removes unused imports leaving empty `if` blocks,
# RUF050 removes those blocks, then F401 cleans up the
# now-unused guard imports on subsequent fix iterations.
import os
import sys
# F401 removes the unused `ExceptionGroup` import, leaving `pass`.
# Then RUF050 removes the empty `if`, and F401 removes `sys`.
if sys.version_info < (3, 11):
from exceptiongroup import ExceptionGroup
# Already-empty block handled in a single pass by RUF050
if sys.version_info < (3, 11):
pass
print(os.getcwd())
@@ -0,0 +1,31 @@
### Errors (both RUF047 and RUF050 converge)
# RUF047 removes the else, then RUF050 removes the remaining if.
if sys.version_info >= (3, 11):
pass
else:
pass
# Same with elif.
if sys.version_info >= (3, 11):
pass
elif sys.version_info >= (3, 10):
pass
else:
pass
# Side-effect in condition: RUF047 removes the else, then RUF050
# replaces the remaining `if` with the condition expression
if foo():
pass
else:
pass
### No errors
# Non-empty if body: neither rule fires.
if sys.version_info >= (3, 11):
bar()
else:
baz()
@@ -1122,6 +1122,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.is_rule_enabled(Rule::NeedlessElse) {
ruff::rules::needless_else(checker, if_.into());
}
if checker.is_rule_enabled(Rule::UnnecessaryIf) {
ruff::rules::unnecessary_if(checker, if_);
}
}
Stmt::Assert(
assert_stmt @ ast::StmtAssert {
+1
View File
@@ -1049,6 +1049,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "047") => rules::ruff::rules::NeedlessElse,
(Ruff, "048") => rules::ruff::rules::MapIntVersionParsing,
(Ruff, "049") => rules::ruff::rules::DataclassEnum,
(Ruff, "050") => rules::ruff::rules::UnnecessaryIf,
(Ruff, "051") => rules::ruff::rules::IfKeyInDictDel,
(Ruff, "052") => rules::ruff::rules::UsedDummyVariable,
(Ruff, "053") => rules::ruff::rules::ClassWithMixedTypeVars,
+42
View File
@@ -98,6 +98,7 @@ mod tests {
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048.py"))]
#[test_case(Rule::MapIntVersionParsing, Path::new("RUF048_1.py"))]
#[test_case(Rule::DataclassEnum, Path::new("RUF049.py"))]
#[test_case(Rule::UnnecessaryIf, Path::new("RUF050.py"))]
#[test_case(Rule::IfKeyInDictDel, Path::new("RUF051.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_0.py"))]
#[test_case(Rule::UsedDummyVariable, Path::new("RUF052_1.py"))]
@@ -199,6 +200,47 @@ mod tests {
Ok(())
}
/// Test that RUF047 (needless-else) and RUF050 (unnecessary-if) converge
/// when both are enabled: RUF047 removes the empty `else` first, then
/// RUF050 removes the remaining empty `if` on the next fix iteration.
#[test]
fn unnecessary_if_and_needless_else() -> Result<()> {
use ruff_python_ast::{PySourceType, SourceType};
let path = test_resource_path("fixtures").join("ruff/RUF050_RUF047.py");
let source_type = SourceType::Python(PySourceType::from(&path));
let source_kind = SourceKind::from_path(&path, source_type)?.expect("valid source");
let settings =
settings::LinterSettings::for_rules(vec![Rule::NeedlessElse, Rule::UnnecessaryIf]);
let (diagnostics, transformed) = test_contents(&source_kind, &path, &settings);
assert_diagnostics!(diagnostics);
insta::assert_snapshot!(transformed.source_code());
Ok(())
}
/// Reproduces issue #9472: F401 removes unused imports leaving empty `if`
/// blocks, then RUF050 removes those, then F401 cleans up the now-unused
/// guard imports. Verifies the full chain converges and produces the
/// expected output.
#[test]
fn unnecessary_if_and_unused_import() -> Result<()> {
use ruff_python_ast::{PySourceType, SourceType};
let path = test_resource_path("fixtures").join("ruff/RUF050_F401.py");
let source_type = SourceType::Python(PySourceType::from(&path));
let source_kind = SourceKind::from_path(&path, source_type)?.expect("valid source");
let settings =
settings::LinterSettings::for_rules(vec![Rule::UnusedImport, Rule::UnnecessaryIf]);
let (diagnostics, transformed) = test_contents(&source_kind, &path, &settings);
assert_diagnostics!(diagnostics);
insta::assert_snapshot!(transformed.source_code());
Ok(())
}
#[test]
fn missing_fstring_syntax_backslash_py311() -> Result<()> {
assert_diagnostics_diff!(
@@ -55,6 +55,7 @@ pub(crate) use test_rules::*;
pub(crate) use unmatched_suppression_comment::*;
pub(crate) use unnecessary_assign_before_yield::*;
pub(crate) use unnecessary_cast_to_int::*;
pub(crate) use unnecessary_if::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*;
pub(crate) use unnecessary_literal_within_deque_call::*;
@@ -131,6 +132,7 @@ pub(crate) mod test_rules;
mod unmatched_suppression_comment;
mod unnecessary_assign_before_yield;
mod unnecessary_cast_to_int;
mod unnecessary_if;
mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check;
mod unnecessary_literal_within_deque_call;
@@ -0,0 +1,187 @@
use std::cmp::Ordering;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::{
any_over_expr, comment_indentation_after, contains_effect, is_stub_body,
};
use ruff_python_ast::token::TokenKind;
use ruff_python_ast::whitespace::indentation;
use ruff_python_ast::{Expr, StmtIf};
use ruff_python_semantic::analyze::typing;
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::checkers::ast::Checker;
use crate::{AlwaysFixableViolation, Edit, Fix, fix};
/// ## What it does
/// Checks for `if` statements (without `elif` or `else` branches) where the
/// body contains only `pass` or `...`
///
/// ## Why is this bad?
/// An `if` statement with an empty body either does nothing (when the
/// condition is side-effect-free) or could be replaced with just the
/// condition expression (when it has side effects). This pattern commonly
/// arises when auto-fixers remove unused imports from conditional blocks
/// (e.g., version-dependent imports), leaving behind an empty skeleton.
///
/// ## Example
/// ```python
/// import sys
///
/// if sys.version_info >= (3, 11):
/// pass
/// ```
///
/// Use instead:
/// ```python
/// import sys
/// ```
///
/// ## Fix safety
/// When the condition is side-effect-free, the fix removes the entire `if`
/// statement.
///
/// When the condition has side effects (e.g., a function call), the fix
/// replaces the `if` statement with just the condition as an expression
/// statement, preserving the side effects.
///
/// Note: conditions consisting solely of a name expression (like
/// `if x: pass`) are treated as side-effect-free, even though `if x`
/// implicitly calls `x.__bool__()` (or `x.__len__()`), which could have
/// side effects if overridden. In practice this is very rare, but if you
/// rely on this behavior, suppress the diagnostic with `# noqa: RUF050`.
///
/// ## Related rules
/// - [`needless-else (RUF047)`]: Detects empty `else` clauses. For `if`/`else`
/// statements where all branches are empty, `RUF047` first removes the empty
/// `else`, and then this rule catches the remaining empty `if`.
/// - [`empty-type-checking-block (TC005)`]: Detects empty `if TYPE_CHECKING`
/// blocks specifically.
#[derive(ViolationMetadata)]
#[violation_metadata(preview_since = "NEXT_RUFF_VERSION")]
pub(crate) struct UnnecessaryIf;
impl AlwaysFixableViolation for UnnecessaryIf {
#[derive_message_formats]
fn message(&self) -> String {
"Empty `if` statement".to_string()
}
fn fix_title(&self) -> String {
"Remove the `if` statement".to_string()
}
}
/// RUF050
pub(crate) fn unnecessary_if(checker: &Checker, stmt: &StmtIf) {
let StmtIf {
test,
body,
elif_else_clauses,
..
} = stmt;
// Only handle bare `if` blocks — `elif`/`else` branches are handled by
// RUF047 (needless-else)
if !elif_else_clauses.is_empty() {
return;
}
if !is_stub_body(body) {
return;
}
// Skip `if TYPE_CHECKING` blocks — handled by TC005
if typing::is_type_checking_block(stmt, checker.semantic()) {
return;
}
// Skip if the body contains a comment
if if_contains_comments(stmt, checker) {
return;
}
let has_side_effects = contains_effect(test, |id| checker.semantic().has_builtin_binding(id))
|| any_over_expr(test, &|expr| matches!(expr, Expr::Named(_)));
let mut diagnostic = checker.report_diagnostic(UnnecessaryIf, stmt.range());
if has_side_effects {
// Replace `if cond: pass` with `cond` as an expression statement.
// Walrus operators need parentheses to be valid as statements.
let condition_text = checker.locator().slice(test.range());
let replacement = if test.is_named_expr() {
format!("({condition_text})")
} else {
condition_text.to_string()
};
let edit = Edit::range_replacement(replacement, stmt.range());
diagnostic.set_fix(Fix::safe_edit(edit));
} else {
let stmt_ref = checker.semantic().current_statement();
let parent = checker.semantic().current_statement_parent();
let edit = fix::edits::delete_stmt(stmt_ref, parent, checker.locator(), checker.indexer());
let fix = Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().current_statement_parent_id(),
));
diagnostic.set_fix(fix);
}
}
/// Returns `true` if the `if` statement contains a comment
fn if_contains_comments(stmt: &StmtIf, checker: &Checker) -> bool {
let source = checker.source();
// Use `line_end` (before the newline) instead of `full_line_end` (after
// the newline) to avoid touching the range of a comment on the next line.
// `TextRange::intersect` considers touching ranges as intersecting.
let stmt_line_end = source.line_end(stmt.end());
let check_range = TextRange::new(stmt.start(), stmt_line_end);
let Some(last_stmt) = stmt.body.last() else {
return false;
};
let stmt_full_end = source.full_line_end(stmt.end());
checker.comment_ranges().intersects(check_range)
|| if_has_trailing_comment(stmt, last_stmt, stmt_full_end, checker)
}
/// Returns `true` if the `if` branch has a trailing own-line comment
fn if_has_trailing_comment(
stmt: &StmtIf,
last_body_stmt: &ruff_python_ast::Stmt,
stmt_full_end: TextSize,
checker: &Checker,
) -> bool {
let (tokens, source) = (checker.tokens(), checker.source());
// Compare against the `if` keyword indentation rather than the body
// statement — handles single-line forms like `if True: pass`
let stmt_indentation = indentation(source, stmt).unwrap_or_default().text_len();
for token in tokens.after(stmt_full_end) {
match token.kind() {
TokenKind::Comment => {
let comment_indentation =
comment_indentation_after(last_body_stmt.into(), token.range(), source);
match comment_indentation.cmp(&stmt_indentation) {
Ordering::Greater => return true,
Ordering::Equal | Ordering::Less => break,
}
}
TokenKind::NonLogicalNewline
| TokenKind::Newline
| TokenKind::Indent
| TokenKind::Dedent => {}
_ => break,
}
}
false
}
@@ -0,0 +1,330 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF050 [*] Empty `if` statement
--> RUF050.py:4:1
|
3 | # Simple if with pass
4 | / if True:
5 | | pass
| |________^
6 |
7 | # Simple if with ellipsis
|
help: Remove the `if` statement
1 | ### Errors (condition removed entirely)
2 |
3 | # Simple if with pass
- if True:
- pass
4 |
5 | # Simple if with ellipsis
6 | if True:
RUF050 [*] Empty `if` statement
--> RUF050.py:8:1
|
7 | # Simple if with ellipsis
8 | / if True:
9 | | ...
| |_______^
10 |
11 | # Side-effect-free condition (comparison)
|
help: Remove the `if` statement
5 | pass
6 |
7 | # Simple if with ellipsis
- if True:
- ...
8 |
9 | # Side-effect-free condition (comparison)
10 | import sys
RUF050 [*] Empty `if` statement
--> RUF050.py:13:1
|
11 | # Side-effect-free condition (comparison)
12 | import sys
13 | / if sys.version_info >= (3, 11):
14 | | pass
| |________^
15 |
16 | # Side-effect-free condition (boolean operator)
|
help: Remove the `if` statement
10 |
11 | # Side-effect-free condition (comparison)
12 | import sys
- if sys.version_info >= (3, 11):
- pass
13 |
14 | # Side-effect-free condition (boolean operator)
15 | if x and y:
RUF050 [*] Empty `if` statement
--> RUF050.py:17:1
|
16 | # Side-effect-free condition (boolean operator)
17 | / if x and y:
18 | | pass
| |________^
19 |
20 | # Nested in function
|
help: Remove the `if` statement
14 | pass
15 |
16 | # Side-effect-free condition (boolean operator)
- if x and y:
- pass
17 |
18 | # Nested in function
19 | def nested():
RUF050 [*] Empty `if` statement
--> RUF050.py:22:5
|
20 | # Nested in function
21 | def nested():
22 | / if a:
23 | | pass
| |____________^
24 |
25 | # Single-line form (pass)
|
help: Remove the `if` statement
19 |
20 | # Nested in function
21 | def nested():
- if a:
- pass
22 + pass
23 |
24 | # Single-line form (pass)
25 | if True: pass
RUF050 [*] Empty `if` statement
--> RUF050.py:26:1
|
25 | # Single-line form (pass)
26 | if True: pass
| ^^^^^^^^^^^^^
27 |
28 | # Single-line form (ellipsis)
|
help: Remove the `if` statement
23 | pass
24 |
25 | # Single-line form (pass)
- if True: pass
26 |
27 | # Single-line form (ellipsis)
28 | if True: ...
RUF050 [*] Empty `if` statement
--> RUF050.py:29:1
|
28 | # Single-line form (ellipsis)
29 | if True: ...
| ^^^^^^^^^^^^
30 |
31 | # Multiple pass statements
|
help: Remove the `if` statement
26 | if True: pass
27 |
28 | # Single-line form (ellipsis)
- if True: ...
29 |
30 | # Multiple pass statements
31 | if True:
RUF050 [*] Empty `if` statement
--> RUF050.py:32:1
|
31 | # Multiple pass statements
32 | / if True:
33 | | pass
34 | | pass
| |________^
35 |
36 | # Mixed pass and ellipsis
|
help: Remove the `if` statement
29 | if True: ...
30 |
31 | # Multiple pass statements
- if True:
- pass
- pass
32 |
33 | # Mixed pass and ellipsis
34 | if True:
RUF050 [*] Empty `if` statement
--> RUF050.py:37:1
|
36 | # Mixed pass and ellipsis
37 | / if True:
38 | | pass
39 | | ...
| |_______^
40 |
41 | # Only statement in a with block
|
help: Remove the `if` statement
34 | pass
35 |
36 | # Mixed pass and ellipsis
- if True:
- pass
- ...
37 |
38 | # Only statement in a with block
39 | with pytest.raises(ValueError, match=msg):
RUF050 [*] Empty `if` statement
--> RUF050.py:43:5
|
41 | # Only statement in a with block
42 | with pytest.raises(ValueError, match=msg):
43 | / if obj1:
44 | | pass
| |____________^
|
help: Remove the `if` statement
40 |
41 | # Only statement in a with block
42 | with pytest.raises(ValueError, match=msg):
- if obj1:
- pass
43 + pass
44 |
45 |
46 | ### Errors (condition preserved as expression statement)
RUF050 [*] Empty `if` statement
--> RUF050.py:50:1
|
49 | # Function call
50 | / if foo():
51 | | pass
| |________^
52 |
53 | # Method call
|
help: Remove the `if` statement
47 | ### Errors (condition preserved as expression statement)
48 |
49 | # Function call
- if foo():
- pass
50 + foo()
51 |
52 | # Method call
53 | if bar.baz():
RUF050 [*] Empty `if` statement
--> RUF050.py:54:1
|
53 | # Method call
54 | / if bar.baz():
55 | | pass
| |________^
56 |
57 | # Nested call in boolean operator
|
help: Remove the `if` statement
51 | pass
52 |
53 | # Method call
- if bar.baz():
- pass
54 + bar.baz()
55 |
56 | # Nested call in boolean operator
57 | if x and foo():
RUF050 [*] Empty `if` statement
--> RUF050.py:58:1
|
57 | # Nested call in boolean operator
58 | / if x and foo():
59 | | pass
| |________^
60 |
61 | # Walrus operator with call
|
help: Remove the `if` statement
55 | pass
56 |
57 | # Nested call in boolean operator
- if x and foo():
- pass
58 + x and foo()
59 |
60 | # Walrus operator with call
61 | if (x := foo()):
RUF050 [*] Empty `if` statement
--> RUF050.py:62:1
|
61 | # Walrus operator with call
62 | / if (x := foo()):
63 | | pass
| |________^
64 |
65 | # Walrus operator without call
|
help: Remove the `if` statement
59 | pass
60 |
61 | # Walrus operator with call
- if (x := foo()):
- pass
62 + (x := foo())
63 |
64 | # Walrus operator without call
65 | if (x := y):
RUF050 [*] Empty `if` statement
--> RUF050.py:66:1
|
65 | # Walrus operator without call
66 | / if (x := y):
67 | | pass
| |________^
68 |
69 | # Only statement in a suite
|
help: Remove the `if` statement
63 | pass
64 |
65 | # Walrus operator without call
- if (x := y):
- pass
66 + (x := y)
67 |
68 | # Only statement in a suite
69 | class Foo:
RUF050 [*] Empty `if` statement
--> RUF050.py:71:5
|
69 | # Only statement in a suite
70 | class Foo:
71 | / if foo():
72 | | pass
| |____________^
|
help: Remove the `if` statement
68 |
69 | # Only statement in a suite
70 | class Foo:
- if foo():
- pass
71 + foo()
72 |
73 |
74 | ### No errors
@@ -0,0 +1,26 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
expression: transformed.source_code()
---
### Errors (both RUF047 and RUF050 converge)
# RUF047 removes the else, then RUF050 removes the remaining if.
# Same with elif.
if sys.version_info >= (3, 11):
pass
elif sys.version_info >= (3, 10):
pass
# Side-effect in condition: RUF047 removes the else, then RUF050
# replaces the remaining `if` with the condition expression
foo()
### No errors
# Non-empty if body: neither rule fires.
if sys.version_info >= (3, 11):
bar()
else:
baz()
@@ -0,0 +1,63 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF047 [*] Empty `else` clause
--> RUF050_RUF047.py:6:1
|
4 | if sys.version_info >= (3, 11):
5 | pass
6 | / else:
7 | | pass
| |________^
8 |
9 | # Same with elif.
|
help: Remove the `else` clause
3 | # RUF047 removes the else, then RUF050 removes the remaining if.
4 | if sys.version_info >= (3, 11):
5 | pass
- else:
- pass
6 |
7 | # Same with elif.
8 | if sys.version_info >= (3, 11):
RUF047 [*] Empty `else` clause
--> RUF050_RUF047.py:14:1
|
12 | elif sys.version_info >= (3, 10):
13 | pass
14 | / else:
15 | | pass
| |________^
16 |
17 | # Side-effect in condition: RUF047 removes the else, then RUF050
|
help: Remove the `else` clause
11 | pass
12 | elif sys.version_info >= (3, 10):
13 | pass
- else:
- pass
14 |
15 | # Side-effect in condition: RUF047 removes the else, then RUF050
16 | # replaces the remaining `if` with the condition expression
RUF047 [*] Empty `else` clause
--> RUF050_RUF047.py:21:1
|
19 | if foo():
20 | pass
21 | / else:
22 | | pass
| |________^
|
help: Remove the `else` clause
18 | # replaces the remaining `if` with the condition expression
19 | if foo():
20 | pass
- else:
- pass
21 |
22 |
23 | ### No errors
@@ -0,0 +1,17 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
expression: transformed.source_code()
---
# Reproduces the scenario from issue #9472:
# F401 removes unused imports leaving empty `if` blocks,
# RUF050 removes those blocks, then F401 cleans up the
# now-unused guard imports on subsequent fix iterations.
import os
# F401 removes the unused `ExceptionGroup` import, leaving `pass`.
# Then RUF050 removes the empty `if`, and F401 removes `sys`.
# Already-empty block handled in a single pass by RUF050
print(os.getcwd())
@@ -0,0 +1,41 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
F401 [*] `exceptiongroup.ExceptionGroup` imported but unused
--> RUF050_F401.py:12:32
|
10 | # Then RUF050 removes the empty `if`, and F401 removes `sys`.
11 | if sys.version_info < (3, 11):
12 | from exceptiongroup import ExceptionGroup
| ^^^^^^^^^^^^^^
13 |
14 | # Already-empty block handled in a single pass by RUF050
|
help: Remove unused import: `exceptiongroup.ExceptionGroup`
9 | # F401 removes the unused `ExceptionGroup` import, leaving `pass`.
10 | # Then RUF050 removes the empty `if`, and F401 removes `sys`.
11 | if sys.version_info < (3, 11):
- from exceptiongroup import ExceptionGroup
12 + pass
13 |
14 | # Already-empty block handled in a single pass by RUF050
15 | if sys.version_info < (3, 11):
RUF050 [*] Empty `if` statement
--> RUF050_F401.py:15:1
|
14 | # Already-empty block handled in a single pass by RUF050
15 | / if sys.version_info < (3, 11):
16 | | pass
| |________^
17 |
18 | print(os.getcwd())
|
help: Remove the `if` statement
12 | from exceptiongroup import ExceptionGroup
13 |
14 | # Already-empty block handled in a single pass by RUF050
- if sys.version_info < (3, 11):
- pass
15 |
16 | print(os.getcwd())
+1
View File
@@ -4196,6 +4196,7 @@
"RUF048",
"RUF049",
"RUF05",
"RUF050",
"RUF051",
"RUF052",
"RUF053",