Skip to content
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

Catch usage of undefined name with global statement #324

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
37 changes: 34 additions & 3 deletions pyflakes/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ def __init__(self):
super(FunctionScope, self).__init__()
# Simplify: manage the special locals as globals
self.globals = self.alwaysUsed.copy()
self.global_names = []
self.returnValue = None # First non-empty return
self.isGenerator = False # Detect a generator

Expand Down Expand Up @@ -574,6 +575,9 @@ def _in_doctest(self):
return (len(self.scopeStack) >= 2 and
isinstance(self.scopeStack[1], DoctestScope))

def _global_scope(self):
return self.scopeStack[1 if self._in_doctest() else 0]

@property
def futuresAllowed(self):
if not all(isinstance(scope, ModuleScope)
Expand Down Expand Up @@ -735,6 +739,11 @@ def addBinding(self, node, value):
elif isinstance(existing, Importation) and value.redefines(existing):
existing.redefined.append(node)

if (isinstance(self.scope, FunctionScope) and
isinstance(value, Importation) and
value.name in self.scope.global_names):
self.store_global_scope(value.name, value)

if value.name in self.scope:
# then assume the rebound name is used as a global or within a loop
value.used = self.scope[value.name].used
Expand All @@ -757,6 +766,12 @@ def handleNodeLoad(self, node):
in_generators = None
importStarred = None

if (isinstance(self.scope, FunctionScope) and
name in self.scope.global_names and
name not in self._global_scope()):
# report name declared with global statement but undefined
self.report(messages.UndefinedName, node, name)

# try enclosing function scopes and global scope
for scope in self.scopeStack[-1::-1]:
if isinstance(scope, ClassScope):
Expand Down Expand Up @@ -833,6 +848,10 @@ def handleNodeStore(self, node):
scope[name].used[1], name, scope[name].source)
break

if (isinstance(self.scope, FunctionScope) and
name in self.scope.global_names):
self.store_global_scope(name, Assignment(name, node))

parent_stmt = self.getParent(node)
if isinstance(parent_stmt, (ast.For, ast.comprehension)) or (
parent_stmt != node.parent and
Expand Down Expand Up @@ -868,8 +887,15 @@ def on_conditional_branch():
# be executed.
return

if isinstance(self.scope, FunctionScope) and name in self.scope.globals:
self.scope.globals.remove(name)
if isinstance(self.scope, FunctionScope):
if name in self.scope.globals:
self.scope.globals.remove(name)
if name in self.scope.global_names:
self.scope.global_names.remove(name)
try:
del self._global_scope()[name]
except KeyError:
self.report(messages.UndefinedName, node, name)
else:
try:
del self.scope[name]
Expand Down Expand Up @@ -1016,6 +1042,10 @@ def handleForwardAnnotation():
def ignore(self, node):
pass

def store_global_scope(self, name, value):
"""This store name in global scope"""
self._global_scope()[name] = value

# "stmt" type nodes
DELETE = PRINT = FOR = ASYNCFOR = WHILE = IF = WITH = WITHITEM = \
ASYNCWITH = ASYNCWITHITEM = TRYFINALLY = EXEC = \
Expand Down Expand Up @@ -1122,7 +1152,8 @@ def GLOBAL(self, node):
m.message_args[0] != node_name]

# Bind name to global scope if it doesn't exist already.
global_scope.setdefault(node_name, node_value)
if isinstance(self.scope, FunctionScope):
self.scope.global_names.append(node_name)

# Bind name to non-global scopes, but as already "used".
node_value.used = (global_scope, node)
Expand Down
5 changes: 5 additions & 0 deletions pyflakes/test/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,11 @@ def f(): global foo; import foo
def g(): foo.is_used()
''')

self.flakes('''
def f(): global foo; import foo.bar
def g(): foo.is_used()
''')

@skipIf(version_info >= (3,), 'deprecated syntax')
def test_usedInBackquote(self):
self.flakes('import fu; `fu`')
Expand Down
8 changes: 8 additions & 0 deletions pyflakes/test/test_undefined_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,14 @@ def f2():
global m
''', m.UndefinedName)

def test_undefined_global(self):
"""Use an undefined name with global statement"""
self.flakes('''
def f():
global m
print(m)
''', m.UndefinedName)

def test_del(self):
"""Del deletes bindings."""
self.flakes('a = 1; del a; a', m.UndefinedName)
Expand Down