diff --git a/pyrefly/lib/alt/overload.rs b/pyrefly/lib/alt/overload.rs index a369adfd52..7c21721e74 100644 --- a/pyrefly/lib/alt/overload.rs +++ b/pyrefly/lib/alt/overload.rs @@ -844,7 +844,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { // ambiguity in overload selection. This matches pyright, mypy, and ty. let owner = Owner::new(); let mut changed = false; - let should_materialize = |arg_range| { + let should_materialize = |arg_range, top_level_any| { if spec_compliant { return true; } @@ -855,28 +855,47 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { // If we can't find the expected type, be conservative and assume there may be multiple. return true; }; + let mut all_equivalent = true; + let mut only_object_fallbacks = !first.is_any() + && !matches!(first, Type::ClassType(cls) if cls.is_builtin("object")); for t in param_types { - if !self.is_equivalent(first, t) { - return true; - } + let equivalent = self.is_equivalent(first, t); + all_equivalent &= equivalent; + only_object_fallbacks &= equivalent + || matches!(t, Type::ClassType(cls) if cls.is_builtin("object")); } - false + // Mypy and pyright prefer the specific overload when a gradual type nested + // inside the argument makes both it and a later `object` fallback match. Keep + // materializing a top-level `Any`, since that call is genuinely ambiguous. + !all_equivalent && (top_level_any || !only_object_fallbacks) }; let materialized_args = args.map(|arg| { - let (materialized_arg, arg_changed) = if should_materialize(arg.range()) { - arg.materialize(self, errors, &owner) - } else { - (arg.clone(), false) + let top_level_any = match arg { + CallArg::Arg(TypeOrExpr::Type(ty, _)) + | CallArg::Star(TypeOrExpr::Type(ty, _), _) => ty.is_any(), + CallArg::Arg(TypeOrExpr::Expr(_)) + | CallArg::Star(TypeOrExpr::Expr(_), _) => true, }; + let (materialized_arg, arg_changed) = + if should_materialize(arg.range(), top_level_any) { + arg.materialize(self, errors, &owner) + } else { + (arg.clone(), false) + }; changed |= arg_changed; materialized_arg }); let materialized_keywords = keywords.map(|kw| { - let (materialized_kw, kw_changed) = if should_materialize(kw.range()) { - kw.materialize(self, errors, &owner) - } else { - (kw.clone(), false) + let top_level_any = match kw.value { + TypeOrExpr::Type(ty, _) => ty.is_any(), + TypeOrExpr::Expr(_) => true, }; + let (materialized_kw, kw_changed) = + if should_materialize(kw.range(), top_level_any) { + kw.materialize(self, errors, &owner) + } else { + (kw.clone(), false) + }; changed |= kw_changed; materialized_kw }); diff --git a/pyrefly/lib/test/operators.rs b/pyrefly/lib/test/operators.rs index 3e8f2fac34..bbb9d3174a 100644 --- a/pyrefly/lib/test/operators.rs +++ b/pyrefly/lib/test/operators.rs @@ -701,6 +701,33 @@ assert_type(A() == 42, bool) "#, ); +// Regression test for https://github.com/facebook/pyrefly/issues/3977. +testcase!( + test_overloaded_eq_preserves_bounded_typevar, + r#" +from typing import Any, Generic, TypeVar, assert_type, overload + +ShapeT = TypeVar("ShapeT", bound=tuple[int, ...]) +TypeT = TypeVar("TypeT") +AnyShape = tuple[Any, ...] + +class Array(Generic[ShapeT, TypeT]): ... + +def widen_shape(arr: Array[Any, TypeT]) -> Array[AnyShape, TypeT]: ... + +class C: + @overload + def __eq__(self, other: Array[ShapeT, Any]) -> Array[ShapeT, bool]: ... + @overload + def __eq__(self, other: object) -> bool: ... + def __eq__(self, other: object) -> Any: ... + +def test(x: C, concrete: Array[tuple[int], str]) -> None: + widened = widen_shape(concrete) + assert_type(x == widened, Array[AnyShape, bool]) + "#, +); + testcase!( test_in_generator, r#" diff --git a/pyrefly/lib/test/overload.rs b/pyrefly/lib/test/overload.rs index 42d38fba01..ac237609fa 100644 --- a/pyrefly/lib/test/overload.rs +++ b/pyrefly/lib/test/overload.rs @@ -1547,6 +1547,40 @@ def g(x: list[Any]): "#, ); +testcase!( + test_nested_any_prefers_over_object_fallback, + r#" +from typing import Any, assert_type, overload + +@overload +def f(x: list[int]) -> int: ... +@overload +def f(x: object) -> str: ... +def f(x: object) -> int | str: ... + +def g(nested: list[Any], dynamic: Any): + assert_type(f(nested), int) + assert_type(f(dynamic), Any) + "#, +); + +testcase!( + test_nested_any_with_object_fallback_spec_compliant, + TestEnv::new().enable_spec_compliant_overloads(), + r#" +from typing import Any, assert_type, overload + +@overload +def f(x: list[int]) -> int: ... +@overload +def f(x: object) -> str: ... +def f(x: object) -> int | str: ... + +def g(nested: list[Any]): + assert_type(f(nested), Any) + "#, +); + testcase!( test_callable_param_materialization, r#"