mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
Implement unnecessary-if (RUF050) (#24114)
This commit is contained in:
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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+330
@@ -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
|
||||
+26
@@ -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()
|
||||
+63
@@ -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
|
||||
+17
@@ -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())
|
||||
+41
@@ -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())
|
||||
Generated
+1
@@ -4196,6 +4196,7 @@
|
||||
"RUF048",
|
||||
"RUF049",
|
||||
"RUF05",
|
||||
"RUF050",
|
||||
"RUF051",
|
||||
"RUF052",
|
||||
"RUF053",
|
||||
|
||||
Reference in new issue
Block a user