2424from pylint .checkers .utils import (
2525 in_type_checking_block ,
2626 is_module_ignored ,
27+ is_node_in_type_annotation_context ,
2728 is_postponed_evaluation_enabled ,
2829 is_sys_guard ,
2930 overridden_method ,
@@ -434,6 +435,14 @@ def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) ->
434435 "Used when an imported module or variable is not used from a "
435436 "`'from X import *'` style import." ,
436437 ),
438+ "R0615" : (
439+ "`%s` used only for typechecking but imported outside of a typechecking block" ,
440+ "unguarded-typing-import" ,
441+ "Used when an import is used only for typechecking but imported outside of a typechecking block." ,
442+ {
443+ "default_enabled" : False ,
444+ },
445+ ),
437446 "W0621" : (
438447 "Redefining name %r from outer scope (line %s)" ,
439448 "redefined-outer-name" ,
@@ -507,6 +516,7 @@ class NamesConsumer:
507516
508517 to_consume : Consumption
509518 consumed : Consumption
519+ consumed_as_type : Consumption
510520 consumed_uncertain : Consumption
511521 """Retrieves nodes filtered out by get_next_to_consume() that may not
512522 have executed.
@@ -523,6 +533,7 @@ def __init__(self, node: nodes.NodeNG, scope_type: str):
523533
524534 self .to_consume = copy .copy (node .locals )
525535 self .consumed = {}
536+ self .consumed_as_type = {}
526537 self .consumed_uncertain = defaultdict (list )
527538
528539 self .names_under_always_false_test : set [str ] = set ()
@@ -531,30 +542,46 @@ def __init__(self, node: nodes.NodeNG, scope_type: str):
531542 def __repr__ (self ) -> str :
532543 _to_consumes = [f"{ k } ->{ v } " for k , v in self .to_consume .items ()]
533544 _consumed = [f"{ k } ->{ v } " for k , v in self .consumed .items ()]
545+ _consumed_as_type = [f"{ k } ->{ v } " for k , v in self .consumed_as_type .items ()]
534546 _consumed_uncertain = [f"{ k } ->{ v } " for k , v in self .consumed_uncertain .items ()]
535547 to_consumes = ", " .join (_to_consumes )
536548 consumed = ", " .join (_consumed )
549+ consumed_as_type = ", " .join (_consumed_as_type )
537550 consumed_uncertain = ", " .join (_consumed_uncertain )
538551 return f"""
539552to_consume : { to_consumes }
540553consumed : { consumed }
554+ consumed_as_type : { consumed_as_type }
541555consumed_uncertain: { consumed_uncertain }
542556scope_type : { self .scope_type }
543557"""
544558
545- def mark_as_consumed (self , name : str , consumed_nodes : list [nodes .NodeNG ]) -> None :
559+ def mark_as_consumed (
560+ self ,
561+ name : str ,
562+ consumed_nodes : list [nodes .NodeNG ],
563+ consumed_as_type : bool = False ,
564+ ) -> None :
546565 """Mark the given nodes as consumed for the name.
547566
548567 If all of the nodes for the name were consumed, delete the name from
549568 the to_consume dictionary
550569 """
551- unconsumed = [ n for n in self .to_consume [ name ] if n not in set ( consumed_nodes )]
552- self . consumed [name ] = consumed_nodes
570+ consumed = self .consumed_as_type if consumed_as_type else self . consumed
571+ consumed [name ] = consumed_nodes
553572
554- if unconsumed :
555- self .to_consume [name ] = unconsumed
556- else :
557- del self .to_consume [name ]
573+ if name in self .to_consume :
574+ unconsumed = [
575+ n for n in self .to_consume [name ] if n not in set (consumed_nodes )
576+ ]
577+
578+ if unconsumed :
579+ self .to_consume [name ] = unconsumed
580+ else :
581+ del self .to_consume [name ]
582+
583+ if not consumed_as_type and name in self .consumed_as_type :
584+ del self .consumed_as_type [name ]
558585
559586 def get_next_to_consume (self , node : nodes .Name ) -> list [nodes .NodeNG ] | None :
560587 """Return a list of the nodes that define `node` from this scope.
@@ -594,6 +621,9 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
594621 if VariablesChecker ._comprehension_between_frame_and_node (node ):
595622 return found_nodes
596623
624+ if found_nodes is None :
625+ found_nodes = self .consumed_as_type .get (name )
626+
597627 # Filter out assignments in ExceptHandlers that node is not contained in
598628 if found_nodes :
599629 found_nodes = [
@@ -1386,7 +1416,8 @@ def leave_module(self, node: nodes.Module) -> None:
13861416 assert len (self ._to_consume ) == 1
13871417
13881418 self ._check_metaclasses (node )
1389- not_consumed = self ._to_consume .pop ().to_consume
1419+ consumer = self ._to_consume .pop ()
1420+ not_consumed = consumer .to_consume
13901421 # attempt to check for __all__ if defined
13911422 if "__all__" in node .locals :
13921423 self ._check_all (node , not_consumed )
@@ -1398,7 +1429,7 @@ def leave_module(self, node: nodes.Module) -> None:
13981429 if not self .linter .config .init_import and node .package :
13991430 return
14001431
1401- self ._check_imports (not_consumed )
1432+ self ._check_imports (not_consumed , consumer . consumed_as_type )
14021433 self ._type_annotation_names = []
14031434
14041435 def visit_classdef (self , node : nodes .ClassDef ) -> None :
@@ -1702,7 +1733,11 @@ def _undefined_and_used_before_checker(
17021733 # They will have already had a chance to emit used-before-assignment.
17031734 # We check here instead of before every single return in _check_consumer()
17041735 nodes_to_consume += current_consumer .consumed_uncertain [node .name ]
1705- current_consumer .mark_as_consumed (node .name , nodes_to_consume )
1736+ current_consumer .mark_as_consumed (
1737+ node .name ,
1738+ nodes_to_consume ,
1739+ consumed_as_type = is_node_in_type_annotation_context (node ),
1740+ )
17061741 if action is VariableVisitConsumerAction .CONTINUE :
17071742 continue
17081743 if action is VariableVisitConsumerAction .RETURN :
@@ -3236,7 +3271,11 @@ def _check_globals(self, not_consumed: Consumption) -> None:
32363271 self .add_message ("unused-variable" , args = (name ,), node = node )
32373272
32383273 # pylint: disable = too-many-branches
3239- def _check_imports (self , not_consumed : Consumption ) -> None :
3274+ def _check_imports (
3275+ self ,
3276+ not_consumed : Consumption ,
3277+ consumed_as_type : Consumption ,
3278+ ) -> None :
32403279 local_names = _fix_dot_imports (not_consumed )
32413280 checked = set ()
32423281 unused_wildcard_imports : defaultdict [
@@ -3324,8 +3363,26 @@ def _check_imports(self, not_consumed: Consumption) -> None:
33243363 self .add_message (
33253364 "unused-wildcard-import" , args = (arg_string , module [0 ]), node = module [1 ]
33263365 )
3366+
3367+ self ._check_type_imports (consumed_as_type )
3368+
33273369 del self ._to_consume
33283370
3371+ def _check_type_imports (
3372+ self ,
3373+ consumed_as_type : dict [str , list [nodes .NodeNG ]],
3374+ ) -> None :
3375+ for name , import_node in _fix_dot_imports (consumed_as_type ):
3376+ if import_node .names [0 ][0 ] == "*" :
3377+ continue
3378+
3379+ if not in_type_checking_block (import_node ):
3380+ self .add_message (
3381+ "unguarded-typing-import" ,
3382+ args = name ,
3383+ node = import_node ,
3384+ )
3385+
33293386 def _check_metaclasses (self , node : nodes .Module | nodes .FunctionDef ) -> None :
33303387 """Update consumption analysis for metaclasses."""
33313388 consumed : list [tuple [Consumption , str ]] = []
0 commit comments