Skip to content

Commit 8b3a10f

Browse files
Fix used-before-assignment false positive for TYPE_CHECKING if/elif/else usage (#8071) (#8229)
(cherry picked from commit 58fce61) Co-authored-by: Zen Lee <[email protected]>
1 parent 55f1482 commit 8b3a10f

File tree

7 files changed

+186
-35
lines changed

7 files changed

+186
-35
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix false positive for ``used-before-assignment`` when
2+
``typing.TYPE_CHECKING`` is used with if/elif/else blocks.
3+
4+
Closes #7574

pylint/checkers/utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,6 +2193,55 @@ def is_class_attr(name: str, klass: nodes.ClassDef) -> bool:
21932193
return False
21942194

21952195

2196+
def is_defined(name: str, node: nodes.NodeNG) -> bool:
2197+
"""Checks whether a node defines the given variable name."""
2198+
is_defined_so_far = False
2199+
2200+
if isinstance(node, nodes.NamedExpr) and node.target.name == name:
2201+
return True
2202+
2203+
if isinstance(node, (nodes.Import, nodes.ImportFrom)) and any(
2204+
node_name[0] == name for node_name in node.names
2205+
):
2206+
return True
2207+
2208+
if isinstance(node, nodes.With):
2209+
is_defined_so_far = any(
2210+
isinstance(item[1], nodes.AssignName) and item[1].name == name
2211+
for item in node.items
2212+
)
2213+
2214+
if isinstance(node, (nodes.ClassDef, nodes.FunctionDef)):
2215+
is_defined_so_far = node.name == name
2216+
2217+
if isinstance(node, nodes.AnnAssign):
2218+
is_defined_so_far = (
2219+
node.value
2220+
and isinstance(node.target, nodes.AssignName)
2221+
and node.target.name == name
2222+
)
2223+
2224+
if isinstance(node, nodes.Assign):
2225+
is_defined_so_far = any(
2226+
any(
2227+
(
2228+
(
2229+
isinstance(elt, nodes.Starred)
2230+
and isinstance(elt.value, nodes.AssignName)
2231+
and elt.value.name == name
2232+
)
2233+
or (isinstance(elt, nodes.AssignName) and elt.name == name)
2234+
)
2235+
for elt in get_all_elements(target)
2236+
)
2237+
for target in node.targets
2238+
)
2239+
2240+
return is_defined_so_far or any(
2241+
is_defined(name, child) for child in node.get_children()
2242+
)
2243+
2244+
21962245
def get_inverse_comparator(op: str) -> str:
21972246
"""Returns the inverse comparator given a comparator.
21982247

pylint/checkers/variables.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2025,7 +2025,7 @@ def _in_lambda_or_comprehension_body(
20252025
parent = parent.parent
20262026
return False
20272027

2028-
# pylint: disable = too-many-statements, too-many-branches
2028+
# pylint: disable = too-many-branches
20292029
@staticmethod
20302030
def _is_variable_violation(
20312031
node: nodes.Name,
@@ -2209,27 +2209,21 @@ def _is_variable_violation(
22092209
)
22102210
):
22112211
# Exempt those definitions that are used inside the type checking
2212-
# guard or that are defined in both type checking guard branches.
2212+
# guard or that are defined in any elif/else type checking guard branches.
22132213
used_in_branch = defstmt_parent.parent_of(node)
2214-
defined_in_or_else = False
2215-
2216-
for definition in defstmt_parent.orelse:
2217-
if isinstance(definition, nodes.Assign):
2214+
if not used_in_branch:
2215+
if defstmt_parent.has_elif_block():
2216+
defined_in_or_else = utils.is_defined(
2217+
node.name, defstmt_parent.orelse[0]
2218+
)
2219+
else:
22182220
defined_in_or_else = any(
2219-
target.name == node.name
2220-
for target in definition.targets
2221-
if isinstance(target, nodes.AssignName)
2221+
utils.is_defined(node.name, content)
2222+
for content in defstmt_parent.orelse
22222223
)
2223-
elif isinstance(
2224-
definition, (nodes.ClassDef, nodes.FunctionDef)
2225-
):
2226-
defined_in_or_else = definition.name == node.name
2227-
2228-
if defined_in_or_else:
2229-
break
22302224

2231-
if not used_in_branch and not defined_in_or_else:
2232-
maybe_before_assign = True
2225+
if not defined_in_or_else:
2226+
maybe_before_assign = True
22332227

22342228
return maybe_before_assign, annotation_return, use_outer_definition
22352229

tests/functional/u/undefined/undefined_variable_py38.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
# Tests for annotation of variables and potentially undefinition
55

6+
from typing import TYPE_CHECKING
67

78
def typing_and_assignment_expression():
89
"""The variable gets assigned in an assignment expression"""
@@ -190,3 +191,22 @@ def expression_in_ternary_operator_inside_container_wrong_position():
190191
if (still_defined := False) == 1:
191192
NEVER_DEFINED_EITHER = 1
192193
print(still_defined)
194+
195+
196+
if TYPE_CHECKING:
197+
import enum
198+
import weakref
199+
elif input():
200+
if input() + 1:
201+
pass
202+
elif (enum := None):
203+
pass
204+
else:
205+
print(None if (weakref := '') else True)
206+
else:
207+
pass
208+
209+
def defined_by_walrus_in_type_checking() -> weakref:
210+
"""Usage of variables defined in TYPE_CHECKING blocks"""
211+
print(enum)
212+
return weakref
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
used-before-assignment:17:15:17:18:typing_and_self_referencing_assignment_expression:Using variable 'var' before assignment:HIGH
2-
used-before-assignment:23:15:23:18:self_referencing_assignment_expression:Using variable 'var' before assignment:HIGH
3-
undefined-variable:48:6:48:16::Undefined variable 'no_default':UNDEFINED
4-
undefined-variable:56:6:56:22::Undefined variable 'again_no_default':UNDEFINED
5-
undefined-variable:82:6:82:19::Undefined variable 'else_assign_1':INFERENCE
6-
undefined-variable:105:6:105:19::Undefined variable 'else_assign_2':INFERENCE
7-
used-before-assignment:140:10:140:16:type_annotation_used_improperly_after_comprehension:Using variable 'my_int' before assignment:HIGH
8-
used-before-assignment:147:10:147:16:type_annotation_used_improperly_after_comprehension_2:Using variable 'my_int' before assignment:HIGH
9-
used-before-assignment:177:12:177:16:expression_in_ternary_operator_inside_container_wrong_position:Using variable 'val3' before assignment:HIGH
10-
used-before-assignment:181:9:181:10::Using variable 'z' before assignment:HIGH
11-
used-before-assignment:188:6:188:19::Using variable 'NEVER_DEFINED' before assignment:CONTROL_FLOW
1+
used-before-assignment:18:15:18:18:typing_and_self_referencing_assignment_expression:Using variable 'var' before assignment:HIGH
2+
used-before-assignment:24:15:24:18:self_referencing_assignment_expression:Using variable 'var' before assignment:HIGH
3+
undefined-variable:49:6:49:16::Undefined variable 'no_default':UNDEFINED
4+
undefined-variable:57:6:57:22::Undefined variable 'again_no_default':UNDEFINED
5+
undefined-variable:83:6:83:19::Undefined variable 'else_assign_1':INFERENCE
6+
undefined-variable:106:6:106:19::Undefined variable 'else_assign_2':INFERENCE
7+
used-before-assignment:141:10:141:16:type_annotation_used_improperly_after_comprehension:Using variable 'my_int' before assignment:HIGH
8+
used-before-assignment:148:10:148:16:type_annotation_used_improperly_after_comprehension_2:Using variable 'my_int' before assignment:HIGH
9+
used-before-assignment:178:12:178:16:expression_in_ternary_operator_inside_container_wrong_position:Using variable 'val3' before assignment:HIGH
10+
used-before-assignment:182:9:182:10::Using variable 'z' before assignment:HIGH
11+
used-before-assignment:189:6:189:19::Using variable 'NEVER_DEFINED' before assignment:CONTROL_FLOW

tests/functional/u/used/used_before_assignment_typing.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,65 @@
11
"""Tests for used-before-assignment for typing related issues"""
2-
# pylint: disable=missing-function-docstring
2+
# pylint: disable=missing-function-docstring,ungrouped-imports,invalid-name
33

44

55
from typing import List, Optional, TYPE_CHECKING
66

77
if TYPE_CHECKING:
88
if True: # pylint: disable=using-constant-test
99
import math
10+
from urllib.request import urlopen
11+
import array
12+
import base64
13+
import binascii
14+
import bisect
15+
import calendar
16+
import collections
17+
import copy
1018
import datetime
19+
import email
20+
import heapq
21+
import json
22+
import mailbox
23+
import mimetypes
24+
import numbers
25+
import pprint
26+
import types
27+
import zoneinfo
28+
elif input():
29+
import calendar, bisect # pylint: disable=multiple-imports
30+
if input() + 1:
31+
import heapq
32+
else:
33+
import heapq
34+
elif input():
35+
try:
36+
numbers = None if input() else 1
37+
import array
38+
except Exception as e: # pylint: disable=broad-exception-caught
39+
import types
40+
finally:
41+
copy = None
42+
elif input():
43+
for i in range(1,2):
44+
email = None
45+
else: # pylint: disable=useless-else-on-loop
46+
json = None
47+
while input():
48+
import mailbox
49+
else: # pylint: disable=useless-else-on-loop
50+
mimetypes = None
51+
elif input():
52+
with input() as base64:
53+
pass
54+
with input() as temp:
55+
import binascii
56+
else:
1157
from urllib.request import urlopen
58+
zoneinfo: str = ''
59+
def pprint():
60+
pass
61+
class collections: # pylint: disable=too-few-public-methods,missing-class-docstring
62+
pass
1263

1364
class MyClass:
1465
"""Type annotation or default values for first level methods can't refer to their own class"""
@@ -111,3 +162,36 @@ class ConditionalImportGuardedWhenUsed: # pylint: disable=too-few-public-method
111162
"""Conditional imports also guarded by TYPE_CHECKING when used."""
112163
if TYPE_CHECKING:
113164
print(urlopen)
165+
166+
167+
class TypeCheckingMultiBranch: # pylint: disable=too-few-public-methods,unused-variable
168+
"""Test for defines in TYPE_CHECKING if/elif/else branching"""
169+
def defined_in_elif_branch(self) -> calendar.Calendar:
170+
print(bisect)
171+
return calendar.Calendar()
172+
173+
def defined_in_else_branch(self) -> urlopen:
174+
print(zoneinfo)
175+
print(pprint())
176+
print(collections())
177+
return urlopen
178+
179+
def defined_in_nested_if_else(self) -> heapq:
180+
print(heapq)
181+
return heapq
182+
183+
def defined_in_try_except(self) -> array:
184+
print(types)
185+
print(copy)
186+
print(numbers)
187+
return array
188+
189+
def defined_in_loops(self) -> json:
190+
print(email)
191+
print(mailbox)
192+
print(mimetypes)
193+
return json
194+
195+
def defined_in_with(self) -> base64:
196+
print(binascii)
197+
return base64
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
undefined-variable:17:21:17:28:MyClass.incorrect_typing_method:Undefined variable 'MyClass':UNDEFINED
2-
undefined-variable:22:26:22:33:MyClass.incorrect_nested_typing_method:Undefined variable 'MyClass':UNDEFINED
3-
undefined-variable:27:20:27:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED
4-
used-before-assignment:88:35:88:39:MyFourthClass.is_close:Using variable 'math' before assignment:HIGH
5-
used-before-assignment:101:20:101:28:VariableAnnotationsGuardedByTypeChecking:Using variable 'datetime' before assignment:HIGH
1+
undefined-variable:68:21:68:28:MyClass.incorrect_typing_method:Undefined variable 'MyClass':UNDEFINED
2+
undefined-variable:73:26:73:33:MyClass.incorrect_nested_typing_method:Undefined variable 'MyClass':UNDEFINED
3+
undefined-variable:78:20:78:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED
4+
used-before-assignment:139:35:139:39:MyFourthClass.is_close:Using variable 'math' before assignment:HIGH
5+
used-before-assignment:152:20:152:28:VariableAnnotationsGuardedByTypeChecking:Using variable 'datetime' before assignment:HIGH

0 commit comments

Comments
 (0)