Skip to content

Commit efa1954

Browse files
Fix used-before-assignment false positive for names shadowed by a later method (#11493)
Signed-off-by: Gaurav Jadhav <gaurav.vjadhav01@gmail.com> Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
1 parent 72a40fb commit efa1954

4 files changed

Lines changed: 212 additions & 1 deletion

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a false positive for ``used-before-assignment`` when a class body uses a module-level or builtin name that a method or nested class defined later in the same class body shadows.
2+
3+
Closes #9134

‎pylint/checkers/variables.py‎

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,56 @@ def _get_unpacking_extra_info(node: nodes.Assign, inferred: InferenceResult) ->
128128
return more
129129

130130

131+
def _is_bound_before_class(
132+
class_node: nodes.ClassDef,
133+
lookup_scope: nodes.LocalsDictNodeNG,
134+
bindings: list[nodes.NodeNG],
135+
) -> bool:
136+
"""Whether a module-level or builtin binding exists when the class body runs."""
137+
if not bindings:
138+
return False
139+
if lookup_scope is not class_node.root():
140+
return True
141+
if any(
142+
isinstance(ancestor, (nodes.FunctionDef, nodes.Lambda))
143+
for ancestor in class_node.node_ancestors()
144+
):
145+
# The class body runs when the function is called, which can be after
146+
# the module-level binding.
147+
return True
148+
return any(
149+
binding.lineno is not None and binding.lineno < class_node.lineno
150+
for binding in bindings
151+
)
152+
153+
154+
def _is_def_in_class_body(
155+
defstmt: _base_nodes.Statement, frame: nodes.LocalsDictNodeNG
156+
) -> bool:
157+
"""Whether ``defstmt`` is a def or class statement directly in the class body frame.
158+
159+
Such a statement is its own frame, but it binds its name in the class body.
160+
"""
161+
return (
162+
isinstance(defstmt, (nodes.FunctionDef, nodes.ClassDef))
163+
and isinstance(frame, nodes.ClassDef)
164+
and defstmt.parent.frame() is frame
165+
)
166+
167+
168+
def _has_outer_binding(
169+
name: str, defframe: nodes.LocalsDictNodeNG, class_node: nodes.ClassDef | None
170+
) -> bool:
171+
"""Whether name has a module-level or builtin binding.
172+
173+
When class_node is given, the binding must also exist when its body runs.
174+
"""
175+
lookup_scope, bindings = defframe.root().lookup(name)
176+
if class_node is None:
177+
return bool(bindings)
178+
return _is_bound_before_class(class_node, lookup_scope, bindings)
179+
180+
131181
def _detect_global_scope(
132182
node: nodes.Name,
133183
frame: nodes.LocalsDictNodeNG,
@@ -2374,6 +2424,9 @@ def _is_variable_violation(
23742424
maybe_before_assign = True
23752425
annotation_return = False
23762426
use_outer_definition = False
2427+
def_in_class_body = _is_def_in_class_body(defstmt, frame)
2428+
if def_in_class_body:
2429+
defframe = frame
23772430
if frame is not defframe:
23782431
maybe_before_assign = _detect_global_scope(node, frame, defframe)
23792432
elif defframe.parent is None:
@@ -2394,7 +2447,9 @@ def _is_variable_violation(
23942447
isinstance(frame, nodes.FunctionDef)
23952448
or isinstance(node.frame(), nodes.Lambda)
23962449
) and _assigned_locally(node)
2397-
if not forbid_lookup and defframe.root().lookup(node.name)[1]:
2450+
if not forbid_lookup and _has_outer_binding(
2451+
node.name, defframe, frame if def_in_class_body else None
2452+
):
23982453
maybe_before_assign = False
23992454
use_outer_definition = stmt == defstmt and not isinstance(
24002455
defnode, nodes.Comprehension
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""A class body may use a module-level or builtin name that a later method shadows.
2+
3+
Names in a class body are looked up in the class namespace first and then in the
4+
globals and builtins. A ``def`` or ``class`` further down the class body has not
5+
run yet, so it does not shadow the outer name at that point.
6+
7+
https://github.com/pylint-dev/pylint/issues/9134
8+
"""
9+
# pylint: disable=missing-class-docstring,missing-function-docstring
10+
# pylint: disable=too-few-public-methods,invalid-name,unused-argument
11+
import operator
12+
from datetime import timezone
13+
from functools import cached_property
14+
from os import path
15+
from string import Template
16+
17+
18+
class ModuleNameShadowedByMethod:
19+
joined = path.join("a", "b")
20+
21+
def path(self):
22+
return self
23+
24+
25+
class ModuleNameShadowedByNestedClass:
26+
joined = path.join("a", "b")
27+
28+
class path:
29+
pass
30+
31+
32+
class ModuleNameShadowedByAsyncMethod:
33+
joined = path.join("a", "b")
34+
35+
async def path(self):
36+
return self
37+
38+
39+
class ModuleClassShadowedByMethod:
40+
template = Template("$a")
41+
42+
def Template(self):
43+
return self
44+
45+
46+
class ModuleNameUsedInDecorator:
47+
@staticmethod
48+
def make(arg=path.join("a")):
49+
return arg
50+
51+
def path(self):
52+
return self
53+
54+
55+
class BuiltinShadowedByMethod:
56+
length = len("abc")
57+
58+
def len(self):
59+
return self
60+
61+
62+
class ModuleNameShadowedByProperty:
63+
equals = operator.eq
64+
65+
@property
66+
def operator(self):
67+
return self.equals
68+
69+
70+
class ModuleNameShadowedByCachedProperty:
71+
default = timezone.utc
72+
73+
@cached_property
74+
def timezone(self):
75+
return self.default
76+
77+
78+
class NameOnlyDefinedByLaterMethod:
79+
joined = missing.join("a") # [used-before-assignment]
80+
81+
def missing(self):
82+
return self
83+
84+
85+
class NameUsedInsideMethodBody:
86+
def method(self):
87+
return path.join("a"), Template("$a"), operator.eq, timezone.utc
88+
89+
def path(self):
90+
return self
91+
92+
93+
class NameUsedAfterMethod:
94+
def path(self):
95+
return self
96+
97+
joined = path(None)
98+
99+
100+
class ModuleNameDefinedAfterClass:
101+
value = defined_later(1) # [used-before-assignment]
102+
103+
def defined_later(self):
104+
return self
105+
106+
107+
defined_later = len
108+
109+
110+
class ModuleNameDefinedAfterClassByAssignment:
111+
# Known false negative, the same as for any other assignment in a class body.
112+
value = assigned_later(1)
113+
114+
assigned_later = 2
115+
116+
117+
assigned_later = len
118+
119+
120+
def enclosing_function():
121+
from os import sep # pylint: disable=import-outside-toplevel,unused-import
122+
123+
class Inner:
124+
joined = sep.join("a") # [used-before-assignment]
125+
126+
def sep(self):
127+
return self
128+
129+
return Inner
130+
131+
132+
def late_bound_module_name():
133+
class A:
134+
x = later([1])
135+
136+
def later(self): ...
137+
138+
return A
139+
140+
141+
later = len
142+
late_bound_module_name()
143+
144+
145+
class OuterClass:
146+
class NestedClass:
147+
joined = path.join("a", "b")
148+
149+
def path(self):
150+
return self
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
used-before-assignment:79:13:79:20:NameOnlyDefinedByLaterMethod:Using variable 'missing' before assignment:HIGH
2+
used-before-assignment:101:12:101:25:ModuleNameDefinedAfterClass:Using variable 'defined_later' before assignment:HIGH
3+
used-before-assignment:124:17:124:20:enclosing_function.Inner:Using variable 'sep' before assignment:HIGH

0 commit comments

Comments
 (0)