From d22991a694805d751f5e534fb8be14728e6fbf5f Mon Sep 17 00:00:00 2001 From: Timothy Nguyen Date: Sat, 27 Dec 2025 18:54:56 +0700 Subject: [PATCH 1/4] fix/issue-930-final --- pyrefly/lib/alt/narrow.rs | 10 ++++++++++ pyrefly/lib/test/attribute_narrow.rs | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pyrefly/lib/alt/narrow.rs b/pyrefly/lib/alt/narrow.rs index ffe82dac7a..64532684d8 100644 --- a/pyrefly/lib/alt/narrow.rs +++ b/pyrefly/lib/alt/narrow.rs @@ -959,6 +959,16 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { type_info.clone().with_ty(ty) } NarrowOp::Atomic(Some(facet_subject), op) => { + if let AtomicNarrowOp::Call(func, args) + | AtomicNarrowOp::NotCall(func, args) = op + { + if self + .resolve_narrowing_call(func.as_ref(), args, errors) + .is_none() + { + return type_info.clone(); + } + } if facet_subject.origin == FacetOrigin::GetMethod && !self.supports_dict_get_subject(type_info, facet_subject, range) { diff --git a/pyrefly/lib/test/attribute_narrow.rs b/pyrefly/lib/test/attribute_narrow.rs index 16a1cb93cd..de9b33bbd2 100644 --- a/pyrefly/lib/test/attribute_narrow.rs +++ b/pyrefly/lib/test/attribute_narrow.rs @@ -116,6 +116,21 @@ 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_attr_assignment_introduction, r#" From 81d40113dd49929b9a2bd11f8dcbe5f5b85161a4 Mon Sep 17 00:00:00 2001 From: Timothy Nguyen Date: Tue, 30 Dec 2025 23:15:15 +0700 Subject: [PATCH 2/4] fix/issue-930-final: fix lint blocking GitHub Action --- pyrefly/lib/alt/narrow.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pyrefly/lib/alt/narrow.rs b/pyrefly/lib/alt/narrow.rs index 64532684d8..d5f2fcfaca 100644 --- a/pyrefly/lib/alt/narrow.rs +++ b/pyrefly/lib/alt/narrow.rs @@ -959,15 +959,12 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { type_info.clone().with_ty(ty) } NarrowOp::Atomic(Some(facet_subject), op) => { - if let AtomicNarrowOp::Call(func, args) - | AtomicNarrowOp::NotCall(func, args) = op - { - if self + if let AtomicNarrowOp::Call(func, args) | AtomicNarrowOp::NotCall(func, args) = op + && self .resolve_narrowing_call(func.as_ref(), args, errors) .is_none() - { - return type_info.clone(); - } + { + return type_info.clone(); } if facet_subject.origin == FacetOrigin::GetMethod && !self.supports_dict_get_subject(type_info, facet_subject, range) From 873d089e8a9181944985d64ff506e4c447842528 Mon Sep 17 00:00:00 2001 From: Timothy Nguyen Date: Thu, 1 Jan 2026 21:34:45 +0700 Subject: [PATCH 3/4] fix/issue-930-final: fix error from mypy_primer --- pyrefly/lib/alt/narrow.rs | 37 ++++++++++++++++++++++------ pyrefly/lib/test/attribute_narrow.rs | 35 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/pyrefly/lib/alt/narrow.rs b/pyrefly/lib/alt/narrow.rs index d5f2fcfaca..862f185147 100644 --- a/pyrefly/lib/alt/narrow.rs +++ b/pyrefly/lib/alt/narrow.rs @@ -959,13 +959,22 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { type_info.clone().with_ty(ty) } NarrowOp::Atomic(Some(facet_subject), op) => { - if let AtomicNarrowOp::Call(func, args) | AtomicNarrowOp::NotCall(func, args) = op - && self + 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) - .is_none() - { + .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) { @@ -973,7 +982,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, ); @@ -986,7 +995,13 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { let base_ty = self.get_facet_chain_type(type_info, &prefix_chain, range); if let Some(narrowed_ty) = - self.atomic_narrow_for_facet(&base_ty, last, op, range, errors) + 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); @@ -995,7 +1010,13 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { _ => { let base_ty = type_info.ty(); if let Some(narrowed_ty) = - self.atomic_narrow_for_facet(base_ty, last, op, range, errors) + 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 de9b33bbd2..f34fd5dcbe 100644 --- a/pyrefly/lib/test/attribute_narrow.rs +++ b/pyrefly/lib/test/attribute_narrow.rs @@ -131,6 +131,41 @@ def f(x: str): "#, ); +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#" From b6d193fd5ee3092b53ee1ecb763582797bce4e93 Mon Sep 17 00:00:00 2001 From: Timothy Nguyen Date: Fri, 2 Jan 2026 21:43:57 +0700 Subject: [PATCH 4/4] fix/issue-930-final: fix lint errors --- pyrefly/lib/alt/narrow.rs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/pyrefly/lib/alt/narrow.rs b/pyrefly/lib/alt/narrow.rs index f5ce389256..27be9c3be8 100644 --- a/pyrefly/lib/alt/narrow.rs +++ b/pyrefly/lib/alt/narrow.rs @@ -1081,30 +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); - if let Some(narrowed_ty) = - self.atomic_narrow_for_facet( - &base_ty, - last, - op_for_narrow, - 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(); - if let Some(narrowed_ty) = - self.atomic_narrow_for_facet( - base_ty, - last, - op_for_narrow, - 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); }