Repository: python/mypy
Issue category: Bug Report

Issue:
Conditional decorated functions are not type checked
**Bug Report**

Going through Mypy's tests, I realized that some tests are passing that shouldn't. This involves for example:

- `testConditionalFunctionDefinitionUsingDecorator3` (there's an error in the test that the test author probably didn't notice: Positional only args of a callable are never assignable to a normal function with positional-or-keyword params; typically an error in Mypy would look like this: `Incompatible redefinition (redefinition with type "Callable[[int], None]", original type "Callable[[Arg(int, 'x')], None]")`)
- `testConditionalFunctionDefinitionUsingDecorator4` (there's even a TODO in there that complains about this not being done)
- `testNewAnalyzerConditionalDecoratedFunc`

**To Reproduce**

```python
from typing import Callable
def dec(f) -> Callable[[int], None]: raise NotImplementedError

if int():
    def f(x: str): pass
else:
    @dec
    def f() -> None: pass   
```

https://mypy-play.net/?mypy=latest&python=3.12&gist=18b131da809d740eced20dd7652ea213

**Expected Behavior**

It would be nice if Mypy complained here (like in AFAIK all other conditional definitions).

I'm happy to assist if somebody wants to fix this.


Hints:

Patch:
diff --git a/mypy/checker.py b/mypy/checker.py
index 4b3d6c3298b4..f52bebdaa052 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1072,46 +1072,7 @@ def _visit_func_def(self, defn: FuncDef) -> None:
         if defn.original_def:
             # Override previous definition.
             new_type = self.function_type(defn)
-            if isinstance(defn.original_def, FuncDef):
-                # Function definition overrides function definition.
-                old_type = self.function_type(defn.original_def)
-                if not is_same_type(new_type, old_type):
-                    self.msg.incompatible_conditional_function_def(defn, old_type, new_type)
-            else:
-                # Function definition overrides a variable initialized via assignment or a
-                # decorated function.
-                orig_type = defn.original_def.type
-                if orig_type is None:
-                    # If other branch is unreachable, we don't type check it and so we might
-                    # not have a type for the original definition
-                    return
-                if isinstance(orig_type, PartialType):
-                    if orig_type.type is None:
-                        # Ah this is a partial type. Give it the type of the function.
-                        orig_def = defn.original_def
-                        if isinstance(orig_def, Decorator):
-                            var = orig_def.var
-                        else:
-                            var = orig_def
-                        partial_types = self.find_partial_types(var)
-                        if partial_types is not None:
-                            var.type = new_type
-                            del partial_types[var]
-                    else:
-                        # Trying to redefine something like partial empty list as function.
-                        self.fail(message_registry.INCOMPATIBLE_REDEFINITION, defn)
-                else:
-                    name_expr = NameExpr(defn.name)
-                    name_expr.node = defn.original_def
-                    self.binder.assign_type(name_expr, new_type, orig_type)
-                    self.check_subtype(
-                        new_type,
-                        orig_type,
-                        defn,
-                        message_registry.INCOMPATIBLE_REDEFINITION,
-                        "redefinition with type",
-                        "original type",
-                    )
+            self.check_func_def_override(defn, new_type)
 
     def check_func_item(
         self,
@@ -1147,6 +1108,49 @@ def check_func_item(
             if dataclasses_plugin.is_processed_dataclass(defn.info):
                 dataclasses_plugin.check_post_init(self, defn, defn.info)
 
+    def check_func_def_override(self, defn: FuncDef, new_type: FunctionLike) -> None:
+        assert defn.original_def is not None
+        if isinstance(defn.original_def, FuncDef):
+            # Function definition overrides function definition.
+            old_type = self.function_type(defn.original_def)
+            if not is_same_type(new_type, old_type):
+                self.msg.incompatible_conditional_function_def(defn, old_type, new_type)
+        else:
+            # Function definition overrides a variable initialized via assignment or a
+            # decorated function.
+            orig_type = defn.original_def.type
+            if orig_type is None:
+                # If other branch is unreachable, we don't type check it and so we might
+                # not have a type for the original definition
+                return
+            if isinstance(orig_type, PartialType):
+                if orig_type.type is None:
+                    # Ah this is a partial type. Give it the type of the function.
+                    orig_def = defn.original_def
+                    if isinstance(orig_def, Decorator):
+                        var = orig_def.var
+                    else:
+                        var = orig_def
+                    partial_types = self.find_partial_types(var)
+                    if partial_types is not None:
+                        var.type = new_type
+                        del partial_types[var]
+                else:
+                    # Trying to redefine something like partial empty list as function.
+                    self.fail(message_registry.INCOMPATIBLE_REDEFINITION, defn)
+            else:
+                name_expr = NameExpr(defn.name)
+                name_expr.node = defn.original_def
+                self.binder.assign_type(name_expr, new_type, orig_type)
+                self.check_subtype(
+                    new_type,
+                    orig_type,
+                    defn,
+                    message_registry.INCOMPATIBLE_REDEFINITION,
+                    "redefinition with type",
+                    "original type",
+                )
+
     @contextmanager
     def enter_attribute_inference_context(self) -> Iterator[None]:
         old_types = self.inferred_attribute_types
@@ -5120,6 +5124,10 @@ def visit_decorator_inner(self, e: Decorator, allow_empty: bool = False) -> None
             if e.type and not isinstance(get_proper_type(e.type), (FunctionLike, AnyType)):
                 self.fail(message_registry.BAD_CONSTRUCTOR_TYPE, e)
 
+        if e.func.original_def and isinstance(sig, FunctionLike):
+            # Function definition overrides function definition.
+            self.check_func_def_override(e.func, sig)
+
     def check_for_untyped_decorator(
         self, func: FuncDef, dec_type: Type, dec_expr: Expression
     ) -> None:


Test patch:
diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test
index 96f9815019e6..b8a02a1ec7d4 100644
--- a/test-data/unit/check-functions.test
+++ b/test-data/unit/check-functions.test
@@ -1474,7 +1474,7 @@ def dec(f) -> Callable[[int], None]: pass
 
 x = int()
 if x:
-    def f(x: int) -> None: pass
+    def f(x: int, /) -> None: pass
 else:
     @dec
     def f(): pass
@@ -1489,9 +1489,12 @@ x = int()
 if x:
     def f(x: str) -> None: pass
 else:
-    # TODO: Complain about incompatible redefinition
     @dec
-    def f(): pass
+    def f(): pass   # E: All conditional function variants must have identical signatures \
+                    # N: Original: \
+                    # N:     def f(x: str) -> None \
+                    # N: Redefinition: \
+                    # N:     def f(int, /) -> None
 
 [case testConditionalFunctionDefinitionUnreachable]
 def bar() -> None:
@@ -1599,7 +1602,7 @@ else:
     def f():
         yield
 [file m.py]
-def f(): pass
+def f() -> None: pass
 
 [case testDefineConditionallyAsImportedAndDecoratedWithInference]
 if int():
diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test
index 511c7b003015..fe02ac3ccd5e 100644
--- a/test-data/unit/check-newsemanal.test
+++ b/test-data/unit/check-newsemanal.test
@@ -1908,9 +1908,9 @@ else:
     @dec
     def f(x: int) -> None:
         1()  # E: "int" not callable
-reveal_type(f) # N: Revealed type is "def (x: builtins.str)"
+reveal_type(f) # N: Revealed type is "def (builtins.str)"
 [file m.py]
-def f(x: str) -> None: pass
+def f(x: str, /) -> None: pass
 
 [case testNewAnalyzerConditionallyDefineFuncOverVar]
 from typing import Callable
diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test
index e414c1c9b0b6..9d01ce6bd480 100644
--- a/test-data/unit/check-overloading.test
+++ b/test-data/unit/check-overloading.test
@@ -6463,7 +6463,7 @@ class D: ...
 def f1(g: A) -> A: ...
 if True:
     @overload  # E: Single overload definition, multiple required
-    def f1(g: B) -> B: ...
+    def f1(g: B) -> B: ...  # E: Incompatible redefinition (redefinition with type "Callable[[B], B]", original type "Callable[[A], A]")
     if maybe_true:  # E: Condition can't be inferred, unable to merge overloads \
                     # E: Name "maybe_true" is not defined
         @overload
@@ -6480,14 +6480,14 @@ if True:
         def f2(g: B) -> B: ...
     elif maybe_true:  # E: Name "maybe_true" is not defined
         @overload  # E: Single overload definition, multiple required
-        def f2(g: C) -> C: ...
+        def f2(g: C) -> C: ...  # E: Incompatible redefinition (redefinition with type "Callable[[C], C]", original type "Callable[[A], A]")
 def f2(g): ...  # E: Name "f2" already defined on line 21
 
 @overload  # E: Single overload definition, multiple required
 def f3(g: A) -> A: ...
 if True:
     @overload  # E: Single overload definition, multiple required
-    def f3(g: B) -> B: ...
+    def f3(g: B) -> B: ...  # E: Incompatible redefinition (redefinition with type "Callable[[B], B]", original type "Callable[[A], A]")
     if True:
         pass  # Some other node
         @overload  # E: Name "f3" already defined on line 32 \