@@ -992,6 +992,31 @@ def visit_annotated_value(self, node: ast.expr) -> None:
992
992
self .import_visitor .in_soft_use_context = previous_context
993
993
994
994
995
+ class CastTypeExpressionVisitor (AnnotationVisitor ):
996
+ """Visit a cast type expression and collect all the quoted names."""
997
+
998
+ def __init__ (self , typing_lookup : SupportsIsTyping ) -> None :
999
+ #: All the quoted_names referenced inside the type expression
1000
+ self .quoted_names : set [str ] = set ()
1001
+ self ._typing_lookup = typing_lookup
1002
+
1003
+ def is_typing (self , node : ast .AST , symbol : str ) -> bool :
1004
+ """Check if the given node matches the given typing symbol."""
1005
+ return self ._typing_lookup .is_typing (node , symbol )
1006
+
1007
+ def visit_annotation_name (self , node : ast .Name ) -> None :
1008
+ """Ignore visited names."""
1009
+ # We could either record them as quoted names pre-emptively or
1010
+ # as uses, but neither seems ideal, let's just skip these names
1011
+ # as we have previously.
1012
+
1013
+ def visit_annotation_string (self , node : ast .Constant ) -> None :
1014
+ """Collect all the names referenced inside the forward reference."""
1015
+ visitor = StringAnnotationVisitor (self ._typing_lookup )
1016
+ visitor .parse_and_visit_string_annotation (node .value )
1017
+ self .quoted_names .update (visitor .names )
1018
+
1019
+
995
1020
class ImportVisitor (
996
1021
DunderAllMixin ,
997
1022
FunctoolsSingledispatchMixin ,
@@ -1081,6 +1106,10 @@ def __init__(
1081
1106
#: Where typing.cast() is called with an unquoted type.
1082
1107
self .unquoted_types_in_casts : list [tuple [int , int , str ]] = []
1083
1108
1109
+ #: All forward referenced names used in cast type expressions
1110
+ # we need to track this in order to avoid false negatives for TC001-003
1111
+ self .quoted_type_names_in_casts : set [str ] = set ()
1112
+
1084
1113
#: For tracking which comprehension/IfExp we're currently inside of
1085
1114
self .active_context : Comprehension | ast .IfExp | None = None
1086
1115
@@ -1895,6 +1924,10 @@ def register_unquoted_type_in_typing_cast(self, node: ast.Call) -> None:
1895
1924
1896
1925
arg = node .args [0 ]
1897
1926
1927
+ visitor = CastTypeExpressionVisitor (self )
1928
+ visitor .visit (arg )
1929
+ self .quoted_type_names_in_casts .update (visitor .quoted_names )
1930
+
1898
1931
if isinstance (arg , ast .Constant ) and isinstance (arg .value , str ):
1899
1932
return # Type argument is already a string literal.
1900
1933
@@ -1999,10 +2032,13 @@ def unused_imports(self) -> Flake8Generator:
1999
2032
unused_imports = all_imports - self .visitor .names - self .visitor .soft_uses
2000
2033
used_imports = all_imports - unused_imports
2001
2034
already_imported_modules = [self .visitor .imports [name ].module for name in used_imports ]
2002
- annotation_names = (
2003
- [n for i in self .visitor .wrapped_annotations for n in i .names ]
2004
- + [i .annotation for i in self .visitor .unwrapped_annotations ]
2005
- + [n for i in self .visitor .excess_wrapped_annotations for n in i .names ]
2035
+ annotation_names = list (
2036
+ chain (
2037
+ (n for i in self .visitor .wrapped_annotations for n in i .names ),
2038
+ (i .annotation for i in self .visitor .unwrapped_annotations ),
2039
+ (n for i in self .visitor .excess_wrapped_annotations for n in i .names ),
2040
+ self .visitor .quoted_type_names_in_casts ,
2041
+ )
2006
2042
)
2007
2043
2008
2044
for name in unused_imports :
0 commit comments