Avoid if-else simplification for `TYPE_CHECKING` blocks (#8072)

Closes https://github.com/astral-sh/ruff/issues/8071.
This commit is contained in:
Charlie Marsh 2023-10-19 19:15:54 -04:00 committed by GitHub
parent 962472da96
commit 256b98ab9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 97 additions and 43 deletions

View File

@ -126,3 +126,12 @@ def f():
x = yield 3 x = yield 3
else: else:
x = yield 5 x = yield 5
from typing import TYPE_CHECKING
# OK
if TYPE_CHECKING:
x = 3
else:
x = 5

View File

@ -1044,16 +1044,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_simplify::rules::if_with_same_arms(checker, checker.locator, if_); flake8_simplify::rules::if_with_same_arms(checker, checker.locator, if_);
} }
if checker.enabled(Rule::NeedlessBool) { if checker.enabled(Rule::NeedlessBool) {
flake8_simplify::rules::needless_bool(checker, stmt); flake8_simplify::rules::needless_bool(checker, if_);
} }
if checker.enabled(Rule::IfElseBlockInsteadOfDictLookup) { if checker.enabled(Rule::IfElseBlockInsteadOfDictLookup) {
flake8_simplify::rules::manual_dict_lookup(checker, if_); flake8_simplify::rules::if_else_block_instead_of_dict_lookup(checker, if_);
} }
if checker.enabled(Rule::IfElseBlockInsteadOfIfExp) { if checker.enabled(Rule::IfElseBlockInsteadOfIfExp) {
flake8_simplify::rules::use_ternary_operator(checker, stmt); flake8_simplify::rules::if_else_block_instead_of_if_exp(checker, if_);
} }
if checker.enabled(Rule::IfElseBlockInsteadOfDictGet) { if checker.enabled(Rule::IfElseBlockInsteadOfDictGet) {
flake8_simplify::rules::use_dict_get_with_default(checker, if_); flake8_simplify::rules::if_else_block_instead_of_dict_get(checker, if_);
} }
if checker.enabled(Rule::TypeCheckWithoutTypeError) { if checker.enabled(Rule::TypeCheckWithoutTypeError) {
tryceratops::rules::type_check_without_type_error( tryceratops::rules::type_check_without_type_error(

View File

@ -9,6 +9,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::AnyNodeRef; use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::{self as ast, whitespace, Constant, ElifElseClause, Expr, Stmt}; use ruff_python_ast::{self as ast, whitespace, Constant, ElifElseClause, Expr, Stmt};
use ruff_python_codegen::Stylist; use ruff_python_codegen::Stylist;
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer}; use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator; use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -96,6 +97,16 @@ pub(crate) fn nested_if_statements(
return; return;
}; };
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
if is_sys_version_block(stmt_if, checker.semantic()) {
return;
}
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
if is_type_checking_block(stmt_if, checker.semantic()) {
return;
}
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
CollapsibleIf, CollapsibleIf,
TextRange::new(nested_if.start(), colon.end()), TextRange::new(nested_if.start(), colon.end()),

View File

@ -5,6 +5,7 @@ use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, Arguments, CmpOp, ElifElseClause, Expr, ExprContext, Identifier, Stmt, self as ast, Arguments, CmpOp, ElifElseClause, Expr, ExprContext, Identifier, Stmt,
}; };
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -55,7 +56,7 @@ impl Violation for IfElseBlockInsteadOfDictGet {
} }
/// SIM401 /// SIM401
pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &ast::StmtIf) { pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if: &ast::StmtIf) {
let ast::StmtIf { let ast::StmtIf {
test, test,
body, body,
@ -136,6 +137,16 @@ pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &ast::St
return; return;
} }
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
if is_sys_version_block(stmt_if, checker.semantic()) {
return;
}
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
if is_type_checking_block(stmt_if, checker.semantic()) {
return;
}
// Check that the default value is not "complex". // Check that the default value is not "complex".
if contains_effect(default_value, |id| checker.semantic().is_builtin(id)) { if contains_effect(default_value, |id| checker.semantic().is_builtin(id)) {
return; return;

View File

@ -5,6 +5,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableConstant; use ruff_python_ast::comparable::ComparableConstant;
use ruff_python_ast::helpers::contains_effect; use ruff_python_ast::helpers::contains_effect;
use ruff_python_ast::{self as ast, CmpOp, ElifElseClause, Expr, Stmt}; use ruff_python_ast::{self as ast, CmpOp, ElifElseClause, Expr, Stmt};
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -39,7 +40,7 @@ impl Violation for IfElseBlockInsteadOfDictLookup {
} }
} }
/// SIM116 /// SIM116
pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) { pub(crate) fn if_else_block_instead_of_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
// Throughout this rule: // Throughout this rule:
// * Each if or elif statement's test must consist of a constant equality check with the same variable. // * Each if or elif statement's test must consist of a constant equality check with the same variable.
// * Each if or elif statement's body must consist of a single `return`. // * Each if or elif statement's body must consist of a single `return`.
@ -75,6 +76,7 @@ pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else { let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else {
return; return;
}; };
if value if value
.as_ref() .as_ref()
.is_some_and(|value| contains_effect(value, |id| checker.semantic().is_builtin(id))) .is_some_and(|value| contains_effect(value, |id| checker.semantic().is_builtin(id)))
@ -82,6 +84,16 @@ pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &ast::StmtIf) {
return; return;
} }
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
if is_sys_version_block(stmt_if, checker.semantic()) {
return;
}
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
if is_type_checking_block(stmt_if, checker.semantic()) {
return;
}
let mut constants: FxHashSet<ComparableConstant> = FxHashSet::default(); let mut constants: FxHashSet<ComparableConstant> = FxHashSet::default();
constants.insert(constant.into()); constants.insert(constant.into());

