Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions pyrefly/lib/alt/overload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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
});
Expand Down
27 changes: 27 additions & 0 deletions pyrefly/lib/test/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
34 changes: 34 additions & 0 deletions pyrefly/lib/test/overload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
Loading