diff --git a/.github/mypy-primer-ty.toml b/.github/mypy-primer-ty.toml
index c0bcd88695..7df65b2994 100644
--- a/.github/mypy-primer-ty.toml
+++ b/.github/mypy-primer-ty.toml
@@ -6,3 +6,4 @@
possibly-unresolved-reference = "warn"
possibly-missing-import = "warn"
division-by-zero = "warn"
+unsupported-dynamic-base = "warn"
diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 4a15c1c29b..d1173e0013 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -8,7 +8,7 @@
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -80,7 +80,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.7 ·
Related issues ·
-View source
+View source
@@ -135,7 +135,7 @@ def f(x: object):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -167,7 +167,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -198,7 +198,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -230,7 +230,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -262,7 +262,7 @@ class B(A): ...
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -290,7 +290,7 @@ type B = A
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -346,7 +346,7 @@ false positives it can produce.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -373,7 +373,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -529,7 +529,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -559,7 +559,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -585,7 +585,7 @@ t[3] # IndexError: tuple index out of range
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -674,7 +674,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -701,7 +701,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -729,7 +729,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -763,7 +763,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -799,7 +799,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -823,7 +823,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -850,7 +850,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -879,7 +879,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -923,7 +923,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -965,7 +965,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.35 ·
Related issues ·
-View source
+View source
@@ -1009,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1077,7 +1077,7 @@ a = 20 / 0 # type: ignore
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -1116,7 +1116,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1151,7 +1151,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1185,7 +1185,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1292,7 +1292,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1346,7 +1346,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -1376,7 +1376,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1426,7 +1426,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1452,7 +1452,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1483,7 +1483,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1517,7 +1517,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1566,7 +1566,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1591,7 +1591,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1687,7 +1687,7 @@ class C: ...
Default level: error ·
Added in 0.0.10 ·
Related issues ·
-View source
+View source
@@ -1733,7 +1733,7 @@ class MyClass:
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1760,7 +1760,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -1807,7 +1807,7 @@ Bar[int] # error: too few arguments
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1837,7 +1837,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1867,7 +1867,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1901,7 +1901,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1935,7 +1935,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1970,7 +1970,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.9 ·
Related issues ·
-View source
+View source
@@ -2001,7 +2001,7 @@ class Foo(TypedDict):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2026,7 +2026,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -2059,7 +2059,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2088,7 +2088,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2114,7 +2114,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2138,7 +2138,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -2171,7 +2171,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2198,7 +2198,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2225,7 +2225,7 @@ f(x=1) # Error raised here
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2253,7 +2253,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2285,7 +2285,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: ignore ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2322,7 +2322,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2386,7 +2386,7 @@ def test(): -> "int":
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2413,7 +2413,7 @@ cast(int, f()) # Redundant
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2443,7 +2443,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2472,7 +2472,7 @@ class B(A): ... # Error raised here
Default level: error ·
Preview (since 0.0.1-alpha.30) ·
Related issues ·
-View source
+View source
@@ -2506,7 +2506,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2533,7 +2533,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2561,7 +2561,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2607,7 +2607,7 @@ class A:
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2631,7 +2631,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2658,7 +2658,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2686,7 +2686,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2744,7 +2744,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2769,7 +2769,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2794,7 +2794,7 @@ print(x) # NameError: name 'x' is not defined
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2833,7 +2833,7 @@ class D(C): ... # error: [unsupported-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2864,13 +2864,54 @@ not b1 # exception raised here
b1 < b2 < b1 # exception raised here
```
+## `unsupported-dynamic-base`
+
+
+Default level: ignore ·
+Preview (since 1.0.0) ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Checks for dynamic class definitions (using `type()`) that have bases
+which are unsupported by ty.
+
+This is equivalent to [`unsupported-base`] but applies to classes created
+via `type()` rather than `class` statements.
+
+**Why is this bad?**
+
+If a dynamically created class has a base that is an unsupported type
+such as `type[T]`, ty will not be able to resolve the
+[method resolution order] (MRO) for the class. This may lead to an inferior
+understanding of your codebase and unpredictable type-checking behavior.
+
+**Default level**
+
+This rule is disabled by default because it will not cause a runtime error,
+and may be noisy on codebases that use `type()` in highly dynamic ways.
+
+**Examples**
+
+```python
+def factory(base: type[Base]) -> type:
+ # `base` has type `type[Base]`, not `type[Base]` itself
+ return type("Dynamic", (base,), {}) # error: [unsupported-dynamic-base]
+```
+
+[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
+[`unsupported-base`]: https://docs.astral.sh/ty/rules/unsupported-base
+
## `unsupported-operator`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2934,7 +2975,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments.
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2997,7 +3038,7 @@ def foo(x: int | str) -> int | str:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs
index b0c538c820..1cb7778e96 100644
--- a/crates/ty_ide/src/goto.rs
+++ b/crates/ty_ide/src/goto.rs
@@ -231,7 +231,8 @@ impl<'db> Definitions<'db> {
ty_python_semantic::types::TypeDefinition::Module(module) => {
ResolvedDefinition::Module(module.file(db)?)
}
- ty_python_semantic::types::TypeDefinition::Class(definition)
+ ty_python_semantic::types::TypeDefinition::StaticClass(definition)
+ | ty_python_semantic::types::TypeDefinition::DynamicClass(definition)
| ty_python_semantic::types::TypeDefinition::Function(definition)
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md
index f9342151ea..5d783a93d3 100644
--- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md
+++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md
@@ -13,54 +13,6 @@ bool(1, 2)
bool(NotBool())
```
-## Calls to `type()`
-
-A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
-tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the
-tests for the `__class__` attribute.)
-
-```py
-reveal_type(type(1)) # revealed:
-```
-
-But a three-argument call to type creates a dynamic instance of the `type` class:
-
-```py
-class Base: ...
-
-reveal_type(type("Foo", (), {})) # revealed: type
-
-reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
-```
-
-Other numbers of arguments are invalid
-
-```py
-# error: [no-matching-overload] "No overload of class `type` matches arguments"
-type("Foo", ())
-
-# error: [no-matching-overload] "No overload of class `type` matches arguments"
-type("Foo", (), {}, weird_other_arg=42)
-```
-
-The following calls are also invalid, due to incorrect argument types:
-
-```py
-class Base: ...
-
-# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
-type(b"Foo", (), {})
-
-# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found ``"
-type("Foo", Base, {})
-
-# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
-type("Foo", (1, 2), {})
-
-# error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
-type("Foo", (Base,), {b"attr": 1})
-```
-
## Calls to `str()`
### Valid calls
diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md
new file mode 100644
index 0000000000..8769f15b8c
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/call/type.md
@@ -0,0 +1,815 @@
+# Calls to `type()`
+
+## Single-argument form
+
+A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
+tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the
+tests for the `__class__` attribute.)
+
+```py
+reveal_type(type(1)) # revealed:
+```
+
+## Three-argument form (dynamic class creation)
+
+A three-argument call to `type()` creates a new class. We synthesize a class type using the name
+from the first argument:
+
+```py
+class Base: ...
+class Mixin: ...
+
+# We synthesize a class type using the name argument
+Foo = type("Foo", (), {})
+reveal_type(Foo) # revealed:
+
+# With a single base class
+Foo2 = type("Foo", (Base,), {"attr": 1})
+reveal_type(Foo2) # revealed:
+
+# With multiple base classes
+Foo3 = type("Foo", (Base, Mixin), {})
+reveal_type(Foo3) # revealed:
+
+# The inferred type is assignable to type[Base] since Foo inherits from Base
+tests: list[type[Base]] = []
+testCaseClass = type("Foo", (Base,), {})
+tests.append(testCaseClass) # No error - type[Foo] is assignable to type[Base]
+```
+
+The name can also be provided indirectly via a variable with a string literal type:
+
+```py
+name = "IndirectClass"
+IndirectClass = type(name, (), {})
+reveal_type(IndirectClass) # revealed:
+
+# Works with base classes too
+class Base: ...
+
+base_name = "DerivedClass"
+DerivedClass = type(base_name, (Base,), {})
+reveal_type(DerivedClass) # revealed:
+```
+
+## Distinct class types
+
+Each `type()` call produces a distinct class type, even if they have the same name and bases:
+
+```py
+from ty_extensions import static_assert, is_equivalent_to
+
+class Base: ...
+
+Foo1 = type("Foo", (Base,), {})
+Foo2 = type("Foo", (Base,), {})
+
+# Even though they have the same name and bases, they are distinct types
+static_assert(not is_equivalent_to(Foo1, Foo2))
+
+# Each instance is typed with its respective class
+foo1 = Foo1()
+foo2 = Foo2()
+
+def takes_foo1(x: Foo1) -> None: ...
+def takes_foo2(x: Foo2) -> None: ...
+
+takes_foo1(foo1) # OK
+takes_foo2(foo2) # OK
+
+# error: [invalid-argument-type] "Argument to function `takes_foo1` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`"
+takes_foo1(foo2)
+# error: [invalid-argument-type] "Argument to function `takes_foo2` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`"
+takes_foo2(foo1)
+```
+
+## Instances and attribute access
+
+Instances of dynamic classes are typed with the synthesized class name. Attributes from all base
+classes are accessible:
+
+```py
+class Base:
+ base_attr: int = 1
+
+ def base_method(self) -> str:
+ return "hello"
+
+class Mixin:
+ mixin_attr: str = "mixin"
+
+Foo = type("Foo", (Base,), {})
+foo = Foo()
+
+# Instance is typed with the synthesized class name
+reveal_type(foo) # revealed: Foo
+
+# Inherited attributes are accessible
+reveal_type(foo.base_attr) # revealed: int
+reveal_type(foo.base_method()) # revealed: str
+
+# Multiple inheritance: attributes from all bases are accessible
+Bar = type("Bar", (Base, Mixin), {})
+bar = Bar()
+reveal_type(bar.base_attr) # revealed: int
+reveal_type(bar.mixin_attr) # revealed: str
+```
+
+Attributes from the namespace dict (third argument) are not tracked. Like Pyright, we error when
+attempting to access them:
+
+```py
+class Base: ...
+
+Foo = type("Foo", (Base,), {"custom_attr": 42})
+foo = Foo()
+
+# error: [unresolved-attribute] "Object of type `Foo` has no attribute `custom_attr`"
+reveal_type(foo.custom_attr) # revealed: Unknown
+```
+
+## Inheritance from dynamic classes
+
+Regular classes can inherit from dynamic classes:
+
+```py
+class Base:
+ base_attr: int = 1
+
+DynamicClass = type("DynamicClass", (Base,), {})
+
+class Child(DynamicClass):
+ child_attr: str = "child"
+
+child = Child()
+
+# Attributes from the dynamic class's base are accessible
+reveal_type(child.base_attr) # revealed: int
+
+# The child class's own attributes are accessible
+reveal_type(child.child_attr) # revealed: str
+
+# Child instances are subtypes of DynamicClass instances
+def takes_dynamic(x: DynamicClass) -> None: ...
+
+takes_dynamic(child) # No error - Child is a subtype of DynamicClass
+
+# isinstance narrows to the dynamic class instance type
+def check_isinstance(x: object) -> None:
+ if isinstance(x, DynamicClass):
+ reveal_type(x) # revealed: DynamicClass
+
+# Dynamic class inheriting from int narrows correctly with isinstance
+IntSubclass = type("IntSubclass", (int,), {})
+
+def check_int_subclass(x: IntSubclass | str) -> None:
+ if isinstance(x, int):
+ # IntSubclass inherits from int, so it's included in the narrowed type
+ reveal_type(x) # revealed: IntSubclass
+ else:
+ reveal_type(x) # revealed: str
+```
+
+## Disjointness
+
+Dynamic classes are not considered disjoint from unrelated types (since a subclass could inherit
+from both):
+
+```py
+class Base: ...
+
+Foo = type("Foo", (Base,), {})
+
+def check_disjointness(x: Foo | int) -> None:
+ if isinstance(x, int):
+ reveal_type(x) # revealed: int
+ else:
+ # Foo and int are not considered disjoint because `class C(Foo, int)` could exist.
+ reveal_type(x) # revealed: Foo & ~int
+```
+
+Disjointness also works for `type[]` of dynamic classes:
+
+```py
+from ty_extensions import is_disjoint_from, static_assert
+
+# Dynamic classes with disjoint bases have disjoint type[] types.
+IntClass = type("IntClass", (int,), {})
+StrClass = type("StrClass", (str,), {})
+
+static_assert(is_disjoint_from(type[IntClass], type[StrClass]))
+static_assert(is_disjoint_from(type[StrClass], type[IntClass]))
+
+# Dynamic classes that share a common base are not disjoint.
+class Base: ...
+
+Foo = type("Foo", (Base,), {})
+Bar = type("Bar", (Base,), {})
+
+static_assert(not is_disjoint_from(type[Foo], type[Bar]))
+```
+
+## Using dynamic classes with `super()`
+
+Dynamic classes can be used as the pivot class in `super()` calls:
+
+```py
+class Base:
+ def method(self) -> int:
+ return 42
+
+DynamicChild = type("DynamicChild", (Base,), {})
+
+# Using dynamic class as pivot with dynamic class instance owner
+fc = DynamicChild()
+reveal_type(super(DynamicChild, fc)) # revealed: , DynamicChild>
+reveal_type(super(DynamicChild, fc).method()) # revealed: int
+
+# Regular class inheriting from dynamic class
+class RegularChild(DynamicChild):
+ pass
+
+rc = RegularChild()
+reveal_type(super(RegularChild, rc)) # revealed: , RegularChild>
+reveal_type(super(RegularChild, rc).method()) # revealed: int
+
+# Using dynamic class as pivot with regular class instance owner
+reveal_type(super(DynamicChild, rc)) # revealed: , RegularChild>
+reveal_type(super(DynamicChild, rc).method()) # revealed: int
+```
+
+## Dynamic class inheritance chains
+
+Dynamic classes can inherit from other dynamic classes:
+
+```py
+class Base:
+ base_attr: int = 1
+
+# Create a dynamic class that inherits from a regular class.
+Parent = type("Parent", (Base,), {})
+reveal_type(Parent) # revealed:
+
+# Create a dynamic class that inherits from another dynamic class.
+ChildCls = type("ChildCls", (Parent,), {})
+reveal_type(ChildCls) # revealed:
+
+# Child instances have access to attributes from the entire inheritance chain.
+child = ChildCls()
+reveal_type(child) # revealed: ChildCls
+reveal_type(child.base_attr) # revealed: int
+
+# Child instances are subtypes of `Parent` instances.
+def takes_parent(x: Parent) -> None: ...
+
+takes_parent(child) # No error - `ChildCls` is a subtype of `Parent`
+```
+
+## Dataclass transform inheritance
+
+Dynamic classes that inherit from a `@dataclass_transform()` decorated base class are recognized as
+dataclass-like and have the synthesized `__dataclass_fields__` attribute:
+
+```py
+from dataclasses import Field
+from typing_extensions import dataclass_transform
+
+@dataclass_transform()
+class DataclassBase:
+ """Base class decorated with @dataclass_transform()."""
+
+ pass
+
+# A dynamic class inheriting from a dataclass_transform base
+DynamicModel = type("DynamicModel", (DataclassBase,), {})
+
+# The dynamic class has __dataclass_fields__ synthesized
+reveal_type(DynamicModel.__dataclass_fields__) # revealed: dict[str, Field[Any]]
+```
+
+## Applying `@dataclass` decorator directly
+
+Applying the `@dataclass` decorator directly to a dynamic class is supported:
+
+```py
+from dataclasses import dataclass
+
+Foo = type("Foo", (), {})
+Foo = dataclass(Foo)
+
+reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
+```
+
+## Generic base classes
+
+Dynamic classes with generic base classes:
+
+```py
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+class Container(Generic[T]):
+ value: T
+
+# Dynamic class inheriting from a generic class specialization
+IntContainer = type("IntContainer", (Container[int],), {})
+reveal_type(IntContainer) # revealed:
+
+container = IntContainer()
+reveal_type(container) # revealed: IntContainer
+reveal_type(container.value) # revealed: int
+```
+
+## `type()` and `__class__` on dynamic instances
+
+`type(instance)` returns the class of the dynamic instance:
+
+```py
+class Base: ...
+
+Foo = type("Foo", (Base,), {})
+foo = Foo()
+
+# type() on an instance returns the class
+reveal_type(type(foo)) # revealed: type[Foo]
+```
+
+`__class__` attribute access on dynamic instances:
+
+```py
+class Base: ...
+
+Foo = type("Foo", (Base,), {})
+foo = Foo()
+
+# __class__ returns the class type
+reveal_type(foo.__class__) # revealed: type[Foo]
+```
+
+`__class__` on the dynamic class itself returns the metaclass (consistent with static classes):
+
+```py
+class StaticClass: ...
+
+DynamicClass = type("DynamicClass", (), {})
+
+# Both static and dynamic classes have `type` as their metaclass
+reveal_type(StaticClass.__class__) # revealed:
+reveal_type(DynamicClass.__class__) # revealed:
+```
+
+## Subtype relationships
+
+Dynamic instances are subtypes of `object`:
+
+```py
+class Base: ...
+
+Foo = type("Foo", (Base,), {})
+foo = Foo()
+
+# All dynamic instances are subtypes of object
+def takes_object(x: object) -> None: ...
+
+takes_object(foo) # No error - Foo is a subtype of object
+
+# Even dynamic classes with no explicit bases are subtypes of object
+EmptyBases = type("EmptyBases", (), {})
+empty = EmptyBases()
+takes_object(empty) # No error
+```
+
+## Attributes from `builtins.type`
+
+Attributes defined on `builtins.type` are accessible on dynamic classes:
+
+```py
+T = type("T", (), {})
+
+# Inherited from `builtins.type`:
+reveal_type(T.__dictoffset__) # revealed: int
+reveal_type(T.__name__) # revealed: str
+reveal_type(T.__bases__) # revealed: tuple[type, ...]
+reveal_type(T.__mro__) # revealed: tuple[type, ...]
+```
+
+## Invalid calls
+
+Other numbers of arguments are invalid:
+
+```py
+# error: [no-matching-overload] "No overload of class `type` matches arguments"
+reveal_type(type("Foo", ())) # revealed: Unknown
+
+# TODO: the keyword arguments for `Foo`/`Bar`/`Baz` here are invalid
+# (you cannot pass `metaclass=` to `type()`, and none of them have
+# base classes with `__init_subclass__` methods),
+# but `type[Unknown]` would be better than `Unknown` here
+#
+# error: [no-matching-overload] "No overload of class `type` matches arguments"
+reveal_type(type("Foo", (), {}, weird_other_arg=42)) # revealed: Unknown
+# error: [no-matching-overload] "No overload of class `type` matches arguments"
+reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown
+# error: [no-matching-overload] "No overload of class `type` matches arguments"
+reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown
+```
+
+The following calls are also invalid, due to incorrect argument types.
+
+Inline calls (not assigned to a variable) fall back to regular `type` overload matching, which
+produces slightly different error messages than assigned dynamic class creation:
+
+```py
+class Base: ...
+
+# error: 6 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
+type(b"Foo", (), {})
+
+# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found ``"
+type("Foo", Base, {})
+
+# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
+type("Foo", (1, 2), {})
+
+# error: 22 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
+type("Foo", (Base,), {b"attr": 1})
+```
+
+## `type[...]` as base class
+
+`type[...]` (SubclassOf) types cannot be used as base classes. When a `type[...]` is used in the
+bases tuple, we emit a diagnostic and insert `Unknown` into the MRO. This gives exactly one
+diagnostic about the unsupported base, rather than cascading errors:
+
+```py
+from ty_extensions import reveal_mro
+
+class Base:
+ base_attr: int = 1
+
+def f(x: type[Base]):
+ # error: [unsupported-dynamic-base] "Unsupported class base"
+ Child = type("Child", (x,), {})
+
+ # The class is still created with `Unknown` in MRO, allowing attribute access
+ reveal_type(Child) # revealed:
+ reveal_mro(Child) # revealed: (, Unknown, )
+ child = Child()
+ reveal_type(child) # revealed: Child
+
+ # Attributes from `Unknown` are accessible without further errors
+ reveal_type(child.base_attr) # revealed: Unknown
+```
+
+## MRO errors
+
+MRO errors are detected and reported:
+
+```py
+class A: ...
+
+# Duplicate bases are detected
+# error: [duplicate-base] "Duplicate base class in class `Dup`"
+Dup = type("Dup", (A, A), {})
+```
+
+Unknown bases (from unresolved imports) don't trigger duplicate-base diagnostics, since we can't
+know if they represent the same type:
+
+```py
+from unresolved_module import Bar, Baz # error: [unresolved-import]
+
+# No duplicate-base error here - Bar and Baz are Unknown, and we can't
+# know if they're the same type.
+X = type("X", (Bar, Baz), {})
+```
+
+```py
+class A: ...
+class B(A): ...
+class C(A): ...
+
+# This creates an inconsistent MRO because D would need B before C (from first base)
+# but also C before B (from second base inheritance through A)
+class X(B, C): ...
+class Y(C, B): ...
+
+# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Conflict` with bases `[, ]`"
+Conflict = type("Conflict", (X, Y), {})
+```
+
+## Metaclass conflicts
+
+Metaclass conflicts are detected and reported:
+
+```py
+class Meta1(type): ...
+class Meta2(type): ...
+class A(metaclass=Meta1): ...
+class B(metaclass=Meta2): ...
+
+# error: [conflicting-metaclass] "The metaclass of a derived class (`Bad`) must be a subclass of the metaclasses of all its bases, but `Meta1` (metaclass of base class ``) and `Meta2` (metaclass of base class ``) have no subclass relationship"
+Bad = type("Bad", (A, B), {})
+```
+
+## Cyclic dynamic class definitions
+
+Self-referential class definitions using `type()` are detected. The name being defined is referenced
+in the bases tuple before it's available:
+
+```pyi
+# error: [unresolved-reference] "Name `X` used when not defined"
+X = type("X", (X,), {})
+```
+
+## Dynamic class names (non-literal strings)
+
+When the class name is not a string literal, we still create a class literal type but with a
+placeholder name ``:
+
+```py
+def make_class(name: str):
+ # When the name is a dynamic string, we use a placeholder name
+ cls = type(name, (), {})
+ reveal_type(cls) # revealed: '>
+ return cls
+
+def make_classes(name1: str, name2: str):
+ cls1 = type(name1, (), {})
+ cls2 = type(name2, (), {})
+
+ def inner(x: cls1): ...
+
+ # error: [invalid-argument-type] "Argument to function `inner` is incorrect: Expected `mdtest_snippet.. @ src/mdtest_snippet.py:8`, found `mdtest_snippet.. @ src/mdtest_snippet.py:9`"
+ inner(cls2())
+```
+
+When the name comes from a union of string literals, we also use a placeholder name:
+
+```py
+import random
+
+name = "Foo" if random.random() > 0.5 else "Bar"
+reveal_type(name) # revealed: Literal["Foo", "Bar"]
+
+# We cannot determine which name will be used at runtime
+cls = type(name, (), {})
+reveal_type(cls) # revealed: '>
+```
+
+## Dynamic bases (variable tuple)
+
+When the bases tuple is a function parameter with a non-literal tuple type, we still create a class
+literal type but with `Unknown` in the MRO. This means instances are treated highly dynamically -
+any attribute access returns `Unknown`:
+
+```py
+from ty_extensions import reveal_mro
+
+class Base1: ...
+class Base2: ...
+
+def make_class(bases: tuple[type, ...]):
+ # Class literal is created with Unknown base in MRO
+ cls = type("Cls", bases, {})
+ reveal_type(cls) # revealed:
+ reveal_mro(cls) # revealed: (, Unknown, )
+
+ # Instances have dynamic attribute access due to Unknown base
+ instance = cls()
+ reveal_type(instance) # revealed: Cls
+ reveal_type(instance.any_attr) # revealed: Unknown
+ reveal_type(instance.any_method()) # revealed: Unknown
+
+ return cls
+```
+
+When `bases` is a module-level variable holding a tuple of class literals, we can extract the base
+classes:
+
+```py
+class Base:
+ attr: int = 1
+
+bases = (Base,)
+Cls = type("Cls", bases, {})
+reveal_type(Cls) # revealed:
+
+instance = Cls()
+reveal_type(instance.attr) # revealed: int
+```
+
+## Variadic arguments
+
+Unpacking arguments with `*args` or `**kwargs`:
+
+```py
+from ty_extensions import reveal_mro
+
+class Base: ...
+
+# Unpacking a tuple for bases
+bases_tuple = (Base,)
+Cls1 = type("Cls1", (*bases_tuple,), {})
+reveal_type(Cls1) # revealed:
+reveal_mro(Cls1) # revealed: (, @Todo(StarredExpression), )
+
+# Unpacking a dict for the namespace - the dict contents are not tracked anyway
+namespace = {"attr": 1}
+Cls2 = type("Cls2", (Base,), {**namespace})
+reveal_type(Cls2) # revealed:
+```
+
+When `*args` or `**kwargs` fill an unknown number of parameters, we cannot determine which overload
+of `type()` is being called:
+
+```py
+def f(*args, **kwargs):
+ # Completely dynamic: could be 1-arg or 3-arg form
+ A = type(*args, **kwargs)
+ reveal_type(A) # revealed: type[Unknown]
+
+ # Has a string first arg, but unknown additional args from *args
+ B = type("B", *args, **kwargs)
+ # TODO: `type[Unknown]` would cause fewer false positives
+ reveal_type(B) # revealed:
+
+ # Has string and tuple, but unknown additional args
+ C = type("C", (), *args, **kwargs)
+ # TODO: `type[Unknown]` would cause fewer false positives
+ reveal_type(C) # revealed: type
+
+ # All three positional args provided, only **kwargs unknown
+ D = type("D", (), {}, **kwargs)
+ # TODO: `type[Unknown]` would cause fewer false positives
+ reveal_type(D) # revealed: type
+
+ # Three starred expressions - we can't know how they expand
+ a = ("E",)
+ b = ((),)
+ c = ({},)
+ E = type(*a, *b, *c)
+ # TODO: `type[Unknown]` would cause fewer false positives
+ reveal_type(E) # revealed: type
+```
+
+## Explicit type annotations
+
+TODO: Annotated assignments with `type()` calls don't currently synthesize the specific class type.
+This will be fixed when we support all `type()` calls (including inline) via generic handling.
+
+```py
+class Base: ...
+
+# TODO: Should infer `` instead of `type`
+T: type = type("T", (), {})
+reveal_type(T) # revealed: type
+
+# TODO: Should infer `` instead of `type[Base]}
+# error: [invalid-assignment] "Object of type `type` is not assignable to `type[Base]`"
+Derived: type[Base] = type("Derived", (Base,), {})
+reveal_type(Derived) # revealed: type[Base]
+```
+
+## Special base classes
+
+Some special base classes work with dynamic class creation, but special semantics may not be fully
+synthesized:
+
+### Protocol bases
+
+```py
+# Protocol bases work - the class is created as a subclass of the protocol
+from typing import Protocol
+from ty_extensions import reveal_mro
+
+class MyProtocol(Protocol):
+ def method(self) -> int: ...
+
+ProtoImpl = type("ProtoImpl", (MyProtocol,), {})
+reveal_type(ProtoImpl) # revealed:
+reveal_mro(ProtoImpl) # revealed: (, , typing.Protocol, typing.Generic, )
+
+instance = ProtoImpl()
+reveal_type(instance) # revealed: ProtoImpl
+```
+
+### TypedDict bases
+
+```py
+# TypedDict bases work but TypedDict semantics aren't applied to dynamic subclasses
+from typing_extensions import TypedDict
+from ty_extensions import reveal_mro
+
+class MyDict(TypedDict):
+ name: str
+ age: int
+
+DictSubclass = type("DictSubclass", (MyDict,), {})
+reveal_type(DictSubclass) # revealed:
+reveal_mro(DictSubclass) # revealed: (, , typing.TypedDict, )
+```
+
+### NamedTuple bases
+
+```py
+# NamedTuple bases work but the dynamic subclass isn't recognized as a NamedTuple
+from typing import NamedTuple
+from ty_extensions import reveal_mro
+
+class Point(NamedTuple):
+ x: int
+ y: int
+
+Point3D = type("Point3D", (Point,), {})
+reveal_type(Point3D) # revealed:
+# fmt: off
+reveal_mro(Point3D) # revealed: (, , , , , , , , typing.Protocol, typing.Generic, )
+# fmt: on
+```
+
+### Enum bases
+
+```py
+# Enum subclassing via type() is not supported - EnumMeta requires special dict handling
+# that type() cannot provide. This applies even to empty enums.
+from enum import Enum
+
+class Color(Enum):
+ RED = 1
+ GREEN = 2
+
+class EmptyEnum(Enum):
+ pass
+
+# TODO: We should emit a diagnostic here - type() cannot create Enum subclasses
+ExtendedColor = type("ExtendedColor", (Color,), {})
+reveal_type(ExtendedColor) # revealed:
+
+# Even empty enums fail - EnumMeta requires special dict handling
+# TODO: We should emit a diagnostic here too
+ValidExtension = type("ValidExtension", (EmptyEnum,), {})
+reveal_type(ValidExtension) # revealed:
+```
+
+## `__init_subclass__` keyword arguments
+
+When a base class defines `__init_subclass__` with required arguments, those should be passed to
+`type()`. This is not yet supported:
+
+```py
+class Base:
+ def __init_subclass__(cls, required_arg: str, **kwargs):
+ super().__init_subclass__(**kwargs)
+ cls.config = required_arg
+
+# Regular class definition - this works and passes the argument
+class Child(Base, required_arg="value"):
+ pass
+
+# The dynamically assigned attribute has Unknown in its type
+reveal_type(Child.config) # revealed: Unknown | str
+
+# Dynamic class creation - keyword arguments are not yet supported
+# TODO: This should work: type("DynamicChild", (Base,), {}, required_arg="value")
+# error: [no-matching-overload]
+DynamicChild = type("DynamicChild", (Base,), {}, required_arg="value")
+```
+
+## Empty bases tuple
+
+When the bases tuple is empty, the class implicitly inherits from `object`:
+
+```py
+from ty_extensions import reveal_mro
+
+EmptyBases = type("EmptyBases", (), {})
+reveal_type(EmptyBases) # revealed:
+reveal_mro(EmptyBases) # revealed: (, )
+
+instance = EmptyBases()
+reveal_type(instance) # revealed: EmptyBases
+
+# object methods are available
+reveal_type(instance.__hash__()) # revealed: int
+reveal_type(instance.__str__()) # revealed: str
+```
+
+## Custom metaclass via bases
+
+When a base class has a custom metaclass, the dynamic class inherits that metaclass:
+
+```py
+class MyMeta(type):
+ custom_attr: str = "meta"
+
+class Base(metaclass=MyMeta): ...
+
+# Dynamic class inherits the metaclass from Base
+Dynamic = type("Dynamic", (Base,), {})
+reveal_type(Dynamic) # revealed:
+
+# Metaclass attributes are accessible on the class
+reveal_type(Dynamic.custom_attr) # revealed: str
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
index 5ad4ce1ddd..f8fe27ebdf 100644
--- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
+++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
@@ -1686,3 +1686,38 @@ reveal_type(ordered_foo) # revealed:
reveal_type(ordered_foo()) # revealed: Foo
reveal_type(ordered_foo() < ordered_foo()) # revealed: bool
```
+
+## Dynamic class literals
+
+Dynamic classes created with `type()` can be wrapped with `dataclass()` as a function:
+
+```py
+from dataclasses import dataclass
+
+# Basic dynamic class wrapped with dataclass
+DynamicFoo = type("DynamicFoo", (), {})
+DynamicFoo = dataclass(DynamicFoo)
+
+# The class is recognized as a dataclass
+reveal_type(DynamicFoo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
+
+# Can create instances
+instance = DynamicFoo()
+reveal_type(instance) # revealed: DynamicFoo
+```
+
+Dynamic classes that inherit from a dataclass base also work:
+
+```py
+from dataclasses import dataclass
+
+@dataclass
+class Base:
+ x: int
+
+# Dynamic class inheriting from a dataclass
+DynamicChild = type("DynamicChild", (Base,), {})
+DynamicChild = dataclass(DynamicChild)
+
+reveal_type(DynamicChild.__dataclass_fields__) # revealed: dict[str, Field[Any]]
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md
index d8aec60bd2..2d9734455b 100644
--- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md
+++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md
@@ -870,6 +870,102 @@ static_assert(not has_member(F, "__match_args__"))
static_assert(not has_member(F(), "__weakref__"))
```
+### Dynamic classes (created via `type()`)
+
+Dynamic classes created using the three-argument form of `type()` support autocomplete for members
+inherited from their base classes on the class object:
+
+```py
+from ty_extensions import has_member, static_assert
+
+class Base:
+ base_attr: int = 1
+
+ def base_method(self) -> str:
+ return "hello"
+
+class Mixin:
+ mixin_attr: str = "mixin"
+
+# Dynamic class with a single base
+DynamicSingle = type("DynamicSingle", (Base,), {})
+
+# The class object has access to base class attributes
+static_assert(has_member(DynamicSingle, "base_attr"))
+static_assert(has_member(DynamicSingle, "base_method"))
+
+# Dynamic class with multiple bases
+DynamicMulti = type("DynamicMulti", (Base, Mixin), {})
+
+static_assert(has_member(DynamicMulti, "base_attr"))
+static_assert(has_member(DynamicMulti, "mixin_attr"))
+```
+
+Members from `object` and the `type` metaclass are available on the class object:
+
+```py
+from ty_extensions import has_member, static_assert
+
+Dynamic = type("Dynamic", (), {})
+
+# object members are available on the class
+static_assert(has_member(Dynamic, "__doc__"))
+static_assert(has_member(Dynamic, "__init__"))
+
+# type metaclass members are available on the class
+static_assert(has_member(Dynamic, "__name__"))
+static_assert(has_member(Dynamic, "__bases__"))
+static_assert(has_member(Dynamic, "__mro__"))
+static_assert(has_member(Dynamic, "__subclasses__"))
+```
+
+Attributes from the namespace dict (third argument) are not tracked:
+
+```py
+from ty_extensions import has_member, static_assert
+
+DynamicWithDict = type("DynamicWithDict", (), {"custom_attr": 42})
+
+# TODO: these should pass -- namespace dict attributes are not yet available for autocomplete
+static_assert(has_member(DynamicWithDict, "custom_attr")) # error: [static-assert-error]
+static_assert(has_member(DynamicWithDict(), "custom_attr")) # error: [static-assert-error]
+```
+
+Dynamic classes inheriting from classes with custom metaclasses get metaclass members:
+
+```py
+from ty_extensions import has_member, static_assert
+
+class MyMeta(type):
+ meta_attr: str = "meta"
+
+class Base(metaclass=MyMeta):
+ base_attr: int = 1
+
+Dynamic = type("Dynamic", (Base,), {})
+
+# Metaclass attributes are available on the class
+static_assert(has_member(Dynamic, "meta_attr"))
+static_assert(has_member(Dynamic, "base_attr"))
+```
+
+However, instances of dynamic classes currently do not expose members for autocomplete:
+
+```py
+from ty_extensions import has_member, static_assert
+
+class Base:
+ base_attr: int = 1
+
+DynamicSingle = type("DynamicSingle", (Base,), {})
+instance = DynamicSingle()
+
+# TODO: these should pass; instance members should be available
+static_assert(has_member(instance, "base_attr")) # error: [static-assert-error]
+static_assert(has_member(instance, "__repr__")) # error: [static-assert-error]
+static_assert(has_member(instance, "__hash__")) # error: [static-assert-error]
+```
+
### Attributes not available at runtime
Typeshed includes some attributes in `object` that are not available for some (builtin) types. For
diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md
index cad02f63cd..c4c6332ebb 100644
--- a/crates/ty_python_semantic/resources/mdtest/narrow/type.md
+++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md
@@ -165,13 +165,14 @@ def _(x: A | B):
## No narrowing for multiple arguments
-No narrowing should occur if `type` is used to dynamically create a class:
+Narrowing does not occur in the same way if `type` is used to dynamically create a class:
```py
def _(x: str | int):
- # The following diagnostic is valid, since the three-argument form of `type`
- # can only be called with `str` as the first argument.
- # error: [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
+ # Inline type() calls fall back to regular type overload matching.
+ # TODO: Once inline type() calls synthesize class types, this should narrow x to Never.
+ #
+ # error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
if type(x, (), {}) is str:
reveal_type(x) # revealed: str | int
else:
diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs
index e831cb5d91..312a782e68 100644
--- a/crates/ty_python_semantic/src/place.rs
+++ b/crates/ty_python_semantic/src/place.rs
@@ -1592,7 +1592,9 @@ pub(crate) mod implicit_globals {
use crate::place::{Definedness, PlaceAndQualifiers};
use crate::semantic_index::symbol::Symbol;
use crate::semantic_index::{place_table, use_def_map};
- use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type};
+ use crate::types::{
+ ClassLiteral, KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type,
+ };
use ruff_python_ast::PythonVersion;
use super::{DefinedPlace, Place, place_from_declarations};
@@ -1611,7 +1613,10 @@ pub(crate) mod implicit_globals {
else {
return Place::Undefined.into();
};
- let module_type_scope = module_type_class.body_scope(db);
+ let Some(class) = module_type_class.as_static() else {
+ return Place::Undefined.into();
+ };
+ let module_type_scope = class.body_scope(db);
let place_table = place_table(db, module_type_scope);
let Some(symbol_id) = place_table.symbol_id(name) else {
return Place::Undefined.into();
@@ -1739,8 +1744,10 @@ pub(crate) mod implicit_globals {
return smallvec::SmallVec::default();
};
- let module_type_scope = module_type.body_scope(db);
- let module_type_symbol_table = place_table(db, module_type_scope);
+ let ClassLiteral::Static(module_type) = module_type else {
+ return smallvec::SmallVec::default();
+ };
+ let module_type_symbol_table = place_table(db, module_type.body_scope(db));
module_type_symbol_table
.symbols()
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index fb91c37259..76cc4684b1 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -24,6 +24,7 @@ use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module};
use type_ordering::union_or_intersection_elements_ordering;
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
+pub(crate) use self::class::DynamicClassLiteral;
pub use self::cyclic::CycleDetector;
pub(crate) use self::cyclic::{PairVisitor, TypeTransformer};
pub(crate) use self::diagnostic::register_lints;
@@ -76,7 +77,7 @@ use crate::types::visitor::any_over_type;
use crate::unpack::EvaluationMode;
use crate::{Db, FxOrderSet, Program};
pub use class::KnownClass;
-pub(crate) use class::{ClassLiteral, ClassType, GenericAlias};
+pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, StaticClassLiteral};
use instance::Protocol;
pub use instance::{NominalInstanceType, ProtocolInstanceType};
pub use special_form::SpecialFormType;
@@ -782,7 +783,7 @@ pub enum Type<'db> {
Callable(CallableType<'db>),
/// A specific module object
ModuleLiteral(ModuleLiteralType<'db>),
- /// A specific class object
+ /// A specific class object (either from a `class` statement or `type()` call)
ClassLiteral(ClassLiteral<'db>),
/// A specialization of a generic class
GenericAlias(GenericAlias<'db>),
@@ -976,9 +977,9 @@ impl<'db> Type<'db> {
}
fn is_enum(&self, db: &'db dyn Db) -> bool {
- self.as_nominal_instance()
- .and_then(|instance| crate::types::enums::enum_metadata(db, instance.class_literal(db)))
- .is_some()
+ self.as_nominal_instance().is_some_and(|instance| {
+ crate::types::enums::enum_metadata(db, instance.class_literal(db)).is_some()
+ })
}
fn is_typealias_special_form(&self) -> bool {
@@ -1097,25 +1098,21 @@ impl<'db> Type<'db> {
pub(crate) fn specialization_of(
self,
db: &'db dyn Db,
- expected_class: ClassLiteral<'_>,
+ expected_class: StaticClassLiteral<'_>,
) -> Option> {
- self.class_and_specialization_of_optional(db, Some(expected_class))
- .map(|(_, specialization)| specialization)
+ self.specialization_of_optional(db, Some(expected_class))
}
- /// If this type is a class instance, returns its class literal and specialization.
- pub(crate) fn class_specialization(
- self,
- db: &'db dyn Db,
- ) -> Option<(ClassLiteral<'db>, Specialization<'db>)> {
- self.class_and_specialization_of_optional(db, None)
+ /// If this type is a class instance, returns its specialization.
+ pub(crate) fn class_specialization(self, db: &'db dyn Db) -> Option> {
+ self.specialization_of_optional(db, None)
}
- fn class_and_specialization_of_optional(
+ fn specialization_of_optional(
self,
db: &'db dyn Db,
- expected_class: Option>,
- ) -> Option<(ClassLiteral<'db>, Specialization<'db>)> {
+ expected_class: Option>,
+ ) -> Option> {
let class_type = match self {
Type::NominalInstance(instance) => instance,
Type::ProtocolInstance(instance) => instance.to_nominal_instance()?,
@@ -1124,12 +1121,12 @@ impl<'db> Type<'db> {
}
.class(db);
- let (class_literal, specialization) = class_type.class_literal(db);
+ let (class_literal, specialization) = class_type.static_class_literal(db)?;
if expected_class.is_some_and(|expected_class| expected_class != class_literal) {
return None;
}
- Some((class_literal, specialization?))
+ specialization
}
/// Returns the top materialization (or upper bound materialization) of this type, which is the
@@ -2048,7 +2045,10 @@ impl<'db> Type<'db> {
return;
};
- let (class_literal, Some(specialization)) = instance.class(db).class_literal(db) else {
+
+ let Some((class_literal, Some(specialization))) =
+ instance.class(db).static_class_literal(db)
+ else {
return;
};
let generic_context = specialization.generic_context(db);
@@ -3248,11 +3248,14 @@ impl<'db> Type<'db> {
Type::NominalInstance(instance)
if matches!(name_str, "value" | "_value_")
- && is_single_member_enum(db, instance.class(db).class_literal(db).0) =>
+ && is_single_member_enum(db, instance.class_literal(db)) =>
{
- enum_metadata(db, instance.class(db).class_literal(db).0)
- .and_then(|metadata| metadata.members.get_index(0).map(|(_, v)| *v))
- .map_or(Place::Undefined, Place::bound)
+ enum_metadata(db, instance.class_literal(db))
+ .and_then(|metadata| {
+ let (_, ty) = metadata.members.get_index(0)?;
+ Some(Place::bound(*ty))
+ })
+ .unwrap_or_default()
.into()
}
@@ -3298,7 +3301,7 @@ impl<'db> Type<'db> {
)
.map(|outcome| Place::bound(outcome.return_type(db)))
// TODO: Handle call errors here.
- .unwrap_or(Place::Undefined)
+ .unwrap_or_default()
.into()
};
@@ -3319,7 +3322,7 @@ impl<'db> Type<'db> {
)
.map(|outcome| Place::bound(outcome.return_type(db)))
// TODO: Handle call errors here.
- .unwrap_or(Place::Undefined)
+ .unwrap_or_default()
.into()
};
@@ -3356,14 +3359,15 @@ impl<'db> Type<'db> {
}
Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => {
- if let Some(enum_class) = match self {
+ let enum_class = match self {
Type::ClassLiteral(literal) => Some(literal),
Type::SubclassOf(subclass_of) => subclass_of
.subclass_of()
.into_class(db)
- .map(|class| class.class_literal(db).0),
+ .map(|class| class.class_literal(db)),
_ => None,
- } {
+ };
+ if let Some(enum_class) = enum_class {
if let Some(metadata) = enum_metadata(db, enum_class) {
if let Some(resolved_name) = metadata.resolve_member(&name) {
return Place::bound(Type::EnumLiteral(EnumLiteralType::new(
@@ -5096,7 +5100,9 @@ impl<'db> Type<'db> {
let from_class_base = |base: ClassBase<'db>| {
let class = base.into_class()?;
if class.is_known(db, KnownClass::Generator) {
- if let Some(specialization) = class.class_literal_specialized(db, None).1 {
+ if let Some((_, Some(specialization))) =
+ class.static_class_literal_specialized(db, None)
+ {
if let [_, _, return_ty] = specialization.types(db) {
return Some(*return_ty);
}
@@ -5623,9 +5629,11 @@ impl<'db> Type<'db> {
});
};
- Ok(typing_self(db, scope_id, typevar_binding_context, class)
- .map(Type::TypeVar)
- .unwrap_or(*self))
+ Ok(
+ typing_self(db, scope_id, typevar_binding_context, class.into())
+ .map(Type::TypeVar)
+ .unwrap_or(*self),
+ )
}
// We ensure that `typing.TypeAlias` used in the expected position (annotating an
// annotated assignment statement) doesn't reach here. Using it in any other type
@@ -6521,13 +6529,9 @@ impl<'db> Type<'db> {
Some(TypeDefinition::Function(function.definition(db)))
}
Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))),
- Self::ClassLiteral(class_literal) => {
- Some(TypeDefinition::Class(class_literal.definition(db)))
- }
- Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))),
- Self::NominalInstance(instance) => {
- Some(TypeDefinition::Class(instance.class(db).definition(db)))
- }
+ Self::ClassLiteral(class_literal) => Some(class_literal.type_definition(db)),
+ Self::GenericAlias(alias) => Some(TypeDefinition::StaticClass(alias.definition(db))),
+ Self::NominalInstance(instance) => Some(instance.class(db).type_definition(db)),
Self::KnownInstance(instance) => match instance {
KnownInstanceType::TypeVar(var) => {
Some(TypeDefinition::TypeVar(var.definition(db)?))
@@ -6540,9 +6544,11 @@ impl<'db> Type<'db> {
},
Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
- SubclassOfInner::Class(class) => Some(TypeDefinition::Class(class.definition(db))),
SubclassOfInner::Dynamic(_) => None,
- SubclassOfInner::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)),
+ SubclassOfInner::Class(class) => Some(class.type_definition(db)),
+ SubclassOfInner::TypeVar(bound_typevar) => {
+ Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?))
+ }
},
Self::TypeAlias(alias) => alias.value_type(db).definition(db),
@@ -6569,13 +6575,11 @@ impl<'db> Type<'db> {
Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)),
Self::ProtocolInstance(protocol) => match protocol.inner {
- Protocol::FromClass(class) => Some(TypeDefinition::Class(class.definition(db))),
+ Protocol::FromClass(class) => Some(class.type_definition(db)),
Protocol::Synthesized(_) => None,
},
- Self::TypedDict(typed_dict) => {
- typed_dict.definition(db).map(TypeDefinition::Class)
- }
+ Self::TypedDict(typed_dict) => typed_dict.type_definition(db),
Self::Union(_) | Self::Intersection(_) => None,
@@ -6656,7 +6660,7 @@ impl<'db> Type<'db> {
}
}
- pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option> {
+ pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option> {
match self {
Type::GenericAlias(generic) => Some(generic.origin(db)),
Type::NominalInstance(instance) => {
@@ -8985,7 +8989,12 @@ impl<'db> UnionTypeInstance<'db> {
) -> Result> + 'db, InvalidTypeExpressionError<'db>> {
let to_class_literal = |ty: Type<'db>| {
ty.as_nominal_instance()
- .map(|instance| Type::ClassLiteral(instance.class(db).class_literal(db).0))
+ .and_then(|instance| {
+ instance
+ .class(db)
+ .static_class_literal(db)
+ .map(|(lit, _)| Type::ClassLiteral(lit.into()))
+ })
.unwrap_or_else(Type::unknown)
};
@@ -11718,7 +11727,7 @@ impl<'db> TypeAliasType<'db> {
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(super) struct MetaclassCandidate<'db> {
metaclass: ClassType<'db>,
- explicit_metaclass_of: ClassLiteral<'db>,
+ explicit_metaclass_of: StaticClassLiteral<'db>,
}
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
@@ -12925,7 +12934,7 @@ impl<'db> TypeGuardLike<'db> for TypeGuardType<'db> {
/// being added to the given class.
pub(super) fn determine_upper_bound<'db>(
db: &'db dyn Db,
- class_literal: ClassLiteral<'db>,
+ class_literal: StaticClassLiteral<'db>,
specialization: Option>,
is_known_base: impl Fn(ClassBase<'db>) -> bool,
) -> Type<'db> {
diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs
index e4c41e37f1..6765bf0175 100644
--- a/crates/ty_python_semantic/src/types/bound_super.rs
+++ b/crates/ty_python_semantic/src/types/bound_super.rs
@@ -392,7 +392,7 @@ impl<'db> BoundSuperType<'db> {
typevar: TypeVarInstance<'db>,
make_owner: fn(BoundTypeVarInstance<'db>, ClassType<'db>) -> SuperOwnerKind<'db>|
-> Result, BoundSuperError<'db>> {
- let pivot_class_literal = pivot_class.into_class().map(|c| c.class_literal(db).0);
+ let pivot_class_literal = pivot_class.into_class().map(|c| c.class_literal(db));
let mut builder = UnionBuilder::new(db);
for constraint in constraints.elements(db) {
let class = match constraint {
@@ -409,7 +409,7 @@ impl<'db> BoundSuperType<'db> {
| ClassBase::Protocol
| ClassBase::TypedDict => false,
ClassBase::Class(superclass) => {
- superclass.class_literal(db).0 == pivot
+ superclass.class_literal(db) == pivot
}
}) {
return Err(BoundSuperError::FailingConditionCheck {
@@ -627,11 +627,11 @@ impl<'db> BoundSuperType<'db> {
if let Some(pivot_class) = pivot_class.into_class()
&& let Some(owner_class) = owner.into_class(db)
{
- let pivot_class = pivot_class.class_literal(db).0;
+ let pivot_class = pivot_class.class_literal(db);
if !owner_class.iter_mro(db).any(|superclass| match superclass {
ClassBase::Dynamic(_) => true,
ClassBase::Generic | ClassBase::Protocol | ClassBase::TypedDict => false,
- ClassBase::Class(superclass) => superclass.class_literal(db).0 == pivot_class,
+ ClassBase::Class(superclass) => superclass.class_literal(db) == pivot_class,
}) {
return Err(BoundSuperError::FailingConditionCheck {
pivot_class: pivot_class_type,
@@ -748,7 +748,7 @@ impl<'db> BoundSuperType<'db> {
}
};
- let (class_literal, _) = class.class_literal(db);
+ let class_literal = class.class_literal(db);
// TODO properly support super() with generic types
// * requires a fix for https://github.com/astral-sh/ruff/issues/17432
// * also requires understanding how we should handle cases like this:
diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs
index 04621c995d..0b72c42197 100644
--- a/crates/ty_python_semantic/src/types/call/arguments.rs
+++ b/crates/ty_python_semantic/src/types/call/arguments.rs
@@ -352,7 +352,7 @@ pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
.any(|element| is_expandable_type(db, element)),
Tuple::Variable(_) => false,
})
- || enum_metadata(db, class.class_literal(db).0).is_some()
+ || enum_metadata(db, class.class_literal(db)).is_some()
}
Type::Union(_) => true,
_ => false,
@@ -403,7 +403,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> {
};
}
- if let Some(enum_members) = enum_member_literals(db, class.class_literal(db).0, None) {
+ if let Some(enum_members) = enum_member_literals(db, class.class_literal(db), None) {
return Some(enum_members.collect());
}
diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs
index 77732c05df..ba4f3b61fc 100644
--- a/crates/ty_python_semantic/src/types/class.rs
+++ b/crates/ty_python_semantic/src/types/class.rs
@@ -19,7 +19,10 @@ use crate::semantic_index::{
use crate::types::bound_super::BoundSuperError;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::context::InferContext;
-use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD};
+use crate::types::diagnostic::{
+ DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD,
+ report_conflicting_metaclass_from_bases,
+};
use crate::types::enums::{
enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value,
};
@@ -31,6 +34,7 @@ use crate::types::generics::{
};
use crate::types::infer::{infer_expression_type, infer_unpack_types, nearest_enclosing_class};
use crate::types::member::{Member, class_member};
+use crate::types::mro::{DynamicMroError, DynamicMroErrorKind};
use crate::types::relation::{
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation,
};
@@ -41,7 +45,7 @@ use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion
use crate::types::{
ApplyTypeMappingVisitor, Binding, BindingContext, BoundSuperType, CallableType,
CallableTypeKind, CallableTypes, DATACLASS_FLAGS, DataclassFlags, DataclassParams,
- DeprecatedInstance, FindLegacyTypeVarsVisitor, IntersectionType, KnownInstanceType,
+ DeprecatedInstance, FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownInstanceType,
ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType,
TypeAliasType, TypeContext, TypeMapping, TypedDictParams, UnionBuilder, VarianceInferable,
binding_type, declaration_type, determine_upper_bound,
@@ -56,11 +60,11 @@ use crate::{
attribute_assignments,
definition::{DefinitionKind, TargetKind},
place_table,
- scope::ScopeId,
+ scope::{FileScopeId, ScopeId},
semantic_index, use_def_map,
},
types::{
- CallArguments, CallError, CallErrorKind, MetaclassCandidate, UnionType,
+ CallArguments, CallError, CallErrorKind, MetaclassCandidate, TypeDefinition, UnionType,
definition_expression_type,
},
};
@@ -78,7 +82,7 @@ use ty_module_resolver::{KnownModule, file_to_module};
fn explicit_bases_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
- _self: ClassLiteral<'db>,
+ _self: StaticClassLiteral<'db>,
) -> Box<[Type<'db>]> {
Box::default()
}
@@ -86,7 +90,7 @@ fn explicit_bases_cycle_initial<'db>(
fn inheritance_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
- _self: ClassLiteral<'db>,
+ _self: StaticClassLiteral<'db>,
) -> Option {
None
}
@@ -122,7 +126,7 @@ fn implicit_attribute_cycle_recover<'db>(
fn try_mro_cycle_initial<'db>(
db: &'db dyn Db,
_id: salsa::Id,
- self_: ClassLiteral<'db>,
+ self_: StaticClassLiteral<'db>,
specialization: Option>,
) -> Result, MroError<'db>> {
Err(MroError::cycle(
@@ -134,7 +138,7 @@ fn try_mro_cycle_initial<'db>(
fn is_typed_dict_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
- _self: ClassLiteral<'db>,
+ _self: StaticClassLiteral<'db>,
) -> bool {
false
}
@@ -143,7 +147,7 @@ fn is_typed_dict_cycle_initial<'db>(
fn try_metaclass_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
- _self_: ClassLiteral<'db>,
+ _self_: StaticClassLiteral<'db>,
) -> Result<(Type<'db>, Option>), MetaclassError<'db>> {
Err(MetaclassError {
kind: MetaclassErrorKind::Cycle,
@@ -153,7 +157,7 @@ fn try_metaclass_cycle_initial<'db>(
fn decorators_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
- _self: ClassLiteral<'db>,
+ _self: StaticClassLiteral<'db>,
) -> Box<[Type<'db>]> {
Box::default()
}
@@ -161,7 +165,7 @@ fn decorators_cycle_initial<'db>(
fn fields_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
- _self: ClassLiteral<'db>,
+ _self: StaticClassLiteral<'db>,
_specialization: Option>,
_field_policy: CodeGeneratorKind<'db>,
) -> FxIndexMap> {
@@ -185,12 +189,25 @@ impl<'db> CodeGeneratorKind<'db> {
class: ClassLiteral<'db>,
specialization: Option>,
) -> Option {
- #[salsa::tracked(cycle_initial=code_generator_of_class_initial,
+ match class {
+ ClassLiteral::Static(static_class) => {
+ Self::from_static_class(db, static_class, specialization)
+ }
+ ClassLiteral::Dynamic(dynamic_class) => Self::from_dynamic_class(db, dynamic_class),
+ }
+ }
+
+ fn from_static_class(
+ db: &'db dyn Db,
+ class: StaticClassLiteral<'db>,
+ specialization: Option>,
+ ) -> Option {
+ #[salsa::tracked(cycle_initial=code_generator_of_static_class_initial,
heap_size=ruff_memory_usage::heap_size
)]
- fn code_generator_of_class<'db>(
+ fn code_generator_of_static_class<'db>(
db: &'db dyn Db,
- class: ClassLiteral<'db>,
+ class: StaticClassLiteral<'db>,
specialization: Option>,
) -> Option> {
if class.dataclass_params(db).is_some() {
@@ -200,7 +217,9 @@ impl<'db> CodeGeneratorKind<'db> {
} else if let Some(transformer_params) =
class.iter_mro(db, specialization).skip(1).find_map(|base| {
base.into_class().and_then(|class| {
- class.class_literal(db).0.dataclass_transformer_params(db)
+ class
+ .static_class_literal(db)
+ .and_then(|(lit, _)| lit.dataclass_transformer_params(db))
})
})
{
@@ -217,16 +236,41 @@ impl<'db> CodeGeneratorKind<'db> {
}
}
- fn code_generator_of_class_initial<'db>(
+ fn code_generator_of_static_class_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
- _class: ClassLiteral<'db>,
+ _class: StaticClassLiteral<'db>,
_specialization: Option>,
) -> Option> {
None
}
- code_generator_of_class(db, class, specialization)
+ code_generator_of_static_class(db, class, specialization)
+ }
+
+ fn from_dynamic_class(db: &'db dyn Db, class: DynamicClassLiteral<'db>) -> Option {
+ #[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
+ fn code_generator_of_dynamic_class<'db>(
+ db: &'db dyn Db,
+ class: DynamicClassLiteral<'db>,
+ ) -> Option> {
+ // Check if the dynamic class was passed to `dataclass()` as a function.
+ if class.dataclass_params(db).is_some() {
+ return Some(CodeGeneratorKind::DataclassLike(None));
+ }
+
+ // Dynamic classes can also inherit from classes with dataclass_transform.
+ class.iter_mro(db).skip(1).find_map(|base| {
+ base.into_class().and_then(|class| {
+ class
+ .static_class_literal(db)
+ .and_then(|(lit, _)| lit.dataclass_transformer_params(db))
+ .map(|params| CodeGeneratorKind::DataclassLike(Some(params)))
+ })
+ })
+ }
+
+ code_generator_of_dynamic_class(db, class)
}
pub(super) fn matches(
@@ -262,7 +306,7 @@ impl<'db> CodeGeneratorKind<'db> {
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
pub struct GenericAlias<'db> {
- pub(crate) origin: ClassLiteral<'db>,
+ pub(crate) origin: StaticClassLiteral<'db>,
pub(crate) specialization: Specialization<'db>,
}
@@ -336,7 +380,7 @@ impl<'db> GenericAlias<'db> {
.find_legacy_typevars_impl(db, binding_context, typevars, visitor);
}
- pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
+ pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
self.origin(db).is_typed_dict(db)
}
}
@@ -382,7 +426,7 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> {
// inferred one. The inference is done lazily, as we can
// sometimes determine the result just from the passed
// variance. This operation is commutative, so we could
- // infer either first. We choose to make the `ClassLiteral`
+ // infer either first. We choose to make the `StaticClassLiteral`
// variance lazy, as it is known to be expensive, requiring
// that we traverse all members.
//
@@ -400,6 +444,373 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> {
}
}
+/// A class literal, either defined via a `class` statement or a `type` function call.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ Ord,
+ PartialEq,
+ PartialOrd,
+ salsa::Supertype,
+ salsa::Update,
+ get_size2::GetSize,
+)]
+pub enum ClassLiteral<'db> {
+ /// A class defined via a `class` statement.
+ Static(StaticClassLiteral<'db>),
+ /// A class created dynamically via `type(name, bases, dict)`.
+ Dynamic(DynamicClassLiteral<'db>),
+}
+
+impl<'db> ClassLiteral<'db> {
+ /// Returns the name of the class.
+ pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name {
+ match self {
+ Self::Static(class) => class.name(db),
+ Self::Dynamic(class) => class.name(db),
+ }
+ }
+
+ /// Returns the known class, if any.
+ pub(crate) fn known(self, db: &'db dyn Db) -> Option {
+ self.as_static()?.known(db)
+ }
+
+ /// Returns whether this class has PEP 695 type parameters.
+ pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
+ self.as_static()
+ .is_some_and(|class| class.has_pep_695_type_params(db))
+ }
+
+ /// Returns an iterator over the MRO.
+ pub(crate) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> {
+ MroIterator::new(db, self, None)
+ }
+
+ /// Returns the metaclass of this class.
+ pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
+ match self {
+ Self::Static(class) => class.metaclass(db),
+ Self::Dynamic(class) => class.metaclass(db),
+ }
+ }
+
+ /// Look up a class-level member by iterating through the MRO.
+ pub(crate) fn class_member(
+ self,
+ db: &'db dyn Db,
+ name: &str,
+ policy: MemberLookupPolicy,
+ ) -> PlaceAndQualifiers<'db> {
+ match self {
+ Self::Static(class) => class.class_member(db, name, policy),
+ Self::Dynamic(class) => class.class_member(db, name, policy),
+ }
+ }
+
+ /// Look up a class-level member using a provided MRO iterator.
+ ///
+ /// This is used by `super()` to start the MRO lookup after the pivot class.
+ pub(super) fn class_member_from_mro(
+ self,
+ db: &'db dyn Db,
+ name: &str,
+ policy: MemberLookupPolicy,
+ mro_iter: impl Iterator- >,
+ ) -> PlaceAndQualifiers<'db> {
+ match self {
+ Self::Static(class) => class.class_member_from_mro(db, name, policy, mro_iter),
+ Self::Dynamic(_) => {
+ // Dynamic classes don't have inherited generic context and are never `object`.
+ let result = MroLookup::new(db, mro_iter).class_member(name, policy, None, false);
+ match result {
+ ClassMemberResult::Done(result) => result.finalize(db),
+ ClassMemberResult::TypedDict => KnownClass::TypedDictFallback
+ .to_class_literal(db)
+ .find_name_in_mro_with_policy(db, name, policy)
+ .expect("Will return Some() when called on class literal"),
+ }
+ }
+ }
+ }
+
+ /// Returns whether this is a known class.
+ pub(crate) fn is_known(self, db: &'db dyn Db, known: KnownClass) -> bool {
+ self.known(db) == Some(known)
+ }
+
+ /// Returns the default specialization for this class.
+ ///
+ /// For static classes, this applies default type arguments.
+ /// For dynamic classes, this returns a non-generic class type.
+ pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
+ match self {
+ Self::Static(class) => class.default_specialization(db),
+ Self::Dynamic(_) => ClassType::NonGeneric(self),
+ }
+ }
+
+ /// Returns the identity specialization for this class (same as default for non-generic).
+ pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
+ match self {
+ Self::Static(class) => class.identity_specialization(db),
+ Self::Dynamic(_) => ClassType::NonGeneric(self),
+ }
+ }
+
+ /// Returns the generic context if this is a generic class.
+ ///
+ // TODO: We should emit a diagnostic if a dynamic class (created via `type()`) attempts
+ // to inherit from `Generic[T]`, since dynamic classes can't be generic. See also: `is_protocol`.
+ pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> {
+ self.as_static().and_then(|class| class.generic_context(db))
+ }
+
+ /// Returns whether this class is a protocol.
+ ///
+ // TODO: We should emit a diagnostic if a dynamic class (created via `type()`) attempts
+ // to inherit from `Protocol`, since dynamic classes can't be protocols. See also: `generic_context`.
+ pub(crate) fn is_protocol(self, db: &'db dyn Db) -> bool {
+ self.as_static().is_some_and(|class| class.is_protocol(db))
+ }
+
+ /// Returns whether this class is a `TypedDict`.
+ // TODO: We should emit a diagnostic if a dynamic class (created via `type()`) attempts
+ // to inherit from `TypedDict`. To create a dynamic TypedDict, you should invoke
+ // `TypedDict` as a function, not `type`. See also: `generic_context`, `is_protocol`.
+ pub fn is_typed_dict(self, db: &'db dyn Db) -> bool {
+ match self {
+ Self::Static(class) => class.is_typed_dict(db),
+ Self::Dynamic(_) => false,
+ }
+ }
+
+ /// Returns whether this class is `builtins.tuple` exactly
+ pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool {
+ match self {
+ Self::Static(class) => class.is_tuple(db),
+ Self::Dynamic(_) => false,
+ }
+ }
+
+ /// Return a type representing "the set of all instances of the metaclass of this class".
+ pub(crate) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> {
+ self.metaclass(db)
+ .to_instance(db)
+ .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass")
+ }
+
+ /// Returns whether this class is type-check only.
+ pub(crate) fn type_check_only(self, db: &'db dyn Db) -> bool {
+ self.as_static()
+ .is_some_and(|class| class.type_check_only(db))
+ }
+
+ /// Returns the file containing the class definition.
+ pub(crate) fn file(self, db: &dyn Db) -> File {
+ match self {
+ Self::Static(class) => class.file(db),
+ Self::Dynamic(class) => class.file(db),
+ }
+ }
+
+ /// Returns the range of the class's "header".
+ ///
+ /// For static classes, this is the class name and any arguments passed to the `class` statement.
+ /// For dynamic classes, this is the entire `type()` call expression.
+ pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange {
+ match self {
+ Self::Static(class) => class.header_range(db),
+ Self::Dynamic(class) => class.header_range(db),
+ }
+ }
+
+ /// Returns the deprecated info if this class is deprecated.
+ pub(crate) fn deprecated(self, db: &'db dyn Db) -> Option> {
+ self.as_static().and_then(|class| class.deprecated(db))
+ }
+
+ /// Returns whether this class is final.
+ // TODO: Support `@final` on dynamic classes, e.g. `X = final(type("X", (), {}))`.
+ // We should either recognize and track this, or emit a diagnostic if unsupported.
+ pub(crate) fn is_final(self, db: &'db dyn Db) -> bool {
+ self.as_static().is_some_and(|class| class.is_final(db))
+ }
+
+ /// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`,
+ /// `__ge__`) in its own body (not inherited). Used by `@total_ordering` to determine if
+ /// synthesis is valid.
+ // TODO: A dynamic class could provide ordering methods in the namespace dictionary:
+ // ```python
+ // >>> X = type("X", (), {"__lt__": lambda self, other: True})
+ // ```
+ pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool {
+ self.as_static()
+ .is_some_and(|class| class.has_own_ordering_method(db))
+ }
+
+ /// Returns the static class definition if this is one.
+ pub(crate) fn as_static(self) -> Option> {
+ match self {
+ Self::Static(class) => Some(class),
+ Self::Dynamic(_) => None,
+ }
+ }
+
+ /// Returns an unknown specialization for this class.
+ pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> {
+ match self {
+ Self::Static(class) => class.unknown_specialization(db),
+ Self::Dynamic(_) => ClassType::NonGeneric(self),
+ }
+ }
+
+ /// Returns the definition of this class.
+ pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
+ match self {
+ Self::Static(class) => class.definition(db),
+ Self::Dynamic(class) => class.definition(db),
+ }
+ }
+
+ /// Returns the type definition for this class.
+ ///
+ /// For static classes, returns `TypeDefinition::StaticClass`.
+ /// For dynamic classes, returns `TypeDefinition::DynamicClass`.
+ pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> {
+ match self {
+ Self::Static(class) => TypeDefinition::StaticClass(class.definition(db)),
+ Self::Dynamic(class) => TypeDefinition::DynamicClass(class.definition(db)),
+ }
+ }
+
+ /// Returns the qualified name of this class.
+ pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> {
+ QualifiedClassName::from_class_literal(db, self)
+ }
+
+ /// Returns a [`Span`] pointing to the definition of this class.
+ ///
+ /// For static classes, this is the class header (name and arguments).
+ /// For dynamic classes, this is the `type()` call expression.
+ pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
+ match self {
+ Self::Static(class) => class.header_span(db),
+ Self::Dynamic(class) => class.header_span(db),
+ }
+ }
+
+ /// Returns whether this class is a disjoint base.
+ // TODO: A dynamic class could provide __slots__ in the namespace dictionary, which would make
+ // it a disjoint base:
+ // ```python
+ // >>> X = type("X", (), {"__slots__": ("a",)})
+ // >>> class Foo(int, X): ...
+ // ...
+ // Traceback (most recent call last):
+ // File "", line 1, in
+ // class Foo(int, X): ...
+ // TypeError: multiple bases have instance lay-out conflict
+ // ```
+ pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> {
+ self.as_static()
+ .and_then(|class| class.as_disjoint_base(db))
+ }
+
+ /// Returns a non-generic instance of this class.
+ pub(crate) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> {
+ match self {
+ Self::Static(class) => class.to_non_generic_instance(db),
+ Self::Dynamic(_) => Type::instance(db, ClassType::NonGeneric(self)),
+ }
+ }
+
+ /// Returns the protocol class if this is a protocol.
+ pub(super) fn into_protocol_class(
+ self,
+ db: &'db dyn Db,
+ ) -> Option> {
+ self.as_static()
+ .and_then(|class| class.into_protocol_class(db))
+ }
+
+ /// Apply a specialization to this class.
+ pub(crate) fn apply_specialization(
+ self,
+ db: &'db dyn Db,
+ f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>,
+ ) -> ClassType<'db> {
+ match self {
+ Self::Static(class) => class.apply_specialization(db, f),
+ Self::Dynamic(_) => ClassType::NonGeneric(self),
+ }
+ }
+
+ /// Returns the instance member lookup.
+ pub(crate) fn instance_member(
+ self,
+ db: &'db dyn Db,
+ specialization: Option>,
+ name: &str,
+ ) -> PlaceAndQualifiers<'db> {
+ match self {
+ Self::Static(class) => class.instance_member(db, specialization, name),
+ Self::Dynamic(class) => class.instance_member(db, name),
+ }
+ }
+
+ /// Returns the top materialization for this class.
+ pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> {
+ match self {
+ Self::Static(class) => class.top_materialization(db),
+ Self::Dynamic(_) => ClassType::NonGeneric(self),
+ }
+ }
+
+ /// Returns the `TypedDict` member lookup.
+ pub(crate) fn typed_dict_member(
+ self,
+ db: &'db dyn Db,
+ specialization: Option>,
+ name: &str,
+ policy: MemberLookupPolicy,
+ ) -> PlaceAndQualifiers<'db> {
+ match self {
+ Self::Static(class) => class.typed_dict_member(db, specialization, name, policy),
+ Self::Dynamic(_) => Place::Undefined.into(),
+ }
+ }
+
+ /// Returns a new `ClassLiteral` with the given dataclass params, preserving all other fields.
+ pub(crate) fn with_dataclass_params(
+ self,
+ db: &'db dyn Db,
+ dataclass_params: Option>,
+ ) -> Self {
+ match self {
+ Self::Static(class) => Self::Static(class.with_dataclass_params(db, dataclass_params)),
+ Self::Dynamic(class) => {
+ Self::Dynamic(class.with_dataclass_params(db, dataclass_params))
+ }
+ }
+ }
+}
+
+impl<'db> From> for ClassLiteral<'db> {
+ fn from(literal: StaticClassLiteral<'db>) -> Self {
+ ClassLiteral::Static(literal)
+ }
+}
+
+impl<'db> From> for ClassLiteral<'db> {
+ fn from(literal: DynamicClassLiteral<'db>) -> Self {
+ ClassLiteral::Dynamic(literal)
+ }
+}
+
/// Represents a class type, which might be a non-generic class, or a specialization of a generic
/// class.
#[derive(
@@ -419,7 +830,7 @@ pub enum ClassType<'db> {
// `NonGeneric` is intended to mean that the `ClassLiteral` has no type parameters. There are
// places where we currently violate this rule (e.g. so that we print `Foo` instead of
// `Foo[Unknown]`), but most callers who need to make a `ClassType` from a `ClassLiteral`
- // should use `ClassLiteral::default_specialization` instead of assuming
+ // should use `StaticClassLiteral::default_specialization` instead of assuming
// `ClassType::NonGeneric`.
NonGeneric(ClassLiteral<'db>),
Generic(GenericAlias<'db>),
@@ -470,67 +881,79 @@ impl<'db> ClassType<'db> {
}
pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
+ self.class_literal(db).has_pep_695_type_params(db)
+ }
+
+ /// Returns the underlying class literal for this class, ignoring any specialization.
+ ///
+ /// For a non-generic class, this returns the class literal directly.
+ /// For a generic alias, this returns the alias's origin.
+ pub(crate) fn class_literal(self, db: &'db dyn Db) -> ClassLiteral<'db> {
match self {
- Self::NonGeneric(class) => class.has_pep_695_type_params(db),
- Self::Generic(generic) => generic.origin(db).has_pep_695_type_params(db),
+ Self::NonGeneric(literal) => literal,
+ Self::Generic(generic) => ClassLiteral::Static(generic.origin(db)),
}
}
- /// Returns the class literal and specialization for this class. For a non-generic class, this
- /// is the class itself. For a generic alias, this is the alias's origin.
- pub(crate) fn class_literal(
+ /// Returns the statement-defined class literal and specialization for this class.
+ /// For a non-generic class, this is the class itself. For a generic alias, this is the alias's origin.
+ pub(crate) fn static_class_literal(
self,
db: &'db dyn Db,
- ) -> (ClassLiteral<'db>, Option>) {
+ ) -> Option<(StaticClassLiteral<'db>, Option>)> {
match self {
- Self::NonGeneric(non_generic) => (non_generic, None),
- Self::Generic(generic) => (generic.origin(db), Some(generic.specialization(db))),
+ Self::NonGeneric(ClassLiteral::Static(class)) => Some((class, None)),
+ Self::NonGeneric(ClassLiteral::Dynamic(_)) => None,
+ Self::Generic(generic) => Some((generic.origin(db), Some(generic.specialization(db)))),
}
}
- /// Returns the class literal and specialization for this class, with an additional
+ /// Returns the statement-defined class literal and specialization for this class, with an additional
/// specialization applied if the class is generic.
- pub(crate) fn class_literal_specialized(
+ pub(crate) fn static_class_literal_specialized(
self,
db: &'db dyn Db,
additional_specialization: Option>,
- ) -> (ClassLiteral<'db>, Option>) {
+ ) -> Option<(StaticClassLiteral<'db>, Option>)> {
match self {
- Self::NonGeneric(non_generic) => (non_generic, None),
- Self::Generic(generic) => (
+ Self::NonGeneric(ClassLiteral::Static(class)) => Some((class, None)),
+ Self::NonGeneric(ClassLiteral::Dynamic(_)) => None,
+ Self::Generic(generic) => Some((
generic.origin(db),
Some(
generic
.specialization(db)
.apply_optional_specialization(db, additional_specialization),
),
- ),
+ )),
}
}
- pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name {
- let (class_literal, _) = self.class_literal(db);
- class_literal.name(db)
+ pub(crate) fn name(self, db: &'db dyn Db) -> &'db Name {
+ self.class_literal(db).name(db)
}
pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> {
- let (class_literal, _) = self.class_literal(db);
- class_literal.qualified_name(db)
+ self.class_literal(db).qualified_name(db)
}
pub(crate) fn known(self, db: &'db dyn Db) -> Option {
- let (class_literal, _) = self.class_literal(db);
- class_literal.known(db)
+ self.class_literal(db).known(db)
}
+ /// Returns the definition for this class.
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
- let (class_literal, _) = self.class_literal(db);
- class_literal.definition(db)
+ self.class_literal(db).definition(db)
+ }
+
+ /// Returns the type definition for this class.
+ pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> {
+ self.class_literal(db).type_definition(db)
}
/// Return `Some` if this class is known to be a [`DisjointBase`], or `None` if it is not.
pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> {
- self.class_literal(db).0.as_disjoint_base(db)
+ self.class_literal(db).as_disjoint_base(db)
}
/// Return `true` if this class represents `known_class`
@@ -577,13 +1000,19 @@ impl<'db> ClassType<'db> {
///
/// If the MRO could not be accurately resolved, this method falls back to iterating
/// over an MRO that has the class directly inheriting from `Unknown`. Use
- /// [`ClassLiteral::try_mro`] if you need to distinguish between the success and failure
+ /// [`StaticClassLiteral::try_mro`] if you need to distinguish between the success and failure
/// cases rather than simply iterating over the inferred resolution order for the class.
///
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
pub(super) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> {
- let (class_literal, specialization) = self.class_literal(db);
- class_literal.iter_mro(db, specialization)
+ match self {
+ Self::NonGeneric(class) => class.iter_mro(db),
+ Self::Generic(generic) => MroIterator::new(
+ db,
+ ClassLiteral::Static(generic.origin(db)),
+ Some(generic.specialization(db)),
+ ),
+ }
}
/// Iterate over the method resolution order ("MRO") of the class, optionally applying an
@@ -593,22 +1022,32 @@ impl<'db> ClassType<'db> {
db: &'db dyn Db,
additional_specialization: Option>,
) -> MroIterator<'db> {
- let (class_literal, specialization) =
- self.class_literal_specialized(db, additional_specialization);
- class_literal.iter_mro(db, specialization)
+ match self {
+ Self::NonGeneric(class) => class.iter_mro(db),
+ Self::Generic(generic) => MroIterator::new(
+ db,
+ ClassLiteral::Static(generic.origin(db)),
+ Some(
+ generic
+ .specialization(db)
+ .apply_optional_specialization(db, additional_specialization),
+ ),
+ ),
+ }
}
/// Is this class final?
pub(super) fn is_final(self, db: &'db dyn Db) -> bool {
- let (class_literal, _) = self.class_literal(db);
- class_literal.is_final(db)
+ self.class_literal(db).is_final(db)
}
/// Returns `true` if any class in this class's MRO (excluding `object`) defines an ordering
/// method (`__lt__`, `__le__`, `__gt__`, `__ge__`). Used by `@total_ordering` validation.
pub(super) fn has_ordering_method_in_mro(self, db: &'db dyn Db) -> bool {
- let (class_literal, specialization) = self.class_literal(db);
- class_literal.has_ordering_method_in_mro(db, specialization)
+ self.iter_mro(db)
+ .filter_map(ClassBase::into_class)
+ .filter(|class| !class.is_object(db))
+ .any(|class| class.class_literal(db).has_own_ordering_method(db))
}
/// Return `true` if `other` is present in this class's MRO.
@@ -655,15 +1094,18 @@ impl<'db> ClassType<'db> {
}
},
- // Protocol, Generic, and TypedDict are not represented by a ClassType.
+ // Protocol, Generic, and TypedDict are special bases that don't match ClassType.
ClassBase::Protocol | ClassBase::Generic | ClassBase::TypedDict => {
ConstraintSet::from(false)
}
ClassBase::Class(base) => match (base, other) {
- (ClassType::NonGeneric(base), ClassType::NonGeneric(other)) => {
- ConstraintSet::from(base == other)
+ // Two non-generic classes match if they have the same class literal.
+ (ClassType::NonGeneric(base_literal), ClassType::NonGeneric(other_literal)) => {
+ ConstraintSet::from(base_literal == other_literal)
}
+
+ // Two generic classes match if they have the same origin and compatible specializations.
(ClassType::Generic(base), ClassType::Generic(other)) => {
ConstraintSet::from(base.origin(db) == other.origin(db)).and(db, || {
base.specialization(db).has_relation_to_impl(
@@ -676,6 +1118,8 @@ impl<'db> ClassType<'db> {
)
})
}
+
+ // Generic and non-generic classes don't match.
(ClassType::Generic(_), ClassType::NonGeneric(_))
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => {
ConstraintSet::from(false)
@@ -697,8 +1141,8 @@ impl<'db> ClassType<'db> {
}
match (self, other) {
- // A non-generic class is never equivalent to a generic class.
// Two non-generic classes are only equivalent if they are equal (handled above).
+ // A non-generic class is never equivalent to a generic class.
(ClassType::NonGeneric(_), _) | (_, ClassType::NonGeneric(_)) => {
ConstraintSet::from(false)
}
@@ -718,10 +1162,13 @@ impl<'db> ClassType<'db> {
/// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred.
pub(super) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
- let (class_literal, specialization) = self.class_literal(db);
- class_literal
- .metaclass(db)
- .apply_optional_specialization(db, specialization)
+ match self {
+ Self::NonGeneric(class) => class.metaclass(db),
+ Self::Generic(generic) => generic
+ .origin(db)
+ .metaclass(db)
+ .apply_optional_specialization(db, Some(generic.specialization(db))),
+ }
}
/// Return the [`DisjointBase`] that appears first in the MRO of this class.
@@ -836,8 +1283,15 @@ impl<'db> ClassType<'db> {
name: &str,
policy: MemberLookupPolicy,
) -> PlaceAndQualifiers<'db> {
- let (class_literal, specialization) = self.class_literal(db);
- class_literal.class_member_inner(db, specialization, name, policy)
+ match self {
+ Self::NonGeneric(class) => class.class_member(db, name, policy),
+ Self::Generic(generic) => generic.origin(db).class_member_inner(
+ db,
+ Some(generic.specialization(db)),
+ name,
+ policy,
+ ),
+ }
}
/// Returns the inferred type of the class member named `name`. Only bound members
@@ -869,7 +1323,9 @@ impl<'db> ClassType<'db> {
Signature::new(parameters, return_annotation)
}
- let (class_literal, specialization) = self.class_literal(db);
+ let Some((class_literal, specialization)) = self.static_class_literal(db) else {
+ return Member::unbound();
+ };
let fallback_member_lookup = || {
class_literal
@@ -1155,21 +1611,35 @@ impl<'db> ClassType<'db> {
///
/// See [`Type::instance_member`] for more details.
pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
- let (class_literal, specialization) = self.class_literal(db);
+ match self {
+ Self::NonGeneric(ClassLiteral::Dynamic(class)) => class.instance_member(db, name),
+ Self::NonGeneric(ClassLiteral::Static(class)) => {
+ if class.is_typed_dict(db) {
+ return Place::Undefined.into();
+ }
+ class.instance_member(db, None, name)
+ }
+ Self::Generic(generic) => {
+ let class_literal = generic.origin(db);
+ let specialization = Some(generic.specialization(db));
- if class_literal.is_typed_dict(db) {
- return Place::Undefined.into();
+ if class_literal.is_typed_dict(db) {
+ return Place::Undefined.into();
+ }
+
+ class_literal
+ .instance_member(db, specialization, name)
+ .map_type(|ty| ty.apply_optional_specialization(db, specialization))
+ }
}
-
- class_literal
- .instance_member(db, specialization, name)
- .map_type(|ty| ty.apply_optional_specialization(db, specialization))
}
/// A helper function for `instance_member` that looks up the `name` attribute only on
/// this class, not on its superclasses.
- fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> {
- let (class_literal, specialization) = self.class_literal(db);
+ pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> {
+ let Some((class_literal, specialization)) = self.static_class_literal(db) else {
+ return Member::unbound();
+ };
class_literal
.own_instance_member(db, name)
.map_type(|ty| ty.apply_optional_specialization(db, specialization))
@@ -1183,8 +1653,10 @@ impl<'db> ClassType<'db> {
// consolidate the two? Can we invoke a class by upcasting the class into a Callable, and
// then relying on the call binding machinery to Just Work™?
- let (class_literal, _) = self.class_literal(db);
- let class_generic_context = class_literal.generic_context(db);
+ // Dynamic classes don't have a generic context.
+ let class_generic_context = self
+ .static_class_literal(db)
+ .and_then(|(class_literal, _)| class_literal.generic_context(db));
let self_ty = Type::from(self);
let metaclass_dunder_call_function_symbol = self_ty
@@ -1364,11 +1836,16 @@ impl<'db> ClassType<'db> {
}
pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool {
- self.class_literal(db).0.is_protocol(db)
+ self.static_class_literal(db)
+ .is_some_and(|(class, _)| class.is_protocol(db))
}
- pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
- self.class_literal(db).0.header_span(db)
+ /// Returns a [`Span`] pointing to the definition of this class.
+ ///
+ /// For static classes, this is the class header (name and arguments).
+ /// For dynamic classes, this is the `type()` call expression.
+ pub(super) fn definition_span(self, db: &'db dyn Db) -> Span {
+ self.class_literal(db).header_span(db)
}
}
@@ -1386,6 +1863,24 @@ impl<'db> From> for ClassType<'db> {
}
}
+impl<'db> From> for Type<'db> {
+ fn from(class: ClassLiteral<'db>) -> Type<'db> {
+ Type::ClassLiteral(class)
+ }
+}
+
+impl<'db> From> for Type<'db> {
+ fn from(class: StaticClassLiteral<'db>) -> Type<'db> {
+ Type::ClassLiteral(class.into())
+ }
+}
+
+impl<'db> From> for Type<'db> {
+ fn from(class: DynamicClassLiteral<'db>) -> Type<'db> {
+ Type::ClassLiteral(class.into())
+ }
+}
+
impl<'db> From> for Type<'db> {
fn from(class: ClassType<'db>) -> Type<'db> {
match class {
@@ -1398,14 +1893,15 @@ impl<'db> From> for Type<'db> {
impl<'db> VarianceInferable<'db> for ClassType<'db> {
fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
match self {
- Self::NonGeneric(class) => class.variance_of(db, typevar),
+ Self::NonGeneric(ClassLiteral::Static(class)) => class.variance_of(db, typevar),
+ Self::NonGeneric(ClassLiteral::Dynamic(_)) => TypeVarVariance::Bivariant,
Self::Generic(generic) => generic.variance_of(db, typevar),
}
}
}
/// A filter that describes which methods are considered when looking for implicit attribute assignments
-/// in [`ClassLiteral::implicit_attribute`].
+/// in [`StaticClassLiteral::implicit_attribute`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(super) enum MethodDecorator {
None,
@@ -1512,10 +2008,10 @@ impl<'db> Field<'db> {
/// The id may change between runs, or when the class literal was garbage collected and recreated.
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
-pub struct ClassLiteral<'db> {
+pub struct StaticClassLiteral<'db> {
/// Name of the class at definition
#[returns(ref)]
- pub(crate) name: ast::name::Name,
+ pub(crate) name: Name,
pub(crate) body_scope: ScopeId<'db>,
@@ -1534,18 +2030,18 @@ pub struct ClassLiteral<'db> {
}
// The Salsa heap is tracked separately.
-impl get_size2::GetSize for ClassLiteral<'_> {}
+impl get_size2::GetSize for StaticClassLiteral<'_> {}
fn generic_context_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
- _self: ClassLiteral<'db>,
+ _self: StaticClassLiteral<'db>,
) -> Option> {
None
}
#[salsa::tracked]
-impl<'db> ClassLiteral<'db> {
+impl<'db> StaticClassLiteral<'db> {
/// Return `true` if this class represents `known_class`
pub(crate) fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
self.known(db) == Some(known_class)
@@ -1555,13 +2051,13 @@ impl<'db> ClassLiteral<'db> {
self.is_known(db, KnownClass::Tuple)
}
- /// Returns a new `ClassLiteral` with the given dataclass params, preserving all other fields.
+ /// Returns a new [`StaticClassLiteral`] with the given dataclass params, preserving all other fields.
pub(crate) fn with_dataclass_params(
self,
db: &'db dyn Db,
dataclass_params: Option>,
) -> Self {
- ClassLiteral::new(
+ Self::new(
db,
self.name(db).clone(),
self.body_scope(db),
@@ -1600,6 +2096,9 @@ impl<'db> ClassLiteral<'db> {
///
/// Following `functools.total_ordering` precedence, we prefer `__lt__` > `__le__` > `__gt__` >
/// `__ge__`, regardless of whether the method is defined locally or inherited.
+ ///
+ /// Note: We use direct scope lookups here to avoid infinite recursion
+ /// through `own_class_member` -> `own_synthesized_member`.
pub(super) fn total_ordering_root_method(
self,
db: &'db dyn Db,
@@ -1612,7 +2111,10 @@ impl<'db> ClassLiteral<'db> {
let Some(base_class) = base.into_class() else {
continue;
};
- let (base_literal, base_specialization) = base_class.class_literal(db);
+ let Some((base_literal, base_specialization)) = base_class.static_class_literal(db)
+ else {
+ continue;
+ };
if base_literal.is_known(db, KnownClass::Object) {
continue;
}
@@ -1763,7 +2265,7 @@ impl<'db> ClassLiteral<'db> {
f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>,
) -> ClassType<'db> {
match self.generic_context(db) {
- None => ClassType::NonGeneric(self),
+ None => ClassType::NonGeneric(self.into()),
Some(generic_context) => {
let specialization = f(generic_context);
@@ -1835,27 +2337,34 @@ impl<'db> ClassLiteral<'db> {
/// would depend on the class's AST and rerun for every change in that file.
#[salsa::tracked(returns(deref), cycle_initial=explicit_bases_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
pub(super) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
- tracing::trace!("ClassLiteral::explicit_bases_query: {}", self.name(db));
+ tracing::trace!(
+ "StaticClassLiteral::explicit_bases_query: {}",
+ self.name(db)
+ );
let module = parsed_module(db, self.file(db)).load(db);
let class_stmt = self.node(db, &module);
let class_definition =
semantic_index(db, self.file(db)).expect_single_definition(class_stmt);
- if self.is_known(db, KnownClass::VersionInfo) {
- let tuple_type = TupleType::new(db, &TupleSpec::version_info_spec(db))
- .expect("sys.version_info tuple spec should always be a valid tuple");
+ match self.known(db) {
+ Some(KnownClass::VersionInfo) => {
+ let tuple_type = TupleType::new(db, &TupleSpec::version_info_spec(db))
+ .expect("sys.version_info tuple spec should always be a valid tuple");
- Box::new([
- definition_expression_type(db, class_definition, &class_stmt.bases()[0]),
- Type::from(tuple_type.to_class_type(db)),
- ])
- } else {
- class_stmt
+ Box::new([
+ definition_expression_type(db, class_definition, &class_stmt.bases()[0]),
+ Type::from(tuple_type.to_class_type(db)),
+ ])
+ }
+ // Special-case `NotImplementedType`: typeshed says that it inherits from `Any`,
+ // but this causes more problems than it fixes.
+ Some(KnownClass::NotImplementedType) => Box::new([]),
+ _ => class_stmt
.bases()
.iter()
.map(|base_node| definition_expression_type(db, class_definition, base_node))
- .collect()
+ .collect(),
}
}
@@ -1915,7 +2424,7 @@ impl<'db> ClassLiteral<'db> {
/// Return the types of the decorators on this class
#[salsa::tracked(returns(deref), cycle_initial=decorators_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
fn decorators(self, db: &'db dyn Db) -> Box<[Type<'db>]> {
- tracing::trace!("ClassLiteral::decorators: {}", self.name(db));
+ tracing::trace!("StaticClassLiteral::decorators: {}", self.name(db));
let module = parsed_module(db, self.file(db)).load(db);
@@ -1972,7 +2481,7 @@ impl<'db> ClassLiteral<'db> {
pub(super) fn is_final(self, db: &'db dyn Db) -> bool {
self.known_function_decorators(db)
.contains(&KnownFunction::Final)
- || enum_metadata(db, self).is_some()
+ || enum_metadata(db, ClassLiteral::Static(self)).is_some()
}
/// Attempt to resolve the [method resolution order] ("MRO") for this class.
@@ -1990,15 +2499,15 @@ impl<'db> ClassLiteral<'db> {
db: &'db dyn Db,
specialization: Option>,
) -> Result, MroError<'db>> {
- tracing::trace!("ClassLiteral::try_mro: {}", self.name(db));
- Mro::of_class(db, self, specialization)
+ tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db));
+ Mro::of_static_class(db, self, specialization)
}
/// Iterate over the [method resolution order] ("MRO") of the class.
///
/// If the MRO could not be accurately resolved, this method falls back to iterating
/// over an MRO that has the class directly inheriting from `Unknown`. Use
- /// [`ClassLiteral::try_mro`] if you need to distinguish between the success and failure
+ /// [`StaticClassLiteral::try_mro`] if you need to distinguish between the success and failure
/// cases rather than simply iterating over the inferred resolution order for the class.
///
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
@@ -2007,7 +2516,7 @@ impl<'db> ClassLiteral<'db> {
db: &'db dyn Db,
specialization: Option>,
) -> MroIterator<'db> {
- MroIterator::new(db, self, specialization)
+ MroIterator::new(db, ClassLiteral::Static(self), specialization)
}
/// Return `true` if `other` is present in this class's MRO.
@@ -2077,14 +2586,6 @@ impl<'db> ClassLiteral<'db> {
.unwrap_or_else(|_| SubclassOfType::subclass_of_unknown())
}
- /// Return a type representing "the set of all instances of the metaclass of this class".
- pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> {
- self
- .metaclass(db)
- .to_instance(db)
- .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass")
- }
-
/// Return the metaclass of this class, or an error if the metaclass cannot be inferred.
#[salsa::tracked(cycle_initial=try_metaclass_cycle_initial,
heap_size=ruff_memory_usage::heap_size,
@@ -2093,7 +2594,7 @@ impl<'db> ClassLiteral<'db> {
self,
db: &'db dyn Db,
) -> Result<(Type<'db>, Option>), MetaclassError<'db>> {
- tracing::trace!("ClassLiteral::try_metaclass: {}", self.name(db));
+ tracing::trace!("StaticClassLiteral::try_metaclass: {}", self.name(db));
// Identify the class's own metaclass (or take the first base class's metaclass).
let mut base_classes = self.fully_static_explicit_bases(db).peekable();
@@ -2114,7 +2615,11 @@ impl<'db> ClassLiteral<'db> {
let (metaclass, class_metaclass_was_from) = if let Some(metaclass) = explicit_metaclass {
(metaclass, self)
} else if let Some(base_class) = base_classes.next() {
- let (base_class_literal, _) = base_class.class_literal(db);
+ // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking.
+ let base_class_literal = base_class
+ .static_class_literal(db)
+ .map(|(lit, _)| lit)
+ .unwrap_or(self);
(base_class.metaclass(db), base_class_literal)
} else {
(KnownClass::Type.to_class_literal(db), self)
@@ -2165,8 +2670,12 @@ impl<'db> ClassLiteral<'db> {
let Some(metaclass) = metaclass.to_class_type(db) else {
continue;
};
+ // For dynamic classes, we can't get a StaticClassLiteral, so use self for tracking.
+ let base_class_literal = base_class
+ .static_class_literal(db)
+ .map(|(lit, _)| lit)
+ .unwrap_or(self);
if metaclass.is_subclass_of(db, candidate.metaclass) {
- let (base_class_literal, _) = base_class.class_literal(db);
candidate = MetaclassCandidate {
metaclass,
explicit_metaclass_of: base_class_literal,
@@ -2176,7 +2685,6 @@ impl<'db> ClassLiteral<'db> {
if candidate.metaclass.is_subclass_of(db, metaclass) {
continue;
}
- let (base_class_literal, _) = base_class.class_literal(db);
return Err(MetaclassError {
kind: MetaclassErrorKind::Conflict {
candidate1: candidate,
@@ -2189,11 +2697,11 @@ impl<'db> ClassLiteral<'db> {
});
}
- let (metaclass_literal, _) = candidate.metaclass.class_literal(db);
- Ok((
- candidate.metaclass.into(),
- metaclass_literal.dataclass_transformer_params(db),
- ))
+ let dataclass_transformer_params = candidate
+ .metaclass
+ .static_class_literal(db)
+ .and_then(|(metaclass_literal, _)| metaclass_literal.dataclass_transformer_params(db));
+ Ok((candidate.metaclass.into(), dataclass_transformer_params))
}
/// Returns the class member of this class named `name`.
@@ -2251,105 +2759,34 @@ impl<'db> ClassLiteral<'db> {
policy: MemberLookupPolicy,
mro_iter: impl Iterator
- >,
) -> PlaceAndQualifiers<'db> {
- // If we encounter a dynamic type in this class's MRO, we'll save that dynamic type
- // in this variable. After we've traversed the MRO, we'll either:
- // (1) Use that dynamic type as the type for this attribute,
- // if no other classes in the MRO define the attribute; or,
- // (2) Intersect that dynamic type with the type of the attribute
- // from the non-dynamic members of the class's MRO.
- let mut dynamic_type_to_intersect_with: Option> = None;
+ let result = MroLookup::new(db, mro_iter).class_member(
+ name,
+ policy,
+ self.inherited_generic_context(db),
+ self.is_known(db, KnownClass::Object),
+ );
- let mut lookup_result: LookupResult<'db> =
- Err(LookupError::Undefined(TypeQualifiers::empty()));
+ match result {
+ ClassMemberResult::Done(result) => result.finalize(db),
- for superclass in mro_iter {
- match superclass {
- ClassBase::Generic | ClassBase::Protocol => {
- // Skip over these very special class bases that aren't really classes.
- }
- ClassBase::Dynamic(_) => {
- // Note: calling `Type::from(superclass).member()` would be incorrect here.
- // What we'd really want is a `Type::Any.own_class_member()` method,
- // but adding such a method wouldn't make much sense -- it would always return `Any`!
- dynamic_type_to_intersect_with.get_or_insert(Type::from(superclass));
- }
- ClassBase::Class(class) => {
- let known = class.known(db);
-
- if known == Some(KnownClass::Object)
- // Only exclude `object` members if this is not an `object` class itself
- && (policy.mro_no_object_fallback() && !self.is_known(db, KnownClass::Object))
- {
- continue;
- }
-
- if known == Some(KnownClass::Type) && policy.meta_class_no_type_fallback() {
- continue;
- }
-
- if matches!(known, Some(KnownClass::Int | KnownClass::Str))
- && policy.mro_no_int_or_str_fallback()
- {
- continue;
- }
-
- lookup_result = lookup_result.or_else(|lookup_error| {
- lookup_error.or_fall_back_to(
- db,
- class
- .own_class_member(db, self.inherited_generic_context(db), name)
- .inner,
- )
- });
- }
- ClassBase::TypedDict => {
- return KnownClass::TypedDictFallback
- .to_class_literal(db)
- .find_name_in_mro_with_policy(db, name, policy)
- .expect("Will return Some() when called on class literal")
- .map_type(|ty| {
- ty.apply_type_mapping(
+ ClassMemberResult::TypedDict => KnownClass::TypedDictFallback
+ .to_class_literal(db)
+ .find_name_in_mro_with_policy(db, name, policy)
+ .expect("Will return Some() when called on class literal")
+ .map_type(|ty| {
+ ty.apply_type_mapping(
+ db,
+ &TypeMapping::ReplaceSelf {
+ new_upper_bound: determine_upper_bound(
db,
- &TypeMapping::ReplaceSelf {
- new_upper_bound: determine_upper_bound(
- db,
- self,
- None,
- ClassBase::is_typed_dict,
- ),
- },
- TypeContext::default(),
- )
- });
- }
- }
- if lookup_result.is_ok() {
- break;
- }
- }
-
- match (
- PlaceAndQualifiers::from(lookup_result),
- dynamic_type_to_intersect_with,
- ) {
- (symbol_and_qualifiers, None) => symbol_and_qualifiers,
-
- (
- PlaceAndQualifiers {
- place: Place::Defined(DefinedPlace { ty, .. }),
- qualifiers,
- },
- Some(dynamic_type),
- ) => Place::bound(IntersectionType::from_elements(db, [ty, dynamic_type]))
- .with_qualifiers(qualifiers),
-
- (
- PlaceAndQualifiers {
- place: Place::Undefined,
- qualifiers,
- },
- Some(dynamic_type),
- ) => Place::bound(dynamic_type).with_qualifiers(qualifiers),
+ self,
+ None,
+ ClassBase::is_typed_dict,
+ ),
+ },
+ TypeContext::default(),
+ )
+ }),
}
}
@@ -2357,7 +2794,7 @@ impl<'db> ClassLiteral<'db> {
/// or those marked as `ClassVars` are considered.
///
/// Returns [`Place::Undefined`] if `name` cannot be found in this class's scope
- /// directly. Use [`ClassLiteral::class_member`] if you require a method that will
+ /// directly. Use [`StaticClassLiteral::class_member`] if you require a method that will
/// traverse through the MRO until it finds the member.
pub(super) fn own_class_member(
self,
@@ -2368,7 +2805,7 @@ impl<'db> ClassLiteral<'db> {
) -> Member<'db> {
// Check if this class is dataclass-like (either via @dataclass or via dataclass_transform)
if matches!(
- CodeGeneratorKind::from_class(db, self, specialization),
+ CodeGeneratorKind::from_class(db, self.into(), specialization),
Some(CodeGeneratorKind::DataclassLike(_))
) {
if name == "__dataclass_fields__" {
@@ -2391,7 +2828,7 @@ impl<'db> ClassLiteral<'db> {
}
}
- if CodeGeneratorKind::NamedTuple.matches(db, self, specialization) {
+ if CodeGeneratorKind::NamedTuple.matches(db, self.into(), specialization) {
if let Some(field) = self
.own_fields(db, specialization, CodeGeneratorKind::NamedTuple)
.get(name)
@@ -2477,14 +2914,17 @@ impl<'db> ClassLiteral<'db> {
// inherited from a superclass, excluding `object`).
//
// Only synthesize methods that are not already defined in the MRO.
+ // Note: We use direct scope lookups here to avoid infinite recursion
+ // through `own_class_member` -> `own_synthesized_member`.
if self.total_ordering(db)
&& matches!(name, "__lt__" | "__le__" | "__gt__" | "__ge__")
&& !self
.iter_mro(db, specialization)
.filter_map(ClassBase::into_class)
- .filter(|class| !class.class_literal(db).0.is_known(db, KnownClass::Object))
- .any(|class| {
- class_member(db, class.class_literal(db).0.body_scope(db), name)
+ .filter_map(|class| class.static_class_literal(db))
+ .filter(|(class, _)| !class.is_known(db, KnownClass::Object))
+ .any(|(class, _)| {
+ class_member(db, class.body_scope(db), name)
.ignore_possibly_undefined()
.is_some()
})
@@ -2515,7 +2955,7 @@ impl<'db> ClassLiteral<'db> {
return Some(synthesized_callables.into_type(db));
}
- let field_policy = CodeGeneratorKind::from_class(db, self, specialization)?;
+ let field_policy = CodeGeneratorKind::from_class(db, self.into(), specialization)?;
let mut transformer_params =
if let CodeGeneratorKind::DataclassLike(Some(transformer_params)) = field_policy {
@@ -2635,7 +3075,7 @@ impl<'db> ClassLiteral<'db> {
if let Some(ref mut default_ty) = default_ty {
*default_ty = default_ty
- .try_call_dunder_get(db, Type::none(db), Type::ClassLiteral(self))
+ .try_call_dunder_get(db, Type::none(db), Type::from(self))
.map(|(return_ty, _)| return_ty)
.unwrap_or_else(Type::unknown);
}
@@ -2810,6 +3250,7 @@ impl<'db> ClassLiteral<'db> {
KnownClass::NamedTupleFallback
.to_class_literal(db)
.as_class_literal()?
+ .as_static()?
.own_class_member(db, self.inherited_generic_context(db), None, name)
.ignore_possibly_undefined()
.map(|ty| {
@@ -3268,7 +3709,7 @@ impl<'db> ClassLiteral<'db> {
/// Returns a list of all annotated attributes defined in this class, or any of its superclasses.
///
- /// See [`ClassLiteral::own_fields`] for more details.
+ /// See [`StaticClassLiteral::own_fields`] for more details.
#[salsa::tracked(
returns(ref),
cycle_initial=fields_cycle_initial,
@@ -3285,22 +3726,20 @@ impl<'db> ClassLiteral<'db> {
return self.own_fields(db, specialization, field_policy);
}
- let matching_classes_in_mro: Vec<_> = self
- .iter_mro(db, specialization)
- .filter_map(|superclass| {
- if let Some(class) = superclass.into_class() {
- let (class_literal, specialization) = class.class_literal(db);
- if field_policy.matches(db, class_literal, specialization) {
+ let matching_classes_in_mro: Vec<(StaticClassLiteral<'db>, Option>)> =
+ self.iter_mro(db, specialization)
+ .filter_map(|superclass| {
+ let class = superclass.into_class()?;
+ // Dynamic classes don't have fields (no class body).
+ let (class_literal, specialization) = class.static_class_literal(db)?;
+ if field_policy.matches(db, class_literal.into(), specialization) {
Some((class_literal, specialization))
} else {
None
}
- } else {
- None
- }
- })
- // We need to collect into a `Vec` here because we iterate the MRO in reverse order
- .collect();
+ })
+ // We need to collect into a `Vec` here because we iterate the MRO in reverse order
+ .collect();
matching_classes_in_mro
.into_iter()
@@ -3490,87 +3929,20 @@ impl<'db> ClassLiteral<'db> {
return Place::Undefined.into();
}
- let mut union = UnionBuilder::new(db);
- let mut union_qualifiers = TypeQualifiers::empty();
- let mut is_definitely_bound = false;
-
- for superclass in self.iter_mro(db, specialization) {
- match superclass {
- ClassBase::Generic | ClassBase::Protocol => {
- // Skip over these very special class bases that aren't really classes.
- }
- ClassBase::Dynamic(_) => {
- return PlaceAndQualifiers::todo(
- "instance attribute on class with dynamic base",
- );
- }
- ClassBase::Class(class) => {
- if let member @ PlaceAndQualifiers {
- place:
- Place::Defined(DefinedPlace {
- ty,
- origin,
- definedness: boundness,
- ..
- }),
- qualifiers,
- } = class.own_instance_member(db, name).inner
- {
- if boundness == Definedness::AlwaysDefined {
- if origin.is_declared() {
- // We found a definitely-declared attribute. Discard possibly collected
- // inferred types from subclasses and return the declared type.
- return member;
- }
-
- is_definitely_bound = true;
- }
-
- // If the attribute is not definitely declared on this class, keep looking higher
- // up in the MRO, and build a union of all inferred types (and possibly-declared
- // types):
- union = union.add(ty);
-
- // TODO: We could raise a diagnostic here if there are conflicting type qualifiers
- union_qualifiers |= qualifiers;
- }
- }
- ClassBase::TypedDict => {
- return KnownClass::TypedDictFallback
- .to_instance(db)
- .instance_member(db, name)
- .map_type(|ty| {
- ty.apply_type_mapping(
- db,
- &TypeMapping::ReplaceSelf {
- new_upper_bound: Type::instance(
- db,
- self.unknown_specialization(db),
- ),
- },
- TypeContext::default(),
- )
- });
- }
- }
- }
-
- if union.is_empty() {
- PlaceAndQualifiers::default()
- } else {
- let boundness = if is_definitely_bound {
- Definedness::AlwaysDefined
- } else {
- Definedness::PossiblyUndefined
- };
-
- Place::Defined(DefinedPlace {
- ty: union.build(),
- origin: TypeOrigin::Inferred,
- definedness: boundness,
- widening: Widening::None,
- })
- .with_qualifiers(union_qualifiers)
+ match MroLookup::new(db, self.iter_mro(db, specialization)).instance_member(name) {
+ InstanceMemberResult::Done(result) => result,
+ InstanceMemberResult::TypedDict => KnownClass::TypedDictFallback
+ .to_instance(db)
+ .instance_member(db, name)
+ .map_type(|ty| {
+ ty.apply_type_mapping(
+ db,
+ &TypeMapping::ReplaceSelf {
+ new_upper_bound: Type::instance(db, self.unknown_specialization(db)),
+ },
+ TypeContext::default(),
+ )
+ }),
}
}
@@ -4063,7 +4435,7 @@ impl<'db> ClassLiteral<'db> {
}
pub(super) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> {
- Type::instance(db, ClassType::NonGeneric(self))
+ Type::instance(db, ClassType::NonGeneric(self.into()))
}
/// Return this class' involvement in an inheritance cycle, if any.
@@ -4077,17 +4449,20 @@ impl<'db> ClassLiteral<'db> {
/// Also, populates `visited_classes` with all base classes of `self`.
fn is_cyclically_defined_recursive<'db>(
db: &'db dyn Db,
- class: ClassLiteral<'db>,
- classes_on_stack: &mut IndexSet>,
- visited_classes: &mut IndexSet>,
+ class: StaticClassLiteral<'db>,
+ classes_on_stack: &mut IndexSet>,
+ visited_classes: &mut IndexSet>,
) -> bool {
let mut result = false;
for explicit_base in class.explicit_bases(db) {
let explicit_base_class_literal = match explicit_base {
- Type::ClassLiteral(class_literal) => *class_literal,
- Type::GenericAlias(generic_alias) => generic_alias.origin(db),
+ Type::ClassLiteral(class_literal) => class_literal.as_static(),
+ Type::GenericAlias(generic_alias) => Some(generic_alias.origin(db)),
_ => continue,
};
+ let Some(explicit_base_class_literal) = explicit_base_class_literal else {
+ continue;
+ };
if !classes_on_stack.insert(explicit_base_class_literal) {
return true;
}
@@ -4146,20 +4521,10 @@ impl<'db> ClassLiteral<'db> {
.unwrap_or_else(|| class_name.end()),
)
}
-
- pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> {
- QualifiedClassName { db, class: self }
- }
-}
-
-impl<'db> From> for Type<'db> {
- fn from(class: ClassLiteral<'db>) -> Type<'db> {
- Type::ClassLiteral(class)
- }
}
#[salsa::tracked]
-impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
+impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> {
#[salsa::tracked(cycle_initial=crate::types::variance_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
let typevar_in_generic_context = self
@@ -4180,7 +4545,7 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
.map(|class| class.variance_of(db, typevar));
let default_attribute_variance = {
- let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self, None);
+ let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self.into(), None);
// Python 3.13 introduced a synthesized `__replace__` method on dataclasses which uses
// their field types in contravariant position, thus meaning a frozen dataclass must
// still be invariant in its field types. Other synthesized methods on dataclasses are
@@ -4280,6 +4645,538 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
}
}
+impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
+ fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance {
+ match self {
+ Self::Static(class) => class.variance_of(db, typevar),
+ Self::Dynamic(_) => TypeVarVariance::Bivariant,
+ }
+ }
+}
+
+/// A class created dynamically via a three-argument `type()` call.
+///
+/// For example:
+/// ```python
+/// Foo = type("Foo", (Base,), {"attr": 1})
+/// ```
+///
+/// The type of `Foo` would be `` where `Foo` is a `DynamicClassLiteral` with:
+/// - name: "Foo"
+/// - bases: [Base]
+///
+/// # Limitations
+///
+/// TODO: Attributes from the namespace dict (third argument to `type()`) are not tracked.
+/// This matches Pyright's behavior. For example:
+///
+/// ```python
+/// Foo = type("Foo", (), {"attr": 42})
+/// Foo().attr # Error: no attribute 'attr'
+/// ```
+///
+/// Supporting namespace dict attributes would require parsing dict literals and tracking
+/// the attribute types, similar to how TypedDict handles its fields.
+///
+/// # Salsa interning
+///
+/// Each `type()` call is uniquely identified by its [`Definition`], which provides
+/// stable identity without depending on AST node indices that can change when code
+/// is inserted above the call site.
+///
+/// Two different `type()` calls always produce distinct `DynamicClassLiteral`
+/// instances, even if they have the same name and bases:
+///
+/// ```python
+/// Foo1 = type("Foo", (Base,), {})
+/// Foo2 = type("Foo", (Base,), {})
+/// # Foo1 and Foo2 are distinct types
+/// ```
+///
+/// Note: Only assigned `type()` calls are currently supported (e.g., `Foo = type(...)`).
+/// Inline calls like `process(type(...))` fall back to normal call handling.
+#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)]
+#[derive(PartialOrd, Ord)]
+pub struct DynamicClassLiteral<'db> {
+ /// The name of the class (from the first argument to `type()`).
+ #[returns(ref)]
+ pub name: Name,
+
+ /// The base classes (from the second argument to `type()`).
+ #[returns(deref)]
+ pub bases: Box<[ClassBase<'db>]>,
+
+ /// The definition where this class is created.
+ pub definition: Definition<'db>,
+
+ /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator
+ /// or passed to `dataclass()` as a function.
+ pub dataclass_params: Option>,
+}
+
+impl get_size2::GetSize for DynamicClassLiteral<'_> {}
+
+#[salsa::tracked]
+impl<'db> DynamicClassLiteral<'db> {
+ /// Returns a [`Span`] with the range of the `type()` call expression.
+ ///
+ /// See [`Self::header_range`] for more details.
+ pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
+ Span::from(self.file(db)).with_range(self.header_range(db))
+ }
+
+ /// Returns the range of the `type()` call expression that created this class.
+ pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange {
+ let definition = self.definition(db);
+ let file = definition.file(db);
+ let module = parsed_module(db, file).load(db);
+
+ // Dynamic classes are only created from regular assignments (e.g., `Foo = type(...)`).
+ let DefinitionKind::Assignment(assignment) = definition.kind(db) else {
+ unreachable!("DynamicClassLiteral should only be created from Assignment definitions");
+ };
+ assignment.value(&module).range()
+ }
+
+ /// Returns the file containing the `type()` call.
+ pub(crate) fn file(self, db: &'db dyn Db) -> File {
+ self.definition(db).file(db)
+ }
+
+ /// Returns the scope containing the `type()` call.
+ pub(crate) fn file_scope(self, db: &'db dyn Db) -> FileScopeId {
+ self.definition(db).file_scope(db)
+ }
+
+ /// Get the metaclass of this dynamic class.
+ ///
+ /// Derives the metaclass from base classes: finds the most derived metaclass
+ /// that is a subclass of all other base metaclasses.
+ ///
+ /// See
+ pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
+ self.try_metaclass(db)
+ .unwrap_or_else(|_| SubclassOfType::subclass_of_unknown())
+ }
+
+ /// Try to get the metaclass of this dynamic class.
+ ///
+ /// Returns `Err(DynamicMetaclassConflict)` if there's a metaclass conflict
+ /// (i.e., two base classes have metaclasses that are not in a subclass relationship).
+ ///
+ /// See
+ pub(crate) fn try_metaclass(
+ self,
+ db: &'db dyn Db,
+ ) -> Result, DynamicMetaclassConflict<'db>> {
+ let bases = self.bases(db);
+
+ // If no bases, metaclass is `type`.
+ // To dynamically create a class with no bases that has a custom metaclass,
+ // you have to invoke that metaclass rather than `type()`.
+ if bases.is_empty() {
+ return Ok(KnownClass::Type.to_class_literal(db));
+ }
+
+ // If there's an MRO error, return unknown to avoid cascading errors.
+ if self.try_mro(db).is_err() {
+ return Ok(SubclassOfType::subclass_of_unknown());
+ }
+
+ // Start with the first base's metaclass as the candidate.
+ let mut candidate = bases[0].metaclass(db);
+
+ // Track which base the candidate metaclass came from.
+ let (mut candidate_base, rest) = bases.split_first().unwrap();
+
+ // Reconcile with other bases' metaclasses.
+ for base in rest {
+ let base_metaclass = base.metaclass(db);
+
+ // Get the ClassType for comparison.
+ let Some(candidate_class) = candidate.to_class_type(db) else {
+ // If candidate isn't a class type, keep it as is.
+ continue;
+ };
+ let Some(base_metaclass_class) = base_metaclass.to_class_type(db) else {
+ continue;
+ };
+
+ // If base's metaclass is more derived, use it.
+ if base_metaclass_class.is_subclass_of(db, candidate_class) {
+ candidate = base_metaclass;
+ candidate_base = base;
+ continue;
+ }
+
+ // If candidate is already more derived, keep it.
+ if candidate_class.is_subclass_of(db, base_metaclass_class) {
+ continue;
+ }
+
+ // Conflict: neither metaclass is a subclass of the other.
+ // Python raises `TypeError: metaclass conflict` at runtime.
+ return Err(DynamicMetaclassConflict {
+ metaclass1: candidate_class,
+ base1: *candidate_base,
+ metaclass2: base_metaclass_class,
+ base2: *base,
+ });
+ }
+
+ Ok(candidate)
+ }
+
+ /// Iterate over the MRO of this class using C3 linearization.
+ ///
+ /// The MRO includes the class itself as the first element, followed
+ /// by the merged base class MROs (consistent with `ClassType::iter_mro`).
+ ///
+ /// If the MRO cannot be computed (e.g., due to inconsistent ordering), falls back
+ /// to iterating over base MROs sequentially with deduplication.
+ pub(crate) fn iter_mro(self, db: &'db dyn Db) -> MroIterator<'db> {
+ MroIterator::new(db, ClassLiteral::Dynamic(self), None)
+ }
+
+ /// Look up an instance member by iterating through the MRO.
+ pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
+ match MroLookup::new(db, self.iter_mro(db)).instance_member(name) {
+ InstanceMemberResult::Done(result) => result,
+ InstanceMemberResult::TypedDict => {
+ // Simplified `TypedDict` handling without type mapping.
+ KnownClass::TypedDictFallback
+ .to_instance(db)
+ .instance_member(db, name)
+ }
+ }
+ }
+
+ /// Look up a class-level member by iterating through the MRO.
+ ///
+ /// Uses `MroLookup` with:
+ /// - No inherited generic context (dynamic classes aren't generic).
+ /// - `is_self_object = false` (dynamic classes are never `object`).
+ pub(crate) fn class_member(
+ self,
+ db: &'db dyn Db,
+ name: &str,
+ policy: MemberLookupPolicy,
+ ) -> PlaceAndQualifiers<'db> {
+ // Check if this dynamic class is dataclass-like (via dataclass_transform inheritance).
+ if matches!(
+ CodeGeneratorKind::from_class(db, self.into(), None),
+ Some(CodeGeneratorKind::DataclassLike(_))
+ ) {
+ if name == "__dataclass_fields__" {
+ // Make this class look like a subclass of the `DataClassInstance` protocol.
+ return Place::declared(KnownClass::Dict.to_specialized_instance(
+ db,
+ &[
+ KnownClass::Str.to_instance(db),
+ KnownClass::Field.to_specialized_instance(db, &[Type::any()]),
+ ],
+ ))
+ .with_qualifiers(TypeQualifiers::CLASS_VAR);
+ } else if name == "__dataclass_params__" {
+ // There is no typeshed class for this. For now, we model it as `Any`.
+ return Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR);
+ }
+ }
+
+ let result = MroLookup::new(db, self.iter_mro(db)).class_member(
+ name, policy, None, // No inherited generic context.
+ false, // Dynamic classes are never `object`.
+ );
+
+ match result {
+ ClassMemberResult::Done(result) => result.finalize(db),
+ ClassMemberResult::TypedDict => {
+ // Simplified `TypedDict` handling without type mapping.
+ KnownClass::TypedDictFallback
+ .to_class_literal(db)
+ .find_name_in_mro_with_policy(db, name, policy)
+ .expect("Will return Some() when called on class literal")
+ }
+ }
+ }
+
+ /// Try to compute the MRO for this dynamic class.
+ ///
+ /// Returns `Ok(Mro)` if successful, or `Err(DynamicMroError)` if there's
+ /// an error (duplicate bases or C3 linearization failure).
+ #[salsa::tracked(returns(ref), heap_size = ruff_memory_usage::heap_size)]
+ pub(crate) fn try_mro(self, db: &'db dyn Db) -> Result, DynamicMroError<'db>> {
+ Mro::of_dynamic_class(db, self)
+ }
+
+ /// Returns a new [`DynamicClassLiteral`] with the given dataclass params, preserving all other fields.
+ pub(crate) fn with_dataclass_params(
+ self,
+ db: &'db dyn Db,
+ dataclass_params: Option>,
+ ) -> Self {
+ Self::new(
+ db,
+ self.name(db).clone(),
+ self.bases(db),
+ self.definition(db),
+ dataclass_params,
+ )
+ }
+}
+
+/// Error for metaclass conflicts in dynamic classes.
+///
+/// This mirrors `MetaclassErrorKind::Conflict` for regular classes.
+#[derive(Debug, Clone)]
+pub(crate) struct DynamicMetaclassConflict<'db> {
+ /// The first conflicting metaclass and its originating base class.
+ pub(crate) metaclass1: ClassType<'db>,
+ pub(crate) base1: ClassBase<'db>,
+ /// The second conflicting metaclass and its originating base class.
+ pub(crate) metaclass2: ClassType<'db>,
+ pub(crate) base2: ClassBase<'db>,
+}
+
+/// Performs member lookups over an MRO (Method Resolution Order).
+///
+/// This struct encapsulates the shared logic for looking up class and instance
+/// members by iterating through an MRO. Both `StaticClassLiteral` and `DynamicClassLiteral`
+/// use this to avoid duplicating the MRO traversal logic.
+pub(super) struct MroLookup<'db, I> {
+ db: &'db dyn Db,
+ mro_iter: I,
+}
+
+impl<'db, I: Iterator
- >> MroLookup<'db, I> {
+ /// Create a new MRO lookup from a database and an MRO iterator.
+ pub(super) fn new(db: &'db dyn Db, mro_iter: I) -> Self {
+ Self { db, mro_iter }
+ }
+
+ /// Look up a class member by iterating through the MRO.
+ ///
+ /// Parameters:
+ /// - `name`: The member name to look up
+ /// - `policy`: Controls which classes in the MRO to skip
+ /// - `inherited_generic_context`: Generic context for `own_class_member` calls
+ /// - `is_self_object`: Whether the class itself is `object` (affects policy filtering)
+ ///
+ /// Returns `ClassMemberResult::TypedDict` if a `TypedDict` base is encountered,
+ /// allowing the caller to handle this case specially.
+ ///
+ /// If we encounter a dynamic type in the MRO, we save it and after traversal:
+ /// 1. Use it as the type if no other classes define the attribute, or
+ /// 2. Intersect it with the type from non-dynamic MRO members.
+ pub(super) fn class_member(
+ self,
+ name: &str,
+ policy: MemberLookupPolicy,
+ inherited_generic_context: Option>,
+ is_self_object: bool,
+ ) -> ClassMemberResult<'db> {
+ let db = self.db;
+ let mut dynamic_type: Option> = None;
+ let mut lookup_result: LookupResult<'db> =
+ Err(LookupError::Undefined(TypeQualifiers::empty()));
+
+ for superclass in self.mro_iter {
+ match superclass {
+ ClassBase::Generic | ClassBase::Protocol => {
+ // Skip over these very special class bases that aren't really classes.
+ }
+ ClassBase::Dynamic(_) => {
+ // Note: calling `Type::from(superclass).member()` would be incorrect here.
+ // What we'd really want is a `Type::Any.own_class_member()` method,
+ // but adding such a method wouldn't make much sense -- it would always return `Any`!
+ dynamic_type.get_or_insert(Type::from(superclass));
+ }
+ ClassBase::Class(class) => {
+ let known = class.known(db);
+
+ // Only exclude `object` members if this is not an `object` class itself
+ if known == Some(KnownClass::Object)
+ && policy.mro_no_object_fallback()
+ && !is_self_object
+ {
+ continue;
+ }
+
+ if known == Some(KnownClass::Type) && policy.meta_class_no_type_fallback() {
+ continue;
+ }
+
+ if matches!(known, Some(KnownClass::Int | KnownClass::Str))
+ && policy.mro_no_int_or_str_fallback()
+ {
+ continue;
+ }
+
+ lookup_result = lookup_result.or_else(|lookup_error| {
+ lookup_error.or_fall_back_to(
+ db,
+ class
+ .own_class_member(db, inherited_generic_context, name)
+ .inner,
+ )
+ });
+ }
+ ClassBase::TypedDict => {
+ return ClassMemberResult::TypedDict;
+ }
+ }
+ if lookup_result.is_ok() {
+ break;
+ }
+ }
+
+ ClassMemberResult::Done(CompletedMemberLookup {
+ lookup_result,
+ dynamic_type,
+ })
+ }
+
+ /// Look up an instance member by iterating through the MRO.
+ ///
+ /// Unlike class member lookup, instance member lookup:
+ /// - Uses `own_instance_member` to check each class
+ /// - Builds a union of inferred types from multiple classes
+ /// - Stops on the first definitely-declared attribute
+ ///
+ /// Returns `InstanceMemberResult::TypedDict` if a `TypedDict` base is encountered,
+ /// allowing the caller to handle this case specially.
+ pub(super) fn instance_member(self, name: &str) -> InstanceMemberResult<'db> {
+ let db = self.db;
+ let mut union = UnionBuilder::new(db);
+ let mut union_qualifiers = TypeQualifiers::empty();
+ let mut is_definitely_bound = false;
+
+ for superclass in self.mro_iter {
+ match superclass {
+ ClassBase::Generic | ClassBase::Protocol => {
+ // Skip over these very special class bases that aren't really classes.
+ }
+ ClassBase::Dynamic(_) => {
+ return InstanceMemberResult::Done(PlaceAndQualifiers::todo(
+ "instance attribute on class with dynamic base",
+ ));
+ }
+ ClassBase::Class(class) => {
+ if let member @ PlaceAndQualifiers {
+ place:
+ Place::Defined(DefinedPlace {
+ ty,
+ origin,
+ definedness: boundness,
+ ..
+ }),
+ qualifiers,
+ } = class.own_instance_member(db, name).inner
+ {
+ if boundness == Definedness::AlwaysDefined {
+ if origin.is_declared() {
+ // We found a definitely-declared attribute. Discard possibly collected
+ // inferred types from subclasses and return the declared type.
+ return InstanceMemberResult::Done(member);
+ }
+
+ is_definitely_bound = true;
+ }
+
+ // If the attribute is not definitely declared on this class, keep looking
+ // higher up in the MRO, and build a union of all inferred types (and
+ // possibly-declared types):
+ union = union.add(ty);
+
+ // TODO: We could raise a diagnostic here if there are conflicting type
+ // qualifiers
+ union_qualifiers |= qualifiers;
+ }
+ }
+ ClassBase::TypedDict => {
+ return InstanceMemberResult::TypedDict;
+ }
+ }
+ }
+
+ let result = if union.is_empty() {
+ Place::Undefined.with_qualifiers(TypeQualifiers::empty())
+ } else {
+ let boundness = if is_definitely_bound {
+ Definedness::AlwaysDefined
+ } else {
+ Definedness::PossiblyUndefined
+ };
+
+ Place::Defined(DefinedPlace {
+ ty: union.build(),
+ origin: TypeOrigin::Inferred,
+ definedness: boundness,
+ widening: Widening::None,
+ })
+ .with_qualifiers(union_qualifiers)
+ };
+
+ InstanceMemberResult::Done(result)
+ }
+}
+
+/// Result of class member lookup from MRO iteration.
+pub(super) enum ClassMemberResult<'db> {
+ /// Found the member or exhausted the MRO.
+ Done(CompletedMemberLookup<'db>),
+ /// Encountered a `TypedDict` base.
+ TypedDict,
+}
+
+pub(super) struct CompletedMemberLookup<'db> {
+ lookup_result: LookupResult<'db>,
+ dynamic_type: Option>,
+}
+
+impl<'db> CompletedMemberLookup<'db> {
+ /// Finalize the lookup result by handling dynamic type intersection.
+ pub(super) fn finalize(self, db: &'db dyn Db) -> PlaceAndQualifiers<'db> {
+ match (
+ PlaceAndQualifiers::from(self.lookup_result),
+ self.dynamic_type,
+ ) {
+ (symbol_and_qualifiers, None) => symbol_and_qualifiers,
+
+ (
+ PlaceAndQualifiers {
+ place: Place::Defined(DefinedPlace { ty, .. }),
+ qualifiers,
+ },
+ Some(dynamic),
+ ) => Place::bound(
+ IntersectionBuilder::new(db)
+ .add_positive(ty)
+ .add_positive(dynamic)
+ .build(),
+ )
+ .with_qualifiers(qualifiers),
+
+ (
+ PlaceAndQualifiers {
+ place: Place::Undefined,
+ qualifiers,
+ },
+ Some(dynamic),
+ ) => Place::bound(dynamic).with_qualifiers(qualifiers),
+ }
+ }
+}
+
+/// Result of instance member lookup from MRO iteration.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub(super) enum InstanceMemberResult<'db> {
+ /// Found the member or exhausted the MRO
+ Done(PlaceAndQualifiers<'db>),
+ /// Encountered a `TypedDict` base - caller should handle this specially
+ TypedDict,
+}
+
// N.B. It would be incorrect to derive `Eq`, `PartialEq`, or `Hash` for this struct,
// because two `QualifiedClassName` instances might refer to different classes but
// have the same components. You'd expect them to compare equal, but they'd compare
@@ -4290,7 +5187,11 @@ pub(super) struct QualifiedClassName<'db> {
class: ClassLiteral<'db>,
}
-impl QualifiedClassName<'_> {
+impl<'db> QualifiedClassName<'db> {
+ pub(super) fn from_class_literal(db: &'db dyn Db, class: ClassLiteral<'db>) -> Self {
+ Self { db, class }
+ }
+
/// Returns the components of the qualified name of this class, excluding this class itself.
///
/// For example, calling this method on a class `C` in the module `a.b` would return
@@ -4298,16 +5199,28 @@ impl QualifiedClassName<'_> {
/// `m` inside the namespace of a class `C` in the module `a.b` would return
/// `["a", "b", "C", ""]`.
pub(super) fn components_excluding_self(&self) -> Vec {
- let body_scope = self.class.body_scope(self.db);
- let file = body_scope.file(self.db);
+ let (file, file_scope_id, skip_count) = match self.class {
+ ClassLiteral::Static(class) => {
+ let body_scope = class.body_scope(self.db);
+ // Skip the class body scope itself.
+ (
+ body_scope.file(self.db),
+ body_scope.file_scope_id(self.db),
+ 1,
+ )
+ }
+ ClassLiteral::Dynamic(class) => {
+ // Dynamic classes don't have a body scope; start from the enclosing scope.
+ (class.file(self.db), class.file_scope(self.db), 0)
+ }
+ };
+
let module_ast = parsed_module(self.db, file).load(self.db);
let index = semantic_index(self.db, file);
- let file_scope_id = body_scope.file_scope_id(self.db);
let mut name_parts = vec![];
- // Skips itself
- for (_, ancestor_scope) in index.ancestor_scopes(file_scope_id).skip(1) {
+ for (_, ancestor_scope) in index.ancestor_scopes(file_scope_id).skip(skip_count) {
let node = ancestor_scope.node();
match ancestor_scope.kind() {
@@ -4383,14 +5296,14 @@ impl InheritanceCycle {
/// [PEP 800]: https://peps.python.org/pep-0800/
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub(super) struct DisjointBase<'db> {
- pub(super) class: ClassLiteral<'db>,
+ pub(super) class: StaticClassLiteral<'db>,
pub(super) kind: DisjointBaseKind,
}
impl<'db> DisjointBase<'db> {
/// Creates a [`DisjointBase`] instance where we know the class is a disjoint base
/// because it has the `@disjoint_base` decorator on its definition
- fn due_to_decorator(class: ClassLiteral<'db>) -> Self {
+ fn due_to_decorator(class: StaticClassLiteral<'db>) -> Self {
Self {
class,
kind: DisjointBaseKind::DisjointBaseDecorator,
@@ -4399,7 +5312,7 @@ impl<'db> DisjointBase<'db> {
/// Creates a [`DisjointBase`] instance where we know the class is a disjoint base
/// because of its `__slots__` definition.
- fn due_to_dunder_slots(class: ClassLiteral<'db>) -> Self {
+ fn due_to_dunder_slots(class: StaticClassLiteral<'db>) -> Self {
Self {
class,
kind: DisjointBaseKind::DefinesSlots,
@@ -5284,17 +6197,13 @@ impl KnownClass {
T: Into]>>,
'db: 't,
{
- fn inner<'db>(
+ fn to_specialized_class_type_impl<'db>(
db: &'db dyn Db,
class: KnownClass,
+ class_literal: StaticClassLiteral<'db>,
specialization: Cow<[Type<'db>]>,
- ) -> Option> {
- let Type::ClassLiteral(class_literal) = class.to_class_literal(db) else {
- return None;
- };
-
- let generic_context = class_literal.generic_context(db)?;
-
+ generic_context: GenericContext<'db>,
+ ) -> ClassType<'db> {
if specialization.len() != generic_context.len(db) {
// a cache of the `KnownClass`es that we have already seen mismatched-arity
// specializations for (and therefore that we've already logged a warning for)
@@ -5307,17 +6216,24 @@ impl KnownClass {
class.display(db)
);
}
- return Some(class_literal.default_specialization(db));
+ return class_literal.default_specialization(db);
}
- Some(
- class_literal
- .apply_specialization(db, |_| generic_context.specialize(db, specialization)),
- )
+ class_literal
+ .apply_specialization(db, |_| generic_context.specialize(db, specialization))
}
+ let class_literal = self.to_class_literal(db).as_class_literal()?.as_static()?;
+ let generic_context = class_literal.generic_context(db)?;
let specialization = specialization.into();
- inner(db, self, specialization)
+
+ Some(to_specialized_class_type_impl(
+ db,
+ self,
+ class_literal,
+ specialization,
+ generic_context,
+ ))
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
@@ -5352,16 +6268,16 @@ impl KnownClass {
fn try_to_class_literal_without_logging(
self,
db: &dyn Db,
- ) -> Result, KnownClassLookupError<'_>> {
+ ) -> Result, KnownClassLookupError<'_>> {
let symbol = known_module_symbol(db, self.canonical_module(db), self.name(db)).place;
match symbol {
Place::Defined(DefinedPlace {
- ty: Type::ClassLiteral(class_literal),
+ ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)),
definedness: Definedness::AlwaysDefined,
..
}) => Ok(class_literal),
Place::Defined(DefinedPlace {
- ty: Type::ClassLiteral(class_literal),
+ ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)),
definedness: Definedness::PossiblyUndefined,
..
}) => Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal }),
@@ -5375,7 +6291,7 @@ impl KnownClass {
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
- pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> {
+ pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> {
#[salsa::interned(heap_size=ruff_memory_usage::heap_size)]
struct KnownClassArgument {
class: KnownClass,
@@ -5385,7 +6301,7 @@ impl KnownClass {
_db: &'db dyn Db,
_id: salsa::Id,
_class: KnownClassArgument<'db>,
- ) -> Option> {
+ ) -> Option> {
None
}
@@ -5393,7 +6309,7 @@ impl KnownClass {
fn known_class_to_class_literal<'db>(
db: &'db dyn Db,
class: KnownClassArgument<'db>,
- ) -> Option> {
+ ) -> Option> {
let class = class.class(db);
class
.try_to_class_literal_without_logging(db)
@@ -5429,7 +6345,7 @@ impl KnownClass {
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type<'_> {
self.try_to_class_literal(db)
- .map(Type::ClassLiteral)
+ .map(|class| Type::ClassLiteral(ClassLiteral::Static(class)))
.unwrap_or_else(Type::unknown)
}
@@ -5987,7 +6903,7 @@ impl KnownClass {
};
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
- if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
+ if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class.into(), None) {
if let Some(builder) = context
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
{
@@ -6028,7 +6944,7 @@ impl KnownClass {
let bound_super = BoundSuperType::build(
db,
- Type::ClassLiteral(enclosing_class),
+ Type::ClassLiteral(ClassLiteral::Static(enclosing_class)),
first_param,
)
.unwrap_or_else(|err| {
@@ -6041,7 +6957,11 @@ impl KnownClass {
[Some(pivot_class_type), Some(owner_type)] => {
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) {
- if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
+ if CodeGeneratorKind::NamedTuple.matches(
+ db,
+ enclosing_class.into(),
+ None,
+ ) {
if let Some(builder) = context
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
{
@@ -6129,6 +7049,69 @@ impl KnownClass {
)));
}
+ KnownClass::Type => {
+ // Check for MRO and metaclass errors in three-argument type() calls.
+ if let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) =
+ overload.return_type()
+ {
+ // Check for MRO errors
+ if let Err(error) = dynamic_class.try_mro(db) {
+ match error.reason() {
+ DynamicMroErrorKind::DuplicateBases(duplicates) => {
+ if let Some(builder) =
+ context.report_lint(&DUPLICATE_BASE, call_expression)
+ {
+ builder.into_diagnostic(format_args!(
+ "Duplicate base class{maybe_s} {dupes} in class `{class}`",
+ maybe_s = if duplicates.len() == 1 { "" } else { "es" },
+ dupes = duplicates
+ .iter()
+ .map(|base: &ClassBase<'_>| base.display(db))
+ .join(", "),
+ class = dynamic_class.name(db),
+ ));
+ }
+ }
+ DynamicMroErrorKind::UnresolvableMro => {
+ if let Some(builder) =
+ context.report_lint(&INCONSISTENT_MRO, call_expression)
+ {
+ builder.into_diagnostic(format_args!(
+ "Cannot create a consistent method resolution order (MRO) \
+ for class `{}` with bases `[{}]`",
+ dynamic_class.name(db),
+ dynamic_class
+ .bases(db)
+ .iter()
+ .map(|base| base.display(db))
+ .join(", ")
+ ));
+ }
+ }
+ }
+ }
+
+ // Check for metaclass conflicts
+ if let Err(DynamicMetaclassConflict {
+ metaclass1,
+ base1,
+ metaclass2,
+ base2,
+ }) = dynamic_class.try_metaclass(db)
+ {
+ report_conflicting_metaclass_from_bases(
+ context,
+ call_expression.into(),
+ dynamic_class.name(db),
+ metaclass1,
+ base1.display(db),
+ metaclass2,
+ base2.display(db),
+ );
+ }
+ }
+ }
+
_ => {}
}
}
@@ -6144,7 +7127,9 @@ pub(crate) enum KnownClassLookupError<'db> {
SymbolNotAClass { found_type: Type<'db> },
/// There is a symbol by that name in the expected typeshed module,
/// and it's a class definition, but it's possibly unbound.
- ClassPossiblyUnbound { class_literal: ClassLiteral<'db> },
+ ClassPossiblyUnbound {
+ class_literal: StaticClassLiteral<'db>,
+ },
}
impl<'db> KnownClassLookupError<'db> {
@@ -6243,7 +7228,7 @@ enum SlotsKind {
}
impl SlotsKind {
- fn from(db: &dyn Db, base: ClassLiteral) -> Self {
+ fn from(db: &dyn Db, base: StaticClassLiteral) -> Self {
let Place::Defined(DefinedPlace {
ty: slots_ty,
definedness: bound,
diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs
index 07eda5753e..6d644fea5f 100644
--- a/crates/ty_python_semantic/src/types/class_base.rs
+++ b/crates/ty_python_semantic/src/types/class_base.rs
@@ -1,11 +1,13 @@
use crate::Db;
use crate::types::class::CodeGeneratorKind;
use crate::types::generics::{ApplySpecialization, Specialization};
+use crate::types::mro::MroIterator;
+
use crate::types::tuple::TupleType;
use crate::types::{
ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType,
- MaterializationKind, MroError, MroIterator, NormalizedVisitor, SpecialFormType, Type,
- TypeContext, TypeMapping, todo_type,
+ MaterializationKind, MroError, NormalizedVisitor, SpecialFormType, Type, TypeContext,
+ TypeMapping, todo_type,
};
/// Enumeration of the possible kinds of types we allow in class bases.
@@ -245,7 +247,8 @@ impl<'db> ClassBase<'db> {
SpecialFormType::Generic => Some(Self::Generic),
SpecialFormType::NamedTuple => {
- let fields = subclass.own_fields(db, None, CodeGeneratorKind::NamedTuple);
+ let class = subclass.as_static()?;
+ let fields = class.own_fields(db, None, CodeGeneratorKind::NamedTuple);
Self::try_from_type(
db,
TupleType::heterogeneous(
@@ -309,6 +312,16 @@ impl<'db> ClassBase<'db> {
}
}
+ /// Return the metaclass of this class base.
+ pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> {
+ match self {
+ Self::Class(class) => class.metaclass(db),
+ Self::Dynamic(dynamic) => Type::Dynamic(dynamic),
+ // TODO: all `Protocol` classes actually have `_ProtocolMeta` as their metaclass.
+ Self::Protocol | Self::Generic | Self::TypedDict => KnownClass::Type.to_instance(db),
+ }
+ }
+
fn apply_type_mapping_impl<'a>(
self,
db: &'db dyn Db,
@@ -359,7 +372,13 @@ impl<'db> ClassBase<'db> {
pub(super) fn has_cyclic_mro(self, db: &'db dyn Db) -> bool {
match self {
ClassBase::Class(class) => {
- let (class_literal, specialization) = class.class_literal(db);
+ let Some((class_literal, specialization)) = class.static_class_literal(db) else {
+ // Dynamic classes can't have cyclic MRO since their bases must
+ // already exist at creation time. Unlike statement classes, we do not
+ // permit dynamic classes to have forward references in their
+ // bases list.
+ return false;
+ };
class_literal
.try_mro(db, specialization)
.is_err_and(MroError::is_cycle)
diff --git a/crates/ty_python_semantic/src/types/definition.rs b/crates/ty_python_semantic/src/types/definition.rs
index 8e5bc8deb0..ffe65ec71d 100644
--- a/crates/ty_python_semantic/src/types/definition.rs
+++ b/crates/ty_python_semantic/src/types/definition.rs
@@ -9,7 +9,10 @@ use ty_module_resolver::Module;
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum TypeDefinition<'db> {
Module(Module<'db>),
- Class(Definition<'db>),
+ /// A class created via a `class` statement.
+ StaticClass(Definition<'db>),
+ /// A class created dynamically via `type(name, bases, dict)`.
+ DynamicClass(Definition<'db>),
Function(Definition<'db>),
TypeVar(Definition<'db>),
TypeAlias(Definition<'db>),
@@ -21,7 +24,8 @@ impl TypeDefinition<'_> {
pub fn focus_range(&self, db: &dyn Db) -> Option {
match self {
Self::Module(_) => None,
- Self::Class(definition)
+ Self::StaticClass(definition)
+ | Self::DynamicClass(definition)
| Self::Function(definition)
| Self::TypeVar(definition)
| Self::TypeAlias(definition)
@@ -40,7 +44,8 @@ impl TypeDefinition<'_> {
let source = source_text(db, file);
Some(FileRange::new(file, TextRange::up_to(source.text_len())))
}
- Self::Class(definition)
+ Self::StaticClass(definition)
+ | Self::DynamicClass(definition)
| Self::Function(definition)
| Self::TypeVar(definition)
| Self::TypeAlias(definition)
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 62c8f3e18a..fac705cabf 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -2,7 +2,7 @@ use super::call::CallErrorKind;
use super::context::InferContext;
use super::mro::DuplicateBaseError;
use super::{
- CallArguments, CallDunderError, ClassBase, ClassLiteral, KnownClass,
+ CallArguments, CallDunderError, ClassBase, ClassLiteral, KnownClass, StaticClassLiteral,
add_inferred_python_version_hint_to_diagnostic,
};
use crate::diagnostic::did_you_mean;
@@ -113,6 +113,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&UNRESOLVED_IMPORT);
registry.register_lint(&UNRESOLVED_REFERENCE);
registry.register_lint(&UNSUPPORTED_BASE);
+ registry.register_lint(&UNSUPPORTED_DYNAMIC_BASE);
registry.register_lint(&UNSUPPORTED_OPERATOR);
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
registry.register_lint(&STATIC_ASSERT_ERROR);
@@ -841,6 +842,40 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Checks for dynamic class definitions (using `type()`) that have bases
+ /// which are unsupported by ty.
+ ///
+ /// This is equivalent to [`unsupported-base`] but applies to classes created
+ /// via `type()` rather than `class` statements.
+ ///
+ /// ## Why is this bad?
+ /// If a dynamically created class has a base that is an unsupported type
+ /// such as `type[T]`, ty will not be able to resolve the
+ /// [method resolution order] (MRO) for the class. This may lead to an inferior
+ /// understanding of your codebase and unpredictable type-checking behavior.
+ ///
+ /// ## Default level
+ /// This rule is disabled by default because it will not cause a runtime error,
+ /// and may be noisy on codebases that use `type()` in highly dynamic ways.
+ ///
+ /// ## Examples
+ /// ```python
+ /// def factory(base: type[Base]) -> type:
+ /// # `base` has type `type[Base]`, not `type[Base]` itself
+ /// return type("Dynamic", (base,), {}) # error: [unsupported-dynamic-base]
+ /// ```
+ ///
+ /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
+ /// [`unsupported-base`]: https://docs.astral.sh/ty/rules/unsupported-base
+ pub(crate) static UNSUPPORTED_DYNAMIC_BASE = {
+ summary: "detects dynamic class bases that are unsupported as ty could not feasibly calculate the class's MRO",
+ status: LintStatus::preview("1.0.0"),
+ default_level: Level::Ignore,
+ }
+}
+
declare_lint! {
/// ## What it does
/// Checks for expressions used in `with` statements
@@ -2800,12 +2835,12 @@ pub(super) fn report_implicit_return_type(
"Only classes that directly inherit from `typing.Protocol` \
or `typing_extensions.Protocol` are considered protocol classes",
);
- sub_diagnostic.annotate(
- Annotation::primary(class.header_span(db)).message(format_args!(
+ sub_diagnostic.annotate(Annotation::primary(class.definition_span(db)).message(
+ format_args!(
"`Protocol` not present in `{class}`'s immediate bases",
class = class.name(db)
- )),
- );
+ ),
+ ));
diagnostic.sub(sub_diagnostic);
diagnostic.info("See https://typing.python.org/en/latest/spec/protocol.html#");
@@ -2974,7 +3009,7 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast:
pub(crate) fn report_instance_layout_conflict(
context: &InferContext,
- class: ClassLiteral,
+ class: StaticClassLiteral,
node: &ast::StmtClassDef,
disjoint_bases: &IncompatibleBases,
) {
@@ -3009,7 +3044,7 @@ pub(crate) fn report_instance_layout_conflict(
let span = context.span(&node.bases()[*node_index]);
let mut annotation = Annotation::secondary(span.clone());
- if disjoint_base.class == *originating_base {
+ if originating_base.as_static() == Some(disjoint_base.class) {
match disjoint_base.kind {
DisjointBaseKind::DefinesSlots => {
annotation = annotation.message(format_args!(
@@ -3060,6 +3095,32 @@ pub(crate) fn report_instance_layout_conflict(
diagnostic.sub(subdiagnostic);
}
+/// Emit a diagnostic for a metaclass conflict where both conflicting metaclasses
+/// are inherited from base classes.
+pub(super) fn report_conflicting_metaclass_from_bases(
+ context: &InferContext,
+ node: AnyNodeRef,
+ class_name: &str,
+ metaclass1: ClassType,
+ base1: impl std::fmt::Display,
+ metaclass2: ClassType,
+ base2: impl std::fmt::Display,
+) {
+ let Some(builder) = context.report_lint(&CONFLICTING_METACLASS, node) else {
+ return;
+ };
+ let db = context.db();
+ builder.into_diagnostic(format_args!(
+ "The metaclass of a derived class (`{class_name}`) \
+ must be a subclass of the metaclasses of all its bases, \
+ but `{metaclass1}` (metaclass of base class `{base1}`) \
+ and `{metaclass2}` (metaclass of base class `{base2}`) \
+ have no subclass relationship",
+ metaclass1 = metaclass1.name(db),
+ metaclass2 = metaclass2.name(db),
+ ));
+}
+
/// Information regarding the conflicting disjoint bases a class is inferred to have in its MRO.
///
/// For each disjoint base, we record information about which element in the class's bases list
@@ -3232,9 +3293,9 @@ pub(crate) fn report_bad_argument_to_protocol_interface(
class.name(db)
),
);
- class_def_diagnostic.annotate(Annotation::primary(
- class.class_literal(db).0.header_span(db),
- ));
+ if let Some((class_literal, _)) = class.static_class_literal(db) {
+ class_def_diagnostic.annotate(Annotation::primary(class_literal.header_span(db)));
+ }
diagnostic.sub(class_def_diagnostic);
}
@@ -3293,7 +3354,7 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol(
),
);
class_def_diagnostic.annotate(
- Annotation::primary(protocol.header_span(db))
+ Annotation::primary(protocol.definition_span(db))
.message(format_args!("`{class_name}` declared here")),
);
diagnostic.sub(class_def_diagnostic);
@@ -3324,7 +3385,7 @@ pub(crate) fn report_attempted_protocol_instantiation(
format_args!("Protocol classes cannot be instantiated"),
);
class_def_diagnostic.annotate(
- Annotation::primary(protocol.header_span(db))
+ Annotation::primary(protocol.definition_span(db))
.message(format_args!("`{class_name}` declared as a protocol here")),
);
diagnostic.sub(class_def_diagnostic);
@@ -3412,7 +3473,7 @@ pub(crate) fn report_undeclared_protocol_member(
leads to an ambiguous interface",
);
class_def_diagnostic.annotate(
- Annotation::primary(protocol_class.header_span(db))
+ Annotation::primary(protocol_class.definition_span(db))
.message(format_args!("`{class_name}` declared as a protocol here",)),
);
diagnostic.sub(class_def_diagnostic);
@@ -3425,7 +3486,7 @@ pub(crate) fn report_undeclared_protocol_member(
pub(crate) fn report_duplicate_bases(
context: &InferContext,
- class: ClassLiteral,
+ class: StaticClassLiteral,
duplicate_base_error: &DuplicateBaseError,
bases_list: &[ast::Expr],
) {
@@ -3472,7 +3533,7 @@ pub(crate) fn report_invalid_or_unsupported_base(
context: &InferContext,
base_node: &ast::Expr,
base_type: Type,
- class: ClassLiteral,
+ class: StaticClassLiteral,
) {
let db = context.db();
let instance_of_type = KnownClass::Type.to_instance(db);
@@ -3582,7 +3643,7 @@ fn report_unsupported_base(
context: &InferContext,
base_node: &ast::Expr,
base_type: Type,
- class: ClassLiteral,
+ class: StaticClassLiteral,
) {
let Some(builder) = context.report_lint(&UNSUPPORTED_BASE, base_node) else {
return;
@@ -3605,7 +3666,7 @@ fn report_invalid_base<'ctx, 'db>(
context: &'ctx InferContext<'db, '_>,
base_node: &ast::Expr,
base_type: Type<'db>,
- class: ClassLiteral<'db>,
+ class: StaticClassLiteral<'db>,
) -> Option> {
let builder = context.report_lint(&INVALID_BASE, base_node)?;
let mut diagnostic = builder.into_diagnostic(format_args!(
@@ -3701,7 +3762,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'db>(
context: &InferContext<'db, '_>,
- class: ClassLiteral<'db>,
+ class: StaticClassLiteral<'db>,
(field, field_def): (&str, Option>),
(field_with_default, field_with_default_def): &(Name, Option>),
) {
@@ -3750,7 +3811,7 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'
pub(super) fn report_named_tuple_field_with_leading_underscore<'db>(
context: &InferContext<'db, '_>,
- class: ClassLiteral<'db>,
+ class: StaticClassLiteral<'db>,
field_name: &str,
field_definition: Option>,
) {
@@ -3874,7 +3935,7 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>(
pub(crate) fn report_invalid_type_param_order<'db>(
context: &InferContext<'db, '_>,
- class: ClassLiteral<'db>,
+ class: StaticClassLiteral<'db>,
node: &ast::StmtClassDef,
typevar_with_default: TypeVarInstance<'db>,
invalid_later_typevars: &[TypeVarInstance<'db>],
@@ -3959,7 +4020,7 @@ pub(crate) fn report_invalid_type_param_order<'db>(
pub(crate) fn report_rebound_typevar<'db>(
context: &InferContext<'db, '_>,
typevar_name: &ast::name::Name,
- class: ClassLiteral<'db>,
+ class: StaticClassLiteral<'db>,
class_node: &ast::StmtClassDef,
other_typevar: BoundTypeVarInstance<'db>,
) {
@@ -4034,10 +4095,8 @@ pub(super) fn report_invalid_method_override<'db>(
let superclass_name = superclass.name(db);
let overridden_method = if class_name == superclass_name {
- format!(
- "{superclass}.{member}",
- superclass = superclass.qualified_name(db),
- )
+ let qualified_name = superclass.qualified_name(db);
+ format!("{qualified_name}.{member}")
} else {
format!("{superclass_name}.{member}")
};
@@ -4090,7 +4149,10 @@ pub(super) fn report_invalid_method_override<'db>(
);
}
- let superclass_scope = superclass.class_literal(db).0.body_scope(db);
+ let Some((superclass_literal, _)) = superclass.static_class_literal(db) else {
+ return;
+ };
+ let superclass_scope = superclass_literal.body_scope(db);
match superclass_method_kind {
MethodKind::NotSynthesized => {
@@ -4157,7 +4219,7 @@ pub(super) fn report_invalid_method_override<'db>(
};
sub.annotate(
- Annotation::primary(superclass.header_span(db))
+ Annotation::primary(superclass.definition_span(db))
.message(format_args!("Definition of `{superclass_name}`")),
);
diagnostic.sub(sub);
@@ -4277,9 +4339,10 @@ pub(super) fn report_overridden_final_method<'db>(
// but you'd want to delete the `@my_property.deleter` as well as the getter and the deleter,
// and we don't model property deleters at all right now.
if let Type::FunctionLiteral(function) = subclass_type {
- let class_node = subclass
- .class_literal(db)
- .0
+ let Some((subclass_literal, _)) = subclass.static_class_literal(db) else {
+ return;
+ };
+ let class_node = subclass_literal
.body_scope(db)
.node(db)
.expect_class()
@@ -4577,9 +4640,9 @@ fn report_unsupported_binary_operation_impl<'a>(
pub(super) fn report_bad_frozen_dataclass_inheritance<'db>(
context: &InferContext<'db, '_>,
- class: ClassLiteral<'db>,
+ class: StaticClassLiteral<'db>,
class_node: &ast::StmtClassDef,
- base_class: ClassLiteral<'db>,
+ base_class: StaticClassLiteral<'db>,
base_class_node: &ast::Expr,
base_class_params: DataclassFlags,
) {
diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs
index 859a12d0c3..dd2811aab2 100644
--- a/crates/ty_python_semantic/src/types/display.rs
+++ b/crates/ty_python_semantic/src/types/display.rs
@@ -432,8 +432,12 @@ impl<'db> TypeVisitor<'db> for AmbiguousClassCollector<'db> {
fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {
match ty {
Type::ClassLiteral(class) => self.record_class(db, class),
- Type::EnumLiteral(literal) => self.record_class(db, literal.enum_class(db)),
- Type::GenericAlias(alias) => self.record_class(db, alias.origin(db)),
+ Type::EnumLiteral(literal) => {
+ self.record_class(db, literal.enum_class(db));
+ }
+ Type::GenericAlias(alias) => {
+ self.record_class(db, ClassLiteral::Static(alias.origin(db)));
+ }
// Visit the class (as if it were a nominal-instance type)
// rather than the protocol members, if it is a class-based protocol.
// (For the purposes of displaying the type, we'll use the class name.)
@@ -558,13 +562,15 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> {
let ty = Type::ClassLiteral(self.class);
if qualification_level.is_some() {
- write!(f.with_type(ty), "{}", self.class.qualified_name(self.db))?;
+ let qualified_name = self.class.qualified_name(self.db);
+ write!(f.with_type(ty), "{qualified_name}")?;
} else {
write!(f.with_type(ty), "{}", self.class.name(self.db))?;
}
if qualification_level == Some(&QualificationLevel::FileAndLineNumber) {
let file = self.class.file(self.db);
+ let class_offset = self.class.header_range(self.db).start();
let path = file.path(self.db);
let path = match path {
FilePath::System(path) => Cow::Owned(FilePath::System(
@@ -575,7 +581,6 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> {
FilePath::Vendored(_) | FilePath::SystemVirtual(_) => Cow::Borrowed(path),
};
let line_index = line_index(self.db, file);
- let class_offset = self.class.header_range(self.db).start();
let line_number = line_index.line_index(class_offset);
f.set_invalid_type_annotation();
write!(f, " @ {path}:{line_number}")?;
@@ -1287,7 +1292,7 @@ impl<'db> GenericAlias<'db> {
settings: DisplaySettings<'db>,
) -> DisplayGenericAlias<'db> {
DisplayGenericAlias {
- origin: self.origin(db),
+ origin: ClassLiteral::Static(self.origin(db)),
specialization: self.specialization(db),
db,
settings,
diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs
index 206d7f21f1..f5067cc675 100644
--- a/crates/ty_python_semantic/src/types/enums.rs
+++ b/crates/ty_python_semantic/src/types/enums.rs
@@ -1,5 +1,6 @@
use ruff_python_ast::name::Name;
use rustc_hash::FxHashMap;
+use smallvec::SmallVec;
use crate::{
Db, FxIndexMap,
@@ -9,7 +10,7 @@ use crate::{
semantic_index::{place_table, use_def_map},
types::{
ClassBase, ClassLiteral, DynamicType, EnumLiteralType, KnownClass, MemberLookupPolicy,
- Type, TypeQualifiers,
+ StaticClassLiteral, Type, TypeQualifiers,
},
};
@@ -54,6 +55,23 @@ pub(crate) fn enum_metadata<'db>(
db: &'db dyn Db,
class: ClassLiteral<'db>,
) -> Option> {
+ let class = match class {
+ ClassLiteral::Static(class) => class,
+ ClassLiteral::Dynamic(..) => {
+ // Classes created via `type` cannot be enums; the following fails at runtime:
+ // ```python
+ // import enum
+ //
+ // class BaseEnum(enum.Enum):
+ // pass
+ //
+ // MyEnum = type("MyEnum", (BaseEnum,), {"A": 1, "B": 2})
+ // ```
+ // TODO: Add a diagnostic for including an enum in a `type(...)` call.
+ return None;
+ }
+ };
+
// This is a fast path to avoid traversing the MRO of known classes
if class
.known(db)
@@ -139,42 +157,43 @@ pub(crate) fn enum_metadata<'db>(
auto_counter += 1;
// `StrEnum`s have different `auto()` behaviour to enums inheriting from `(str, Enum)`
- let auto_value_ty = if Type::ClassLiteral(class)
- .is_subtype_of(db, KnownClass::StrEnum.to_subclass_of(db))
- {
- Type::string_literal(db, &name.to_lowercase())
- } else {
- let custom_mixins: smallvec::SmallVec<[Option; 1]> =
- class
- .iter_mro(db, None)
- .skip(1)
- .filter_map(ClassBase::into_class)
- .filter(|class| {
- !Type::from(*class).is_subtype_of(
- db,
- KnownClass::Enum.to_subclass_of(db),
- )
- })
- .map(|class| class.known(db))
- .filter(|class| {
- !matches!(class, Some(KnownClass::Object))
- })
- .collect();
-
- // `IntEnum`s have the same `auto()` behaviour to enums inheriting from `(int, Enum)`,
- // and `IntEnum`s also have `int` in their MROs, so both cases are handled here.
- //
- // In general, the `auto()` behaviour for enums with non-`int` mixins is hard to predict,
- // so we fall back to `Any` in those cases.
- if matches!(
- custom_mixins.as_slice(),
- [] | [Some(KnownClass::Int)]
- ) {
- Type::IntLiteral(auto_counter)
+ let auto_value_ty =
+ if Type::ClassLiteral(ClassLiteral::Static(class))
+ .is_subtype_of(db, KnownClass::StrEnum.to_subclass_of(db))
+ {
+ Type::string_literal(db, &name.to_lowercase())
} else {
- Type::any()
- }
- };
+ let custom_mixins: SmallVec<[Option; 1]> =
+ class
+ .iter_mro(db, None)
+ .skip(1)
+ .filter_map(ClassBase::into_class)
+ .filter(|class| {
+ !Type::from(*class).is_subtype_of(
+ db,
+ KnownClass::Enum.to_subclass_of(db),
+ )
+ })
+ .map(|class| class.known(db))
+ .filter(|class| {
+ !matches!(class, Some(KnownClass::Object))
+ })
+ .collect();
+
+ // `IntEnum`s have the same `auto()` behaviour to enums inheriting from `(int, Enum)`,
+ // and `IntEnum`s also have `int` in their MROs, so both cases are handled here.
+ //
+ // In general, the `auto()` behaviour for enums with non-`int` mixins is hard to predict,
+ // so we fall back to `Any` in those cases.
+ if matches!(
+ custom_mixins.as_slice(),
+ [] | [Some(KnownClass::Int)]
+ ) {
+ Type::IntLiteral(auto_counter)
+ } else {
+ Type::any()
+ }
+ };
Some(auto_value_ty)
}
@@ -308,8 +327,12 @@ pub(crate) fn is_enum_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
///
/// This is a lighter-weight check than `enum_metadata`, which additionally
/// verifies that the class has members.
-pub(crate) fn is_enum_class_by_inheritance<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> bool {
- Type::ClassLiteral(class).is_subtype_of(db, KnownClass::Enum.to_subclass_of(db))
+pub(crate) fn is_enum_class_by_inheritance<'db>(
+ db: &'db dyn Db,
+ class: StaticClassLiteral<'db>,
+) -> bool {
+ Type::ClassLiteral(ClassLiteral::Static(class))
+ .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db))
|| class
.metaclass(db)
.is_subtype_of(db, KnownClass::EnumType.to_subclass_of(db))
diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs
index e532dbebee..48a6b13e8f 100644
--- a/crates/ty_python_semantic/src/types/function.rs
+++ b/crates/ty_python_semantic/src/types/function.rs
@@ -564,10 +564,11 @@ impl<'db> OverloadLiteral<'db> {
let index = semantic_index(db, scope_id.file(db));
let class = nearest_enclosing_class(db, index, scope_id).unwrap();
- let typing_self = typing_self(db, scope_id, typevar_binding_context, class).expect(
- "We should always find the surrounding class \
+ let typing_self = typing_self(db, scope_id, typevar_binding_context, class.into())
+ .expect(
+ "We should always find the surrounding class \
for an implicit self: Self annotation",
- );
+ );
if self.is_classmethod(db) {
Some(SubclassOfType::from(
@@ -1227,10 +1228,7 @@ fn is_instance_truthiness<'db>(
.class(db)
.iter_mro(db)
.filter_map(ClassBase::into_class)
- .any(|c| match c {
- ClassType::Generic(c) => c.origin(db) == class,
- ClassType::NonGeneric(c) => c == class,
- })
+ .any(|c| c.class_literal(db) == class)
{
return true;
}
@@ -2023,7 +2021,7 @@ impl KnownFunction {
if !class.has_ordering_method_in_mro(db) {
report_invalid_total_ordering_call(
context,
- class.class_literal(db).0,
+ class.class_literal(db),
call_expression,
);
}
diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs
index c3cd69cb37..70fa611c77 100644
--- a/crates/ty_python_semantic/src/types/ide_support.rs
+++ b/crates/ty_python_semantic/src/types/ide_support.rs
@@ -8,7 +8,8 @@ use crate::semantic_index::{attribute_scopes, global_scope, semantic_index, use_
use crate::types::call::{CallArguments, MatchedArgument};
use crate::types::signatures::{ParameterKind, Signature};
use crate::types::{
- CallDunderError, CallableTypes, ClassBase, KnownUnion, Type, TypeContext, UnionType,
+ CallDunderError, CallableTypes, ClassBase, ClassLiteral, ClassType, KnownUnion, Type,
+ TypeContext, UnionType,
};
use crate::{Db, DisplaySettings, HasType, SemanticModel};
use ruff_db::files::FileRange;
@@ -266,7 +267,10 @@ pub fn definitions_for_attribute<'db>(
let class_literal = match meta_type {
Type::ClassLiteral(class_literal) => class_literal,
Type::SubclassOf(subclass) => match subclass.subclass_of().into_class(db) {
- Some(cls) => cls.class_literal(db).0,
+ Some(cls) => match cls.static_class_literal(db) {
+ Some((lit, _)) => ClassLiteral::Static(lit),
+ None => continue,
+ },
None => continue,
},
_ => continue,
@@ -274,9 +278,9 @@ pub fn definitions_for_attribute<'db>(
// Walk the MRO: include class and its ancestors, but stop when we find a match
'scopes: for ancestor in class_literal
- .iter_mro(db, None)
+ .iter_mro(db)
.filter_map(ClassBase::into_class)
- .map(|cls| cls.class_literal(db).0)
+ .filter_map(|cls: ClassType<'db>| cls.static_class_literal(db).map(|(lit, _)| lit))
{
let class_scope = ancestor.body_scope(db);
let class_place_table = crate::semantic_index::place_table(db, class_scope);
diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs
index 3a1f442592..f437f128e5 100644
--- a/crates/ty_python_semantic/src/types/infer.rs
+++ b/crates/ty_python_semantic/src/types/infer.rs
@@ -53,7 +53,8 @@ use crate::types::function::FunctionType;
use crate::types::generics::Specialization;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
- ClassLiteral, KnownClass, Truthiness, Type, TypeAndQualifiers, declaration_type,
+ ClassLiteral, KnownClass, StaticClassLiteral, Truthiness, Type, TypeAndQualifiers,
+ declaration_type,
};
use crate::unpack::Unpack;
use builder::TypeInferenceBuilder;
@@ -465,7 +466,7 @@ pub(crate) fn nearest_enclosing_class<'db>(
db: &'db dyn Db,
semantic: &SemanticIndex<'db>,
scope: ScopeId,
-) -> Option> {
+) -> Option> {
semantic
.ancestor_scopes(scope.file_scope_id(db))
.find_map(|(_, ancestor_scope)| {
@@ -474,6 +475,7 @@ pub(crate) fn nearest_enclosing_class<'db>(
declaration_type(db, definition)
.inner_type()
.as_class_literal()
+ .and_then(ClassLiteral::as_static)
})
}
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 91605695b8..9abed64628 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -53,32 +53,37 @@ use crate::semantic_index::{
use crate::subscript::{PyIndex, PySlice};
use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex};
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
-use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator};
+use crate::types::class::{
+ ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, DynamicMetaclassConflict, FieldKind,
+ MetaclassErrorKind, MethodDecorator,
+};
use crate::types::context::{InNoTypeCheck, InferContext};
use crate::types::cyclic::CycleDetector;
use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
- CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY,
- INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS,
- INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY,
- INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE,
- INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL,
- INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
+ CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE,
+ DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT,
+ INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS,
+ INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE,
+ INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC,
+ INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases,
NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL,
UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
- UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
+ UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
+ hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict,
- report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
- report_instance_layout_conflict, report_invalid_arguments_to_annotated,
- report_invalid_assignment, report_invalid_attribute_assignment,
- report_invalid_exception_caught, report_invalid_exception_cause,
- report_invalid_exception_raised, report_invalid_exception_tuple_caught,
- report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
- report_invalid_or_unsupported_base, report_invalid_return_type, report_invalid_total_ordering,
+ report_conflicting_metaclass_from_bases, report_duplicate_bases, report_implicit_return_type,
+ report_index_out_of_bounds, report_instance_layout_conflict,
+ report_invalid_arguments_to_annotated, report_invalid_assignment,
+ report_invalid_attribute_assignment, report_invalid_exception_caught,
+ report_invalid_exception_cause, report_invalid_exception_raised,
+ report_invalid_exception_tuple_caught, report_invalid_generator_function_return_type,
+ report_invalid_key_on_typed_dict, report_invalid_or_unsupported_base,
+ report_invalid_return_type, report_invalid_total_ordering,
report_invalid_type_checking_constant, report_invalid_type_param_order,
report_named_tuple_field_with_leading_underscore,
report_namedtuple_field_without_default_after_field_with_default, report_not_subscriptable,
@@ -96,7 +101,7 @@ use crate::types::generics::{
};
use crate::types::infer::nearest_enclosing_function;
use crate::types::instance::SliceLiteral;
-use crate::types::mro::MroErrorKind;
+use crate::types::mro::{DynamicMroErrorKind, MroErrorKind};
use crate::types::newtype::NewType;
use crate::types::subclass_of::SubclassOfInner;
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleType};
@@ -107,12 +112,12 @@ use crate::types::typed_dict::{
use crate::types::visitor::any_over_type;
use crate::types::{
BoundTypeVarIdentity, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType,
- CallableTypeKind, ClassLiteral, ClassType, DataclassParams, DynamicType, InternedType,
- IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion,
- LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
- ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType,
- SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers,
- TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
+ CallableTypeKind, ClassType, DataclassParams, DynamicType, InternedType, IntersectionBuilder,
+ IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LintDiagnosticGuard,
+ MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter,
+ ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType,
+ TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext,
+ TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types,
todo_type,
@@ -578,27 +583,32 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
if self.db().should_check_file(self.file()) {
- self.check_class_definitions();
+ self.check_static_class_definitions();
self.check_overloaded_functions(node);
}
}
- /// Iterate over all class definitions to check that the definition will not cause an exception
- /// to be raised at runtime. This needs to be done after most other types in the scope have been
- /// inferred, due to the fact that base classes can be deferred. If it looks like a class
- /// definition is invalid in some way, issue a diagnostic.
+ /// Iterate over all static class definitions (created using `class` statements) to check that
+ /// the definition will not cause an exception to be raised at runtime. This needs to be done
+ /// after most other types in the scope have been inferred, due to the fact that base classes
+ /// can be deferred. If it looks like a class definition is invalid in some way, issue a
+ /// diagnostic.
+ ///
+ /// Note: Dynamic classes created via `type()` calls are checked separately during type
+ /// inference of the call expression.
///
/// Among the things we check for in this method are whether Python will be able to determine a
/// consistent "[method resolution order]" and [metaclass] for each class.
///
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
/// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses
- fn check_class_definitions(&mut self) {
+ fn check_static_class_definitions(&mut self) {
let class_definitions = self.declarations.iter().filter_map(|(definition, ty)| {
// Filter out class literals that result from imports
if let DefinitionKind::Class(class) = definition.kind(self.db()) {
ty.inner_type()
.as_class_literal()
+ .and_then(ClassLiteral::as_static)
.map(|class_literal| (class_literal, class.node(self.module())))
} else {
None
@@ -625,7 +635,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
continue;
}
- let is_named_tuple = CodeGeneratorKind::NamedTuple.matches(self.db(), class, None);
+ let is_named_tuple =
+ CodeGeneratorKind::NamedTuple.matches(self.db(), class.into(), None);
// (2) If it's a `NamedTuple` class, check that no field without a default value
// appears after a field with a default value.
@@ -729,7 +740,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
};
if let Some(disjoint_base) = base_class.nearest_disjoint_base(self.db()) {
- disjoint_bases.insert(disjoint_base, i, base_class.class_literal(self.db()).0);
+ disjoint_bases.insert(disjoint_base, i, base_class.class_literal(self.db()));
}
if is_protocol
@@ -760,12 +771,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- let (base_class_literal, _) = base_class.class_literal(self.db());
-
- if let (Some(base_params), Some(class_params)) = (
- base_class_literal.dataclass_params(self.db()),
- class.dataclass_params(self.db()),
- ) {
+ if let Some((base_class_literal, _)) = base_class.static_class_literal(self.db())
+ && let (Some(base_params), Some(class_params)) = (
+ base_class_literal.dataclass_params(self.db()),
+ class.dataclass_params(self.db()),
+ )
+ {
let base_params = base_params.flags(self.db());
let class_is_frozen = class_params.flags(self.db()).is_frozen();
@@ -864,7 +875,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
function.is_known(self.db(), KnownFunction::TotalOrdering)
})
}) {
- report_invalid_total_ordering(&self.context, class, decorator);
+ report_invalid_total_ordering(
+ &self.context,
+ ClassLiteral::Static(class),
+ decorator,
+ );
}
}
@@ -915,35 +930,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
},
candidate1_is_base_class,
} => {
- if let Some(builder) =
+ if *candidate1_is_base_class {
+ report_conflicting_metaclass_from_bases(
+ &self.context,
+ class_node.into(),
+ class.name(self.db()),
+ *metaclass1,
+ class1.name(self.db()),
+ *metaclass2,
+ class2.name(self.db()),
+ );
+ } else if let Some(builder) =
self.context.report_lint(&CONFLICTING_METACLASS, class_node)
{
- if *candidate1_is_base_class {
- builder.into_diagnostic(format_args!(
- "The metaclass of a derived class (`{class}`) \
- must be a subclass of the metaclasses of all its bases, \
- but `{metaclass1}` (metaclass of base class `{base1}`) \
- and `{metaclass2}` (metaclass of base class `{base2}`) \
- have no subclass relationship",
- class = class.name(self.db()),
- metaclass1 = metaclass1.name(self.db()),
- base1 = class1.name(self.db()),
- metaclass2 = metaclass2.name(self.db()),
- base2 = class2.name(self.db()),
- ));
- } else {
- builder.into_diagnostic(format_args!(
- "The metaclass of a derived class (`{class}`) \
+ builder.into_diagnostic(format_args!(
+ "The metaclass of a derived class (`{class}`) \
must be a subclass of the metaclasses of all its bases, \
but `{metaclass_of_class}` (metaclass of `{class}`) \
and `{metaclass_of_base}` (metaclass of base class `{base}`) \
have no subclass relationship",
- class = class.name(self.db()),
- metaclass_of_class = metaclass1.name(self.db()),
- metaclass_of_base = metaclass2.name(self.db()),
- base = class2.name(self.db()),
- ));
- }
+ class = class.name(self.db()),
+ metaclass_of_class = metaclass1.name(self.db()),
+ metaclass_of_base = metaclass2.name(self.db()),
+ base = class2.name(self.db()),
+ ));
}
}
}
@@ -1030,7 +1040,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// (7) Check that a dataclass does not have more than one `KW_ONLY`.
if let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) =
- CodeGeneratorKind::from_class(self.db(), class, None)
+ CodeGeneratorKind::from_class(self.db(), class.into(), None)
{
let specialization = None;
@@ -2979,7 +2989,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::SpecialForm(SpecialFormType::NamedTuple)
}
(None, "Any") if in_typing_module() => Type::SpecialForm(SpecialFormType::Any),
- _ => Type::from(ClassLiteral::new(
+ _ => Type::from(StaticClassLiteral::new(
self.db(),
name.id.clone(),
body_scope,
@@ -4285,10 +4295,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if object_ty
.as_nominal_instance()
.and_then(|instance| {
- file_to_module(
- db,
- instance.class(db).class_literal(db).0.file(db),
- )
+ instance.class(db).static_class_literal(db)
+ })
+ .and_then(|(class_literal, _)| {
+ file_to_module(db, class_literal.file(db))
})
.and_then(|module| module.search_path(db))
.is_some_and(ty_module_resolver::SearchPath::is_first_party)
@@ -4625,9 +4635,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
// Check if class-level attribute already has a value
- {
- let class_definition = class_ty.class_literal(db).0;
- let class_scope_id = class_definition.body_scope(db).file_scope_id(db);
+ if let Some((class_literal, _)) = class_ty.static_class_literal(db) {
+ let class_scope_id = class_literal.body_scope(db).file_scope_id(db);
let place_table = builder.index.place_table(class_scope_id);
if let Some(symbol) = place_table.symbol_by_name(attribute) {
@@ -5399,6 +5408,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Some(KnownClass::NewType) => {
self.infer_newtype_expression(target, call_expr, definition)
}
+ Some(KnownClass::Type) => {
+ // Try to extract the dynamic class with definition.
+ // This returns `None` if it's not a three-arg call to `type()`,
+ // signalling that we must fall back to normal call inference.
+ self.infer_dynamic_type_expression(call_expr, definition)
+ .unwrap_or_else(|| {
+ self.infer_call_expression_impl(call_expr, callable_type, tcx)
+ })
+ }
Some(_) | None => {
self.infer_call_expression_impl(call_expr, callable_type, tcx)
}
@@ -6002,6 +6020,245 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
+ /// Try to infer a 3-argument `type(name, bases, dict)` call expression, capturing the definition.
+ ///
+ /// This is called when we detect a `type()` call in assignment context and want to
+ /// associate the resulting `DynamicClassLiteral` with its definition for go-to-definition.
+ ///
+ /// Returns `None` if any keywords were provided or the number of arguments is not three,
+ /// signalling that no types were stored for any AST sub-expressions and that we should
+ /// therefore fallback to normal call binding for error reporting.
+ fn infer_dynamic_type_expression(
+ &mut self,
+ call_expr: &ast::ExprCall,
+ definition: Definition<'db>,
+ ) -> Option> {
+ let db = self.db();
+
+ let ast::Arguments {
+ args,
+ keywords,
+ range: _,
+ node_index: _,
+ } = &call_expr.arguments;
+
+ if !keywords.is_empty() {
+ return None;
+ }
+
+ let [name_arg, bases_arg, namespace_arg] = &**args else {
+ return None;
+ };
+
+ // If any argument is a starred expression, we can't know how many positional arguments
+ // we're receiving, so fall back to normal call binding.
+ if args.iter().any(ast::Expr::is_starred_expr) {
+ return None;
+ }
+
+ // Infer the argument types.
+ let name_type = self.infer_expression(name_arg, TypeContext::default());
+ let bases_type = self.infer_expression(bases_arg, TypeContext::default());
+ let namespace_type = self.infer_expression(namespace_arg, TypeContext::default());
+
+ if !namespace_type.is_assignable_to(
+ db,
+ KnownClass::Dict
+ .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]),
+ ) && let Some(builder) = self
+ .context
+ .report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg)
+ {
+ let mut diagnostic = builder
+ .into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`");
+ diagnostic.set_primary_message(format_args!(
+ "Expected `dict[str, Any]`, found `{}`",
+ namespace_type.display(db)
+ ));
+ }
+
+ // Extract name and base classes.
+ let name = if let Type::StringLiteral(literal) = name_type {
+ ast::name::Name::new(literal.value(db))
+ } else {
+ if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db))
+ && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg)
+ {
+ let mut diagnostic =
+ builder.into_diagnostic("Invalid argument to parameter 1 (`name`) of `type()`");
+ diagnostic.set_primary_message(format_args!(
+ "Expected `str`, found `{}`",
+ name_type.display(db)
+ ));
+ }
+ ast::name::Name::new_static("")
+ };
+
+ let bases = self.extract_dynamic_type_bases(bases_arg, bases_type, &name);
+
+ let dynamic_class = DynamicClassLiteral::new(db, name, bases, definition, None);
+
+ // Check for MRO errors.
+ if let Err(error) = dynamic_class.try_mro(db) {
+ match error.reason() {
+ DynamicMroErrorKind::DuplicateBases(duplicates) => {
+ if let Some(builder) = self.context.report_lint(&DUPLICATE_BASE, call_expr) {
+ builder.into_diagnostic(format_args!(
+ "Duplicate base class{maybe_s} {dupes} in class `{class}`",
+ maybe_s = if duplicates.len() == 1 { "" } else { "es" },
+ dupes = duplicates
+ .iter()
+ .map(|base: &ClassBase<'_>| base.display(db))
+ .join(", "),
+ class = dynamic_class.name(db),
+ ));
+ }
+ }
+ DynamicMroErrorKind::UnresolvableMro => {
+ if let Some(builder) = self.context.report_lint(&INCONSISTENT_MRO, call_expr) {
+ builder.into_diagnostic(format_args!(
+ "Cannot create a consistent method resolution order (MRO) \
+ for class `{}` with bases `[{}]`",
+ dynamic_class.name(db),
+ dynamic_class
+ .bases(db)
+ .iter()
+ .map(|base| base.display(db))
+ .join(", ")
+ ));
+ }
+ }
+ }
+ }
+
+ // Check for metaclass conflicts.
+ if let Err(DynamicMetaclassConflict {
+ metaclass1,
+ base1,
+ metaclass2,
+ base2,
+ }) = dynamic_class.try_metaclass(db)
+ {
+ report_conflicting_metaclass_from_bases(
+ &self.context,
+ call_expr.into(),
+ dynamic_class.name(db),
+ metaclass1,
+ base1.display(db),
+ metaclass2,
+ base2.display(db),
+ );
+ }
+
+ Some(Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)))
+ }
+
+ /// Extract base classes from the second argument of a `type()` call.
+ ///
+ /// If any bases were invalid, diagnostics are emitted and the dynamic
+ /// class is inferred as inheriting from `Unknown`.
+ fn extract_dynamic_type_bases(
+ &mut self,
+ bases_node: &ast::Expr,
+ bases_type: Type<'db>,
+ name: &ast::name::Name,
+ ) -> Box<[ClassBase<'db>]> {
+ let db = self.db();
+
+ // Get AST nodes for base expressions (for diagnostics).
+ let bases_tuple_elts = bases_node.as_tuple_expr().map(|t| t.elts.as_slice());
+
+ // We use a placeholder class literal for try_from_type (the subclass parameter is only
+ // used for Protocol/TypedDict detection which doesn't apply here).
+ let placeholder_class: ClassLiteral<'db> =
+ KnownClass::Object.try_to_class_literal(db).unwrap().into();
+
+ bases_type
+ .tuple_instance_spec(db)
+ .as_deref()
+ .and_then(|spec| spec.as_fixed_length())
+ .map(|tuple| {
+ // Fixed-length tuple: extract each base class
+ tuple
+ .elements_slice()
+ .iter()
+ .enumerate()
+ .map(|(idx, base)| {
+ // First try the standard conversion.
+ if let Some(class_base) =
+ ClassBase::try_from_type(db, *base, placeholder_class)
+ {
+ return class_base;
+ }
+
+ let diagnostic_node = bases_tuple_elts
+ .and_then(|elts| elts.get(idx))
+ .unwrap_or(bases_node);
+
+ // If that fails, check if the type is "type-like" (e.g., `type[Base]`).
+ // For type-like bases we emit `unsupported-dynamic-base` and use
+ // `Unknown` to avoid cascading errors. For non-type-like bases (like
+ // integers), we return `None` to fall through to regular call binding
+ // which will emit `invalid-argument-type`.
+ let instance_of_type = KnownClass::Type.to_instance(db);
+
+ if base.is_assignable_to(db, instance_of_type) {
+ if let Some(builder) = self
+ .context
+ .report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_node)
+ {
+ let mut diagnostic =
+ builder.into_diagnostic("Unsupported class base");
+ diagnostic.set_primary_message(format_args!(
+ "Has type `{}`",
+ base.display(db)
+ ));
+ diagnostic.info(format_args!(
+ "ty cannot determine a MRO for class `{name}` due to this base"
+ ));
+ diagnostic.info(
+ "Only class objects or `Any` are supported as class bases",
+ );
+ }
+ } else {
+ if let Some(builder) =
+ self.context.report_lint(&INVALID_BASE, diagnostic_node)
+ {
+ let mut diagnostic = builder.into_diagnostic(format_args!(
+ "Invalid class base with type `{}`",
+ base.display(db)
+ ));
+ if bases_tuple_elts.is_none() {
+ diagnostic.info(format_args!(
+ "Element {} of the tuple is invalid",
+ idx + 1
+ ));
+ }
+ }
+ }
+
+ ClassBase::unknown()
+ })
+ .collect()
+ })
+ .unwrap_or_else(|| {
+ if !bases_type.is_assignable_to(
+ db,
+ Type::homogeneous_tuple(db, KnownClass::Type.to_instance(db)),
+ ) && let Some(builder) =
+ self.context.report_lint(&INVALID_ARGUMENT_TYPE, bases_node)
+ {
+ let mut diagnostic = builder
+ .into_diagnostic("Invalid argument to parameter 2 (`bases`) of `type()`");
+ diagnostic.set_primary_message(format_args!(
+ "Expected `tuple[type, ...]`, found `{}`",
+ bases_type.display(db)
+ ));
+ }
+ Box::from([ClassBase::unknown()])
+ })
+ }
+
fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) {
if assignment.target.is_name_expr() {
self.infer_definition(assignment);
@@ -6145,14 +6402,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let class_literal = infer_definition_types(db, class_definition)
.declaration_type(class_definition)
.inner_type()
- .as_class_literal()?;
+ .as_class_literal()?
+ .as_static()?;
class_literal
.dataclass_params(db)
.map(|params| SmallVec::from(params.field_specifiers(db)))
.or_else(|| {
Some(SmallVec::from(
- CodeGeneratorKind::from_class(db, class_literal, None)?
+ CodeGeneratorKind::from_class(db, class_literal.into(), None)?
.dataclass_transformer_params()?
.field_specifiers(db),
))
@@ -8962,11 +9220,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// are handled by the default constructor-call logic (we synthesize a `__new__` method for them
// in `ClassType::own_class_member()`).
class.is_known(self.db(), KnownClass::Tuple) && !class.is_generic()
- ) || CodeGeneratorKind::TypedDict.matches(
- self.db(),
- class.class_literal(self.db()).0,
- class.class_literal(self.db()).1,
- );
+ ) || class
+ .static_class_literal(self.db())
+ .is_some_and(|(class_literal, specialization)| {
+ CodeGeneratorKind::TypedDict.matches(
+ self.db(),
+ class_literal.into(),
+ specialization,
+ )
+ });
// temporary special-casing for all subclasses of `enum.Enum`
// until we support the functional syntax for creating enum classes
@@ -11936,7 +12198,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
));
}
- if let Some(generic_context) = class.generic_context(self.db()) {
+ if let Some(generic_context) = class.generic_context(self.db())
+ && let Some(class) = class.as_static()
+ {
return self.infer_explicit_class_specialization(
subscript,
value_ty,
@@ -12273,7 +12537,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
&mut self,
subscript: &ast::ExprSubscript,
value_ty: Type<'db>,
- generic_class: ClassLiteral<'db>,
+ generic_class: StaticClassLiteral<'db>,
generic_context: GenericContext<'db>,
) -> Type<'db> {
let db = self.db();
@@ -12991,7 +13255,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// TODO: properly handle old-style generics; get rid of this temporary hack
if !value_ty
.as_class_literal()
- .is_some_and(|class| class.iter_mro(db, None).contains(&ClassBase::Generic))
+ .is_some_and(|class| class.iter_mro(db).contains(&ClassBase::Generic))
{
report_not_subscriptable(context, subscript, value_ty, "__class_getitem__");
}
diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
index cd1d53c114..c7a59e0986 100644
--- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
@@ -1045,12 +1045,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
value_ty
}
Type::ClassLiteral(class) => {
- match class.generic_context(self.db()) {
- Some(generic_context) => {
+ match (class.generic_context(self.db()), class.as_static()) {
+ (Some(generic_context), Some(static_class)) => {
let specialized_class = self.infer_explicit_class_specialization(
subscript,
value_ty,
- class,
+ static_class,
generic_context,
);
@@ -1062,7 +1062,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
)
.unwrap_or(Type::unknown())
}
- None => {
+ _ => {
// TODO: emit a diagnostic if you try to specialize a non-generic class.
self.infer_type_expression(slice);
todo_type!("specialized non-generic class")
diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs
index 1d64549500..9065b4b87e 100644
--- a/crates/ty_python_semantic/src/types/instance.rs
+++ b/crates/ty_python_semantic/src/types/instance.rs
@@ -35,27 +35,39 @@ impl<'db> Type<'db> {
}
pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self {
- let (class_literal, specialization) = class.class_literal(db);
- match class_literal.known(db) {
- Some(KnownClass::Tuple) => Type::tuple(TupleType::new(
- db,
- specialization
- .and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?)))
- .unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown())))
- .as_ref(),
- )),
- Some(KnownClass::Object) => Type::object(),
- _ => class_literal
- .is_typed_dict(db)
- .then(|| Type::typed_dict(class))
- .or_else(|| {
- class.into_protocol_class(db).map(|protocol_class| {
- Self::ProtocolInstance(ProtocolInstanceType::from_class(protocol_class))
- })
- })
- .unwrap_or(Type::NominalInstance(NominalInstanceType(
- NominalInstanceInner::NonTuple(class),
- ))),
+ match class.class_literal(db) {
+ // Dynamic classes created via `type()` don't have special instance types.
+ // TODO: When we add functional TypedDict support, this branch should check
+ // for TypedDict and return `Type::typed_dict(class)` for that case.
+ ClassLiteral::Dynamic(_) => {
+ Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple(class)))
+ }
+ ClassLiteral::Static(class_literal) => {
+ let specialization = class.into_generic_alias().map(|g| g.specialization(db));
+ match class_literal.known(db) {
+ Some(KnownClass::Tuple) => Type::tuple(TupleType::new(
+ db,
+ specialization
+ .and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?)))
+ .unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown())))
+ .as_ref(),
+ )),
+ Some(KnownClass::Object) => Type::object(),
+ _ => class_literal
+ .is_typed_dict(db)
+ .then(|| Type::typed_dict(class))
+ .or_else(|| {
+ class.into_protocol_class(db).map(|protocol_class| {
+ Self::ProtocolInstance(ProtocolInstanceType::from_class(
+ protocol_class,
+ ))
+ })
+ })
+ .unwrap_or(Type::NominalInstance(NominalInstanceType(
+ NominalInstanceInner::NonTuple(class),
+ ))),
+ }
+ }
}
}
@@ -225,18 +237,9 @@ impl<'db> NominalInstanceType<'db> {
}
}
+ /// Returns the class literal for this instance.
pub(super) fn class_literal(&self, db: &'db dyn Db) -> ClassLiteral<'db> {
- let class = match self.0 {
- NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db),
- NominalInstanceInner::NonTuple(class) => class,
- NominalInstanceInner::Object => {
- return KnownClass::Object
- .try_to_class_literal(db)
- .expect("Typeshed should always have a `object` class in `builtins.pyi`");
- }
- };
- let (class_literal, _) = class.class_literal(db);
- class_literal
+ self.class(db).class_literal(db)
}
/// Returns the [`KnownClass`] that this is a nominal instance of, or `None` if it is not an
@@ -275,7 +278,7 @@ impl<'db> NominalInstanceType<'db> {
.find_map(|class| match class.known(db)? {
// N.B. this is a pure optimisation: iterating through the MRO would give us
// the correct tuple spec for `sys._version_info`, since we special-case the class
- // in `ClassLiteral::explicit_bases()` so that it is inferred as inheriting from
+ // in `StmtClassLiteral::explicit_bases()` so that it is inferred as inheriting from
// a tuple type with the correct spec for the user's configured Python version and platform.
KnownClass::VersionInfo => {
Some(Cow::Owned(TupleSpec::version_info_spec(db)))
@@ -337,10 +340,9 @@ impl<'db> NominalInstanceType<'db> {
NominalInstanceInner::ExactTuple(_) | NominalInstanceInner::Object => return None,
NominalInstanceInner::NonTuple(class) => class,
};
- let (class, Some(specialization)) = class.class_literal(db) else {
- return None;
- };
- if !class.is_known(db, KnownClass::Slice) {
+ let (class_literal, specialization) = class.static_class_literal(db)?;
+ let specialization = specialization?;
+ if !class_literal.is_known(db, KnownClass::Slice) {
return None;
}
let [start, stop, step] = specialization.types(db) else {
@@ -480,8 +482,13 @@ impl<'db> NominalInstanceType<'db> {
}
}
}
+
result.or(db, || {
- ConstraintSet::from(!(self.class(db)).could_coexist_in_mro_with(db, other.class(db)))
+ ConstraintSet::from(
+ !self
+ .class(db)
+ .could_coexist_in_mro_with(db, other.class(db)),
+ )
})
}
@@ -496,7 +503,7 @@ impl<'db> NominalInstanceType<'db> {
NominalInstanceInner::NonTuple(class) => class
.known(db)
.map(KnownClass::is_singleton)
- .unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db).0)),
+ .unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db))),
}
}
@@ -508,7 +515,7 @@ impl<'db> NominalInstanceType<'db> {
.known(db)
.and_then(KnownClass::is_single_valued)
.or_else(|| Some(self.tuple_spec(db)?.is_single_valued(db)))
- .unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db).0)),
+ .unwrap_or_else(|| is_single_member_enum(db, class.class_literal(db))),
}
}
@@ -621,7 +628,7 @@ pub(super) fn walk_protocol_instance_type<'db, V: super::visitor::TypeVisitor<'d
} else {
match protocol.inner {
Protocol::FromClass(class) => {
- if let Some(specialization) = class.class_literal(db).1 {
+ if let Some((_, Some(specialization))) = class.static_class_literal(db) {
walk_specialization(db, specialization, visitor);
}
}
diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs
index 1c7d20b5a6..1dc500e1b5 100644
--- a/crates/ty_python_semantic/src/types/list_members.rs
+++ b/crates/ty_python_semantic/src/types/list_members.rs
@@ -21,8 +21,9 @@ use crate::{
semantic_index, use_def_map,
},
types::{
- ClassBase, ClassLiteral, KnownClass, KnownInstanceType, SubclassOfInner, Type,
- TypeVarBoundOrConstraints, class::CodeGeneratorKind, generics::Specialization,
+ ClassBase, ClassLiteral, KnownClass, KnownInstanceType, StaticClassLiteral,
+ SubclassOfInner, Type, TypeVarBoundOrConstraints, class::CodeGeneratorKind,
+ generics::Specialization,
},
};
@@ -201,9 +202,20 @@ impl<'db> AllMembers<'db> {
),
Type::NominalInstance(instance) => {
- let (class_literal, specialization) = instance.class(db).class_literal(db);
- self.extend_with_instance_members(db, ty, class_literal);
- self.extend_with_synthetic_members(db, ty, class_literal, specialization);
+ let class = instance.class(db);
+ if let Some((class_literal, specialization)) = class.static_class_literal(db) {
+ self.extend_with_instance_members(db, ty, class_literal);
+ self.extend_with_synthetic_members(
+ db,
+ ty,
+ ClassLiteral::Static(class_literal),
+ specialization,
+ );
+ } else {
+ // For dynamic classes, we can't enumerate instance members (requires body scope),
+ // but we can still add synthetic members for dataclass-like classes.
+ self.extend_with_synthetic_members(db, ty, class.class_literal(db), None);
+ }
}
Type::NewTypeInstance(newtype) => {
@@ -232,8 +244,13 @@ impl<'db> AllMembers<'db> {
Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db);
- self.extend_with_class_members(db, ty, class_literal);
- self.extend_with_synthetic_members(db, ty, class_literal, None);
+ self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal));
+ self.extend_with_synthetic_members(
+ db,
+ ty,
+ ClassLiteral::Static(class_literal),
+ None,
+ );
if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) {
self.extend_with_class_members(db, ty, metaclass);
}
@@ -245,11 +262,23 @@ impl<'db> AllMembers<'db> {
}
_ => {
if let Some(class_type) = subclass_of_type.subclass_of().into_class(db) {
- let (class_literal, specialization) = class_type.class_literal(db);
- self.extend_with_class_members(db, ty, class_literal);
- self.extend_with_synthetic_members(db, ty, class_literal, specialization);
- if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) {
- self.extend_with_class_members(db, ty, metaclass);
+ if let Some((class_literal, specialization)) =
+ class_type.static_class_literal(db)
+ {
+ self.extend_with_class_members(
+ db,
+ ty,
+ ClassLiteral::Static(class_literal),
+ );
+ self.extend_with_synthetic_members(
+ db,
+ ty,
+ ClassLiteral::Static(class_literal),
+ specialization,
+ );
+ if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) {
+ self.extend_with_class_members(db, ty, metaclass);
+ }
}
}
}
@@ -308,13 +337,15 @@ impl<'db> AllMembers<'db> {
self.extend_with_class_members(db, ty, class_literal);
}
Type::SubclassOf(subclass_of) => {
- if let Some(class) = subclass_of.subclass_of().into_class(db) {
- self.extend_with_class_members(db, ty, class.class_literal(db).0);
+ if let Some(class) = subclass_of.subclass_of().into_class(db)
+ && let Some((class_literal, _)) = class.static_class_literal(db)
+ {
+ self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal));
}
}
Type::GenericAlias(generic_alias) => {
let class_literal = generic_alias.origin(db);
- self.extend_with_class_members(db, ty, class_literal);
+ self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal));
}
_ => {}
},
@@ -324,7 +355,7 @@ impl<'db> AllMembers<'db> {
self.extend_with_class_members(db, ty, class_literal);
}
- if let Type::ClassLiteral(class) =
+ if let Type::ClassLiteral(ClassLiteral::Static(class)) =
KnownClass::TypedDictFallback.to_class_literal(db)
{
self.extend_with_instance_members(db, ty, class);
@@ -430,9 +461,9 @@ impl<'db> AllMembers<'db> {
class_literal: ClassLiteral<'db>,
) {
for parent in class_literal
- .iter_mro(db, None)
+ .iter_mro(db)
.filter_map(ClassBase::into_class)
- .map(|class| class.class_literal(db).0)
+ .filter_map(|class| class.static_class_literal(db).map(|(lit, _)| lit))
{
let parent_scope = parent.body_scope(db);
for memberdef in all_end_of_scope_members(db, parent_scope) {
@@ -448,52 +479,64 @@ impl<'db> AllMembers<'db> {
}
}
- fn extend_with_instance_members(
+ /// Extend with instance members from a single class (not its MRO).
+ fn extend_with_instance_members_for_class(
&mut self,
db: &'db dyn Db,
ty: Type<'db>,
- class_literal: ClassLiteral<'db>,
+ class_literal: StaticClassLiteral<'db>,
) {
- for parent in class_literal
- .iter_mro(db, None)
- .filter_map(ClassBase::into_class)
- .map(|class| class.class_literal(db).0)
- {
- let class_body_scope = parent.body_scope(db);
- let file = class_body_scope.file(db);
- let index = semantic_index(db, file);
- for function_scope_id in attribute_scopes(db, class_body_scope) {
- for place_expr in index.place_table(function_scope_id).members() {
- let Some(name) = place_expr.as_instance_attribute() else {
- continue;
- };
- let result = ty.member(db, name);
- let Some(ty) = result.place.ignore_possibly_undefined() else {
- continue;
- };
- self.members.insert(Member {
- name: Name::new(name),
- ty,
- });
- }
- }
-
- // This is very similar to `extend_with_class_members`,
- // but uses the type of the class instance to query the
- // class member. This gets us the right type for each
- // member, e.g., `SomeClass.__delattr__` is not a bound
- // method, but `instance_of_SomeClass.__delattr__` is.
- for memberdef in all_end_of_scope_members(db, class_body_scope) {
- let result = ty.member(db, memberdef.member.name.as_str());
+ let class_body_scope = class_literal.body_scope(db);
+ let file = class_body_scope.file(db);
+ let index = semantic_index(db, file);
+ for function_scope_id in attribute_scopes(db, class_body_scope) {
+ for place_expr in index.place_table(function_scope_id).members() {
+ let Some(name) = place_expr.as_instance_attribute() else {
+ continue;
+ };
+ let result = ty.member(db, name);
let Some(ty) = result.place.ignore_possibly_undefined() else {
continue;
};
self.members.insert(Member {
- name: memberdef.member.name,
+ name: Name::new(name),
ty,
});
}
}
+
+ // This is very similar to `extend_with_class_members`,
+ // but uses the type of the class instance to query the
+ // class member. This gets us the right type for each
+ // member, e.g., `SomeClass.__delattr__` is not a bound
+ // method, but `instance_of_SomeClass.__delattr__` is.
+ for memberdef in all_end_of_scope_members(db, class_body_scope) {
+ let result = ty.member(db, memberdef.member.name.as_str());
+ let Some(ty) = result.place.ignore_possibly_undefined() else {
+ continue;
+ };
+ self.members.insert(Member {
+ name: memberdef.member.name,
+ ty,
+ });
+ }
+ }
+
+ /// Extend with instance members from a class and all classes in its MRO.
+ fn extend_with_instance_members(
+ &mut self,
+ db: &'db dyn Db,
+ ty: Type<'db>,
+ class_literal: StaticClassLiteral<'db>,
+ ) {
+ for class in class_literal
+ .iter_mro(db, None)
+ .filter_map(ClassBase::into_class)
+ {
+ if let Some((class_literal, _)) = class.static_class_literal(db) {
+ self.extend_with_instance_members_for_class(db, ty, class_literal);
+ }
+ }
}
fn extend_with_synthetic_members(
diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs
index 21501060da..82430a0e8a 100644
--- a/crates/ty_python_semantic/src/types/mro.rs
+++ b/crates/ty_python_semantic/src/types/mro.rs
@@ -2,17 +2,20 @@ use std::collections::VecDeque;
use std::ops::Deref;
use indexmap::IndexMap;
-use rustc_hash::FxBuildHasher;
+use rustc_hash::{FxBuildHasher, FxHashSet};
use crate::Db;
use crate::types::class_base::ClassBase;
use crate::types::generics::Specialization;
-use crate::types::{ClassLiteral, ClassType, KnownClass, KnownInstanceType, SpecialFormType, Type};
+use crate::types::{
+ ClassLiteral, ClassType, DynamicClassLiteral, KnownInstanceType, SpecialFormType,
+ StaticClassLiteral, Type,
+};
/// The inferred method resolution order of a given class.
///
/// An MRO cannot contain non-specialized generic classes. (This is why [`ClassBase`] contains a
-/// [`ClassType`], not a [`ClassLiteral`].) Any generic classes in a base class list are always
+/// [`ClassType`], not a [`StaticClassLiteral`].) Any generic classes in a base class list are always
/// specialized — either because the class is explicitly specialized if there is a subscript
/// expression, or because we create the default specialization if there isn't.
///
@@ -29,12 +32,12 @@ use crate::types::{ClassLiteral, ClassType, KnownClass, KnownInstanceType, Speci
///
/// See [`ClassType::iter_mro`] for more details.
#[derive(PartialEq, Eq, Clone, Debug, salsa::Update, get_size2::GetSize)]
-pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>);
+pub(crate) struct Mro<'db>(Box<[ClassBase<'db>]>);
impl<'db> Mro<'db> {
/// Attempt to resolve the MRO of a given class. Because we derive the MRO from the list of
/// base classes in the class definition, this operation is performed on a [class
- /// literal][ClassLiteral], not a [class type][ClassType]. (You can _also_ get the MRO of a
+ /// literal][StaticClassLiteral], not a [class type][ClassType]. (You can _also_ get the MRO of a
/// class type, but this is done by first getting the MRO of the underlying class literal, and
/// specializing each base class as needed if the class type is a generic alias.)
///
@@ -46,35 +49,11 @@ impl<'db> Mro<'db> {
///
/// (We emit a diagnostic warning about the runtime `TypeError` in
/// [`super::infer::infer_scope_types`].)
- pub(super) fn of_class(
+ pub(super) fn of_static_class(
db: &'db dyn Db,
- class_literal: ClassLiteral<'db>,
+ class_literal: StaticClassLiteral<'db>,
specialization: Option>,
) -> Result> {
- let class = class_literal.apply_optional_specialization(db, specialization);
- // Special-case `NotImplementedType`: typeshed says that it inherits from `Any`,
- // but this causes more problems than it fixes.
- if class_literal.is_known(db, KnownClass::NotImplementedType) {
- return Ok(Self::from([ClassBase::Class(class), ClassBase::object(db)]));
- }
- Self::of_class_impl(db, class, class_literal.explicit_bases(db), specialization)
- .map_err(|err| err.into_mro_error(db, class))
- }
-
- pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self {
- Self::from([
- ClassBase::Class(class),
- ClassBase::unknown(),
- ClassBase::object(db),
- ])
- }
-
- fn of_class_impl(
- db: &'db dyn Db,
- class: ClassType<'db>,
- original_bases: &[Type<'db>],
- specialization: Option>,
- ) -> Result> {
/// Possibly add `Generic` to the resolved bases list.
///
/// This function is called in two cases:
@@ -106,6 +85,10 @@ impl<'db> Mro<'db> {
resolved_bases.push(ClassBase::Generic);
}
+ let class = class_literal.apply_optional_specialization(db, specialization);
+
+ let original_bases = class_literal.explicit_bases(db);
+
match original_bases {
// `builtins.object` is the special case:
// the only class in Python that has an MRO with length <2
@@ -156,18 +139,20 @@ impl<'db> Mro<'db> {
)
) =>
{
- ClassBase::try_from_type(db, *single_base, class.class_literal(db).0).map_or_else(
- || Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
- |single_base| {
- if single_base.has_cyclic_mro(db) {
- Err(MroErrorKind::InheritanceCycle)
- } else {
- Ok(std::iter::once(ClassBase::Class(class))
- .chain(single_base.mro(db, specialization))
- .collect())
- }
- },
- )
+ ClassBase::try_from_type(db, *single_base, ClassLiteral::Static(class_literal))
+ .map_or_else(
+ || Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
+ |single_base| {
+ if single_base.has_cyclic_mro(db) {
+ Err(MroErrorKind::InheritanceCycle)
+ } else {
+ Ok(std::iter::once(ClassBase::Class(class))
+ .chain(single_base.mro(db, specialization))
+ .collect())
+ }
+ },
+ )
+ .map_err(|err| err.into_mro_error(db, class))
}
// The class has multiple explicit bases.
@@ -191,7 +176,11 @@ impl<'db> Mro<'db> {
&original_bases[i + 1..],
);
} else {
- match ClassBase::try_from_type(db, *base, class.class_literal(db).0) {
+ match ClassBase::try_from_type(
+ db,
+ *base,
+ ClassLiteral::Static(class_literal),
+ ) {
Some(valid_base) => resolved_bases.push(valid_base),
None => invalid_bases.push((i, *base)),
}
@@ -199,7 +188,8 @@ impl<'db> Mro<'db> {
}
if !invalid_bases.is_empty() {
- return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()));
+ return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice())
+ .into_mro_error(db, class));
}
// `Generic` is implicitly added to the bases list of a class that has PEP-695 type parameters
@@ -211,7 +201,7 @@ impl<'db> Mro<'db> {
let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])];
for base in &resolved_bases {
if base.has_cyclic_mro(db) {
- return Err(MroErrorKind::InheritanceCycle);
+ return Err(MroErrorKind::InheritanceCycle.into_mro_error(db, class));
}
seqs.push(base.mro(db, specialization).collect());
}
@@ -239,7 +229,9 @@ impl<'db> Mro<'db> {
)
})
{
- return Err(MroErrorKind::Pep695ClassWithGenericInheritance);
+ return Err(
+ MroErrorKind::Pep695ClassWithGenericInheritance.into_mro_error(db, class)
+ );
}
let mut duplicate_dynamic_bases = false;
@@ -258,9 +250,11 @@ impl<'db> Mro<'db> {
// `inconsistent-mro` diagnostic (which would be accurate -- but not nearly as
// precise!).
for (index, base) in original_bases.iter().enumerate() {
- let Some(base) =
- ClassBase::try_from_type(db, *base, class.class_literal(db).0)
- else {
+ let Some(base) = ClassBase::try_from_type(
+ db,
+ *base,
+ ClassLiteral::Static(class_literal),
+ ) else {
continue;
};
base_to_indices.entry(base).or_default().push(index);
@@ -299,16 +293,118 @@ impl<'db> Mro<'db> {
} else {
Err(MroErrorKind::UnresolvableMro {
bases_list: original_bases.iter().copied().collect(),
- })
+ }
+ .into_mro_error(db, class))
}
} else {
- Err(MroErrorKind::DuplicateBases(
- duplicate_bases.into_boxed_slice(),
- ))
+ Err(
+ MroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice())
+ .into_mro_error(db, class),
+ )
}
}
}
}
+
+ pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self {
+ Self::from([
+ ClassBase::Class(class),
+ ClassBase::unknown(),
+ ClassBase::object(db),
+ ])
+ }
+
+ /// Attempt to resolve the MRO of a dynamic class (created via `type(name, bases, dict)`).
+ ///
+ /// Uses C3 linearization when possible, returning an error if the MRO cannot be resolved.
+ pub(super) fn of_dynamic_class(
+ db: &'db dyn Db,
+ dynamic: DynamicClassLiteral<'db>,
+ ) -> Result> {
+ let bases = dynamic.bases(db);
+
+ // Check for duplicate bases first, but skip dynamic bases like `Unknown` or `Any`.
+ let mut seen = FxHashSet::default();
+ let mut duplicates = Vec::new();
+ for base in bases {
+ if matches!(base, ClassBase::Dynamic(_)) {
+ continue;
+ }
+ if !seen.insert(*base) {
+ duplicates.push(*base);
+ }
+ }
+ if !duplicates.is_empty() {
+ return Err(
+ DynamicMroErrorKind::DuplicateBases(duplicates.into_boxed_slice())
+ .into_error(db, dynamic),
+ );
+ }
+
+ // Check if any bases are dynamic, like `Unknown` or `Any`.
+ let has_dynamic_bases = bases
+ .iter()
+ .any(|base| matches!(base, ClassBase::Dynamic(_)));
+
+ // Compute MRO using C3 linearization.
+ let mro_bases = if bases.is_empty() {
+ // Empty bases: MRO is just `object`.
+ Some(vec![ClassBase::object(db)])
+ } else if bases.len() == 1 {
+ // Single base: MRO is just that base's MRO.
+ Some(bases[0].mro(db, None).collect())
+ } else {
+ // Multiple bases: use C3 merge algorithm.
+ let mut seqs: Vec>> = Vec::with_capacity(bases.len() + 1);
+
+ // Add each base's MRO.
+ for base in bases {
+ seqs.push(base.mro(db, None).collect());
+ }
+
+ // Add the list of bases in order.
+ seqs.push(bases.iter().copied().collect());
+
+ c3_merge(seqs).map(|mro| mro.iter().copied().collect())
+ };
+
+ match mro_bases {
+ Some(mro) => {
+ let mut result = vec![ClassBase::Class(ClassType::NonGeneric(dynamic.into()))];
+ result.extend(mro);
+ Ok(Self::from(result))
+ }
+ None => {
+ // C3 merge failed. If there are dynamic bases, use the fallback MRO.
+ // Otherwise, report an error.
+ if has_dynamic_bases {
+ Ok(Self::dynamic_fallback(db, dynamic))
+ } else {
+ Err(DynamicMroErrorKind::UnresolvableMro.into_error(db, dynamic))
+ }
+ }
+ }
+ }
+
+ /// Compute a fallback MRO for a dynamic class when `of_dynamic_class` fails.
+ ///
+ /// Iterates over base MROs sequentially with deduplication.
+ pub(super) fn dynamic_fallback(db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>) -> Self {
+ let self_base = ClassBase::Class(ClassType::NonGeneric(dynamic.into()));
+ let mut result = vec![self_base];
+ let mut seen = FxHashSet::default();
+ seen.insert(self_base);
+
+ for base in dynamic.bases(db) {
+ for item in base.mro(db, None) {
+ if seen.insert(item) {
+ result.push(item);
+ }
+ }
+ }
+
+ Self::from(result)
+ }
}
impl<'db, const N: usize> From<[ClassBase<'db>; N]> for Mro<'db> {
@@ -354,8 +450,8 @@ impl<'db> FromIterator> for Mro<'db> {
///
/// Even for first-party code, where we will have to resolve the MRO for every class we encounter,
/// loading the cached MRO comes with a certain amount of overhead, so it's best to avoid calling the
-/// Salsa-tracked [`ClassLiteral::try_mro`] method unless it's absolutely necessary.
-pub(super) struct MroIterator<'db> {
+/// Salsa-tracked [`StaticClassLiteral::try_mro`] method unless it's absolutely necessary.
+pub(crate) struct MroIterator<'db> {
db: &'db dyn Db,
/// The class whose MRO we're iterating over
@@ -390,19 +486,39 @@ impl<'db> MroIterator<'db> {
}
}
+ fn first_element(&self) -> ClassBase<'db> {
+ match self.class {
+ ClassLiteral::Static(literal) => ClassBase::Class(
+ literal.apply_optional_specialization(self.db, self.specialization),
+ ),
+ ClassLiteral::Dynamic(literal) => {
+ ClassBase::Class(ClassType::NonGeneric(literal.into()))
+ }
+ }
+ }
+
/// Materialize the full MRO of the class.
/// Return an iterator over that MRO which skips the first element of the MRO.
- fn full_mro_except_first_element(&mut self) -> impl Iterator
- > + '_ {
+ fn full_mro_except_first_element(&mut self) -> &mut std::slice::Iter<'db, ClassBase<'db>> {
self.subsequent_elements
- .get_or_insert_with(|| {
- let mut full_mro_iter = match self.class.try_mro(self.db, self.specialization) {
- Ok(mro) => mro.iter(),
- Err(error) => error.fallback_mro().iter(),
- };
- full_mro_iter.next();
- full_mro_iter
+ .get_or_insert_with(|| match self.class {
+ ClassLiteral::Static(literal) => {
+ let mut full_mro_iter = match literal.try_mro(self.db, self.specialization) {
+ Ok(mro) => mro.iter(),
+ Err(error) => error.fallback_mro().iter(),
+ };
+ full_mro_iter.next();
+ full_mro_iter
+ }
+ ClassLiteral::Dynamic(literal) => {
+ let mut full_mro_iter = match literal.try_mro(self.db) {
+ Ok(mro) => mro.iter(),
+ Err(error) => error.fallback_mro().iter(),
+ };
+ full_mro_iter.next();
+ full_mro_iter
+ }
})
- .copied()
}
}
@@ -412,12 +528,9 @@ impl<'db> Iterator for MroIterator<'db> {
fn next(&mut self) -> Option {
if !self.first_element_yielded {
self.first_element_yielded = true;
- return Some(ClassBase::Class(
- self.class
- .apply_optional_specialization(self.db, self.specialization),
- ));
+ return Some(self.first_element());
}
- self.full_mro_except_first_element().next()
+ self.full_mro_except_first_element().next().copied()
}
}
@@ -544,3 +657,49 @@ fn c3_merge(mut sequences: Vec>) -> Option {
}
}
}
+
+/// Error for dynamic class MRO computation with fallback MRO.
+///
+/// Separate from [`MroError`] because dynamic classes can only have a subset of MRO errors.
+#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)]
+pub(crate) struct DynamicMroError<'db> {
+ kind: DynamicMroErrorKind<'db>,
+ fallback_mro: Mro<'db>,
+}
+
+impl<'db> DynamicMroError<'db> {
+ /// Return the error kind describing why we could not resolve the MRO.
+ pub(crate) fn reason(&self) -> &DynamicMroErrorKind<'db> {
+ &self.kind
+ }
+
+ /// Return the fallback MRO to use for type inference.
+ pub(crate) fn fallback_mro(&self) -> &Mro<'db> {
+ &self.fallback_mro
+ }
+}
+
+/// Error kinds for dynamic class MRO computation.
+///
+/// These mirror the relevant variants from `MroErrorKind` for static classes.
+#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)]
+pub(crate) enum DynamicMroErrorKind<'db> {
+ /// The class has duplicate bases in its bases tuple.
+ DuplicateBases(Box<[ClassBase<'db>]>),
+
+ /// The MRO is unresolvable through the C3-merge algorithm.
+ UnresolvableMro,
+}
+
+impl<'db> DynamicMroErrorKind<'db> {
+ fn into_error(
+ self,
+ db: &'db dyn Db,
+ class_literal: DynamicClassLiteral<'db>,
+ ) -> DynamicMroError<'db> {
+ DynamicMroError {
+ kind: self,
+ fallback_mro: Mro::dynamic_fallback(db, class_literal),
+ }
+ }
+}
diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs
index e939c59826..49461f9d06 100644
--- a/crates/ty_python_semantic/src/types/narrow.rs
+++ b/crates/ty_python_semantic/src/types/narrow.rs
@@ -155,7 +155,7 @@ impl ClassInfoConstraintFunction {
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> {
- let constraint_fn = |class: ClassLiteral<'db>| match self {
+ let constraint_from_class_literal = |class: ClassLiteral<'db>| match self {
ClassInfoConstraintFunction::IsInstance => {
Type::instance(db, class.top_materialization(db))
}
@@ -166,9 +166,11 @@ impl ClassInfoConstraintFunction {
match classinfo {
Type::TypeAlias(alias) => self.generate_constraint(db, alias.value_type(db)),
- Type::ClassLiteral(class_literal) => Some(constraint_fn(class_literal)),
+ Type::ClassLiteral(class_literal) => Some(constraint_from_class_literal(class_literal)),
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
- SubclassOfInner::Class(ClassType::NonGeneric(class)) => Some(constraint_fn(class)),
+ SubclassOfInner::Class(ClassType::NonGeneric(class_literal)) => {
+ Some(constraint_from_class_literal(class_literal))
+ }
// It's not valid to use a generic alias as the second argument to `isinstance()` or `issubclass()`,
// e.g. `isinstance(x, list[int])` fails at runtime.
SubclassOfInner::Class(ClassType::Generic(_)) => None,
diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs
index a320032542..15094518e8 100644
--- a/crates/ty_python_semantic/src/types/overrides.rs
+++ b/crates/ty_python_semantic/src/types/overrides.rs
@@ -16,7 +16,7 @@ use crate::{
symbol::ScopedSymbolId, use_def_map,
},
types::{
- ClassBase, ClassLiteral, ClassType, KnownClass, Type,
+ ClassBase, ClassType, KnownClass, StaticClassLiteral, Type,
class::CodeGeneratorKind,
context::InferContext,
diagnostic::{
@@ -45,7 +45,10 @@ const PROHIBITED_NAMEDTUPLE_ATTRS: &[&str] = &[
"_source",
];
-pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLiteral<'db>) {
+// TODO: Support dynamic class literals. If we allow dynamic classes to define attributes in their
+// namespace dictionary, we should also check whether those attributes are valid overrides of
+// attributes in their superclasses.
+pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>) {
let db = context.db();
let configuration = OverrideRulesConfig::from(context);
if configuration.no_rules_enabled() {
@@ -118,8 +121,10 @@ fn check_class_declaration<'db>(
return;
};
- let (literal, specialization) = class.class_literal(db);
- let class_kind = CodeGeneratorKind::from_class(db, literal, specialization);
+ let Some((literal, specialization)) = class.static_class_literal(db) else {
+ return;
+ };
+ let class_kind = CodeGeneratorKind::from_class(db, literal.into(), specialization);
// Check for prohibited `NamedTuple` attribute overrides.
//
@@ -171,7 +176,11 @@ fn check_class_declaration<'db>(
ClassBase::Class(class) => class,
};
- let (superclass_literal, superclass_specialization) = superclass.class_literal(db);
+ let Some((superclass_literal, superclass_specialization)) =
+ superclass.static_class_literal(db)
+ else {
+ continue;
+ };
let superclass_scope = superclass_literal.body_scope(db);
let superclass_symbol_table = place_table(db, superclass_scope);
let superclass_symbol_id = superclass_symbol_table.symbol_id(&member.name);
@@ -191,10 +200,13 @@ fn check_class_declaration<'db>(
{
continue;
}
- method_kind =
- CodeGeneratorKind::from_class(db, superclass_literal, superclass_specialization)
- .map(MethodKind::Synthesized)
- .unwrap_or_default();
+ method_kind = CodeGeneratorKind::from_class(
+ db,
+ superclass_literal.into(),
+ superclass_specialization,
+ )
+ .map(MethodKind::Synthesized)
+ .unwrap_or_default();
}
let Place::Defined(DefinedPlace {
diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs
index 3262769459..83a9a9c89e 100644
--- a/crates/ty_python_semantic/src/types/protocol_class.rs
+++ b/crates/ty_python_semantic/src/types/protocol_class.rs
@@ -16,9 +16,9 @@ use crate::{
},
semantic_index::{definition::Definition, place::ScopedPlaceId, place_table, use_def_map},
types::{
- ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral,
- ClassType, FindLegacyTypeVarsVisitor, InstanceFallbackShadowsNonDataDescriptor,
- KnownFunction, MemberLookupPolicy, NormalizedVisitor, PropertyInstanceType, Signature,
+ ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassType,
+ FindLegacyTypeVarsVisitor, InstanceFallbackShadowsNonDataDescriptor, KnownFunction,
+ MemberLookupPolicy, NormalizedVisitor, PropertyInstanceType, Signature, StaticClassLiteral,
Type, TypeMapping, TypeQualifiers, TypeVarVariance, VarianceInferable,
constraints::{ConstraintSet, IteratorConstraintsExtension, OptionConstraintsExtension},
context::InferContext,
@@ -29,11 +29,11 @@ use crate::{
},
};
-impl<'db> ClassLiteral<'db> {
+impl<'db> StaticClassLiteral<'db> {
/// Returns `Some` if this is a protocol class, `None` otherwise.
pub(super) fn into_protocol_class(self, db: &'db dyn Db) -> Option> {
self.is_protocol(db)
- .then_some(ProtocolClass(ClassType::NonGeneric(self)))
+ .then_some(ProtocolClass(ClassType::NonGeneric(self.into())))
}
}
@@ -76,10 +76,12 @@ impl<'db> ProtocolClass<'db> {
}
pub(super) fn is_runtime_checkable(self, db: &'db dyn Db) -> bool {
- self.class_literal(db)
- .0
- .known_function_decorators(db)
- .contains(&KnownFunction::RuntimeCheckable)
+ self.static_class_literal(db)
+ .is_some_and(|(class_literal, _)| {
+ class_literal
+ .known_function_decorators(db)
+ .contains(&KnownFunction::RuntimeCheckable)
+ })
}
/// Iterate through the body of the protocol class. Check that all definitions
@@ -88,7 +90,10 @@ impl<'db> ProtocolClass<'db> {
pub(super) fn validate_members(self, context: &InferContext) {
let db = context.db();
let interface = self.interface(db);
- let body_scope = self.class_literal(db).0.body_scope(db);
+ let Some((class_literal, _)) = self.static_class_literal(db) else {
+ return;
+ };
+ let body_scope = class_literal.body_scope(db);
let class_place_table = place_table(db, body_scope);
for (symbol_id, mut bindings_iterator) in
@@ -104,7 +109,11 @@ impl<'db> ProtocolClass<'db> {
self.iter_mro(db)
.filter_map(ClassBase::into_class)
.any(|superclass| {
- let superclass_scope = superclass.class_literal(db).0.body_scope(db);
+ let Some((superclass_literal, _)) = superclass.static_class_literal(db)
+ else {
+ return false;
+ };
+ let superclass_scope = superclass_literal.body_scope(db);
let Some(scoped_symbol_id) =
place_table(db, superclass_scope).symbol_id(symbol_name)
else {
@@ -879,7 +888,7 @@ impl BoundOnClass {
}
}
-/// Inner Salsa query for [`ProtocolClassLiteral::interface`].
+/// Inner Salsa query for [`ProtocolClass::interface`].
#[salsa::tracked(cycle_initial=proto_interface_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
fn cached_protocol_interface<'db>(
db: &'db dyn Db,
@@ -887,15 +896,16 @@ fn cached_protocol_interface<'db>(
) -> ProtocolInterface<'db> {
let mut members = BTreeMap::default();
- for (parent_protocol, specialization) in class
+ for (parent_scope, specialization) in class
.iter_mro(db)
.filter_map(ClassBase::into_class)
.filter_map(|class| {
- let (class, specialization) = class.class_literal(db);
- Some((class.into_protocol_class(db)?, specialization))
+ let (class_literal, specialization) = class.static_class_literal(db)?;
+ let protocol_class = class_literal.into_protocol_class(db)?;
+ let parent_scope = protocol_class.static_class_literal(db)?.0.body_scope(db);
+ Some((parent_scope, specialization))
})
{
- let parent_scope = parent_protocol.class_literal(db).0.body_scope(db);
let use_def_map = use_def_map(db, parent_scope);
let place_table = place_table(db, parent_scope);
let mut direct_members = FxHashMap::default();
diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs
index 8038b19c88..aaac6aed54 100644
--- a/crates/ty_python_semantic/src/types/subclass_of.rs
+++ b/crates/ty_python_semantic/src/types/subclass_of.rs
@@ -7,8 +7,8 @@ use crate::types::protocol_class::ProtocolClass;
use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation};
use crate::types::variance::VarianceInferable;
use crate::types::{
- ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, DynamicType,
- FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, MemberLookupPolicy,
+ ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, ClassType, DynamicClassLiteral,
+ DynamicType, FindLegacyTypeVarsVisitor, KnownClass, MaterializationKind, MemberLookupPolicy,
NormalizedVisitor, SpecialFormType, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints,
TypedDictType, UnionType, todo_type,
};
@@ -334,7 +334,7 @@ impl<'db> SubclassOfType<'db> {
pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
self.subclass_of
.into_class(db)
- .is_some_and(|class| class.class_literal(db).0.is_typed_dict(db))
+ .is_some_and(|class| class.class_literal(db).is_typed_dict(db))
}
}
@@ -534,3 +534,9 @@ impl<'db> From> for Type<'db> {
}
}
}
+
+impl<'db> From> for SubclassOfInner<'db> {
+ fn from(value: DynamicClassLiteral<'db>) -> Self {
+ SubclassOfInner::Class(ClassType::NonGeneric(ClassLiteral::Dynamic(value)))
+ }
+}
diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs
index dfc484cabd..70a30e5555 100644
--- a/crates/ty_python_semantic/src/types/tuple.rs
+++ b/crates/ty_python_semantic/src/types/tuple.rs
@@ -1313,6 +1313,14 @@ pub enum Tuple {
}
impl Tuple {
+ /// Returns the inner fixed-length tuple if this is a `Tuple::Fixed` variant.
+ pub(crate) fn as_fixed_length(&self) -> Option<&FixedLengthTuple> {
+ match self {
+ Tuple::Fixed(tuple) => Some(tuple),
+ Tuple::Variable(_) => None,
+ }
+ }
+
pub(crate) const fn homogeneous(element: T) -> Self {
Self::Variable(VariableLengthTuple::homogeneous(element))
}
diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs
index af512ae463..d7a93d8bcf 100644
--- a/crates/ty_python_semantic/src/types/typed_dict.rs
+++ b/crates/ty_python_semantic/src/types/typed_dict.rs
@@ -19,6 +19,7 @@ use super::diagnostic::{
use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor};
use crate::Db;
use crate::semantic_index::definition::Definition;
+use crate::types::TypeDefinition;
use crate::types::class::FieldKind;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::generics::InferableTypeVars;
@@ -80,7 +81,9 @@ impl<'db> TypedDictType<'db> {
pub(crate) fn items(self, db: &'db dyn Db) -> &'db TypedDictSchema<'db> {
#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)]
fn class_based_items<'db>(db: &'db dyn Db, class: ClassType<'db>) -> TypedDictSchema<'db> {
- let (class_literal, specialization) = class.class_literal(db);
+ let Some((class_literal, specialization)) = class.static_class_literal(db) else {
+ return TypedDictSchema::default();
+ };
class_literal
.fields(db, specialization, CodeGeneratorKind::TypedDict)
.into_iter()
@@ -305,6 +308,13 @@ impl<'db> TypedDictType<'db> {
}
}
+ pub fn type_definition(self, db: &'db dyn Db) -> Option> {
+ match self {
+ TypedDictType::Class(defining_class) => Some(defining_class.type_definition(db)),
+ TypedDictType::Synthesized(_) => None,
+ }
+ }
+
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
match self {
TypedDictType::Class(_) => {
diff --git a/crates/ty_test/src/matcher.rs b/crates/ty_test/src/matcher.rs
index 1645aa6d3c..5e67f5f778 100644
--- a/crates/ty_test/src/matcher.rs
+++ b/crates/ty_test/src/matcher.rs
@@ -200,13 +200,28 @@ impl UnmatchedWithColumn for &Diagnostic {
/// Discard `@Todo`-type metadata from expected types, which is not available
/// when running in release mode.
+///
+/// Some `@Todo` variants (like `@Todo(StarredExpression)` and `@Todo(typing.Unpack)`)
+/// are hardcoded enum variants that always display their message, so we preserve those.
fn discard_todo_metadata(ty: &str) -> Cow<'_, str> {
#[cfg(not(debug_assertions))]
{
+ /// `@Todo` variants that are hardcoded and always display their message,
+ /// even in release mode.
+ const PRESERVED_TODO_VARIANTS: &[&str] =
+ &["@Todo(StarredExpression)", "@Todo(typing.Unpack)"];
+
static TODO_METADATA_REGEX: LazyLock =
LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap());
- TODO_METADATA_REGEX.replace_all(ty, "@Todo")
+ TODO_METADATA_REGEX.replace_all(ty, |caps: ®ex::Captures| {
+ let matched = caps.get(0).unwrap().as_str();
+ if PRESERVED_TODO_VARIANTS.contains(&matched) {
+ matched.to_string()
+ } else {
+ "@Todo".to_string()
+ }
+ })
}
#[cfg(debug_assertions)]
diff --git a/ty.schema.json b/ty.schema.json
index 458cb9c2af..acdf95b7ef 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -1160,6 +1160,16 @@
}
]
},
+ "unsupported-dynamic-base": {
+ "title": "detects dynamic class bases that are unsupported as ty could not feasibly calculate the class's MRO",
+ "description": "## What it does\nChecks for dynamic class definitions (using `type()`) that have bases\nwhich are unsupported by ty.\n\nThis is equivalent to [`unsupported-base`] but applies to classes created\nvia `type()` rather than `class` statements.\n\n## Why is this bad?\nIf a dynamically created class has a base that is an unsupported type\nsuch as `type[T]`, ty will not be able to resolve the\n[method resolution order] (MRO) for the class. This may lead to an inferior\nunderstanding of your codebase and unpredictable type-checking behavior.\n\n## Default level\nThis rule is disabled by default because it will not cause a runtime error,\nand may be noisy on codebases that use `type()` in highly dynamic ways.\n\n## Examples\n```python\ndef factory(base: type[Base]) -> type:\n # `base` has type `type[Base]`, not `type[Base]` itself\n return type(\"Dynamic\", (base,), {}) # error: [unsupported-dynamic-base]\n```\n\n[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order\n[`unsupported-base`]: https://docs.astral.sh/ty/rules/unsupported-base",
+ "default": "ignore",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"unsupported-operator": {
"title": "detects binary, unary, or comparison expressions where the operands don't support the operator",
"description": "## What it does\nChecks for binary expressions, comparisons, and unary expressions where\nthe operands don't support the operator.\n\n## Why is this bad?\nAttempting to use an unsupported operator will raise a `TypeError` at\nruntime.\n\n## Examples\n```python\nclass A: ...\n\nA() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'\n```",