diff --git a/pyrefly/lib/alt/narrow.rs b/pyrefly/lib/alt/narrow.rs index 7effe5f1da..27be9c3be8 100644 --- a/pyrefly/lib/alt/narrow.rs +++ b/pyrefly/lib/alt/narrow.rs @@ -1044,6 +1044,22 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { type_info.clone().with_ty(ty) } NarrowOp::Atomic(Some(facet_subject), op) => { + let resolved_call_op = match op { + AtomicNarrowOp::Call(func, args) => { + self.resolve_narrowing_call(func.as_ref(), args, errors) + } + AtomicNarrowOp::NotCall(func, args) => self + .resolve_narrowing_call(func.as_ref(), args, errors) + .map(|resolved_op| resolved_op.negate()), + _ => None, + }; + let op_for_narrow = if let Some(resolved_op) = resolved_call_op.as_ref() { + resolved_op + } else if matches!(op, AtomicNarrowOp::Call(..) | AtomicNarrowOp::NotCall(..)) { + return type_info.clone(); + } else { + op + }; if facet_subject.origin == FacetOrigin::GetMethod && !self.supports_dict_get_subject(type_info, facet_subject, range) { @@ -1051,7 +1067,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } let ty = self.atomic_narrow( &self.get_facet_chain_type(type_info, &facet_subject.chain, range), - op, + op_for_narrow, range, errors, ); @@ -1065,26 +1081,26 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { let prefix_chain = FacetChain::new(prefix_facets); let base_ty = self.get_facet_chain_type(type_info, &prefix_chain, range); - let dict_get_key_falsy = matches!(op, AtomicNarrowOp::IsFalsy) - && matches!(last, FacetKind::Key(_)); - if dict_get_key_falsy { - narrowed.update_for_assignment(facet_subject.chain.facets(), None); - } else if let Some(narrowed_ty) = - self.atomic_narrow_for_facet(&base_ty, last, op, range, errors) - && narrowed_ty != base_ty + if let Some(narrowed_ty) = self.atomic_narrow_for_facet( + &base_ty, + last, + op_for_narrow, + range, + errors, + ) && narrowed_ty != base_ty { narrowed = narrowed.with_narrow(prefix_chain.facets(), narrowed_ty); } } _ => { let base_ty = type_info.ty(); - let dict_get_key_falsy = matches!(op, AtomicNarrowOp::IsFalsy) - && matches!(last, FacetKind::Key(_)); - if dict_get_key_falsy { - narrowed.update_for_assignment(facet_subject.chain.facets(), None); - } else if let Some(narrowed_ty) = - self.atomic_narrow_for_facet(base_ty, last, op, range, errors) - && narrowed_ty != *base_ty + if let Some(narrowed_ty) = self.atomic_narrow_for_facet( + base_ty, + last, + op_for_narrow, + range, + errors, + ) && narrowed_ty != *base_ty { narrowed = narrowed.clone().with_ty(narrowed_ty); } diff --git a/pyrefly/lib/test/attribute_narrow.rs b/pyrefly/lib/test/attribute_narrow.rs index 16a1cb93cd..f34fd5dcbe 100644 --- a/pyrefly/lib/test/attribute_narrow.rs +++ b/pyrefly/lib/test/attribute_narrow.rs @@ -116,6 +116,56 @@ def f(foo: Foo): "#, ); +testcase!( + test_missing_attribute_call_does_not_narrow, + r#" +from typing import reveal_type +def f(x: str): + if ( + len(x.magic) # E: Object of class `str` has no attribute `magic` + or reveal_type( # E: revealed type: Unknown + x.magic # E: Object of class `str` has no attribute `magic` + ) + ): + pass +"#, +); + +testcase!( + test_missing_attribute_call_does_not_narrow_overload, + r#" +from typing import overload +class History: + pass +@overload +def open_like(path: str) -> int: ... +@overload +def open_like(path: bytes) -> int: ... +def open_like(path: object) -> int: + return 0 +def f(history: History): + if ( + len(history.filename) # E: Object of class `History` has no attribute `filename` + or open_like( + history.filename # E: Object of class `History` has no attribute `filename` + ) + ): + pass +"#, +); + +testcase!( + test_missing_attribute_call_does_not_narrow_union, + r#" +def f(x: int | str): + if ( + len(x.missing) # E: Object of class `int` has no attribute `missing`\nObject of class `str` has no attribute `missing` + or x.missing # E: Object of class `int` has no attribute `missing`\nObject of class `str` has no attribute `missing` + ): + pass +"#, +); + testcase!( test_attr_assignment_introduction, r#"