From 6f7f425ddd2f7aec2ade1bef97c600a9ad93b687 Mon Sep 17 00:00:00 2001 From: joshua1b Date: Wed, 17 Jan 2018 23:17:33 +0900 Subject: [PATCH] Catch usage of undefined name with global statement When we use undefined name declared by global statement, pyflakes should catch the error but it doesn't. This will fix pyflakes to catch this error. Note test_undefined_global for example of this. --- pyflakes/checker.py | 37 ++++++++++++++++++++++++--- pyflakes/test/test_imports.py | 5 ++++ pyflakes/test/test_undefined_names.py | 8 ++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index c2f8d2e8..5eb2112d 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -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 @@ -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) @@ -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 @@ -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): @@ -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 @@ -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] @@ -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 = \ @@ -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) diff --git a/pyflakes/test/test_imports.py b/pyflakes/test/test_imports.py index 86e1d378..e9bd0155 100644 --- a/pyflakes/test/test_imports.py +++ b/pyflakes/test/test_imports.py @@ -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`') diff --git a/pyflakes/test/test_undefined_names.py b/pyflakes/test/test_undefined_names.py index 88eba933..818004f7 100644 --- a/pyflakes/test/test_undefined_names.py +++ b/pyflakes/test/test_undefined_names.py @@ -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)