Skip to content

Commit 5144e69

Browse files
fangyi-zhoumeta-codesync[bot]
authored andcommitted
Check for compatibility when inheriting from multiple classes for fields (#1196)
Summary: This diff is an attempt to address the issue in #943. When a class inherits from multiple base classes, we check whether the intersection of the field types is `never`. If so, we raise an error. Next step: Handle method overrides / overloads. Pull Request resolved: #1196 Reviewed By: stroxler Differential Revision: D84155735 Pulled By: yangdanny97 fbshipit-source-id: 1b1e526951ae3ed79034d188fe2cb413d9027b26
1 parent 02819b9 commit 5144e69

File tree

11 files changed

+168
-10
lines changed

11 files changed

+168
-10
lines changed

conformance/third_party/conformance.exp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9961,6 +9961,16 @@
99619961
"name": "bad-override",
99629962
"stop_column": 5,
99639963
"stop_line": 55
9964+
},
9965+
{
9966+
"code": -2,
9967+
"column": 7,
9968+
"concise_description": "Field `x` has inconsistent types inherited from multiple base classes",
9969+
"description": "Field `x` has inconsistent types inherited from multiple base classes\n Inherited types include:\n `int` from `X2`\n `str` from `Y2`",
9970+
"line": 65,
9971+
"name": "inconsistent-inheritance",
9972+
"stop_column": 11,
9973+
"stop_line": 65
99649974
}
99659975
],
99669976
"typeddicts_operations.py": [

conformance/third_party/conformance.result

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,7 @@
343343
"typeddicts_class_syntax.py": [],
344344
"typeddicts_extra_items.py": [],
345345
"typeddicts_final.py": [],
346-
"typeddicts_inheritance.py": [
347-
"Line 65: Expected 1 errors"
348-
],
346+
"typeddicts_inheritance.py": [],
349347
"typeddicts_operations.py": [],
350348
"typeddicts_readonly.py": [],
351349
"typeddicts_readonly_consistency.py": [],

conformance/third_party/results.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"total": 138,
3-
"pass": 94,
4-
"fail": 44,
5-
"pass_rate": 0.68,
6-
"differences": 180,
3+
"pass": 95,
4+
"fail": 43,
5+
"pass_rate": 0.69,
6+
"differences": 179,
77
"passing": [
88
"aliases_explicit.py",
99
"aliases_newtype.py",
@@ -92,6 +92,7 @@
9292
"typeddicts_class_syntax.py",
9393
"typeddicts_extra_items.py",
9494
"typeddicts_final.py",
95+
"typeddicts_inheritance.py",
9596
"typeddicts_operations.py",
9697
"typeddicts_readonly.py",
9798
"typeddicts_readonly_consistency.py",
@@ -142,7 +143,6 @@
142143
"qualifiers_final_annotation.py": 12,
143144
"specialtypes_never.py": 1,
144145
"specialtypes_type.py": 8,
145-
"typeddicts_inheritance.py": 1,
146146
"typeddicts_readonly_inheritance.py": 4,
147147
"typeddicts_required.py": 1
148148
},

crates/pyrefly_config/src/error_kind.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ pub enum ErrorKind {
140140
/// An error related to the import machinery.
141141
/// e.g. failed to import a module.
142142
ImportError,
143+
/// An inconsistency between inherited fields or methods from multiple base classes.
144+
InconsistentInheritance,
143145
/// An inconsistency between the signature of a function overload and the implementation.
144146
InconsistentOverload,
145147
/// Attempting to access a container with an incorrect index.

pyrefly/lib/alt/class/class_field.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1964,6 +1964,67 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
19641964
}
19651965
}
19661966

1967+
/// For classes with multiple inheritance, check that fields inherited from multiple base classes are consistent.
1968+
pub fn check_consistent_multiple_inheritance(&self, cls: &Class, errors: &ErrorCollector) {
1969+
// Maps field from inherited class
1970+
let mro = self.get_mro_for_class(cls);
1971+
let mut inherited_fields: SmallMap<&Name, Vec<(&Name, Type)>> = SmallMap::new();
1972+
1973+
for parent_cls in mro.ancestors_no_object().iter() {
1974+
let class_fields = parent_cls.class_object().fields();
1975+
for field in class_fields {
1976+
let key = KeyClassField(parent_cls.class_object().index(), field.clone());
1977+
let field_entry = self.get_from_class(cls, &key);
1978+
if let Some(field_entry) = field_entry.as_ref() {
1979+
inherited_fields
1980+
.entry(field)
1981+
.or_default()
1982+
.push((parent_cls.name(), field_entry.ty()));
1983+
}
1984+
}
1985+
}
1986+
1987+
for (field_name, class_and_types) in inherited_fields.iter() {
1988+
if class_and_types.len() > 1 {
1989+
let types: Vec<Type> = class_and_types.iter().map(|(_, ty)| ty.clone()).collect();
1990+
if types.iter().any(|ty| {
1991+
matches!(
1992+
ty,
1993+
Type::BoundMethod(..)
1994+
| Type::Function(..)
1995+
| Type::Forall(_)
1996+
| Type::Overload(_)
1997+
)
1998+
}) {
1999+
// TODO(fangyizhou): Handle bound methods and functions properly.
2000+
// This is a leftover from https://github.com/facebook/pyrefly/pull/1196
2001+
continue;
2002+
}
2003+
let intersect = self.intersects(&types);
2004+
if matches!(intersect, Type::Never(_)) {
2005+
let mut error_msg = vec1![
2006+
format!(
2007+
"Field `{field_name}` has inconsistent types inherited from multiple base classes"
2008+
),
2009+
"Inherited types include:".to_owned()
2010+
];
2011+
for (cls, ty) in class_and_types.iter() {
2012+
error_msg.push(format!(
2013+
" `{}` from `{}`",
2014+
self.for_display(ty.clone()),
2015+
cls
2016+
));
2017+
}
2018+
errors.add(
2019+
cls.range(),
2020+
ErrorInfo::Kind(ErrorKind::InconsistentInheritance),
2021+
error_msg,
2022+
);
2023+
}
2024+
}
2025+
}
2026+
}
2027+
19672028
fn get_non_synthesized_field_from_current_class_only(
19682029
&self,
19692030
cls: &Class,

pyrefly/lib/alt/narrow.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
100100
self.intersect_with_fallback(left, right, Type::never)
101101
}
102102

