Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,27 @@ def f():
import os

print(os)


def f():
import pathlib

type Paths = list[pathlib.Path]


def f():
import pathlib

type Paths = list[pathlib.Path]

print(Paths)


def f():
import pathlib

type Paths = list[pathlib.Path]
type PathsMapping = dict[str, Paths]

# FIXME: false positive for indirect runtime use of Paths
print(PathsMapping)
50 changes: 48 additions & 2 deletions crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,62 @@ use crate::rules::flake8_type_checking::settings::Settings;

/// Returns `true` if the [`ResolvedReference`] is in a typing-only context _or_ a runtime-evaluated
/// context (with quoting enabled).
pub(crate) fn is_typing_reference(reference: &ResolvedReference, settings: &Settings) -> bool {
pub(crate) fn is_typing_reference(
semantic: &SemanticModel,
reference: &ResolvedReference,
settings: &Settings,
) -> bool {
reference.in_type_checking_block()
// if we're not in a type checking block, we necessarily need to be within a
// type definition to be considered a typing reference
|| (reference.in_type_definition()
&& (reference.in_typing_only_annotation()
|| reference.in_string_type_definition()
|| (reference.in_deferred_type_alias_value() && !parent_type_alias_has_runtime_references(semantic, reference))
|| (settings.quote_annotations && reference.in_runtime_evaluated_annotation())))
}

/// Find the [`Binding`] defined by the [PEP 695] type alias from a
/// [`Reference`] originating from its value expression and check
/// whether or not the binding has any any runtime references
///
/// [PEP 695]: https://peps.python.org/pep-0695/#generic-type-alias
pub(crate) fn parent_type_alias_has_runtime_references(
semantic: &SemanticModel,
reference: &ResolvedReference,
) -> bool {
// For now this check is non-recursive, i.e. if one of the references
// occurs in another type alias that itself has a runtime reference
// we would currently fail to detect that as a runtime reference.
//
// On the plus-side we don't have to worry about cyclic dependencies
// between type aliases this way. Otherwise we would need to detect
// and break cycles in order to not get caught in an infinite recursion.
let Some(expression_id) = reference.expression_id() else {
return false;
};
// type statements are wrapped in an extra scope for the type parameters
// so if we want to look up the binding defined by the type alias we need
// to look at the parent scope
let Some(scope_id) = semantic.parent_scope_id(reference.scope_id()) else {
return false;
};
let statement = semantic.statement(expression_id);
let scope = &semantic.scopes[scope_id];
for binding_id in scope.binding_ids() {
let binding = semantic.binding(binding_id);
let Some(binding_statement) = binding.statement(semantic) else {
continue;
};
if statement == binding_statement {
return binding
.references()
.any(|reference_id| semantic.reference(reference_id).in_runtime_context());
}
}
false
}

/// Returns `true` if the [`Binding`] represents a runtime-required import.
pub(crate) fn is_valid_runtime_import(
binding: &Binding,
Expand All @@ -42,7 +88,7 @@ pub(crate) fn is_valid_runtime_import(
&& binding
.references()
.map(|reference_id| semantic.reference(reference_id))
.any(|reference| !is_typing_reference(reference, settings))
.any(|reference| !is_typing_reference(semantic, reference, settings))
} else {
false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,11 @@ pub(crate) fn typing_only_runtime_import(
.references()
.map(|reference_id| checker.semantic().reference(reference_id))
.all(|reference| {
is_typing_reference(reference, &checker.settings.flake8_type_checking)
is_typing_reference(
checker.semantic(),
reference,
&checker.settings.flake8_type_checking,
)
})
{
let qualified_name = import.qualified_name();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,64 @@ TC003.py:8:12: TC003 [*] Move standard library import `os` into a type-checking
8 |- import os
9 12 |
10 13 | x: os
11 14 |
11 14 |

TC003.py:20:12: TC003 [*] Move standard library import `pathlib` into a type-checking block
|
19 | def f():
20 | import pathlib
| ^^^^^^^ TC003
21 |
22 | type Paths = list[pathlib.Path]
|
= help: Move into type-checking block

ℹ Unsafe fix
2 2 |
3 3 | For typing-only import detection tests, see `TC002.py`.
4 4 | """
5 |+from typing import TYPE_CHECKING
6 |+
7 |+if TYPE_CHECKING:
8 |+ import pathlib
5 9 |
6 10 |
7 11 | def f():
--------------------------------------------------------------------------------
17 21 |
18 22 |
19 23 | def f():
20 |- import pathlib
21 24 |
22 25 | type Paths = list[pathlib.Path]
23 26 |

TC003.py:34:12: TC003 [*] Move standard library import `pathlib` into a type-checking block
|
33 | def f():
34 | import pathlib
| ^^^^^^^ TC003
35 |
36 | type Paths = list[pathlib.Path]
|
= help: Move into type-checking block

ℹ Unsafe fix
2 2 |
3 3 | For typing-only import detection tests, see `TC002.py`.
4 4 | """
5 |+from typing import TYPE_CHECKING
6 |+
7 |+if TYPE_CHECKING:
8 |+ import pathlib
5 9 |
6 10 |
7 11 | def f():
--------------------------------------------------------------------------------
31 35 |
32 36 |
33 37 | def f():
34 |- import pathlib
35 38 |
36 39 | type Paths = list[pathlib.Path]
37 40 | type PathsMapping = dict[str, Paths]
8 changes: 8 additions & 0 deletions crates/ruff_python_semantic/src/reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ impl ResolvedReference {
.intersects(SemanticModelFlags::DUNDER_ALL_DEFINITION)
}

/// Return `true` if the context is in the r.h.s. of a [PEP 695] type alias.
///
/// [PEP 695]: https://peps.python.org/pep-0695/#generic-type-alias
pub const fn in_deferred_type_alias_value(&self) -> bool {
self.flags
.intersects(SemanticModelFlags::DEFERRED_TYPE_ALIAS)
}

/// Return `true` if the context is in the r.h.s. of a [PEP 613] type alias.
///
/// [PEP 613]: https://peps.python.org/pep-0613/
Expand Down
Loading