Skip to content
Open
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
13 changes: 10 additions & 3 deletions pyrefly/lib/alt/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
let mut error_messages = Vec::new();
let mut success = true;
let (found, not_found, error) = lookup_result.decompose();
// Check if we have a partial union failure (attribute exists on some union members
// but not others) before consuming the vectors. This helps us decide whether to suggest.
let is_partial_union_failure = !found.is_empty() && !not_found.is_empty();
for (attr, _) in found {
match self.resolve_get_access(attr_name, attr, range, errors, context) {
Ok(ty) => types.push(ty),
Expand All @@ -571,9 +574,13 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
error_messages.sort();
error_messages.dedup();
let mut msg = vec1![error_messages.join("\n")];
if let Some(suggestion) = attr_base
.as_ref()
.and_then(|attr_base| self.suggest_attribute_name(attr_name, attr_base))
// Skip suggestions when we have a partial union failure to avoid suggesting
// attributes from the types that have them when the problem is that some types
// don't have the attribute at all.
if !is_partial_union_failure
&& let Some(suggestion) = attr_base
.as_ref()
.and_then(|attr_base| self.suggest_attribute_name(attr_name, attr_base))
{
msg.push(format!("Did you mean `{suggestion}`?"));
}
Expand Down
62 changes: 62 additions & 0 deletions pyrefly/lib/test/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2112,6 +2112,68 @@ def f(x: Color) -> Color:
"#,
);

testcase!(
test_union_attribute_missing_no_suggestion,
r#"
# When an attribute exists on some union members but not others,
# we shouldn't suggest similar attributes from the types that have it
def f(x: str | None):
return x.split() # E: Object of class `NoneType` has no attribute `split`
"#,
);

testcase!(
test_union_attribute_missing_no_suggestion_three_types,
r#"
# Partial union failure with 3 types: attribute exists on 1, missing on 2
def f(x: str | int | None):
return x.split() # E: Object of class `NoneType` has no attribute `split`\nObject of class `int` has no attribute `split`
"#,
);

testcase!(
test_union_attribute_missing_no_suggestion_mostly_have_it,
r#"
# Even if most types have the attribute, if ANY don't, skip suggestion
class A:
upper: int
lower: int
class B:
upper: int
lower: int
def f(x: str | A | B):
# str has "upper" method, A and B have "upper" attribute
# But str doesn't have "lower" attribute, A and B do
x.lowerr # E: Object of class `str` has no attribute `lowerr`
"#,
);

testcase!(
test_union_both_missing_should_suggest,
r#"
# When an attribute is missing on ALL union members, we should still suggest
class A:
value: int
class B:
value: str
def f(x: A | B):
return x.vaule # E: Object of class `A` has no attribute `vaule`\nObject of class `B` has no attribute `vaule`\n Did you mean `value`?
"#,
);

testcase!(
test_union_all_have_attribute_no_error,
r#"
# When all union members have the attribute, there should be no error
class A:
value: int
class B:
value: str
def f(x: A | B):
return x.value # No error - both A and B have 'value'
"#,
);

testcase!(
test_class_toplevel_inherited_attr_name,
r#"
Expand Down