103-
fn intersects(&self, ts: &[Type]) -> Type {
103+
/// Calculate the intersection of a number of types
104+
pub fn intersects(&self, ts: &[Type]) -> Type {
104105
match ts {
105106
[] => Type::ClassType(self.stdlib.object().clone()),
106107
[ty] => ty.clone(),

pyrefly/lib/alt/solve.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1557,6 +1557,12 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
15571557
errors,
15581558
);
15591559
}
1560+
1561+
// If we are inheriting from multiple base types, we should
1562+
// check whether the multiple inheritance is consistent
1563+
if class_bases.as_ref().base_type_count() > 1 {
1564+
self.check_consistent_multiple_inheritance(cls, errors);
1565+
}
15601566
}
15611567
Arc::new(EmptyAnswer)
15621568
}

pyrefly/lib/alt/types/class_bases.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ impl ClassBases {
6262
pub fn is_empty(&self) -> bool {
6363
self.base_types.is_empty()
6464
}
65+
66+
pub fn base_type_count(&self) -> usize {
67+
self.base_types.len()
68+
}
6569
}
6670

6771
impl fmt::Display for ClassBases {

pyrefly/lib/alt/types/class_metadata.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ pub struct TotalOrderingMetadata {
438438
/// If a class is present in multiple places of the inheritance tree (and is
439439
/// linearizable using C3 linearization), it is possible it appears with
440440
/// different type arguments. The type arguments computed here will always be
441-
/// those coming from the instance that was selected during lineariation.
441+
/// those coming from the instance that was selected during linearization.
442442
#[derive(Clone, Debug, VisitMut, TypeEq, PartialEq, Eq)]
443443
pub enum ClassMro {
444444
Resolved(Vec<ClassType>),

pyrefly/lib/test/class_subtyping.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,66 @@ class B(type(A)): # E: Invalid expression form for base class: `type(A)`
261261
assert_type(B.x, Any)
262262
"#,
263263
);
264+
265+
testcase!(
266+
test_multiple_inheritance_incompatible_field,
267+
r#"
268+
class Foo:
269+
p: int
270+
class Bar:
271+
p: str
272+
273+
class Both(Foo, Bar): # E: Field `p` has inconsistent types inherited from multiple base classes
274+
...
275+
"#,
276+
);
277+
278+
testcase!(
279+
test_nested_multiple_inheritance_incompatible_field_without_override,
280+
r#"
281+
class A:
282+
x: int
283+
class B:
284+
x: str
285+
class C(A, B): # E: Field `x` has inconsistent types inherited from multiple base classes
286+
pass
287+
class D:
288+
x: int
289+
290+
# Here we repeat the error on E, despite the error already being reported in C.
291+
class E(C, D): # E: Field `x` has inconsistent types inherited from multiple base classes
292+
pass
293+
"#,
294+
);
295+
296+
testcase!(
297+
test_nested_multiple_inheritance_incompatible_field_with_override,
298+
r#"
299+
class A:
300+
x: int
301+
class B:
302+
x: str
303+
class C(A, B): # E: Field `x` has inconsistent types inherited from multiple base classes
304+
x: int # E: Class member `C.x` overrides parent class `B` in an inconsistent manner
305+
class D:
306+
x: int
307+
308+
# Here we still report the error on E, despite the field being overridden in C.
309+
class E(C, D): # E: Field `x` has inconsistent types inherited from multiple base classes
310+
pass
311+
"#,
312+
);
313+
314+
testcase!(
315+
bug = "This is currently not handled",
316+
test_multiple_inheritance_incompatible_methods,
317+
r#"
318+
class Foo:
319+
def foo(self) -> int: ...
320+
class Bar:
321+
def foo(self) -> str: ...
322+
323+
class Both(Foo, Bar): # Expect error here
324+
...
325+
"#,
326+
);

0 commit comments

Comments
 (0)