diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index 92e8d0424..f153ce06e 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -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), @@ -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}`?")); } diff --git a/pyrefly/lib/test/attributes.rs b/pyrefly/lib/test/attributes.rs index d6e4d7aae..87db5b86f 100644 --- a/pyrefly/lib/test/attributes.rs +++ b/pyrefly/lib/test/attributes.rs @@ -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#"