Consider all TYPE_CHECKING symbols for type-checking blocks (#16669)

## Summary

This PR stabilizes the preview behavior introduced in
https://github.com/astral-sh/ruff/pull/15719 to recognize all symbols
named `TYPE_CHECKING` as type-checking
checks in `if TYPE_CHECKING` conditions. This ensures compatibility with
mypy and pyright.

This PR also stabilizes the new behavior that removes `if 0:` and `if
False` to be no longer considered type checking blocks.
Since then, this syntax has been removed from the typing spec and was
only used for Python modules that don't have a `typing` module
([comment](https://github.com/astral-sh/ruff/pull/15719#issuecomment-2612787793)).

The preview behavior was first released with Ruff 0.9.5 (6th of
February), which was about a month ago. There are no open issues or PRs
for the changed behavior


## Test Plan

The snapshots for `SIM108` change because `SIM108` ignored type checking
blocks but it can no
simplify `if 0` or `if False` blocks again because they're no longer
considered type checking blocks.

The changes in the `TC005` snapshot or only due to that `if 0` and `if
False` are no longer recognized as type checking blocks

<!-- How was it tested? -->
This commit is contained in:
Micha Reiser 2025-03-13 08:44:04 +01:00
parent 3d2f2a2f8d
commit 92193a3254
6 changed files with 78 additions and 152 deletions

View file

@ -4,13 +4,6 @@ if TYPE_CHECKING:
pass # TC005 pass # TC005
if False:
pass # TC005
if 0:
pass # TC005
def example(): def example():
if TYPE_CHECKING: if TYPE_CHECKING:
pass # TC005 pass # TC005
@ -32,13 +25,6 @@ if TYPE_CHECKING:
x: List x: List
if False:
x: List
if 0:
x: List
from typing_extensions import TYPE_CHECKING from typing_extensions import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:

View file

@ -246,11 +246,7 @@ impl<'a> Checker<'a> {
notebook_index: Option<&'a NotebookIndex>, notebook_index: Option<&'a NotebookIndex>,
target_version: PythonVersion, target_version: PythonVersion,
) -> Checker<'a> { ) -> Checker<'a> {
let mut semantic = SemanticModel::new(&settings.typing_modules, path, module); let semantic = SemanticModel::new(&settings.typing_modules, path, module);
if settings.preview.is_enabled() {
// Set the feature flag to test `TYPE_CHECKING` semantic changes
semantic.flags |= SemanticModelFlags::NEW_TYPE_CHECKING_BLOCK_DETECTION;
}
Self { Self {
parsed, parsed,
parsed_type_annotation: None, parsed_type_annotation: None,

View file

@ -226,6 +226,33 @@ SIM108.py:167:1: SIM108 [*] Use ternary operator `z = 1 if True else other` inst
172 169 | if False: 172 169 | if False:
173 170 | z = 1 173 170 | z = 1
SIM108.py:172:1: SIM108 [*] Use ternary operator `z = 1 if False else other` instead of `if`-`else`-block
|
170 | z = other
171 |
172 | / if False:
173 | | z = 1
174 | | else:
175 | | z = other
| |_____________^ SIM108
176 |
177 | if 1:
|
= help: Replace `if`-`else`-block with `z = 1 if False else other`
Unsafe fix
169 169 | else:
170 170 | z = other
171 171 |
172 |-if False:
173 |- z = 1
174 |-else:
175 |- z = other
172 |+z = 1 if False else other
176 173 |
177 174 | if 1:
178 175 | z = True
SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block SIM108.py:177:1: SIM108 [*] Use ternary operator `z = True if 1 else other` instead of `if`-`else`-block
| |
175 | z = other 175 | z = other

View file

@ -16,102 +16,64 @@ TC005.py:4:5: TC005 [*] Found empty type-checking block
4 |- pass # TC005 4 |- pass # TC005
5 3 | 5 3 |
6 4 | 6 4 |
7 5 | if False: 7 5 | def example():
TC005.py:8:5: TC005 [*] Found empty type-checking block TC005.py:9:9: TC005 [*] Found empty type-checking block
| |
7 | if False: 7 | def example():
8 | pass # TC005 8 | if TYPE_CHECKING:
| ^^^^ TC005 9 | pass # TC005
9 | | ^^^^ TC005
10 | if 0: 10 | return
| |
= help: Delete empty type-checking block = help: Delete empty type-checking block
Safe fix Safe fix
4 4 | pass # TC005 5 5 |
5 5 | 6 6 |
6 6 | 7 7 | def example():
7 |-if False: 8 |- if TYPE_CHECKING:
8 |- pass # TC005 9 |- pass # TC005
9 7 | 10 8 | return
10 8 | if 0: 11 9 |
11 9 | pass # TC005
TC005.py:11:5: TC005 [*] Found empty type-checking block
|
10 | if 0:
11 | pass # TC005
| ^^^^ TC005
|
= help: Delete empty type-checking block
Safe fix
7 7 | if False:
8 8 | pass # TC005
9 9 |
10 |-if 0:
11 |- pass # TC005
12 10 | 12 10 |
13 11 |
14 12 | def example():
TC005.py:16:9: TC005 [*] Found empty type-checking block TC005.py:15:9: TC005 [*] Found empty type-checking block
| |
14 | def example(): 13 | class Test:
15 | if TYPE_CHECKING: 14 | if TYPE_CHECKING:
16 | pass # TC005 15 | pass # TC005
| ^^^^ TC005 | ^^^^ TC005
17 | return 16 | x = 2
| |
= help: Delete empty type-checking block = help: Delete empty type-checking block
Safe fix Safe fix
11 11 |
12 12 | 12 12 |
13 13 | 13 13 | class Test:
14 14 | def example(): 14 |- if TYPE_CHECKING:
15 |- if TYPE_CHECKING: 15 |- pass # TC005
16 |- pass # TC005 16 14 | x = 2
17 15 | return 17 15 |
18 16 | 18 16 |
19 17 |
TC005.py:22:9: TC005 [*] Found empty type-checking block TC005.py:31:5: TC005 [*] Found empty type-checking block
| |
20 | class Test: 30 | if TYPE_CHECKING:
21 | if TYPE_CHECKING: 31 | pass # TC005
22 | pass # TC005
| ^^^^ TC005
23 | x = 2
|
= help: Delete empty type-checking block
Safe fix
18 18 |
19 19 |
20 20 | class Test:
21 |- if TYPE_CHECKING:
22 |- pass # TC005
23 21 | x = 2
24 22 |
25 23 |
TC005.py:45:5: TC005 [*] Found empty type-checking block
|
44 | if TYPE_CHECKING:
45 | pass # TC005
| ^^^^ TC005 | ^^^^ TC005
46 | 32 |
47 | # https://github.com/astral-sh/ruff/issues/11368 33 | # https://github.com/astral-sh/ruff/issues/11368
| |
= help: Delete empty type-checking block = help: Delete empty type-checking block
Safe fix Safe fix
41 41 | 27 27 |
42 42 | from typing_extensions import TYPE_CHECKING 28 28 | from typing_extensions import TYPE_CHECKING
43 43 | 29 29 |
44 |-if TYPE_CHECKING: 30 |-if TYPE_CHECKING:
45 |- pass # TC005 31 |- pass # TC005
46 44 | 32 30 |
47 45 | # https://github.com/astral-sh/ruff/issues/11368 33 31 | # https://github.com/astral-sh/ruff/issues/11368
48 46 | if TYPE_CHECKING: 34 32 | if TYPE_CHECKING:

View file

@ -1,10 +1,10 @@
//! Analysis rules for the `typing` module. //! Analysis rules for the `typing` module.
use ruff_python_ast::helpers::{any_over_expr, is_const_false, map_subscript}; use ruff_python_ast::helpers::{any_over_expr, map_subscript};
use ruff_python_ast::identifier::Identifier; use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::name::QualifiedName; use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, Expr, ExprCall, ExprName, Int, Operator, ParameterWithDefault, Parameters, Stmt, self as ast, Expr, ExprCall, ExprName, Operator, ParameterWithDefault, Parameters, Stmt,
StmtAssign, StmtAssign,
}; };
use ruff_python_stdlib::typing::{ use ruff_python_stdlib::typing::{
@ -391,44 +391,19 @@ pub fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
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;
if semantic.use_new_type_checking_block_detection_semantics() { match test.as_ref() {
return match test.as_ref() { // As long as the symbol's name is "TYPE_CHECKING" we will treat it like `typing.TYPE_CHECKING`
// As long as the symbol's name is "TYPE_CHECKING" we will treat it like `typing.TYPE_CHECKING` // for this specific check even if it's defined somewhere else, like the current module.
// for this specific check even if it's defined somewhere else, like the current module. // Ex) `if TYPE_CHECKING:`
// Ex) `if TYPE_CHECKING:` Expr::Name(ast::ExprName { id, .. }) => {
Expr::Name(ast::ExprName { id, .. }) => { id == "TYPE_CHECKING"
id == "TYPE_CHECKING"
// Ex) `if TC:` with `from typing import TYPE_CHECKING as TC` // Ex) `if TC:` with `from typing import TYPE_CHECKING as TC`
|| semantic.match_typing_expr(test, "TYPE_CHECKING") || semantic.match_typing_expr(test, "TYPE_CHECKING")
} }
// Ex) `if typing.TYPE_CHECKING:` // Ex) `if typing.TYPE_CHECKING:`
Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr == "TYPE_CHECKING", Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr == "TYPE_CHECKING",
_ => false, _ => false,
};
} }
// Ex) `if False:`
if is_const_false(test) {
return true;
}
// Ex) `if 0:`
if matches!(
test.as_ref(),
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(Int::ZERO),
..
})
) {
return true;
}
// Ex) `if typing.TYPE_CHECKING:`
if semantic.match_typing_expr(test, "TYPE_CHECKING") {
return true;
}
false
} }
/// Returns `true` if the [`ast::StmtIf`] is a version-checking block (e.g., `if sys.version_info >= ...:`). /// Returns `true` if the [`ast::StmtIf`] is a version-checking block (e.g., `if sys.version_info >= ...:`).

View file

@ -2014,18 +2014,6 @@ impl<'a> SemanticModel<'a> {
.intersects(SemanticModelFlags::DEFERRED_CLASS_BASE) .intersects(SemanticModelFlags::DEFERRED_CLASS_BASE)
} }
/// Return `true` if we should use the new semantics to recognize
/// type checking blocks. Previously we only recognized type checking
/// blocks if `TYPE_CHECKING` was imported from a typing module.
///
/// With this feature flag enabled we recognize any symbol named
/// `TYPE_CHECKING`, regardless of where it comes from to mirror
/// what mypy and pyright do.
pub const fn use_new_type_checking_block_detection_semantics(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::NEW_TYPE_CHECKING_BLOCK_DETECTION)
}
/// Return an iterator over all bindings shadowed by the given [`BindingId`], within the /// Return an iterator over all bindings shadowed by the given [`BindingId`], within the
/// containing scope, and across scopes. /// containing scope, and across scopes.
pub fn shadowed_bindings( pub fn shadowed_bindings(
@ -2557,14 +2545,6 @@ bitflags! {
/// [#13824]: https://github.com/astral-sh/ruff/issues/13824 /// [#13824]: https://github.com/astral-sh/ruff/issues/13824
const NO_TYPE_CHECK = 1 << 30; const NO_TYPE_CHECK = 1 << 30;
/// The model special-cases any symbol named `TYPE_CHECKING`.
///
/// Previously we only recognized `TYPE_CHECKING` if it was part of
/// one of the configured `typing` modules. This flag exists to
/// test out the semantic change only in preview. This flag will go
/// away once this change has been stabilized.
const NEW_TYPE_CHECKING_BLOCK_DETECTION = 1 << 31;
/// The context is in any type annotation. /// The context is in any type annotation.
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits(); const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();