Skip to content

Commit b0824e9

Browse files
authored
Fix scoping for function annotations, decorators and base classes (#3713)
Fix scoping for function annotations, decorators and base classes Closes #1082, #3434, #3461 Reduce number of branches in variables checker Co-authored-by: Andrew Simmons <[email protected]>
1 parent b1f0a93 commit b0824e9

File tree

8 files changed

+95
-23
lines changed

8 files changed

+95
-23
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ What's New in Pylint 2.6.0?
77

88
Release date: TBA
99

10+
* Fix various scope-related bugs in ``undefined-variable`` checker
11+
12+
Close #1082, #3434, #3461
13+
1014
* bad-continuation and bad-whitespace have been removed, black or another formatter can help you with this better than Pylint
1115

1216
Close #246, #289, #638, #747, #1148, #1179, #1943, #2041, #2301, #2304, #2944, #3565

pylint/checkers/utils.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,13 +374,17 @@ def is_defined_before(var_node: astroid.Name) -> bool:
374374
return False
375375

376376

377-
def is_default_argument(node: astroid.node_classes.NodeNG) -> bool:
377+
def is_default_argument(
378+
node: astroid.node_classes.NodeNG,
379+
scope: Optional[astroid.node_classes.NodeNG] = None,
380+
) -> bool:
378381
"""return true if the given Name node is used in function or lambda
379382
default argument's value
380383
"""
381-
parent = node.scope()
382-
if isinstance(parent, (astroid.FunctionDef, astroid.Lambda)):
383-
for default_node in parent.args.defaults:
384+
if not scope:
385+
scope = node.scope()
386+
if isinstance(scope, (astroid.FunctionDef, astroid.Lambda)):
387+
for default_node in scope.args.defaults:
384388
for default_name_node in default_node.nodes_of_class(astroid.Name):
385389
if default_name_node is node:
386390
return True

pylint/checkers/variables.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ class NamesConsumer:
512512

513513
def __init__(self, node, scope_type):
514514
self._atomic = ScopeConsumer(copy.copy(node.locals), {}, scope_type)
515+
self.node = node
515516

516517
def __repr__(self):
517518
msg = "\nto_consume : {:s}\n".format(
@@ -958,17 +959,7 @@ def visit_name(self, node):
958959

959960
name = node.name
960961
frame = stmt.scope()
961-
# if the name node is used as a function default argument's value or as
962-
# a decorator, then start from the parent frame of the function instead
963-
# of the function frame - and thus open an inner class scope
964-
if (
965-
utils.is_default_argument(node)
966-
or utils.is_func_decorator(node)
967-
or utils.is_ancestor_name(frame, node)
968-
):
969-
start_index = len(self._to_consume) - 2
970-
else:
971-
start_index = len(self._to_consume) - 1
962+
start_index = len(self._to_consume) - 1
972963

973964
undefined_variable_is_enabled = self.linter.is_message_enabled(
974965
"undefined-variable"
@@ -982,16 +973,33 @@ def visit_name(self, node):
982973
# pylint: disable=too-many-nested-blocks; refactoring this block is a pain.
983974
for i in range(start_index, -1, -1):
984975
current_consumer = self._to_consume[i]
985-
# if the current scope is a class scope but it's not the inner
976+
977+
# The list of base classes in the class definition is not part
978+
# of the class body.
979+
# If the current scope is a class scope but it's not the inner
986980
# scope, ignore it. This prevents to access this scope instead of
987981
# the globals one in function members when there are some common
988982
# names.
989-
if current_consumer.scope_type == "class" and i != start_index:
990-
# The only exceptions are: when the variable forms an iter within a
991-
# comprehension scope; and/or when used as a default, decorator,
992-
# or annotation within a function.
993-
if self._ignore_class_scope(node):
994-
continue
983+
if current_consumer.scope_type == "class" and (
984+
utils.is_ancestor_name(current_consumer.node, node)
985+
or (i != start_index and self._ignore_class_scope(node))
986+
):
987+
continue
988+
989+
# if the name node is used as a function default argument's value or as
990+
# a decorator, then start from the parent frame of the function instead
991+
# of the function frame - and thus open an inner class scope
992+
if (
993+
current_consumer.scope_type == "function"
994+
and self._defined_in_function_definition(node, current_consumer.node)
995+
):
996+
# ignore function scope if is an annotation/default/decorator, as not in the body
997+
continue
998+
999+
if current_consumer.scope_type == "lambda" and utils.is_default_argument(
1000+
node, current_consumer.node
1001+
):
1002+
continue
9951003

9961004
# the name has already been consumed, only check it's not a loop
9971005
# variable used outside the loop
@@ -1463,13 +1471,19 @@ def _ignore_class_scope(self, node):
14631471
# tp = 2
14641472
# def func(self, arg=tp):
14651473
# ...
1474+
# class C:
1475+
# class Tp:
1476+
# pass
1477+
# class D(Tp):
1478+
# ...
14661479

14671480
name = node.name
14681481
frame = node.statement().scope()
14691482
in_annotation_or_default_or_decorator = self._defined_in_function_definition(
14701483
node, frame
14711484
)
1472-
if in_annotation_or_default_or_decorator:
1485+
in_ancestor_list = utils.is_ancestor_name(frame, node)
1486+
if in_annotation_or_default_or_decorator or in_ancestor_list:
14731487
frame_locals = frame.parent.scope().locals
14741488
else:
14751489
frame_locals = frame.locals

tests/checkers/unittest_variables.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,26 @@ def fun(self):
152152
with self.assertNoMessages():
153153
self.walk(module)
154154

155+
def test_listcomp_in_ancestors(self):
156+
""" Ensure list comprehensions in base classes are scoped correctly
157+
158+
https://github.com/PyCQA/pylint/issues/3434
159+
"""
160+
module = astroid.parse(
161+
"""
162+
import collections
163+
164+
165+
l = ["a","b","c"]
166+
167+
168+
class Foo(collections.namedtuple("Foo",[x+"_foo" for x in l])):
169+
pass
170+
"""
171+
)
172+
with self.assertNoMessages():
173+
self.walk(module)
174+
155175
def test_return_type_annotation(self):
156176
""" Make sure class attributes in scope for return type annotations.
157177

tests/functional/c/class_scope.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,23 @@ class Sub(Data):
2121
def func(self):
2222
"""check Sub is not defined here"""
2323
return Sub(), self # [undefined-variable]
24+
25+
26+
class Right:
27+
"""right"""
28+
class Result1:
29+
"""result one"""
30+
OK = 0
31+
def work(self) -> Result1:
32+
"""good type hint"""
33+
return self.Result1.OK
34+
35+
36+
class Wrong:
37+
"""wrong"""
38+
class Result2:
39+
"""result two"""
40+
OK = 0
41+
def work(self) -> self.Result2: # [undefined-variable]
42+
"""bad type hint"""
43+
return self.Result2.OK

tests/functional/c/class_scope.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ undefined-variable:13:Well.<lambda>:Undefined variable 'get_attr_bad'
44
undefined-variable:14:Well:Undefined variable 'attr'
55
undefined-variable:20:Well.Sub:Undefined variable 'Data'
66
undefined-variable:23:Well.func:Undefined variable 'Sub'
7+
undefined-variable:41:Wrong.work:Undefined variable 'self'

tests/functional/u/undefined_variable.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,10 @@ class DunderClass:
287287
def method(self):
288288
# This name is not defined in the AST but it's present at runtime
289289
return __class__
290+
291+
292+
def undefined_annotation(a:x): # [undefined-variable]
293+
if x == 2: # [used-before-assignment]
294+
for x in [1, 2]:
295+
pass
296+
return a

tests/functional/u/undefined_variable.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ undefined-variable:225:LambdaClass4.<lambda>:Undefined variable 'LambdaClass4'
2626
undefined-variable:233:LambdaClass5.<lambda>:Undefined variable 'LambdaClass5'
2727
used-before-assignment:254:func_should_fail:Using variable 'datetime' before assignment
2828
undefined-variable:281:not_using_loop_variable_accordingly:Undefined variable 'iteree'
29+
undefined-variable:292:undefined_annotation:Undefined variable 'x'
30+
used-before-assignment:293:undefined_annotation:Using variable 'x' before assignment

0 commit comments

Comments
 (0)