Repository: python/mypy

Issue:
New semantic analyzer: Support idem forward aliases
This is a follow-up for #6390

This tricky corner case is used in some real code and is supported by old semantic analyzer
```python
C = C
class C:  # type: ignore
    pass

x: C
```
But with new analyzer this results in `Invalid type` since the first definition always wins. Probably we just need to special-case this somehow.


Discussion:
**EDIT:** The new behavior is actually correct, see below.

~~Another pattern that we need to support with new semantic analyzer is this:~~
```python
class In:
    ...
class Out:
    In = In  # recognized as variable
```
~~this currently (both before and after assignment refactor) causes a bunch of troubles in internal code bases. Raising priority to high.~~
Oh, wait, this is actually correct, by the rules non-indexed assignments at class scope are always variables, so this was previously a false negative, OK back to normal priority.
Another repro for this issue with a weird error:
```
x: C
class C:
    pass
C = C
```
fails with
```
main:2: error: Name 'C' already defined on line 4
```
cc @JukkaL 
The examples look meaningless, since the code won't work at runtime. I wonder if the actual code where this comes up is different (involving try/except perhaps). This could also affect the way we'd fix this.
Yes, the actual code is like
```python
try:
    C = C
except NameError:
    class C:
        ...
```
Also the second [example](https://github.com/python/mypy/issues/6404#issuecomment-500008713) works without try/except.

I think we should fix this before the release since this is actually used in some projects.
Yes, it would be good to make this better. I'm increasing priority.

But even that example seems incomplete, as that will always generate a `NameError`. I assume it's more like this:

```py
try:
    ModuleNotFoundError = ModuleNotFoundError
except NameError:
    class ModuleNotFoundError(Exception):
        ...
```

The assignment will fail on some Python versions and pass on some.
Most likely, I don't remember TBH, also `aiohttp` (who first reported this) used to have tons of `*` imports. This may be some other name they define in another module depending on `sys.platform` and then star import it.
I did some digging and it looks like this happens sometimes after star imports from C acceleration modules.

Resolving patch:
diff --git a/mypy/newsemanal/semanal.py b/mypy/newsemanal/semanal.py
--- a/mypy/newsemanal/semanal.py
+++ b/mypy/newsemanal/semanal.py
@@ -1824,6 +1824,11 @@ def visit_import_all(self, i: ImportAll) -> None:
 
     def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
         self.statement = s
+
+        # Special case assignment like X = X.
+        if self.analyze_identity_global_assignment(s):
+            return
+
         tag = self.track_incomplete_refs()
         s.rvalue.accept(self)
         if self.found_incomplete_ref(tag) or self.should_wait_rhs(s.rvalue):
@@ -1868,6 +1873,47 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
             self.process_module_assignment(s.lvalues, s.rvalue, s)
         self.process__all__(s)
 
+    def analyze_identity_global_assignment(self, s: AssignmentStmt) -> bool:
+        """Special case 'X = X' in global scope.
+
+        This allows supporting some important use cases.
+
+        Return true if special casing was applied.
+        """
+        if not isinstance(s.rvalue, NameExpr) or len(s.lvalues) != 1:
+            # Not of form 'X = X'
+            return False
+        lvalue = s.lvalues[0]
+        if not isinstance(lvalue, NameExpr) or s.rvalue.name != lvalue.name:
+            # Not of form 'X = X'
+            return False
+        if self.type is not None or self.is_func_scope():
+            # Not in global scope
+            return False
+        # It's an assignment like 'X = X' in the global scope.
+        name = lvalue.name
+        sym = self.lookup(name, s)
+        if sym is None:
+            if self.final_iteration:
+                # Fall back to normal assignment analysis.
+                return False
+            else:
+                self.defer()
+                return True
+        else:
+            if sym.node is None:
+                # Something special -- fall back to normal assignment analysis.
+                return False
+            if name not in self.globals:
+                # The name is from builtins. Add an alias to the current module.
+                self.add_symbol(name, sym.node, s)
+            if not isinstance(sym.node, PlaceholderNode):
+                for node in s.rvalue, lvalue:
+                    node.node = sym.node
+                    node.kind = GDEF
+                    node.fullname = sym.node.fullname()
+            return True
+
     def should_wait_rhs(self, rv: Expression) -> bool:
         """Can we already classify this r.h.s. of an assignment or should we wait?
 


Test patch:
diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test
--- a/test-data/unit/check-newsemanal.test
+++ b/test-data/unit/check-newsemanal.test
@@ -2886,3 +2886,85 @@ def f() -> None:
     x, (y, *z) = t
     reveal_type(z)  # N: Revealed type is 'builtins.list[builtins.str*]'
 [builtins fixtures/list.pyi]
+
+[case testNewAnalyzerIdentityAssignment1]
+from foo import *
+
+try:
+    X = X
+except:
+    class X: # E: Name 'X' already defined (possibly by an import)
+        pass
+
+reveal_type(X()) # N: Revealed type is 'foo.X'
+
+[file foo.py]
+class X: pass
+
+[case testNewAnalyzerIdentityAssignment2]
+try:
+    int = int
+    reveal_type(int()) # N: Revealed type is 'builtins.int'
+except:
+    class int: # E: Name 'int' already defined (possibly by an import)
+        pass
+
+reveal_type(int()) # N: Revealed type is 'builtins.int'
+
+[case testNewAnalyzerIdentityAssignment3]
+forwardref: C
+
+try:
+    int = int
+    reveal_type(int()) # N: Revealed type is 'builtins.int'
+except:
+    class int: # E: Name 'int' already defined (possibly by an import)
+        pass
+
+reveal_type(int()) # N: Revealed type is 'builtins.int'
+
+class C: pass
+
+[case testNewAnalyzerIdentityAssignment4]
+try:
+    C = C
+    C
+except:
+    class C:
+        pass
+
+reveal_type(C()) # N: Revealed type is '__main__.C'
+
+[case testNewAnalyzerIdentityAssignment5]
+forwardref: D
+
+try:
+    C = C
+    C
+except:
+    class C:
+        pass
+
+class D: pass
+
+reveal_type(C()) # N: Revealed type is '__main__.C'
+
+[case testNewAnalyzerIdentityAssignment6]
+x: C
+class C:
+    pass
+C = C
+
+reveal_type(C()) # N: Revealed type is '__main__.C'
+reveal_type(x) # N: Revealed type is '__main__.C'
+
+[case testNewAnalyzerIdentityAssignment7]
+C = C # E: Name 'C' is not defined
+
+reveal_type(C) # N: Revealed type is 'Any' \
+               # E: Name 'C' is not defined
+
+[case testNewAnalyzerIdentityAssignment8]
+from typing import Final
+x: Final = 0
+x = x # E: Cannot assign to final name "x"
diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test
--- a/test-data/unit/check-type-aliases.test
+++ b/test-data/unit/check-type-aliases.test
@@ -589,7 +589,6 @@ def foo(x: Bogus[int]) -> None:
 [builtins fixtures/dict.pyi]
 
 [case testOverrideByIdemAliasCorrectType]
-# flags: --no-new-semantic-analyzer
 C = C
 class C:  # type: ignore
     pass