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 ,
@@ -409,6 +410,14 @@ def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) ->
409
410
"Used when an imported module or variable is not used from a "
410
411
"`'from X import *'` style import." ,
411
412
),
413
+ "R0615" : (
414
+ "`%s` used only for typechecking but imported outside of a typechecking block" ,
415
+ "unguarded-typing-import" ,
416
+ "Used when an import is used only for typechecking but imported outside of a typechecking block." ,
417
+ {
418
+ "default_enabled" : False ,
419
+ },
420
+ ),
412
421
"W0621" : (
413
422
"Redefining name %r from outer scope (line %s)" ,
414
423
"redefined-outer-name" ,
@@ -482,6 +491,7 @@ class NamesConsumer:
482
491
483
492
to_consume : Consumption
484
493
consumed : Consumption
494
+ consumed_as_type : Consumption
485
495
consumed_uncertain : Consumption
486
496
"""Retrieves nodes filtered out by get_next_to_consume() that may not
487
497
have executed.
@@ -498,6 +508,7 @@ def __init__(self, node: nodes.NodeNG, scope_type: str):
498
508
499
509
self .to_consume = copy .copy (node .locals )
500
510
self .consumed = {}
511
+ self .consumed_as_type = {}
501
512
self .consumed_uncertain = defaultdict (list )
502
513
503
514
self .names_under_always_false_test : set [str ] = set ()
@@ -506,30 +517,46 @@ def __init__(self, node: nodes.NodeNG, scope_type: str):
506
517
def __repr__ (self ) -> str :
507
518
_to_consumes = [f"{ k } ->{ v } " for k , v in self .to_consume .items ()]
508
519
_consumed = [f"{ k } ->{ v } " for k , v in self .consumed .items ()]
520
+ _consumed_as_type = [f"{ k } ->{ v } " for k , v in self .consumed_as_type .items ()]
509
521
_consumed_uncertain = [f"{ k } ->{ v } " for k , v in self .consumed_uncertain .items ()]
510
522
to_consumes = ", " .join (_to_consumes )
511
523
consumed = ", " .join (_consumed )
524
+ consumed_as_type = ", " .join (_consumed_as_type )
512
525
consumed_uncertain = ", " .join (_consumed_uncertain )
513
526
return f"""
514
527
to_consume : { to_consumes }
515
528
consumed : { consumed }
529
+ consumed_as_type : { consumed_as_type }
516
530
consumed_uncertain: { consumed_uncertain }
517
531
scope_type : { self .scope_type }
518
532
"""
519
533
520
- def mark_as_consumed (self , name : str , consumed_nodes : list [nodes .NodeNG ]) -> None :
534
+ def mark_as_consumed (
535
+ self ,
536
+ name : str ,
537
+ consumed_nodes : list [nodes .NodeNG ],
538
+ consumed_as_type : bool = False ,
539
+ ) -> None :
521
540
"""Mark the given nodes as consumed for the name.
522
541
523
542
If all of the nodes for the name were consumed, delete the name from
524
543
the to_consume dictionary
525
544
"""
526
- unconsumed = [ n for n in self .to_consume [ name ] if n not in set ( consumed_nodes )]
527
- self . consumed [name ] = consumed_nodes
545
+ consumed = self .consumed_as_type if consumed_as_type else self . consumed
546
+ consumed [name ] = consumed_nodes
528
547
529
- if unconsumed :
530
- self .to_consume [name ] = unconsumed
531
- else :
532
- del self .to_consume [name ]
548
+ if name in self .to_consume :
549
+ unconsumed = [
550
+ n for n in self .to_consume [name ] if n not in set (consumed_nodes )
551
+ ]
552
+
553
+ if unconsumed :
554
+ self .to_consume [name ] = unconsumed
555
+ else :
556
+ del self .to_consume [name ]
557
+
558
+ if not consumed_as_type and name in self .consumed_as_type :
559
+ del self .consumed_as_type [name ]
533
560
534
561
def get_next_to_consume (self , node : nodes .Name ) -> list [nodes .NodeNG ] | None :
535
562
"""Return a list of the nodes that define `node` from this scope.
@@ -572,6 +599,9 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
572
599
if VariablesChecker ._comprehension_between_frame_and_node (node ):
573
600
return found_nodes
574
601
602
+ if found_nodes is None :
603
+ found_nodes = self .consumed_as_type .get (name )
604
+
575
605
# Filter out assignments in ExceptHandlers that node is not contained in
576
606
if found_nodes :
577
607
found_nodes = [
@@ -1356,7 +1386,8 @@ def leave_module(self, node: nodes.Module) -> None:
1356
1386
assert len (self ._to_consume ) == 1
1357
1387
1358
1388
self ._check_metaclasses (node )
1359
- not_consumed = self ._to_consume .pop ().to_consume
1389
+ consumer = self ._to_consume .pop ()
1390
+ not_consumed = consumer .to_consume
1360
1391
# attempt to check for __all__ if defined
1361
1392
if "__all__" in node .locals :
1362
1393
self ._check_all (node , not_consumed )
@@ -1368,7 +1399,7 @@ def leave_module(self, node: nodes.Module) -> None:
1368
1399
if not self .linter .config .init_import and node .package :
1369
1400
return
1370
1401
1371
- self ._check_imports (not_consumed )
1402
+ self ._check_imports (not_consumed , consumer . consumed_as_type )
1372
1403
self ._type_annotation_names = []
1373
1404
1374
1405
def visit_classdef (self , node : nodes .ClassDef ) -> None :
@@ -1672,7 +1703,11 @@ def _undefined_and_used_before_checker(
1672
1703
# They will have already had a chance to emit used-before-assignment.
1673
1704
# We check here instead of before every single return in _check_consumer()
1674
1705
nodes_to_consume += current_consumer .consumed_uncertain [node .name ]
1675
- current_consumer .mark_as_consumed (node .name , nodes_to_consume )
1706
+ current_consumer .mark_as_consumed (
1707
+ node .name ,
1708
+ nodes_to_consume ,
1709
+ consumed_as_type = is_node_in_type_annotation_context (node ),
1710
+ )
1676
1711
if action is VariableVisitConsumerAction .CONTINUE :
1677
1712
continue
1678
1713
if action is VariableVisitConsumerAction .RETURN :
@@ -3163,7 +3198,11 @@ def _check_globals(self, not_consumed: Consumption) -> None:
3163
3198
self .add_message ("unused-variable" , args = (name ,), node = node )
3164
3199
3165
3200
# pylint: disable = too-many-branches
3166
- def _check_imports (self , not_consumed : Consumption ) -> None :
3201
+ def _check_imports (
3202
+ self ,
3203
+ not_consumed : Consumption ,
3204
+ consumed_as_type : Consumption ,
3205
+ ) -> None :
3167
3206
local_names = _fix_dot_imports (not_consumed )
3168
3207
checked = set ()
3169
3208
unused_wildcard_imports : defaultdict [
@@ -3251,8 +3290,26 @@ def _check_imports(self, not_consumed: Consumption) -> None:
3251
3290
self .add_message (
3252
3291
"unused-wildcard-import" , args = (arg_string , module [0 ]), node = module [1 ]
3253
3292
)
3293
+
3294
+ self ._check_type_imports (consumed_as_type )
3295
+
3254
3296
del self ._to_consume
3255
3297
3298
+ def _check_type_imports (
3299
+ self ,
3300
+ consumed_as_type : dict [str , list [nodes .NodeNG ]],
3301
+ ) -> None :
3302
+ for name , import_node in _fix_dot_imports (consumed_as_type ):
3303
+ if import_node .names [0 ][0 ] == "*" :
3304
+ continue
3305
+
3306
+ if not in_type_checking_block (import_node ):
3307
+ self .add_message (
3308
+ "unguarded-typing-import" ,
3309
+ args = name ,
3310
+ node = import_node ,
3311
+ )
3312
+
3256
3313
def _check_metaclasses (self , node : nodes .Module | nodes .FunctionDef ) -> None :
3257
3314
"""Update consumption analysis for metaclasses."""
3258
3315
consumed : list [tuple [Consumption , str ]] = []
0 commit comments