Repository: serge-sans-paille/beniget

Issue:
Bug: a name that is being bound cannot use itself, even if it’s declared in enclosing scopes.
This name error is uncaught by `beniget`.

```python3
def outer():
    x = 1
    def middle():
        x = 1
        class mytype(str):
            x = x+1 # <- this triggers NameError: name 'x' is not defined
    return middle
outer()()
```

Discussion:

Patch:
diff --git a/beniget/beniget.py b/beniget/beniget.py
index 00d484a..9754e19 100644
--- a/beniget/beniget.py
+++ b/beniget/beniget.py
@@ -169,13 +169,51 @@ Builtins["__file__"] = __file__
 DeclarationStep, DefinitionStep = object(), object()
 
 
-class CollectGlobals(ast.NodeVisitor):
+class CollectLocals(ast.NodeVisitor):
     def __init__(self):
-        self.Globals = defaultdict(list)
+        self.Locals = set()
+        self.NonLocals = set()
 
-    def visit_Global(self, node):
-        for name in node.names:
-            self.Globals[name].append((node, name))
+    def visit_FunctionDef(self, node):
+        self.Locals.add(node.name)
+        # no recursion
+
+    visit_AsyncFunctionDef = visit_FunctionDef
+
+    visit_ClassDef = visit_FunctionDef
+
+    def visit_Nonlocal(self, node):
+        self.NonLocals.update(name for name in node.names)
+
+    visit_Global = visit_Nonlocal
+
+    def visit_Name(self, node):
+        if isinstance(node.ctx, ast.Store) and node.id not in self.NonLocals:
+            self.Locals.add(node.id)
+
+    def skip(self, node):
+        pass
+
+    if sys.version_info.major >= 3:
+        visit_SetComp = visit_DictComp = visit_ListComp = skip
+        visit_GeneratorExp = skip
+
+    visit_Lambda = skip
+
+    def visit_Import(self, node):
+        for alias in node.names:
+            base = alias.name.split(".", 1)[0]
+            self.Locals.add(alias.asname or base)
+
+    def visit_ImportFrom(self, node):
+        for alias in node.names:
+            self.Locals.add(alias.asname or alias.name)
+
+
+def collect_locals(node):
+    visitor = CollectLocals()
+    visitor.generic_visit(node)
+    return visitor.Locals
 
 
 class DefUseChains(ast.NodeVisitor):
