[mypyc] Reject unknown bases for native classes (#22204)

Fixes https://github.com/mypyc/mypyc/issues/1200.

Reject native classes with an `Any` base, since their instance layout is
not known during compilation. Suggest using
`@mypyc_attr(native_class=False)`
instead.

Preserve the original base expressions for non-native classes with
unknown bases so Python determines their inheritance and instance 
layout at runtime.
diff --git a/mypyc/doc/native_classes.rst b/mypyc/doc/native_classes.rst
index f7d9180..a97e4d3 100644
--- a/mypyc/doc/native_classes.rst
+++ b/mypyc/doc/native_classes.rst
@@ -66,6 +66,12 @@
 * ``LookupError``
 * ``UserWarning``
 
+A native class can't inherit from a base class with type ``Any``, such
+as a class imported from a module that isn't type checked, even
+indirectly through other base classes, since mypyc needs to know the
+layout of all base classes. Use a
+:ref:`non-native class <non-native-classes>` instead.
+
 By default, a non-native class can't inherit a native class, and you
 can't inherit from a native class outside the compilation unit that
 defines the class. You can enable these through
@@ -231,6 +237,8 @@
    If a class definition uses an unsupported class decorator, *mypyc
    compiles the class into a regular Python class* (non-native class).
 
+.. _non-native-classes:
+
 Defining non-native classes
 ---------------------------
 
diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py
index ae63ff9..a0aa7c4 100644
--- a/mypyc/irbuild/classdef.py
+++ b/mypyc/irbuild/classdef.py
@@ -599,14 +599,16 @@
     """Should the base classes of a non-extension class be evaluated as written?
 
     Resolved base types erase Generic[...] bases and type arguments, but they are
-    needed at runtime, e.g. to make the class generic.
+    needed at runtime, e.g. to make the class generic. Any bases are missing from
+    resolved base types, so Python must determine the bases and instance layout.
     """
     info = cdef.info
     if info.is_named_tuple or info.typeddict_type is not None:
         # These have synthesized base classes that need special casing.
         return False
     return bool(
-        cdef.type_args
+        info.fallback_to_any
+        or cdef.type_args
         or cdef.removed_base_type_exprs
         or any(isinstance(expr, IndexExpr) for expr in cdef.base_type_exprs)
     )
diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py
index 301e8cf..e358938 100644
--- a/mypyc/irbuild/prepare.py
+++ b/mypyc/irbuild/prepare.py
@@ -34,6 +34,7 @@
     OverloadedFuncDef,
     RefExpr,
     SymbolNode,
+    TypeAlias,
     TypeInfo,
     Var,
 )
@@ -541,6 +542,48 @@
                 )
 
 
