Skip to content

Commit e84b0f4

Browse files
authored
[flake8-type-checking] Add sub-diagnostic showing the runtime use of an annotation (TC004) (#23091)
## Summary <!-- What's the purpose of the change? What does it do, and why? --> Part of #17203, have not changed `ImportBinding` #16490 (comment) ## Test Plan <!-- How was it tested? --> Updated snapshots. --------- Signed-off-by: Bhuminjay <bhuminjaysoni@gmail.com>
1 parent c2ffad5 commit e84b0f4

15 files changed

Lines changed: 95 additions & 10 deletions

crates/ruff_linter/src/rules/flake8_type_checking/imports.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ruff_python_semantic::{AnyImport, Binding, ResolvedReferenceId};
1+
use ruff_python_semantic::{AnyImport, Binding, ResolvedReference, ResolvedReferenceId};
22
use ruff_text_size::{Ranged, TextRange};
33

44
/// An import with its surrounding context.
@@ -15,6 +15,8 @@ pub(crate) struct ImportBinding<'a> {
1515
pub(crate) parent_range: Option<TextRange>,
1616
/// Whether the binding needs `from __future__ import annotations` to be imported.
1717
pub(crate) needs_future_import: bool,
18+
/// A runtime reference to this import, used for diagnostic annotations.
19+
pub(crate) runtime_reference: Option<&'a ResolvedReference>,
1820
}
1921

2022
impl Ranged for ImportBinding<'_> {

crates/ruff_linter/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,12 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S
123123
};
124124

125125
if binding.context.is_typing()
126-
&& binding.references().any(|reference_id| {
126+
&& let Some(runtime_reference) = binding.references().find_map(|reference_id| {
127127
let reference = checker.semantic().reference(reference_id);
128128

129-
reference.in_runtime_context()
130-
&& !(ignore_dunder_all_references && reference.in_dunder_all_definition())
129+
(reference.in_runtime_context()
130+
&& !(ignore_dunder_all_references && reference.in_dunder_all_definition()))
131+
.then_some(reference)
131132
})
132133
{
133134
let Some(node_id) = binding.source else {
@@ -141,6 +142,7 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S
141142
range: binding.range(),
142143
parent_range: binding.parent_range(checker.semantic()),
143144
needs_future_import: false, // TODO(brent) See #19359.
145+
runtime_reference: Some(runtime_reference),
144146
};
145147

146148
if checker.rule_is_ignored(Rule::RuntimeImportInTypeCheckingBlock, import.start())
@@ -197,6 +199,7 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S
197199
import,
198200
range,
199201
parent_range,
202+
runtime_reference,
200203
..
201204
} in imports
202205
{
@@ -207,6 +210,9 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S
207210
},
208211
range,
209212
);
213+
if let Some(runtime_reference) = runtime_reference {
214+
diagnostic.secondary_annotation("Used at runtime here", runtime_reference);
215+
}
210216
if let Some(range) = parent_range {
211217
diagnostic.set_parent(range.start());
212218
}
@@ -225,6 +231,7 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S
225231
import,
226232
range,
227233
parent_range,
234+
runtime_reference,
228235
..
229236
} in imports
230237
{
@@ -235,6 +242,9 @@ pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &S
235242
},
236243
range,
237244
);
245+
if let Some(runtime_reference) = runtime_reference {
246+
diagnostic.secondary_annotation("Used at runtime here", runtime_reference);
247+
}
238248
if let Some(range) = parent_range {
239249
diagnostic.set_parent(range.start());
240250
}

crates/ruff_linter/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ pub(crate) fn typing_only_runtime_import(
374374
range: binding.range(),
375375
parent_range: binding.parent_range(checker.semantic()),
376376
needs_future_import,
377+
runtime_reference: None,
377378
};
378379

379380
if checker.rule_is_ignored(rule_for(import_type), import.start())

crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quote_runtime-import-in-type-checking-block_quote.py.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ TC004 [*] Move import `pandas.DataFrame` out of type-checking block. Import is u
99
| ^^^^^^^^^
1010
111 |
1111
112 | x: TypeAlias = DataFrame | None
12+
| --------- Used at runtime here
1213
|
1314
help: Move out of type-checking block
1415
1 + from pandas import DataFrame

crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_1.py.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ TC004 [*] Move import `datetime.datetime` out of type-checking block. Import is
88
4 | from datetime import datetime
99
| ^^^^^^^^
1010
5 | x = datetime
11+
| -------- Used at runtime here
1112
|
1213
help: Move out of type-checking block
1314
1 | from typing import TYPE_CHECKING

crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_11.py.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ TC004 [*] Move import `typing.List` out of type-checking block. Import is used f
99
| ^^^^
1010
5 |
1111
6 | __all__ = ("List",)
12+
| ------ Used at runtime here
1213
|
1314
help: Move out of type-checking block
1415
1 | from typing import TYPE_CHECKING

crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_12.py.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ TC004 [*] Move import `collections.abc.Callable` out of type-checking block. Imp
99
| ^^^^^^^^
1010
7 |
1111
8 | AnyCallable: TypeAlias = Callable[..., Any]
12+
| -------- Used at runtime here
1213
|
1314
help: Move out of type-checking block
1415
1 | from __future__ import annotations

crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_17.py.snap

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
33
---
44
TC004 [*] Move import `pandas.DataFrame` out of type-checking block. Import is used for more than type hinting.
5-
--> TC004_17.py:6:24
6-
|
7-
5 | if TYPE_CHECKING:
8-
6 | from pandas import DataFrame
9-
| ^^^^^^^^^
10-
|
5+
--> TC004_17.py:6:24
6+
|
7+
5 | if TYPE_CHECKING:
8+
6 | from pandas import DataFrame
9+
| ^^^^^^^^^
10+
|
11+
::: TC004_17.py:10:9
12+
|
13+
9 | def example() -> DataFrame:
14+
10 | x = DataFrame()
15+
| --------- Used at runtime here
16+
|
1117
help: Move out of type-checking block
1218
1 | from __future__ import annotations
1319
2 |

crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TC004_2.py.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ TC004 [*] Move import `datetime.date` out of type-checking block. Import is used
88
4 | from datetime import date
99
| ^^^^
1010
|
11+
::: TC004_2.py:8:12
12+
|
13+
7 | def example():
14+
8 | return date()
15+
| ---- Used at runtime here
16+
|
1117
help: Move out of type-checking block
1218
1 | from typing import TYPE_CHECKING
1319
2 + from datetime import date

crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_module__app.py.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ TC004 [*] Move import `datetime` out of type-checking block. Import is used for
99
| ^^^^^^^^
1010
10 | from array import array # TC004
1111
|
12+
::: app.py:20:25
13+
|
14+
19 | @app.put("/datetime")
15+
20 | def set_datetime(value: datetime.datetime):
16+
| -------- Used at runtime here
17+
21 | pass
18+
|
1219
help: Move out of type-checking block
1320
4 |
1421
5 | import fastapi
@@ -32,6 +39,13 @@ TC004 [*] Move import `array.array` out of type-checking block. Import is used f
3239
11 |
3340
12 | app = fastapi.FastAPI("First application")
3441
|
42+
::: app.py:24:20
43+
|
44+
23 | @app_container.app.get("/array")
45+
24 | def get_array() -> array:
46+
| ----- Used at runtime here
47+
25 | pass
48+
|
3549
help: Move out of type-checking block
3650
4 |
3751
5 | import fastapi

0 commit comments

Comments
 (0)