@@ -204,27 +242,34 @@ class DefUseChains(ast.NodeVisitor):
         """
         self.chains = {}
         self.locals = defaultdict(list)
+
         self.filename = filename
 
         # deep copy of builtins, to remain reentrant
         self._builtins = {k: Def(v) for k, v in Builtins.items()}
 
         # function body are not executed when the function definition is met
-        # this holds a stack of the functions met during body processing
+        # this holds a list of the functions met during body processing
         self._defered = []
 
         # stack of mapping between an id and Names
         self._definitions = []
 
+        # stack of scope depth
+        self._scope_depths = []
+
         # stack of variable defined with the global keywords
-        self._promoted_locals = []
+        self._globals = []
+
+        # stack of local identifiers, used to detect 'read before assign'
+        self._precomputed_locals = []
 
         # stack of variable that were undefined when we met them, but that may
         # be defined in another path of the control flow (esp. in loop)
         self._undefs = []
 
-        # stack of current node holding definitions: class, module, function...
-        self._currenthead = []
+        # stack of nodes starting a scope: class, module, function...
+        self._scopes = []
 
         self._breaks = []
         self._continues = []
@@ -248,28 +293,60 @@ class DefUseChains(ast.NodeVisitor):
             chains.append(str(d))
         return chains
 
-    def unbound_identifier(self, name, node):
+    def location(self, node):
         if hasattr(node, "lineno"):
             filename = "{}:".format(
                 "<unknown>" if self.filename is None else self.filename
             )
-            location = " at {}{}:{}".format(filename,
+            return " at {}{}:{}".format(filename,
                                             node.lineno,
                                             node.col_offset)
         else:
-            location = ""
+            return ""
+
+    def unbound_identifier(self, name, node):
+        location = self.location(node)
         print("W: unbound identifier '{}'{}".format(name, location))
 
-    def lookup_identifier(self, name):
-        for d in reversed(self._definitions):
-            if name in d:
-                return d[name]
-        return []
+    def maybe_unbound_identifier(self, name, node):
+        location = self.location(node)
+        print("W: identifier '{}' may be unbound at runtime{}".format(name, location))
+
 
     def defs(self, node):
+        '''
+        Performs an actual lookup of node's id in current context, returning
+        the list of def linked to that use.
+        '''
         name = node.id
         stars = []
-        for d in reversed(self._definitions):
+
+        # If the `global` keyword has been used, honor it
+        if any(name in _globals for _globals in self._globals):
+            looked_up_definitions = self._definitions[0:1]
+        else:
+            # List of definitions to check. This includes all non-class
+            # definitions *and* the last definition. Class definitions are not
+            # included because they require fully qualified access.
+            looked_up_definitions = []
+
+            scopes_iter = iter(reversed(self._scopes))
+            depths_iter = iter(reversed(self._scope_depths))
+
+            # Keep the last scope because we could be in class scope, in which
+            # case we don't need fully qualified access.
+            lvl = depth = next(depths_iter)
+            looked_up_definitions.extend(reversed(self._definitions[depth:]))
+            _ = next(scopes_iter)
+
+            # Iterate over scopes, filtering out class scopes.
+            for scope, depth in zip(scopes_iter, depths_iter):
+                if not isinstance(scope, ast.ClassDef):
+                    looked_up_definitions.extend(
+                            reversed(self._definitions[lvl + depth: lvl]))
+                lvl += depth
+
+        for d in looked_up_definitions:
             if name in d:
                 return d[name] if not stars else stars + list(d[name])
             if "*" in d:
@@ -312,57 +389,67 @@ class DefUseChains(ast.NodeVisitor):
         self._undefs.pop()
 
     @contextmanager
-    def DefinitionContext(self, node):
-        self._currenthead.append(node)
+    def ScopeContext(self, node):
+        self._scopes.append(node)
         self._definitions.append(defaultdict(ordered_set))
-        self._promoted_locals.append(set())
+        self._scope_depths.append(-1)
+        self._globals.append(set())
+        self._precomputed_locals.append(collect_locals(node))
         yield
-        self._promoted_locals.pop()
+        self._precomputed_locals.pop()
+        self._globals.pop()
+        self._scope_depths.pop()
         self._definitions.pop()
-        self._currenthead.pop()
+        self._scopes.pop()
 
     @contextmanager
-    def CompDefinitionContext(self, node):
+    def CompScopeContext(self, node):
         if sys.version_info.major >= 3:
-            self._currenthead.append(node)
+            self._scopes.append(node)
             self._definitions.append(defaultdict(ordered_set))
-            self._promoted_locals.append(set())
+            self._globals.append(set())
         yield
         if sys.version_info.major >= 3:
-            self._promoted_locals.pop()
+            self._globals.pop()
             self._definitions.pop()
-            self._currenthead.pop()
+            self._scopes.pop()
+
+    @contextmanager
+    def DefinitionContext(self, definitions):
+        self._definitions.append(definitions)
+        self._scope_depths[-1] -= 1
+        yield self._definitions[-1]
+        self._scope_depths[-1] += 1
+        self._definitions.pop()
+
+    @contextmanager
+    def SwitchScopeContext(self, defs, scopes, scope_depths):
+        scope_depths, self._scope_depths = self._scope_depths, scope_depths
+        scopes, self._scopes = self._scopes, scopes
+        defs, self._definitions = self._definitions, defs
+        yield
+        self._definitions = defs
+        self._scopes = scopes
+        self._scope_depths = scope_depths
+
 
     # stmt
     def visit_Module(self, node):
         self.module = node
-        with self.DefinitionContext(node):
+        with self.ScopeContext(node):
 
             self._definitions[-1].update(
                 {k: ordered_set((v,)) for k, v in self._builtins.items()}
             )
 
-            self._defered.append([])
             self.process_body(node.body)
 
-            # handle `global' keyword specifically
-            cg = CollectGlobals()
-            cg.visit(node)
-            for nodes in cg.Globals.values():
-                for n, name in nodes:
-                    if name not in self._definitions[-1]:
-                        dnode = Def((n, name))
-                        self.set_definition(name, dnode)
-                        self.locals[node].append(dnode)
-
             # handle function bodies
-            for fnode, ctx in self._defered[-1]:
+            for fnode, defs, scopes, scope_depths in self._defered:
                 visitor = getattr(self,
                                   "visit_{}".format(type(fnode).__name__))
-                defs, self._definitions = self._definitions, ctx
-                visitor(fnode, step=DefinitionStep)
-                self._definitions = defs
-            self._defered.pop()
+                with self.SwitchScopeContext(defs, scopes, scope_depths):
+                    visitor(fnode, step=DefinitionStep)
 
             # various sanity checks
             if __debug__:
@@ -380,7 +467,8 @@ class DefUseChains(ast.NodeVisitor):
                 assert nb_defs == nb_heads + nb_bltns - nb_overloaded_bltns
 
         assert not self._definitions
-        assert not self._defered
+        assert not self._scopes
+        assert not self._scope_depths
 
     def set_definition(self, name, dnode_or_dnodes):
         if self.deadcode:
@@ -403,11 +491,44 @@ class DefUseChains(ast.NodeVisitor):
         DefUseChains.add_to_definition(self._definitions[-1], name,
                                        dnode_or_dnodes)
 
+    def extend_global(self, name, dnode_or_dnodes):
+        if self.deadcode:
+            return
+        DefUseChains.add_to_definition(self._definitions[0], name,
+                                       dnode_or_dnodes)
+
+    def set_or_extend_global(self, name, dnode):
+        if self.deadcode:
+            return
+        if name not in self._definitions[0]:
+            self.locals[self.module].append(dnode)
+        DefUseChains.add_to_definition(self._definitions[0], name, dnode)
+
+    def visit_annotation(self, node):
+        annotation = getattr(node, 'annotation', None)
+        if annotation:
+            self.visit(annotation)
+
+    def visit_skip_annotation(self, node):
+        if isinstance(node, ast.Name):
+            self.visit_Name(node, skip_annotation=True)
+        else:
+            self.visit(node)
+
     def visit_FunctionDef(self, node, step=DeclarationStep):
         if step is DeclarationStep:
             dnode = self.chains.setdefault(node, Def(node))
-            self.set_definition(node.name, dnode)
-            self.locals[self._currenthead[-1]].append(dnode)
+            self.locals[self._scopes[-1]].append(dnode)
+            for arg in node.args.args:
+                self.visit_annotation(arg)
+            for arg in node.args.posonlyargs:
+                self.visit_annotation(arg)
+            if node.args.vararg:
+                self.visit_annotation(node.args.vararg)
+            for arg in node.args.kwonlyargs:
+                self.visit_annotation(arg)
+            if node.args.kwarg:
+                self.visit_annotation(node.args.kwarg)
 
             for kw_default in filter(None, node.args.kw_defaults):
                 self.visit(kw_default).add_user(dnode)
@@ -416,26 +537,37 @@ class DefUseChains(ast.NodeVisitor):
             for decorator in node.decorator_list:
                 self.visit(decorator)
 
-            definitions = list(self._definitions)
-            if isinstance(self._currenthead[-1], ast.ClassDef):
-                definitions.pop()
-            self._defered[-1].append((node, definitions))
-        elif step is DefinitionStep:
-            # function is not considered as defined when evaluating returns
             if node.returns:
                 self.visit(node.returns)
-            with self.DefinitionContext(node):
-                self.visit(node.args)
+
+            self.set_definition(node.name, dnode)
+
+            self._defered.append((node,
+                                  list(self._definitions),
+                                  list(self._scopes),
+                                  list(self._scope_depths)))
+        elif step is DefinitionStep:
+            with self.ScopeContext(node):
+                for arg in node.args.args:
+                    self.visit_skip_annotation(arg)
+                for arg in node.args.posonlyargs:
+                    self.visit_skip_annotation(arg)
+                if node.args.vararg:
+                    self.visit_skip_annotation(node.args.vararg)
+                for arg in node.args.kwonlyargs:
+                    self.visit_skip_annotation(arg)
+                if node.args.kwarg:
+                    self.visit_skip_annotation(node.args.kwarg)
                 self.process_body(node.body)
         else:
             raise NotImplementedError()
 
     visit_AsyncFunctionDef = visit_FunctionDef
 
-    def visit_ClassDef(self, node):
+    def visit_ClassDef(self, node, step=DeclarationStep):
         dnode = self.chains.setdefault(node, Def(node))
-        self.locals[self._currenthead[-1]].append(dnode)
-        self.set_definition(node.name, dnode)
+        self.locals[self._scopes[-1]].append(dnode)
+
         for base in node.bases:
             self.visit(base).add_user(dnode)
         for keyword in node.keywords:
@@ -443,10 +575,13 @@ class DefUseChains(ast.NodeVisitor):
         for decorator in node.decorator_list:
             self.visit(decorator).add_user(dnode)
 
-        with self.DefinitionContext(node):
+        with self.ScopeContext(node):
             self.set_definition("__class__", Def("__class__"))
             self.process_body(node.body)
 
+        self.set_definition(node.name, dnode)
+
+
     def visit_Return(self, node):
         if node.value:
             self.visit(node.value)
@@ -487,15 +622,15 @@ class DefUseChains(ast.NodeVisitor):
             dtarget = self.visit(node.target)
             dvalue.add_user(dtarget)
             node.target.ctx = ctx
-            if node.target.id in self._promoted_locals[-1]:
-                self.extend_definition(node.target.id, dtarget)
+            if any(node.target.id in _globals for _globals in self._globals):
+                self.extend_global(node.target.id, dtarget)
             else:
                 loaded_from = [d.name() for d in self.defs(node.target)]
                 self.set_definition(node.target.id, dtarget)
                 # If we augassign from a value that comes from '*', let's use
                 # this node as the definition point.
                 if '*' in loaded_from:
-                    self.locals[self._currenthead[-1]].append(dtarget)
+                    self.locals[self._scopes[-1]].append(dtarget)
         else:
             self.visit(node.target).add_user(dvalue)
 
@@ -512,29 +647,27 @@ class DefUseChains(ast.NodeVisitor):
         self._continues.append(defaultdict(ordered_set))
 
         self._undefs.append(defaultdict(list))
-        self._definitions.append(self._definitions[-1].copy())
-        self.visit(node.target)
-        self.process_body(node.body)
-        self.process_undefs()
+        with self.DefinitionContext(self._definitions[-1].copy()) as body_defs:
+            self.visit(node.target)
+            self.process_body(node.body)
+            self.process_undefs()
 
-        continue_defs = self._continues.pop()
-        for d, u in continue_defs.items():
-            self.extend_definition(d, u)
-        self._continues.append(defaultdict(ordered_set))
+            continue_defs = self._continues.pop()
+            for d, u in continue_defs.items():
+                self.extend_definition(d, u)
+            self._continues.append(defaultdict(ordered_set))
 
-        # extra round to ``emulate'' looping
-        self.visit(node.target)
-        self.process_body(node.body)
+            # extra round to ``emulate'' looping
+            self.visit(node.target)
+            self.process_body(node.body)
 
-        # process else clause in case of late break
-        self._definitions.append(defaultdict(ordered_set))
-        self.process_body(node.orelse)
-        orelse_defs = self._definitions.pop()
+            # process else clause in case of late break
+            with self.DefinitionContext(defaultdict(ordered_set)) as orelse_defs:
+                self.process_body(node.orelse)
 
-        break_defs = self._breaks.pop()
-        continue_defs = self._continues.pop()
+            break_defs = self._breaks.pop()
+            continue_defs = self._continues.pop()
 
-        body_defs = self._definitions.pop()
 
         for d, u in orelse_defs.items():
             self.extend_definition(d, u)
@@ -552,39 +685,35 @@ class DefUseChains(ast.NodeVisitor):
 
     def visit_While(self, node):
 
-        self._definitions.append(self._definitions[-1].copy())
-        self._undefs.append(defaultdict(list))
-        self._breaks.append(defaultdict(ordered_set))
-        self._continues.append(defaultdict(ordered_set))
-
-        self.process_body(node.orelse)
+        with self.DefinitionContext(self._definitions[-1].copy()):
+            self._undefs.append(defaultdict(list))
+            self._breaks.append(defaultdict(ordered_set))
+            self._continues.append(defaultdict(ordered_set))
 
-        self._definitions.pop()
+            self.process_body(node.orelse)
 
-        self._definitions.append(self._definitions[-1].copy())
+        with self.DefinitionContext(self._definitions[-1].copy()) as body_defs:
 
-        self.visit(node.test)
-        self.process_body(node.body)
+            self.visit(node.test)
+            self.process_body(node.body)
 
-        self.process_undefs()
+            self.process_undefs()
 
-        continue_defs = self._continues.pop()
-        for d, u in continue_defs.items():
-            self.extend_definition(d, u)
-        self._continues.append(defaultdict(ordered_set))
+            continue_defs = self._continues.pop()
+            for d, u in continue_defs.items():
+                self.extend_definition(d, u)
+            self._continues.append(defaultdict(ordered_set))
 
-        # extra round to simulate loop
-        self.visit(node.test)
-        self.process_body(node.body)
+            # extra round to simulate loop
+            self.visit(node.test)
+            self.process_body(node.body)
 
-        # the false branch of the eval
-        self.visit(node.test)
+            # the false branch of the eval
+            self.visit(node.test)
 
-        self._definitions.append(self._definitions[-1].copy())
-        self.process_body(node.orelse)
+            with self.DefinitionContext(self._definitions[-1].copy()) as orelse_defs:
+                self.process_body(node.orelse)
 
-        orelse_defs = self._definitions.pop()
-        body_defs = self._definitions.pop()
         break_defs = self._breaks.pop()
         continue_defs = self._continues.pop()
 
@@ -604,13 +733,12 @@ class DefUseChains(ast.NodeVisitor):
         self.visit(node.test)
 
         # putting a copy of current level to handle nested conditions
-        self._definitions.append(self._definitions[-1].copy())
-        self.process_body(node.body)
-        body_defs = self._definitions.pop()
+        with self.DefinitionContext(self._definitions[-1].copy()) as body_defs:
+            self.process_body(node.body)
+
+        with self.DefinitionContext(self._definitions[-1].copy()) as orelse_defs:
+            self.process_body(node.orelse)
 
-        self._definitions.append(self._definitions[-1].copy())
-        self.process_body(node.orelse)
-        orelse_defs = self._definitions.pop()
         for d in body_defs:
             if d in orelse_defs:
                 self.set_definition(d, body_defs[d] + orelse_defs[d])
@@ -637,19 +765,18 @@ class DefUseChains(ast.NodeVisitor):
             self.visit(node.cause)
 
     def visit_Try(self, node):
-        self._definitions.append(self._definitions[-1].copy())
-        self.process_body(node.body)
-        self.process_body(node.orelse)
-        failsafe_defs = self._definitions.pop()
+        with self.DefinitionContext(self._definitions[-1].copy()) as failsafe_defs:
+            self.process_body(node.body)
+            self.process_body(node.orelse)
 
         # handle the fact that definitions may have fail
         for d in failsafe_defs:
             self.extend_definition(d, failsafe_defs[d])
 
         for excepthandler in node.handlers:
-            self._definitions.append(defaultdict(ordered_set))
-            self.visit(excepthandler)
-            handler_def = self._definitions.pop()
+            with self.DefinitionContext(defaultdict(ordered_set)) as handler_def:
+                self.visit(excepthandler)
+
             for hd in handler_def:
                 self.extend_definition(hd, handler_def[hd])
 
@@ -665,13 +792,13 @@ class DefUseChains(ast.NodeVisitor):
             dalias = self.chains.setdefault(alias, Def(alias))
             base = alias.name.split(".", 1)[0]
             self.set_definition(alias.asname or base, dalias)
-            self.locals[self._currenthead[-1]].append(dalias)
+            self.locals[self._scopes[-1]].append(dalias)
 
     def visit_ImportFrom(self, node):
         for alias in node.names:
             dalias = self.chains.setdefault(alias, Def(alias))
             self.set_definition(alias.asname or alias.name, dalias)
-            self.locals[self._currenthead[-1]].append(dalias)
+            self.locals[self._scopes[-1]].append(dalias)
 
     def visit_Exec(self, node):
         dnode = self.chains.setdefault(node, Def(node))
@@ -701,7 +828,7 @@ class DefUseChains(ast.NodeVisitor):
 
     def visit_Global(self, node):
         for name in node.names:
-            self._promoted_locals[-1].add(name)
+            self._globals[-1].add(name)
 
     def visit_Nonlocal(self, node):
         for name in node.names:
@@ -739,11 +866,10 @@ class DefUseChains(ast.NodeVisitor):
     def visit_Lambda(self, node, step=DeclarationStep):
         if step is DeclarationStep:
             dnode = self.chains.setdefault(node, Def(node))
-            self._defered[-1].append((node, list(self._definitions)))
             return dnode
         elif step is DefinitionStep:
             dnode = self.chains[node]
-            with self.DefinitionContext(node):
+            with self.ScopeContext(node):
                 self.visit(node.args)
                 self.visit(node.body).add_user(dnode)
             return dnode
@@ -774,7 +900,7 @@ class DefUseChains(ast.NodeVisitor):
     def visit_ListComp(self, node):
         dnode = self.chains.setdefault(node, Def(node))
 
-        with self.CompDefinitionContext(node):
+        with self.CompScopeContext(node):
             for comprehension in node.generators:
                 self.visit(comprehension).add_user(dnode)
             self.visit(node.elt).add_user(dnode)
@@ -786,7 +912,7 @@ class DefUseChains(ast.NodeVisitor):
     def visit_DictComp(self, node):
         dnode = self.chains.setdefault(node, Def(node))
 
-        with self.CompDefinitionContext(node):
+        with self.CompScopeContext(node):
             for comprehension in node.generators:
                 self.visit(comprehension).add_user(dnode)
             self.visit(node.key).add_user(dnode)
@@ -860,32 +986,43 @@ class DefUseChains(ast.NodeVisitor):
         self.visit(node.target)
         return dnode
 
-    def visit_Name(self, node):
+    def is_in_current_scope(self, name):
+        return any(name in defs
+                   for defs in self._definitions[self._scope_depths[-1]:])
 
+    def visit_Name(self, node, skip_annotation=False):
         if isinstance(node.ctx, (ast.Param, ast.Store)):
             dnode = self.chains.setdefault(node, Def(node))
-            if node.id in self._promoted_locals[-1]:
-                self.extend_definition(node.id, dnode)
-                if dnode not in self.locals[self.module]:
-                    self.locals[self.module].append(dnode)
+            if any(node.id in _globals for _globals in self._globals):
+                self.set_or_extend_global(node.id, dnode)
             else:
                 self.set_definition(node.id, dnode)
-                if dnode not in self.locals[self._currenthead[-1]]:
-                    self.locals[self._currenthead[-1]].append(dnode)
+                if dnode not in self.locals[self._scopes[-1]]:
+                    self.locals[self._scopes[-1]].append(dnode)
 
-            if node.annotation is not None:
+            if node.annotation is not None and not skip_annotation:
                 self.visit(node.annotation)
 
         elif isinstance(node.ctx, (ast.Load, ast.Del)):
-            node_in_chains = node in self.chains
-            if node_in_chains:
-                dnode = self.chains[node]
-            else:
+            # We hit the situation where we refer to a local variable that's not
+            # bound yet. This is a runtime error in Python, so we issue a warning.
+            # Note that because we may be in a condition, it's just a *may* and it
+            # *may* be fine to ignore this warning.
+            current_locals = self._precomputed_locals[-1]
+            if node.id in current_locals and not self.is_in_current_scope(node.id):
+                self.maybe_unbound_identifier(node.id, node)
                 dnode = Def(node)
-            for d in self.defs(node):
-                d.add_user(dnode)
-            if not node_in_chains:
                 self.chains[node] = dnode
+            else:
+                node_in_chains = node in self.chains
+                if node_in_chains:
+                    dnode = self.chains[node]
+                else:
+                    dnode = Def(node)
+                for d in self.defs(node):
+                    d.add_user(dnode)
+                if not node_in_chains:
+                    self.chains[node] = dnode
             # currently ignore the effect of a del
         else:
             raise NotImplementedError()


Test patch:
diff --git a/tests/chains.py b/tests/chains.py
index 184993f..6480c85 100644
--- a/tests/chains.py
+++ b/tests/chains.py
@@ -344,6 +344,56 @@ while done:
         code = "from some import decorator\n@decorator\nclass C:pass"
         self.checkChains(code, ["decorator -> (decorator -> (C -> ()))", "C -> ()"])
 
+    @skipIf(sys.version_info.major < 3, "Python 3 syntax")
+    def test_def_used_in_self_default(self):
+        code = "def foo(x:foo): return foo"
+        self.checkChains(code, ["foo -> (foo -> ())"])
+
+    def test_unbound_class_variable(self):
+        code = '''
+def middle():
+    x = 1
+    class mytype(str):
+        x = x+1 # <- this triggers NameError: name 'x' is not defined
+    return x
+        '''
+        c = beniget.DefUseChains()
+        node = ast.parse(code)
+        c.visit(node)
+        self.assertEqual(c.dump_chains(node.body[0]), ['x -> (x -> ())', 'mytype -> ()'])
+    
+    def test_unbound_class_variable2(self):
+        code = '''class A:\n  a = 10\n  def f(self):\n    return a # a is not defined'''
+        c = beniget.DefUseChains()
+        node = ast.parse(code)
+        c.visit(node)
+        self.assertEqual(c.dump_chains(node.body[0]), ['a -> ()', 'f -> ()'])
+
+    def test_unbound_class_variable3(self):
+        code = '''class A:\n  a = 10\n  class I:\n    b = a + 1 # a is not defined'''
+        c = beniget.DefUseChains()
+        node = ast.parse(code)
+        c.visit(node)
+        self.assertEqual(c.dump_chains(node.body[0]), ['a -> ()', 'I -> ()'])
+    
+    def test_unbound_class_variable4(self):
+        code = '''class A:\n  a = 10\n  f = lambda: a # a is not defined'''
+        c = beniget.DefUseChains()
+        node = ast.parse(code)
+        c.visit(node)
+        self.assertEqual(c.dump_chains(node.body[0]), ['a -> ()', 'f -> ()'])
+
+    def test_unbound_class_variable5(self):
+        code = '''class A:\n  a = 10\n  b = [a for _ in range(10)]  # a is not defined'''
+        c = beniget.DefUseChains()
+        node = ast.parse(code)
+        c.visit(node)
+        if sys.version_info.major >= 3:
+            self.assertEqual(c.dump_chains(node.body[0]), ['a -> ()', 'b -> ()'])
+        else:
+            self.assertEqual(c.dump_chains(node.body[0]),
+                             ['a -> (a -> (ListComp -> ()))', '_ -> ()', 'b -> ()'])
+
     @skipIf(sys.version_info.major < 3, "Python 3 syntax")
     def test_functiondef_returns(self):
         code = "x = 1\ndef foo() -> x: pass"
@@ -354,21 +404,68 @@ while done:
         code = "type_ = int\ndef foo(bar: type_): pass"
         self.checkChains(code, ["type_ -> (type_ -> ())", "foo -> ()"])
 
-    def check_unbound_identifier_message(self, code, expected_messages, filename=None):
+
+    @skipIf(sys.version_info.major < 3, "Python 3 syntax")
+    def test_annotation_inner_class(self):
+
+        code = '''
+def outer():
+    def middle():
+        class mytype(str):
+            def count(self) -> mytype: # this should trigger unbound identifier
+                def c(x) -> mytype(): # this one shouldn't
+                    ...
+        '''
+        c = beniget.DefUseChains()
+        node = ast.parse(code)
+        c.visit(node)
+        self.assertEqual(c.dump_chains(node.body[0].body[0]), ['mytype -> (mytype -> (Call -> ()))'])
+
+    def check_message(self, code, expected_messages, filename=None):
         node = ast.parse(code)
         c = beniget.DefUseChains(filename)
         with captured_output() as (out, err):
             c.visit(node)
-        produced_messages = out.getvalue().strip().split("\n")
 
-        self.assertEqual(len(expected_messages), len(produced_messages))
+        if not out.getvalue():
+            produced_messages = []
+        else:
+            produced_messages = out.getvalue().strip().split("\n")
+
+        self.assertEqual(len(expected_messages), len(produced_messages),
+                         produced_messages)
         for expected, produced in zip(expected_messages, produced_messages):
-            self.assertIn(expected, produced, "actual message contains expected message")
+            self.assertIn(expected, produced,
+                          "actual message does not contains expected message")
 
     def test_unbound_identifier_message_format(self):
         code = "foo(1)\nbar(2)"
-        self.check_unbound_identifier_message(code, ["<unknown>:1", "<unknown>:2"])
-        self.check_unbound_identifier_message(code, ["foo.py:1", "foo.py:2"], filename="foo.py")
+        self.check_message(code, ["<unknown>:1", "<unknown>:2"])
+        self.check_message(code, ["foo.py:1", "foo.py:2"], filename="foo.py")
+
+    def test_unbound_class_variable_reference_message_format(self):
+        code = "class A:\n a = 10\n def f(self): return a # a is undef"
+        self.check_message(code, ["unbound identifier 'a' at <unknown>:3"])
+
+    def test_maybe_unbound_identifier_message_format(self):
+        code = "x = 1\ndef foo(): y = x; x = 2"
+        self.check_message(code,
+                           ["'x' may be unbound at runtime at <unknown>:2"])
+
+    def test_unbound_local_identifier_in_func(self):
+        code = "def A():\n x = 1\n class B: x = x"
+        self.check_message(code,
+                           ["'x' may be unbound at runtime at <unknown>:3"])
+
+    @skipIf(sys.version_info < (3, 0), 'Python 3 syntax')
+    def test_unbound_local_identifier_nonlocal(self):
+        code = "def A():\n x = 1\n class B: nonlocal x; x = x"
+        self.check_message(code, [])
+
+    def test_unbound_local_identifier_in_augassign(self):
+        code = "def A():\n x = 1\n class B: x += 1"
+        self.check_message(code,
+                           ["'x' may be unbound at runtime at <unknown>:3"])
 
     def test_star_import_with_conditional_redef(self):
         code = '''
diff --git a/tests/definitions.py b/tests/definitions.py
index 4794227..b618568 100644
--- a/tests/definitions.py
+++ b/tests/definitions.py
@@ -158,14 +158,15 @@ class TestGlobals(TestCase):
 
     def testGlobalThroughKeyword(self):
         code = "def foo(): global x"
-        self.checkGlobals(code, ["foo", "x"])
+        # a global keyword does not actually create a global
+        self.checkGlobals(code, ["foo"])
 
-    def testGlobalThroughKeywords(self):
-        code = "def foo(): global x, y"
-        self.checkGlobals(code, ["foo", "x", "y"])
+    def testGlobalThroughKeywordAndAssign(self):
+        code = "def foo(): global x; x = 1"
+        self.checkGlobals(code, ["foo", "x"])
 
     def testGlobalThroughMultipleKeyword(self):
-        code = "def foo(): global x\ndef bar(): global x"
+        code = "def foo(): global x\ndef bar(): global x; x = 1"
         self.checkGlobals(code, ["bar", "foo", "x"])
 
     def testGlobalBeforeKeyword(self):
@@ -174,7 +175,7 @@ class TestGlobals(TestCase):
 
     def testGlobalsBeforeKeyword(self):
         code = "x = 1\ndef foo(): global x, y"
-        self.checkGlobals(code, ["foo", "x", "y"])
+        self.checkGlobals(code, ["foo", "x"])
 
     if sys.version_info.major >= 3:
 
@@ -184,7 +185,11 @@ class TestGlobals(TestCase):
 
         def testGlobalsAfterKeyword(self):
             code = "def foo(): global x, y\ny : 1"
-            self.checkGlobals(code, ["foo", "x", "y"])
+            self.checkGlobals(code, ["foo", "y"])
+
+    def testGlobalKeyworaInClassd(self):
+        code = "class F: global x; x = 1"
+        self.checkGlobals(code, ["F", "x"])
 
     def testGlobalImport(self):
         code = "import foo"
@@ -352,7 +357,7 @@ class TestLocals(TestCase):
             )
             self.checkLocals(code, ["a", "bar"])
 
-    def test_LocalGlobal(self):
+    def test_LocalMadeGlobal(self):
         code = "def foo(): global a; a = 1"
         self.checkLocals(code, [])