diff options
-rw-r--r-- | ChangeLog | 5 | ||||
-rw-r--r-- | doc/whatsnew/2.12.rst | 5 | ||||
-rw-r--r-- | pylint/checkers/variables.py | 42 | ||||
-rw-r--r-- | tests/functional/u/use/used_before_assignement.py | 8 | ||||
-rw-r--r-- | tests/functional/u/use/used_before_assignement.txt | 2 | ||||
-rw-r--r-- | tests/functional/u/use/used_before_assignment.py | 38 | ||||
-rw-r--r-- | tests/functional/u/use/used_before_assignment.txt | 5 | ||||
-rw-r--r-- | tests/functional/u/use/used_before_assignment_py37.py | 28 | ||||
-rw-r--r-- | tests/functional/u/use/used_before_assignment_py37.rc | 2 | ||||
-rw-r--r-- | tests/functional/u/use/used_before_assignment_py37.txt | 1 |
10 files changed, 126 insertions, 10 deletions
@@ -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 |