Skip to content

Commit

Permalink
[pyflakes] Visit forward annotations in TypeAliasType as types (`…
Browse files Browse the repository at this point in the history
…F401`) (#15829)

## Summary

Fixes #15812 by visiting the
second argument as a type definition.

## Test Plan

New F401 tests based on the report.

---------

Co-authored-by: Alex Waygood <[email protected]>
  • Loading branch information
ntBre and AlexWaygood authored Jan 30, 2025
1 parent 4f2aea8 commit fe516e2
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 3 deletions.
67 changes: 67 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F401_34.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Regression tests for https://github.com/astral-sh/ruff/issues/15812"""


def f():
from typing import Union

from typing_extensions import TypeAliasType

Json = TypeAliasType(
"Json",
"Union[dict[str, Json], list[Json], str, int, float, bool, None]",
)


def f():
from typing import Union

from typing_extensions import TypeAliasType, TypeVar

T = TypeVar("T")
V = TypeVar("V")
Json = TypeAliasType(
"Json",
"Union[dict[str, Json], list[Json], str, int, float, bool, T, V, None]",
type_params=(T, V),
)


def f():
from typing import Union

from typing_extensions import TypeAliasType

Json = TypeAliasType(
value="Union[dict[str, Json], list[Json], str, int, float, bool, None]",
name="Json",
)


# strictly speaking it's a false positive to emit F401 for both of these, but
# we can't really be expected to understand that the strings here are type
# expressions (and type checkers probably wouldn't understand them as type
# expressions either!)
def f():
from typing import Union

from typing_extensions import TypeAliasType

args = [
"Json",
"Union[dict[str, Json], list[Json], str, int, float, bool, None]",
]

Json = TypeAliasType(*args)


def f():
from typing import Union

from typing_extensions import TypeAliasType

kwargs = {
"name": "Json",
"value": "Union[dict[str, Json], list[Json], str, int, float, bool, None]",
}

Json = TypeAliasType(**kwargs)
29 changes: 26 additions & 3 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::str::Quote;
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
use ruff_python_ast::{
self as ast, AnyParameterRef, Comprehension, ElifElseClause, ExceptHandler, Expr, ExprContext,
FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern, Stmt, Suite,
UnaryOp,
self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr,
ExprContext, FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern,
Stmt, Suite, UnaryOp,
};
use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Stylist};
Expand Down Expand Up @@ -1269,6 +1269,11 @@ impl<'a> Visitor<'a> for Checker<'a> {
.match_typing_qualified_name(&qualified_name, "TypeVar")
{
Some(typing::Callable::TypeVar)
} else if self
.semantic
.match_typing_qualified_name(&qualified_name, "TypeAliasType")
{
Some(typing::Callable::TypeAliasType)
} else if self
.semantic
.match_typing_qualified_name(&qualified_name, "NamedTuple")
Expand Down Expand Up @@ -1354,6 +1359,24 @@ impl<'a> Visitor<'a> for Checker<'a> {
}
}
}
Some(typing::Callable::TypeAliasType) => {
// Ex) TypeAliasType("Json", "Union[dict[str, Json]]", type_params=())
for (i, arg) in arguments.arguments_source_order().enumerate() {
match (i, arg) {
(1, ArgOrKeyword::Arg(arg)) => self.visit_type_definition(arg),
(_, ArgOrKeyword::Arg(arg)) => self.visit_non_type_definition(arg),
(_, ArgOrKeyword::Keyword(Keyword { arg, value, .. })) => {
if let Some(id) = arg {
if matches!(&**id, "value" | "type_params") {
self.visit_type_definition(value);
} else {
self.visit_non_type_definition(value);
}
}
}
}
}
}
Some(typing::Callable::NamedTuple) => {
// Ex) NamedTuple("a", [("a", int)])
let mut args = arguments.args.iter();
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ mod tests {
#[test_case(Rule::UnusedImport, Path::new("F401_22.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_23.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_32.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_34.py"))]
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.ipynb"))]
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F401_34.py:45:24: F401 [*] `typing.Union` imported but unused
|
43 | # expressions either!)
44 | def f():
45 | from typing import Union
| ^^^^^ F401
46 |
47 | from typing_extensions import TypeAliasType
|
= help: Remove unused import: `typing.Union`

Safe fix
42 42 | # expressions (and type checkers probably wouldn't understand them as type
43 43 | # expressions either!)
44 44 | def f():
45 |- from typing import Union
46 45 |
47 46 | from typing_extensions import TypeAliasType
48 47 |

F401_34.py:58:24: F401 [*] `typing.Union` imported but unused
|
57 | def f():
58 | from typing import Union
| ^^^^^ F401
59 |
60 | from typing_extensions import TypeAliasType
|
= help: Remove unused import: `typing.Union`

Safe fix
55 55 |
56 56 |
57 57 | def f():
58 |- from typing import Union
59 58 |
60 59 | from typing_extensions import TypeAliasType
61 60 |
1 change: 1 addition & 0 deletions crates/ruff_python_semantic/src/analyze/typing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub enum Callable {
NamedTuple,
TypedDict,
MypyExtension,
TypeAliasType,
}

#[derive(Debug, Copy, Clone)]
Expand Down

0 comments on commit fe516e2

Please sign in to comment.