summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>2021-10-23 17:43:07 +0200
committerGitHub <noreply@github.com>2021-10-23 17:43:07 +0200
commit91f525f276ce73c436949a6128709f2dea99e64e (patch)
tree5ae017035e6cad129d8500e44dbc270c4489081a
parent8b60e428457fb1125f61bd97296665aef53eefb8 (diff)
downloadpylint-git-91f525f276ce73c436949a6128709f2dea99e64e.tar.gz
Make ``used-before-assignment`` consider classes in method annotation and defaults (#5184)
This closes #3771 Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
-rw-r--r--ChangeLog5
-rw-r--r--doc/whatsnew/2.12.rst5
-rw-r--r--pylint/checkers/variables.py42
-rw-r--r--tests/functional/u/use/used_before_assignement.py8
-rw-r--r--tests/functional/u/use/used_before_assignement.txt2
-rw-r--r--tests/functional/u/use/used_before_assignment.py38
-rw-r--r--tests/functional/u/use/used_before_assignment.txt5
-rw-r--r--tests/functional/u/use/used_before_assignment_py37.py28
-rw-r--r--tests/functional/u/use/used_before_assignment_py37.rc2
-rw-r--r--tests/functional/u/use/used_before_assignment_py37.txt1
10 files changed, 126 insertions, 10 deletions
diff --git a/ChangeLog b/ChangeLog
index 634e887b6..13a2d977f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -31,6 +31,11 @@ Release date: TBA
Closes #4031
+* ``used-before-assignment`` now correctly considers references to classes as type annotation
+ or default values in first-level methods
+
+ Closes #3771
+
* Fix bug with importing namespace packages with relative imports
Closes #2967 and #5131
diff --git a/doc/whatsnew/2.12.rst b/doc/whatsnew/2.12.rst
index f98406800..2a6c321fe 100644
--- a/doc/whatsnew/2.12.rst
+++ b/doc/whatsnew/2.12.rst
@@ -77,6 +77,11 @@ Other Changes
Closes #4031
+* ``used-before-assignment`` now correctly considers references to classes as type annotation
+ or default values in first-level methods
+
+ Closes #3771
+
* Fix double emitting of ``not-callable`` on inferrable ``properties``
Closes #4426
diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py
index 1d78184c8..c8d1f4f5d 100644
--- a/pylint/checkers/variables.py
+++ b/pylint/checkers/variables.py
@@ -59,6 +59,7 @@ import copy
import itertools
import os
import re
+import sys
from functools import lru_cache
from typing import DefaultDict, List, Tuple
@@ -71,6 +72,11 @@ from pylint.constants import PY39_PLUS
from pylint.interfaces import HIGH, INFERENCE, INFERENCE_FAILURE, IAstroidChecker
from pylint.utils import get_global_option
+if sys.version_info >= (3, 8):
+ from typing import Literal
+else:
+ from typing_extensions import Literal
+
SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$")
FUTURE = "__future__"
# regexp for ignored argument name
@@ -1195,6 +1201,16 @@ class VariablesChecker(BaseChecker):
)
elif self._is_only_type_assignment(node, defstmt):
self.add_message("undefined-variable", node=node, args=node.name)
+ elif isinstance(defstmt, nodes.ClassDef):
+ is_first_level_ref = self._is_first_level_self_reference(
+ node, defstmt
+ )
+ if is_first_level_ref == 2:
+ self.add_message(
+ "used-before-assignment", node=node, args=node.name
+ )
+ if is_first_level_ref:
+ break
current_consumer.mark_as_consumed(node.name, found_nodes)
# check it's not a loop variable used outside the loop
@@ -1569,6 +1585,32 @@ class VariablesChecker(BaseChecker):
return False
return True
+ @staticmethod
+ def _is_first_level_self_reference(
+ node: nodes.Name, defstmt: nodes.ClassDef
+ ) -> Literal[0, 1, 2]:
+ """Check if a first level method's annotation or default values
+ refers to its own class.
+
+ Return values correspond to:
+ 0 = Continue
+ 1 = Break
+ 2 = Break + emit message
+ """
+ if node.frame().parent == defstmt:
+ # Check if used as type annotation
+ # Break but don't emit message if postponed evaluation is enabled
+ if utils.is_node_in_type_annotation_context(node):
+ if not utils.is_postponed_evaluation_enabled(node):
+ return 2
+ return 1
+ # Check if used as default value by calling the class
+ if isinstance(node.parent, nodes.Call) and isinstance(
+ node.parent.parent, nodes.Arguments
+ ):
+ return 2
+ return 0
+
def _ignore_class_scope(self, node):
"""
Return True if the node is in a local class scope, as an assignment.
diff --git a/tests/functional/u/use/used_before_assignement.py b/tests/functional/u/use/used_before_assignement.py
deleted file mode 100644
index 285f3d180..000000000
--- a/tests/functional/u/use/used_before_assignement.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""pylint doesn't see the NameError in this module"""
-#pylint: disable=consider-using-f-string
-__revision__ = None
-
-MSG = "hello %s" % MSG # [used-before-assignment]
-
-MSG2 = ("hello %s" %
- MSG2) # [used-before-assignment]
diff --git a/tests/functional/u/use/used_before_assignement.txt b/tests/functional/u/use/used_before_assignement.txt
deleted file mode 100644
index d19fe322a..000000000
--- a/tests/functional/u/use/used_before_assignement.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-used-before-assignment:5:19::Using variable 'MSG' before assignment
-used-before-assignment:8:8::Using variable 'MSG2' before assignment
diff --git a/tests/functional/u/use/used_before_assignment.py b/tests/functional/u/use/used_before_assignment.py
new file mode 100644
index 000000000..376515c54
--- /dev/null
+++ b/tests/functional/u/use/used_before_assignment.py
@@ -0,0 +1,38 @@
+"""pylint doesn't see the NameError in this module"""
+# pylint: disable=consider-using-f-string, missing-function-docstring
+__revision__ = None
+
+from typing import List
+
+
+MSG = "hello %s" % MSG # [used-before-assignment]
+
+MSG2 = "hello %s" % MSG2 # [used-before-assignment]
+
+
+class MyClass:
+ """Type annotation or default values for first level methods can't refer to their own class"""
+
+ def incorrect_typing_method(
+ self, other: MyClass # [used-before-assignment]
+ ) -> bool:
+ return self == other
+
+ def incorrect_nested_typing_method(
+ self, other: List[MyClass] # [used-before-assignment]
+ ) -> bool:
+ return self == other[0]
+
+ def incorrect_default_method(
+ self, other=MyClass() # [used-before-assignment]
+ ) -> bool:
+ return self == other
+
+ def correct_string_typing_method(self, other: "MyClass") -> bool:
+ return self == other
+
+ def correct_inner_typing_method(self) -> bool:
+ def inner_method(self, other: MyClass) -> bool:
+ return self == other
+
+ return inner_method(self, MyClass())
diff --git a/tests/functional/u/use/used_before_assignment.txt b/tests/functional/u/use/used_before_assignment.txt
new file mode 100644
index 000000000..096144dc7
--- /dev/null
+++ b/tests/functional/u/use/used_before_assignment.txt
@@ -0,0 +1,5 @@
+used-before-assignment:8:19::Using variable 'MSG' before assignment:HIGH
+used-before-assignment:10:20::Using variable 'MSG2' before assignment:HIGH
+used-before-assignment:17:21:MyClass.incorrect_typing_method:Using variable 'MyClass' before assignment:HIGH
+used-before-assignment:22:26:MyClass.incorrect_nested_typing_method:Using variable 'MyClass' before assignment:HIGH
+used-before-assignment:27:20:MyClass.incorrect_default_method:Using variable 'MyClass' before assignment:HIGH
diff --git a/tests/functional/u/use/used_before_assignment_py37.py b/tests/functional/u/use/used_before_assignment_py37.py
new file mode 100644
index 000000000..e17c345a9
--- /dev/null
+++ b/tests/functional/u/use/used_before_assignment_py37.py
@@ -0,0 +1,28 @@
+"""Tests for used-before-assignment with functions added in python 3.7"""
+# pylint: disable=missing-function-docstring
+from __future__ import annotations
+from typing import List
+
+
+class MyClass:
+ """With the future import only default values can't refer to the base class"""
+
+ def correct_typing_method(self, other: MyClass) -> bool:
+ return self == other
+
+ def second_correct_typing_method(self, other: List[MyClass]) -> bool:
+ return self == other[0]
+
+ def incorrect_default_method(
+ self, other=MyClass() # [used-before-assignment]
+ ) -> bool:
+ return self == other
+
+ def correct_string_typing_method(self, other: "MyClass") -> bool:
+ return self == other
+
+ def correct_inner_typing_method(self) -> bool:
+ def inner_method(self, other: MyClass) -> bool:
+ return self == other
+
+ return inner_method(self, MyClass())
diff --git a/tests/functional/u/use/used_before_assignment_py37.rc b/tests/functional/u/use/used_before_assignment_py37.rc
new file mode 100644
index 000000000..a17bb22da
--- /dev/null
+++ b/tests/functional/u/use/used_before_assignment_py37.rc
@@ -0,0 +1,2 @@
+[testoptions]
+min_pyver=3.7
diff --git a/tests/functional/u/use/used_before_assignment_py37.txt b/tests/functional/u/use/used_before_assignment_py37.txt
new file mode 100644
index 000000000..ec4fb036b
--- /dev/null
+++ b/tests/functional/u/use/used_before_assignment_py37.txt
@@ -0,0 +1 @@
+used-before-assignment:17:20:MyClass.incorrect_default_method:Using variable 'MyClass' before assignment:HIGH