From f8d122182fa85c8e61016e1d8b016551df4a5d0e Mon Sep 17 00:00:00 2001 From: Zanie Date: Wed, 1 Nov 2023 20:32:29 -0500 Subject: [PATCH 1/6] Add `typing_extensions.TYPE_CHECKING` to detection of type checking blocks --- crates/ruff_python_semantic/src/analyze/typing.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index d6032573dc433..c8ffdea8b1489 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -315,10 +315,12 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> b } // Ex) `if typing.TYPE_CHECKING:` - if semantic - .resolve_call_path(test) - .is_some_and(|call_path| matches!(call_path.as_slice(), ["typing", "TYPE_CHECKING"])) - { + if semantic.resolve_call_path(test).is_some_and(|call_path| { + matches!( + call_path.as_slice(), + ["typing", "TYPE_CHECKING"] | ["typing_extensions" | "TYPE_CHECKING"] + ) + }) { return true; } From 7e95b4b76b1d75becdac9cc1e513689cc4edbbd9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Nov 2023 23:39:08 -0500 Subject: [PATCH 2/6] Re-use typing_extensions.TYPE_CHECKING if available --- .../fixtures/flake8_type_checking/TCH002.py | 11 ++++ .../flake8_type_checking/TCH004_16.py | 10 ++++ .../flake8_type_checking/TCH004_17.py | 10 ++++ .../fixtures/flake8_type_checking/TCH005.py | 6 ++ .../flake8_type_checking/typing_modules_1.py | 9 +++ .../flake8_type_checking/typing_modules_2.py | 9 +++ crates/ruff_linter/src/importer/mod.rs | 30 ++++++++-- .../src/rules/flake8_type_checking/mod.rs | 4 ++ ...__empty-type-checking-block_TCH005.py.snap | 15 +++++ ...t-in-type-checking-block_TCH004_16.py.snap | 4 ++ ...t-in-type-checking-block_TCH004_17.py.snap | 25 ++++++++ ...ing-only-third-party-import_TCH002.py.snap | 1 + ...hird-party-import_typing_modules_1.py.snap | 29 +++++++++ ...hird-party-import_typing_modules_2.py.snap | 27 +++++++++ .../src/analyze/typing.rs | 7 +-- crates/ruff_python_semantic/src/model.rs | 23 +++---- crates/ruff_python_stdlib/src/typing.rs | 60 ------------------- 17 files changed, 198 insertions(+), 82 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_16.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_17.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_type_checking/typing_modules_1.py create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_type_checking/typing_modules_2.py create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TCH004_16.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TCH004_17.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_typing_modules_1.py.snap create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_typing_modules_2.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH002.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH002.py index 9248c10775302..d3c36312eea7a 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH002.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH002.py @@ -172,3 +172,14 @@ def f(): from module import Member x: Member = 1 + + +def f(): + from typing_extensions import TYPE_CHECKING + + from pandas import y + + if TYPE_CHECKING: + _type = x + elif True: + _type = y diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_16.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_16.py new file mode 100644 index 0000000000000..5246360f3fc65 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_16.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from typing_extensions import TYPE_CHECKING + +if TYPE_CHECKING: + from pandas import DataFrame + + +def example() -> DataFrame: + pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_17.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_17.py new file mode 100644 index 0000000000000..edcd878addf22 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_17.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from typing_extensions import TYPE_CHECKING + +if TYPE_CHECKING: + from pandas import DataFrame + + +def example() -> DataFrame: + x = DataFrame() diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH005.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH005.py index 2036166c7e231..ee1c43e0ae28f 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH005.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH005.py @@ -37,3 +37,9 @@ class Test: if 0: x: List + + +from typing_extensions import TYPE_CHECKING + +if TYPE_CHECKING: + pass # TCH005 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/typing_modules_1.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/typing_modules_1.py new file mode 100644 index 0000000000000..f73487e3eb9ec --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/typing_modules_1.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from typing_extensions import Self + + +def func(): + from pandas import DataFrame + + df: DataFrame diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/typing_modules_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/typing_modules_2.py new file mode 100644 index 0000000000000..56db40970f087 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/typing_modules_2.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import typing_extensions + + +def func(): + from pandas import DataFrame + + df: DataFrame diff --git a/crates/ruff_linter/src/importer/mod.rs b/crates/ruff_linter/src/importer/mod.rs index 4624f12f688e0..e65a4d9412acd 100644 --- a/crates/ruff_linter/src/importer/mod.rs +++ b/crates/ruff_linter/src/importer/mod.rs @@ -132,11 +132,7 @@ impl<'a> Importer<'a> { )?; // Import the `TYPE_CHECKING` symbol from the typing module. - let (type_checking_edit, type_checking) = self.get_or_import_symbol( - &ImportRequest::import_from("typing", "TYPE_CHECKING"), - at, - semantic, - )?; + let (type_checking_edit, type_checking) = self.get_or_import_type_checking(at, semantic)?; // Add the import to a `TYPE_CHECKING` block. let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) { @@ -161,6 +157,30 @@ impl<'a> Importer<'a> { }) } + /// Generate an [`Edit`] to reference `typing.TYPE_CHECKING`. Returns the [`Edit`] necessary to + /// make the symbol available in the current scope along with the bound name of the symbol. + fn get_or_import_type_checking( + &self, + at: TextSize, + semantic: &SemanticModel, + ) -> Result<(Edit, String), ResolutionError> { + for module in semantic.typing_modules() { + if let Some((edit, name)) = self.get_symbol( + &ImportRequest::import_from(module, "TYPE_CHECKING"), + at, + semantic, + )? { + return Ok((edit, name)); + } + } + + self.get_or_import_symbol( + &ImportRequest::import_from("typing", "TYPE_CHECKING"), + at, + semantic, + ) + } + /// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make /// the symbol available in the current scope along with the bound name of the symbol. /// diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index cbea3cfacf60f..14bb56bdba7dd 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -23,6 +23,8 @@ mod tests { #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_13.py"))] #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_14.pyi"))] #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_15.py"))] + #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_16.py"))] + #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_17.py"))] #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_2.py"))] #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_3.py"))] #[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_4.py"))] @@ -36,6 +38,8 @@ mod tests { #[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))] #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))] #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))] + #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_1.py"))] + #[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("typing_modules_2.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__empty-type-checking-block_TCH005.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__empty-type-checking-block_TCH005.py.snap index e10dc87663bde..3a99c690d360b 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__empty-type-checking-block_TCH005.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__empty-type-checking-block_TCH005.py.snap @@ -96,4 +96,19 @@ TCH005.py:22:9: TCH005 [*] Found empty type-checking block 24 22 | 25 23 | +TCH005.py:45:5: TCH005 [*] Found empty type-checking block + | +44 | if TYPE_CHECKING: +45 | pass # TCH005 + | ^^^^ TCH005 + | + = help: Delete empty type-checking block + +ℹ Safe fix +41 41 | +42 42 | from typing_extensions import TYPE_CHECKING +43 43 | +44 |-if TYPE_CHECKING: +45 |- pass # TCH005 + diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TCH004_16.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TCH004_16.py.snap new file mode 100644 index 0000000000000..6c5ead27428ce --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TCH004_16.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TCH004_17.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TCH004_17.py.snap new file mode 100644 index 0000000000000..785c4c1d2e6c4 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__runtime-import-in-type-checking-block_TCH004_17.py.snap @@ -0,0 +1,25 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +TCH004_17.py:6:24: TCH004 [*] Move import `pandas.DataFrame` out of type-checking block. Import is used for more than type hinting. + | +5 | if TYPE_CHECKING: +6 | from pandas import DataFrame + | ^^^^^^^^^ TCH004 + | + = help: Move out of type-checking block + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from typing_extensions import TYPE_CHECKING + 4 |+from pandas import DataFrame +4 5 | +5 6 | if TYPE_CHECKING: +6 |- from pandas import DataFrame + 7 |+ pass +7 8 | +8 9 | +9 10 | def example() -> DataFrame: + + diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap index 4221412f21d1a..7d8365daa6ef9 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap @@ -248,5 +248,6 @@ TCH002.py:172:24: TCH002 [*] Move third-party import `module.Member` into a type 172 |- from module import Member 173 176 | 174 177 | x: Member = 1 +175 178 | diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_typing_modules_1.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_typing_modules_1.py.snap new file mode 100644 index 0000000000000..3e758fc6c110d --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_typing_modules_1.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +typing_modules_1.py:7:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block + | +6 | def func(): +7 | from pandas import DataFrame + | ^^^^^^^^^ TCH002 +8 | +9 | df: DataFrame + | + = help: Move into type-checking block + +ℹ Unsafe fix +1 1 | from __future__ import annotations +2 2 | +3 3 | from typing_extensions import Self + 4 |+from typing import TYPE_CHECKING + 5 |+ + 6 |+if TYPE_CHECKING: + 7 |+ from pandas import DataFrame +4 8 | +5 9 | +6 10 | def func(): +7 |- from pandas import DataFrame +8 11 | +9 12 | df: DataFrame + + diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_typing_modules_2.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_typing_modules_2.py.snap new file mode 100644 index 0000000000000..820be3f860f27 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__typing-only-third-party-import_typing_modules_2.py.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +typing_modules_2.py:7:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block + | +6 | def func(): +7 | from pandas import DataFrame + | ^^^^^^^^^ TCH002 +8 | +9 | df: DataFrame + | + = help: Move into type-checking block + +ℹ Unsafe fix +2 2 | +3 3 | import typing_extensions +4 4 | + 5 |+if typing_extensions.TYPE_CHECKING: + 6 |+ from pandas import DataFrame + 7 |+ +5 8 | +6 9 | def func(): +7 |- from pandas import DataFrame +8 10 | +9 11 | df: DataFrame + + diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index c8ffdea8b1489..bd18f70ba8aae 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -315,12 +315,7 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> b } // Ex) `if typing.TYPE_CHECKING:` - if semantic.resolve_call_path(test).is_some_and(|call_path| { - matches!( - call_path.as_slice(), - ["typing", "TYPE_CHECKING"] | ["typing_extensions" | "TYPE_CHECKING"] - ) - }) { + if semantic.match_typing_expr(test, "TYPE_CHECKING") { return true; } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 28b09e653fd0c..a56259afc9bf7 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -175,20 +175,13 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the call path is a reference to `typing.${target}`. pub fn match_typing_call_path(&self, call_path: &CallPath, target: &str) -> bool { - if call_path.as_slice() == ["typing", target] { + if matches!( + call_path.as_slice(), + ["typing" | "_typeshed" | "typing_extensions", target] + ) { return true; } - if call_path.as_slice() == ["_typeshed", target] { - return true; - } - - if is_typing_extension(target) { - if call_path.as_slice() == ["typing_extensions", target] { - return true; - } - } - if self.typing_modules.iter().any(|module| { let mut module: CallPath = from_unqualified_name(module); module.push(target); @@ -200,6 +193,14 @@ impl<'a> SemanticModel<'a> { false } + /// Return an iterator over the set of `typing` modules allowed in the semantic model. + pub fn typing_modules(&self) -> impl Iterator { + ["typing", "_typeshed", "typing_extensions"] + .iter() + .copied() + .chain(self.typing_modules.iter().map(String::as_str)) + } + /// Create a new [`Binding`] for a builtin. pub fn push_builtin(&mut self) -> BindingId { self.bindings.push(Binding { diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index d210964efbebc..45b472ce707d6 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -1,63 +1,3 @@ -/// Returns `true` if a name is a member of Python's `typing_extensions` module. -/// -/// See: -pub fn is_typing_extension(member: &str) -> bool { - matches!( - member, - "Annotated" - | "Any" - | "AsyncContextManager" - | "AsyncGenerator" - | "AsyncIterable" - | "AsyncIterator" - | "Awaitable" - | "ChainMap" - | "ClassVar" - | "Concatenate" - | "ContextManager" - | "Coroutine" - | "Counter" - | "DefaultDict" - | "Deque" - | "Final" - | "Literal" - | "LiteralString" - | "NamedTuple" - | "Never" - | "NewType" - | "NotRequired" - | "OrderedDict" - | "ParamSpec" - | "ParamSpecArgs" - | "ParamSpecKwargs" - | "Protocol" - | "Required" - | "Self" - | "TYPE_CHECKING" - | "Text" - | "Type" - | "TypeAlias" - | "TypeGuard" - | "TypeVar" - | "TypeVarTuple" - | "TypedDict" - | "Unpack" - | "assert_never" - | "assert_type" - | "clear_overloads" - | "final" - | "get_type_hints" - | "get_args" - | "get_origin" - | "get_overloads" - | "is_typeddict" - | "overload" - | "override" - | "reveal_type" - | "runtime_checkable" - ) -} - /// Returns `true` if a call path is a generic from the Python standard library (e.g. `list`, which /// can be used as `list[int]`). /// From cd0e48aef087a1160f65069dc3735d6625bbe63b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 8 Nov 2023 23:40:41 -0500 Subject: [PATCH 3/6] Remove import --- crates/ruff_python_semantic/src/model.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index a56259afc9bf7..00ac16729bfce 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -8,7 +8,6 @@ use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallP use ruff_python_ast::helpers::from_relative_import; use ruff_python_ast::{self as ast, Expr, Operator, Stmt}; use ruff_python_stdlib::path::is_python_stub_file; -use ruff_python_stdlib::typing::is_typing_extension; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::binding::{ @@ -177,7 +176,7 @@ impl<'a> SemanticModel<'a> { pub fn match_typing_call_path(&self, call_path: &CallPath, target: &str) -> bool { if matches!( call_path.as_slice(), - ["typing" | "_typeshed" | "typing_extensions", target] + ["typing" | "_typeshed" | "typing_extensions", _target] ) { return true; } From 545bd30c7bdc50009cf931ad585884cdd2117535 Mon Sep 17 00:00:00 2001 From: Zanie Date: Thu, 9 Nov 2023 00:32:32 -0600 Subject: [PATCH 4/6] Fix match in `match_typing_call_path` --- ...er__rules__flake8_pyi__tests__PYI050_PYI050.py.snap | 9 +++++++++ ...r__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap | 10 ++++++++++ crates/ruff_python_semantic/src/model.rs | 9 ++++----- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap index 11b5d19b5ebd7..1f70291a11d98 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap @@ -8,6 +8,15 @@ PYI050.py:13:24: PYI050 Prefer `typing.Never` over `NoReturn` for argument annot 14 | ... | +PYI050.py:18:10: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | +17 | def foo_no_return_typing_extensions( +18 | arg: typing_extensions.NoReturn, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI050 +19 | ): +20 | ... + | + PYI050.py:23:44: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations | 23 | def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap index a0cb503548fca..ee4be9d9f38bb 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap @@ -11,6 +11,16 @@ PYI050.pyi:6:24: PYI050 Prefer `typing.Never` over `NoReturn` for argument annot 8 | arg: typing_extensions.NoReturn, | +PYI050.pyi:8:10: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations + | + 6 | def foo_no_return(arg: NoReturn): ... # Error: PYI050 + 7 | def foo_no_return_typing_extensions( + 8 | arg: typing_extensions.NoReturn, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI050 + 9 | ): ... # Error: PYI050 +10 | def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): ... # Error: PYI050 + | + PYI050.pyi:10:44: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations | 8 | arg: typing_extensions.NoReturn, diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 00ac16729bfce..332cda194eb45 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -174,11 +174,10 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the call path is a reference to `typing.${target}`. pub fn match_typing_call_path(&self, call_path: &CallPath, target: &str) -> bool { - if matches!( - call_path.as_slice(), - ["typing" | "_typeshed" | "typing_extensions", _target] - ) { - return true; + if let ["typing" | "_typeshed" | "typing_extensions", name] = call_path.as_slice() { + if *name == target { + return true; + } } if self.typing_modules.iter().any(|module| { From 0e0e7382d863373c835d30fb639a1926f0239233 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Nov 2023 09:32:06 -0500 Subject: [PATCH 5/6] Revert extensions change --- ...__flake8_pyi__tests__PYI050_PYI050.py.snap | 9 --- ..._flake8_pyi__tests__PYI050_PYI050.pyi.snap | 10 ---- crates/ruff_python_semantic/src/model.rs | 13 +++- crates/ruff_python_stdlib/src/typing.rs | 60 +++++++++++++++++++ 4 files changed, 71 insertions(+), 21 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap index 1f70291a11d98..11b5d19b5ebd7 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.py.snap @@ -8,15 +8,6 @@ PYI050.py:13:24: PYI050 Prefer `typing.Never` over `NoReturn` for argument annot 14 | ... | -PYI050.py:18:10: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations - | -17 | def foo_no_return_typing_extensions( -18 | arg: typing_extensions.NoReturn, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI050 -19 | ): -20 | ... - | - PYI050.py:23:44: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations | 23 | def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap index ee4be9d9f38bb..a0cb503548fca 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI050_PYI050.pyi.snap @@ -11,16 +11,6 @@ PYI050.pyi:6:24: PYI050 Prefer `typing.Never` over `NoReturn` for argument annot 8 | arg: typing_extensions.NoReturn, | -PYI050.pyi:8:10: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations - | - 6 | def foo_no_return(arg: NoReturn): ... # Error: PYI050 - 7 | def foo_no_return_typing_extensions( - 8 | arg: typing_extensions.NoReturn, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI050 - 9 | ): ... # Error: PYI050 -10 | def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): ... # Error: PYI050 - | - PYI050.pyi:10:44: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations | 8 | arg: typing_extensions.NoReturn, diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 332cda194eb45..42617ddaf7396 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -8,6 +8,7 @@ use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallP use ruff_python_ast::helpers::from_relative_import; use ruff_python_ast::{self as ast, Expr, Operator, Stmt}; use ruff_python_stdlib::path::is_python_stub_file; +use ruff_python_stdlib::typing::is_typing_extension; use ruff_text_size::{Ranged, TextRange, TextSize}; use crate::binding::{ @@ -174,8 +175,16 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the call path is a reference to `typing.${target}`. pub fn match_typing_call_path(&self, call_path: &CallPath, target: &str) -> bool { - if let ["typing" | "_typeshed" | "typing_extensions", name] = call_path.as_slice() { - if *name == target { + if call_path.as_slice() == ["typing", target] { + return true; + } + + if call_path.as_slice() == ["_typeshed", target] { + return true; + } + + if is_typing_extension(target) { + if call_path.as_slice() == ["typing_extensions", target] { return true; } } diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index 45b472ce707d6..d210964efbebc 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -1,3 +1,63 @@ +/// Returns `true` if a name is a member of Python's `typing_extensions` module. +/// +/// See: +pub fn is_typing_extension(member: &str) -> bool { + matches!( + member, + "Annotated" + | "Any" + | "AsyncContextManager" + | "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "ChainMap" + | "ClassVar" + | "Concatenate" + | "ContextManager" + | "Coroutine" + | "Counter" + | "DefaultDict" + | "Deque" + | "Final" + | "Literal" + | "LiteralString" + | "NamedTuple" + | "Never" + | "NewType" + | "NotRequired" + | "OrderedDict" + | "ParamSpec" + | "ParamSpecArgs" + | "ParamSpecKwargs" + | "Protocol" + | "Required" + | "Self" + | "TYPE_CHECKING" + | "Text" + | "Type" + | "TypeAlias" + | "TypeGuard" + | "TypeVar" + | "TypeVarTuple" + | "TypedDict" + | "Unpack" + | "assert_never" + | "assert_type" + | "clear_overloads" + | "final" + | "get_type_hints" + | "get_args" + | "get_origin" + | "get_overloads" + | "is_typeddict" + | "overload" + | "override" + | "reveal_type" + | "runtime_checkable" + ) +} + /// Returns `true` if a call path is a generic from the Python standard library (e.g. `list`, which /// can be used as `list[int]`). /// From 1d7499d4de198c77c77ff350c71e2c61a1592194 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 9 Nov 2023 12:01:14 -0500 Subject: [PATCH 6/6] Avoid extra get_symbol --- crates/ruff_linter/src/importer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/importer/mod.rs b/crates/ruff_linter/src/importer/mod.rs index e65a4d9412acd..ecf3f38399f59 100644 --- a/crates/ruff_linter/src/importer/mod.rs +++ b/crates/ruff_linter/src/importer/mod.rs @@ -174,7 +174,7 @@ impl<'a> Importer<'a> { } } - self.get_or_import_symbol( + self.import_symbol( &ImportRequest::import_from("typing", "TYPE_CHECKING"), at, semantic,