Repository: python/mypy

Issue:
Inconsistent return type when calling overloaded function with Any argument
The results of type checking the following fragment are unexpected:

```py
from typing import Any, overload, Union

@overload
def f(x: int) -> int: ...
@overload
def f(x: Union[int, float]) -> float: ...
def f(x): pass

@overload
def g(x: int) -> int: ...
@overload
def g(x: float) -> float: ...
def g(x): pass

a: Any
reveal_type(f(a)) # Any   <--- okay
reveal_type(g(a)) # int   <--- unexpected
```

The correct result would arguably be `Any` for both of the calls. `Union[int, float]` should be equivalent to just `float`.

This seems to interact with the result type of `os.path.join` in Python 2 mode with #3300.


Discussion:
On current master both of them return ``int``.
Interestingly, with `strict-optional` it reports 
```
main.py:4: error: Overloaded function signatures 1 and 2 overlap with incompatible return types
main.py:16: error: Revealed type is 'Any'
main.py:17: error: Revealed type is 'builtins.int'
```
EDIT: this is on 0.521
Something strange happens, I just installed from master on another machine and it gives me:
```
tst22.py:16: error: Revealed type is 'Any'
tst22.py:17: error: Revealed type is 'builtins.int'
```
_both_ with and without ``--strict-optional``. I tried this twice, probably my previous comment is just wrong.
Strange. Just to avoid concerns about inconsistencies, I should have mentioned I was using 0.521, so it seems the overload issue was fixed in master.

Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -1,7 +1,7 @@
 """Expression type checker. This file is conceptually part of TypeChecker."""
 
 from collections import OrderedDict
-from typing import cast, Dict, Set, List, Tuple, Callable, Union, Optional
+from typing import cast, Dict, Set, List, Tuple, Callable, Union, Optional, Iterable
 
 from mypy.errors import report_internal_error
 from mypy.typeanal import has_any_from_unimported_type, check_for_explicit_any, set_any_tvars
@@ -1076,8 +1076,10 @@ def overload_call_target(self, arg_types: List[Type], arg_kinds: List[int],
             if similarity > 0 and similarity >= best_match:
                 if (match and not is_same_type(match[-1].ret_type,
                                                typ.ret_type) and
-                    not mypy.checker.is_more_precise_signature(
-                        match[-1], typ)):
+                    (not mypy.checker.is_more_precise_signature(match[-1], typ)
+                     or (any(isinstance(arg, AnyType) for arg in arg_types)
+                         and any_arg_causes_overload_ambiguity(
+                             match + [typ], arg_types, arg_kinds, arg_names)))):
                     # Ambiguous return type. Either the function overload is
                     # overlapping (which we don't handle very well here) or the
                     # caller has provided some Any argument types; in either
@@ -2764,3 +2766,72 @@ def overload_arg_similarity(actual: Type, formal: Type) -> int:
         return 2
     # Fall back to a conservative equality check for the remaining kinds of type.
     return 2 if is_same_type(erasetype.erase_type(actual), erasetype.erase_type(formal)) else 0
+
+
+def any_arg_causes_overload_ambiguity(items: List[CallableType],
+                                      arg_types: List[Type],
+                                      arg_kinds: List[int],
+                                      arg_names: List[Optional[str]]) -> bool:
+    """May an Any actual argument cause ambiguous result type on call to overloaded function?
+
+    Note that this sometimes returns True even if there is no ambiguity, since a correct
+    implementation would be complex (and the call would be imprecisely typed due to Any
+    types anyway).
+
+    Args:
+        items: Overload items matching the actual arguments
+        arg_types: Actual argument types
+        arg_kinds: Actual argument kinds
+        arg_names: Actual argument names
+    """
+    actual_to_formal = [
+        map_formals_to_actuals(
+            arg_kinds, arg_names, item.arg_kinds, item.arg_names, lambda i: arg_types[i])
+        for item in items
+    ]
+
+    for arg_idx, arg_type in enumerate(arg_types):
+        if isinstance(arg_type, AnyType):
+            matching_formals_unfiltered = [(item_idx, lookup[arg_idx])
+                                           for item_idx, lookup in enumerate(actual_to_formal)
+                                           if lookup[arg_idx]]
+            matching_formals = []
+            for item_idx, formals in matching_formals_unfiltered:
+                if len(formals) > 1:
+                    # An actual maps to multiple formals -- give up as too
+                    # complex, just assume it overlaps.
+                    return True
+                matching_formals.append((item_idx, items[item_idx].arg_types[formals[0]]))
+            if (not all_same_types(t for _, t in matching_formals) and
+                    not all_same_types(items[idx].ret_type
+                                       for idx, _ in matching_formals)):
+                # Any maps to multiple different types, and the return types of these items differ.
+                return True
+    return False
+
+
+def all_same_types(types: Iterable[Type]) -> bool:
+    types = list(types)
+    if len(types) == 0:
+        return True
+    return all(is_same_type(t, types[0]) for t in types[1:])
+
+
+def map_formals_to_actuals(caller_kinds: List[int],
+                           caller_names: List[Optional[str]],
+                           callee_kinds: List[int],
+                           callee_names: List[Optional[str]],
+                           caller_arg_type: Callable[[int],
+                                                     Type]) -> List[List[int]]:
+    """Calculate the reverse mapping of map_actuals_to_formals."""
+    formal_to_actual = map_actuals_to_formals(caller_kinds,
+                                              caller_names,
+                                              callee_kinds,
+                                              callee_names,
+                                              caller_arg_type)
+    # Now reverse the mapping.
+    actual_to_formal = [[] for _ in caller_kinds]  # type: List[List[int]]
+    for formal, actuals in enumerate(formal_to_actual):
+        for actual in actuals:
+            actual_to_formal[actual].append(formal)
+    return actual_to_formal


