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 = [
@@ -1384,7 +1414,8 @@ def leave_module(self, node: nodes.Module) -> None:
13841414 assert len (self ._to_consume ) == 1
13851415
13861416 self ._check_metaclasses (node )
1387- not_consumed = self ._to_consume .pop ().to_consume
1417+ consumer = self ._to_consume .pop ()
1418+ not_consumed = consumer .to_consume
13881419 # attempt to check for __all__ if defined
13891420 if "__all__" in node .locals :
13901421 self ._check_all (node , not_consumed )
@@ -1396,7 +1427,7 @@ def leave_module(self, node: nodes.Module) -> None:
13961427 if not self .linter .config .init_import and node .package :
13971428 return
13981429
1399- self ._check_imports (not_consumed )
1430+ self ._check_imports (not_consumed , consumer . consumed_as_type )
14001431 self ._type_annotation_names = []
14011432
14021433 def visit_classdef (self , node : nodes .ClassDef ) -> None :
@@ -1700,7 +1731,11 @@ def _undefined_and_used_before_checker(
17001731 # They will have already had a chance to emit used-before-assignment.
17011732 # We check here instead of before every single return in _check_consumer()
17021733 nodes_to_consume += current_consumer .consumed_uncertain [node .name ]
1703- current_consumer .mark_as_consumed (node .name , nodes_to_consume )
1734+ current_consumer .mark_as_consumed (
1735+ node .name ,
1736+ nodes_to_consume ,
1737+ consumed_as_type = is_node_in_type_annotation_context (node ),
1738+ )
17041739 if action is VariableVisitConsumerAction .CONTINUE :
17051740 continue
17061741 if action is VariableVisitConsumerAction .RETURN :
@@ -3218,7 +3253,11 @@ def _check_globals(self, not_consumed: Consumption) -> None:
32183253 self .add_message ("unused-variable" , args = (name ,), node = node )
32193254
32203255 # pylint: disable = too-many-branches
3221- def _check_imports (self , not_consumed : Consumption ) -> None :
3256+ def _check_imports (
3257+ self ,
3258+ not_consumed : Consumption ,
3259+ consumed_as_type : Consumption ,
3260+ ) -> None :
32223261 local_names = _fix_dot_imports (not_consumed )
32233262 checked = set ()
32243263 unused_wildcard_imports : defaultdict [
@@ -3306,8 +3345,26 @@ def _check_imports(self, not_consumed: Consumption) -> None:
33063345 self .add_message (
33073346 "unused-wildcard-import" , args = (arg_string , module [0 ]), node = module [1 ]
33083347 )
3348+
3349+ self ._check_type_imports (consumed_as_type )
3350+
33093351 del self ._to_consume
33103352
3353+ def _check_type_imports (
3354+ self ,
3355+ consumed_as_type : dict [str , list [nodes .NodeNG ]],
3356+ ) -> None :
3357+ for name , import_node in _fix_dot_imports (consumed_as_type ):
3358+ if import_node .names [0 ][0 ] == "*" :
3359+ continue
3360+
3361+ if not in_type_checking_block (import_node ):
3362+ self .add_message (
3363+ "unguarded-typing-import" ,
3364+ args = name ,
3365+ node = import_node ,
3366+ )
3367+
33113368 def _check_metaclasses (self , node : nodes .Module | nodes .FunctionDef ) -> None :
33123369 """Update consumption analysis for metaclasses."""
33133370 consumed : list [tuple [Consumption , str ]] = []
0 commit comments