Repository: quora/pyanalyze (python)

Issue:
False positive undefined_name with doubly-nested class
```python
def f():
    y = 3

    def inner():

        class Nested:
            classvar = y
```

Produces:

```
Undefined name: y (code: undefined_name)
In nestedcls.py at line 7
   4:     def inner():
   5: 
   6:         class Nested:
   7:             classvar = y
                             ^
```

Pull request description:
Fix incorrect undefined_name in doubly nested classes
Fixes #733


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index bab7659..9875de0 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- Fix incorrect `undefined_name` errors when a class is nested in a nested
+  function and uses a name from the outer function (#750)
 - Fix incorrect `possibly_undefined_name` error on certain uses of the
   walrus operator (#749)
 - Fix narrowing on `isinstance` calls with arguments that are not
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index dc8c659..3ad7fc4 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -1822,7 +1822,15 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
     def _visit_class_and_get_value(
         self, node: ast.ClassDef, current_class: Optional[type]
     ) -> Value:
-        if self._is_checking():
+        if self._is_collecting():
+            # If this is a nested class, we need to run the collecting phase to get data
+            # about names accessed from the class.
+            if len(self.scopes.scopes) > 2:
+                with self.scopes.add_scope(
+                    ScopeType.class_scope, scope_node=node, scope_object=current_class
+                ), self._set_current_class(current_class):
+                    self._generic_visit_list(node.body)
+        else:
             with self.scopes.add_scope(
                 ScopeType.class_scope, scope_node=None, scope_object=current_class
             ), self._set_current_class(current_class):


Test patch:
diff --git a/pyanalyze/test_name_check_visitor.py b/pyanalyze/test_name_check_visitor.py
index 55810ce..ed68845 100644
--- a/pyanalyze/test_name_check_visitor.py
+++ b/pyanalyze/test_name_check_visitor.py
@@ -396,23 +396,6 @@ class TestNameCheckVisitor(TestNameCheckVisitorBase):
         def run():
             print({key: 1})  # E: unhashable_key
 
-    @assert_passes()
-    def test_nested_classes(self):
-        class Caviids(object):
-            class Capybaras(object):
-                if False:
-                    print(neochoerus)  # E: undefined_name
-
-            def method(self, cap: Capybaras):
-                assert_is_value(cap, TypedValue(Caviids.Capybaras))
-
-    @assert_passes()
-    def test_class_in_function(self):
-        def get_capybaras(object):
-            class Capybaras(object):
-                if False:
-                    print(neochoerus)  # E: undefined_name
-
     @assert_passes()
     def test_cant_del_tuple(self):
         tpl = (1, 2, 3)
diff --git a/pyanalyze/test_stacked_scopes.py b/pyanalyze/test_stacked_scopes.py
index ee1a86c..28c3d67 100644
--- a/pyanalyze/test_stacked_scopes.py
+++ b/pyanalyze/test_stacked_scopes.py
@@ -1893,3 +1893,65 @@ class TestInvalidation(TestNameCheckVisitorBase):
                 assert_is_value(key, TypedValue(int))
             else:
                 assert_is_value(key, KnownValue(None))
+
+
+class TestClassNesting(TestNameCheckVisitorBase):
+
+    @assert_passes()
+    def test_class_in_class(self):
+        class Caviids(object):
+            class Capybaras(object):
+                if False:
+                    print(neochoerus)  # E: undefined_name
+
+            def method(self, cap: Capybaras):
+                assert_is_value(cap, TypedValue(Caviids.Capybaras))
+
+    @assert_passes()
+    def test_class_in_function(self):
+        from typing_extensions import Literal, assert_type
+
+        def get_capybaras(object):
+            x = 3
+
+            class Capybaras(object):
+                if False:
+                    print(neochoerus)  # E: undefined_name
+
+                assert_type(x, Literal[3])
+
+    @assert_passes()
+    def test_double_nesting(self):
+        from typing_extensions import Literal, assert_type
+
+        def outer():
+            outer_var = "outer"
+
+            def inner():
+                inner_var = "inner"
+
+                class Nested:
+                    assert_type(outer_var, Literal["outer"])
+                    assert_type(inner_var, Literal["inner"])
+
+                return Nested
+
+            return inner
+
+    @assert_passes()
+    def test_triple_function_nesting(self):
+        from typing_extensions import Literal, assert_type
+
+        def outer():
+            outer_var = "outer"
+
+            def inner():
+                inner_var = "inner"
+
+                def innermost():
+                    assert_type(outer_var, Literal["outer"])
+                    assert_type(inner_var, Literal["inner"])
+
+                return innermost
+
+            return inner