Skip to content

Commit 9f2066a

Browse files
committed
Avoid if-else simplification for TYPE_CHECKING blocks
1 parent ec1be60 commit 9f2066a

8 files changed

Lines changed: 97 additions & 43 deletions

File tree

crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM108.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,12 @@ def f():
126126
x = yield 3
127127
else:
128128
x = yield 5
129+
130+
131+
from typing import TYPE_CHECKING
132+
133+
# OK
134+
if TYPE_CHECKING:
135+
x = 3
136+
else:
137+
x = 5

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,16 +1044,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
10441044
flake8_simplify::rules::if_with_same_arms(checker, checker.locator, if_);
10451045
}
10461046
if checker.enabled(Rule::NeedlessBool) {
1047-
flake8_simplify::rules::needless_bool(checker, stmt);
1047+
flake8_simplify::rules::needless_bool(checker, if_);
10481048
}
10491049
if checker.enabled(Rule::IfElseBlockInsteadOfDictLookup) {
1050-
flake8_simplify::rules::manual_dict_lookup(checker, if_);
1050+
flake8_simplify::rules::if_else_block_instead_of_dict_lookup(checker, if_);
10511051
}
10521052
if checker.enabled(Rule::IfElseBlockInsteadOfIfExp) {
1053-
flake8_simplify::rules::use_ternary_operator(checker, stmt);
1053+
flake8_simplify::rules::if_else_block_instead_of_if_exp(checker, if_);
10541054
}
10551055
if checker.enabled(Rule::IfElseBlockInsteadOfDictGet) {
1056-
flake8_simplify::rules::use_dict_get_with_default(checker, if_);
1056+
flake8_simplify::rules::if_else_block_instead_of_dict_get(checker, if_);
10571057
}
10581058
if checker.enabled(Rule::TypeCheckWithoutTypeError) {
10591059
tryceratops::rules::type_check_without_type_error(

crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use ruff_macros::{derive_message_formats, violation};
99
use ruff_python_ast::AnyNodeRef;
1010
use ruff_python_ast::{self as ast, whitespace, Constant, ElifElseClause, Expr, Stmt};
1111
use ruff_python_codegen::Stylist;
12+
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
1213
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
1314
use ruff_source_file::Locator;
1415
use ruff_text_size::{Ranged, TextRange};
@@ -96,6 +97,16 @@ pub(crate) fn nested_if_statements(
9697
return;
9798
};
9899

100+
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
101+
if is_sys_version_block(stmt_if, checker.semantic()) {
102+
return;
103+
}
104+
105+
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
106+
if is_type_checking_block(stmt_if, checker.semantic()) {
107+
return;
108+
}
109+
99110
let mut diagnostic = Diagnostic::new(
100111
CollapsibleIf,
101112
TextRange::new(nested_if.start(), colon.end()),

crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use ruff_python_ast::helpers::contains_effect;
55
use ruff_python_ast::{
66
self as ast, Arguments, CmpOp, ElifElseClause, Expr, ExprContext, Identifier, Stmt,
77
};
8+
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
89
use ruff_text_size::{Ranged, TextRange};
910

1011
use crate::checkers::ast::Checker;
@@ -55,7 +56,7 @@ impl Violation for IfElseBlockInsteadOfDictGet {
5556
}
5657

5758
/// SIM401
58-
pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &ast::StmtIf) {
59+
pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if: &ast::StmtIf) {
5960
let ast::StmtIf {
6061
test,
6162
body,
@@ -136,6 +137,16 @@ pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &ast::St
136137
return;
137138
}
138139

140+
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
141+
if is_sys_version_block(stmt_if, checker.semantic()) {
142+
return;
143+
}
144+
145+
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
146+
if is_type_checking_block(stmt_if, checker.semantic()) {
147+
return;
148+
}
149+
139150
// Check that the default value is not "complex".
140151
if contains_effect(default_value, |id| checker.semantic().is_builtin(id)) {
141152
return;

crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_lookup.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
55
use ruff_python_ast::comparable::ComparableConstant;
66
use ruff_python_ast::helpers::contains_effect;
77
use ruff_python_ast::{self as ast, CmpOp, ElifElseClause, Expr, Stmt};
8+
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
89
use ruff_text_size::Ranged;
910

1011
use crate::checkers::ast::Checker;
@@ -39,7 +40,7 @@ impl Violation for IfElseBlockInsteadOfDictLookup {
3940
}
4041
}
4142
/// SIM116
42-
pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
43+
pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
4344
// Throughout this rule:
4445
// * Each if or elif statement's test must consist of a constant equality check with the same variable.
4546
// * Each if or elif statement's body must consist of a single `return`.
@@ -75,13 +76,24 @@ pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
7576
let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else {
7677
return;
7778
};
79+
7880
if value
7981
.as_ref()
8082
.is_some_and(|value| contains_effect(value, |id| checker.semantic().is_builtin(id)))
8183
{
8284
return;
8385
}
8486

87+
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
88+
if is_sys_version_block(stmt_if, checker.semantic()) {
89+
return;
90+
}
91+
92+
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
93+
if is_type_checking_block(stmt_if, checker.semantic()) {
94+
return;
95+
}
96+
8597
let mut constants: FxHashSet<ComparableConstant> = FxHashSet::default();
8698
constants.insert(constant.into());
8799

crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
22
use ruff_macros::{derive_message_formats, violation};
3-
use ruff_python_ast::helpers::any_over_expr;
43
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
5-
use ruff_python_semantic::SemanticModel;
4+
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
65
use ruff_text_size::{Ranged, TextRange};
76

87
use crate::checkers::ast::Checker;
@@ -51,16 +50,14 @@ impl Violation for IfElseBlockInsteadOfIfExp {
5150
}
5251

5352
/// SIM108
54-
pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
55-
let Stmt::If(ast::StmtIf {
53+
pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &ast::StmtIf) {
54+
let ast::StmtIf {
5655
test,
5756
body,
5857
elif_else_clauses,
5958
range: _,
60-
}) = stmt
61-
else {
62-
return;
63-
};
59+
} = stmt_if;
60+
6461
// `test: None` to only match an `else` clause
6562
let [ElifElseClause {
6663
body: else_body,
@@ -99,13 +96,6 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
9996
return;
10097
}
10198

102-
// Avoid suggesting ternary for `if sys.version_info >= ...`-style and
103-
// `if sys.platform.startswith("...")`-style checks.
104-
let ignored_call_paths: &[&[&str]] = &[&["sys", "version_info"], &["sys", "platform"]];
105-
if contains_call_path(test, ignored_call_paths, checker.semantic()) {
106-
return;
107-
}
108-
10999
// Avoid suggesting ternary for `if (yield ...)`-style checks.
110100
// TODO(charlie): Fix precedence handling for yields in generator.
111101
if matches!(
@@ -121,14 +111,24 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
121111
return;
122112
}
123113

114+
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
115+
if is_sys_version_block(stmt_if, checker.semantic()) {
116+
return;
117+
}
118+
119+
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
120+
if is_type_checking_block(stmt_if, checker.semantic()) {
121+
return;
122+
}
123+
124124
let target_var = &body_target;
125125
let ternary = ternary(target_var, body_value, test, else_value);
126126
let contents = checker.generator().stmt(&ternary);
127127

128128
// Don't flag if the resulting expression would exceed the maximum line length.
129129
if !fits(
130130
&contents,
131-
stmt.into(),
131+
stmt_if.into(),
132132
checker.locator(),
133133
checker.settings.line_length,
134134
checker.settings.tab_size,
@@ -140,26 +140,17 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
140140
IfElseBlockInsteadOfIfExp {
141141
contents: contents.clone(),
142142
},
143-
stmt.range(),
143+
stmt_if.range(),
144144
);
145-
if !checker.indexer().has_comments(stmt, checker.locator()) {
145+
if !checker.indexer().has_comments(stmt_if, checker.locator()) {
146146
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
147147
contents,
148-
stmt.range(),
148+
stmt_if.range(),
149149
)));
150150
}
151151
checker.diagnostics.push(diagnostic);
152152
}
153153

154-
/// Return `true` if the `Expr` contains a reference to any of the given `${module}.${target}`.
155-
fn contains_call_path(expr: &Expr, targets: &[&[&str]], semantic: &SemanticModel) -> bool {
156-
any_over_expr(expr, &|expr| {
157-
semantic
158-
.resolve_call_path(expr)
159-
.is_some_and(|call_path| targets.iter().any(|target| &call_path.as_slice() == target))
160-
})
161-
}
162-
163154
fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Expr) -> Stmt {
164155
let node = ast::ExprIfExp {
165156
test: Box::new(test.clone()),

crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
22
use ruff_macros::{derive_message_formats, violation};
33
use ruff_python_ast::{self as ast, Arguments, Constant, ElifElseClause, Expr, ExprContext, Stmt};
4+
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
45
use ruff_text_size::{Ranged, TextRange};
56

67
use crate::checkers::ast::Checker;
@@ -48,24 +49,22 @@ impl Violation for NeedlessBool {
4849
}
4950

5051
/// SIM103
51-
pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
52-
let Stmt::If(ast::StmtIf {
52+
pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
53+
let ast::StmtIf {
5354
test: if_test,
5455
body: if_body,
5556
elif_else_clauses,
5657
range: _,
57-
}) = stmt
58-
else {
59-
return;
60-
};
58+
} = stmt_if;
59+
6160
// Extract an `if` or `elif` (that returns) followed by an else (that returns the same value)
6261
let (if_test, if_body, else_body, range) = match elif_else_clauses.as_slice() {
6362
// if-else case
6463
[ElifElseClause {
6564
body: else_body,
6665
test: None,
6766
..
68-
}] => (if_test.as_ref(), if_body, else_body, stmt.range()),
67+
}] => (if_test.as_ref(), if_body, else_body, stmt_if.range()),
6968
// elif-else case
7069
[.., ElifElseClause {
7170
body: elif_body,
@@ -97,6 +96,16 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
9796
return;
9897
}
9998

99+
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
100+
if is_sys_version_block(stmt_if, checker.semantic()) {
101+
return;
102+
}
103+
104+
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
105+
if is_type_checking_block(stmt_if, checker.semantic()) {
106+
return;
107+
}
108+
100109
let condition = checker.generator().expr(if_test);
101110
let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, range);
102111
if matches!(if_return, Bool::True)

crates/ruff_python_semantic/src/analyze/typing.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Analysis rules for the `typing` module.
22
33
use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath};
4-
use ruff_python_ast::helpers::{is_const_false, map_subscript};
4+
use ruff_python_ast::helpers::{any_over_expr, is_const_false, map_subscript};
55
use ruff_python_ast::{
66
self as ast, Constant, Expr, Int, Operator, ParameterWithDefault, Parameters, Stmt,
77
};
@@ -302,7 +302,7 @@ pub fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
302302
}
303303
}
304304

305-
/// Return `true` if [`Expr`] is a guard for a type-checking block.
305+
/// Return `true` if [`ast::StmtIf`] is a guard for a type-checking block.
306306
pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool {
307307
let ast::StmtIf { test, .. } = stmt;
308308

@@ -333,6 +333,17 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> b
333333
false
334334
}
335335

336+
/// Returns `true` if the [`ast::StmtIf`] is a version-checking block (e.g., `if sys.version_info >= ...:`).
337+
pub fn is_sys_version_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool {
338+
let ast::StmtIf { test, .. } = stmt;
339+
340+
any_over_expr(test, &|expr| {
341+
semantic.resolve_call_path(expr).is_some_and(|call_path| {
342+
matches!(call_path.as_slice(), ["sys", "version_info" | "platform"])
343+
})
344+
})
345+
}
346+
336347
/// Abstraction for a type checker, conservatively checks for the intended type(s).
337348
trait TypeChecker {
338349
/// Check annotation expression to match the intended type(s).

0 commit comments

Comments
 (0)