24
24
from pylint .checkers .utils import (
25
25
in_type_checking_block ,
26
26
is_module_ignored ,
27
+ is_node_in_type_annotation_context ,
27
28
is_postponed_evaluation_enabled ,
28
29
is_sys_guard ,
29
30
overridden_method ,
@@ -434,6 +435,14 @@ def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) ->
434
435
"Used when an imported module or variable is not used from a "
435
436
"`'from X import *'` style import." ,
436
437
),
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
+ ),
437
446
"W0621" : (
438
447
"Redefining name %r from outer scope (line %s)" ,
439
448
"redefined-outer-name" ,
@@ -507,6 +516,7 @@ class NamesConsumer:
507
516
508
517
to_consume : Consumption
509
518
consumed : Consumption
519
+ consumed_as_type : Consumption
510
520
consumed_uncertain : Consumption
511
521
"""Retrieves nodes filtered out by get_next_to_consume() that may not
512
522
have executed.
@@ -523,6 +533,7 @@ def __init__(self, node: nodes.NodeNG, scope_type: str):
523
533
524
534
self .to_consume = copy .copy (node .locals )
525
535
self .consumed = {}
536
+ self .consumed_as_type = {}
526
537
self .consumed_uncertain = defaultdict (list )
527
538
528
539
self .names_under_always_false_test : set [str ] = set ()
@@ -531,30 +542,46 @@ def __init__(self, node: nodes.NodeNG, scope_type: str):
531
542
def __repr__ (self ) -> str :
532
543
_to_consumes = [f"{ k } ->{ v } " for k , v in self .to_consume .items ()]
533
544
_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 ()]
534
546
_consumed_uncertain = [f"{ k } ->{ v } " for k , v in self .consumed_uncertain .items ()]
535
547
to_consumes = ", " .join (_to_consumes )
536
548
consumed = ", " .join (_consumed )
549
+ consumed_as_type = ", " .join (_consumed_as_type )
537
550
consumed_uncertain = ", " .join (_consumed_uncertain )
538
551
return f"""
539
552
to_consume : { to_consumes }
540
553
consumed : { consumed }
554
+ consumed_as_type : { consumed_as_type }
541
555
consumed_uncertain: { consumed_uncertain }
542
556
scope_type : { self .scope_type }
543
557
"""
544
558
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 :
546
565
"""Mark the given nodes as consumed for the name.
547
566
548
567
If all of the nodes for the name were consumed, delete the name from
549
568
the to_consume dictionary
550
569
"""
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
553
572
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 ]
558
585
559
586
def get_next_to_consume (self , node : nodes .Name ) -> list [nodes .NodeNG ] | None :
560
587
"""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:
594
621
if VariablesChecker ._comprehension_between_frame_and_node (node ):
595
622
return found_nodes
596
623
624
+ if found_nodes is None :
625
+ found_nodes = self .consumed_as_type .get (name )
626
+
597
627
# Filter out assignments in ExceptHandlers that node is not contained in
598
628
if found_nodes :
599
629
found_nodes = [
@@ -1386,7 +1416,8 @@ def leave_module(self, node: nodes.Module) -> None:
1386
1416
assert len (self ._to_consume ) == 1
1387
1417
1388
1418
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
1390
1421
# attempt to check for __all__ if defined
1391
1422
if "__all__" in node .locals :
1392
1423
self ._check_all (node , not_consumed )
@@ -1398,7 +1429,7 @@ def leave_module(self, node: nodes.Module) -> None:
1398
1429
if not self .linter .config .init_import and node .package :
1399
1430
return
1400
1431
1401
- self ._check_imports (not_consumed )
1432
+ self ._check_imports (not_consumed , consumer . consumed_as_type )
1402
1433
self ._type_annotation_names = []
1403
1434
1404
1435
def visit_classdef (self , node : nodes .ClassDef ) -> None :
@@ -1702,7 +1733,11 @@ def _undefined_and_used_before_checker(
1702
1733
# They will have already had a chance to emit used-before-assignment.
1703
1734
# We check here instead of before every single return in _check_consumer()
1704
1735
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
+ )
1706
1741
if action is VariableVisitConsumerAction .CONTINUE :
1707
1742
continue
1708
1743
if action is VariableVisitConsumerAction .RETURN :
@@ -3236,7 +3271,11 @@ def _check_globals(self, not_consumed: Consumption) -> None:
3236
3271
self .add_message ("unused-variable" , args = (name ,), node = node )
3237
3272
3238
3273
# 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 :
3240
3279
local_names = _fix_dot_imports (not_consumed )
3241
3280
checked = set ()
3242
3281
unused_wildcard_imports : defaultdict [
@@ -3324,8 +3363,26 @@ def _check_imports(self, not_consumed: Consumption) -> None:
3324
3363
self .add_message (
3325
3364
"unused-wildcard-import" , args = (arg_string , module [0 ]), node = module [1 ]
3326
3365
)
3366
+
3367
+ self ._check_type_imports (consumed_as_type )
3368
+
3327
3369
del self ._to_consume
3328
3370
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
+
3329
3386
def _check_metaclasses (self , node : nodes .Module | nodes .FunctionDef ) -> None :
3330
3387
"""Update consumption analysis for metaclasses."""
3331
3388
consumed : list [tuple [Consumption , str ]] = []
0 commit comments