Repository: python/mypy
Pull request: Fix dataclass plugin interaction with member expressions
Fixes https://github.com/python/mypy/issues/6516

It also fixes minor corner case that affects tests cases proposed in the issue.

Problem statement:
Incorrect "Attributes without a default cannot follow attributes with one" in dataclasses
Mypy incorrectly (in my understanding) complains about members in dataclasses having default values, even though many `dataclasses.field` assignments do not actually assign a default value.

I feel the correct behaviour should be to only detect an error if a field assignment that leads to a default value being assigned in the init method is followed by a default value.

This results (incorrectly) in an error:
```
import dataclasses

@dataclasses.dataclass
class A:
    x: int = dataclasses.field(metadata={"doc": "foo"})
    y: str
```

So does this:
```
import dataclasses

@dataclasses.dataclass
class A:
    x: int = dataclasses.field(init=False, default=1)
    y: str
```

Whereas this does not emit an error (where it should):
```
import dataclasses

@dataclasses.dataclass
class A:
    x: int = dataclasses.field(default=1)
    y: str = dataclasses.field(metadata={"doc": "foo"})
```

I understand that there may be quite some complexities to fix this correctly, but I would suggest in that case just removing this check altogether until it works correctly.

Tested on python 3.7.2 with mypy 0.670 and master, and python 3.6.6 with mypy 0.670

Hints from issue discussion:
Sorry for late answer, these examples indeed look like a bug. I think the fix can be simple, just use `RefExpr` in `_collect_field_args`.

Commit message: Fix dataclass plugin interaction with member expressions

Patch to review:
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
index d9fb3939515f..0f510695bee9 100644
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -3,7 +3,7 @@
 
 from mypy.nodes import (
     ARG_OPT, ARG_POS, MDEF, Argument, AssignmentStmt, CallExpr,
-    Context, Expression, FuncDef, JsonDict, NameExpr,
+    Context, Expression, FuncDef, JsonDict, NameExpr, RefExpr,
     SymbolTableNode, TempNode, TypeInfo, Var, TypeVarExpr
 )
 from mypy.plugin import ClassDefContext
@@ -300,7 +300,7 @@ def collect_attributes(self) -> List[DataclassAttribute]:
                     Context(line=attr.line, column=attr.column),
                 )
 
-            found_default = found_default or attr.has_default
+            found_default = found_default or attr.has_default and attr.is_in_init
 
         return all_attrs
 
@@ -337,7 +337,7 @@ def _collect_field_args(expr: Expression) -> Tuple[bool, Dict[str, Expression]]:
     """
     if (
             isinstance(expr, CallExpr) and
-            isinstance(expr.callee, NameExpr) and
+            isinstance(expr.callee, RefExpr) and
             expr.callee.fullname == 'dataclasses.field'
     ):
         # field() only takes keyword arguments.
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index e91dd2b14775..3ded77623e79 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -586,3 +586,22 @@ class B:
 
 reveal_type(B)  # E: Revealed type is 'def (x: Any) -> __main__.B'
 [builtins fixtures/list.pyi]
+
+[case testMemberExprWorksAsField]
+import dataclasses
+
+@dataclasses.dataclass
+class A:
+    x: int = dataclasses.field(metadata={"doc": "foo"})
+    y: str
+
+@dataclasses.dataclass
+class B:
+    x: int = dataclasses.field(init=False, default=1)
+    y: str
+
+@dataclasses.dataclass
+class C:
+    x: int = dataclasses.field(default=1)
+    y: str = dataclasses.field(metadata={"doc": "foo"})  # E: Attributes without a default cannot follow attributes with one
+[builtins fixtures/dict.pyi]


Code review comments:
diff_hunk: @@ -300,7 +300,7 @@ def collect_attributes(self) -> List[DataclassAttribute]:
                     Context(line=attr.line, column=attr.column),
                 )
 
-            found_default = found_default or attr.has_default
+            found_default = found_default or attr.has_default and attr.is_in_init
path: mypy/plugins/dataclasses.py
text: @user1:
Style nit: add parentheses to make precedence explicit.