diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 559e888bc8..6b1ee6cd92 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -1073,16 +1073,23 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { let mut t_acc = Type::never(); let last_index = values.len() - 1; for (i, value) in values.iter().enumerate() { - // If there isn't a hint for the overall expression, use the preceding branches as a "soft" hint - // for the next one. Most useful for expressions like `optional_list or []`. - let hint = hint.or_else(|| { - if t_acc.is_never() { - None - } else { - Some(HintRef::soft(&t_acc)) - } - }); - let mut t = self.expr_infer_with_hint(value, hint, errors); + let operand_hint = if matches!(op, BoolOp::Or) && matches!(value, Expr::Call(_)) { + // Drop the hint for function calls in 'OR' expressions. + // This prevents the expected type (e.g. Optional[str]) from improperly influencing + // a generic function's inference (forcing it to return Optional[str] + // instead of str), while still preserving hints for literals like []. + None + } else { + // Use external hint, or fall back to previous branches + hint.or_else(|| { + if t_acc.is_never() { + None + } else { + Some(HintRef::soft(&t_acc)) + } + }) + }; + let mut t = self.expr_infer_with_hint(value, operand_hint, errors); self.expand_vars_mut(&mut t); // If this is not the last entry, we have to make a type-dependent decision and also narrow the // result; both operations require us to force `Var` first or they become unpredictable. diff --git a/pyrefly/lib/test/operators.rs b/pyrefly/lib/test/operators.rs index 24d8783e96..6a82628876 100644 --- a/pyrefly/lib/test/operators.rs +++ b/pyrefly/lib/test/operators.rs @@ -681,3 +681,33 @@ def f[S, T](x: type[S], y: type[T]): return x == y "#, ); + +testcase!( + test_or_generic_function_hint_poisoning_fix, + r#" +from typing import TypeVar, assert_type + +T = TypeVar("T") + +def get_value(default: T) -> T: + return default + +def test(config: str | None): + # 1. Setup: config is explicitly set to None. + # The variable 'config' still carries the type hint 'str | None'. + config = None + + # 2. The Test: + # We reassign to 'config' using an OR expression with a generic function call. + # + # Without the fix, the 'str | None' hint from the variable definition flows + # into 'get_value', causing it to infer T='str | None' instead of T='str'. + # + # With the fix, the hint is dropped for the function call, so it infers + # T='str' purely from the argument "default". + config = config or get_value("default") + + # Since left side was None (discarded) and right side is str, result must be str. + assert_type(config, str) +"#, +);