Test patch:
diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test
--- a/test-data/unit/check-overloading.test
+++ b/test-data/unit/check-overloading.test
@@ -1157,3 +1157,118 @@ class Test(object):
 
 t = Test()
 reveal_type(t.do_chain)  # E: Revealed type is '__main__.Chain'
+
+[case testOverloadWithOverlappingItemsAndAnyArgument1]
+from typing import overload, Any
+
+@overload
+def f(x: int) -> int: ...
+@overload
+def f(x: object) -> object: ...
+def f(x): pass
+
+a: Any
+reveal_type(f(a))  # E: Revealed type is 'Any'
+
+[case testOverloadWithOverlappingItemsAndAnyArgument2]
+from typing import overload, Any
+
+@overload
+def f(x: int) -> int: ...
+@overload
+def f(x: float) -> float: ...
+def f(x): pass
+
+a: Any
+reveal_type(f(a))  # E: Revealed type is 'Any'
+
+[case testOverloadWithOverlappingItemsAndAnyArgument3]
+from typing import overload, Any
+
+@overload
+def f(x: int) -> int: ...
+@overload
+def f(x: str) -> str: ...
+def f(x): pass
+
+a: Any
+reveal_type(f(a))  # E: Revealed type is 'Any'
+
+[case testOverloadWithOverlappingItemsAndAnyArgument4]
+from typing import overload, Any
+
+@overload
+def f(x: int, y: int, z: str) -> int: ...
+@overload
+def f(x: object, y: int, z: str) -> object: ...
+def f(x): pass
+
+a: Any
+# Any causes ambiguity
+reveal_type(f(a, 1, ''))  # E: Revealed type is 'Any'
+# Any causes no ambiguity
+reveal_type(f(1, a, a))  # E: Revealed type is 'builtins.int'
+reveal_type(f('', a, a))  # E: Revealed type is 'builtins.object'
+# Like above, but use keyword arguments.
+reveal_type(f(y=1, z='', x=a))  # E: Revealed type is 'Any'
+reveal_type(f(y=a, z='', x=1))  # E: Revealed type is 'builtins.int'
+reveal_type(f(z='', x=1, y=a))  # E: Revealed type is 'builtins.int'
+reveal_type(f(z='', x=a, y=1))  # E: Revealed type is 'Any'
+
+[case testOverloadWithOverlappingItemsAndAnyArgument5]
+from typing import overload, Any, Union
+
+@overload
+def f(x: int) -> int: ...
+@overload
+def f(x: Union[int, float]) -> float: ...
+def f(x): pass
+
+a: Any
+reveal_type(f(a))  # E: Revealed type is 'Any'
+
+[case testOverloadWithOverlappingItemsAndAnyArgument6]
+from typing import overload, Any
+
+@overload
+def f(x: int, y: int) -> int: ...
+@overload
+def f(x: float, y: int, z: str) -> float: ...
+@overload
+def f(x: object, y: int, z: str, a: None) -> object: ...
+def f(x): pass
+
+a: Any
+# Any causes ambiguity
+reveal_type(f(*a))  # E: Revealed type is 'Any'
+reveal_type(f(a, *a))  # E: Revealed type is 'Any'
+reveal_type(f(1, *a))  # E: Revealed type is 'Any'
+reveal_type(f(1.1, *a))  # E: Revealed type is 'Any'
+reveal_type(f('', *a))  # E: Revealed type is 'builtins.object'
+
+[case testOverloadWithOverlappingItemsAndAnyArgument7]
+from typing import overload, Any
+
+@overload
+def f(x: int, y: int, z: int) -> int: ...
+@overload
+def f(x: object, y: int, z: int) -> object: ...
+def f(x): pass
+
+a: Any
+# TODO: We could infer 'int' here
+reveal_type(f(1, *a))  # E: Revealed type is 'Any'
+
+[case testOverloadWithOverlappingItemsAndAnyArgument8]
+from typing import overload, Any
+
+@overload
+def f(x: int, y: int, z: int) -> str: ...
+@overload
+def f(x: object, y: int, z: int) -> str: ...
+def f(x): pass
+
+a: Any
+# The return type is not ambiguous so Any arguments cause no ambiguity.
+reveal_type(f(a, 1, 1)) # E: Revealed type is 'builtins.str'
+reveal_type(f(1, *a))  # E: Revealed type is 'builtins.str'