Add error code "explicit-override" for strict @override mode (PEP 698) (#15512)

Add the strict mode for [PEP
698](https://peps.python.org/pep-0698/#strict-enforcement-per-project).

Closes: #14072
diff --git a/docs/source/class_basics.rst b/docs/source/class_basics.rst
index 82bbf00..73f95f1 100644
--- a/docs/source/class_basics.rst
+++ b/docs/source/class_basics.rst
@@ -210,7 +210,9 @@
 
 In order to ensure that your code remains correct when renaming methods,
 it can be helpful to explicitly mark a method as overriding a base
-method. This can be done with the ``@override`` decorator. If the base
+method. This can be done with the ``@override`` decorator. ``@override``
+can be imported from ``typing`` starting with Python 3.12 or from
+``typing_extensions`` for use with older Python versions. If the base
 method is then renamed while the overriding method is not, mypy will
 show an error:
 
@@ -233,6 +235,11 @@
        def g(self, y: str) -> None:   # Error: no corresponding base method found
            ...
 
+.. note::
+
+   Use :ref:`--enable-error-code explicit-override <code-explicit-override>` to require
+   that method overrides use the ``@override`` decorator. Emit an error if it is missing.
+
 You can also override a statically typed method with a dynamically
 typed one. This allows dynamically typed code to override methods
 defined in library classes without worrying about their type
diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst
index e1d47f7..30fad07 100644
--- a/docs/source/error_code_list2.rst
+++ b/docs/source/error_code_list2.rst
@@ -442,3 +442,42 @@
         # The following will not generate an error on either
         # Python 3.8, or Python 3.9
         42 + "testing..."  # type: ignore
+
+.. _code-explicit-override:
+
+Check that ``@override`` is used when overriding a base class method [explicit-override]
+----------------------------------------------------------------------------------------
+
+If you use :option:`--enable-error-code explicit-override <mypy --enable-error-code>`
+mypy generates an error if you override a base class method without using the
+``@override`` decorator. An error will not be emitted for overrides of ``__init__``
+or ``__new__``. See `PEP 698 <https://peps.python.org/pep-0698/#strict-enforcement-per-project>`_.
+
+.. note::
+
+    Starting with Python 3.12, the ``@override`` decorator can be imported from ``typing``.
+    To use it with older Python versions, import it from ``typing_extensions`` instead.
+
+Example:
+
+.. code-block:: python
+
+    # Use "mypy --enable-error-code explicit-override ..."
+
+    from typing import override
+
+    class Parent:
+        def f(self, x: int) -> None:
+            pass
+
+        def g(self, y: int) -> None:
+            pass
+
+
+    class Child(Parent):
+        def f(self, x: int) -> None:  # Error: Missing @override decorator
+            pass
+
+        @override
+        def g(self, y: int) -> None:
+            pass
diff --git a/mypy/checker.py b/mypy/checker.py
index 5ed1c79..71c9746 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -643,9 +643,14 @@
         if defn.impl:
             defn.impl.accept(self)
         if defn.info:
-            found_base_method = self.check_method_override(defn)
-            if defn.is_explicit_override and found_base_method is False:
+            found_method_base_classes = self.check_method_override(defn)
+            if (
+                defn.is_explicit_override
+                and not found_method_base_classes
+                and found_method_base_classes is not None
+            ):
                 self.msg.no_overridable_method(defn.name, defn)
+            self.check_explicit_override_decorator(defn, found_method_base_classes, defn.impl)
             self.check_inplace_operator_method(defn)
         if not defn.is_property:
             self.check_overlapping_overloads(defn)
@@ -972,7 +977,8 @@
                 # overload, the legality of the override has already
                 # been typechecked, and decorated methods will be
                 # checked when the decorator is.
-                self.check_method_override(defn)
+                found_method_base_classes = self.check_method_override(defn)
+                self.check_explicit_override_decorator(defn, found_method_base_classes)
             self.check_inplace_operator_method(defn)
         if defn.original_def:
             # Override previous definition.
@@ -1813,23 +1819,41 @@
         else:
             return [(defn, typ)]
 
-    def check_method_override(self, defn: FuncDef | OverloadedFuncDef | Decorator) -> bool | None:
+    def check_explicit_override_decorator(
+        self,
+        defn: FuncDef | OverloadedFuncDef,
+        found_method_base_classes: list[TypeInfo] | None,
+        context: Context | None = None,
+    ) -> None:
+        if (
+            found_method_base_classes
+            and not defn.is_explicit_override
+            and defn.name not in ("__init__", "__new__")
+        ):
+            self.msg.explicit_override_decorator_missing(
+                defn.name, found_method_base_classes[0].fullname, context or defn
+            )
+
+    def check_method_override(
+        self, defn: FuncDef | OverloadedFuncDef | Decorator
+    ) -> list[TypeInfo] | None:
         """Check if function definition is compatible with base classes.
 
         This may defer the method if a signature is not available in at least one base class.
         Return ``None`` if that happens.
 
-        Return ``True`` if an attribute with the method name was found in the base class.
+        Return a list of base classes which contain an attribute with the method name.
         """
         # Check against definitions in base classes.
-        found_base_method = False
+        found_method_base_classes: list[TypeInfo] = []
         for base in defn.info.mro[1:]:
             result = self.check_method_or_accessor_override_for_base(defn, base)
             if result is None:
                 # Node was deferred, we will have another attempt later.
                 return None
-            found_base_method |= result
-        return found_base_method
+            if result:
+                found_method_base_classes.append(base)
+        return found_method_base_classes
 
     def check_method_or_accessor_override_for_base(
         self, defn: FuncDef | OverloadedFuncDef | Decorator, base: TypeInfo
@@ -4739,9 +4763,14 @@
             self.check_incompatible_property_override(e)
         # For overloaded functions we already checked override for overload as a whole.
         if e.func.info and not e.func.is_dynamic() and not e.is_overload:
-            found_base_method = self.check_method_override(e)
-            if e.func.is_explicit_override and found_base_method is False:
+            found_method_base_classes = self.check_method_override(e)
+            if (
+                e.func.is_explicit_override
+                and not found_method_base_classes
+                and found_method_base_classes is not None
+            ):
                 self.msg.no_overridable_method(e.func.name, e.func)
+            self.check_explicit_override_decorator(e.func, found_method_base_classes)
 
         if e.func.info and e.func.name in ("__init__", "__new__"):
             if e.type and not isinstance(get_proper_type(e.type), (FunctionLike, AnyType)):
diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py
index 68ae4b4..717629a 100644
--- a/mypy/errorcodes.py
+++ b/mypy/errorcodes.py
@@ -235,6 +235,12 @@
 UNUSED_IGNORE: Final = ErrorCode(
     "unused-ignore", "Ensure that all type ignores are used", "General", default_enabled=False
 )
+EXPLICIT_OVERRIDE_REQUIRED: Final = ErrorCode(
+    "explicit-override",
+    "Require @override decorator if method is overriding a base class method",
+    "General",
+    default_enabled=False,
+)
 
 
 # Syntax errors are often blocking.
diff --git a/mypy/messages.py b/mypy/messages.py
index 021ad2c..ae7fba1 100644
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -1525,6 +1525,16 @@
             context,
         )
 
+    def explicit_override_decorator_missing(
+        self, name: str, base_name: str, context: Context
+    ) -> None:
+        self.fail(
+            f'Method "{name}" is not using @override '
+            f'but is overriding a method in class "{base_name}"',
+            context,
+            code=codes.EXPLICIT_OVERRIDE_REQUIRED,
+        )
+
     def final_cant_override_writable(self, name: str, ctx: Context) -> None:
         self.fail(f'Cannot override writable attribute "{name}" with a final one', ctx)
 
diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test
index 141d18a..0de4798 100644
--- a/test-data/unit/check-functions.test
+++ b/test-data/unit/check-functions.test
@@ -2759,8 +2759,7 @@
 class F(E):
     @override
     def f(self, x: int) -> str: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
 
 [case explicitOverrideStaticmethod]
 # flags: --python-version 3.12
@@ -2792,8 +2791,8 @@
     def f(x: str) -> str: pass  # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
                                 # N: This violates the Liskov substitution principle \
                                 # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/callable.pyi]
+[typing fixtures/typing-override.pyi]
+[builtins fixtures/staticmethod.pyi]
 
 [case explicitOverrideClassmethod]
 # flags: --python-version 3.12
@@ -2825,8 +2824,8 @@
     def f(cls, x: str) -> str: pass  # E: Argument 1 of "f" is incompatible with supertype "A"; supertype defines the argument type as "int" \
                                      # N: This violates the Liskov substitution principle \
                                      # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/callable.pyi]
+[typing fixtures/typing-override.pyi]
+[builtins fixtures/classmethod.pyi]
 
 [case explicitOverrideProperty]
 # flags: --python-version 3.12
@@ -2860,8 +2859,8 @@
                               # N:          str \
                               # N:      Subclass: \
                               # N:          int
+[typing fixtures/typing-override.pyi]
 [builtins fixtures/property.pyi]
-[typing fixtures/typing-full.pyi]
 
 [case explicitOverrideSettableProperty]
 # flags: --python-version 3.12
@@ -2898,8 +2897,8 @@
 
     @f.setter
     def f(self, value: int) -> None: pass
+[typing fixtures/typing-override.pyi]
 [builtins fixtures/property.pyi]
-[typing fixtures/typing-full.pyi]
 
 [case invalidExplicitOverride]
 # flags: --python-version 3.12
@@ -2914,8 +2913,7 @@
 def g() -> None:
     @override  # E: "override" used with a non-method
     def h(b: bool) -> int: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
 
 [case explicitOverrideSpecialMethods]
 # flags: --python-version 3.12
@@ -2931,8 +2929,7 @@
 class C:
     @override
     def __init__(self, a: int) -> None: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
 
 [case explicitOverrideFromExtensions]
 from typing_extensions import override
@@ -2943,7 +2940,6 @@
 class B(A):
     @override
     def f2(self, x: int) -> str: pass  # E: Method "f2" is marked as an override, but no base method was found with this name
-[typing fixtures/typing-full.pyi]
 [builtins fixtures/tuple.pyi]
 
 [case explicitOverrideOverloads]
@@ -2960,8 +2956,7 @@
     def f2(self, x: str) -> str: pass
     @override
     def f2(self, x: int | str) -> str: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
 
 [case explicitOverrideNotOnOverloadsImplementation]
 # flags: --python-version 3.12
@@ -2985,8 +2980,7 @@
     @overload
     def f(self, y: str) -> str: pass
     def f(self, y: int | str) -> str: pass
-[typing fixtures/typing-full.pyi]
-[builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
 
 [case explicitOverrideOnMultipleOverloads]
 # flags: --python-version 3.12
@@ -3012,5 +3006,157 @@
     def f(self, y: str) -> str: pass
     @override
     def f(self, y: int | str) -> str: pass
-[typing fixtures/typing-full.pyi]
+[typing fixtures/typing-override.pyi]
+
+[case explicitOverrideCyclicDependency]
+# flags: --python-version 3.12
+import b
+[file a.py]
+from typing import override
+import b
+import c
+
+class A(b.B):
+    @override  # This is fine
+    @c.deco
+    def meth(self) -> int: ...
+[file b.py]
+import a
+import c
+
+class B:
+    @c.deco
+    def meth(self) -> int: ...
+[file c.py]
+from typing import TypeVar, Tuple, Callable
+T = TypeVar('T')
+def deco(f: Callable[..., T]) -> Callable[..., Tuple[T, int]]: ...
 [builtins fixtures/tuple.pyi]
+[typing fixtures/typing-override.pyi]
+
+[case requireExplicitOverrideMethod]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import override
+
+class A:
+    def f(self, x: int) -> str: pass
+
+class B(A):
+    @override
+    def f(self, y: int) -> str: pass
+
+class C(A):
+    def f(self, y: int) -> str: pass  # E: Method "f" is not using @override but is overriding a method in class "__main__.A"
+
+class D(B):
+    def f(self, y: int) -> str: pass  # E: Method "f" is not using @override but is overriding a method in class "__main__.B"
+[typing fixtures/typing-override.pyi]
+
+[case requireExplicitOverrideSpecialMethod]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import Callable, Self, TypeVar, override, overload
+
+T = TypeVar('T')
+def some_decorator(f: Callable[..., T]) -> Callable[..., T]: ...
+
+# Don't require override decorator for __init__ and __new__
+# See: https://github.com/python/typing/issues/1376
+class A:
+    def __init__(self) -> None: pass
+    def __new__(cls) -> Self: pass
+
+class B(A):
+    def __init__(self) -> None: pass
+    def __new__(cls) -> Self: pass
+
+class C(A):
+    @some_decorator
+    def __init__(self) -> None: pass
+
+    @some_decorator
+    def __new__(cls) -> Self: pass
+
+class D(A):
+    @overload
+    def __init__(self, x: int) -> None: ...
+    @overload
+    def __init__(self, x: str) -> None: ...
+    def __init__(self, x): pass
+
+    @overload
+    def __new__(cls, x: int) -> Self: pass
+    @overload
+    def __new__(cls, x: str) -> Self: pass
+    def __new__(cls, x): pass
+[typing fixtures/typing-override.pyi]
+
+[case requireExplicitOverrideProperty]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import override
+
+class A:
+    @property
+    def prop(self) -> int: pass
+
+class B(A):
+    @override
+    @property
+    def prop(self) -> int: pass
+
+class C(A):
+    @property
+    def prop(self) -> int: pass  # E: Method "prop" is not using @override but is overriding a method in class "__main__.A"
+[typing fixtures/typing-override.pyi]
+[builtins fixtures/property.pyi]
+
+[case requireExplicitOverrideOverload]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import overload, override
+
+class A:
+    @overload
+    def f(self, x: int) -> str: ...
+    @overload
+    def f(self, x: str) -> str: ...
+    def f(self, x): pass
+
+class B(A):
+    @overload
+    def f(self, y: int) -> str: ...
+    @overload
+    def f(self, y: str) -> str: ...
+    @override
+    def f(self, y): pass
+
+class C(A):
+    @overload
+    @override
+    def f(self, y: int) -> str: ...
+    @overload
+    def f(self, y: str) -> str: ...
+    def f(self, y): pass
+
+class D(A):
+    @overload
+    def f(self, y: int) -> str: ...
+    @overload
+    def f(self, y: str) -> str: ...
+    def f(self, y): pass  # E: Method "f" is not using @override but is overriding a method in class "__main__.A"
+[typing fixtures/typing-override.pyi]
+
+[case requireExplicitOverrideMultipleInheritance]
+# flags: --enable-error-code explicit-override --python-version 3.12
+from typing import override
+
+class A:
+    def f(self, x: int) -> str: pass
+class B:
+    def f(self, y: int) -> str: pass
+
+class C(A, B):
+    @override
+    def f(self, z: int) -> str: pass
+
+class D(A, B):
+    def f(self, z: int) -> str: pass  # E: Method "f" is not using @override but is overriding a method in class "__main__.A"
+[typing fixtures/typing-override.pyi]
diff --git a/test-data/unit/fixtures/typing-override.pyi b/test-data/unit/fixtures/typing-override.pyi
new file mode 100644
index 0000000..606ca63
--- /dev/null
+++ b/test-data/unit/fixtures/typing-override.pyi
@@ -0,0 +1,25 @@
+TypeVar = 0
+Generic = 0
+Any = 0
+overload = 0
+Type = 0
+Literal = 0
+Optional = 0
+Self = 0
+Tuple = 0
+ClassVar = 0
+Callable = 0
+
+T = TypeVar('T')
+T_co = TypeVar('T_co', covariant=True)
+KT = TypeVar('KT')
+
+class Iterable(Generic[T_co]): pass
+class Iterator(Iterable[T_co]): pass
+class Sequence(Iterable[T_co]): pass
+class Mapping(Iterable[KT], Generic[KT, T_co]):
+    def keys(self) -> Iterable[T]: pass  # Approximate return type
+    def __getitem__(self, key: T) -> T_co: pass
+
+
+def override(__arg: T) -> T: ...