From 0fb6a120c6f0fbc94e305542fe29e790274be096 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 26 Jan 2022 01:26:48 -0500 Subject: [PATCH] Fix #5713: Emit `used-before-assignment` instead of `undefined-variable` when accessing unused type annotations (#5718) * Add confidence level HIGH to `used-before-assignment` --- ChangeLog | 5 ++ doc/whatsnew/2.13.rst | 5 ++ pylint/checkers/variables.py | 9 +- .../u/undefined/undefined_variable.py | 86 +----------------- .../u/undefined/undefined_variable.txt | 5 +- ...used_before_assignment_type_annotations.py | 90 +++++++++++++++++++ ...sed_before_assignment_type_annotations.txt | 3 + 7 files changed, 113 insertions(+), 90 deletions(-) create mode 100644 tests/functional/u/use/used_before_assignment_type_annotations.py create mode 100644 tests/functional/u/use/used_before_assignment_type_annotations.txt diff --git a/ChangeLog b/ChangeLog index f11a4b09b4..3ee22bbffe 100644 --- a/ChangeLog +++ b/ChangeLog @@ -89,6 +89,11 @@ Release date: TBA * Pyreverse - add output in mermaidjs format +* Emit ``used-before-assignment`` instead of ``undefined-variable`` when attempting + to access unused type annotations. + + Closes #5713 + * ``used-before-assignment`` now considers that assignments in a try block may not have occurred when the except or finally blocks are executed. diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index b84cb09cb3..d96776f2b7 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -124,6 +124,11 @@ Other Changes Closes #4798 Closes #5081 +* Emit ``used-before-assignment`` instead of ``undefined-variable`` when attempting + to access unused type annotations. + + Closes #5713 + * Fixed extremely long processing of long lines with comma's. Closes #5483 diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 9df08cece4..e9ec67d38f 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1551,7 +1551,14 @@ def _check_consumer( ) elif self._is_only_type_assignment(node, defstmt): - self.add_message("undefined-variable", args=node.name, node=node) + if node.scope().locals.get(node.name): + self.add_message( + "used-before-assignment", args=node.name, node=node, confidence=HIGH + ) + else: + self.add_message( + "undefined-variable", args=node.name, node=node, confidence=HIGH + ) return (VariableVisitConsumerAction.CONSUME, found_nodes) elif isinstance(defstmt, nodes.ClassDef): diff --git a/tests/functional/u/undefined/undefined_variable.py b/tests/functional/u/undefined/undefined_variable.py index f9961509a4..3ea7cf064e 100644 --- a/tests/functional/u/undefined/undefined_variable.py +++ b/tests/functional/u/undefined/undefined_variable.py @@ -4,7 +4,7 @@ from __future__ import print_function # pylint: disable=wrong-import-position -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING DEFINED = 1 @@ -340,40 +340,6 @@ def decorated4(x): from types import GenericAlias object().__class_getitem__ = classmethod(GenericAlias) -# Tests for annotation of variables and potentially undefinition - -def value_and_type_assignment(): - """The variable assigned a value and type""" - variable: int = 2 - print(variable) - - -def only_type_assignment(): - """The variable never gets assigned a value""" - variable: int - print(variable) # [undefined-variable] - - -def both_type_and_value_assignment(): - """The variable first gets a type and subsequently a value""" - variable: int - variable = 1 - print(variable) - - -def value_assignment_after_access(): - """The variable gets a value after it has been accessed""" - variable: int - print(variable) # [undefined-variable] - variable = 1 - - -def value_assignment_from_iterator(): - """The variables gets a value from an iterator""" - variable: int - for variable in (1, 2): - print(variable) - GLOBAL_VAR: int GLOBAL_VAR_TWO: int @@ -390,53 +356,3 @@ def global_var_mixed_assignment(): GLOBAL_VAR: int GLOBAL_VAR_TWO: int - - -def assignment_in_comprehension(): - """A previously typed variables gets used in a comprehension. Don't crash!""" - some_list: List[int] - some_list = [1, 2, 3] - some_list = [i * 2 for i in some_list] - - -def decorator_returning_function(): - """A decorator that returns a wrapper function with decoupled typing""" - def wrapper_with_decoupled_typing(): - print(var) - - var: int - var = 2 - return wrapper_with_decoupled_typing - - -def decorator_returning_incorrect_function(): - """A decorator that returns a wrapper function with decoupled typing""" - def wrapper_with_type_and_no_value(): - print(var) # [undefined-variable] - - var: int - return wrapper_with_type_and_no_value - - -def typing_and_value_assignment_with_tuple_assignment(): - """The typed variables get assigned with a tuple assignment""" - var_one: int - var_two: int - var_one, var_two = 1, 1 - print(var_one) - print(var_two) - - -def nested_class_as_return_annotation(): - """A namedtuple as a class attribute is used as a return annotation - - Taken from https://github.com/PyCQA/pylint/issues/5568""" - from collections import namedtuple - - class MyObject: - Coords = namedtuple('Point', ['x', 'y']) - - def my_method(self) -> Coords: - pass - - print(MyObject) diff --git a/tests/functional/u/undefined/undefined_variable.txt b/tests/functional/u/undefined/undefined_variable.txt index bee5ab475a..03d50baf66 100644 --- a/tests/functional/u/undefined/undefined_variable.txt +++ b/tests/functional/u/undefined/undefined_variable.txt @@ -32,7 +32,4 @@ undefined-variable:293:27:293:28:undefined_annotation:Undefined variable 'x':UND used-before-assignment:294:7:294:8:undefined_annotation:Using variable 'x' before assignment:UNDEFINED undefined-variable:324:11:324:12:decorated3:Undefined variable 'x':UNDEFINED undefined-variable:329:19:329:20:decorated4:Undefined variable 'y':UNDEFINED -undefined-variable:354:10:354:18:only_type_assignment:Undefined variable 'variable':UNDEFINED -undefined-variable:367:10:367:18:value_assignment_after_access:Undefined variable 'variable':UNDEFINED -undefined-variable:384:10:384:20:global_var_mixed_assignment:Undefined variable 'GLOBAL_VAR':UNDEFINED -undefined-variable:415:14:415:17:decorator_returning_incorrect_function.wrapper_with_type_and_no_value:Undefined variable 'var':UNDEFINED +undefined-variable:350:10:350:20:global_var_mixed_assignment:Undefined variable 'GLOBAL_VAR':HIGH diff --git a/tests/functional/u/use/used_before_assignment_type_annotations.py b/tests/functional/u/use/used_before_assignment_type_annotations.py new file mode 100644 index 0000000000..1a03050c34 --- /dev/null +++ b/tests/functional/u/use/used_before_assignment_type_annotations.py @@ -0,0 +1,90 @@ +"""Tests for annotation of variables and potential use before assignment""" +# pylint: disable=too-few-public-methods, global-variable-not-assigned +from collections import namedtuple +from typing import List + +def value_and_type_assignment(): + """The variable assigned a value and type""" + variable: int = 2 + print(variable) + + +def only_type_assignment(): + """The variable never gets assigned a value""" + variable: int + print(variable) # [used-before-assignment] + + +def both_type_and_value_assignment(): + """The variable first gets a type and subsequently a value""" + variable: int + variable = 1 + print(variable) + + +def value_assignment_after_access(): + """The variable gets a value after it has been accessed""" + variable: int + print(variable) # [used-before-assignment] + variable = 1 + + +def value_assignment_from_iterator(): + """The variables gets a value from an iterator""" + variable: int + for variable in (1, 2): + print(variable) + + +def assignment_in_comprehension(): + """A previously typed variables gets used in a comprehension. Don't crash!""" + some_list: List[int] + some_list = [1, 2, 3] + some_list = [i * 2 for i in some_list] + + +def decorator_returning_function(): + """A decorator that returns a wrapper function with decoupled typing""" + def wrapper_with_decoupled_typing(): + print(var) + + var: int + var = 2 + return wrapper_with_decoupled_typing + + +def decorator_returning_incorrect_function(): + """A decorator that returns a wrapper function with decoupled typing""" + def wrapper_with_type_and_no_value(): + # This emits NameError rather than UnboundLocalError, so + # undefined-variable is okay, even though the traceback refers + # to "free variable 'var' referenced before assignment" + print(var) # [undefined-variable] + + var: int + return wrapper_with_type_and_no_value + + +def typing_and_value_assignment_with_tuple_assignment(): + """The typed variables get assigned with a tuple assignment""" + var_one: int + var_two: int + var_one, var_two = 1, 1 + print(var_one) + print(var_two) + + +def nested_class_as_return_annotation(): + """A namedtuple as a class attribute is used as a return annotation + + Taken from https://github.com/PyCQA/pylint/issues/5568""" + class MyObject: + """namedtuple as class attribute""" + Coords = namedtuple('Point', ['x', 'y']) + + def my_method(self) -> Coords: + """Return annotation is valid""" + # pylint: disable=unnecessary-pass + pass + + print(MyObject) diff --git a/tests/functional/u/use/used_before_assignment_type_annotations.txt b/tests/functional/u/use/used_before_assignment_type_annotations.txt new file mode 100644 index 0000000000..81e1646da8 --- /dev/null +++ b/tests/functional/u/use/used_before_assignment_type_annotations.txt @@ -0,0 +1,3 @@ +used-before-assignment:15:10:15:18:only_type_assignment:Using variable 'variable' before assignment:HIGH +used-before-assignment:28:10:28:18:value_assignment_after_access:Using variable 'variable' before assignment:HIGH +undefined-variable:62:14:62:17:decorator_returning_incorrect_function.wrapper_with_type_and_no_value:Undefined variable 'var':HIGH