From 5302f1fd29abee6cfd58fc90a01d446733a4af96 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Fri, 24 Dec 2021 12:44:34 -0500 Subject: [PATCH] Global variables must be assigned to be used --- pyflakes/checker.py | 34 ++++++++++++---- pyflakes/messages.py | 8 ++++ pyflakes/test/test_undefined_names.py | 56 +++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index 29d15d34..c9b86bea 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -250,10 +250,11 @@ class Binding: the node that this binding was last used. """ - def __init__(self, name, source): + def __init__(self, name, source, *, assigned=True): self.name = name self.source = source self.used = False + self.assigned = assigned def __str__(self): return self.name @@ -1007,6 +1008,12 @@ def addBinding(self, node, value): break existing = scope.get(value.name) + global_scope = self.scopeStack[-1] + if (existing and global_scope.get(value.name) == existing and + not existing.assigned): + # make sure the variable is in the global scope before setting as assigned + existing.assigned = True + if (existing and not isinstance(existing, Builtin) and not self.differentForks(node, existing.source)): @@ -1089,6 +1096,10 @@ def handleNodeLoad(self, node): continue binding = scope.get(name, None) + + if binding and binding.assigned is False: + self.report(messages.UndefinedName, node, name) + if isinstance(binding, Annotation) and not self._in_postponed_annotation: scope[name].used = True continue @@ -1159,12 +1170,19 @@ def handleNodeStore(self, node): continue # if the name was defined in that scope, and the name has # been accessed already in the current scope, and hasn't - # been declared global + # been assigned globally used = name in scope and scope[name].used if used and used[0] is self.scope and name not in self.scope.globals: # then it's probably a mistake self.report(messages.UndefinedLocal, scope[name].used[1], name, scope[name].source) + + # and remove UndefinedName messages already reported for this name + self.messages = [ + m for m in self.messages if not + isinstance(m, messages.UndefinedName) or + m.message_args[0] != name] + break parent_stmt = self.getParent(node) @@ -1836,7 +1854,7 @@ def ASSERT(self, node): self.report(messages.AssertTuple, node) self.handleChildren(node) - def GLOBAL(self, node): + def GLOBAL(self, node, assign_by_default=False): """ Keep track of globals declarations. """ @@ -1848,11 +1866,9 @@ def GLOBAL(self, node): # One 'global' statement can bind multiple (comma-delimited) names. for node_name in node.names: - node_value = Assignment(node_name, node) + node_value = Assignment(node_name, node, assigned=assign_by_default) # Remove UndefinedName messages already reported for this name. - # TODO: if the global is not used in this scope, it does not - # become a globally defined name. See test_unused_global. self.messages = [ m for m in self.messages if not isinstance(m, messages.UndefinedName) or @@ -1866,7 +1882,11 @@ def GLOBAL(self, node): for scope in self.scopeStack[global_scope_index + 1:]: scope[node_name] = node_value - NONLOCAL = GLOBAL + def NONLOCAL(self, node): + for node_name in node.names: + if not any(node_name in scope for scope in self.scopeStack[:-1]): + self.report(messages.NoBindingForNonlocal, node, node_name) + self.GLOBAL(node, assign_by_default=True) def GENERATOREXP(self, node): self.pushScope(GeneratorScope) diff --git a/pyflakes/messages.py b/pyflakes/messages.py index f45fd467..c85565c5 100644 --- a/pyflakes/messages.py +++ b/pyflakes/messages.py @@ -73,6 +73,14 @@ def __init__(self, filename, loc, name): self.message_args = (name,) +class NoBindingForNonlocal(Message): + message = 'no binding for nonlocal %r found' + + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc) + self.message_args = (name,) + + class DoctestSyntaxError(Message): message = 'syntax error in doctest' diff --git a/pyflakes/test/test_undefined_names.py b/pyflakes/test/test_undefined_names.py index c2d2d87f..6242c5fb 100644 --- a/pyflakes/test/test_undefined_names.py +++ b/pyflakes/test/test_undefined_names.py @@ -298,6 +298,62 @@ def c(): bar def b(): global bar; bar = 1 ''') + def test_unassigned_global_is_undefined(self): + """ + If a "global" is never given a value, it is undefined + """ + self.flakes(''' + def a(): + global fu + fu + ''', m.UndefinedName) + + self.flakes(''' + global fu + fu + ''', m.UndefinedName) + + def test_scope_defined_global(self): + """ + If a "global" is defined inside of a function only, + outside of the function it is undefined + """ + self.flakes(''' + global fu + def a(): + fu = 1 + fu + a() + fu + ''', m.UndefinedName) + + def test_scope_defined_nonlocal(self): + """ + If a "nonlocal" is declared in a previous scope, + it is defined + """ + self.flakes(''' + def a(): + fu = 1 + def b(): + nonlocal fu + fu + ''') + + def test_scope_undefined_nonlocal(self): + """ + If a "nonlocal" is never given a value, it is undefined + """ + self.flakes(''' + def a(): + nonlocal fu + ''', m.NoBindingForNonlocal) + + self.flakes(''' + def a(): + nonlocal fu, bar + ''', m.NoBindingForNonlocal, m.NoBindingForNonlocal) + def test_definedByGlobalMultipleNames(self): """ "global" can accept multiple names.