diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI018.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI018.py index 75dbf08dd698b..402c17d7ba734 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI018.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI018.py @@ -1,8 +1,13 @@ import typing +import typing_extensions from typing import TypeVar +from typing_extensions import ParamSpec, TypeVarTuple _T = typing.TypeVar("_T") -_P = TypeVar("_P") +_Ts = typing_extensions.TypeVarTuple("_Ts") +_P = ParamSpec("_P") +_P2 = typing.ParamSpec("_P2") +_Ts2 = TypeVarTuple("_Ts2") # OK _UsedTypeVar = TypeVar("_UsedTypeVar") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI018.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI018.pyi index 75dbf08dd698b..402c17d7ba734 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI018.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI018.pyi @@ -1,8 +1,13 @@ import typing +import typing_extensions from typing import TypeVar +from typing_extensions import ParamSpec, TypeVarTuple _T = typing.TypeVar("_T") -_P = TypeVar("_P") +_Ts = typing_extensions.TypeVarTuple("_Ts") +_P = ParamSpec("_P") +_P2 = typing.ParamSpec("_P2") +_Ts2 = TypeVarTuple("_Ts2") # OK _UsedTypeVar = TypeVar("_UsedTypeVar") diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs index 094d1697c9f06..5da722a904394 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs @@ -7,28 +7,35 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; /// ## What it does -/// Checks for the presence of unused private `TypeVar` declarations. +/// Checks for the presence of unused private `TypeVar`, `ParamSpec` or +/// `TypeVarTuple` declarations. /// /// ## Why is this bad? -/// A private `TypeVar` that is defined but not used is likely a mistake, and +/// A private `TypeVar` that is defined but not used is likely a mistake. It /// should either be used, made public, or removed to avoid confusion. /// /// ## Example /// ```python /// import typing +/// import typing_extensions /// /// _T = typing.TypeVar("_T") +/// _Ts = typing_extensions.TypeVarTuple("_Ts") /// ``` #[violation] pub struct UnusedPrivateTypeVar { - name: String, + type_var_like_name: String, + type_var_like_kind: String, } impl Violation for UnusedPrivateTypeVar { #[derive_message_formats] fn message(&self) -> String { - let UnusedPrivateTypeVar { name } = self; - format!("Private TypeVar `{name}` is never used") + let UnusedPrivateTypeVar { + type_var_like_name, + type_var_like_kind, + } = self; + format!("Private {type_var_like_kind} `{type_var_like_name}` is never used") } } @@ -185,13 +192,26 @@ pub(crate) fn unused_private_type_var( let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { continue; }; - if !checker.semantic().match_typing_expr(func, "TypeVar") { + + let semantic = checker.semantic(); + let Some(type_var_like_kind) = semantic.resolve_call_path(func).and_then(|call_path| { + if semantic.match_typing_call_path(&call_path, "TypeVar") { + Some("TypeVar") + } else if semantic.match_typing_call_path(&call_path, "ParamSpec") { + Some("ParamSpec") + } else if semantic.match_typing_call_path(&call_path, "TypeVarTuple") { + Some("TypeVarTuple") + } else { + None + } + }) else { continue; - } + }; diagnostics.push(Diagnostic::new( UnusedPrivateTypeVar { - name: id.to_string(), + type_var_like_name: id.to_string(), + type_var_like_kind: type_var_like_kind.to_string(), }, binding.range(), )); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI018_PYI018.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI018_PYI018.py.snap index 878d32f43d633..9ca737a9209ca 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI018_PYI018.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI018_PYI018.py.snap @@ -1,22 +1,52 @@ --- source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs --- -PYI018.py:4:1: PYI018 Private TypeVar `_T` is never used +PYI018.py:6:1: PYI018 Private TypeVar `_T` is never used | -2 | from typing import TypeVar -3 | -4 | _T = typing.TypeVar("_T") +4 | from typing_extensions import ParamSpec, TypeVarTuple +5 | +6 | _T = typing.TypeVar("_T") | ^^ PYI018 -5 | _P = TypeVar("_P") +7 | _Ts = typing_extensions.TypeVarTuple("_Ts") +8 | _P = ParamSpec("_P") | -PYI018.py:5:1: PYI018 Private TypeVar `_P` is never used +PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used | -4 | _T = typing.TypeVar("_T") -5 | _P = TypeVar("_P") - | ^^ PYI018 -6 | -7 | # OK +6 | _T = typing.TypeVar("_T") +7 | _Ts = typing_extensions.TypeVarTuple("_Ts") + | ^^^ PYI018 +8 | _P = ParamSpec("_P") +9 | _P2 = typing.ParamSpec("_P2") | +PYI018.py:8:1: PYI018 Private ParamSpec `_P` is never used + | + 6 | _T = typing.TypeVar("_T") + 7 | _Ts = typing_extensions.TypeVarTuple("_Ts") + 8 | _P = ParamSpec("_P") + | ^^ PYI018 + 9 | _P2 = typing.ParamSpec("_P2") +10 | _Ts2 = TypeVarTuple("_Ts2") + | + +PYI018.py:9:1: PYI018 Private ParamSpec `_P2` is never used + | + 7 | _Ts = typing_extensions.TypeVarTuple("_Ts") + 8 | _P = ParamSpec("_P") + 9 | _P2 = typing.ParamSpec("_P2") + | ^^^ PYI018 +10 | _Ts2 = TypeVarTuple("_Ts2") + | + +PYI018.py:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used + | + 8 | _P = ParamSpec("_P") + 9 | _P2 = typing.ParamSpec("_P2") +10 | _Ts2 = TypeVarTuple("_Ts2") + | ^^^^ PYI018 +11 | +12 | # OK + | + diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI018_PYI018.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI018_PYI018.pyi.snap index d82b93d9b1c24..4585ae25babeb 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI018_PYI018.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI018_PYI018.pyi.snap @@ -1,22 +1,52 @@ --- source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs --- -PYI018.pyi:4:1: PYI018 Private TypeVar `_T` is never used +PYI018.pyi:6:1: PYI018 Private TypeVar `_T` is never used | -2 | from typing import TypeVar -3 | -4 | _T = typing.TypeVar("_T") +4 | from typing_extensions import ParamSpec, TypeVarTuple +5 | +6 | _T = typing.TypeVar("_T") | ^^ PYI018 -5 | _P = TypeVar("_P") +7 | _Ts = typing_extensions.TypeVarTuple("_Ts") +8 | _P = ParamSpec("_P") | -PYI018.pyi:5:1: PYI018 Private TypeVar `_P` is never used +PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used | -4 | _T = typing.TypeVar("_T") -5 | _P = TypeVar("_P") - | ^^ PYI018 -6 | -7 | # OK +6 | _T = typing.TypeVar("_T") +7 | _Ts = typing_extensions.TypeVarTuple("_Ts") + | ^^^ PYI018 +8 | _P = ParamSpec("_P") +9 | _P2 = typing.ParamSpec("_P2") | +PYI018.pyi:8:1: PYI018 Private ParamSpec `_P` is never used + | + 6 | _T = typing.TypeVar("_T") + 7 | _Ts = typing_extensions.TypeVarTuple("_Ts") + 8 | _P = ParamSpec("_P") + | ^^ PYI018 + 9 | _P2 = typing.ParamSpec("_P2") +10 | _Ts2 = TypeVarTuple("_Ts2") + | + +PYI018.pyi:9:1: PYI018 Private ParamSpec `_P2` is never used + | + 7 | _Ts = typing_extensions.TypeVarTuple("_Ts") + 8 | _P = ParamSpec("_P") + 9 | _P2 = typing.ParamSpec("_P2") + | ^^^ PYI018 +10 | _Ts2 = TypeVarTuple("_Ts2") + | + +PYI018.pyi:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used + | + 8 | _P = ParamSpec("_P") + 9 | _P2 = typing.ParamSpec("_P2") +10 | _Ts2 = TypeVarTuple("_Ts2") + | ^^^^ PYI018 +11 | +12 | # OK + | +