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
46 changes: 31 additions & 15 deletions pyrefly/lib/alt/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,14 +1044,30 @@ 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)
{
return type_info.clone();
}
let ty = self.atomic_narrow(
&self.get_facet_chain_type(type_info, &facet_subject.chain, range),
op,
op_for_narrow,
range,
errors,
);
Expand All @@ -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);
}
Expand Down
50 changes: 50 additions & 0 deletions pyrefly/lib/test/attribute_narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
Loading