Repository: python/mypy

Issue:
Spurious "Expression has type Any" error following attribute access
As originally described in #5788, I've run into a problem with `--disallow-any-expr` reporting spurious errors.

Consider the following file `any.py`:

```python3
def access_before_declaration(self) -> None:
    obj = Object('foo')
    obj.value
    x = 1

    reveal_type(x) # Revealed type is 'builtins.int'
    x = x + 1 # Expression has type "Any"

class Object:
    def __init__(self, value: str) -> None:
        self.value = value

def access_after_declaration(self) -> None:
    obj = Object('foo')
    obj.value
    x = 1

    reveal_type(x) # Revealed type is 'builtins.int'
    x = x + 1
```

Running mypy on it produces:

```
$ python3 -m mypy --version
mypy 0.641
$ python3 -m mypy --disallow-any-expr any.py
any.py:6: error: Revealed type is 'builtins.int'
any.py:7: error: Expression has type "Any"
any.py:18: error: Revealed type is 'builtins.int'
```

What we see is that `x` is correctly identified as `int` in `access_after_declaration`, but incorrectly as `Any` in `access_before_declaration`. The error is only triggered in very specific circumstances:

* There is a class defined **after** the inspected line
* An attribute of an instance of that class is accessed **before** the inspected line


Discussion:
I set aside some time and followed the [debugging technique](https://github.com/python/mypy/issues/5788#issuecomment-429680252) suggested by @gvanrossum.

First, while experimenting, I changed the code snippet given above a bit to see if it fails for different kinds of operations (yes, it does):

```python
def access_before_declaration(self) -> None:
    obj = Object('foo')
    obj.value
    x = [1]
    print(x[1]) # Expression has type "Any"

# ...
```

The error is triggered in `ExpressionChecker.accept()` when examining `x[1]`:

```
(Pdb) node.base.name, node.index.value, node.method_type, node.analyzed
('x', 1, Any, None)
```

Most of the logic for determining the type of the node happens in `visit_index_expr_helper()`, where first `left_type` is determined:

```
...
  mypy/checkexpr.py(3011)accept()
-> typ = node.accept(self)
  mypy/nodes.py(1464)accept()
-> return visitor.visit_index_expr(self)
  mypy/checkexpr.py(2228)visit_index_expr()
-> result = self.visit_index_expr_helper(e)
  mypy/checkexpr.py(2235)visit_index_expr_helper()
-> left_type = self.accept(e.base)
  mypy/checkexpr.py(3011)accept()
-> typ = node.accept(self)
  mypy/nodes.py(1350)accept()
-> return visitor.visit_name_expr(self)
  mypy/checkexpr.py(155)visit_name_expr()
-> result = self.analyze_ref_expr(e)
  mypy/checkexpr.py(163)analyze_ref_expr()
-> result = self.analyze_var_ref(node, e)
> mypy/checkexpr.py(218)analyze_var_ref()
-> return AnyType(TypeOfAny.special_form)
```

`Any` is returned here because `var.type` is `None`:

```
(Pdb) var.serialize()
{'.class': 'Var', 'name': 'x', 'fullname': 'x', 'type': None, 'flags': []}
```

A few steps later, `visit_index_expr_helper()` will check the getitem operation:

```
...
  mypy/checkexpr.py(3011)accept()
-> typ = node.accept(self)
  mypy/nodes.py(1464)accept()
-> return visitor.visit_index_expr(self)
  mypy/checkexpr.py(2228)visit_index_expr()
-> result = self.visit_index_expr_helper(e)
  mypy/checkexpr.py(2260)visit_index_expr_helper()
-> result, method_type = self.check_op('__getitem__', left_type, e.index, e)
  mypy/checkexpr.py(2128)check_op()
-> local_errors=self.msg,
  mypy/checkexpr.py(1818)check_op_local_by_name()
-> return self.check_op_local(method, method_type, base_type, arg, context, local_errors)
  mypy/checkexpr.py(1840)check_op_local()
-> callable_name=callable_name, object_type=object_type)
> mypy/checkexpr.py(695)check_call()
-> return (AnyType(TypeOfAny.from_another_any, source_any=callee),
```

With `left_type` / `base_type` / `method_type` = `Any` as input, it will figure the overall result
to be `Any`, resulting in the mentioned error.

To me it **seems** that the deduction of `Any` is correct here and the problem is with the input,
i.e. why is the ref a `Var` with type `None`?

This is my first look at the code base though, so maybe someone more familiar with the inner
workings of mypy has thoughts if this is the right direction to investigate further or if there are
more promising leads :)

Just a small update, this can still be reproduced on the latest mypy:

```
$ python3 -m mypy --version
mypy 0.740
$ python3 -m mypy --disallow-any-expr any.py
any.py:6: error: Expression has type "Any"
any.py:6: note: Revealed type is 'builtins.int'
any.py:7: error: Expression has type "Any"
any.py:18: note: Revealed type is 'builtins.int'
Found 2 errors in 1 file (checked 1 source file)
```

(*Almost* the same result as before, now with an additional `Any` error for `reveal_type(x)` in line 6)

FYI, we use `--disallow-any-expr` for our now completely annotated code base, and this bug triggers dozens of errors. Is there anything we can do to move this issue forward? :slightly_smiling_face: 
I think there _may_ be a very simple fix: don't emit this error if current node is deferred, i.e. add a `not self.chk.current_node_deferred` to the long bunch of `and`s near the end of `mypy.checkexpr.ExpressionChecker.accept()`.

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -3613,7 +3613,7 @@ def accept(self,
                 not always_allow_any and
                 not self.chk.is_stub and
                 self.chk.in_checked_function() and
-                has_any_type(typ)):
+                has_any_type(typ) and not self.chk.current_node_deferred):
             self.msg.disallowed_any_type(typ, node)
 
         if not self.chk.in_checked_function() or self.chk.current_node_deferred:


Test patch:
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -6489,3 +6489,26 @@ reveal_type(Foo().y)  # N: Revealed type is 'builtins.list[Any]'
 class Foo:
     def bad():  # E: Method must have at least one argument
         self.x = 0  # E: Name 'self' is not defined
+
+[case testTypeAfterAttributeAccessWithDisallowAnyExpr]
+# flags: --disallow-any-expr
+
+def access_before_declaration(self) -> None:
+    obj = Foo('bar')
+    obj.value
+    x = 1
+
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+    x = x + 1
+
+class Foo:
+    def __init__(self, value: str) -> None:
+        self.value = value
+
+def access_after_declaration(self) -> None:
+    obj = Foo('bar')
+    obj.value
+    x = 1
+
+    reveal_type(x)  # N: Revealed type is 'builtins.int'
+    x = x + 1