Repository: python/mypy

Issue:
Errors due to disallow_any_expr not always reported in incremental mode
I found this in an audit of internal mypy runs with and without incremental mode. It seems that when you change the `disallow_any_expr` flag, then incremental mode doesn't consider the cache dirty, so it won't recheck files that might be affected. The same may be true for other `disallow_any*` flags.

However I'm still surprised, because the code in options.py does seem to add these flags to `OPTIONS_AFFECTING_CACHE`.


Discussion:
Digging a little deeper I am wondering if this may have been caused by the warnings bleeding between files. The specific run contained a diff that added this flag for two specific files and those were the files for which discrepancies were found (incremental not giving as many warnings of this nature as full mode). But *some* warnings of both forms were still reported by incremental mode:
```
Expression type contains "Any" (has type "Type[Attr]")
Expression has type "Any"
```
**UPDATE:**

- The first form is produced if the type *contains* `Any`; the second (simpler) form if it *is* `Any`; so there's really not much of a difference (both are generated by the same call).
- Both files for which I noticed this were changed in the diff, so the mechanism that records the flag settings in the cache is exonerated.
- So it's likely that the issue is somehow related to *other* files that were or were not read from the cache.
- One common factor between the missed errors is in all but one the offending expression was an untyped property (e.g. `User.id` where `id` is a property without annotation).
- The one exception was of the form `isinstance(x, Gid)` where `Gid` is a class whose `__init__` has an argument with type `Any`.
- I wonder if the data structures representing these cases are subtly different when loaded from the cache.
Here's a repro.

- In file `gid.py`:
```py
from typing import Any

class Gid:
    def __init__(self, arg: Any) -> None: pass
```

- In file `main.py`:
```py
from gid import Gid

def f(a: object) -> None:
    isinstance(a, Gid)
```

- Do a full mypy run:
```
$ mypy --show-column-numbers --disallow-any-expr main.py 
main.py:4:18: error: Expression type contains "Any" (has type "Type[Gid]")
$
```

- But an incremental mypy run is clean!
```
$ mypy -i --show-column-numbers --disallow-any-expr main.py
$
```

(I trust I could create a repro for an untyped property too, but I'll move on to other tasks for now.)
Here's the repro for the untyped property -- it's pretty similar.

- File `ns.py`:
```py
class Namespace:
    @property
    def user(self):
        pass
```

- File `main.py`:
```py
import ns
user = ns.Namespace.user
```

- Run `mypy --show-column-numbers --disallow-any-expr main.py`:
```
main.py:2:7: error: Expression type contains "Any" (has type "Callable[[Any], Any]")
```

- Run it with `-i`, `mypy -i --show-column-numbers --disallow-any-expr main.py` gives no errors.
Actually it hasn't got anything to do with untyped properties per se -- I can repro a similar result as in the previous comment with the following in `ns.py`:
```py
class Namespace:
    def __init__(self):
        self.user = f()
def f(): pass
```
This presents as
```
main.py:2:7: error: Expression has type "Any"
```
which is not shown with `-i`.
Hm... With `-i` the `Any` object has its `type_of_any` attribute set to `special_form`. Without `-i` it's `unannotated` (as expected). Apparently deserializing `Any` *always* sets it to `special_form`. Fix coming up.

Resolving patch:
diff --git a/mypy/types.py b/mypy/types.py
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -327,12 +327,15 @@ def __eq__(self, other: object) -> bool:
         return isinstance(other, AnyType)
 
     def serialize(self) -> JsonDict:
-        return {'.class': 'AnyType'}
+        return {'.class': 'AnyType', 'type_of_any': self.type_of_any.name,
+                'source_any': self.source_any.serialize() if self.source_any is not None else None}
 
     @classmethod
     def deserialize(cls, data: JsonDict) -> 'AnyType':
         assert data['.class'] == 'AnyType'
-        return AnyType(TypeOfAny.special_form)
+        source = data['source_any']
+        return AnyType(TypeOfAny[data['type_of_any']],
+                       AnyType.deserialize(source) if source is not None else None)
 
 
 class UninhabitedType(Type):


Test patch:
diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test
--- a/test-data/unit/check-incremental.test
+++ b/test-data/unit/check-incremental.test
@@ -3447,3 +3447,22 @@ import m  # No error here
 tmp/m/a.py:1: error: Unsupported operand types for + ("int" and "str")
 [out2]
 tmp/m/a.py:1: error: Unsupported operand types for + ("int" and "str")
+
+[case testDisallowAnyExprIncremental]
+# cmd: mypy -m main
+# flags:  --disallow-any-expr
+
+[file ns.py]
+class Namespace:
+    def __init__(self):
+        self.user = 0
+
+[file main.py]
+import ns
+user = ns.Namespace.user
+
+[out1]
+tmp/main.py:2: error: Expression has type "Any"
+
+[out2]
+tmp/main.py:2: error: Expression has type "Any"