From dd28694a63967920ce7da3f15d08fa65564608b7 Mon Sep 17 00:00:00 2001
From: UncIeRick <yhe3@andrew.cmu.edu>
Date: Fri, 9 Dec 2022 14:44:03 -0500
Subject: [PATCH 1/5] added 2 testcases

---
 test-data/unit/check-dataclasses.test | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index c248f8db8585..a1a426ec9bd5 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -2001,3 +2001,29 @@ class Bar(Foo): ...
 e: Element[Bar]
 reveal_type(e.elements)  # N: Revealed type is "typing.Sequence[__main__.Element[__main__.Bar]]"
 [builtins fixtures/dataclasses.pyi]
+
+
+
+[case testErrorFinalFieldNoInitNoArgumentPassed]
+from typing import Final
+from dataclasses import dataclass, field
+@dataclass
+class Foo:
+    a: Final[int] = field(init=False)
+Foo().a # E: Final field "a" not set
+[builtins fixtures/dataclasses.pyi]
+
+[case testNoErrorFinalFieldDelayedInit]
+from typing import Final
+from dataclasses import dataclass, field
+
+@dataclass
+class Bar:
+    a: Final[int] = field()
+    b: Final[int]
+
+    def __init__(self) -> None:
+        self.a = 1
+        self.b = 1
+
+[builtins fixtures/dataclasses.pyi]

From 13fbb451ede370d815beed106ab5e6049c09eb6a Mon Sep 17 00:00:00 2001
From: Jake Zych <zych.jake@gmail.com>
Date: Sun, 11 Dec 2022 19:32:45 -0500
Subject: [PATCH 2/5] add unit tests ensuring correct behavior is preserved

---
 test-data/unit/check-dataclasses.test | 50 +++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index a1a426ec9bd5..a32764e06999 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -2027,3 +2027,53 @@ class Bar:
         self.b = 1
 
 [builtins fixtures/dataclasses.pyi]
+
+[case testErrorFinalFieldInitNoArgumentPassed]
+from typing import Final
+from dataclasses import dataclass, field
+
+@dataclass
+class Foo:
+    a: Final[int] = field() 
+
+Foo().a # E: Missing positional argument "a" in call to "Foo"
+[builtins fixtures/dataclasses.pyi]
+
+[case testFinalFieldGeneratedInitArgumentPassed]
+from typing import Final
+from dataclasses import dataclass, field
+
+@dataclass
+class Foo:
+    a: Final[int] = field() 
+
+Foo(1).a 
+[builtins fixtures/dataclasses.pyi]
+
+[case testFinalFieldInit]
+from typing import Final
+from dataclasses import dataclass, field
+
+@dataclass
+class Foo:
+    a: Final[int] = field(init=False)
+
+    def __init__(self):
+        self.a = 1
+
+Foo().a 
+[builtins fixtures/dataclasses.pyi]
+
+[case testFinalFieldPostInit]
+from typing import Final
+from dataclasses import dataclass, field
+
+@dataclass
+class Foo:
+    a: Final[int] = field(init=False)
+
+    def __post_init__(self):
+        self.a = 1
+
+Foo().a 
+[builtins fixtures/dataclasses.pyi]
\ No newline at end of file

From da944e8d97308856039052a9bb5feccff392af60 Mon Sep 17 00:00:00 2001
From: Jake Zych <zych.jake@gmail.com>
Date: Sun, 11 Dec 2022 21:30:28 -0500
Subject: [PATCH 3/5] fix init=False dataclass field errors

---
 mypy/checkmember.py                   | 21 ++++++++++++++++
 mypy/messages.py                      |  3 +++
 mypy/plugins/dataclasses.py           | 36 +++++++++++++++++++++++++++
 test-data/unit/check-dataclasses.test |  6 ++---
 test-data/unit/check-final.test       |  6 ++---
 5 files changed, 65 insertions(+), 7 deletions(-)