+def report_needs_non_native_class(errors: Errors, msg: str, path: str, line: int) -> None:
+    errors.error(msg, path, line)
+    errors.note(
+        "Potential workaround: @mypy_extensions.mypyc_attr(native_class=False)", path, line
+    )
+    errors.note(
+        "https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes",
+        path,
+        line,
+    )
+
+
+def check_unknown_base_classes(path: str, cdef: ClassDef, errors: Errors, mapper: Mapper) -> None:
+    """Reject native classes whose layout depends on an Any base."""
+    if not cdef.info.fallback_to_any:
+        return
+
+    base_description = "a base class with an unknown layout"
+    for base_expr in cdef.base_type_exprs:
+        if isinstance(base_expr, IndexExpr):
+            base_expr = base_expr.base
+        if not isinstance(base_expr, (NameExpr, MemberExpr)):
+            continue
+        base_node = base_expr.node
+        if isinstance(base_node, TypeAlias):
+            target = get_proper_type(base_node.target)
+            if isinstance(target, Instance):
+                base_node = target.type
+        if not isinstance(base_node, TypeInfo) or base_node.fallback_to_any:
+            base_description = f'"{base_expr.name}" (unknown base class layout)'
+            break
+    msg = f"Native classes cannot inherit from {base_description}"
+    if any(
+        base.fallback_to_any and base in mapper.type_to_ir and mapper.type_to_ir[base].is_ext_class
+        for base in cdef.info.mro[1:]
+    ):
+        # A native base class already reported the error with the notes
+        errors.error(msg, path, cdef.line)
+    else:
+        report_needs_non_native_class(errors, msg, path, cdef.line)
+
+
 def prepare_class_def(
     path: str,
     module_name: str,
@@ -558,6 +601,8 @@
     ir = mapper.type_to_ir[cdef.info]
     info = cdef.info
 
+    check_unknown_base_classes(path, cdef, errors, mapper)
+
     attrs, attrs_lines = get_mypyc_attrs(cdef, path, errors)
     if attrs.get("allow_interpreted_subclasses") is True:
         ir.allow_interpreted_subclasses = True
@@ -582,18 +627,8 @@
                 # isn't in builtins, bad things will happen and we won't
                 # catch it here! But this should catch a lot of the most
                 # common pitfalls.
-                errors.error(
-                    "Inheriting from most builtin types is unimplemented", path, cdef.line
-                )
-                errors.note(
-                    "Potential workaround: @mypy_extensions.mypyc_attr(native_class=False)",
-                    path,
-                    cdef.line,
-                )
-                errors.note(
-                    "https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes",
-                    path,
-                    cdef.line,
+                report_needs_non_native_class(
+                    errors, "Inheriting from most builtin types is unimplemented", path, cdef.line
                 )
 
     free_list_len = attrs.get("free_list_len")
diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test
index 43e9c2b..7e75ddd 100644
--- a/mypyc/test-data/irbuild-classes.test
+++ b/mypyc/test-data/irbuild-classes.test
@@ -1772,6 +1772,90 @@
 class BadUse():  # E: native_class must be used with True or False only
     pass
 
+[case testNativeClassUnknownBase]
+from mypy_extensions import mypyc_attr
+from typing import Any
+from missing_module import Base  # type: ignore
+
+class C(Base):  # E: Native classes cannot inherit from "Base" (unknown base class layout) \
+                # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+class D(C):  # E: Native classes cannot inherit from "C" (unknown base class layout)
+    pass
+
+@mypyc_attr(native_class=False)
+class NonNative(Base):
+    pass
+
+class NativeChild(NonNative):  # E: Native classes cannot inherit from "NonNative" (unknown base class layout) \
+                             # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                             # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+ExplicitAny: Any = str
+class S(ExplicitAny):  # E: Native classes cannot inherit from "ExplicitAny" (unknown base class layout) \
+                      # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                      # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+class Known:
+    pass
+
+class KnownFirst(Known, Base):  # E: Native classes cannot inherit from "Base" (unknown base class layout) \
+                              # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                              # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+class UnknownFirst(Base, Known):  # E: Native classes cannot inherit from "Base" (unknown base class layout) \
+                                # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                                # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+class TwoUnknown(Base, ExplicitAny):  # E: Native classes cannot inherit from "Base" (unknown base class layout) \
+                                     # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                                     # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+Alias = Known
+class AliasFirst(Alias, Base):  # E: Native classes cannot inherit from "Base" (unknown base class layout) \
+                              # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                              # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+class Indexed(Base[int]):  # type: ignore  # E: Native classes cannot inherit from "Base" (unknown base class layout) \
+                                           # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                                           # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+def make_base() -> Any: ...
+class Dynamic(make_base()):  # type: ignore  # E: Native classes cannot inherit from a base class with an unknown layout \
+                                            # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                                            # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+[case testNativeClassIndirectUnknownBase]
+# Any base class anywhere in the MRO makes the layout unknown
+from stubbed import Indirect, IndirectAlias
+
+class C(Indirect):  # E: Native classes cannot inherit from "Indirect" (unknown base class layout) \
+                    # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                    # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+class D(IndirectAlias):  # E: Native classes cannot inherit from "IndirectAlias" (unknown base class layout) \
+                         # N: Potential workaround: @mypy_extensions.mypyc_attr(native_class=False) \
+                         # N: https://mypyc.readthedocs.io/en/stable/native_classes.html#defining-non-native-classes
+    pass
+
+[file stubbed.pyi]
+from typing import Any
+
+class AnyBase(Any): ...
+class Indirect(AnyBase): ...
+IndirectAlias = Indirect
+
 [case testMypycAttrNativeClassMetaError]
 from mypy_extensions import mypyc_attr
 
diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test
index 19a1804..2eb4dc2 100644
--- a/mypyc/test-data/run-classes.test
+++ b/mypyc/test-data/run-classes.test
@@ -1543,6 +1543,161 @@
         assert False
     assert x
 
+[case testSubclassUnknownPythonBase]
+# The base class is unknown during compilation, so it has type Any
+from mypy_extensions import mypyc_attr
+from typing import Any, Generic, TypeVar
+from unchecked import Base, MetaBase, SecondBase, StrBase  # type: ignore
+
+T = TypeVar("T")
+
+@mypyc_attr(native_class=False)
+class C(Base):
+    def __init__(self, x: int) -> None:
+        super().__init__()
+        self.x = x
+
+    def marker(self) -> str:
+        return "override " + super().marker()
+
+@mypyc_attr(native_class=False)
+class InheritedInit(Base):
+    pass
+
+@mypyc_attr(native_class=False)
+class D(InheritedInit):
+    pass
+
+@mypyc_attr(native_class=False)
+class S(StrBase):
+    pass
+
+@mypyc_attr(native_class=False)
+class WithMeta(MetaBase):
+    pass
+
+@mypyc_attr(native_class=False)
+class Known:
+    def marker(self) -> str:
+        return "known"
+
+@mypyc_attr(native_class=False)
+class KnownFirst(Known, Base):
+    pass
+
+@mypyc_attr(native_class=False)
+class UnknownFirst(Base, Known):
+    pass
+
+@mypyc_attr(native_class=False)
+class TwoUnknown(Base, SecondBase):
+    pass
+
+@mypyc_attr(native_class=False)
+class GenericLast(Base, Generic[T]):
+    pass
+
+@mypyc_attr(native_class=False)
+class GenericFirst(Generic[T], Base):
+    pass
+
+@mypyc_attr(native_class=False)
+class KnownGeneric(Generic[T]):
+    pass
+
+@mypyc_attr(native_class=False)
+class KnownGenericBase(Base, KnownGeneric[int]):
+    pass
+
+def test_unknown_base() -> None:
+    c = C(1)
+    assert c.x == 1
+    assert c.y == 2
+    assert c.label == "base attribute"
+    assert c.marker() == "override base"
+    c.z = 3
+    assert c.z == 3
+    assert c.__dict__ == {"x": 1, "y": 2, "z": 3}
+    assert isinstance(c, Base)
+
+def test_inherited_init() -> None:
+    assert InheritedInit().y == 2
+    assert InheritedInit(7).y == 7
+    assert InheritedInit(x=11).y == 11
+    d = D(13)
+    assert d.y == 13
+    assert d.label == "base attribute"
+    assert d.marker() == "base"
+    assert D(x=17).y == 17
+    try:
+        InheritedInit(-1)
+    except ValueError as e:
+        assert str(e) == "negative"
+    else:
+        assert False
+
+def test_unknown_extension_base() -> None:
+    assert S.__basicsize__ >= StrBase.__basicsize__
+    c = S("abc")
+    assert isinstance(c, str)
+    assert c == "abc"
+    assert len(c) == 3
+    assert c.upper() == "ABC"
+
+def test_unknown_base_metaclass() -> None:
+    assert isinstance(WithMeta, type(MetaBase))
+    assert isinstance(WithMeta(), MetaBase)
+
+def test_multiple_inheritance() -> None:
+    assert KnownFirst.__bases__ == (Known, Base)
+    assert UnknownFirst.__bases__ == (Base, Known)
+    assert TwoUnknown.__bases__ == (Base, SecondBase)
+    assert KnownFirst(3).y == 3
+    assert UnknownFirst(x=5).y == 5
+    assert KnownFirst().marker() == "known"
+    assert UnknownFirst().marker() == "base"
+    assert TwoUnknown(7).y == 7
+    assert TwoUnknown().other() == "second"
+
+def test_generic_bases() -> None:
+    generic: Any = globals()["Generic"]
+    type_var: Any = globals()["T"]
+    assert GenericLast.__bases__ == (Base, generic)
+    assert GenericFirst.__bases__ == (generic, Base)
+    assert GenericLast.__orig_bases__ == (Base, generic[type_var])
+    assert GenericFirst.__orig_bases__ == (generic[type_var], Base)
+    assert GenericLast.__parameters__ == (type_var,)
+    assert GenericFirst.__parameters__ == (type_var,)
+    assert GenericLast[int](3).y == 3
+    assert GenericFirst[str](5).y == 5
+    assert KnownGenericBase.__bases__ == (Base, KnownGeneric)
+    assert KnownGenericBase.__orig_bases__ == (Base, KnownGeneric[int])
+    assert KnownGenericBase(7).y == 7
+
+[file unchecked.py]
+StrBase = str
+
+class SecondBase:
+    def other(self):
+        return "second"
+
+class Meta(type):
+    pass
+
+class MetaBase(metaclass=Meta):
+    pass
+
+class Base:
+    label = "base attribute"
+
+    def __init__(self, x=2):
+        if x < 0:
+            raise ValueError("negative")
+        self.y = x
+
+    def marker(self):
+        return "base"
+
 [case testSubclassPy]
 from b import B, V
 class A(B):
@@ -2066,26 +2221,30 @@
 [out]
 ok
 
-[case testNoMetaclass]
-from foo import Base
+[case testNativeClassHiddenMetaclass]
+# The stub hides the metaclass of the base class, so it's only detected at runtime
+from hidden import Base
 
-class Nothing(Base):  # type: ignore
+class C(Base):
     pass
 
-[file foo.py]
-from typing import Any
+[file hidden.pyi]
+class Base: ...
+
+[file hidden.py]
 class Meta(type):
     pass
 
-class _Base(metaclass=Meta):
+class Base(metaclass=Meta):
     pass
-Base = _Base  # type: Any
 
 [file driver.py]
 try:
     import native
 except TypeError as e:
-    assert(str(e) == "mypyc classes can't have a metaclass")
+    assert str(e) == "mypyc classes can't have a metaclass"
+else:
+    assert False
 
 [case testMetaclass]
 from meta import Meta