diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI049.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI049.py index 237d6bb151f579..d1cd972597a31c 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI049.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI049.py @@ -11,13 +11,25 @@ class _UnusedTypedDict2(typing.TypedDict): class _UsedTypedDict(TypedDict): - foo: bytes + foo: bytes class _CustomClass(_UsedTypedDict): bar: list[int] + _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int}) _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes}) + def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ... + + +# In `.py` files, we don't flag unused definitions in class scopes (unlike in `.pyi` +# files). +class _CustomClass3: + class _UnusedTypeDict4(TypedDict): + pass + + def method(self) -> None: + _CustomClass3._UnusedTypeDict4() diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI049.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI049.pyi index 29612a224fed62..8703409eae0e12 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI049.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI049.pyi @@ -35,3 +35,13 @@ _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int}) _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes}) def uses_UsedTypedDict3(arg: _UsedTypedDict3) -> None: ... + + +# In `.pyi` files, we flag unused definitions in class scopes as well as in the global +# scope (unlike in `.py` files). +class _CustomClass3: + class _UnusedTypeDict4(TypedDict): + pass + + def method(self) -> None: + _CustomClass3._UnusedTypeDict4() diff --git a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs index 64938222d5520f..e070ae5adbc0b0 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs @@ -281,17 +281,21 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) { } } - if checker.enabled(Rule::UnusedPrivateTypeVar) { - flake8_pyi::rules::unused_private_type_var(checker, scope, &mut diagnostics); - } - if checker.enabled(Rule::UnusedPrivateProtocol) { - flake8_pyi::rules::unused_private_protocol(checker, scope, &mut diagnostics); - } - if checker.enabled(Rule::UnusedPrivateTypeAlias) { - flake8_pyi::rules::unused_private_type_alias(checker, scope, &mut diagnostics); - } - if checker.enabled(Rule::UnusedPrivateTypedDict) { - flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics); + if checker.source_type.is_stub() + || matches!(scope.kind, ScopeKind::Module | ScopeKind::Function(_)) + { + if checker.enabled(Rule::UnusedPrivateTypeVar) { + flake8_pyi::rules::unused_private_type_var(checker, scope, &mut diagnostics); + } + if checker.enabled(Rule::UnusedPrivateProtocol) { + flake8_pyi::rules::unused_private_protocol(checker, scope, &mut diagnostics); + } + if checker.enabled(Rule::UnusedPrivateTypeAlias) { + flake8_pyi::rules::unused_private_type_alias(checker, scope, &mut diagnostics); + } + if checker.enabled(Rule::UnusedPrivateTypedDict) { + flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics); + } } if checker.enabled(Rule::AsyncioDanglingTask) { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI049_PYI049.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI049_PYI049.py.snap index 577fa93a43c398..c50ecfef7c7b18 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI049_PYI049.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI049_PYI049.py.snap @@ -15,13 +15,11 @@ PYI049.py:9:7: PYI049 Private TypedDict `_UnusedTypedDict2` is never used 10 | bar: int | -PYI049.py:20:1: PYI049 Private TypedDict `_UnusedTypedDict3` is never used +PYI049.py:21:1: PYI049 Private TypedDict `_UnusedTypedDict3` is never used | -18 | bar: list[int] -19 | -20 | _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int}) +21 | _UnusedTypedDict3 = TypedDict("_UnusedTypedDict3", {"foo": int}) | ^^^^^^^^^^^^^^^^^ PYI049 -21 | _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes}) +22 | _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes}) | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI049_PYI049.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI049_PYI049.pyi.snap index 4235d1fe381918..189b4bade5d8d1 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI049_PYI049.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI049_PYI049.pyi.snap @@ -24,4 +24,13 @@ PYI049.pyi:34:1: PYI049 Private TypedDict `_UnusedTypedDict3` is never used 35 | _UsedTypedDict3 = TypedDict("_UsedTypedDict3", {"bar": bytes}) | +PYI049.pyi:43:11: PYI049 Private TypedDict `_UnusedTypeDict4` is never used + | +41 | # scope (unlike in `.py` files). +42 | class _CustomClass3: +43 | class _UnusedTypeDict4(TypedDict): + | ^^^^^^^^^^^^^^^^ PYI049 +44 | pass + | +