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```",