Skip to content

Commit ae75b30

Browse files
Avoid attributing runtime references to module-level imports (#4942)
1 parent 20240fc commit ae75b30

File tree

4 files changed

+98
-4
lines changed

4 files changed

+98
-4
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Test that runtime typing references are properly attributed to scoped imports."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, cast
6+
7+
if TYPE_CHECKING:
8+
from threading import Thread
9+
10+
11+
def fn(thread: Thread):
12+
from threading import Thread
13+
14+
# The `Thread` on the left-hand side should resolve to the `Thread` imported at the
15+
# top level.
16+
x: Thread
17+
18+
19+
def fn(thread: Thread):
20+
from threading import Thread
21+
22+
# The `Thread` on the left-hand side should resolve to the `Thread` imported at the
23+
# top level.
24+
cast("Thread", thread)
25+
26+
27+
def fn(thread: Thread):
28+
from threading import Thread
29+
30+
# The `Thread` on the right-hand side should resolve to the`Thread` imported within
31+
# `fn`.
32+
cast(Thread, thread)

crates/ruff/src/rules/pyflakes/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ mod tests {
4242
#[test_case(Rule::UnusedImport, Path::new("F401_14.py"))]
4343
#[test_case(Rule::UnusedImport, Path::new("F401_15.py"))]
4444
#[test_case(Rule::UnusedImport, Path::new("F401_16.py"))]
45+
#[test_case(Rule::UnusedImport, Path::new("F401_17.py"))]
4546
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
4647
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
4748
#[test_case(Rule::LateFutureImport, Path::new("F404.py"))]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
source: crates/ruff/src/rules/pyflakes/mod.rs
3+
---
4+
F401_17.py:12:27: F401 [*] `threading.Thread` imported but unused
5+
|
6+
12 | def fn(thread: Thread):
7+
13 | from threading import Thread
8+
| ^^^^^^ F401
9+
14 |
10+
15 | # The `Thread` on the left-hand side should resolve to the `Thread` imported at the
11+
|
12+
= help: Remove unused import: `threading.Thread`
13+
14+
Fix
15+
9 9 |
16+
10 10 |
17+
11 11 | def fn(thread: Thread):
18+
12 |- from threading import Thread
19+
13 12 |
20+
14 13 | # The `Thread` on the left-hand side should resolve to the `Thread` imported at the
21+
15 14 | # top level.
22+
23+
F401_17.py:20:27: F401 [*] `threading.Thread` imported but unused
24+
|
25+
20 | def fn(thread: Thread):
26+
21 | from threading import Thread
27+
| ^^^^^^ F401
28+
22 |
29+
23 | # The `Thread` on the left-hand side should resolve to the `Thread` imported at the
30+
|
31+
= help: Remove unused import: `threading.Thread`
32+
33+
Fix
34+
17 17 |
35+
18 18 |
36+
19 19 | def fn(thread: Thread):
37+
20 |- from threading import Thread
38+
21 20 |
39+
22 21 | # The `Thread` on the left-hand side should resolve to the `Thread` imported at the
40+
23 22 | # top level.
41+
42+

crates/ruff_python_semantic/src/model.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ impl<'a> SemanticModel<'a> {
178178
pub fn resolve_reference(&mut self, symbol: &str, range: TextRange) -> ResolvedReference {
179179
// PEP 563 indicates that if a forward reference can be resolved in the module scope, we
180180
// should prefer it over local resolutions.
181-
if self.in_deferred_type_definition() {
181+
if self.in_forward_reference() {
182182
if let Some(binding_id) = self.scopes.global().get(symbol) {
183183
// Mark the binding as used.
184184
let context = self.execution_context();
@@ -239,9 +239,7 @@ impl<'a> SemanticModel<'a> {
239239
//
240240
// The `name` in `print(name)` should be treated as unresolved, but the `name` in
241241
// `name: str` should be treated as used.
242-
if !self.in_deferred_type_definition()
243-
&& self.bindings[binding_id].kind.is_annotation()
244-
{
242+
if !self.in_forward_reference() && self.bindings[binding_id].kind.is_annotation() {
245243
continue;
246244
}
247245

@@ -756,6 +754,27 @@ impl<'a> SemanticModel<'a> {
756754
|| self.in_future_type_definition()
757755
}
758756

757+
/// Return `true` if the context is in a forward type reference.
758+
///
759+
/// Includes deferred string types, and future types in annotations.
760+
///
761+
/// ## Examples
762+
/// ```python
763+
/// from __future__ import annotations
764+
///
765+
/// from threading import Thread
766+
///
767+
///
768+
/// x: Thread # Forward reference
769+
/// cast("Thread", x) # Forward reference
770+
/// cast(Thread, x) # Non-forward reference
771+
/// ```
772+
pub const fn in_forward_reference(&self) -> bool {
773+
self.in_simple_string_type_definition()
774+
|| self.in_complex_string_type_definition()
775+
|| (self.in_future_type_definition() && self.in_annotation())
776+
}
777+
759778
/// Return `true` if the context is in an exception handler.
760779
pub const fn in_exception_handler(&self) -> bool {
761780
self.flags.contains(SemanticModelFlags::EXCEPTION_HANDLER)

0 commit comments

Comments
 (0)