Skip to content

Commit 848e473

Browse files
authored
[flake8-pyi] Fix PYI047 false negatives on PEP-695 type aliases (#9566)
## Summary Fixes one of the issues listed in #8771. Fairly straightforward! ## Test Plan `cargo test` / `cargo insta review`
1 parent 368e279 commit 848e473

File tree

5 files changed

+81
-17
lines changed

5 files changed

+81
-17
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI047.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,12 @@ def func(arg: _UsedPrivateTypeAlias) -> _UsedPrivateTypeAlias:
2020

2121

2222
def func2(arg: _PrivateTypeAlias) -> None: ...
23+
24+
type _UnusedPEP695 = int
25+
type _UnusedGeneric695[T] = list[T]
26+
27+
type _UsedPEP695 = str
28+
type _UsedGeneric695[T] = tuple[T, ...]
29+
30+
def func4(arg: _UsedPEP695) -> None: ...
31+
def func5(arg: _UsedGeneric695[bytes]) -> None: ...

crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI047.pyi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,12 @@ else:
2020

2121

2222
def func2(arg: _PrivateTypeAlias) -> None: ...
23+
24+
type _UnusedPEP695 = int
25+
type _UnusedGeneric695[T] = list[T]
26+
27+
type _UsedPEP695 = str
28+
type _UsedGeneric695[T] = tuple[T, ...]
29+
30+
def func4(arg: _UsedPEP695) -> None: ...
31+
def func5(arg: _UsedGeneric695[bytes]) -> None: ...

crates/ruff_linter/src/rules/flake8_pyi/rules/unused_private_type_definition.rs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
22
use ruff_macros::{derive_message_formats, violation};
33
use ruff_python_ast::helpers::map_subscript;
44
use ruff_python_ast::{self as ast, Expr, Stmt};
5-
use ruff_python_semantic::Scope;
5+
use ruff_python_semantic::{Scope, SemanticModel};
66
use ruff_text_size::Ranged;
77

88
use crate::checkers::ast::Checker;
@@ -267,9 +267,11 @@ pub(crate) fn unused_private_type_alias(
267267
scope: &Scope,
268268
diagnostics: &mut Vec<Diagnostic>,
269269
) {
270+
let semantic = checker.semantic();
271+
270272
for binding in scope
271273
.binding_ids()
272-
.map(|binding_id| checker.semantic().binding(binding_id))
274+
.map(|binding_id| semantic.binding(binding_id))
273275
{
274276
if !(binding.kind.is_assignment() && binding.is_private_declaration()) {
275277
continue;
@@ -281,32 +283,40 @@ pub(crate) fn unused_private_type_alias(
281283
let Some(source) = binding.source else {
282284
continue;
283285
};
284-
let Stmt::AnnAssign(ast::StmtAnnAssign {
285-
target, annotation, ..
286-
}) = checker.semantic().statement(source)
287-
else {
288-
continue;
289-
};
290-
let Some(ast::ExprName { id, .. }) = target.as_name_expr() else {
291-
continue;
292-
};
293286

294-
if !checker
295-
.semantic()
296-
.match_typing_expr(annotation, "TypeAlias")
297-
{
287+
let Some(alias_name) = extract_type_alias_name(semantic.statement(source), semantic) else {
298288
continue;
299-
}
289+
};
300290

301291
diagnostics.push(Diagnostic::new(
302292
UnusedPrivateTypeAlias {
303-
name: id.to_string(),
293+
name: alias_name.to_string(),
304294
},
305295
binding.range(),
306296
));
307297
}
308298
}
309299

300+
fn extract_type_alias_name<'a>(stmt: &'a ast::Stmt, semantic: &SemanticModel) -> Option<&'a str> {
301+
match stmt {
302+
ast::Stmt::AnnAssign(ast::StmtAnnAssign {
303+
target, annotation, ..
304+
}) => {
305+
let ast::ExprName { id, .. } = target.as_name_expr()?;
306+
if semantic.match_typing_expr(annotation, "TypeAlias") {
307+
Some(id)
308+
} else {
309+
None
310+
}
311+
}
312+
ast::Stmt::TypeAlias(ast::StmtTypeAlias { name, .. }) => {
313+
let ast::ExprName { id, .. } = name.as_name_expr()?;
314+
Some(id)
315+
}
316+
_ => None,
317+
}
318+
}
319+
310320
/// PYI049
311321
pub(crate) fn unused_private_typed_dict(
312322
checker: &Checker,

crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI047_PYI047.py.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,22 @@ PYI047.py:7:1: PYI047 Private TypeAlias `_T` is never used
1717
9 | # OK
1818
|
1919

20+
PYI047.py:24:6: PYI047 Private TypeAlias `_UnusedPEP695` is never used
21+
|
22+
22 | def func2(arg: _PrivateTypeAlias) -> None: ...
23+
23 |
24+
24 | type _UnusedPEP695 = int
25+
| ^^^^^^^^^^^^^ PYI047
26+
25 | type _UnusedGeneric695[T] = list[T]
27+
|
28+
29+
PYI047.py:25:6: PYI047 Private TypeAlias `_UnusedGeneric695` is never used
30+
|
31+
24 | type _UnusedPEP695 = int
32+
25 | type _UnusedGeneric695[T] = list[T]
33+
| ^^^^^^^^^^^^^^^^^ PYI047
34+
26 |
35+
27 | type _UsedPEP695 = str
36+
|
37+
2038

crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI047_PYI047.pyi.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,22 @@ PYI047.pyi:7:1: PYI047 Private TypeAlias `_T` is never used
1717
9 | # OK
1818
|
1919

20+
PYI047.pyi:24:6: PYI047 Private TypeAlias `_UnusedPEP695` is never used
21+
|
22+
22 | def func2(arg: _PrivateTypeAlias) -> None: ...
23+
23 |
24+
24 | type _UnusedPEP695 = int
25+
| ^^^^^^^^^^^^^ PYI047
26+
25 | type _UnusedGeneric695[T] = list[T]
27+
|
28+
29+
PYI047.pyi:25:6: PYI047 Private TypeAlias `_UnusedGeneric695` is never used
30+
|
31+
24 | type _UnusedPEP695 = int
32+
25 | type _UnusedGeneric695[T] = list[T]
33+
| ^^^^^^^^^^^^^^^^^ PYI047
34+
26 |
35+
27 | type _UsedPEP695 = str
36+
|
37+
2038

0 commit comments

Comments
 (0)