diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py index 15a82b1f719c6..906a4fe46a2f7 100644 --- a/mypyc/irbuild/prepare.py +++ b/mypyc/irbuild/prepare.py @@ -26,6 +26,7 @@ Decorator, Expression, FuncDef, + IndexExpr, MemberExpr, MypyFile, NameExpr, @@ -118,6 +119,12 @@ def build_type_map( module.path, module.fullname, cdef, errors, mapper, options ) + # Validate cross-class properties after all ClassIR flags are populated. + for module, cdef in classes: + with catch_errors(module.path, cdef.line): + if mapper.type_to_ir[cdef.info].is_ext_class: + validate_acyclic_class_bases(module.path, cdef, errors, mapper) + # Prepare implicit attribute accessors as needed if an attribute overrides a property. for module, cdef in classes: class_ir = mapper.type_to_ir[cdef.info] @@ -345,6 +352,51 @@ def can_subclass_builtin(builtin_base: str) -> bool: ) +def get_removed_base_fullname(expr: Expression) -> str | None: + if isinstance(expr, IndexExpr): + expr = expr.base + if isinstance(expr, RefExpr): + return expr.fullname + return None + + +def find_non_acyclic_base(cdef: ClassDef, mapper: Mapper) -> str | None: + if cdef.type_args: + return "typing.Generic" + + for expr in cdef.removed_base_type_exprs: + if fullname := get_removed_base_fullname(expr): + return fullname + return "a removed base class" + + for base in cdef.info.mro[1:]: + if base.fullname == "builtins.object": + continue + + base_ir = mapper.type_to_ir.get(base) + if base_ir is not None and base_ir.is_acyclic: + continue + + return base.fullname + + return None + + +def validate_acyclic_class_bases( + path: str, cdef: ClassDef, errors: Errors, mapper: Mapper +) -> None: + ir = mapper.type_to_ir[cdef.info] + if not ir.is_acyclic: + return + + if fullname := find_non_acyclic_base(cdef, mapper): + errors.error( + f'"acyclic" can\'t be used in a class that inherits from non-acyclic type "{fullname}"', + path, + cdef.line, + ) + + def prepare_class_def( path: str, module_name: str, diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 22b8d888c168a..2f5ecfd751040 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -2138,6 +2138,47 @@ class NonNative: class InterpSub: pass +[case testAcyclicClassRequiresAcyclicBases] +from typing import Generic, TypeVar +from mypy_extensions import mypyc_attr, trait + +T = TypeVar("T") + +class NonAcyclicBase: + pass + +@trait +class TraitBase: + pass + +@mypyc_attr(native_class=False) +class NonNativeBase: + pass + +@mypyc_attr(acyclic=True) +class AcyclicBase: + pass + +@mypyc_attr(acyclic=True) +class GoodDerived(AcyclicBase): + pass + +@mypyc_attr(acyclic=True) +class BadDerived(NonAcyclicBase): # E: "acyclic" can't be used in a class that inherits from non-acyclic type "__main__.NonAcyclicBase" + pass + +@mypyc_attr(acyclic=True) +class BadGeneric(Generic[T]): # E: "acyclic" can't be used in a class that inherits from non-acyclic type "typing.Generic" + pass + +@mypyc_attr(acyclic=True) +class BadDerivedTrait(TraitBase): # E: "acyclic" can't be used in a class that inherits from non-acyclic type "__main__.TraitBase" + pass + +@mypyc_attr(acyclic=True) +class BadDerivedNonNative(NonNativeBase): # E: "acyclic" can't be used in a class that inherits from non-acyclic type "__main__.NonNativeBase" + pass + [case testUnsupportedGetAttr] from mypy_extensions import mypyc_attr diff --git a/mypyc/test-data/irbuild-python312.test b/mypyc/test-data/irbuild-python312.test new file mode 100644 index 0000000000000..12f3bfae687c6 --- /dev/null +++ b/mypyc/test-data/irbuild-python312.test @@ -0,0 +1,6 @@ +[case testAcyclicGenericIsRejected_python3_12] +from mypy_extensions import mypyc_attr + +@mypyc_attr(acyclic=True) +class BadGeneric[T]: # E: "acyclic" can't be used in a class that inherits from non-acyclic type "typing.Generic" + pass diff --git a/mypyc/test/test_irbuild.py b/mypyc/test/test_irbuild.py index 6fa7d3979d2ed..f1f0ec777c3da 100644 --- a/mypyc/test/test_irbuild.py +++ b/mypyc/test/test_irbuild.py @@ -64,6 +64,9 @@ "irbuild-match.test", ] +if sys.version_info >= (3, 12): + files.append("irbuild-python312.test") + if sys.version_info >= (3, 14): files.append("irbuild-python314.test")