diff --git a/mypy/checkmember.py b/mypy/checkmember.py
index 554b49d3eda2..f937258f7ac8 100644
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -512,6 +512,9 @@ def analyze_member_var_access(
         if mx.is_lvalue and not mx.chk.get_final_context():
             check_final_member(name, info, mx.msg, mx.context)
 
+        if not mx.is_lvalue and not mx.chk.get_final_context():
+            check_final_assigned_in_init(name, info, mx.msg, mx.context)
+
         return analyze_var(name, v, itype, info, mx, implicit=implicit)
     elif isinstance(v, FuncDef):
         assert False, "Did not expect a function"
@@ -600,6 +603,24 @@ def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Cont
             msg.cant_assign_to_final(name, attr_assign=True, ctx=ctx)
 
 
+def check_final_assigned_in_init(
+    name: str, info: TypeInfo, msg: MessageBuilder, ctx: Context
+) -> None:
+    """Give an error if the final being accessed was never assigned in init (or the class)."""
+    for base in info.mro:
+        sym = base.names.get(name)
+        if (
+            sym
+            and is_final_node(sym.node)
+            and (
+                isinstance(sym.node, Var)
+                and not sym.node.final_set_in_init
+                and sym.node.final_unset_in_class
+            )
+        ):
+            msg.final_field_not_set_in_init(name, ctx=ctx)
+
+
 def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
     """Type check descriptor access.
 
diff --git a/mypy/messages.py b/mypy/messages.py
index 85fa30512534..9bef6afade7c 100644
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -1402,6 +1402,9 @@ def cant_assign_to_final(self, name: str, attr_assign: bool, ctx: Context) -> No
         kind = "attribute" if attr_assign else "name"
         self.fail(f'Cannot assign to final {kind} "{unmangle(name)}"', ctx)
 
+    def final_field_not_set_in_init(self, name: str, ctx: Context) -> None:
+        self.fail(f'Final field "{name}" not set', ctx)
+
     def protocol_members_cant_be_final(self, ctx: Context) -> None:
         self.fail("Protocol member cannot be final", ctx)
 
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
index 75496d5e56f9..ab17a0ed61d4 100644
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -19,7 +19,9 @@
     CallExpr,
     Context,
     Expression,
+    FuncDef,
     JsonDict,
+    MemberExpr,
     NameExpr,
     PlaceholderNode,
     RefExpr,
@@ -412,7 +414,36 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
         # Second, collect attributes belonging to the current class.
         current_attr_names: set[str] = set()
         kw_only = _get_decorator_bool_argument(ctx, "kw_only", False)
+        final_unset_fields = set()
         for stmt in cls.defs.body:
+            # Check statements in __init__ and __post_init__ to see if any
+            # Final class variables = field() are being properly set
+            if isinstance(stmt, FuncDef) and (
+                stmt.name == "__init__" or stmt.name == "__post_init__"
+            ):
+                for init_stmt in stmt.body.body:
+                    if not isinstance(init_stmt, AssignmentStmt):
+                        continue
+                    if not (
+                        isinstance(init_stmt.lvalues[0], MemberExpr)
+                        or isinstance(init_stmt.lvalues[0], NameExpr)
+                    ):
+                        continue
+                    final_lhs = init_stmt.lvalues[0]
+                    sym = cls.info.names.get(final_lhs.name)
+                    if sym is None:
+                        # There was probably a semantic analysis error.
+                        continue
+
+                    node = sym.node
+                    assert not isinstance(node, PlaceholderNode)
+                    assert isinstance(node, Var)
+                    if final_lhs.name in final_unset_fields:
+                        node.final_unset_in_class = False
+                        node.final_set_in_init = True
+                        init_stmt.is_final_def = True
+                continue
+
             # Any assignment that doesn't use the new type declaration
             # syntax can be ignored out of hand.
             if not (isinstance(stmt, AssignmentStmt) and stmt.new_syntax):
@@ -469,6 +500,11 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
             else:
                 is_in_init = bool(ctx.api.parse_bool(is_in_init_param))
 
+            if has_field_call and node.is_final:
+                if not is_in_init:
+                    node.final_unset_in_class = True
+                final_unset_fields.add(lhs.name)
+
             has_default = False
             # Ensure that something like x: int = field() is rejected
             # after an attribute with a default.
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index a32764e06999..e8a64686486e 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -2002,14 +2002,12 @@ e: Element[Bar]
 reveal_type(e.elements)  # N: Revealed type is "typing.Sequence[__main__.Element[__main__.Bar]]"
 [builtins fixtures/dataclasses.pyi]
 
-
-
 [case testErrorFinalFieldNoInitNoArgumentPassed]
 from typing import Final
 from dataclasses import dataclass, field
 @dataclass
 class Foo:
-    a: Final[int] = field(init=False)
+    a: Final[int] = field(init=False) # E: Final name must be initialized with a value
 Foo().a # E: Final field "a" not set
 [builtins fixtures/dataclasses.pyi]
 
@@ -2023,7 +2021,7 @@ class Bar:
     b: Final[int]
 
     def __init__(self) -> None:
-        self.a = 1
+        self.a = 1 
         self.b = 1
 
 [builtins fixtures/dataclasses.pyi]
diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test
index da034caced76..8e0a8ad45f80 100644
--- a/test-data/unit/check-final.test
+++ b/test-data/unit/check-final.test
@@ -210,10 +210,10 @@ class C:
     y: Final[int]  # E: Final name must be initialized with a value
     def __init__(self) -> None:
         self.z: Final  # E: Type in Final[...] can only be omitted if there is an initializer
-reveal_type(x)  # N: Revealed type is "Any"
+reveal_type(x)  # N: Revealed type is "Any" 
 reveal_type(y)  # N: Revealed type is "builtins.int"
-reveal_type(C().x)  # N: Revealed type is "Any"
-reveal_type(C().y)  # N: Revealed type is "builtins.int"
+reveal_type(C().x)  # E: Final field "x" not set # N: Revealed type is "Any" 
+reveal_type(C().y)  # E: Final field "y" not set # N: Revealed type is "builtins.int"
 reveal_type(C().z)  # N: Revealed type is "Any"
 [out]
 

From b699ecae17f19d36a523a1e6e8eb7147be1030bd Mon Sep 17 00:00:00 2001
From: Jake Zych <zych.jake@gmail.com>
Date: Tue, 13 Dec 2022 14:00:33 -0500
Subject: [PATCH 4/5] ignore stubs in final unset field access check

---
 mypy/checkmember.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mypy/checkmember.py b/mypy/checkmember.py
index f937258f7ac8..4b2e47b1ceca 100644
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -512,7 +512,7 @@ def analyze_member_var_access(
         if mx.is_lvalue and not mx.chk.get_final_context():
             check_final_member(name, info, mx.msg, mx.context)
 
-        if not mx.is_lvalue and not mx.chk.get_final_context():
+        if not mx.is_lvalue and not mx.chk.get_final_context() and not mx.chk.is_stub:
             check_final_assigned_in_init(name, info, mx.msg, mx.context)
 
         return analyze_var(name, v, itype, info, mx, implicit=implicit)

From 1d5e1207a3c3f1acf53444569fc2eedc9fa24c88 Mon Sep 17 00:00:00 2001
From: Jake Zych <zych.jake@gmail.com>
Date: Mon, 19 Dec 2022 19:54:40 -0600
Subject: [PATCH 5/5] add check for explicit value

---
 mypy/checkmember.py             | 12 +++++++++---
 test-data/unit/check-final.test |  4 ++--
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/mypy/checkmember.py b/mypy/checkmember.py
index 4b2e47b1ceca..bbda7adac7f0 100644
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -511,10 +511,15 @@ def analyze_member_var_access(
         # independently of types.
         if mx.is_lvalue and not mx.chk.get_final_context():
             check_final_member(name, info, mx.msg, mx.context)
-
-        if not mx.is_lvalue and not mx.chk.get_final_context() and not mx.chk.is_stub:
+        # If accessing a final attribute, check if it was properly assigned
+        # in init (for dataclass field() specifically)
+        if (
+            not mx.is_lvalue
+            and not mx.chk.get_final_context()
+            and not mx.chk.is_stub
+            and not mx.chk.is_typeshed_stub
+        ):
             check_final_assigned_in_init(name, info, mx.msg, mx.context)
-
         return analyze_var(name, v, itype, info, mx, implicit=implicit)
     elif isinstance(v, FuncDef):
         assert False, "Did not expect a function"
@@ -616,6 +621,7 @@ def check_final_assigned_in_init(
                 isinstance(sym.node, Var)
                 and not sym.node.final_set_in_init
                 and sym.node.final_unset_in_class
+                and sym.node.has_explicit_value
             )
         ):
             msg.final_field_not_set_in_init(name, ctx=ctx)
diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test
index 8e0a8ad45f80..9422301b88d0 100644
--- a/test-data/unit/check-final.test
+++ b/test-data/unit/check-final.test
@@ -212,8 +212,8 @@ class C:
         self.z: Final  # E: Type in Final[...] can only be omitted if there is an initializer
 reveal_type(x)  # N: Revealed type is "Any" 
 reveal_type(y)  # N: Revealed type is "builtins.int"
-reveal_type(C().x)  # E: Final field "x" not set # N: Revealed type is "Any" 
-reveal_type(C().y)  # E: Final field "y" not set # N: Revealed type is "builtins.int"
+reveal_type(C().x)  # N: Revealed type is "Any" 
+reveal_type(C().y)  # N: Revealed type is "builtins.int"
 reveal_type(C().z)  # N: Revealed type is "Any"
 [out]