Repository: python/mypy

Issue:
New semantic analyzer: conditionally defined functions
Support conditionally defined functions was dropped at some point when implementing the early versions of the new semantic analyzer. It's probably worth implementing this from scratch since the old implementation was pretty messy.

This issue applies to functions where multiple definitions are considered reachable by mypy. Cases involving platform and Python version checks where only one implementation is reachable already work. Example where this applies:

```py
if cond():  # Condition determined at runtime!
    def foo() -> None:
        ... # something; will be checked
else:
    def foo() -> None:
        ... # something else; will also be checked
```


Discussion:
A clean solution for this may be non-trivial. In general we might need to switch from `node.node` to `node.nodes`, this will also allow us to support conditional definition of variables, conditional imports etc., but this is a big change. It seems to me there is a whole bunch of issues around conditional definitions and it would make sense to discuss/fix them together.
I was thinking of just cleaning up the implementation a bit now, without changing their behavior or general implementation strategy.
We also will need to support something like this:
```python
if cond():
    func: Callable[[str], Sequence[int]] = ...
else:
    def func(spec: str) -> Sequence[int]:
        ...
```
This is supported by old analyzer, and there are examples of this in our internal code (currently this causes a crash).

Resolving patch:
diff --git a/mypy/newsemanal/semanal.py b/mypy/newsemanal/semanal.py
--- a/mypy/newsemanal/semanal.py
+++ b/mypy/newsemanal/semanal.py
@@ -491,8 +491,15 @@ def file_context(self, file_node: MypyFile, fnam: str, options: Options,
         del self.options
 
     def visit_func_def(self, defn: FuncDef) -> None:
-        if not defn.is_decorated and not defn.is_overload:
-            self.add_func_to_symbol_table(defn)
+        defn.is_conditional = self.block_depth[-1] > 0
+
+        # We don't add module top-level functions to symbol tables
+        # when we analyze their bodies in the second phase on analysis,
+        # since they were added in the first phase. Nested functions
+        # get always added, since they aren't separate targets.
+        if not self.recurse_into_functions or len(self.function_stack) > 0:
+            if not defn.is_decorated and not defn.is_overload:
+                self.add_func_to_symbol_table(defn)
 
         if not self.recurse_into_functions:
             return
@@ -515,8 +522,6 @@ def _visit_func_def(self, defn: FuncDef) -> None:
             self.update_function_type_variables(defn.type, defn)
         self.function_stack.pop()
 
-        defn.is_conditional = self.block_depth[-1] > 0
-
         if self.is_class_scope():
             # Method definition
             assert self.type is not None
@@ -574,7 +579,7 @@ def prepare_method_signature(self, func: FuncDef, info: TypeInfo) -> None:
                         leading_type = self.class_type(leading_type)
                     func.type = replace_implicit_first_type(functype, leading_type)
 
-    def set_original_def(self, previous: Optional[Node], new: FuncDef) -> bool:
+    def set_original_def(self, previous: Optional[Node], new: Union[FuncDef, Decorator]) -> bool:
         """If 'new' conditionally redefine 'previous', set 'previous' as original
 
         We reject straight redefinitions of functions, as they are usually
@@ -583,6 +588,8 @@ def set_original_def(self, previous: Optional[Node], new: FuncDef) -> bool:
           def f(): ...
           def f(): ...  # Error: 'f' redefined
         """
+        if isinstance(new, Decorator):
+            new = new.func
         if isinstance(previous, (FuncDef, Var, Decorator)) and new.is_conditional:
             new.original_def = previous
             return True
@@ -2789,6 +2796,7 @@ def process_module_assignment(self, lvals: List[Lvalue], rval: Expression,
                             lnode.node = rnode.node
 
     def visit_decorator(self, dec: Decorator) -> None:
+        dec.func.is_conditional = self.block_depth[-1] > 0
         if not dec.is_overload:
             self.add_symbol(dec.name(), dec, dec)
         dec.func._fullname = self.qualified_name(dec.name())
@@ -3954,12 +3962,39 @@ def add_symbol_table_node(self, name: str, symbol: SymbolTableNode,
                 and context is not None
                 and not isinstance(existing.node, PlaceholderNode)):
             if existing.node != symbol.node:
-                self.name_already_defined(name, context, existing)
+                if isinstance(symbol.node, (FuncDef, Decorator)):
+                    self.add_func_redefinition(names, name, symbol)
+                if not (isinstance(symbol.node, (FuncDef, Decorator))
+                        and self.set_original_def(existing.node, symbol.node)):
+                    self.name_already_defined(name, context, existing)
         elif name not in self.missing_names and '*' not in self.missing_names:
             names[name] = symbol
             return True
         return False
 
+    def add_func_redefinition(self, names: SymbolTable, name: str,
+                              symbol: SymbolTableNode) -> None:
+        """Add a symbol table node that reflects a redefinition of a function.
+
+        Redefinitions need to be added to the symbol table so that they can be found
+        through AST traversal, but they have dummy names of form 'name-redefinition[N]',
+        where N ranges over 2, 3, ... (omitted for the first redefinition).
+        """
+        i = 1
+        while True:
+            if i == 1:
+                new_name = '{}-redefinition'.format(name)
+            else:
+                new_name = '{}-redefinition{}'.format(name, i)
+            existing = names.get(new_name)
+            if existing is None:
+                names[new_name] = symbol
+                return
+            elif existing.node is symbol.node:
+                # Already there
+                return
+            i += 1
+
     def add_module_symbol(self, id: str, as_id: str, module_public: bool,
                           context: Context, module_hidden: bool = False) -> None:
         """Add symbol that is a reference to a module object."""
diff --git a/mypy/newsemanal/semanal_main.py b/mypy/newsemanal/semanal_main.py
--- a/mypy/newsemanal/semanal_main.py
+++ b/mypy/newsemanal/semanal_main.py
@@ -160,7 +160,11 @@ def get_all_leaf_targets(symtable: SymbolTable,
     """Return all leaf targets in a symbol table (module-level and methods)."""
     result = []  # type: List[TargetInfo]
     for name, node in symtable.items():
-        new_prefix = prefix + '.' + name
+        shortname = name
+        if '-redef' in name:
+            # Restore original name from mangled name of multiply defined function
+            shortname = name.split('-redef')[0]
+        new_prefix = prefix + '.' + shortname
         if isinstance(node.node, (FuncDef, TypeInfo, OverloadedFuncDef, Decorator)):
             if node.node.fullname() == new_prefix:
                 if isinstance(node.node, TypeInfo):


Test patch:
diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test
--- a/test-data/unit/check-newsemanal.test
+++ b/test-data/unit/check-newsemanal.test
@@ -1417,3 +1417,68 @@ reveal_type(x)  # E: Revealed type is 'Tuple[builtins.int, builtins.int]'
 [file b.py]
 import a
 x = (1, 2)
+
+[case testNewAnalyzerConditionalFunc]
+if int():
+    def f(x: int) -> None:
+        pass
+    def g(x: int) -> None:
+        pass
+elif bool():
+    def f(x: int) -> None:
+        1()  # E: "int" not callable
+    def g(x: str) -> None:  # E: All conditional function variants must have identical signatures
+        pass
+else:
+    def f(x: int) -> None:
+        ''()  # E: "str" not callable
+
+reveal_type(g) # E: Revealed type is 'def (x: builtins.int)'
+
+[case testNewAnalyzerConditionalFuncDefer]
+if int():
+    def f(x: A) -> None:
+        pass
+    def g(x: A) -> None:
+        pass
+else:
+    def f(x: A) -> None:
+        1()  # E: "int" not callable
+    def g(x: str) -> None:  # E: All conditional function variants must have identical signatures
+        pass
+
+reveal_type(g) # E: Revealed type is 'def (x: __main__.A)'
+
+class A: pass
+
+[case testNewAnalyzerConditionalDecoratedFunc]
+from typing import Callable
+
+def dec(f: Callable[[int], None]) -> Callable[[str], None]:
+    pass
+
+if int():
+    from m import f
+else:
+    @dec
+    def f(x: int) -> None:
+        1()  # E: "int" not callable
+reveal_type(f) # E: Revealed type is 'def (x: builtins.str)'
+[file m.py]
+def f(x: str) -> None: pass
+
+[case testNewAnalyzerConditionallyDefineFuncOverVar]
+from typing import Callable
+
+if int():
+    f: Callable[[str], None]
+else:
+    def f(x: str) -> None:
+        ...
+reveal_type(f) # E: Revealed type is 'def (builtins.str)'
+
+[case testNewAnalyzerConditionallyDefineFuncOverClass]
+class C:
+    1()  # E: "int" not callable
+def C() -> None:  # E: Name 'C' already defined on line 1
+    ''()  # E: "str" not callable