Skip to content

Fix #5713: Emit used-before-assignment instead of undefined-variable when accessing unused type annotations #5718

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions doc/whatsnew/2.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1543,7 +1543,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):
Expand Down
86 changes: 1 addition & 85 deletions tests/functional/u/undefined/undefined_variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)
5 changes: 1 addition & 4 deletions tests/functional/u/undefined/undefined_variable.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
90 changes: 90 additions & 0 deletions tests/functional/u/use/used_before_assignment_type_annotations.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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