View File

@ -1,8 +1,7 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt}; use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -51,16 +50,14 @@ impl Violation for IfElseBlockInsteadOfIfExp {
} }
/// SIM108 /// SIM108
pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) { pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &ast::StmtIf) {
let Stmt::If(ast::StmtIf { let ast::StmtIf {
test, test,
body, body,
elif_else_clauses, elif_else_clauses,
range: _, range: _,
}) = stmt } = stmt_if;
else {
return;
};
// `test: None` to only match an `else` clause // `test: None` to only match an `else` clause
let [ElifElseClause { let [ElifElseClause {
body: else_body, body: else_body,
@ -99,13 +96,6 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
return; return;
} }
// Avoid suggesting ternary for `if sys.version_info >= ...`-style and
// `if sys.platform.startswith("...")`-style checks.
let ignored_call_paths: &[&[&str]] = &[&["sys", "version_info"], &["sys", "platform"]];
if contains_call_path(test, ignored_call_paths, checker.semantic()) {
return;
}
// Avoid suggesting ternary for `if (yield ...)`-style checks. // Avoid suggesting ternary for `if (yield ...)`-style checks.
// TODO(charlie): Fix precedence handling for yields in generator. // TODO(charlie): Fix precedence handling for yields in generator.
if matches!( if matches!(
@ -121,6 +111,16 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
return; return;
} }
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
if is_sys_version_block(stmt_if, checker.semantic()) {
return;
}
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
if is_type_checking_block(stmt_if, checker.semantic()) {
return;
}
let target_var = &body_target; let target_var = &body_target;
let ternary = ternary(target_var, body_value, test, else_value); let ternary = ternary(target_var, body_value, test, else_value);
let contents = checker.generator().stmt(&ternary); let contents = checker.generator().stmt(&ternary);
@ -128,7 +128,7 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
// Don't flag if the resulting expression would exceed the maximum line length. // Don't flag if the resulting expression would exceed the maximum line length.
if !fits( if !fits(
&contents, &contents,
stmt.into(), stmt_if.into(),
checker.locator(), checker.locator(),
checker.settings.line_length, checker.settings.line_length,
checker.settings.tab_size, checker.settings.tab_size,
@ -140,26 +140,17 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
IfElseBlockInsteadOfIfExp { IfElseBlockInsteadOfIfExp {
contents: contents.clone(), contents: contents.clone(),
}, },
stmt.range(), stmt_if.range(),
); );
if !checker.indexer().has_comments(stmt, checker.locator()) { if !checker.indexer().has_comments(stmt_if, checker.locator()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
contents, contents,
stmt.range(), stmt_if.range(),
))); )));
} }
checker.diagnostics.push(diagnostic); checker.diagnostics.push(diagnostic);
} }
/// Return `true` if the `Expr` contains a reference to any of the given `${module}.${target}`.
fn contains_call_path(expr: &Expr, targets: &[&[&str]], semantic: &SemanticModel) -> bool {
any_over_expr(expr, &|expr| {
semantic
.resolve_call_path(expr)
.is_some_and(|call_path| targets.iter().any(|target| &call_path.as_slice() == target))
})
}
fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Expr) -> Stmt { fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Expr) -> Stmt {
let node = ast::ExprIfExp { let node = ast::ExprIfExp {
test: Box::new(test.clone()), test: Box::new(test.clone()),

View File

@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Constant, ElifElseClause, Expr, ExprContext, Stmt}; use ruff_python_ast::{self as ast, Arguments, Constant, ElifElseClause, Expr, ExprContext, Stmt};
use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checking_block};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -48,16 +49,14 @@ impl Violation for NeedlessBool {
} }
/// SIM103 /// SIM103
pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
let Stmt::If(ast::StmtIf { let ast::StmtIf {
test: if_test, test: if_test,
body: if_body, body: if_body,
elif_else_clauses, elif_else_clauses,
range: _, range: _,
}) = stmt } = stmt_if;
else {
return;
};
// Extract an `if` or `elif` (that returns) followed by an else (that returns the same value) // Extract an `if` or `elif` (that returns) followed by an else (that returns the same value)
let (if_test, if_body, else_body, range) = match elif_else_clauses.as_slice() { let (if_test, if_body, else_body, range) = match elif_else_clauses.as_slice() {
// if-else case // if-else case
@ -65,7 +64,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
body: else_body, body: else_body,
test: None, test: None,
.. ..
}] => (if_test.as_ref(), if_body, else_body, stmt.range()), }] => (if_test.as_ref(), if_body, else_body, stmt_if.range()),
// elif-else case // elif-else case
[.., ElifElseClause { [.., ElifElseClause {
body: elif_body, body: elif_body,
@ -97,6 +96,16 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
return; return;
} }
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
if is_sys_version_block(stmt_if, checker.semantic()) {
return;
}
// Avoid suggesting ternary for `if TYPE_CHECKING:`-style checks.
if is_type_checking_block(stmt_if, checker.semantic()) {
return;
}
let condition = checker.generator().expr(if_test); let condition = checker.generator().expr(if_test);
let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, range); let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, range);
if matches!(if_return, Bool::True) if matches!(if_return, Bool::True)

