summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2022-03-21 04:49:45 -0400
committerGitHub <noreply@github.com>2022-03-21 09:49:45 +0100
commit2cb553658ebb97fb4326ddcb6bbd904efa6e520a (patch)
tree1e482edfb5c02cdfb40020d60a4a39d1957589e8
parentd0e9fb1b24370a8a58c53b680340860aaeff7c29 (diff)
downloadpylint-git-2cb553658ebb97fb4326ddcb6bbd904efa6e520a.tar.gz
Fix #4590: `used-before-assignment` false positive for class definition in function scope (#5937)
-rw-r--r--ChangeLog5
-rw-r--r--doc/whatsnew/2.13.rst5
-rw-r--r--pylint/checkers/variables.py28
-rw-r--r--tests/functional/u/used/used_before_assignment_class_nested_under_function.py13
4 files changed, 44 insertions, 7 deletions
diff --git a/ChangeLog b/ChangeLog
index 24e7bdbde..c1917c330 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -376,6 +376,11 @@ Release date: TBA
Closes #5679
+* Fix false positive for ``used-before-assignment`` from a class definition
+ nested under a function subclassing a class defined outside the function.
+
+ Closes #4590
+
* Fix ``unnecessary_dict_index_lookup`` false positive when deleting a dictionary's entry.
Closes #4716
diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst
index c48703c3f..7b2f18165 100644
--- a/doc/whatsnew/2.13.rst
+++ b/doc/whatsnew/2.13.rst
@@ -267,6 +267,11 @@ Other Changes
Closes #5724
+* Fix false positive for ``used-before-assignment`` from a class definition
+ nested under a function subclassing a class defined outside the function.
+
+ Closes #4590
+
* Fix ``unnecessary_dict_index_lookup`` false positive when deleting a dictionary's entry.
Closes #4716
diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py
index d05f578c0..3580e660e 100644
--- a/pylint/checkers/variables.py
+++ b/pylint/checkers/variables.py
@@ -236,27 +236,41 @@ def _get_unpacking_extra_info(node, inferred):
def _detect_global_scope(node, frame, defframe):
- """Detect that the given frames shares a global
- scope.
+ """Detect that the given frames share a global scope.
- Two frames shares a global scope when neither
+ Two frames share a global scope when neither
of them are hidden under a function scope, as well
- as any of parent scope of them, until the root scope.
+ as any parent scope of them, until the root scope.
In this case, depending from something defined later on
- will not work, because it is still undefined.
+ will only work if guarded by a nested function definition.
Example:
class A:
# B has the same global scope as `C`, leading to a NameError.
+ # Return True to indicate a shared scope.
class B(C): ...
class C: ...
+ Whereas this does not lead to a NameError:
+ class A:
+ def guard():
+ # Return False to indicate no scope sharing.
+ class B(C): ...
+ class C: ...
"""
def_scope = scope = None
if frame and frame.parent:
scope = frame.parent.scope()
if defframe and defframe.parent:
def_scope = defframe.parent.scope()
+ if (
+ isinstance(frame, nodes.ClassDef)
+ and scope is not def_scope
+ and scope is utils.get_node_first_ancestor_of_type(node, nodes.FunctionDef)
+ ):
+ # If the current node's scope is a class nested under a function,
+ # and the def_scope is something else, then they aren't shared.
+ return False
if isinstance(frame, nodes.FunctionDef):
# If the parent of the current node is a
# function, then it can be under its scope
@@ -290,12 +304,12 @@ def _detect_global_scope(node, frame, defframe):
if break_scopes and len(set(break_scopes)) != 1:
# Store different scopes than expected.
# If the stored scopes are, in fact, the very same, then it means
- # that the two frames (frame and defframe) shares the same scope,
+ # that the two frames (frame and defframe) share the same scope,
# and we could apply our lineno analysis over them.
# For instance, this works when they are inside a function, the node
# that uses a definition and the definition itself.
return False
- # At this point, we are certain that frame and defframe shares a scope
+ # At this point, we are certain that frame and defframe share a scope
# and the definition of the first depends on the second.
return frame.lineno < defframe.lineno
diff --git a/tests/functional/u/used/used_before_assignment_class_nested_under_function.py b/tests/functional/u/used/used_before_assignment_class_nested_under_function.py
new file mode 100644
index 000000000..3627ae1e1
--- /dev/null
+++ b/tests/functional/u/used/used_before_assignment_class_nested_under_function.py
@@ -0,0 +1,13 @@
+"""https://github.com/PyCQA/pylint/issues/4590"""
+# pylint: disable=too-few-public-methods
+
+
+def conditional_class_factory():
+ """Define a nested class"""
+ class ConditionalClass(ModuleClass):
+ """Subclasses a name from the module scope"""
+ return ConditionalClass
+
+
+class ModuleClass:
+ """Module-level class"""