Skip to content

Commit

Permalink
Emit used-before-assignment when reimporting later in a function (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobtylerwalls authored Jun 23, 2022
1 parent 20a7d5d commit 2b42851
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 8 deletions.
4 changes: 4 additions & 0 deletions doc/whatsnew/2/2.15/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ False negatives fixed

Closes #6812

* Emit ``used-before-assignment`` when relying on a name that is reimported later in a function.

Closes #4624

* Emit ``used-before-assignment`` for self-referencing assignments under if conditions.

Closes #6643
Expand Down
20 changes: 13 additions & 7 deletions pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,15 @@ def _fix_dot_imports(not_consumed):
return sorted(names.items(), key=lambda a: a[1].fromlineno)


def _find_frame_imports(name, frame):
def _find_frame_imports(name: str, frame: nodes.LocalsDictNodeNG) -> bool:
"""Detect imports in the frame, with the required *name*.
Such imports can be considered assignments.
Such imports can be considered assignments if they are not globals.
Returns True if an import for the given name was found.
"""
if name in _flattened_scope_names(frame.nodes_of_class(nodes.Global)):
return False

imports = frame.nodes_of_class((nodes.Import, nodes.ImportFrom))
for import_node in imports:
for import_name, import_alias in import_node.names:
Expand All @@ -312,10 +315,10 @@ def _find_frame_imports(name, frame):
return True
elif import_name and import_name == name:
return True
return None
return False


def _import_name_is_global(stmt, global_names):
def _import_name_is_global(stmt, global_names) -> bool:
for import_name, import_alias in stmt.names:
# If the import uses an alias, check only that.
# Otherwise, check only the import name.
Expand All @@ -334,10 +337,13 @@ def _flattened_scope_names(
return set(itertools.chain.from_iterable(values))


def _assigned_locally(name_node):
def _assigned_locally(name_node: nodes.Name):
"""Checks if name_node has corresponding assign statement in same scope."""
assign_stmts = name_node.scope().nodes_of_class(nodes.AssignName)
return any(a.name == name_node.name for a in assign_stmts)
name_node_scope = name_node.scope()
assign_stmts = name_node_scope.nodes_of_class(nodes.AssignName)
return any(a.name == name_node.name for a in assign_stmts) or _find_frame_imports(
name_node.name, name_node_scope
)


def _has_locals_call_after_node(stmt, scope):
Expand Down
15 changes: 14 additions & 1 deletion tests/functional/u/used/used_before_assignment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""pylint doesn't see the NameError in this module"""
"""Miscellaneous used-before-assignment cases"""
# pylint: disable=consider-using-f-string, missing-function-docstring
__revision__ = None

Expand All @@ -13,3 +13,16 @@ def inner():
pass

outer()


# pylint: disable=unused-import, wrong-import-position, import-outside-toplevel, reimported, redefined-outer-name, global-statement
import time
def redefine_time_import():
print(time.time()) # [used-before-assignment]
import time


def redefine_time_import_with_global():
global time # pylint: disable=invalid-name
print(time.time())
import time
1 change: 1 addition & 0 deletions tests/functional/u/used/used_before_assignment.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
used-before-assignment:6:19:6:22::Using variable 'MSG' before assignment:HIGH
used-before-assignment:8:20:8:24::Using variable 'MSG2' before assignment:HIGH
used-before-assignment:11:4:11:9:outer:Using variable 'inner' before assignment:HIGH
used-before-assignment:21:10:21:14:redefine_time_import:Using variable 'time' before assignment:HIGH

0 comments on commit 2b42851

Please sign in to comment.