View File

@ -1,7 +1,7 @@
//! Analysis rules for the `typing` module. //! Analysis rules for the `typing` module.
use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath}; use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath};
use ruff_python_ast::helpers::{is_const_false, map_subscript}; use ruff_python_ast::helpers::{any_over_expr, is_const_false, map_subscript};
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, Constant, Expr, Int, Operator, ParameterWithDefault, Parameters, Stmt, self as ast, Constant, Expr, Int, Operator, ParameterWithDefault, Parameters, Stmt,
}; };
@ -302,7 +302,7 @@ pub fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
} }
} }
/// Return `true` if [`Expr`] is a guard for a type-checking block. /// Return `true` if [`ast::StmtIf`] is a guard for a type-checking block.
pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool { pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool {
let ast::StmtIf { test, .. } = stmt; let ast::StmtIf { test, .. } = stmt;
@ -333,6 +333,17 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> b
false false
} }
/// Returns `true` if the [`ast::StmtIf`] is a version-checking block (e.g., `if sys.version_info >= ...:`).
pub fn is_sys_version_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool {
let ast::StmtIf { test, .. } = stmt;
any_over_expr(test, &|expr| {
semantic.resolve_call_path(expr).is_some_and(|call_path| {
matches!(call_path.as_slice(), ["sys", "version_info" | "platform"])
})
})
}
/// Abstraction for a type checker, conservatively checks for the intended type(s). /// Abstraction for a type checker, conservatively checks for the intended type(s).
trait TypeChecker { trait TypeChecker {
/// Check annotation expression to match the intended type(s). /// Check annotation expression to match the intended type(s).