diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index d6391b94659cb3..e6a59c660f8a2b 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 4000, + 4057, ); static PYDANTIC: Benchmark = Benchmark::new( @@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new( max_dep_date: "2025-06-17", python_version: PythonVersion::PY312, }, - 13116, + 13200, ); static TANJUN: Benchmark = Benchmark::new( diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 593b1008bffdc6..2e1ae6d75db56d 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -3769,6 +3769,7 @@ quux. __eq__ :: bound method Quux.__eq__(value: object, /) -> bool __format__ :: bound method Quux.__format__(format_spec: str, /) -> str __ge__ :: bound method Quux.__ge__(value: tuple[int | str, ...], /) -> bool + __getattr__ :: bound method NamedTupleFallback.__getattr__(name: str, /) -> Any __getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any __getitem__ :: Overload[(index: Literal[-2, 0], /) -> int, (index: Literal[-1, 1], /) -> str, (index: SupportsIndex, /) -> int | str, (index: slice[Any, Any, Any], /) -> tuple[int | str, ...]] __getstate__ :: bound method Quux.__getstate__() -> object @@ -3783,7 +3784,7 @@ quux. __module__ :: str __mul__ :: bound method Quux.__mul__(value: SupportsIndex, /) -> tuple[int | str, ...] __ne__ :: bound method Quux.__ne__(value: object, /) -> bool - __new__ :: (x: int, y: str) -> None + __new__ :: (x: int, y: str) -> Quux __orig_bases__ :: tuple[Any, ...] __reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...] __reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...] diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index f9342151eaf6d6..1c7a6f50c4ee0c 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -23,14 +23,259 @@ tests for the `__class__` attribute.) reveal_type(type(1)) # revealed: ``` -But a three-argument call to type creates a dynamic instance of the `type` class: +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: ... -reveal_type(type("Foo", (), {})) # revealed: type +# We synthesize a class type using the name argument +reveal_type(type("Foo", (), {})) # revealed: -reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type +# With a single base class +reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: + +# With multiple base classes +reveal_type(type("Foo", (Base, Mixin), {})) # 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] +``` + +Instances of functional 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 +``` + +Regular classes can inherit from functional classes: + +```py +class Base: + base_attr: int = 1 + +FunctionalClass = type("FunctionalClass", (Base,), {}) + +class Child(FunctionalClass): + child_attr: str = "child" + +child = Child() + +# Attributes from the functional 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 FunctionalClass instances +def takes_functional(x: FunctionalClass) -> None: ... + +takes_functional(child) # No error - Child is a subtype of FunctionalClass + +# isinstance narrows to the functional class instance type +def check_isinstance(x: object) -> None: + if isinstance(x, FunctionalClass): + reveal_type(x) # revealed: FunctionalClass +``` + +Functional classes are correctly recognized as disjoint from unrelated types: + +```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 + +# Functional class inheriting from int is NOT disjoint from int +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 also works for `type[]` of functional classes: + +```py +from ty_extensions import is_disjoint_from, static_assert + +# Functional 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])) + +# Functional 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])) +``` + +Functional classes can be used as pivot in `super()`: + +```py +class Base: + def method(self) -> int: + return 42 + +FunctionalChild = type("FunctionalChild", (Base,), {}) + +# Using functional class as pivot with functional class instance owner +fc = FunctionalChild() +reveal_type(super(FunctionalChild, fc)) # revealed: +reveal_type(super(FunctionalChild, fc).method()) # revealed: int + +# Regular class inheriting from functional class +class RegularChild(FunctionalChild): + pass + +rc = RegularChild() +reveal_type(super(RegularChild, rc)) # revealed: , RegularChild> +reveal_type(super(RegularChild, rc).method()) # revealed: int + +# Using functional class as pivot with regular class instance owner +reveal_type(super(FunctionalChild, rc)) # revealed: +reveal_type(super(FunctionalChild, rc).method()) # revealed: int +``` + +Functional classes can inherit from other functional classes: + +```py +class Base: + base_attr: int = 1 + +# Create a functional class that inherits from a regular class. +Parent = type("Parent", (Base,), {}) +reveal_type(Parent) # revealed: + +# Create a functional class that inherits from another functional 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 +``` + +Functional classes with generic base classes: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Container(Generic[T]): + value: T + +# Functional 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(instance)` returns the class of the functional 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 functional instances: + +```py +class Base: ... + +Foo = type("Foo", (Base,), {}) +foo = Foo() + +# __class__ returns the class type +reveal_type(foo.__class__) # revealed: type[Foo] +``` + +Functional instances are subtypes of `object`: + +```py +class Base: ... + +Foo = type("Foo", (Base,), {}) +foo = Foo() + +# All functional instances are subtypes of object +def takes_object(x: object) -> None: ... + +takes_object(foo) # No error - Foo is a subtype of object + +# Even functional classes with no explicit bases are subtypes of object +EmptyBases = type("EmptyBases", (), {}) +empty = EmptyBases() +takes_object(empty) # No error ``` Other numbers of arguments are invalid @@ -61,6 +306,42 @@ type("Foo", (1, 2), {}) type("Foo", (Base,), {b"attr": 1}) ``` +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), {}) +``` + +```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 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), {}) +``` + ## Calls to `str()` ### Valid calls diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 94b7d45da3fb6b..3263d219092a89 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -1688,3 +1688,254 @@ reveal_type(ordered_foo()) # revealed: @Todo(Type::Intersection.call) # TODO: should be `Any` reveal_type(ordered_foo() < ordered_foo()) # revealed: @Todo(Type::Intersection.call) ``` + +## `dataclasses.make_dataclass` + +The `make_dataclass` function creates dataclasses dynamically. + +### Basic usage + +Using tuple syntax for fields (recommended for best type inference): + +```py +from dataclasses import make_dataclass + +Point = make_dataclass("Point", (("x", int), ("y", int))) + +reveal_type(Point) # revealed: + +p = Point(1, 2) +reveal_type(p) # revealed: Point +reveal_type(p.x) # revealed: int +reveal_type(p.y) # revealed: int + +# The constructor accepts keyword arguments. +p2 = Point(x=3, y=4) +reveal_type(p2) # revealed: Point +``` + +### Fields with types + +Fields can be specified as `(name, type)` tuples: + +```py +from dataclasses import make_dataclass + +Person = make_dataclass("Person", (("name", str), ("age", int))) + +alice = Person("Alice", 30) +reveal_type(alice.name) # revealed: str +reveal_type(alice.age) # revealed: int + +# error: [missing-argument] "No argument provided for required parameter `age`" +Person("Bob") + +# error: [too-many-positional-arguments] +Person("Eve", 20, "extra") +``` + +### List syntax + +List syntax works the same as tuple syntax: + +```py +from dataclasses import make_dataclass + +Point = make_dataclass("Point", [("x", int), ("y", int)]) +reveal_type(Point) # revealed: + +p = Point(1, 2) +reveal_type(p) # revealed: Point +reveal_type(p.x) # revealed: int +reveal_type(p.y) # revealed: int +``` + +### Fields with defaults + +Fields can have default values using the 3-tuple syntax `(name, type, default)`: + +```py +from dataclasses import make_dataclass + +Person = make_dataclass("Person", [("name", str), ("age", int, 0)]) + +# With default, `age` is optional. +bob = Person("Bob") +reveal_type(bob) # revealed: Person +reveal_type(bob.name) # revealed: str +reveal_type(bob.age) # revealed: int + +# Can still provide all arguments. +alice = Person("Alice", 30) +reveal_type(alice.age) # revealed: int + +# Keyword arguments work too. +charlie = Person(name="Charlie", age=25) +reveal_type(charlie) # revealed: Person +``` + +### String-only fields + +Fields can be specified as just a string (field name only, type is `Any`): + +```py +from dataclasses import make_dataclass + +# String-only fields have type Any. +Flexible = make_dataclass("Flexible", ["x", "y"]) + +f = Flexible(1, "hello") +reveal_type(f) # revealed: Flexible +reveal_type(f.x) # revealed: Any +reveal_type(f.y) # revealed: Any +``` + +### Base classes + +The `bases` keyword argument specifies base classes: + +```py +from dataclasses import make_dataclass + +class Base: + def greet(self) -> str: + return "Hello" + +Derived = make_dataclass("Derived", [("value", int)], bases=(Base,)) + +d = Derived(42) +reveal_type(d) # revealed: Derived +reveal_type(d.value) # revealed: int +reveal_type(d.greet()) # revealed: str +``` + +### The `order` parameter + +When `order=True`, comparison methods are generated: + +```py +from dataclasses import make_dataclass + +Ordered = make_dataclass("Ordered", [("x", int)], order=True) + +a = Ordered(1) +b = Ordered(2) + +reveal_type(a < b) # revealed: bool +reveal_type(a <= b) # revealed: bool +reveal_type(a > b) # revealed: bool +reveal_type(a >= b) # revealed: bool +``` + +When `order=False` (the default), comparison methods are not generated: + +```py +from dataclasses import make_dataclass + +Unordered = make_dataclass("Unordered", [("x", int)]) + +a = Unordered(1) +b = Unordered(2) + +# error: [unsupported-operator] "Operator `<` is not supported between two objects of type `Unordered`" +a < b +``` + +### The `frozen` parameter + +When `frozen=True`, the dataclass is immutable and hashable: + +```py +from dataclasses import make_dataclass + +Frozen = make_dataclass("Frozen", [("x", int)], frozen=True) + +f = Frozen(1) +reveal_type(hash(f)) # revealed: int +``` + +### The `eq` parameter + +When `eq=False`, the `__eq__` method is not generated, and `__hash__` falls back to +`object.__hash__`: + +```py +from dataclasses import make_dataclass + +NoEq = make_dataclass("NoEq", [("x", int)], eq=False) + +a = NoEq(1) +# __hash__ is inherited from object. +reveal_type(hash(a)) # revealed: int +``` + +### The `init` parameter + +When `init=False`, no `__init__` is generated and calling the class with arguments fails: + +```py +from dataclasses import make_dataclass + +NoInit = make_dataclass("NoInit", [("x", int)], init=False) + +# error: [possibly-missing-implicit-call] "`__init__` method is missing on type ``. Make sure your `object` in typeshed has its definition." +NoInit(1) +``` + +### The `kw_only` parameter + +When `kw_only=True`, all fields are keyword-only: + +```py +from dataclasses import make_dataclass + +KwOnly = make_dataclass("KwOnly", [("x", int), ("y", str)], kw_only=True) + +# Positional arguments are not allowed. +# error: [missing-argument] "No arguments provided for required parameters `x`, `y`" +# error: [too-many-positional-arguments] +KwOnly(1, "hello") + +# Keyword arguments work. +k = KwOnly(x=1, y="hello") +reveal_type(k) # revealed: KwOnly +``` + +### The `unsafe_hash` parameter + +When `unsafe_hash=True`, a `__hash__` method is generated even if `eq=True` and `frozen=False`: + +```py +from dataclasses import make_dataclass + +UnsafeHash = make_dataclass("UnsafeHash", [("x", int)], unsafe_hash=True) + +u = UnsafeHash(1) +reveal_type(hash(u)) # revealed: int +``` + +### Non-literal fields + +When fields are passed as a variable rather than a literal, we cannot synthesize the dataclass +fields. The return type is `type` instead of the specific dataclass class: + +```py +from dataclasses import make_dataclass + +fields = [("x", int), ("y", str)] +Dynamic = make_dataclass("Dynamic", fields) + +# When fields is a variable, we cannot infer the class structure. +reveal_type(Dynamic) # revealed: type +``` + +### Invalid fields type + +When an invalid type is passed as fields (not an iterable of field specs), we emit an error: + +```py +from dataclasses import make_dataclass + +# error: [invalid-argument-type] +Invalid = make_dataclass("Invalid", 42) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 7034a74176923b..2765f6f864f4bf 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -90,11 +90,32 @@ Alternative functional syntax: Person2 = NamedTuple("Person", [("id", int), ("name", str)]) alice2 = Person2(1, "Alice") -# TODO: should be an error +# error: [missing-argument] "No argument provided for required parameter `name`" Person2(1) -reveal_type(alice2.id) # revealed: @Todo(functional `NamedTuple` syntax) -reveal_type(alice2.name) # revealed: @Todo(functional `NamedTuple` syntax) +reveal_type(alice2.id) # revealed: int +reveal_type(alice2.name) # revealed: str +``` + +### Functional syntax with variable fields + +When fields are passed via a variable (not a literal), we fall back to `NamedTupleFallback` which +allows any attribute access. This is a regression test for accessing `Self` attributes in methods of +classes that inherit from namedtuples with dynamic fields: + +```py +from typing import NamedTuple +from typing_extensions import Self + +fields = [("host", str), ("port", int)] + +class Url(NamedTuple("Url", fields)): + def with_port(self, port: int) -> Self: + # Attribute access on Self works via NamedTupleFallback.__getattr__. + reveal_type(self.host) # revealed: Any + reveal_type(self.port) # revealed: Any + reveal_type(self.unknown) # revealed: Any + return self._replace(port=port) ``` ### Definition @@ -305,10 +326,156 @@ reveal_type(IntBox(1)._replace(content=42)) # revealed: IntBox ```py from collections import namedtuple -Person = namedtuple("Person", ["id", "name", "age"], defaults=[None]) +Person = namedtuple("Person", ["id", "name"]) -alice = Person(1, "Alice", 42) -bob = Person(2, "Bob") +alice = Person(1, "Alice") + +# Field access returns Any (no type information available). +reveal_type(alice.id) # revealed: Any +reveal_type(alice.name) # revealed: Any + +# Alternative field specifications. +Point1 = namedtuple("Point1", ["x", "y"]) +Point2 = namedtuple("Point2", "x y") +Point3 = namedtuple("Point3", "x, y") + +p1 = Point1(1, 2) +p2 = Point2(3, 4) +p3 = Point3(5, 6) + +reveal_type(p1.x) # revealed: Any +reveal_type(p2.x) # revealed: Any +reveal_type(p3.x) # revealed: Any +``` + +## `collections.namedtuple` with variable field names + +When field names are passed via a variable (not a literal), we fall back to `NamedTupleFallback` +which allows any attribute access. This is a regression test for accessing `Self` attributes in +methods of classes that inherit from namedtuples with dynamic fields: + +```py +from collections import namedtuple +from typing_extensions import Self + +field_names = ["host", "port"] + +class Url(namedtuple("Url", field_names)): + def with_port(self, port: int) -> Self: + # Attribute access on Self works via NamedTupleFallback.__getattr__. + reveal_type(self.host) # revealed: Any + reveal_type(self.port) # revealed: Any + reveal_type(self.unknown) # revealed: Any + return self._replace(port=port) +``` + +## `collections.namedtuple` with defaults + +The `defaults` parameter provides default values for the rightmost fields: + +```py +from collections import namedtuple + +# Two fields, one default (applies to 'y'). +Point = namedtuple("Point", ["x", "y"], defaults=[0]) + +# Can be called with both arguments. +p1 = Point(1, 2) +reveal_type(p1) # revealed: Point + +# Can be called with just the required argument. +p2 = Point(1) +reveal_type(p2) # revealed: Point + +# error: [missing-argument] "No argument provided for required parameter `x`" +Point() + +# All fields have defaults. +Point3D = namedtuple("Point3D", ["x", "y", "z"], defaults=[0, 0, 0]) +p3 = Point3D() +reveal_type(p3) # revealed: Point3D + +# Namedtuples with defaults are still compatible with tuple types. +def takes_tuple(t: tuple[int, int]) -> None: + pass + +takes_tuple(Point(1, 2)) +``` + +## `collections.namedtuple` with rename + +The `rename` parameter replaces invalid field names with positional names (`_0`, `_1`, etc.): + +```py +from collections import namedtuple + +# Fields with Python keywords are renamed when rename=True. +NT1 = namedtuple("NT1", ["abc", "def"], rename=True) +nt1 = NT1(abc="x", _1="y") +reveal_type(nt1) # revealed: NT1 + +# Fields starting with underscore are renamed when rename=True. +NT2 = namedtuple("NT2", ["abc", "_d"], rename=True) +nt2 = NT2(abc="x", _1="y") +reveal_type(nt2) # revealed: NT2 + +# Duplicate field names are renamed when rename=True. +NT3 = namedtuple("NT3", ["a", "a", "a"], rename=True) +nt3 = NT3(a="x", _1="y", _2="z") +reveal_type(nt3) # revealed: NT3 + +# Without rename=True, the original field names are used. +NT4 = namedtuple("NT4", ["abc", "xyz"]) +nt4 = NT4(abc="x", xyz="y") +reveal_type(nt4) # revealed: NT4 +``` + +## `collections.namedtuple` attributes + +Functional namedtuples have synthesized attributes similar to class-based namedtuples: + +```py +from collections import namedtuple + +Person = namedtuple("Person", ["name", "age"]) + +reveal_type(Person._fields) # revealed: tuple[Literal["name"], Literal["age"]] +reveal_type(Person._field_defaults) # revealed: dict[str, Any] +reveal_type(Person._make) # revealed: bound method ._make(iterable: Iterable[Any]) -> Person +reveal_type(Person._asdict) # revealed: def _asdict(self) -> dict[str, Any] +reveal_type(Person._replace) # revealed: (self: Self, *, name: Any = ..., age: Any = ...) -> Self + +# _make creates instances from an iterable. +reveal_type(Person._make(["Alice", 30])) # revealed: Person + +# _asdict converts to a dictionary. +person = Person("Alice", 30) +reveal_type(person._asdict()) # revealed: dict[str, Any] + +# _replace creates a copy with replaced fields. +reveal_type(person._replace(name="Bob")) # revealed: Person +``` + +## `collections.namedtuple` tuple compatibility + +Functional namedtuples inherit from tuple with `Any` element types since `collections.namedtuple` +doesn't provide type information: + +```py +from collections import namedtuple +from ty_extensions import static_assert, is_subtype_of + +Person = namedtuple("Person", ["name", "age"]) + +# Functional namedtuples inherit from tuple[Any, Any, ...]. +static_assert(is_subtype_of(Person, tuple[object, object])) + +def takes_tuple(t: tuple[str, int]) -> None: + pass + +# Instances are assignable to tuple types since fields are Any. +p = Person("Alice", 30) +takes_tuple(p) ``` ## The symbol `NamedTuple` itself diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index f61c24cca91ac5..3f493a85cbe895 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1576,7 +1576,10 @@ mod implicit_globals { else { return Place::Undefined.into(); }; - let module_type_scope = module_type_class.body_scope(db); + let Some(stmt_class) = module_type_class.as_stmt() else { + return Place::Undefined.into(); + }; + let module_type_scope = stmt_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(); @@ -1704,7 +1707,9 @@ mod implicit_globals { return smallvec::SmallVec::default(); }; - let module_type_scope = module_type.body_scope(db); + let Some(module_type_scope) = module_type.body_scope(db) else { + return smallvec::SmallVec::default(); + }; let module_type_symbol_table = place_table(db, module_type_scope); module_type_symbol_table diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 129af6d7e73404..606ba63bec04fc 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -24,6 +24,9 @@ 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::{ + FunctionalClassLiteral, FunctionalDataclassLiteral, FunctionalNamedTupleLiteral, +}; pub use self::cyclic::CycleDetector; pub(crate) use self::cyclic::{PairVisitor, TypeTransformer}; pub(crate) use self::diagnostic::register_lints; @@ -78,7 +81,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, StmtClassLiteral}; use instance::Protocol; pub use instance::{NominalInstanceType, ProtocolInstanceType}; pub use special_form::SpecialFormType; @@ -814,7 +817,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>), @@ -1009,7 +1012,8 @@ 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))) + .and_then(|instance| instance.class_literal(db)) + .and_then(|class| crate::types::enums::enum_metadata(db, class)) .is_some() } @@ -1129,25 +1133,21 @@ impl<'db> Type<'db> { pub(crate) fn specialization_of( self, db: &'db dyn Db, - expected_class: ClassLiteral<'_>, + expected_class: StmtClassLiteral<'_>, ) -> 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()?, @@ -1156,12 +1156,12 @@ impl<'db> Type<'db> { } .class(db); - let (class_literal, specialization) = class_type.class_literal(db); + let (class_literal, specialization) = class_type.stmt_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 @@ -1303,9 +1303,11 @@ impl<'db> Type<'db> { } #[track_caller] - pub(crate) const fn expect_class_literal(self) -> ClassLiteral<'db> { + pub(crate) fn expect_class_literal(self) -> StmtClassLiteral<'db> { self.as_class_literal() .expect("Expected a Type::ClassLiteral variant") + .as_stmt() + .expect("Expected a statement-based class literal") } pub const fn is_subclass_of(&self) -> bool { @@ -1852,7 +1854,8 @@ impl<'db> Type<'db> { | Type::TypeGuard(_) | Type::TypedDict(_) | Type::TypeAlias(_) - | Type::NewTypeInstance(_) => false, + | Type::NewTypeInstance(_) + => false, } } @@ -1898,16 +1901,17 @@ impl<'db> Type<'db> { } // TODO: This is unsound so in future we can consider an opt-in option to disable it. - Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - SubclassOfInner::Class(class) => Some(class.into_callable(db)), - - SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => { + Type::SubclassOf(subclass_of_ty) => { + let inner = subclass_of_ty.subclass_of(); + if let SubclassOfInner::Class(class) = inner { + Some(class.into_callable(db)) + } else { Some(CallableTypes::one(CallableType::single( db, Signature::new(Parameters::unknown(), Some(Type::from(subclass_of_ty))), ))) } - }, + } Type::Union(union) => { let mut callables = SmallVec::new(); @@ -2199,6 +2203,98 @@ impl<'db> Type<'db> { }) } + ( + Type::KnownInstance(KnownInstanceType::TypingNamedTupleFieldsSchema(_)), + Type::SpecialForm(SpecialFormType::TypingNamedTupleFieldsSchema), + ) if relation.is_assignability() => ConstraintSet::from(true), + + // Iterable[tuple[str, type]] is assignable to TypingNamedTupleFieldsSchema. + // This allows passing fields via variables: + // + // fields = [("x", int), ("y", int)] + // NamedTuple("Point", fields) + // + // Without this, only literal lists would type-check. + (_, Type::SpecialForm(SpecialFormType::TypingNamedTupleFieldsSchema)) + if relation.is_assignability() => + { + // Expected: Iterable[tuple[str, type[Any]]] + let field_tuple = Type::heterogeneous_tuple( + db, + [ + KnownClass::Str.to_instance(db), + KnownClass::Type.to_specialized_instance(db, [Type::any()]), + ], + ); + let expected = KnownClass::Iterable.to_specialized_instance(db, [field_tuple]); + ConstraintSet::from(self.is_assignable_to(db, expected)) + } + + ( + Type::KnownInstance(KnownInstanceType::CollectionsNamedTupleFieldsSchema(_)), + Type::SpecialForm(SpecialFormType::CollectionsNamedTupleFieldsSchema), + ) if relation.is_assignability() => ConstraintSet::from(true), + + // str | Sequence[str] is assignable to CollectionsNamedTupleFieldsSchema. + // This allows passing field names via variables: + // + // field_names = ["x", "y"] + // namedtuple("Point", field_names) + // + // Without this, only literal lists/strings would type-check. + (_, Type::SpecialForm(SpecialFormType::CollectionsNamedTupleFieldsSchema)) + if relation.is_assignability() => + { + let expected = UnionType::from_elements( + db, + [ + KnownClass::Str.to_instance(db), + KnownClass::Sequence + .to_specialized_instance(db, [KnownClass::Str.to_instance(db)]), + ], + ); + ConstraintSet::from(self.is_assignable_to(db, expected)) + } + + ( + Type::KnownInstance(KnownInstanceType::CollectionsNamedTupleDefaultsSchema(_)), + Type::SpecialForm(SpecialFormType::CollectionsNamedTupleDefaultsSchema), + ) if relation.is_assignability() => ConstraintSet::from(true), + + ( + Type::KnownInstance(KnownInstanceType::MakeDataclassFieldsSchema(_)), + Type::SpecialForm(SpecialFormType::MakeDataclassFieldsSchema), + ) if relation.is_assignability() => ConstraintSet::from(true), + + // Iterable[str | tuple[str, Any] | tuple[str, Any, Any]] is assignable to + // MakeDataclassFieldsSchema. This allows passing fields via variables: + // + // fields = [("x", int), ("y", str)] + // make_dataclass("Point", fields) + // + // Without this, only literal lists would type-check. + (_, Type::SpecialForm(SpecialFormType::MakeDataclassFieldsSchema)) + if relation.is_assignability() => + { + let expected_element = UnionType::from_elements( + db, + [ + KnownClass::Str.to_instance(db), + Type::heterogeneous_tuple( + db, + [KnownClass::Str.to_instance(db), Type::any()], + ), + Type::heterogeneous_tuple( + db, + [KnownClass::Str.to_instance(db), Type::any(), Type::any()], + ), + ], + ); + let expected_iterable = + KnownClass::Iterable.to_specialized_instance(db, [expected_element]); + ConstraintSet::from(self.is_assignable_to(db, expected_iterable)) + } + // Dynamic is only a subtype of `object` and only a supertype of `Never`; both were // handled above. It's always assignable, though. // @@ -3236,7 +3332,11 @@ impl<'db> Type<'db> { if literal.enum_class_instance(db) != Type::NominalInstance(instance) { return ConstraintSet::from(false); } - ConstraintSet::from(is_single_member_enum(db, instance.class_literal(db))) + ConstraintSet::from( + instance + .class_literal(db) + .is_some_and(|class| is_single_member_enum(db, class)), + ) } (Type::PropertyInstance(left), Type::PropertyInstance(right)) => { @@ -3771,7 +3871,7 @@ impl<'db> Type<'db> { SubclassOfInner::Class(class_a) => ConstraintSet::from( !class_a.could_exist_in_mro_of(db, ClassType::NonGeneric(class_b)), ), - SubclassOfInner::TypeVar(_) => unreachable!(), + SubclassOfInner::TypeVar(_) => ConstraintSet::from(false), } } @@ -3782,7 +3882,7 @@ impl<'db> Type<'db> { SubclassOfInner::Class(class_a) => ConstraintSet::from( !class_a.could_exist_in_mro_of(db, ClassType::Generic(alias_b)), ), - SubclassOfInner::TypeVar(_) => unreachable!(), + SubclassOfInner::TypeVar(_) => ConstraintSet::from(false), } } @@ -3790,8 +3890,9 @@ impl<'db> Type<'db> { left.is_disjoint_from_impl(db, right, inferable, disjointness_visitor) } - // for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`, - // so although the type is dynamic we can still determine disjointedness in some situations + // For `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any + // larger than `type`, so although the type is dynamic we can still determine + // disjointedness in some situations. (Type::SubclassOf(subclass_of_ty), other) | (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() { SubclassOfInner::Dynamic(_) => { @@ -4166,7 +4267,9 @@ 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).stmt_class_literal(db) + else { return; }; let generic_context = specialization.generic_context(db); @@ -4250,7 +4353,7 @@ impl<'db> Type<'db> { } } - // We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton. + // We eagerly transform `SubclassOf` to `StmtClassLiteral` for final types, so `SubclassOf` is never a singleton. Type::SubclassOf(..) => false, Type::BoundSuper(..) => false, Type::BooleanLiteral(_) @@ -5274,7 +5377,7 @@ impl<'db> Type<'db> { Type::EnumLiteral(enum_literal) if matches!(name_str, "name" | "_name_") - && Type::ClassLiteral(enum_literal.enum_class(db)) + && Type::ClassLiteral(ClassLiteral::Stmt(enum_literal.enum_class(db))) .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) => { Place::bound(Type::string_literal(db, enum_literal.name(db))).into() @@ -5282,7 +5385,7 @@ impl<'db> Type<'db> { Type::EnumLiteral(enum_literal) if matches!(name_str, "value" | "_value_") - && Type::ClassLiteral(enum_literal.enum_class(db)) + && Type::ClassLiteral(ClassLiteral::Stmt(enum_literal.enum_class(db))) .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) => { enum_metadata(db, enum_literal.enum_class(db)) @@ -5306,9 +5409,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) => + && instance.class(db).stmt_class_literal(db).is_some_and( + |(class_literal, _)| is_single_member_enum(db, class_literal), + ) => { - enum_metadata(db, instance.class(db).class_literal(db).0) + instance + .class(db) + .stmt_class_literal(db) + .and_then(|(class_literal, _)| enum_metadata(db, class_literal)) .and_then(|metadata| metadata.members.get_index(0).map(|(_, v)| *v)) .map_or(Place::Undefined, Place::bound) .into() @@ -5407,11 +5515,11 @@ impl<'db> Type<'db> { Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { if let Some(enum_class) = match self { - Type::ClassLiteral(literal) => Some(literal), + Type::ClassLiteral(literal) => literal.as_stmt(), Type::SubclassOf(subclass_of) => subclass_of .subclass_of() .into_class(db) - .map(|class| class.class_literal(db).0), + .and_then(|class| class.stmt_class_literal(db).map(|(lit, _)| lit)), _ => None, } { if let Some(metadata) = enum_metadata(db, enum_class) { @@ -6051,6 +6159,98 @@ impl<'db> Type<'db> { .into() } + // collections.namedtuple(typename, field_names, ...) + Some(KnownFunction::NamedTuple) => Binding::single( + self, + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("typename")) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_or_keyword(Name::new_static("field_names")) + .with_annotated_type(Type::SpecialForm( + SpecialFormType::CollectionsNamedTupleFieldsSchema, + )), + // Additional optional parameters have defaults. + Parameter::keyword_only(Name::new_static("rename")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("defaults")) + .with_annotated_type(Type::SpecialForm( + SpecialFormType::CollectionsNamedTupleDefaultsSchema, + )) + .with_default_type(Type::none(db)), + Parameter::keyword_only(Name::new_static("module")) + .with_default_type(Type::none(db)), + ], + ), + Some(KnownClass::NamedTupleFallback.to_class_literal(db)), + ), + ) + .into(), + + // dataclasses.make_dataclass(cls_name, fields, ...) + // The `fields` parameter accepts either: + // - A list or tuple literal (inferred as MakeDataclassFieldsSchema) + // - Any Iterable (fallback for variables containing fields) + Some(KnownFunction::MakeDataclass) => Binding::single( + self, + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("cls_name")) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_or_keyword(Name::new_static("fields")) + .with_annotated_type(Type::SpecialForm( + SpecialFormType::MakeDataclassFieldsSchema, + )), + // Additional optional parameters have defaults. + Parameter::keyword_only(Name::new_static("bases")) + .with_default_type(Type::empty_tuple(db)), + Parameter::keyword_only(Name::new_static("namespace")) + .with_default_type(Type::none(db)), + Parameter::keyword_only(Name::new_static("init")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("repr")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("eq")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("order")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("unsafe_hash")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("frozen")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("match_args")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("kw_only")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("slots")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("weakref_slot")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("module")) + .with_default_type(Type::none(db)), + ], + ), + // Return type is `type` when we can't synthesize a specific dataclass. + Some(KnownClass::Type.to_instance(db)), + ), + ) + .into(), + _ => CallableBinding::from_overloads( self, function_type.signature(db).overloads.iter().cloned(), @@ -6444,6 +6644,31 @@ impl<'db> Type<'db> { .into() } + // Functional namedtuple: create a signature with the field types as parameters. + None if class.as_functional_namedtuple().is_some() => { + let namedtuple = class.as_functional_namedtuple().unwrap(); + let parameters: Vec<_> = namedtuple + .fields(db) + .iter() + .map(|(name, ty, default_ty)| { + let mut param = Parameter::positional_or_keyword(name.clone()) + .with_annotated_type(*ty); + if let Some(default) = default_ty { + param = param.with_default_type(*default); + } + param + }) + .collect(); + Binding::single( + self, + Signature::new( + Parameters::new(db, parameters), + Some(namedtuple.to_instance(db)), + ), + ) + .into() + } + // Most class literal constructor calls are handled by `try_call_constructor` and // not via getting the signature here. This signature can still be used in some // cases (e.g. evaluating callable subtyping). TODO improve this definition @@ -6487,7 +6712,25 @@ impl<'db> Type<'db> { } Type::SpecialForm(SpecialFormType::NamedTuple) => { - Binding::single(self, Signature::todo("functional `NamedTuple` syntax")).into() + // typing.NamedTuple(typename: str, fields: _TypingNamedTupleFieldsSchema) + Binding::single( + self, + Signature::new( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("typename")) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_or_keyword(Name::new_static("fields")) + .with_annotated_type(Type::SpecialForm( + SpecialFormType::TypingNamedTupleFieldsSchema, + )), + ], + ), + Some(KnownClass::NamedTupleFallback.to_class_literal(db)), + ), + ) + .into() } Type::GenericAlias(_) => { @@ -6500,23 +6743,29 @@ impl<'db> Type<'db> { .into() } - Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { - SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).bindings(db), + Type::SubclassOf(subclass_of_type) => { + let inner = subclass_of_type.subclass_of(); + if let SubclassOfInner::Dynamic(dynamic_type) = inner { + return Type::Dynamic(dynamic_type).bindings(db); + } // Most type[] constructor calls are handled by `try_call_constructor` and not via // getting the signature here. This signature can still be used in some cases (e.g. - // evaluating callable subtyping). TODO improve this definition (intersection of - // `__new__` and `__init__` signatures? and respect metaclass `__call__`). - SubclassOfInner::Class(class) => Type::from(class).bindings(db), - - // TODO annotated return type on `__new__` or metaclass `__call__` - // TODO check call vs signatures of `__new__` and/or `__init__` - SubclassOfInner::TypeVar(_) => Binding::single( - self, - Signature::new(Parameters::gradual_form(), self.to_instance(db)), - ) - .into(), - }, + // evaluating callable subtyping). TODO: improve this definition (intersection of + // `__new__` and `__init__` signatures, and respect metaclass `__call__`). + match inner { + SubclassOfInner::Class(class) => Type::from(class).bindings(db), + // TODO: annotated return type on `__new__` or metaclass `__call__`. + // TODO: check call vs signatures of `__new__` and/or `__init__`. + SubclassOfInner::TypeVar(_) => Binding::single( + self, + Signature::new(Parameters::gradual_form(), self.to_instance(db)), + ) + .into(), + // Dynamic handled by early return above. + SubclassOfInner::Dynamic(_) => unreachable!(), + } + } Type::NominalInstance(_) | Type::ProtocolInstance(_) | Type::NewTypeInstance(_) => { // Note that for objects that have a (possibly not callable!) `__call__` attribute, @@ -6850,7 +7099,8 @@ impl<'db> Type<'db> { | Type::BoundSuper(_) | Type::TypeIs(_) | Type::TypeGuard(_) - | Type::TypedDict(_) => None + | Type::TypedDict(_) + => None } } @@ -7135,7 +7385,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.stmt_class_literal_specialized(db, None) + { if let [_, _, return_ty] = specialization.types(db) { return Some(*return_ty); } @@ -7498,8 +7750,14 @@ impl<'db> Type<'db> { } Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))), - Type::SubclassOf(_) - | Type::BooleanLiteral(_) + Type::SubclassOf(_) => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::InvalidType(*self, scope_id) + ], + fallback_type: Type::unknown(), + }), + + Type::BooleanLiteral(_) | Type::BytesLiteral(_) | Type::EnumLiteral(_) | Type::AlwaysTruthy @@ -7597,6 +7855,11 @@ impl<'db> Type<'db> { } KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)), KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), + // Internal types, should never appear in user code. + KnownInstanceType::TypingNamedTupleFieldsSchema(_) + | KnownInstanceType::CollectionsNamedTupleFieldsSchema(_) + | KnownInstanceType::CollectionsNamedTupleDefaultsSchema(_) + | KnownInstanceType::MakeDataclassFieldsSchema(_) => Ok(Type::unknown()), }, Type::SpecialForm(special_form) => match special_form { @@ -7730,6 +7993,12 @@ impl<'db> Type<'db> { ], fallback_type: Type::unknown(), }), + + // Internal types, should never appear in user code. + SpecialFormType::TypingNamedTupleFieldsSchema + | SpecialFormType::CollectionsNamedTupleFieldsSchema + | SpecialFormType::CollectionsNamedTupleDefaultsSchema + | SpecialFormType::MakeDataclassFieldsSchema => Ok(Type::unknown()), }, Type::Union(union) => { @@ -7816,7 +8085,9 @@ impl<'db> Type<'db> { } Type::BytesLiteral(_) => KnownClass::Bytes.to_class_literal(db), Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), - Type::EnumLiteral(enum_literal) => Type::ClassLiteral(enum_literal.enum_class(db)), + Type::EnumLiteral(enum_literal) => { + Type::ClassLiteral(ClassLiteral::Stmt(enum_literal.enum_class(db))) + } Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db), Type::KnownBoundMethod(method) => method.class().to_class_literal(db), @@ -8004,7 +8275,11 @@ impl<'db> Type<'db> { KnownInstanceType::Specialization(_) | KnownInstanceType::Literal(_) | KnownInstanceType::LiteralStringAlias(_) | - KnownInstanceType::NewType(_) => { + KnownInstanceType::NewType(_) | + KnownInstanceType::TypingNamedTupleFieldsSchema(_) | + KnownInstanceType::CollectionsNamedTupleFieldsSchema(_) | + KnownInstanceType::CollectionsNamedTupleDefaultsSchema(_) | + KnownInstanceType::MakeDataclassFieldsSchema(_) => { // TODO: For some of these, we may need to apply the type mapping to inner types. self }, @@ -8400,7 +8675,11 @@ impl<'db> Type<'db> { | KnownInstanceType::Specialization(_) | KnownInstanceType::Literal(_) | KnownInstanceType::LiteralStringAlias(_) - | KnownInstanceType::NewType(_) => { + | KnownInstanceType::NewType(_) + | KnownInstanceType::TypingNamedTupleFieldsSchema(_) + | KnownInstanceType::CollectionsNamedTupleFieldsSchema(_) + | KnownInstanceType::CollectionsNamedTupleDefaultsSchema(_) + | KnownInstanceType::MakeDataclassFieldsSchema(_) => { // TODO: For some of these, we may need to try to find legacy typevars in inner types. } }, @@ -8551,12 +8830,13 @@ impl<'db> Type<'db> { } Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))), Self::ClassLiteral(class_literal) => { - Some(TypeDefinition::Class(class_literal.definition(db))) + class_literal.definition(db).map(TypeDefinition::Class) } Self::GenericAlias(alias) => Some(TypeDefinition::Class(alias.definition(db))), - Self::NominalInstance(instance) => { - Some(TypeDefinition::Class(instance.class(db).definition(db))) - } + Self::NominalInstance(instance) => instance + .class(db) + .definition(db) + .map(TypeDefinition::Class), Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { Some(TypeDefinition::TypeVar(var.definition(db)?)) @@ -8569,9 +8849,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) => class.definition(db).map(TypeDefinition::Class), + SubclassOfInner::TypeVar(bound_typevar) => { + Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)) + } }, Self::TypeAlias(alias) => alias.value_type(db).definition(db), @@ -8598,7 +8880,7 @@ 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) => class.definition(db).map(TypeDefinition::Class), Protocol::Synthesized(_) => None, }, @@ -8685,7 +8967,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) => { @@ -9067,6 +9349,20 @@ pub enum KnownInstanceType<'db> { /// An identity callable created with `typing.NewType(name, base)`, which behaves like a /// subtype of `base` in type expressions. See the `struct NewType` payload for an example. NewType(NewType<'db>), + + /// Schema for `typing.NamedTuple` fields, extracted from a list or tuple literal. + TypingNamedTupleFieldsSchema(TypingNamedTupleFieldsSchema<'db>), + + /// Schema for `collections.namedtuple` field names, extracted from a list or tuple literal. + CollectionsNamedTupleFieldsSchema(CollectionsNamedTupleFieldsSchema<'db>), + + /// Schema for `collections.namedtuple` defaults count, extracted from a list or tuple literal. + CollectionsNamedTupleDefaultsSchema(CollectionsNamedTupleDefaultsSchema<'db>), + + /// Schema for `dataclasses.make_dataclass` fields. + /// When a list or tuple literal is passed as the `fields` argument to `make_dataclass()`, + /// the literal is inferred as this schema type. + MakeDataclassFieldsSchema(MakeDataclassFieldsSchema<'db>), } fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -9113,6 +9409,23 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( KnownInstanceType::NewType(newtype) => { visitor.visit_type(db, newtype.concrete_base_type(db)); } + KnownInstanceType::TypingNamedTupleFieldsSchema(schema) => { + for (_, field_ty) in schema.fields(db) { + visitor.visit_type(db, *field_ty); + } + } + // CollectionsNamedTupleFieldsSchema only contains field names, no types to visit. + KnownInstanceType::CollectionsNamedTupleFieldsSchema(_) => {} + // CollectionsNamedTupleDefaultsSchema only contains a count, no types to visit. + KnownInstanceType::CollectionsNamedTupleDefaultsSchema(_) => {} + KnownInstanceType::MakeDataclassFieldsSchema(schema) => { + for (_, field_ty, default_ty) in schema.fields(db) { + visitor.visit_type(db, *field_ty); + if let Some(default) = default_ty { + visitor.visit_type(db, *default); + } + } + } } } @@ -9160,6 +9473,18 @@ impl<'db> KnownInstanceType<'db> { // Nothing to normalize self } + Self::TypingNamedTupleFieldsSchema(schema) => { + Self::TypingNamedTupleFieldsSchema(schema.normalized_impl(db, visitor)) + } + Self::CollectionsNamedTupleFieldsSchema(schema) => { + Self::CollectionsNamedTupleFieldsSchema(schema.normalized_impl(db, visitor)) + } + Self::CollectionsNamedTupleDefaultsSchema(schema) => { + Self::CollectionsNamedTupleDefaultsSchema(schema.normalized_impl(db, visitor)) + } + Self::MakeDataclassFieldsSchema(schema) => { + Self::MakeDataclassFieldsSchema(schema.normalized_impl(db, visitor)) + } } } @@ -9209,6 +9534,52 @@ impl<'db> KnownInstanceType<'db> { Self::Specialization(specialization) => specialization .recursive_type_normalized_impl(db, div, true) .map(Self::Specialization), + Self::TypingNamedTupleFieldsSchema(schema) => { + // Normalize the field types. + let normalized_fields: Option> = schema + .fields(db) + .iter() + .map(|(name, ty)| { + ty.recursive_type_normalized_impl(db, div, true) + .map(|normalized_ty| (name.clone(), normalized_ty)) + }) + .collect(); + normalized_fields.map(|fields| { + Self::TypingNamedTupleFieldsSchema(TypingNamedTupleFieldsSchema::new( + db, fields, + )) + }) + } + // CollectionsNamedTupleFieldsSchema only contains field names, no types to normalize. + Self::CollectionsNamedTupleFieldsSchema(schema) => { + Some(Self::CollectionsNamedTupleFieldsSchema(schema)) + } + // CollectionsNamedTupleDefaultsSchema only contains a count, no types to normalize. + Self::CollectionsNamedTupleDefaultsSchema(schema) => { + Some(Self::CollectionsNamedTupleDefaultsSchema(schema)) + } + Self::MakeDataclassFieldsSchema(schema) => { + // Normalize the field types. + let normalized_fields: Option> = schema + .fields(db) + .iter() + .map(|(name, ty, default)| { + ty.recursive_type_normalized_impl(db, div, true) + .and_then(|normalized_ty| { + let normalized_default = match default { + Some(d) => { + Some(d.recursive_type_normalized_impl(db, div, true)?) + } + None => None, + }; + Some((name.clone(), normalized_ty, normalized_default)) + }) + }) + .collect(); + normalized_fields.map(|fields| { + Self::MakeDataclassFieldsSchema(MakeDataclassFieldsSchema::new(db, fields)) + }) + } } } @@ -9235,6 +9606,11 @@ impl<'db> KnownInstanceType<'db> { | Self::Callable(_) => KnownClass::GenericAlias, Self::LiteralStringAlias(_) => KnownClass::Str, Self::NewType(_) => KnownClass::NewType, + // Internal types, no corresponding known class. + Self::TypingNamedTupleFieldsSchema(_) + | Self::CollectionsNamedTupleFieldsSchema(_) + | Self::CollectionsNamedTupleDefaultsSchema(_) + | Self::MakeDataclassFieldsSchema(_) => KnownClass::Object, } } @@ -9738,6 +10114,97 @@ impl<'db> FieldInstance<'db> { } } +/// Schema for `typing.NamedTuple` fields, extracted from a list or tuple literal. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct TypingNamedTupleFieldsSchema<'db> { + #[returns(ref)] + pub fields: Box<[(Name, Type<'db>)]>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for TypingNamedTupleFieldsSchema<'_> {} + +impl<'db> TypingNamedTupleFieldsSchema<'db> { + pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { + TypingNamedTupleFieldsSchema::new( + db, + self.fields(db) + .iter() + .map(|(name, ty)| (name.clone(), ty.normalized_impl(db, visitor))) + .collect::>(), + ) + } +} + +/// Schema for `collections.namedtuple` field names, extracted from a list or tuple literal. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct CollectionsNamedTupleFieldsSchema<'db> { + #[returns(ref)] + pub field_names: Box<[Name]>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for CollectionsNamedTupleFieldsSchema<'_> {} + +impl CollectionsNamedTupleFieldsSchema<'_> { + pub(crate) fn normalized_impl(self, _db: &dyn Db, _visitor: &NormalizedVisitor<'_>) -> Self { + // Field names don't contain types, so no normalization needed. + self + } +} + +/// Schema for `collections.namedtuple` defaults count, extracted from a list or tuple literal. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct CollectionsNamedTupleDefaultsSchema<'db> { + pub count: usize, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for CollectionsNamedTupleDefaultsSchema<'_> {} + +impl CollectionsNamedTupleDefaultsSchema<'_> { + pub(crate) fn normalized_impl(self, _db: &dyn Db, _visitor: &NormalizedVisitor<'_>) -> Self { + self + } +} + +/// Internal schema for the `fields` argument to `dataclasses.make_dataclass()`. +/// +/// When a list or tuple literal is passed as the `fields` argument to `make_dataclass()`, +/// the literal is inferred as this schema type instead of a regular list or tuple type. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct MakeDataclassFieldsSchema<'db> { + /// The fields as (name, type, default) tuples extracted from the literal. + /// The default is `None` if no default was provided. + #[returns(ref)] + pub fields: Box<[(Name, Type<'db>, Option>)]>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for MakeDataclassFieldsSchema<'_> {} + +impl<'db> MakeDataclassFieldsSchema<'db> { + pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { + MakeDataclassFieldsSchema::new( + db, + self.fields(db) + .iter() + .map(|(name, ty, default)| { + ( + name.clone(), + ty.normalized_impl(db, visitor), + default.map(|d| d.normalized_impl(db, visitor)), + ) + }) + .collect::>(), + ) + } +} + /// Whether this typevar was created via the legacy `TypeVar` constructor, using PEP 695 syntax, /// or an implicit typevar like `Self` was used. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)] @@ -11046,7 +11513,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) + .stmt_class_literal(db) + .map(|(lit, _)| Type::ClassLiteral(lit.into())) + }) .unwrap_or_else(Type::unknown) }; @@ -13961,7 +14433,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: StmtClassLiteral<'db>, } #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -14719,7 +15191,7 @@ impl<'db> BytesLiteralType<'db> { #[derive(PartialOrd, Ord)] pub struct EnumLiteralType<'db> { /// A reference to the enum class this literal belongs to - enum_class: ClassLiteral<'db>, + enum_class: StmtClassLiteral<'db>, /// The name of the enum member #[returns(ref)] name: Name, @@ -14947,7 +15419,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: StmtClassLiteral<'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 13fafc47b436ae..0eba11c2723043 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -465,7 +465,8 @@ impl<'db> BoundSuperType<'db> { Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)), Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic), - _ => match subclass_of.subclass_of().into_class(db) { + SubclassOfInner::Class(class) => ClassBase::Class(class), + SubclassOfInner::TypeVar(_) => match subclass_of.subclass_of().into_class(db) { Some(class) => ClassBase::Class(class), None => { return Err(BoundSuperError::InvalidPivotClassType { @@ -485,21 +486,36 @@ 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; - 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, - }) { - return Err(BoundSuperError::FailingConditionCheck { - pivot_class: pivot_class_type, - owner: owner_type, - typevar_context: None, - }); + // Check that the owner's MRO contains the pivot class. + let pivot_in_owner_mro = match pivot_class { + ClassBase::Class(pivot_class_type) => { + let pivot_literal = pivot_class_type.class_literal(db); + match owner.into_class(db) { + Some(owner_class) => { + 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) == pivot_literal + } + }) + } + // Owner doesn't have a class - skip the check. + None => true, + } } + ClassBase::Dynamic(_) => true, + ClassBase::Generic | ClassBase::Protocol | ClassBase::TypedDict => true, + }; + + if !pivot_in_owner_mro { + return Err(BoundSuperError::FailingConditionCheck { + pivot_class: pivot_class_type, + owner: owner_type, + typevar_context: None, + }); } Ok(Type::BoundSuper(BoundSuperType::new( @@ -518,16 +534,22 @@ impl<'db> BoundSuperType<'db> { db: &'db dyn Db, mro_iter: impl Iterator>, ) -> impl Iterator> { - let Some(pivot_class) = self.pivot_class(db).into_class() else { + let pivot = self.pivot_class(db); + + let Some(pivot_class) = pivot.into_class() else { return Either::Left(ClassBase::Dynamic(DynamicType::Unknown).mro(db, None)); }; + let pivot_literal = pivot_class.class_literal(db); let mut pivot_found = false; Either::Right(mro_iter.skip_while(move |superclass| { if pivot_found { false - } else if Some(pivot_class) == superclass.into_class() { + } else if superclass + .into_class() + .is_some_and(|c| c.class_literal(db) == pivot_literal) + { pivot_found = true; true } else { @@ -594,7 +616,8 @@ impl<'db> BoundSuperType<'db> { SuperOwnerKind::Instance(instance) => instance.class(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/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 36a258f20d0126..c35eb9680300ec 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -751,7 +751,9 @@ impl<'db> IntersectionBuilder<'db> { self } Type::NominalInstance(instance) - if enum_metadata(self.db, instance.class_literal(self.db)).is_some() => + if instance + .class_literal(self.db) + .is_some_and(|class| enum_metadata(self.db, class).is_some()) => { let mut contains_enum_literal_as_negative_element = false; for intersection in &self.intersections { @@ -773,10 +775,13 @@ impl<'db> IntersectionBuilder<'db> { // `UnionBuilder` because we would simplify the union to just the enum instance // and end up in this branch again. let db = self.db; + let class_literal = instance + .class_literal(db) + .expect("Already checked that class_literal is Some"); self.add_positive_impl( Type::Union(UnionType::new( db, - enum_member_literals(db, instance.class_literal(db), None) + enum_member_literals(db, class_literal, None) .expect("Calling `enum_member_literals` on an enum class") .collect::>(), RecursivelyDefined::No, diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 04621c995d1455..1d4a6724402402 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -352,7 +352,9 @@ 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() + || class + .stmt_class_literal(db) + .is_some_and(|(lit, _)| enum_metadata(db, lit).is_some()) } Type::Union(_) => true, _ => false, @@ -403,8 +405,10 @@ 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) { - return Some(enum_members.collect()); + if let Some((class_literal, _)) = class.stmt_class_literal(db) { + if let Some(enum_members) = enum_member_literals(db, class_literal, None) { + return Some(enum_members.collect()); + } } None diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 24c98e94dc09e9..9b29aff9936897 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -17,6 +17,8 @@ use std::fmt; use itertools::Itertools; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; +use ruff_python_stdlib::identifiers::is_identifier; +use ruff_python_stdlib::keyword::is_keyword; use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::{SmallVec, smallvec, smallvec_inline}; @@ -25,6 +27,7 @@ use crate::db::Db; use crate::dunder_all::dunder_all_names; use crate::place::{Definedness, Place, known_module_symbol}; use crate::types::call::arguments::{Expansion, is_expandable_type}; +use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSet; use crate::types::diagnostic::{ CALL_NON_CALLABLE, CALL_TOP_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, @@ -40,14 +43,16 @@ use crate::types::generics::{ InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError, }; use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters}; +use crate::types::subclass_of::SubclassOfInner; use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::{ BoundMethodType, BoundTypeVarIdentity, BoundTypeVarInstance, CallableSignature, CallableType, - CallableTypeKind, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, - FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, - NominalInstanceType, PropertyInstanceType, SpecialFormType, TrackedConstraintSet, - TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, WrapperDescriptorKind, - enums, list_members, todo_type, + CallableTypeKind, ClassLiteral, ClassType, DATACLASS_FLAGS, DataclassFlags, DataclassParams, + FieldInstance, FunctionalClassLiteral, FunctionalDataclassLiteral, FunctionalNamedTupleLiteral, + KnownBoundMethodType, KnownClass, KnownInstanceType, MemberLookupPolicy, NominalInstanceType, + PropertyInstanceType, SpecialFormType, StmtClassLiteral, TrackedConstraintSet, TypeAliasType, + TypeContext, TypeVarVariance, UnionBuilder, UnionType, WrapperDescriptorKind, enums, + list_members, todo_type, }; use crate::unpack::EvaluationMode; use crate::{DisplaySettings, Program}; @@ -337,6 +342,7 @@ impl<'db> Bindings<'db> { /// Evaluates the return type of certain known callables, where we have special-case logic to /// determine the return type in a way that isn't directly expressible in the type system. + #[allow(clippy::type_complexity)] fn evaluate_known_cases( &mut self, db: &'db dyn Db, @@ -904,7 +910,10 @@ impl<'db> Bindings<'db> { if let [Some(ty)] = overload.parameter_types() { let return_ty = match ty { Type::ClassLiteral(class) => { - if let Some(metadata) = enums::enum_metadata(db, *class) { + if let Some(metadata) = class + .as_stmt() + .and_then(|stmt| enums::enum_metadata(db, stmt)) + { Type::heterogeneous_tuple( db, metadata @@ -1109,11 +1118,11 @@ impl<'db> Bindings<'db> { } // `dataclass` being used as a non-decorator - if let [Some(Type::ClassLiteral(class_literal))] = + if let [Some(Type::ClassLiteral(ClassLiteral::Stmt(class_literal)))] = overload.parameter_types() { let params = DataclassParams::default_params(db); - overload.set_return_type(Type::from(ClassLiteral::new( + overload.set_return_type(Type::from(StmtClassLiteral::new( db, class_literal.name(db), class_literal.body_scope(db), @@ -1169,9 +1178,271 @@ impl<'db> Bindings<'db> { } } + // collections.namedtuple Some(KnownFunction::NamedTuple) => { - overload - .set_return_type(todo_type!("Support for functional `namedtuple`")); + if let [Some(name_type), Some(field_names_type), ..] = + overload.parameter_types() + { + let name = name_type + .as_string_literal() + .map(|s| Name::new(s.value(db))); + + // Check if field_names_type is a CollectionsNamedTupleFieldsSchema. + let field_names: Option> = if let Type::KnownInstance( + KnownInstanceType::CollectionsNamedTupleFieldsSchema(schema), + ) = field_names_type + { + Some(schema.field_names(db).clone()) + } else if let Some(string_literal) = + field_names_type.as_string_literal() + { + // Handle space/comma-separated string. + // Python's namedtuple replaces commas with spaces first, + // then splits on whitespace. + let field_str = string_literal.value(db); + Some( + field_str + .replace(',', " ") + .split_whitespace() + .map(Name::new) + .collect(), + ) + } else { + None + }; + + // Check if `rename=True`. + let rename = matches!( + overload.parameter_type_by_name("rename", false), + Ok(Some(Type::BooleanLiteral(true))) + ); + + // Extract defaults count from the defaults parameter. + // If it's a CollectionsNamedTupleDefaultsSchema, we can get the count directly. + let num_defaults: usize = overload + .parameter_type_by_name("defaults", false) + .ok() + .flatten() + .and_then(|ty| { + if let Type::KnownInstance( + KnownInstanceType::CollectionsNamedTupleDefaultsSchema( + schema, + ), + ) = ty + { + Some(schema.count(db)) + } else { + None + } + }) + .unwrap_or(0); + + if let (Some(name), Some(mut field_names)) = (name, field_names) { + // Apply rename logic if `rename=True`. + if rename { + let mut seen_names = FxHashSet::<&str>::default(); + for (i, field_name) in field_names.iter_mut().enumerate() { + let name_str = field_name.as_str(); + // Rename if: starts with underscore, is a keyword, + // is not a valid identifier, or is a duplicate. + let needs_rename = name_str.starts_with('_') + || is_keyword(name_str) + || !is_identifier(name_str) + || seen_names.contains(name_str); + if needs_rename { + *field_name = Name::new(format!("_{i}")); + } + seen_names.insert(field_name.as_str()); + } + } + + // Build fields: All fields have type `Any` for collections.namedtuple. + // Fields at the end get defaults from the `defaults` parameter. + let num_fields = field_names.len(); + let fields: Box<[(Name, Type<'db>, Option>)]> = + field_names + .iter() + .enumerate() + .map(|(i, field_name)| { + // Defaults apply to the rightmost fields. + // We use `Type::any()` for the default type since all fields are `Any`. + let default = if num_defaults > 0 + && i >= num_fields - num_defaults + { + Some(Type::any()) + } else { + None + }; + (field_name.clone(), Type::any(), default) + }) + .collect(); + + let namedtuple = + FunctionalNamedTupleLiteral::new(db, name, fields); + overload.set_return_type(Type::ClassLiteral( + ClassLiteral::FunctionalNamedTuple(namedtuple), + )); + } else { + overload.set_return_type( + KnownClass::NamedTupleFallback.to_class_literal(db), + ); + } + } else { + overload.set_return_type( + KnownClass::NamedTupleFallback.to_class_literal(db), + ); + } + } + + Some(KnownFunction::MakeDataclass) => { + // Handle dataclasses.make_dataclass(cls_name, fields, ...) + if let [Some(name_type), Some(fields_type), ..] = + overload.parameter_types() + { + let name = name_type + .as_string_literal() + .map(|s| Name::new(s.value(db))); + + // Extract fields from the schema type inferred from the literal. + let fields: Option, Option>)>> = + if let Type::KnownInstance( + KnownInstanceType::MakeDataclassFieldsSchema(schema), + ) = fields_type + { + Some( + schema + .fields(db) + .iter() + .map(|(name, ty, default)| { + (name.clone(), *ty, *default) + }) + .collect(), + ) + } else { + None + }; + + // Extract bases from keyword argument. + let bases: Box<[ClassBase<'db>]> = overload + .parameter_type_by_name("bases", false) + .ok() + .flatten() + .and_then(|bases_type| { + bases_type.exact_tuple_instance_spec(db).map(|tuple_spec| { + tuple_spec + .fixed_elements() + .filter_map(|ty| match ty { + Type::ClassLiteral(literal) => { + Some(ClassBase::Class( + literal.default_specialization(db), + )) + } + Type::GenericAlias(generic) => { + Some(ClassBase::Class(ClassType::Generic( + *generic, + ))) + } + _ => None, + }) + .collect() + }) + }) + .unwrap_or_default(); + + // Extract dataclass flags from keyword arguments. + let mut flags = DataclassFlags::empty(); + let init = overload + .parameter_type_by_name("init", false) + .ok() + .flatten(); + if to_bool(&init, true) { + flags |= DataclassFlags::INIT; + } + let repr = overload + .parameter_type_by_name("repr", false) + .ok() + .flatten(); + if to_bool(&repr, true) { + flags |= DataclassFlags::REPR; + } + let eq = + overload.parameter_type_by_name("eq", false).ok().flatten(); + if to_bool(&eq, true) { + flags |= DataclassFlags::EQ; + } + let order = overload + .parameter_type_by_name("order", false) + .ok() + .flatten(); + if to_bool(&order, false) { + flags |= DataclassFlags::ORDER; + } + let unsafe_hash = overload + .parameter_type_by_name("unsafe_hash", false) + .ok() + .flatten(); + if to_bool(&unsafe_hash, false) { + flags |= DataclassFlags::UNSAFE_HASH; + } + let frozen = overload + .parameter_type_by_name("frozen", false) + .ok() + .flatten(); + if to_bool(&frozen, false) { + flags |= DataclassFlags::FROZEN; + } + let match_args = overload + .parameter_type_by_name("match_args", false) + .ok() + .flatten(); + if to_bool(&match_args, true) { + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + flags |= DataclassFlags::MATCH_ARGS; + } + } + let kw_only = overload + .parameter_type_by_name("kw_only", false) + .ok() + .flatten(); + if to_bool(&kw_only, false) { + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + flags |= DataclassFlags::KW_ONLY; + } + } + let slots = overload + .parameter_type_by_name("slots", false) + .ok() + .flatten(); + if to_bool(&slots, false) { + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + flags |= DataclassFlags::SLOTS; + } + } + let weakref_slot = overload + .parameter_type_by_name("weakref_slot", false) + .ok() + .flatten(); + if to_bool(&weakref_slot, false) { + if Program::get(db).python_version(db) >= PythonVersion::PY311 { + flags |= DataclassFlags::WEAKREF_SLOT; + } + } + + let params = DataclassParams::from_flags(db, flags); + + if let (Some(name), Some(fields)) = (name, fields) { + let dataclass = FunctionalDataclassLiteral::new( + db, + name, + fields.into_boxed_slice(), + bases, + params, + ); + overload.set_return_type(Type::ClassLiteral( + ClassLiteral::FunctionalDataclass(dataclass), + )); + } + } } _ => { @@ -1373,6 +1644,54 @@ impl<'db> Bindings<'db> { } } + Some(KnownClass::Type) if overload_index == 1 => { + // Three-argument call: type(name, bases, dict) + if let [Some(name_type), Some(bases), Some(_namespace), ..] = + overload.parameter_types() + { + // Extract the name from the first argument (if it's a string literal). + let name = name_type + .as_string_literal() + .map(|s| ruff_python_ast::name::Name::new(s.value(db))); + + // Extract base classes from the bases tuple. + let base_classes: Option]>> = + bases.exact_tuple_instance_spec(db).and_then(|tuple_spec| { + tuple_spec + .fixed_elements() + .map(|base| match base { + Type::ClassLiteral(class) => { + Some(ClassBase::Class( + class.default_specialization(db), + )) + } + Type::GenericAlias(alias) => Some( + ClassBase::Class(ClassType::Generic(*alias)), + ), + Type::SubclassOf(subclass_of) => { + match subclass_of.subclass_of() { + SubclassOfInner::Class(class) => { + Some(ClassBase::Class(class)) + } + SubclassOfInner::Dynamic(_) + | SubclassOfInner::TypeVar(_) => None, + } + } + _ => None, + }) + .collect::>>() + }); + + if let (Some(name), Some(bases)) = (name, base_classes) { + let functional_class = + FunctionalClassLiteral::new(db, name, bases); + overload.set_return_type(Type::ClassLiteral( + ClassLiteral::Functional(functional_class), + )); + } + } + } + Some(KnownClass::Property) => { if let [getter, setter, ..] = overload.parameter_types() { overload.set_return_type(Type::PropertyInstance( @@ -1409,6 +1728,94 @@ impl<'db> Bindings<'db> { overload.set_return_type(todo_type!("Support for functional `TypedDict`")); } + Type::SpecialForm(SpecialFormType::NamedTuple) => { + if let [Some(name_type), Some(fields_type), ..] = overload.parameter_types() + { + let name = name_type + .as_string_literal() + .map(|s| Name::new(s.value(db))); + + // Check if fields_type is a TypingNamedTupleFieldsSchema (from literal inference). + let fields: Option, Option>)]>> = + if let Type::KnownInstance( + KnownInstanceType::TypingNamedTupleFieldsSchema(schema), + ) = fields_type + { + // Extract fields from the schema. + Some( + schema + .fields(db) + .iter() + .map(|(name, ty)| (name.clone(), *ty, None)) + .collect(), + ) + } else { + // Fall back to extracting from a tuple type for the variable case: + // fields = (("x", int), ("y", str)) + // NamedTuple("Foo", fields) + let extract_field = |field_tuple: &Type<'db>| -> Option<( + Name, + Type<'db>, + Option>, + )> { + let field_spec = + field_tuple.exact_tuple_instance_spec(db)?; + let elements: Vec<_> = + field_spec.fixed_elements().collect(); + if elements.len() != 2 { + return None; + } + let field_name = elements[0] + .as_string_literal() + .map(|s| Name::new(s.value(db)))?; + let field_ty = elements[1]; + let resolved_ty = match field_ty { + Type::ClassLiteral(class) => { + class.to_non_generic_instance(db) + } + Type::GenericAlias(alias) => { + Type::instance(db, ClassType::Generic(*alias)) + } + Type::SubclassOf(subclass_of) => { + match subclass_of.subclass_of() { + SubclassOfInner::Class(class) => { + Type::instance(db, class) + } + _ => *field_ty, + } + } + ty => *ty, + }; + Some((field_name, resolved_ty, None)) + }; + + fields_type.exact_tuple_instance_spec(db).and_then( + |tuple_spec| { + tuple_spec + .fixed_elements() + .map(extract_field) + .collect::>>() + }, + ) + }; + + if let (Some(name), Some(fields)) = (name, fields) { + let namedtuple = FunctionalNamedTupleLiteral::new(db, name, fields); + overload.set_return_type(Type::ClassLiteral( + ClassLiteral::FunctionalNamedTuple(namedtuple), + )); + } else { + overload.set_return_type( + KnownClass::NamedTupleFallback.to_class_literal(db), + ); + } + } else { + overload.set_return_type( + KnownClass::NamedTupleFallback.to_class_literal(db), + ); + } + } + // Not a special case _ => {} } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 869c82c90f99db..67a3d5dc2b378b 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -18,7 +18,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::{ + CONFLICTING_METACLASS, DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_TYPE_ALIAS_TYPE, + SUPER_CALL_IN_NAMED_TUPLE_METHOD, +}; use crate::types::enums::{ enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value, }; @@ -30,6 +33,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::FunctionalMroError; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::typed_dict::typed_dict_params_from_class_def; @@ -72,10 +76,246 @@ use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use ty_module_resolver::{KnownModule, file_to_module}; +/// 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 `StmtClassLiteral` and `FunctionalClassLiteral` +/// 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 { + 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(ty, origin, 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( + union.build(), + TypeOrigin::Inferred, + boundness, + 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 { + lookup_result: LookupResult<'db>, + dynamic_type: Option>, + }, + /// Encountered a `TypedDict` base - caller should handle this specially + TypedDict, +} + +impl<'db> ClassMemberResult<'db> { + /// Finalize the lookup result by handling dynamic type intersection. + pub(super) fn finalize(self, db: &'db dyn Db) -> PlaceAndQualifiers<'db> { + match self { + ClassMemberResult::TypedDict => { + // Caller should handle TypedDict case before calling finalize + unreachable!("finalize called on TypedDict result") + } + ClassMemberResult::Done { + lookup_result, + dynamic_type, + } => match (PlaceAndQualifiers::from(lookup_result), dynamic_type) { + (symbol_and_qualifiers, None) => symbol_and_qualifiers, + + ( + PlaceAndQualifiers { + place: Place::Defined(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. +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, +} + fn explicit_bases_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, - _self: ClassLiteral<'db>, + _self: StmtClassLiteral<'db>, ) -> Box<[Type<'db>]> { Box::default() } @@ -83,7 +323,7 @@ fn explicit_bases_cycle_initial<'db>( fn inheritance_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, - _self: ClassLiteral<'db>, + _self: StmtClassLiteral<'db>, ) -> Option { None } @@ -119,7 +359,7 @@ fn implicit_attribute_cycle_recover<'db>( fn try_mro_cycle_initial<'db>( db: &'db dyn Db, _id: salsa::Id, - self_: ClassLiteral<'db>, + self_: StmtClassLiteral<'db>, specialization: Option>, ) -> Result, MroError<'db>> { Err(MroError::cycle( @@ -131,7 +371,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: StmtClassLiteral<'db>, ) -> bool { false } @@ -140,7 +380,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_: StmtClassLiteral<'db>, ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { Err(MetaclassError { kind: MetaclassErrorKind::Cycle, @@ -150,7 +390,7 @@ fn try_metaclass_cycle_initial<'db>( fn decorators_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, - _self: ClassLiteral<'db>, + _self: StmtClassLiteral<'db>, ) -> Box<[Type<'db>]> { Box::default() } @@ -158,7 +398,7 @@ fn decorators_cycle_initial<'db>( fn fields_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, - _self: ClassLiteral<'db>, + _self: StmtClassLiteral<'db>, _specialization: Option>, _field_policy: CodeGeneratorKind<'db>, ) -> FxIndexMap> { @@ -179,7 +419,7 @@ pub(crate) enum CodeGeneratorKind<'db> { impl<'db> CodeGeneratorKind<'db> { pub(crate) fn from_class( db: &'db dyn Db, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, specialization: Option>, ) -> Option { #[salsa::tracked(cycle_initial=code_generator_of_class_initial, @@ -187,7 +427,7 @@ impl<'db> CodeGeneratorKind<'db> { )] fn code_generator_of_class<'db>( db: &'db dyn Db, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, specialization: Option>, ) -> Option> { if class.dataclass_params(db).is_some() { @@ -197,7 +437,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 + .stmt_class_literal(db) + .and_then(|(lit, _)| lit.dataclass_transformer_params(db)) }) }) { @@ -217,7 +459,7 @@ impl<'db> CodeGeneratorKind<'db> { fn code_generator_of_class_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, - _class: ClassLiteral<'db>, + _class: StmtClassLiteral<'db>, _specialization: Option>, ) -> Option> { None @@ -229,7 +471,7 @@ impl<'db> CodeGeneratorKind<'db> { pub(super) fn matches( self, db: &'db dyn Db, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, specialization: Option>, ) -> bool { matches!( @@ -259,7 +501,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: StmtClassLiteral<'db>, pub(crate) specialization: Specialization<'db>, } @@ -333,7 +575,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) } } @@ -379,7 +621,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 `StmtClassLiteral` // variance lazy, as it is known to be expensive, requiring // that we traverse all members. // @@ -397,8 +639,10 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> { } } -/// Represents a class type, which might be a non-generic class, or a specialization of a generic -/// class. +/// A class literal - either a statement-defined class or a functional class. +/// +/// This enum unifies statement-defined classes (from `class` statements) and functional +/// classes (from `type(name, bases, dict)` calls) so they can share the same code paths. #[derive( Clone, Copy, @@ -412,154 +656,498 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> { salsa::Update, get_size2::GetSize, )] -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 - // `ClassType::NonGeneric`. - NonGeneric(ClassLiteral<'db>), - Generic(GenericAlias<'db>), +pub enum ClassLiteral<'db> { + /// A class defined via a `class` statement. + Stmt(StmtClassLiteral<'db>), + /// A class created via the functional form `type(name, bases, dict)`. + Functional(FunctionalClassLiteral<'db>), + /// A namedtuple created via the functional form `namedtuple(name, fields)` or + /// `NamedTuple(name, fields)`. + FunctionalNamedTuple(FunctionalNamedTupleLiteral<'db>), + /// A dataclass created via the functional form `make_dataclass(name, fields)`. + FunctionalDataclass(FunctionalDataclassLiteral<'db>), } -#[salsa::tracked] -impl<'db> ClassType<'db> { - /// Return a `ClassType` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { - KnownClass::Object - .to_class_literal(db) - .to_class_type(db) - .unwrap() - } - - pub(super) const fn is_generic(self) -> bool { - matches!(self, Self::Generic(_)) - } - - pub(super) const fn into_generic_alias(self) -> Option> { +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::NonGeneric(_) => None, - Self::Generic(generic) => Some(generic), + Self::Stmt(stmt) => stmt.name(db), + Self::Functional(functional) => functional.name(db), + Self::FunctionalNamedTuple(namedtuple) => namedtuple.name(db), + Self::FunctionalDataclass(dataclass) => dataclass.name(db), } } - pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { - match self { - Self::NonGeneric(_) => self, - Self::Generic(generic) => Self::Generic(generic.normalized_impl(db, visitor)), - } + /// Returns the known class, if any. + pub(crate) fn known(self, db: &'db dyn Db) -> Option { + self.as_stmt().and_then(|stmt| stmt.known(db)) } - pub(super) fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - match self { - Self::NonGeneric(_) => Some(self), - Self::Generic(generic) => Some(Self::Generic( - generic.recursive_type_normalized_impl(db, div, nested)?, - )), - } + /// 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_stmt() + .is_some_and(|stmt| stmt.has_pep_695_type_params(db)) } - pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + /// 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::NonGeneric(class) => class.has_pep_695_type_params(db), - Self::Generic(generic) => generic.origin(db).has_pep_695_type_params(db), + Self::Stmt(stmt) => stmt.metaclass(db), + Self::Functional(functional) => functional.metaclass(db), + Self::FunctionalNamedTuple(namedtuple) => namedtuple.metaclass(db), + Self::FunctionalDataclass(dataclass) => dataclass.metaclass(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( + /// Look up a class-level member by iterating through the MRO. + pub(crate) fn class_member( self, db: &'db dyn Db, - ) -> (ClassLiteral<'db>, Option>) { + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { match self { - Self::NonGeneric(non_generic) => (non_generic, None), - Self::Generic(generic) => (generic.origin(db), Some(generic.specialization(db))), + // Use the full class_member which has dunder handling. + Self::Stmt(stmt) => stmt.class_member(db, name, policy), + Self::Functional(functional) => functional.class_member(db, name, policy), + Self::FunctionalNamedTuple(namedtuple) => namedtuple.class_member(db, name, policy), + Self::FunctionalDataclass(dataclass) => dataclass.class_member(db, name, policy), } } - /// Returns the class literal and specialization for this class, with an additional - /// specialization applied if the class is generic. - pub(crate) fn class_literal_specialized( + /// 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, - additional_specialization: Option>, - ) -> (ClassLiteral<'db>, Option>) { + name: &str, + policy: MemberLookupPolicy, + mro_iter: impl Iterator>, + ) -> PlaceAndQualifiers<'db> { match self { - Self::NonGeneric(non_generic) => (non_generic, None), - Self::Generic(generic) => ( - generic.origin(db), - Some( - generic - .specialization(db) - .apply_optional_specialization(db, additional_specialization), - ), - ), + Self::Stmt(stmt) => stmt.class_member_from_mro(db, name, policy, mro_iter), + Self::Functional(_) | Self::FunctionalNamedTuple(_) | Self::FunctionalDataclass(_) => { + // Functional 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.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"), + } + } } } - pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { - let (class_literal, _) = self.class_literal(db); - class_literal.name(db) + /// 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) } - pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { - let (class_literal, _) = self.class_literal(db); - class_literal.qualified_name(db) + /// Returns the default specialization for this class. + /// + /// For statement-based classes, this applies default type arguments. + /// For functional classes, this returns a non-generic class type. + pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.into_non_generic_class_type() + .unwrap_or_else(|| self.as_stmt().unwrap().default_specialization(db)) } - pub(crate) fn known(self, db: &'db dyn Db) -> Option { - let (class_literal, _) = self.class_literal(db); - class_literal.known(db) + /// 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> { + self.into_non_generic_class_type() + .unwrap_or_else(|| self.as_stmt().unwrap().identity_specialization(db)) } - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { - let (class_literal, _) = self.class_literal(db); - class_literal.definition(db) + /// Returns the generic context if this is a generic class. + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + self.as_stmt().and_then(|stmt| stmt.generic_context(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) + /// Returns whether this class is a protocol. + pub(crate) fn is_protocol(self, db: &'db dyn Db) -> bool { + self.as_stmt().is_some_and(|stmt| stmt.is_protocol(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) + /// Returns whether this class is a `TypedDict`. + pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { + self.as_stmt().is_some_and(|stmt| stmt.is_typed_dict(db)) } - /// Return `true` if this class represents the builtin class `object` - pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { - self.is_known(db, KnownClass::Object) + /// Returns whether this class is a tuple subclass. + pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { + match self { + Self::Stmt(stmt) => stmt.is_tuple(db), + Self::Functional(_) | Self::FunctionalDataclass(_) => false, + // Functional namedtuples are tuple subclasses. + Self::FunctionalNamedTuple(_) => true, + } } - pub(super) fn apply_type_mapping_impl<'a>( - self, - db: &'db dyn Db, - type_mapping: &TypeMapping<'a, 'db>, - tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, - ) -> Self { + /// Returns the metaclass instance type for this class. + pub(crate) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { match self { - Self::NonGeneric(_) => self, - Self::Generic(generic) => { - Self::Generic(generic.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) - } + Self::Stmt(stmt) => stmt.metaclass_instance_type(db), + Self::Functional(functional) => functional.metaclass(db), + Self::FunctionalNamedTuple(namedtuple) => namedtuple.metaclass(db), + Self::FunctionalDataclass(dataclass) => dataclass.metaclass(db), } } - pub(super) fn find_legacy_typevars_impl( - self, - db: &'db dyn Db, - binding_context: Option>, - typevars: &mut FxOrderSet>, - visitor: &FindLegacyTypeVarsVisitor<'db>, - ) { + /// Returns whether this class is type-check only. + pub(crate) fn type_check_only(self, db: &'db dyn Db) -> bool { + self.as_stmt().is_some_and(|stmt| stmt.type_check_only(db)) + } + + /// Returns the deprecated info if this class is deprecated. + pub(crate) fn deprecated(self, db: &'db dyn Db) -> Option> { + self.as_stmt().and_then(|stmt| stmt.deprecated(db)) + } + + /// Returns whether this class is final. + pub(crate) fn is_final(self, db: &'db dyn Db) -> bool { + self.as_stmt().is_some_and(|stmt| stmt.is_final(db)) + } + + /// Returns the statement class literal if this is one. + pub(crate) fn as_stmt(self) -> Option> { + match self { + Self::Stmt(stmt) => Some(stmt), + Self::Functional(_) | Self::FunctionalNamedTuple(_) | Self::FunctionalDataclass(_) => { + None + } + } + } + + /// Returns the functional namedtuple literal if this is one. + pub(crate) fn as_functional_namedtuple(self) -> Option> { + match self { + Self::FunctionalNamedTuple(namedtuple) => Some(namedtuple), + Self::Stmt(_) | Self::Functional(_) | Self::FunctionalDataclass(_) => None, + } + } + + /// Converts a functional class variant to a non-generic `ClassType`. + /// + /// Returns `None` for statement-based classes (use `default_specialization` instead). + pub(crate) fn into_non_generic_class_type(self) -> Option> { + match self { + Self::Stmt(_) => None, + Self::Functional(f) => Some(ClassType::NonGeneric(f.into())), + Self::FunctionalNamedTuple(n) => Some(ClassType::NonGeneric(n.into())), + Self::FunctionalDataclass(d) => Some(ClassType::NonGeneric(d.into())), + } + } + + /// Returns an unknown specialization for this class. + pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.into_non_generic_class_type() + .unwrap_or_else(|| self.as_stmt().unwrap().unknown_specialization(db)) + } + + /// Returns the body scope of this class, if it's a statement class. + pub(crate) fn body_scope(self, db: &'db dyn Db) -> Option> { + self.as_stmt().map(|stmt| stmt.body_scope(db)) + } + + /// Returns the definition of this class, if it's a statement class. + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + self.as_stmt().map(|stmt| stmt.definition(db)) + } + + /// Returns the qualified name of this class, if it's a statement class. + pub(super) fn qualified_name(self, db: &'db dyn Db) -> Option> { + self.as_stmt().map(|stmt| stmt.qualified_name(db)) + } + + /// Returns whether this class is a disjoint base. + pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + self.as_stmt().and_then(|stmt| stmt.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> { + if let Some(class_type) = self.into_non_generic_class_type() { + Type::instance(db, class_type) + } else { + self.as_stmt().unwrap().to_non_generic_instance(db) + } + } + + /// Returns the protocol class if this is a protocol. + pub(super) fn into_protocol_class( + self, + db: &'db dyn Db, + ) -> Option> { + self.as_stmt().and_then(|stmt| stmt.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> { + // Functional classes don't have generic contexts, so specialization is a no-op. + self.into_non_generic_class_type() + .unwrap_or_else(|| self.as_stmt().unwrap().apply_specialization(db, f)) + } + + /// Returns the instance member lookup. + pub(crate) fn instance_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + match self { + Self::Stmt(stmt) => stmt.instance_member(db, specialization, name), + Self::Functional(functional) => functional.instance_member(db, name), + Self::FunctionalNamedTuple(namedtuple) => namedtuple.instance_member(db, name), + Self::FunctionalDataclass(dataclass) => dataclass.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::Stmt(stmt) => stmt.top_materialization(db), + Self::Functional(functional) => ClassType::NonGeneric(functional.into()), + Self::FunctionalNamedTuple(namedtuple) => ClassType::NonGeneric(namedtuple.into()), + Self::FunctionalDataclass(dataclass) => ClassType::NonGeneric(dataclass.into()), + } + } + + /// Returns the `TypedDict` member lookup. + /// Functional classes cannot be `TypedDicts`, so this delegates to the Stmt variant. + pub(crate) fn typed_dict_member( + self, + db: &'db dyn Db, + specialization: Option>, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + match self { + Self::Stmt(stmt) => stmt.typed_dict_member(db, specialization, name, policy), + Self::Functional(_) | Self::FunctionalNamedTuple(_) | Self::FunctionalDataclass(_) => { + Place::Undefined.into() + } + } + } +} + +impl<'db> From> for ClassLiteral<'db> { + fn from(stmt: StmtClassLiteral<'db>) -> Self { + ClassLiteral::Stmt(stmt) + } +} + +impl<'db> From> for ClassLiteral<'db> { + fn from(functional: FunctionalClassLiteral<'db>) -> Self { + ClassLiteral::Functional(functional) + } +} + +impl<'db> From> for ClassLiteral<'db> { + fn from(namedtuple: FunctionalNamedTupleLiteral<'db>) -> Self { + ClassLiteral::FunctionalNamedTuple(namedtuple) + } +} + +impl<'db> From> for ClassLiteral<'db> { + fn from(dataclass: FunctionalDataclassLiteral<'db>) -> Self { + ClassLiteral::FunctionalDataclass(dataclass) + } +} + +/// Represents a class type, which might be a non-generic class, or a specialization of a generic +/// class. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + salsa::Supertype, + salsa::Update, + get_size2::GetSize, +)] +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 `StmtClassLiteral::default_specialization` instead of assuming + // `ClassType::NonGeneric`. + NonGeneric(ClassLiteral<'db>), + Generic(GenericAlias<'db>), +} + +#[salsa::tracked] +impl<'db> ClassType<'db> { + /// Return a `ClassType` representing the class `builtins.object` + pub(super) fn object(db: &'db dyn Db) -> Self { + KnownClass::Object + .to_class_literal(db) + .to_class_type(db) + .unwrap() + } + + pub(super) const fn is_generic(self) -> bool { + matches!(self, Self::Generic(_)) + } + + pub(super) const fn into_generic_alias(self) -> Option> { + match self { + Self::NonGeneric(_) => None, + Self::Generic(generic) => Some(generic), + } + } + + pub(super) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self { + match self { + Self::NonGeneric(_) => self, + Self::Generic(generic) => Self::Generic(generic.normalized_impl(db, visitor)), + } + } + + pub(super) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + match self { + Self::NonGeneric(_) => Some(self), + Self::Generic(generic) => Some(Self::Generic( + generic.recursive_type_normalized_impl(db, div, nested)?, + )), + } + } + + 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(literal) => literal, + Self::Generic(generic) => ClassLiteral::Stmt(generic.origin(db)), + } + } + + /// 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 stmt_class_literal( + self, + db: &'db dyn Db, + ) -> Option<(StmtClassLiteral<'db>, Option>)> { + match self { + Self::NonGeneric(ClassLiteral::Stmt(stmt)) => Some((stmt, None)), + Self::NonGeneric( + ClassLiteral::Functional(_) + | ClassLiteral::FunctionalNamedTuple(_) + | ClassLiteral::FunctionalDataclass(_), + ) => None, + Self::Generic(generic) => Some((generic.origin(db), Some(generic.specialization(db)))), + } + } + + /// Returns the statement-defined class literal and specialization for this class, with an additional + /// specialization applied if the class is generic. + pub(crate) fn stmt_class_literal_specialized( + self, + db: &'db dyn Db, + additional_specialization: Option>, + ) -> Option<(StmtClassLiteral<'db>, Option>)> { + match self { + Self::NonGeneric(ClassLiteral::Stmt(stmt)) => Some((stmt, None)), + Self::NonGeneric( + ClassLiteral::Functional(_) + | ClassLiteral::FunctionalNamedTuple(_) + | ClassLiteral::FunctionalDataclass(_), + ) => 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 Name { + self.class_literal(db).name(db) + } + + pub(super) fn qualified_name(self, db: &'db dyn Db) -> Option> { + self.class_literal(db).qualified_name(db) + } + + pub(crate) fn known(self, db: &'db dyn Db) -> Option { + self.class_literal(db).known(db) + } + + /// Returns the definition for this class. + /// + /// Returns `None` for functional classes, which don't have an associated definition. + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + self.class_literal(db).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).as_disjoint_base(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) + } + + /// Return `true` if this class represents the builtin class `object` + pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { + self.is_known(db, KnownClass::Object) + } + + pub(super) fn apply_type_mapping_impl<'a>( + self, + db: &'db dyn Db, + type_mapping: &TypeMapping<'a, 'db>, + tcx: TypeContext<'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Self { + match self { + Self::NonGeneric(_) => self, + Self::Generic(generic) => { + Self::Generic(generic.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + } + } + } + + pub(super) fn find_legacy_typevars_impl( + self, + db: &'db dyn Db, + binding_context: Option>, + typevars: &mut FxOrderSet>, + visitor: &FindLegacyTypeVarsVisitor<'db>, + ) { match self { Self::NonGeneric(_) => {} Self::Generic(generic) => { @@ -572,13 +1160,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 + /// [`StmtClassLiteral::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::Stmt(generic.origin(db)), + Some(generic.specialization(db)), + ), + } } /// Iterate over the method resolution order ("MRO") of the class, optionally applying an @@ -588,15 +1182,23 @@ 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::Stmt(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) } /// Return `true` if `other` is present in this class's MRO. @@ -643,15 +1245,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( @@ -664,6 +1269,8 @@ impl<'db> ClassType<'db> { ) }) } + + // Generic and non-generic classes don't match. (ClassType::Generic(_), ClassType::NonGeneric(_)) | (ClassType::NonGeneric(_), ClassType::Generic(_)) => { ConstraintSet::from(false) @@ -685,8 +1292,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) } @@ -706,10 +1313,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. @@ -824,8 +1434,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 @@ -857,11 +1474,56 @@ impl<'db> ClassType<'db> { Signature::new(parameters, Some(return_annotation)) } - let (class_literal, specialization) = self.class_literal(db); - - let fallback_member_lookup = || { - class_literal - .own_class_member(db, inherited_generic_context, specialization, name) + // Handle functional namedtuples separately since they have synthesized class members. + if let Self::NonGeneric(ClassLiteral::FunctionalNamedTuple(namedtuple)) = self { + // Check for synthesized namedtuple class members like _fields, __new__, _replace, etc. + if let Some(ty) = synthesize_namedtuple_class_member( + db, + name, + namedtuple.to_instance(db), + namedtuple.fields(db).iter().cloned(), + inherited_generic_context, + ) { + // For fallback members from NamedTupleFallback, apply type mapping to handle + // `Self` in inherited namedtuple classes. The explicitly synthesized members + // (__new__, _fields, _replace, __replace__) don't need this mapping. + let ty = if matches!(name, "__new__" | "_fields" | "_replace" | "__replace__") { + ty + } else { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: namedtuple.to_instance(db), + }, + TypeContext::default(), + ) + }; + return Member { + inner: Place::bound(ty).into(), + }; + } + + // Check if it's a field name (returns a property descriptor). + for (field_name, field_ty, _) in namedtuple.fields(db).as_ref() { + if field_name.as_str() == name { + return Member { + inner: Place::bound(create_field_property(db, *field_ty)).into(), + }; + } + } + + // Not a synthesized member or field, return unbound + // (tuple base class members will be found via MRO traversal). + return Member::unbound(); + } + + let Some((class_literal, specialization)) = self.stmt_class_literal(db) else { + return Member::unbound(); + }; + + let fallback_member_lookup = || { + class_literal + .own_class_member(db, inherited_generic_context, specialization, name) .map_type(|ty| { let ty = ty.apply_optional_specialization(db, specialization); match specialization.map(|spec| spec.materialization_kind(db)) { @@ -1143,24 +1805,79 @@ 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::Functional(functional)) => { + functional.instance_member(db, name) + } + Self::NonGeneric(ClassLiteral::FunctionalNamedTuple(namedtuple)) => { + namedtuple.instance_member(db, name) + } + Self::NonGeneric(ClassLiteral::FunctionalDataclass(dataclass)) => { + dataclass.instance_member(db, name) + } + Self::NonGeneric(ClassLiteral::Stmt(stmt)) => { + if stmt.is_typed_dict(db) { + return Place::Undefined.into(); + } + stmt.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); - class_literal - .own_instance_member(db, name) - .map_type(|ty| ty.apply_optional_specialization(db, specialization)) + pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + match self { + Self::NonGeneric(ClassLiteral::FunctionalNamedTuple(namedtuple)) => { + // For functional namedtuples, the field attributes are "own" instance members. + for (field_name, field_ty, _) in namedtuple.fields(db).as_ref() { + if field_name.as_str() == name { + return Member { + inner: Place::bound(create_field_property(db, *field_ty)).into(), + }; + } + } + Member::unbound() + } + Self::NonGeneric(ClassLiteral::FunctionalDataclass(dataclass)) => { + // For functional dataclasses, the field attributes are "own" instance members. + for (field_name, field_ty, _) in dataclass.fields(db).as_ref() { + if field_name.as_str() == name { + return Member { + inner: Place::bound(*field_ty).into(), + }; + } + } + Member::unbound() + } + Self::NonGeneric(ClassLiteral::Functional(_)) => { + // Functional type() classes don't have own instance members. + Member::unbound() + } + Self::NonGeneric(ClassLiteral::Stmt(class_literal)) => { + class_literal.own_instance_member(db, name) + } + Self::Generic(generic) => { + generic + .origin(db) + .own_instance_member(db, name) + .map_type(|ty| { + ty.apply_optional_specialization(db, Some(generic.specialization(db))) + }) + } + } } /// Return a callable type (or union of callable types) that represents the callable @@ -1171,8 +1888,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); + // Functional classes don't have a generic context. + let class_generic_context = self + .stmt_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 @@ -1350,11 +2069,14 @@ impl<'db> ClassType<'db> { } pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool { - self.class_literal(db).0.is_protocol(db) + // Functional classes are never protocols. + self.stmt_class_literal(db) + .is_some_and(|(class_literal, _)| class_literal.is_protocol(db)) } - pub(super) fn header_span(self, db: &'db dyn Db) -> Span { - self.class_literal(db).0.header_span(db) + pub(super) fn header_span(self, db: &'db dyn Db) -> Option { + self.stmt_class_literal(db) + .map(|(class_literal, _)| class_literal.header_span(db)) } } @@ -1372,6 +2094,36 @@ 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(stmt: StmtClassLiteral<'db>) -> Type<'db> { + Type::ClassLiteral(stmt.into()) + } +} + +impl<'db> From> for Type<'db> { + fn from(functional: FunctionalClassLiteral<'db>) -> Type<'db> { + Type::ClassLiteral(functional.into()) + } +} + +impl<'db> From> for Type<'db> { + fn from(namedtuple: FunctionalNamedTupleLiteral<'db>) -> Type<'db> { + Type::ClassLiteral(namedtuple.into()) + } +} + +impl<'db> From> for Type<'db> { + fn from(dataclass: FunctionalDataclassLiteral<'db>) -> Type<'db> { + Type::ClassLiteral(dataclass.into()) + } +} + impl<'db> From> for Type<'db> { fn from(class: ClassType<'db>) -> Type<'db> { match class { @@ -1384,14 +2136,19 @@ 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::Stmt(stmt)) => stmt.variance_of(db, typevar), + Self::NonGeneric( + ClassLiteral::Functional(_) + | ClassLiteral::FunctionalNamedTuple(_) + | ClassLiteral::FunctionalDataclass(_), + ) => 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 [`StmtClassLiteral::implicit_attribute`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(super) enum MethodDecorator { None, @@ -1498,10 +2255,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 StmtClassLiteral<'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>, @@ -1517,18 +2274,18 @@ pub struct ClassLiteral<'db> { } // The Salsa heap is tracked separately. -impl get_size2::GetSize for ClassLiteral<'_> {} +impl get_size2::GetSize for StmtClassLiteral<'_> {} fn generic_context_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, - _self: ClassLiteral<'db>, + _self: StmtClassLiteral<'db>, ) -> Option> { None } #[salsa::tracked] -impl<'db> ClassLiteral<'db> { +impl<'db> StmtClassLiteral<'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) @@ -1675,7 +2432,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); @@ -1747,7 +2504,7 @@ 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!("StmtClassLiteral::explicit_bases_query: {}", self.name(db)); let module = parsed_module(db, self.file(db)).load(db); let class_stmt = self.node(db, &module); @@ -1827,7 +2584,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!("StmtClassLiteral::decorators: {}", self.name(db)); let module = parsed_module(db, self.file(db)).load(db); @@ -1902,15 +2659,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!("StmtClassLiteral::try_mro: {}", self.name(db)); + Mro::of_stmt_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 + /// [`StmtClassLiteral::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 @@ -1919,7 +2676,7 @@ impl<'db> ClassLiteral<'db> { db: &'db dyn Db, specialization: Option>, ) -> MroIterator<'db> { - MroIterator::new(db, self, specialization) + MroIterator::new(db, ClassLiteral::Stmt(self), specialization) } /// Return `true` if `other` is present in this class's MRO. @@ -2005,7 +2762,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!("StmtClassLiteral::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(); @@ -2026,7 +2783,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 functional classes, we can't get a StmtClassLiteral, so use self for tracking. + let base_class_literal = base_class + .stmt_class_literal(db) + .map(|(lit, _)| lit) + .unwrap_or(self); (base_class.metaclass(db), base_class_literal) } else { (KnownClass::Type.to_class_literal(db), self) @@ -2077,8 +2838,12 @@ impl<'db> ClassLiteral<'db> { let Some(metaclass) = metaclass.to_class_type(db) else { continue; }; + // For functional classes, we can't get a StmtClassLiteral, so use self for tracking. + let base_class_literal = base_class + .stmt_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, @@ -2088,7 +2853,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, @@ -2101,11 +2865,12 @@ impl<'db> ClassLiteral<'db> { }); } - let (metaclass_literal, _) = candidate.metaclass.class_literal(db); - Ok(( - candidate.metaclass.into(), - metaclass_literal.dataclass_transformer_params(db), - )) + // Functional classes don't have dataclass transformer params. + let dataclass_transformer_params = candidate + .metaclass + .stmt_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`. @@ -2163,118 +2928,45 @@ 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 mut lookup_result: LookupResult<'db> = - Err(LookupError::Undefined(TypeQualifiers::empty())); - - 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; - } + let result = MroLookup::new(db, mro_iter).class_member( + name, + policy, + self.inherited_generic_context(db), + self.is_known(db, KnownClass::Object), + ); - if matches!(known, Some(KnownClass::Int | KnownClass::Str)) - && policy.mro_no_int_or_str_fallback() - { - continue; - } + match result { + ClassMemberResult::Done { .. } => result.finalize(db), - lookup_result = lookup_result.or_else(|lookup_error| { - lookup_error.or_fall_back_to( + ClassMemberResult::TypedDict => { + // `TypedDict`-specific handling with 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") + .map_type(|ty| { + ty.apply_type_mapping( db, - class - .own_class_member(db, self.inherited_generic_context(db), name) - .inner, + &TypeMapping::ReplaceSelf { + new_upper_bound: determine_upper_bound( + db, + self, + None, + ClassBase::is_typed_dict, + ), + }, + TypeContext::default(), ) - }); - } - 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( - 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(ty, _, _, _), - qualifiers, - }, - Some(dynamic_type), - ) => Place::bound( - IntersectionBuilder::new(db) - .add_positive(ty) - .add_positive(dynamic_type) - .build(), - ) - .with_qualifiers(qualifiers), - - ( - PlaceAndQualifiers { - place: Place::Undefined, - qualifiers, - }, - Some(dynamic_type), - ) => Place::bound(dynamic_type).with_qualifiers(qualifiers), - } } /// Returns the inferred type of the class member named `name`. Only bound members /// 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 [`StmtClassLiteral::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, @@ -2313,16 +3005,7 @@ impl<'db> ClassLiteral<'db> { .own_fields(db, specialization, CodeGeneratorKind::NamedTuple) .get(name) { - let property_getter_signature = Signature::new( - Parameters::new( - db, - [Parameter::positional_only(Some(Name::new_static("self")))], - ), - Some(field.declared_ty), - ); - let property_getter = Type::single_callable(db, property_getter_signature); - let property = PropertyInstanceType::new(db, Some(property_getter), None); - return Member::definitely_declared(Type::PropertyInstance(property)); + return Member::definitely_declared(create_field_property(db, field.declared_ty)); } } @@ -2506,7 +3189,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); } @@ -2565,37 +3248,49 @@ impl<'db> ClassLiteral<'db> { .with_annotated_type(instance_ty); signature_from_fields(vec![self_parameter], Some(Type::none(db))) } - (CodeGeneratorKind::NamedTuple, "__new__") => { - let cls_parameter = Parameter::positional_or_keyword(Name::new_static("cls")) - .with_annotated_type(KnownClass::Type.to_instance(db)); - signature_from_fields(vec![cls_parameter], Some(Type::none(db))) - } - (CodeGeneratorKind::NamedTuple, "_replace" | "__replace__") => { - if name == "__replace__" - && Program::get(db).python_version(db) < PythonVersion::PY313 - { - return None; - } - // Use `Self` type variable as return type so that subclasses get the correct - // return type when calling `_replace`. For example, if `IntBox` inherits from - // `Box[int]` (a NamedTuple), then `IntBox(1)._replace(content=42)` should return - // `IntBox`, not `Box[int]`. - let self_ty = Type::TypeVar(BoundTypeVarInstance::synthetic_self( + (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { + let inherited_generic_context = self.inherited_generic_context(db); + let fields_iter = self + .fields(db, specialization, field_policy) + .into_iter() + .map(|(name, field)| { + let default_ty = match &field.kind { + FieldKind::NamedTuple { default_ty } => *default_ty, + _ => None, + }; + (name.clone(), field.declared_ty, default_ty) + }); + let result = synthesize_namedtuple_class_member( db, + name, instance_ty, - BindingContext::Synthetic, - )); - let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(self_ty); - signature_from_fields(vec![self_parameter], Some(self_ty)) - } - (CodeGeneratorKind::NamedTuple, "_fields") => { - // Synthesize a precise tuple type for _fields using literal string types. - // For example, a NamedTuple with `name` and `age` fields gets - // `tuple[Literal["name"], Literal["age"]]`. - let fields = self.fields(db, specialization, field_policy); - let field_types = fields.keys().map(|name| Type::string_literal(db, name)); - Some(Type::heterogeneous_tuple(db, field_types)) + fields_iter, + inherited_generic_context, + ); + // For fallback members from NamedTupleFallback, apply type mapping to handle + // `Self` in inherited namedtuple classes. The explicitly synthesized members + // (__new__, _fields, _replace, __replace__) don't need this mapping. + if matches!(name, "__new__" | "_fields" | "_replace" | "__replace__") { + result + } else { + result.map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: determine_upper_bound( + db, + self, + specialization, + |base| { + base.into_class() + .is_some_and(|c| c.is_known(db, KnownClass::Tuple)) + }, + ), + }, + TypeContext::default(), + ) + }) + } } (CodeGeneratorKind::DataclassLike(_), "__lt__" | "__le__" | "__gt__" | "__ge__") => { if !has_dataclass_param(DataclassFlags::ORDER) { @@ -2677,30 +3372,6 @@ impl<'db> ClassLiteral<'db> { // model it precisely. Some(UnionType::from_elements(db, [Type::any(), Type::none(db)])) } - (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { - KnownClass::NamedTupleFallback - .to_class_literal(db) - .as_class_literal()? - .own_class_member(db, self.inherited_generic_context(db), None, name) - .ignore_possibly_undefined() - .map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::ReplaceSelf { - new_upper_bound: determine_upper_bound( - db, - self, - specialization, - |base| { - base.into_class() - .is_some_and(|c| c.is_known(db, KnownClass::Tuple)) - }, - ), - }, - TypeContext::default(), - ) - }) - } (CodeGeneratorKind::DataclassLike(_), "__replace__") if Program::get(db).python_version(db) >= PythonVersion::PY313 => { @@ -3142,7 +3813,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 [`StmtClassLiteral::own_fields`] for more details. #[salsa::tracked( returns(ref), cycle_initial=fields_cycle_initial, @@ -3159,22 +3830,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); + let matching_classes_in_mro: Vec<(StmtClassLiteral<'db>, Option>)> = + self.iter_mro(db, specialization) + .filter_map(|superclass| { + let class = superclass.into_class()?; + // Functional classes don't have fields (no class body). + let (class_literal, specialization) = class.stmt_class_literal(db)?; if field_policy.matches(db, class_literal, 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() @@ -3364,83 +4033,22 @@ 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(ty, origin, 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(), - ) - }); - } - } + 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(), + ) + }), } - - if union.is_empty() { - Place::Undefined.with_qualifiers(TypeQualifiers::empty()) - } else { - let boundness = if is_definitely_bound { - Definedness::AlwaysDefined - } else { - Definedness::PossiblyUndefined - }; - - Place::Defined( - union.build(), - TypeOrigin::Inferred, - boundness, - Widening::None, - ) - .with_qualifiers(union_qualifiers) - } - } + } /// Tries to find declarations/bindings of an attribute named `name` that are only /// "implicitly" defined (`self.x = …`, `cls.x = …`) in a method of the class that @@ -3920,7 +4528,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. @@ -3934,17 +4542,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: StmtClassLiteral<'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_stmt(), + 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; } @@ -3973,167 +4584,956 @@ impl<'db> ClassLiteral<'db> { } else { Some(InheritanceCycle::Inherited) } - } + } + + /// Returns a [`Span`] with the range of the class's header. + /// + /// 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 class's "header": the class name + /// and any arguments passed to the `class` statement. E.g. + /// + /// ```ignore + /// class Foo(Bar, metaclass=Baz): ... + /// ^^^^^^^^^^^^^^^^^^^^^^^ + /// ``` + pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { + let class_scope = self.body_scope(db); + let module = parsed_module(db, class_scope.file(db)).load(db); + let class_node = class_scope.node(db).expect_class().node(&module); + let class_name = &class_node.name; + TextRange::new( + class_name.start(), + class_node + .arguments + .as_deref() + .map(Ranged::end) + .unwrap_or_else(|| class_name.end()), + ) + } + + pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { + QualifiedClassName { db, class: self } + } +} + +#[salsa::tracked] +impl<'db> VarianceInferable<'db> for StmtClassLiteral<'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 + .generic_context(db) + .is_some_and(|generic_context| generic_context.variables(db).contains(&typevar)); + + if !typevar_in_generic_context { + return TypeVarVariance::Bivariant; + } + let class_body_scope = self.body_scope(db); + + let file = class_body_scope.file(db); + let index = semantic_index(db, file); + + let explicit_bases_variances = self + .explicit_bases(db) + .iter() + .map(|class| class.variance_of(db, typevar)); + + let default_attribute_variance = { + let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self, 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 + // not considered here, since they don't use field types in their signatures. TODO: + // ideally we'd have a single source of truth for information about synthesized + // methods, so we just look them up normally and don't hardcode this knowledge here. + let is_frozen_dataclass = Program::get(db).python_version(db) <= PythonVersion::PY312 + && self + .dataclass_params(db) + .is_some_and(|params| params.flags(db).contains(DataclassFlags::FROZEN)); + if is_namedtuple || is_frozen_dataclass { + TypeVarVariance::Covariant + } else { + TypeVarVariance::Invariant + } + }; + + let init_name: &Name = &"__init__".into(); + let new_name: &Name = &"__new__".into(); + + let use_def_map = index.use_def_map(class_body_scope.file_scope_id(db)); + let table = place_table(db, class_body_scope); + let attribute_places_and_qualifiers = + use_def_map + .all_end_of_scope_symbol_declarations() + .map(|(symbol_id, declarations)| { + let place_and_qual = + place_from_declarations(db, declarations).ignore_conflicting_declarations(); + (symbol_id, place_and_qual) + }) + .chain(use_def_map.all_end_of_scope_symbol_bindings().map( + |(symbol_id, bindings)| { + (symbol_id, place_from_bindings(db, bindings).place.into()) + }, + )) + .filter_map(|(symbol_id, place_and_qual)| { + if let Some(name) = table.place(symbol_id).as_symbol().map(Symbol::name) { + (![init_name, new_name].contains(&name)) + .then_some((name.to_string(), place_and_qual)) + } else { + None + } + }); + + // Dataclasses can have some additional synthesized methods (`__eq__`, `__hash__`, + // `__lt__`, etc.) but none of these will have field types type variables in their signatures, so we + // don't need to consider them for variance. + + let attribute_names = attribute_scopes(db, self.body_scope(db)) + .flat_map(|function_scope_id| { + index + .place_table(function_scope_id) + .members() + .filter_map(|member| member.as_instance_attribute()) + .filter(|name| *name != init_name && *name != new_name) + .map(std::string::ToString::to_string) + .collect::>() + }) + .dedup(); + + let attribute_variances = attribute_names + .map(|name| { + let place_and_quals = self.own_instance_member(db, &name).inner; + (name, place_and_quals) + }) + .chain(attribute_places_and_qualifiers) + .dedup() + .filter_map(|(name, place_and_qual)| { + place_and_qual.ignore_possibly_undefined().map(|ty| { + let variance = if place_and_qual + .qualifiers + // `CLASS_VAR || FINAL` is really `all()`, but + // we want to be robust against new qualifiers + .intersects(TypeQualifiers::CLASS_VAR | TypeQualifiers::FINAL) + // We don't allow mutation of methods or properties + || ty.is_function_literal() + || ty.is_property_instance() + // Underscore-prefixed attributes are assumed not to be externally mutated + || name.starts_with('_') + { + // CLASS_VAR: class vars generally shouldn't contain the + // type variable, but they could if it's a + // callable type. They can't be mutated on instances. + // + // FINAL: final attributes are immutable, and thus covariant + TypeVarVariance::Covariant + } else { + default_attribute_variance + }; + ty.with_polarity(variance).variance_of(db, typevar) + }) + }); + + attribute_variances + .chain(explicit_bases_variances) + .collect() + } +} + +impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { + fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarVariance { + match self { + Self::Stmt(stmt) => stmt.variance_of(db, typevar), + Self::Functional(_) | Self::FunctionalNamedTuple(_) | Self::FunctionalDataclass(_) => { + TypeVarVariance::Bivariant + } + } + } +} + +/// A class created via the functional form: a three-argument `type()` call. +/// +/// For example: +/// ```python +/// Foo = type("Foo", (Base,), {"attr": 1}) +/// ``` +/// +/// The type of `Foo` would be `type[Foo]` where `Foo` is a `FunctionalClassLiteral` with: +/// - name: "Foo" +/// - bases: [Base] +/// +/// This is called "functional" because it mirrors the terminology used for `NamedTuple` +/// and `TypedDict`, where the "functional form" means creating via a function call +/// rather than a class statement. +/// +/// # 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 +/// +/// Two `type()` calls with the same name and bases produce the same `FunctionalClassLiteral` +/// instance. This matches Pyright's behavior where: +/// ```python +/// Foo1 = type("Foo", (Base,), {}) +/// Foo2 = type("Foo", (Base,), {}) +/// # Foo1 and Foo2 have the same type: type[Foo] +/// ``` +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct FunctionalClassLiteral<'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(ref)] + pub bases: Box<[ClassBase<'db>]>, +} + +impl get_size2::GetSize for FunctionalClassLiteral<'_> {} + +impl<'db> FunctionalClassLiteral<'db> { + /// Get the metaclass of this functional 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 functional class. + /// + /// Returns `Err(FunctionalMetaclassConflict)` 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, FunctionalMetaclassConflict<'db>> { + let bases = self.bases(db); + + // If no bases, metaclass is `type`. + if bases.is_empty() { + return Ok(KnownClass::Type.to_instance(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. + // Track which base the candidate metaclass came from. + let mut candidate = bases[0].metaclass(db); + let mut candidate_base = bases[0]; + + // Reconcile with other bases' metaclasses. + for base in &bases[1..] { + 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(FunctionalMetaclassConflict { + metaclass1: candidate_class, + base1: candidate_base, + metaclass2: base_metaclass_class, + base2: *base, + }); + } + + Ok(candidate) + } + + /// Iterate over the MRO of this functional class using C3 linearization. + /// + /// The MRO includes the functional 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::Functional(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 (functional classes aren't generic). + /// - `is_self_object = false` (functional classes are never `object`). + pub(crate) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + let result = MroLookup::new(db, self.iter_mro(db)).class_member( + name, policy, None, // No inherited generic context. + false, // Functional classes are never `object`. + ); + + match result { + ClassMemberResult::Done { .. } => 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 functional class. + /// + /// Returns `Ok(Mro)` if successful, or `Err(FunctionalMroError)` if there's + /// an error (duplicate bases or C3 linearization failure). + pub(crate) fn try_mro(self, db: &'db dyn Db) -> Result, FunctionalMroError<'db>> { + Mro::of_functional_class(db, self) + } +} + +#[salsa::tracked] +impl<'db> FunctionalClassLiteral<'db> { + /// Compute and cache the MRO for this functional class. + /// + /// Uses C3 linearization when possible, falling back to sequential iteration + /// with deduplication when there's an error (duplicate bases or C3 merge failure). + #[salsa::tracked(heap_size = ruff_memory_usage::heap_size)] + pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { + self.try_mro(db) + .unwrap_or_else(|_| Mro::functional_fallback(db, self)) + } +} + +/// Error for metaclass conflicts in functional classes. +/// +/// This mirrors `MetaclassErrorKind::Conflict` for regular classes. +#[derive(Debug, Clone)] +pub(crate) struct FunctionalMetaclassConflict<'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>, +} + +/// Create a read-only property type for a namedtuple field. +/// +/// Namedtuple fields are accessed via read-only properties. This creates a property +/// with a getter that takes `self` and returns the field type. +fn create_field_property<'db>(db: &'db dyn Db, field_ty: Type<'db>) -> Type<'db> { + let property_getter_signature = Signature::new( + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("self")))], + ), + Some(field_ty), + ); + let property_getter = Type::single_callable(db, property_getter_signature); + let property = PropertyInstanceType::new(db, Some(property_getter), None); + Type::PropertyInstance(property) +} + +/// Synthesize a namedtuple class member given the field information. +/// +/// This is used by both `FunctionalNamedTupleLiteral` and `StmtClassLiteral` (for declarative +/// namedtuples) to avoid duplicating the synthesis logic. +/// +/// The `inherited_generic_context` parameter is used for declarative namedtuples to preserve +/// generic context in the synthesized `__new__` signature. +fn synthesize_namedtuple_class_member<'db>( + db: &'db dyn Db, + name: &str, + instance_ty: Type<'db>, + fields: impl Iterator, Option>)>, + inherited_generic_context: Option>, +) -> Option> { + match name { + "__new__" => { + // __new__(cls, field1, field2, ...) -> Self + let mut parameters = vec![ + Parameter::positional_or_keyword(Name::new_static("cls")) + .with_annotated_type(KnownClass::Type.to_instance(db)), + ]; + + for (field_name, field_ty, default_ty) in fields { + let mut param = + Parameter::positional_or_keyword(field_name).with_annotated_type(field_ty); + if let Some(default) = default_ty { + param = param.with_default_type(default); + } + parameters.push(param); + } + + let signature = Signature::new_generic( + inherited_generic_context, + Parameters::new(db, parameters), + Some(instance_ty), + ); + Some(Type::function_like_callable(db, signature)) + } + "_fields" => { + // _fields: tuple[Literal["field1"], Literal["field2"], ...] + let field_types = + fields.map(|(field_name, _, _)| Type::string_literal(db, &field_name)); + Some(Type::heterogeneous_tuple(db, field_types)) + } + "_replace" | "__replace__" => { + if name == "__replace__" && Program::get(db).python_version(db) < PythonVersion::PY313 { + return None; + } + + // _replace(self, *, field1=..., field2=...) -> Self + let self_ty = Type::TypeVar(BoundTypeVarInstance::synthetic_self( + db, + instance_ty, + BindingContext::Synthetic, + )); + + let mut parameters = vec![ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(self_ty), + ]; + + for (field_name, field_ty, _) in fields { + parameters.push( + Parameter::keyword_only(field_name) + .with_annotated_type(field_ty) + .with_default_type(field_ty), + ); + } + + let signature = Signature::new(Parameters::new(db, parameters), Some(self_ty)); + Some(Type::function_like_callable(db, signature)) + } + "__init__" => { + // Namedtuples don't have a custom __init__. All construction happens in __new__. + None + } + _ => { + // Fall back to NamedTupleFallback for other synthesized methods. + KnownClass::NamedTupleFallback + .to_class_literal(db) + .as_class_literal()? + .as_stmt()? + .own_class_member(db, inherited_generic_context, None, name) + .ignore_possibly_undefined() + } + } +} + +/// A namedtuple created via the functional form `namedtuple(name, fields)` or +/// `NamedTuple(name, fields)`. +/// +/// For example: +/// ```python +/// from collections import namedtuple +/// Point = namedtuple("Point", ["x", "y"]) +/// +/// from typing import NamedTuple +/// Person = NamedTuple("Person", [("name", str), ("age", int)]) +/// ``` +/// +/// The type of `Point` would be `type[Point]` where `Point` is a `FunctionalNamedTupleLiteral`. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct FunctionalNamedTupleLiteral<'db> { + /// The name of the namedtuple (from the first argument). + #[returns(ref)] + pub name: Name, + + /// The fields as (name, type, default) tuples. + /// For `collections.namedtuple`, all types are `Any`. + /// For `typing.NamedTuple`, types come from the field definitions. + /// The third element is the default type, if any. + #[returns(ref)] + pub fields: Box<[(Name, Type<'db>, Option>)]>, +} + +impl get_size2::GetSize for FunctionalNamedTupleLiteral<'_> {} + +impl<'db> FunctionalNamedTupleLiteral<'db> { + /// Returns an instance type for this functional namedtuple. + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + Type::instance(db, ClassType::NonGeneric(self.into())) + } + + /// Get the metaclass of this functional namedtuple. + /// + /// Namedtuples always have `type` as their metaclass. + pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + let _ = self; + KnownClass::Type.to_class_literal(db) + } + + /// Compute the tuple type that this namedtuple inherits from. + /// + /// For example, `namedtuple("Point", [("x", int), ("y", int)])` inherits from `tuple[int, int]`. + pub(crate) fn tuple_base_type(self, db: &'db dyn Db) -> ClassType<'db> { + let field_types = self.fields(db).iter().map(|(_, ty, _)| *ty); + TupleType::heterogeneous(db, field_types) + .map(|t| t.to_class_type(db)) + .unwrap_or_else(|| { + KnownClass::Tuple + .to_class_literal(db) + .as_class_literal() + .expect("tuple should be a class literal") + .default_specialization(db) + }) + } + + /// Look up an instance member by name. + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + // First check if it's one of the field names. + for (field_name, field_ty, _) in self.fields(db) { + if field_name.as_str() == name { + return Place::bound(create_field_property(db, *field_ty)).into(); + } + } + + // Fall back to the tuple base type for other attributes. + Type::instance(db, self.tuple_base_type(db)).instance_member(db, name) + } + + /// Look up a class-level member by name. + pub(crate) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + // Handle synthesized namedtuple attributes. + if let Some(ty) = self.synthesized_class_member(db, name) { + return Place::bound(ty).into(); + } + + // Check if it's a field name (returns a property descriptor). + for (field_name, field_ty, _) in self.fields(db) { + if field_name.as_str() == name { + return Place::bound(create_field_property(db, *field_ty)).into(); + } + } + + // Fall back to tuple class members. + self.tuple_base_type(db) + .class_literal(db) + .class_member(db, name, policy) + } + + /// Generate synthesized class members for namedtuples. + fn synthesized_class_member(self, db: &'db dyn Db, name: &str) -> Option> { + let instance_ty = self.to_instance(db); + let result = synthesize_namedtuple_class_member( + db, + name, + instance_ty, + self.fields(db).iter().cloned(), + None, + ); + // For fallback members from NamedTupleFallback, apply type mapping to handle + // `Self` types. The explicitly synthesized members (__new__, _fields, _replace, + // __replace__) don't need this mapping. + if matches!(name, "__new__" | "_fields" | "_replace" | "__replace__") { + result + } else { + result.map(|ty| { + ty.apply_type_mapping( + db, + &TypeMapping::ReplaceSelf { + new_upper_bound: instance_ty, + }, + TypeContext::default(), + ) + }) + } + } +} + +/// A dataclass created dynamically via `dataclasses.make_dataclass()`. +/// +/// For example: +/// ```python +/// from dataclasses import make_dataclass +/// Point = make_dataclass("Point", [("x", int), ("y", int)]) +/// ``` +/// +/// The type of `Point` would be `` where `Point` is a `FunctionalDataclassLiteral`. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] +pub struct FunctionalDataclassLiteral<'db> { + /// The name of the dataclass (from the first argument). + #[returns(ref)] + pub name: Name, + + /// The fields as (name, type, default) tuples. + /// Types come from the field definitions; defaults are optional. + #[returns(ref)] + pub fields: Box<[(Name, Type<'db>, Option>)]>, + + /// The base classes (from the `bases` keyword argument). + #[returns(ref)] + pub bases: Box<[ClassBase<'db>]>, + + /// The dataclass parameters (init, repr, eq, etc.). + pub params: DataclassParams<'db>, +} + +impl get_size2::GetSize for FunctionalDataclassLiteral<'_> {} + +impl<'db> FunctionalDataclassLiteral<'db> { + /// Returns an instance type for this functional dataclass. + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + Type::instance(db, ClassType::NonGeneric(self.into())) + } + + /// Get the metaclass of this functional dataclass. + /// + /// 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 functional dataclass. + /// + /// Returns `Err(FunctionalMetaclassConflict)` 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, FunctionalMetaclassConflict<'db>> { + let bases = self.bases(db); + + // If no bases, metaclass is `type`. + if bases.is_empty() { + return Ok(KnownClass::Type.to_instance(db)); + } + + // Start with the first base's metaclass as the candidate. + // Track which base the candidate metaclass came from. + let mut candidate = bases[0].metaclass(db); + let mut candidate_base = bases[0]; + + // Reconcile with other bases' metaclasses. + for base in &bases[1..] { + 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(FunctionalMetaclassConflict { + metaclass1: candidate_class, + base1: candidate_base, + metaclass2: base_metaclass_class, + base2: *base, + }); + } + + Ok(candidate) + } + + /// Look up an instance member by name. + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + // First check if it's one of the field names. + for (field_name, field_ty, _) in self.fields(db) { + if field_name.as_str() == name { + return Place::bound(*field_ty).into(); + } + } + + // Check for synthesized instance members. + if let Some(ty) = self.synthesized_instance_member(db, name) { + return Place::bound(ty).into(); + } + + // Check base classes for inherited instance members. + for base in self.bases(db) { + if let ClassBase::Class(class) = base { + let member = class.instance_member(db, name); + if !member.place.is_undefined() { + return member; + } + } + } + + // Fall back to object for other attributes. + Type::object().instance_member(db, name) + } + + /// Look up a class-level member by name. + pub(crate) fn class_member( + self, + db: &'db dyn Db, + name: &str, + policy: MemberLookupPolicy, + ) -> PlaceAndQualifiers<'db> { + // Check for synthesized class members like __init__. + if let Some(ty) = self.synthesized_class_member(db, name) { + return Place::bound(ty).into(); + } - /// Returns a [`Span`] with the range of the class's header. - /// - /// 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)) - } + // Check if it's a field name (returns the type directly for class access). + for (field_name, field_ty, _) in self.fields(db) { + if field_name.as_str() == name { + return Place::bound(*field_ty).into(); + } + } - /// Returns the range of the class's "header": the class name - /// and any arguments passed to the `class` statement. E.g. - /// - /// ```ignore - /// class Foo(Bar, metaclass=Baz): ... - /// ^^^^^^^^^^^^^^^^^^^^^^^ - /// ``` - pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { - let class_scope = self.body_scope(db); - let module = parsed_module(db, class_scope.file(db)).load(db); - let class_node = class_scope.node(db).expect_class().node(&module); - let class_name = &class_node.name; - TextRange::new( - class_name.start(), - class_node - .arguments - .as_deref() - .map(Ranged::end) - .unwrap_or_else(|| class_name.end()), - ) - } + // Check base classes for inherited class members. + for base in self.bases(db) { + if let ClassBase::Class(class) = base { + let member = class.class_member(db, name, policy); + if !member.place.is_undefined() { + return member; + } + } + } - pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { - QualifiedClassName { db, class: self } + // Fall back to object class members. + Type::object() + .to_class_type(db) + .map(|class| class.class_member(db, name, policy)) + .unwrap_or_else(|| Place::Undefined.into()) } -} -impl<'db> From> for Type<'db> { - fn from(class: ClassLiteral<'db>) -> Type<'db> { - Type::ClassLiteral(class) + /// Helper to check if a dataclass flag is set. + fn has_flag(self, db: &'db dyn Db, flag: DataclassFlags) -> bool { + self.params(db).flags(db).contains(flag) } -} -#[salsa::tracked] -impl<'db> VarianceInferable<'db> for ClassLiteral<'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 - .generic_context(db) - .is_some_and(|generic_context| generic_context.variables(db).contains(&typevar)); + /// Synthesize instance members for functional dataclasses. + fn synthesized_instance_member(self, db: &'db dyn Db, name: &str) -> Option> { + match name { + "__match_args__" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { + if !self.has_flag(db, DataclassFlags::MATCH_ARGS) { + return None; + } - if !typevar_in_generic_context { - return TypeVarVariance::Bivariant; - } - let class_body_scope = self.body_scope(db); + let kw_only_default = self.has_flag(db, DataclassFlags::KW_ONLY); - let file = class_body_scope.file(db); - let index = semantic_index(db, file); + // For functional dataclasses, we don't have per-field kw_only info, + // so we use the class-level default. + let match_args = if kw_only_default { + // All fields are kw_only, so __match_args__ is empty. + Vec::new() + } else { + self.fields(db) + .iter() + .map(|(name, _, _)| Type::string_literal(db, name)) + .collect() + }; + Some(Type::heterogeneous_tuple(db, match_args)) + } + "__slots__" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { + if !self.has_flag(db, DataclassFlags::SLOTS) { + return None; + } + let slots = self + .fields(db) + .iter() + .map(|(name, _, _)| Type::string_literal(db, name)); + Some(Type::heterogeneous_tuple(db, slots)) + } + "__weakref__" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { + if !self.has_flag(db, DataclassFlags::WEAKREF_SLOT) + || !self.has_flag(db, DataclassFlags::SLOTS) + { + return None; + } + Some(UnionType::from_elements(db, [Type::any(), Type::none(db)])) + } + _ => None, + } + } - let explicit_bases_variances = self - .explicit_bases(db) - .iter() - .map(|class| class.variance_of(db, typevar)); + /// Synthesize class members like __init__ for functional dataclasses. + fn synthesized_class_member(self, db: &'db dyn Db, name: &str) -> Option> { + let instance_ty = self.to_instance(db); - let default_attribute_variance = { - let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self, 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 - // not considered here, since they don't use field types in their signatures. TODO: - // ideally we'd have a single source of truth for information about synthesized - // methods, so we just look them up normally and don't hardcode this knowledge here. - let is_frozen_dataclass = Program::get(db).python_version(db) <= PythonVersion::PY312 - && self - .dataclass_params(db) - .is_some_and(|params| params.flags(db).contains(DataclassFlags::FROZEN)); - if is_namedtuple || is_frozen_dataclass { - TypeVarVariance::Covariant - } else { - TypeVarVariance::Invariant - } - }; + match name { + "__init__" => { + // Only synthesize __init__ if the dataclass has init=True. + if !self.has_flag(db, DataclassFlags::INIT) { + return None; + } - let init_name: &Name = &"__init__".into(); - let new_name: &Name = &"__new__".into(); + // __init__(self, field1, field2, ...) -> None + let kw_only_default = self.has_flag(db, DataclassFlags::KW_ONLY); + let mut parameters = vec![ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty), + ]; - let use_def_map = index.use_def_map(class_body_scope.file_scope_id(db)); - let table = place_table(db, class_body_scope); - let attribute_places_and_qualifiers = - use_def_map - .all_end_of_scope_symbol_declarations() - .map(|(symbol_id, declarations)| { - let place_and_qual = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); - (symbol_id, place_and_qual) - }) - .chain(use_def_map.all_end_of_scope_symbol_bindings().map( - |(symbol_id, bindings)| { - (symbol_id, place_from_bindings(db, bindings).place.into()) - }, - )) - .filter_map(|(symbol_id, place_and_qual)| { - if let Some(name) = table.place(symbol_id).as_symbol().map(Symbol::name) { - (![init_name, new_name].contains(&name)) - .then_some((name.to_string(), place_and_qual)) + for (field_name, field_ty, default_ty) in self.fields(db) { + let mut param = if kw_only_default { + Parameter::keyword_only(field_name.clone()) } else { - None + Parameter::positional_or_keyword(field_name.clone()) } - }); - - // Dataclasses can have some additional synthesized methods (`__eq__`, `__hash__`, - // `__lt__`, etc.) but none of these will have field types type variables in their signatures, so we - // don't need to consider them for variance. + .with_annotated_type(*field_ty); + if let Some(default) = default_ty { + param = param.with_default_type(*default); + } + parameters.push(param); + } - let attribute_names = attribute_scopes(db, self.body_scope(db)) - .flat_map(|function_scope_id| { - index - .place_table(function_scope_id) - .members() - .filter_map(|member| member.as_instance_attribute()) - .filter(|name| *name != init_name && *name != new_name) - .map(std::string::ToString::to_string) - .collect::>() - }) - .dedup(); + let signature = + Signature::new(Parameters::new(db, parameters), Some(Type::none(db))); + Some(Type::function_like_callable(db, signature)) + } + "__lt__" | "__le__" | "__gt__" | "__ge__" => { + if !self.has_flag(db, DataclassFlags::ORDER) { + return None; + } + let signature = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("other")) + .with_annotated_type(instance_ty), + ], + ), + Some(KnownClass::Bool.to_instance(db)), + ); + Some(Type::function_like_callable(db, signature)) + } + "__hash__" => { + let unsafe_hash = self.has_flag(db, DataclassFlags::UNSAFE_HASH); + let frozen = self.has_flag(db, DataclassFlags::FROZEN); + let eq = self.has_flag(db, DataclassFlags::EQ); - let attribute_variances = attribute_names - .map(|name| { - let place_and_quals = self.own_instance_member(db, &name).inner; - (name, place_and_quals) - }) - .chain(attribute_places_and_qualifiers) - .dedup() - .filter_map(|(name, place_and_qual)| { - place_and_qual.ignore_possibly_undefined().map(|ty| { - let variance = if place_and_qual - .qualifiers - // `CLASS_VAR || FINAL` is really `all()`, but - // we want to be robust against new qualifiers - .intersects(TypeQualifiers::CLASS_VAR | TypeQualifiers::FINAL) - // We don't allow mutation of methods or properties - || ty.is_function_literal() - || ty.is_property_instance() - // Underscore-prefixed attributes are assumed not to be externally mutated - || name.starts_with('_') - { - // CLASS_VAR: class vars generally shouldn't contain the - // type variable, but they could if it's a - // callable type. They can't be mutated on instances. - // - // FINAL: final attributes are immutable, and thus covariant - TypeVarVariance::Covariant - } else { - default_attribute_variance - }; - ty.with_polarity(variance).variance_of(db, typevar) - }) - }); + if unsafe_hash || (frozen && eq) { + let signature = Signature::new( + Parameters::new( + db, + [Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty)], + ), + Some(KnownClass::Int.to_instance(db)), + ); + Some(Type::function_like_callable(db, signature)) + } else if eq && !frozen { + Some(Type::none(db)) + } else { + // No `__hash__` is generated, fall back to `object.__hash__`. + None + } + } + "__setattr__" => { + if !self.has_flag(db, DataclassFlags::FROZEN) { + return None; + } + let signature = Signature::new( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("name")), + Parameter::positional_or_keyword(Name::new_static("value")), + ], + ), + Some(Type::Never), + ); + Some(Type::function_like_callable(db, signature)) + } + "__replace__" if Program::get(db).python_version(db) >= PythonVersion::PY313 => { + // __replace__(self, **kwargs) -> Self + let mut parameters = vec![ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty), + ]; + + // Add keyword-only parameters for each field. + for (field_name, field_ty, _) in self.fields(db) { + parameters.push( + Parameter::keyword_only(field_name.clone()) + .with_annotated_type(*field_ty) + .with_default_type(*field_ty), + ); + } - attribute_variances - .chain(explicit_bases_variances) - .collect() + let signature = Signature::new(Parameters::new(db, parameters), Some(instance_ty)); + Some(Type::function_like_callable(db, signature)) + } + _ => None, + } } } @@ -4144,7 +5544,7 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { #[derive(Clone, Copy)] pub(super) struct QualifiedClassName<'db> { db: &'db dyn Db, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, } impl QualifiedClassName<'_> { @@ -4240,14 +5640,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: StmtClassLiteral<'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: StmtClassLiteral<'db>) -> Self { Self { class, kind: DisjointBaseKind::DisjointBaseDecorator, @@ -4256,7 +5656,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: StmtClassLiteral<'db>) -> Self { Self { class, kind: DisjointBaseKind::DefinesSlots, @@ -4368,6 +5768,7 @@ pub enum KnownClass { Iterable, Iterator, Mapping, + Sequence, // typing_extensions ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features // Collections @@ -4484,6 +5885,7 @@ impl KnownClass { | Self::Iterable | Self::Iterator | Self::Mapping + | Self::Sequence // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 // and raises a `TypeError` in Python >=3.14 // (see https://docs.python.org/3/library/constants.html#NotImplemented) @@ -4572,6 +5974,7 @@ impl KnownClass { | KnownClass::Iterable | KnownClass::Iterator | KnownClass::Mapping + | KnownClass::Sequence | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -4659,6 +6062,7 @@ impl KnownClass { | KnownClass::Iterable | KnownClass::Iterator | KnownClass::Mapping + | KnownClass::Sequence | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -4746,6 +6150,7 @@ impl KnownClass { | KnownClass::Iterable | KnownClass::Iterator | KnownClass::Mapping + | KnownClass::Sequence | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -4786,6 +6191,7 @@ impl KnownClass { Self::SupportsIndex | Self::Iterable | Self::Iterator + | Self::Sequence | Self::Awaitable | Self::NamedTupleLike | Self::Generator => true, @@ -4939,6 +6345,7 @@ impl KnownClass { | KnownClass::Iterable | KnownClass::Iterator | KnownClass::Mapping + | KnownClass::Sequence | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -5031,6 +6438,7 @@ impl KnownClass { Self::Iterable => "Iterable", Self::Iterator => "Iterator", Self::Mapping => "Mapping", + Self::Sequence => "Sequence", // For example, `typing.List` is defined as `List = _Alias()` in typeshed Self::StdlibAlias => "_Alias", // This is the name the type of `sys.version_info` has in typeshed, @@ -5140,7 +6548,7 @@ impl KnownClass { fn to_specialized_class_type_impl<'db>( db: &'db dyn Db, class: KnownClass, - class_literal: ClassLiteral<'db>, + class_literal: StmtClassLiteral<'db>, specialization: Box<[Type<'db>]>, generic_context: GenericContext<'db>, ) -> ClassType<'db> { @@ -5163,7 +6571,8 @@ impl KnownClass { .apply_specialization(db, |_| generic_context.specialize(db, specialization)) } - let Type::ClassLiteral(class_literal) = self.to_class_literal(db) else { + let Type::ClassLiteral(ClassLiteral::Stmt(class_literal)) = self.to_class_literal(db) + else { return None; }; @@ -5207,14 +6616,17 @@ 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(Type::ClassLiteral(class_literal), _, Definedness::AlwaysDefined, _) => { - Ok(class_literal) - } Place::Defined( - Type::ClassLiteral(class_literal), + Type::ClassLiteral(ClassLiteral::Stmt(class_literal)), + _, + Definedness::AlwaysDefined, + _, + ) => Ok(class_literal), + Place::Defined( + Type::ClassLiteral(ClassLiteral::Stmt(class_literal)), _, Definedness::PossiblyUndefined, _, @@ -5229,7 +6641,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, @@ -5239,7 +6651,7 @@ impl KnownClass { _db: &'db dyn Db, _id: salsa::Id, _class: KnownClassArgument<'db>, - ) -> Option> { + ) -> Option> { None } @@ -5247,7 +6659,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) @@ -5283,7 +6695,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::Stmt(class))) .unwrap_or_else(Type::unknown) } @@ -5367,6 +6779,7 @@ impl KnownClass { | Self::Iterable | Self::Iterator | Self::Mapping + | Self::Sequence | Self::ProtocolMeta | Self::SupportsIndex => KnownModule::Typing, Self::TypeAliasType @@ -5506,6 +6919,7 @@ impl KnownClass { | Self::Iterable | Self::Iterator | Self::Mapping + | Self::Sequence | Self::NamedTupleFallback | Self::NamedTupleLike | Self::ConstraintSet @@ -5598,6 +7012,7 @@ impl KnownClass { | Self::Iterable | Self::Iterator | Self::Mapping + | Self::Sequence | Self::NamedTupleFallback | Self::NamedTupleLike | Self::ConstraintSet @@ -5662,6 +7077,7 @@ impl KnownClass { "Iterable" => &[Self::Iterable], "Iterator" => &[Self::Iterator], "Mapping" => &[Self::Mapping], + "Sequence" => &[Self::Sequence], "ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec], "ParamSpecArgs" => &[Self::ParamSpecArgs], "ParamSpecKwargs" => &[Self::ParamSpecKwargs], @@ -5805,6 +7221,7 @@ impl KnownClass { | Self::Iterable | Self::Iterator | Self::Mapping + | Self::Sequence | Self::ProtocolMeta | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), @@ -5882,7 +7299,7 @@ impl KnownClass { let bound_super = BoundSuperType::build( db, - Type::ClassLiteral(enclosing_class), + Type::ClassLiteral(ClassLiteral::Stmt(enclosing_class)), first_param, ) .unwrap_or_else(|err| { @@ -5983,6 +7400,73 @@ impl KnownClass { ))); } + KnownClass::Type => { + // Check for MRO and metaclass errors in three-argument type() calls. + if let Type::ClassLiteral(ClassLiteral::Functional(functional_class)) = + overload.return_type() + { + // Check for MRO errors + if let Err(error) = functional_class.try_mro(db) { + match error { + FunctionalMroError::DuplicateBases(duplicates) => { + if let Some(builder) = + context.report_lint(&DUPLICATE_BASE, call_expression) + { + builder.into_diagnostic(format_args!( + "Duplicate base class{} {} in class `{}`", + if duplicates.len() == 1 { "" } else { "es" }, + duplicates.iter().map(|base| base.display(db)).join(", "), + functional_class.name(db), + )); + } + } + FunctionalMroError::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 `[{}]`", + functional_class.name(db), + functional_class + .bases(db) + .iter() + .map(|base| base.display(db)) + .join(", ") + )); + } + } + } + } + + // Check for metaclass conflicts + if let Err(FunctionalMetaclassConflict { + metaclass1, + base1, + metaclass2, + base2, + }) = functional_class.try_metaclass(db) + { + if let Some(builder) = + context.report_lint(&CONFLICTING_METACLASS, call_expression) + { + 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 = functional_class.name(db), + metaclass1 = metaclass1.name(db), + base1 = base1.display(db), + metaclass2 = metaclass2.name(db), + base2 = base2.display(db), + )); + } + } + } + } + _ => {} } } @@ -5998,7 +7482,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: StmtClassLiteral<'db>, + }, } impl<'db> KnownClassLookupError<'db> { @@ -6097,7 +7583,7 @@ enum SlotsKind { } impl SlotsKind { - fn from(db: &dyn Db, base: ClassLiteral) -> Self { + fn from(db: &dyn Db, base: StmtClassLiteral) -> Self { let Place::Defined(slots_ty, _, bound, _) = base .own_class_member(db, base.inherited_generic_context(db), None, "__slots__") .inner diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 9dd74c72973bad..4515623d711b69 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -1,10 +1,11 @@ use crate::Db; use crate::types::class::CodeGeneratorKind; use crate::types::generics::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, + ApplyTypeMappingVisitor, ClassType, DynamicType, KnownClass, KnownInstanceType, + MaterializationKind, MroError, NormalizedVisitor, SpecialFormType, StmtClassLiteral, Type, TypeContext, TypeMapping, todo_type, }; @@ -91,7 +92,7 @@ impl<'db> ClassBase<'db> { pub(super) fn try_from_type( db: &'db dyn Db, ty: Type<'db>, - subclass: ClassLiteral<'db>, + subclass: StmtClassLiteral<'db>, ) -> Option { match ty { Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), @@ -102,10 +103,7 @@ impl<'db> ClassBase<'db> { { Self::try_from_type(db, todo_type!("GenericAlias instance"), subclass) } - Type::SubclassOf(subclass_of) => subclass_of - .subclass_of() - .into_dynamic() - .map(ClassBase::Dynamic), + Type::SubclassOf(subclass_of) => subclass_of.subclass_of().to_class_base(), Type::Intersection(inter) => { let valid_element = inter .positive(db) @@ -197,7 +195,12 @@ impl<'db> ClassBase<'db> { // A class inheriting from a newtype would make intuitive sense, but newtype // wrappers are just identity callables at runtime, so this sort of inheritance // doesn't work and isn't allowed. - | KnownInstanceType::NewType(_) => None, + | KnownInstanceType::NewType(_) + // Internal types, should never be a base. + | KnownInstanceType::TypingNamedTupleFieldsSchema(_) + | KnownInstanceType::CollectionsNamedTupleFieldsSchema(_) + | KnownInstanceType::CollectionsNamedTupleDefaultsSchema(_) + | KnownInstanceType::MakeDataclassFieldsSchema(_) => None, KnownInstanceType::TypeGenericAlias(_) => { Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) } @@ -239,7 +242,11 @@ impl<'db> ClassBase<'db> { | SpecialFormType::TypeOf | SpecialFormType::CallableTypeOf | SpecialFormType::AlwaysTruthy - | SpecialFormType::AlwaysFalsy => None, + | SpecialFormType::AlwaysFalsy + | SpecialFormType::TypingNamedTupleFieldsSchema + | SpecialFormType::CollectionsNamedTupleFieldsSchema + | SpecialFormType::CollectionsNamedTupleDefaultsSchema + | SpecialFormType::MakeDataclassFieldsSchema => None, SpecialFormType::Any => Some(Self::Dynamic(DynamicType::Any)), SpecialFormType::Unknown => Some(Self::unknown()), @@ -312,6 +319,15 @@ 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), + Self::Protocol | Self::Generic | Self::TypedDict => KnownClass::Type.to_instance(db), + } + } + fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, @@ -360,7 +376,11 @@ 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.stmt_class_literal(db) else { + // Functional classes can't have cyclic MRO since their bases must + // already exist at creation time. + return false; + }; class_literal .try_mro(db, specialization) .is_err_and(MroError::is_cycle) diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 0f1a23aae5d54c..6f73aaeb91410d 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, StmtClassLiteral, add_inferred_python_version_hint_to_diagnostic, }; use crate::diagnostic::did_you_mean; @@ -2733,12 +2733,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!( + if let Some(span) = class.header_span(db) { + sub_diagnostic.annotate(Annotation::primary(span).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#"); @@ -2907,7 +2907,7 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast: pub(crate) fn report_instance_layout_conflict( context: &InferContext, - class: ClassLiteral, + class: StmtClassLiteral, node: &ast::StmtClassDef, disjoint_bases: &IncompatibleBases, ) { @@ -2942,7 +2942,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_stmt() == Some(disjoint_base.class) { match disjoint_base.kind { DisjointBaseKind::DefinesSlots => { annotation = annotation.message(format_args!( @@ -3112,7 +3112,7 @@ pub(crate) fn report_invalid_argument_number_to_special_form( pub(crate) fn report_bad_argument_to_get_protocol_members( context: &InferContext, call: &ast::ExprCall, - class: ClassLiteral, + class: StmtClassLiteral, ) { let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, call) else { return; @@ -3165,9 +3165,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.stmt_class_literal(db) { + class_def_diagnostic.annotate(Annotation::primary(class_literal.header_span(db))); + } diagnostic.sub(class_def_diagnostic); } @@ -3225,10 +3225,11 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( but it is not declared as runtime-checkable" ), ); - class_def_diagnostic.annotate( - Annotation::primary(protocol.header_span(db)) - .message(format_args!("`{class_name}` declared here")), - ); + if let Some(span) = protocol.header_span(db) { + class_def_diagnostic.annotate( + Annotation::primary(span).message(format_args!("`{class_name}` declared here")), + ); + } diagnostic.sub(class_def_diagnostic); diagnostic.info(format_args!( @@ -3256,10 +3257,12 @@ pub(crate) fn report_attempted_protocol_instantiation( SubDiagnosticSeverity::Info, format_args!("Protocol classes cannot be instantiated"), ); - class_def_diagnostic.annotate( - Annotation::primary(protocol.header_span(db)) - .message(format_args!("`{class_name}` declared as a protocol here")), - ); + if let Some(span) = protocol.header_span(db) { + class_def_diagnostic.annotate( + Annotation::primary(span) + .message(format_args!("`{class_name}` declared as a protocol here")), + ); + } diagnostic.sub(class_def_diagnostic); } @@ -3344,10 +3347,12 @@ pub(crate) fn report_undeclared_protocol_member( "Assigning to an undeclared variable in a protocol class \ leads to an ambiguous interface", ); - class_def_diagnostic.annotate( - Annotation::primary(protocol_class.header_span(db)) - .message(format_args!("`{class_name}` declared as a protocol here",)), - ); + if let Some(span) = protocol_class.header_span(db) { + class_def_diagnostic.annotate( + Annotation::primary(span) + .message(format_args!("`{class_name}` declared as a protocol here",)), + ); + } diagnostic.sub(class_def_diagnostic); diagnostic.info(format_args!( @@ -3358,7 +3363,7 @@ pub(crate) fn report_undeclared_protocol_member( pub(crate) fn report_duplicate_bases( context: &InferContext, - class: ClassLiteral, + class: StmtClassLiteral, duplicate_base_error: &DuplicateBaseError, bases_list: &[ast::Expr], ) { @@ -3405,7 +3410,7 @@ pub(crate) fn report_invalid_or_unsupported_base( context: &InferContext, base_node: &ast::Expr, base_type: Type, - class: ClassLiteral, + class: StmtClassLiteral, ) { let db = context.db(); let instance_of_type = KnownClass::Type.to_instance(db); @@ -3515,7 +3520,7 @@ fn report_unsupported_base( context: &InferContext, base_node: &ast::Expr, base_type: Type, - class: ClassLiteral, + class: StmtClassLiteral, ) { let Some(builder) = context.report_lint(&UNSUPPORTED_BASE, base_node) else { return; @@ -3538,7 +3543,7 @@ fn report_invalid_base<'ctx, 'db>( context: &'ctx InferContext<'db, '_>, base_node: &ast::Expr, base_type: Type<'db>, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, ) -> Option> { let builder = context.report_lint(&INVALID_BASE, base_node)?; let mut diagnostic = builder.into_diagnostic(format_args!( @@ -3634,7 +3639,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: StmtClassLiteral<'db>, (field, field_def): (&str, Option>), (field_with_default, field_with_default_def): &(Name, Option>), ) { @@ -3683,7 +3688,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: StmtClassLiteral<'db>, field_name: &str, field_definition: Option>, ) { @@ -3807,7 +3812,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: StmtClassLiteral<'db>, node: &ast::StmtClassDef, typevar_with_default: TypeVarInstance<'db>, invalid_later_typevars: &[TypeVarInstance<'db>], @@ -3892,7 +3897,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: StmtClassLiteral<'db>, class_node: &ast::StmtClassDef, other_typevar: BoundTypeVarInstance<'db>, ) { @@ -3911,7 +3916,7 @@ pub(crate) fn report_rebound_typevar<'db>( return; }; let span = match binding_type(db, other_definition) { - Type::ClassLiteral(class) => Some(class.header_span(db)), + Type::ClassLiteral(class) => class.as_stmt().map(|stmt| stmt.header_span(db)), Type::FunctionLiteral(function) => function.spans(db).map(|spans| spans.signature), _ => return, }; @@ -3967,10 +3972,11 @@ 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), - ) + if let Some(qualified_name) = superclass.qualified_name(db) { + format!("{qualified_name}.{member}") + } else { + format!("{superclass_name}.{member}") + } } else { format!("{superclass_name}.{member}") }; @@ -4019,7 +4025,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.stmt_class_literal(db) else { + return; + }; + let superclass_scope = superclass_literal.body_scope(db); match superclass_method_kind { MethodKind::NotSynthesized => { @@ -4085,10 +4094,12 @@ pub(super) fn report_invalid_method_override<'db>( )), }; - sub.annotate( - Annotation::primary(superclass.header_span(db)) - .message(format_args!("Definition of `{superclass_name}`")), - ); + if let Some(span) = superclass.header_span(db) { + sub.annotate( + Annotation::primary(span) + .message(format_args!("Definition of `{superclass_name}`")), + ); + } diagnostic.sub(sub); } } @@ -4150,7 +4161,10 @@ pub(super) fn report_overridden_final_method<'db>( }; let superclass_name = if superclass.name(db) == subclass.name(db) { - superclass.qualified_name(db).to_string() + superclass + .qualified_name(db) + .map(|name| name.to_string()) + .unwrap_or_else(|| superclass.name(db).to_string()) } else { superclass.name(db).to_string() }; @@ -4206,9 +4220,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.stmt_class_literal(db) else { + return; + }; + let class_node = subclass_literal .body_scope(db) .node(db) .expect_class() @@ -4506,9 +4521,9 @@ fn report_unsupported_binary_operation_impl<'a>( pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( context: &InferContext<'db, '_>, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, class_node: &ast::StmtClassDef, - base_class: ClassLiteral<'db>, + base_class: StmtClassLiteral<'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 4b29d343a2e8ab..7db8b5f8790a1f 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -16,7 +16,8 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::Db; use crate::place::Place; use crate::semantic_index::definition::Definition; -use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; +use crate::types::class::{ClassLiteral, ClassType, GenericAlias, StmtClassLiteral}; +use crate::types::class_base::ClassBase; use crate::types::function::{FunctionType, OverloadLiteral}; use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::{ @@ -366,6 +367,9 @@ struct AmbiguousClassCollector<'db> { impl<'db> AmbiguousClassCollector<'db> { fn record_class(&self, db: &'db dyn Db, class: ClassLiteral<'db>) { + let ClassLiteral::Stmt(class) = class else { + return; + }; match self.class_names.borrow_mut().entry(class.name(db)) { Entry::Vacant(entry) => { entry.insert(AmbiguityState::Unambiguous(class)); @@ -413,10 +417,10 @@ impl<'db> AmbiguousClassCollector<'db> { #[derive(Debug, Clone, PartialEq, Eq)] enum AmbiguityState<'db> { /// The class can be displayed unambiguously using its unqualified name - Unambiguous(ClassLiteral<'db>), + Unambiguous(StmtClassLiteral<'db>), /// The class must be displayed using its fully qualified name to avoid ambiguity. RequiresFullyQualifiedName { - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, qualified_name_components: Vec, }, /// Even the class's fully qualified name is not sufficient; @@ -432,8 +436,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, ClassLiteral::Stmt(literal.enum_class(db))); + } + Type::GenericAlias(alias) => { + self.record_class(db, ClassLiteral::Stmt(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.) @@ -536,7 +544,7 @@ impl fmt::Debug for DisplayType<'_> { } } -impl<'db> ClassLiteral<'db> { +impl<'db> StmtClassLiteral<'db> { fn display_with(self, db: &'db dyn Db, settings: DisplaySettings<'db>) -> ClassDisplay<'db> { ClassDisplay { db, @@ -548,7 +556,7 @@ impl<'db> ClassLiteral<'db> { struct ClassDisplay<'db> { db: &'db dyn Db, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, settings: DisplaySettings<'db>, } @@ -556,7 +564,7 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { let qualification_level = self.settings.qualified.get(&**self.class.name(self.db)); - let ty = Type::ClassLiteral(self.class); + let ty = Type::ClassLiteral(ClassLiteral::Stmt(self.class)); if qualification_level.is_some() { write!(f.with_type(ty), "{}", self.class.qualified_name(self.db))?; } else { @@ -649,17 +657,35 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .expect("Specialization::tuple() should always return `Some()` for `KnownClass::Tuple`") .display_with(self.db, self.settings.clone()) .fmt_detailed(f), - (ClassType::NonGeneric(class), _) => { + (ClassType::NonGeneric(ClassLiteral::Stmt(class)), _) => { class.display_with(self.db, self.settings.clone()).fmt_detailed(f) - }, + } + (ClassType::NonGeneric(ClassLiteral::Functional(functional)), _) => { + f.with_type(self.ty).write_str(functional.name(self.db).as_str()) + } + (ClassType::NonGeneric(ClassLiteral::FunctionalNamedTuple(namedtuple)), _) => { + f.with_type(self.ty).write_str(namedtuple.name(self.db).as_str()) + } + (ClassType::NonGeneric(ClassLiteral::FunctionalDataclass(dataclass)), _) => { + f.with_type(self.ty).write_str(dataclass.name(self.db).as_str()) + } (ClassType::Generic(alias), _) => alias.display_with(self.db, self.settings.clone()).fmt_detailed(f), } } Type::ProtocolInstance(protocol) => match protocol.inner { Protocol::FromClass(class) => match *class { - ClassType::NonGeneric(class) => class + ClassType::NonGeneric(ClassLiteral::Stmt(class)) => class .display_with(self.db, self.settings.clone()) .fmt_detailed(f), + ClassType::NonGeneric(ClassLiteral::Functional(functional)) => f + .with_type(self.ty) + .write_str(functional.name(self.db).as_str()), + ClassType::NonGeneric(ClassLiteral::FunctionalNamedTuple(namedtuple)) => f + .with_type(self.ty) + .write_str(namedtuple.name(self.db).as_str()), + ClassType::NonGeneric(ClassLiteral::FunctionalDataclass(dataclass)) => f + .with_type(self.ty) + .write_str(dataclass.name(self.db).as_str()), ClassType::Generic(alias) => alias .display_with(self.db, self.settings.clone()) .fmt_detailed(f), @@ -698,9 +724,25 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.set_invalid_type_annotation(); let mut f = f.with_type(self.ty); f.write_str(" { + stmt_class + .display_with(self.db, self.settings.clone()) + .fmt_detailed(&mut f)?; + } + ClassLiteral::Functional(func_class) => { + // Functional classes don't have qualified names; just use the name. + f.write_str(func_class.name(self.db))?; + } + ClassLiteral::FunctionalNamedTuple(namedtuple) => { + // Functional namedtuples don't have qualified names; just use the name. + f.write_str(namedtuple.name(self.db))?; + } + ClassLiteral::FunctionalDataclass(dataclass) => { + // Functional dataclasses don't have qualified names; just use the name. + f.write_str(dataclass.name(self.db))?; + } + } f.write_str("'>") } Type::GenericAlias(generic) => { @@ -713,7 +755,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_str("'>") } Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { - SubclassOfInner::Class(ClassType::NonGeneric(class)) => { + SubclassOfInner::Class(ClassType::NonGeneric(ClassLiteral::Stmt(class))) => { f.with_type(KnownClass::Type.to_class_literal(self.db)) .write_str("type")?; f.write_char('[')?; @@ -722,6 +764,33 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .fmt_detailed(f)?; f.write_char(']') } + SubclassOfInner::Class(ClassType::NonGeneric(ClassLiteral::Functional( + functional, + ))) => { + f.with_type(KnownClass::Type.to_class_literal(self.db)) + .write_str("type")?; + f.write_char('[')?; + f.write_str(functional.name(self.db).as_str())?; + f.write_char(']') + } + SubclassOfInner::Class(ClassType::NonGeneric( + ClassLiteral::FunctionalNamedTuple(namedtuple), + )) => { + f.with_type(KnownClass::Type.to_class_literal(self.db)) + .write_str("type")?; + f.write_char('[')?; + f.write_str(namedtuple.name(self.db).as_str())?; + f.write_char(']') + } + SubclassOfInner::Class(ClassType::NonGeneric( + ClassLiteral::FunctionalDataclass(dataclass), + )) => { + f.with_type(KnownClass::Type.to_class_literal(self.db)) + .write_str("type")?; + f.write_char('[')?; + f.write_str(dataclass.name(self.db).as_str())?; + f.write_char(']') + } SubclassOfInner::Class(ClassType::Generic(alias)) => { f.with_type(KnownClass::Type.to_class_literal(self.db)) .write_str("type")?; @@ -984,9 +1053,22 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { Type::BoundSuper(bound_super) => { f.set_invalid_type_annotation(); f.write_str(" { + f.with_type(self.ty).write_str(fc.name(self.db))?; + } + ClassBase::Class(ClassType::NonGeneric(ClassLiteral::FunctionalDataclass( + dc, + ))) => { + f.with_type(self.ty).write_str(dc.name(self.db))?; + } + pivot => { + Type::from(pivot) + .display_with(self.db, self.settings.singleline()) + .fmt_detailed(f)?; + } + } f.write_str(", ")?; Type::from(bound_super.owner(self.db)) .display_with(self.db, self.settings.singleline()) @@ -998,9 +1080,18 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { fmt_type_guard_like(self.db, type_guard, &self.settings, f) } Type::TypedDict(TypedDictType::Class(defining_class)) => match defining_class { - ClassType::NonGeneric(class) => class + ClassType::NonGeneric(ClassLiteral::Stmt(class)) => class .display_with(self.db, self.settings.clone()) .fmt_detailed(f), + ClassType::NonGeneric(ClassLiteral::Functional(functional)) => f + .with_type(self.ty) + .write_str(functional.name(self.db).as_str()), + ClassType::NonGeneric(ClassLiteral::FunctionalNamedTuple(namedtuple)) => f + .with_type(self.ty) + .write_str(namedtuple.name(self.db).as_str()), + ClassType::NonGeneric(ClassLiteral::FunctionalDataclass(dataclass)) => f + .with_type(self.ty) + .write_str(dataclass.name(self.db).as_str()), ClassType::Generic(alias) => alias .display_with(self.db, self.settings.clone()) .fmt_detailed(f), @@ -1294,7 +1385,7 @@ impl<'db> GenericAlias<'db> { } pub(crate) struct DisplayGenericAlias<'db> { - origin: ClassLiteral<'db>, + origin: StmtClassLiteral<'db>, specialization: Specialization<'db>, db: &'db dyn Db, settings: DisplaySettings<'db>, @@ -1571,7 +1662,7 @@ impl TupleSpecialization { matches!(self, Self::Yes) } - fn from_class(db: &dyn Db, class: ClassLiteral) -> Self { + fn from_class(db: &dyn Db, class: StmtClassLiteral) -> Self { if class.is_tuple(db) { Self::Yes } else { @@ -2550,6 +2641,64 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.with_type(ty).write_str(declaration.name(self.db))?; f.write_str("'>") } + KnownInstanceType::TypingNamedTupleFieldsSchema(schema) => { + f.set_invalid_type_annotation(); + f.write_str("") + } + KnownInstanceType::CollectionsNamedTupleFieldsSchema(schema) => { + f.set_invalid_type_annotation(); + f.write_str("") + } + KnownInstanceType::CollectionsNamedTupleDefaultsSchema(schema) => { + f.set_invalid_type_annotation(); + write!( + f, + "", + schema.count(self.db) + ) + } + KnownInstanceType::MakeDataclassFieldsSchema(schema) => { + f.set_invalid_type_annotation(); + f.write_str("") + } } } } diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index f91a4567cfb54e..8f18409c53827b 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -7,7 +7,7 @@ use crate::{ semantic_index::{place_table, use_def_map}, types::{ ClassBase, ClassLiteral, DynamicType, EnumLiteralType, KnownClass, MemberLookupPolicy, - Type, TypeQualifiers, + StmtClassLiteral, Type, TypeQualifiers, }, }; @@ -40,7 +40,7 @@ impl EnumMetadata<'_> { fn enum_metadata_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, - _class: ClassLiteral<'db>, + _class: StmtClassLiteral<'db>, ) -> Option> { Some(EnumMetadata::empty()) } @@ -50,7 +50,7 @@ fn enum_metadata_cycle_initial<'db>( #[salsa::tracked(returns(as_ref), cycle_initial=enum_metadata_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn enum_metadata<'db>( db: &'db dyn Db, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, ) -> Option> { // This is a fast path to avoid traversing the MRO of known classes if class @@ -136,7 +136,7 @@ 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) + let auto_value_ty = if Type::ClassLiteral(ClassLiteral::Stmt(class)) .is_subtype_of(db, KnownClass::StrEnum.to_subclass_of(db)) { Type::string_literal(db, &name.to_lowercase()) @@ -265,7 +265,7 @@ pub(crate) fn enum_metadata<'db>( pub(crate) fn enum_member_literals<'a, 'db: 'a>( db: &'db dyn Db, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, exclude_member: Option<&'a Name>, ) -> Option> + 'a> { enum_metadata(db, class).map(|metadata| { @@ -277,13 +277,15 @@ pub(crate) fn enum_member_literals<'a, 'db: 'a>( }) } -pub(crate) fn is_single_member_enum<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> bool { +pub(crate) fn is_single_member_enum<'db>(db: &'db dyn Db, class: StmtClassLiteral<'db>) -> bool { enum_metadata(db, class).is_some_and(|metadata| metadata.members.len() == 1) } pub(crate) fn is_enum_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { match ty { - Type::ClassLiteral(class_literal) => enum_metadata(db, class_literal).is_some(), + Type::ClassLiteral(class_literal) => class_literal + .as_stmt() + .is_some_and(|stmt| enum_metadata(db, stmt).is_some()), _ => false, } } @@ -293,8 +295,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: StmtClassLiteral<'db>, +) -> bool { + Type::ClassLiteral(ClassLiteral::Stmt(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 19f82ae177bbd6..76a8aa6a775ac6 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1221,10 +1221,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; } @@ -1410,6 +1407,9 @@ pub enum KnownFunction { Dataclass, /// `dataclasses.field` Field, + /// `dataclasses.make_dataclass` + #[strum(serialize = "make_dataclass")] + MakeDataclass, /// `inspect.getattr_static` GetattrStatic, @@ -1496,7 +1496,7 @@ impl KnownFunction { Self::AsyncContextManager => { matches!(module, KnownModule::Contextlib) } - Self::Dataclass | Self::Field => { + Self::Dataclass | Self::Field | Self::MakeDataclass => { matches!(module, KnownModule::Dataclasses) } Self::GetattrStatic => module.is_inspect(), @@ -1729,7 +1729,13 @@ impl KnownFunction { if class.is_protocol(db) { return; } - report_bad_argument_to_get_protocol_members(context, call_expression, *class); + if let Some(stmt_class) = class.as_stmt() { + report_bad_argument_to_get_protocol_members( + context, + call_expression, + stmt_class, + ); + } } KnownFunction::RevealProtocolInterface => { @@ -1857,9 +1863,16 @@ impl KnownFunction { } if self == KnownFunction::IsInstance { - overload.set_return_type( - is_instance_truthiness(db, *first_arg, *class).into_type(db), - ); + if let Some(stmt_class) = class.as_stmt() { + overload.set_return_type( + is_instance_truthiness( + db, + *first_arg, + ClassLiteral::Stmt(stmt_class), + ) + .into_type(db), + ); + } } } // The special-casing here is necessary because we recognise the symbol `typing.Any` as an @@ -2028,7 +2041,9 @@ pub(crate) mod tests { KnownFunction::AsyncContextManager => KnownModule::Contextlib, - KnownFunction::Dataclass | KnownFunction::Field => KnownModule::Dataclasses, + KnownFunction::Dataclass | KnownFunction::Field | KnownFunction::MakeDataclass => { + KnownModule::Dataclasses + } KnownFunction::GetattrStatic => KnownModule::Inspect, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 1117096086c76f..cb804777cca498 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -20,9 +20,9 @@ use crate::types::variance::VarianceInferable; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, - ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IntersectionType, - IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, - NormalizedVisitor, Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, + FindLegacyTypeVarsVisitor, HasRelationToVisitor, IntersectionType, IsDisjointVisitor, + IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, + StmtClassLiteral, Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_type_var_bounds, }; @@ -86,7 +86,7 @@ pub(crate) fn typing_self<'db>( db: &'db dyn Db, function_scope_id: ScopeId, typevar_binding_context: Option>, - class: ClassLiteral<'db>, + class: StmtClassLiteral<'db>, ) -> Option> { let index = semantic_index(db, function_scope_id.file(db)); diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index daf00cb2b53ca1..d559151e8c3b2c 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; @@ -168,9 +169,9 @@ pub fn definitions_for_name<'db>( // instead of `int` (hover only shows the docstring of the first definition). .rev() .filter_map(|ty| ty.as_nominal_instance()) - .map(|instance| { - let definition = instance.class_literal(db).definition(db); - ResolvedDefinition::Definition(definition) + .filter_map(|instance| { + let definition = instance.class_literal(db)?.definition(db); + Some(ResolvedDefinition::Definition(definition)) }) .collect(); } @@ -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.stmt_class_literal(db) { + Some((lit, _)) => ClassLiteral::Stmt(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.stmt_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 3a1f4425928c97..cf85270f17b5b3 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, StmtClassLiteral, 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_stmt) }) } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 453eb9a15e1dcc..56c8a4376061a3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -52,7 +52,9 @@ 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, FieldKind, MetaclassErrorKind, MethodDecorator, +}; use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::cyclic::CycleDetector; use crate::types::diagnostic::{ @@ -106,15 +108,16 @@ 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, + CallableTypeKind, ClassType, CollectionsNamedTupleDefaultsSchema, + CollectionsNamedTupleFieldsSchema, 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, - TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, - TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types, - todo_type, + LintDiagnosticGuard, MakeDataclassFieldsSchema, MemberLookupPolicy, MetaclassCandidate, + PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, + SpecialFormType, StmtClassLiteral, SubclassOfType, TrackedConstraintSet, Truthiness, Type, + TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, + TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, TypingNamedTupleFieldsSchema, + UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types, todo_type, }; use crate::types::{CallableTypes, overrides}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; @@ -598,6 +601,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let DefinitionKind::Class(class) = definition.kind(self.db()) { ty.inner_type() .as_class_literal() + .and_then(ClassLiteral::as_stmt) .map(|class_literal| (class_literal, class.node(self.module()))) } else { None @@ -728,7 +732,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 @@ -759,24 +763,24 @@ 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()), - ) { - let base_params = base_params.flags(self.db()); - let class_is_frozen = class_params.flags(self.db()).is_frozen(); + if let Some((base_class_literal, _)) = base_class.stmt_class_literal(self.db()) { + if 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(); - if base_params.is_frozen() != class_is_frozen { - report_bad_frozen_dataclass_inheritance( - &self.context, - class, - class_node, - base_class_literal, - &class_node.bases()[i], - base_params, - ); + if base_params.is_frozen() != class_is_frozen { + report_bad_frozen_dataclass_inheritance( + &self.context, + class, + class_node, + base_class_literal, + &class_node.bases()[i], + base_params, + ); + } } } } @@ -1167,7 +1171,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .expect_class_literal(); if class.is_protocol(self.db()) - || (Type::ClassLiteral(class) + || (Type::ClassLiteral(ClassLiteral::Stmt(class)) .is_subtype_of(self.db(), KnownClass::ABCMeta.to_instance(self.db())) && overloads.iter().all(|overload| { overload.has_known_decorator( @@ -2696,7 +2700,8 @@ 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_stmt()?; let typing_self = typing_self(db, self.scope(), Some(method_definition), class_literal); if is_classmethod { @@ -2873,7 +2878,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(StmtClassLiteral::new( self.db(), name.id.clone(), body_scope, @@ -4168,11 +4173,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If it's a user-defined class, suggest adding a `__setitem__` method. if object_ty .as_nominal_instance() - .and_then(|instance| { - file_to_module( - db, - instance.class(db).class_literal(db).0.file(db), - ) + .and_then(|instance| instance.class(db).stmt_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) @@ -4509,8 +4512,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Check if class-level attribute already has a value - { - let class_definition = class_ty.class_literal(db).0; + if let Some((class_definition, _)) = class_ty.stmt_class_literal(db) { let class_scope_id = class_definition.body_scope(db).file_scope_id(db); let place_table = builder.index.place_table(class_scope_id); @@ -5785,6 +5787,122 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ))) } + /// Extract fields from a list or tuple literal for `typing.NamedTuple`. + fn infer_typing_namedtuple_fields_schema( + &mut self, + field_elts: &[ast::Expr], + ) -> Option> { + let db = self.db(); + let mut fields: Vec<(ast::name::Name, Type<'db>)> = Vec::with_capacity(field_elts.len()); + + for elt in field_elts { + // Each element should be a tuple like ("field_name", type). + let ast::Expr::Tuple(tuple_expr) = elt else { + return None; + }; + + if tuple_expr.elts.len() != 2 { + return None; + } + + // First element: field name (string literal). + let field_name_expr = &tuple_expr.elts[0]; + let field_name_ty = self.infer_expression(field_name_expr, TypeContext::default()); + let field_name_lit = field_name_ty.as_string_literal()?; + let field_name = ast::name::Name::new(field_name_lit.value(db)); + + // Second element: field type. + let field_type_expr = &tuple_expr.elts[1]; + let field_type_ty = self.infer_type_expression(field_type_expr); + + fields.push((field_name, field_type_ty)); + } + + Some(TypingNamedTupleFieldsSchema::new( + db, + fields.into_boxed_slice(), + )) + } + + /// Extract field names from a list or tuple literal for `collections.namedtuple`. + fn infer_collections_namedtuple_fields_schema( + &mut self, + field_elts: &[ast::Expr], + ) -> Option> { + let db = self.db(); + let mut field_names: Vec = Vec::with_capacity(field_elts.len()); + + for elt in field_elts { + // Each element should be a string literal (field name). + let field_ty = self.infer_expression(elt, TypeContext::default()); + let field_lit = field_ty.as_string_literal()?; + field_names.push(ast::name::Name::new(field_lit.value(db))); + } + + Some(CollectionsNamedTupleFieldsSchema::new( + db, + field_names.into_boxed_slice(), + )) + } + + /// Extract fields from a list or tuple literal for `dataclasses.make_dataclass`. + /// + /// Each element can be: + /// - A string literal (field name only, type is Any) + /// - A 2-tuple of (name, type) + /// - A 3-tuple of (name, type, field) + fn infer_make_dataclass_fields_schema( + &mut self, + field_elts: &[ast::Expr], + ) -> Option> { + let db = self.db(); + let mut fields: Vec<(ast::name::Name, Type<'db>, Option>)> = + Vec::with_capacity(field_elts.len()); + + for elt in field_elts { + // Handle string literal (field name only, type is Any). + if let ast::Expr::StringLiteral(string_lit) = elt { + let field_name = ast::name::Name::new(string_lit.value.to_str()); + fields.push((field_name, Type::any(), None)); + continue; + } + + // Handle tuple: (name, type) or (name, type, field). + let ast::Expr::Tuple(tuple_expr) = elt else { + return None; + }; + + if tuple_expr.elts.len() < 2 { + return None; + } + + // First element: field name (string literal). + let field_name_expr = &tuple_expr.elts[0]; + let field_name_ty = self.infer_expression(field_name_expr, TypeContext::default()); + let field_name_lit = field_name_ty.as_string_literal()?; + let field_name = ast::name::Name::new(field_name_lit.value(db)); + + // Second element: field type annotation. + let field_type_expr = &tuple_expr.elts[1]; + let field_type_ty = self.infer_type_expression(field_type_expr); + + // Third element (optional): default value or field() specification. + let default_ty = if tuple_expr.elts.len() >= 3 { + let default_expr = &tuple_expr.elts[2]; + Some(self.infer_expression(default_expr, TypeContext::default())) + } else { + None + }; + + fields.push((field_name, field_type_ty, default_ty)); + } + + Some(MakeDataclassFieldsSchema::new( + db, + fields.into_boxed_slice(), + )) + } + fn infer_assignment_deferred(&mut self, value: &ast::Expr) { // Infer deferred bounds/constraints/defaults of a legacy TypeVar / ParamSpec / NewType. let ast::Expr::Call(ast::ExprCall { @@ -5999,7 +6117,8 @@ 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_stmt()?; class_literal .dataclass_params(db) @@ -7786,6 +7905,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parenthesized: _, } = tuple; + // Extract fields for typing.NamedTuple. + if let Some(Type::SpecialForm(SpecialFormType::TypingNamedTupleFieldsSchema)) = + tcx.annotation + { + if let Some(schema) = self.infer_typing_namedtuple_fields_schema(elts) { + return Type::KnownInstance(KnownInstanceType::TypingNamedTupleFieldsSchema( + schema, + )); + } + } + + if let Some(Type::SpecialForm(SpecialFormType::CollectionsNamedTupleFieldsSchema)) = + tcx.annotation + { + if let Some(schema) = self.infer_collections_namedtuple_fields_schema(elts) { + return Type::KnownInstance(KnownInstanceType::CollectionsNamedTupleFieldsSchema( + schema, + )); + } + } + + // Extract defaults count for collections.namedtuple. + if let Some(Type::SpecialForm(SpecialFormType::CollectionsNamedTupleDefaultsSchema)) = + tcx.annotation + { + for elt in elts { + self.infer_expression(elt, TypeContext::default()); + } + let schema = CollectionsNamedTupleDefaultsSchema::new(self.db(), elts.len()); + return Type::KnownInstance(KnownInstanceType::CollectionsNamedTupleDefaultsSchema( + schema, + )); + } + + if let Some(Type::SpecialForm(SpecialFormType::MakeDataclassFieldsSchema)) = tcx.annotation + { + if let Some(schema) = self.infer_make_dataclass_fields_schema(elts) { + return Type::KnownInstance(KnownInstanceType::MakeDataclassFieldsSchema(schema)); + } + } + // Remove any union elements of that are unrelated to the tuple type. let tcx = tcx.map(|annotation| { let inferable = KnownClass::Tuple @@ -7834,6 +7994,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ctx: _, } = list; + // Extract fields for typing.NamedTuple. + if let Some(Type::SpecialForm(SpecialFormType::TypingNamedTupleFieldsSchema)) = + tcx.annotation + { + if let Some(schema) = self.infer_typing_namedtuple_fields_schema(elts) { + return Type::KnownInstance(KnownInstanceType::TypingNamedTupleFieldsSchema( + schema, + )); + } + } + + if let Some(Type::SpecialForm(SpecialFormType::CollectionsNamedTupleFieldsSchema)) = + tcx.annotation + { + if let Some(schema) = self.infer_collections_namedtuple_fields_schema(elts) { + return Type::KnownInstance(KnownInstanceType::CollectionsNamedTupleFieldsSchema( + schema, + )); + } + } + + // Extract defaults count for collections.namedtuple. + if let Some(Type::SpecialForm(SpecialFormType::CollectionsNamedTupleDefaultsSchema)) = + tcx.annotation + { + for elt in elts { + self.infer_expression(elt, TypeContext::default()); + } + let schema = CollectionsNamedTupleDefaultsSchema::new(self.db(), elts.len()); + return Type::KnownInstance(KnownInstanceType::CollectionsNamedTupleDefaultsSchema( + schema, + )); + } + + if let Some(Type::SpecialForm(SpecialFormType::MakeDataclassFieldsSchema)) = tcx.annotation + { + if let Some(schema) = self.infer_make_dataclass_fields_schema(elts) { + return Type::KnownInstance(KnownInstanceType::MakeDataclassFieldsSchema(schema)); + } + } + let elts = elts.iter().map(|elt| [Some(elt)]); let infer_elt_ty = |builder: &mut Self, elt, tcx| builder.infer_expression(elt, tcx); self.infer_collection_literal(elts, tcx, infer_elt_ty, KnownClass::List) @@ -8808,11 +9009,11 @@ 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 + .stmt_class_literal(self.db()) + .is_some_and(|(class_literal, specialization)| { + CodeGeneratorKind::TypedDict.matches(self.db(), class_literal, specialization) + }); // temporary special-casing for all subclasses of `enum.Enum` // until we support the functional syntax for creating enum classes @@ -11652,12 +11853,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if let Some(generic_context) = class.generic_context(self.db()) { - return self.infer_explicit_class_specialization( - subscript, - value_ty, - class, - generic_context, - ); + if let Some(stmt_class) = class.as_stmt() { + return self.infer_explicit_class_specialization( + subscript, + value_ty, + stmt_class, + generic_context, + ); + } } } Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695( @@ -11988,7 +12191,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, - generic_class: ClassLiteral<'db>, + generic_class: StmtClassLiteral<'db>, generic_context: GenericContext<'db>, ) -> Type<'db> { let db = self.db(); @@ -12702,7 +12905,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 f5cf054bd8e6a4..05e9a005b9c3d7 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 @@ -1033,6 +1033,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } + // Internal types, should never appear in user code. + KnownInstanceType::TypingNamedTupleFieldsSchema(_) + | KnownInstanceType::CollectionsNamedTupleFieldsSchema(_) + | KnownInstanceType::CollectionsNamedTupleDefaultsSchema(_) + | KnownInstanceType::MakeDataclassFieldsSchema(_) => { + self.infer_type_expression(&subscript.slice); + Type::unknown() + } }, Type::Dynamic(DynamicType::UnknownGeneric(_)) => { self.infer_explicit_type_alias_specialization(subscript, value_ty, true) @@ -1045,12 +1053,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_stmt()) { + (Some(generic_context), Some(stmt_class)) => { let specialized_class = self.infer_explicit_class_specialization( subscript, value_ty, - class, + stmt_class, generic_context, ); @@ -1062,7 +1070,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") @@ -1588,7 +1596,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { | SpecialFormType::TypedDict | SpecialFormType::Unknown | SpecialFormType::Any - | SpecialFormType::NamedTuple => { + | SpecialFormType::NamedTuple + | SpecialFormType::TypingNamedTupleFieldsSchema + | SpecialFormType::CollectionsNamedTupleFieldsSchema + | SpecialFormType::CollectionsNamedTupleDefaultsSchema + | SpecialFormType::MakeDataclassFieldsSchema => { self.infer_type_expression(arguments_slice); if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index fbad5fbcdb2f4a..9c73ca1649a9d2 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -13,8 +13,8 @@ use crate::types::generics::{InferableTypeVars, walk_specialization}; use crate::types::protocol_class::{ProtocolClass, walk_protocol_interface}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::{ - ApplyTypeMappingVisitor, ClassBase, ClassLiteral, FindLegacyTypeVarsVisitor, - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, TypeContext, + ApplyTypeMappingVisitor, ClassBase, FindLegacyTypeVarsVisitor, HasRelationToVisitor, + IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, StmtClassLiteral, TypeContext, TypeMapping, TypeRelation, VarianceInferable, }; use crate::{Db, FxOrderSet}; @@ -34,7 +34,12 @@ impl<'db> Type<'db> { } pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { - let (class_literal, specialization) = class.class_literal(db); + let Some((class_literal, specialization)) = class.stmt_class_literal(db) else { + // Functional classes don't have a class literal; just return a nominal instance. + return Type::NominalInstance(NominalInstanceType(NominalInstanceInner::NonTuple( + class, + ))); + }; match class_literal.known(db) { Some(KnownClass::Tuple) => Type::tuple(TupleType::new( db, @@ -227,18 +232,21 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn class_literal(&self, db: &'db dyn Db) -> ClassLiteral<'db> { + /// Returns the statement-defined class literal for this instance, if there is one. + pub(super) fn class_literal(&self, db: &'db dyn Db) -> Option> { 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`"); + return Some( + 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 + let (class_literal, _) = class.stmt_class_literal(db)?; + Some(class_literal) } /// Returns the [`KnownClass`] that this is a nominal instance of, or `None` if it is not an @@ -277,7 +285,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))) @@ -339,10 +347,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.stmt_class_literal(db)?; + let specialization = specialization?; + if !class_literal.is_known(db, KnownClass::Slice) { return None; } let [start, stop, step] = specialization.types(db) else { @@ -482,8 +489,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)), + ) }) } @@ -498,7 +510,11 @@ 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(|| { + class + .stmt_class_literal(db) + .is_some_and(|(lit, _)| is_single_member_enum(db, lit)) + }), } } @@ -510,7 +526,11 @@ 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(|| { + class + .stmt_class_literal(db) + .is_some_and(|(lit, _)| is_single_member_enum(db, lit)) + }), } } @@ -623,7 +643,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.stmt_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 63fcaad7e94fd9..de5c1c588d37aa 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -20,8 +20,8 @@ use crate::{ semantic_index, use_def_map, }, types::{ - ClassBase, ClassLiteral, KnownClass, KnownInstanceType, SubclassOfInner, Type, - TypeVarBoundOrConstraints, class::CodeGeneratorKind, generics::Specialization, + ClassBase, ClassLiteral, KnownClass, KnownInstanceType, StmtClassLiteral, SubclassOfInner, + Type, TypeVarBoundOrConstraints, class::CodeGeneratorKind, generics::Specialization, }, }; @@ -181,9 +181,16 @@ 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.stmt_class_literal(db) { + self.extend_with_instance_members(db, ty, class_literal); + self.extend_with_synthetic_members( + db, + ty, + ClassLiteral::Stmt(class_literal), + specialization, + ); + } } Type::NewTypeInstance(newtype) => { @@ -212,8 +219,8 @@ 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::Stmt(class_literal)); + self.extend_with_synthetic_members(db, ty, ClassLiteral::Stmt(class_literal), None); if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { self.extend_with_class_members(db, ty, metaclass); } @@ -225,11 +232,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.stmt_class_literal(db) + { + self.extend_with_class_members( + db, + ty, + ClassLiteral::Stmt(class_literal), + ); + self.extend_with_synthetic_members( + db, + ty, + ClassLiteral::Stmt(class_literal), + specialization, + ); + if let Type::ClassLiteral(metaclass) = class_literal.metaclass(db) { + self.extend_with_class_members(db, ty, metaclass); + } } } } @@ -289,12 +308,18 @@ impl<'db> AllMembers<'db> { } 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_literal, _)) = class.stmt_class_literal(db) { + self.extend_with_class_members( + db, + ty, + ClassLiteral::Stmt(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::Stmt(class_literal)); } _ => {} }, @@ -304,7 +329,7 @@ impl<'db> AllMembers<'db> { self.extend_with_class_members(db, ty, class_literal); } - if let Type::ClassLiteral(class) = + if let Type::ClassLiteral(ClassLiteral::Stmt(class)) = KnownClass::TypedDictFallback.to_class_literal(db) { self.extend_with_instance_members(db, ty, class); @@ -410,9 +435,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.stmt_class_literal(db).map(|(lit, _)| lit)) { let parent_scope = parent.body_scope(db); for memberdef in all_end_of_scope_members(db, parent_scope) { @@ -428,52 +453,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: StmtClassLiteral<'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: StmtClassLiteral<'db>, + ) { + for class in class_literal + .iter_mro(db, None) + .filter_map(ClassBase::into_class) + { + if let Some((class_literal, _)) = class.stmt_class_literal(db) { + self.extend_with_instance_members_for_class(db, ty, class_literal); + } + } } fn extend_with_synthetic_members( @@ -483,7 +520,10 @@ impl<'db> AllMembers<'db> { class_literal: ClassLiteral<'db>, specialization: Option>, ) { - match CodeGeneratorKind::from_class(db, class_literal, specialization) { + let Some(stmt_class) = class_literal.as_stmt() else { + return; + }; + match CodeGeneratorKind::from_class(db, stmt_class, specialization) { Some(CodeGeneratorKind::NamedTuple) => { if ty.is_nominal_instance() { self.extend_with_type(db, KnownClass::NamedTupleFallback.to_instance(db)); diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 21501060dafd62..64f42ade3161e0 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, FunctionalClassLiteral, KnownClass, KnownInstanceType, + SpecialFormType, StmtClassLiteral, 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 [`StmtClassLiteral`].) 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][StmtClassLiteral], 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,9 +49,9 @@ 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_stmt_class( db: &'db dyn Db, - class_literal: ClassLiteral<'db>, + class_literal: StmtClassLiteral<'db>, specialization: Option>, ) -> Result> { let class = class_literal.apply_optional_specialization(db, specialization); @@ -69,6 +72,81 @@ impl<'db> Mro<'db> { ]) } + /// Attempt to resolve the MRO of a functional 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_functional_class( + db: &'db dyn Db, + functional: FunctionalClassLiteral<'db>, + ) -> Result> { + let bases = functional.bases(db); + + // Check for duplicate bases first. + let mut seen = FxHashSet::default(); + let mut duplicates = Vec::new(); + for base in bases { + if !seen.insert(*base) { + duplicates.push(*base); + } + } + if !duplicates.is_empty() { + return Err(FunctionalMroError::DuplicateBases( + duplicates.into_boxed_slice(), + )); + } + + // Compute MRO using C3 linearization. + let mro_bases = if bases.is_empty() { + // Empty bases: MRO is just `object`. + vec![ClassBase::object(db)] + } else if bases.len() == 1 { + // Single base: MRO is just that base's MRO. + 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()) + .ok_or(FunctionalMroError::UnresolvableMro)? + }; + + let mut result = vec![ClassBase::Class(ClassType::NonGeneric(functional.into()))]; + result.extend(mro_bases); + Ok(Self::from(result)) + } + + /// Compute a fallback MRO for a functional class when `of_functional_class` fails. + /// + /// Iterates over base MROs sequentially with deduplication. + pub(super) fn functional_fallback( + db: &'db dyn Db, + functional: FunctionalClassLiteral<'db>, + ) -> Self { + let self_base = ClassBase::Class(ClassType::NonGeneric(functional.into())); + let mut result = vec![self_base]; + let mut seen = FxHashSet::default(); + seen.insert(self_base); + + for base in functional.bases(db) { + for item in base.mro(db, None) { + if seen.insert(item) { + result.push(item); + } + } + } + + Self::from(result) + } + fn of_class_impl( db: &'db dyn Db, class: ClassType<'db>, @@ -156,7 +234,10 @@ impl<'db> Mro<'db> { ) ) => { - ClassBase::try_from_type(db, *single_base, class.class_literal(db).0).map_or_else( + let Some((class_literal, _)) = class.stmt_class_literal(db) else { + return Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))); + }; + ClassBase::try_from_type(db, *single_base, class_literal).map_or_else( || Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))), |single_base| { if single_base.has_cyclic_mro(db) { @@ -176,6 +257,11 @@ impl<'db> Mro<'db> { // what MRO Python will give this class at runtime // (if an MRO is indeed resolvable at all!) _ => { + let Some((class_literal, _)) = class.stmt_class_literal(db) else { + // Functional classes don't have explicit bases to resolve. + return Ok(std::iter::once(ClassBase::Class(class)).collect()); + }; + let mut resolved_bases = vec![]; let mut invalid_bases = vec![]; @@ -191,7 +277,7 @@ 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, class_literal) { Some(valid_base) => resolved_bases.push(valid_base), None => invalid_bases.push((i, *base)), } @@ -258,9 +344,7 @@ 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, class_literal) else { continue; }; base_to_indices.entry(base).or_default().push(index); @@ -354,8 +438,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 [`StmtClassLiteral::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 @@ -372,9 +456,33 @@ pub(super) struct MroIterator<'db> { /// The full MRO is expensive to materialize, so this field is `None` /// unless we actually *need* to iterate past the first element of the MRO, /// at which point it is lazily materialized. - subsequent_elements: Option>>, + subsequent_elements: Option>, +} + +/// The subsequent elements of an MRO (everything after the first element). +/// +/// For statement-defined classes, we borrow from the cached MRO via `returns(as_ref)`. +/// For functional classes, we clone the MRO since the cache returns an owned value. +enum SubsequentMroElements<'db> { + /// Iterator over a borrowed MRO slice (for statement-defined classes). + Borrowed(std::slice::Iter<'db, ClassBase<'db>>), + /// Iterator over an owned MRO (for functional classes). + Owned(std::vec::IntoIter>), } +impl<'db> Iterator for SubsequentMroElements<'db> { + type Item = ClassBase<'db>; + + fn next(&mut self) -> Option { + match self { + Self::Borrowed(iter) => iter.next().copied(), + Self::Owned(iter) => iter.next(), + } + } +} + +impl std::iter::FusedIterator for SubsequentMroElements<'_> {} + impl<'db> MroIterator<'db> { pub(super) fn new( db: &'db dyn Db, @@ -390,19 +498,70 @@ impl<'db> MroIterator<'db> { } } + fn first_element(&self) -> ClassBase<'db> { + match self.class { + ClassLiteral::Stmt(stmt) => { + ClassBase::Class(stmt.apply_optional_specialization(self.db, self.specialization)) + } + ClassLiteral::Functional(functional) => { + ClassBase::Class(ClassType::NonGeneric(functional.into())) + } + ClassLiteral::FunctionalNamedTuple(namedtuple) => { + ClassBase::Class(ClassType::NonGeneric(namedtuple.into())) + } + ClassLiteral::FunctionalDataclass(dataclass) => { + ClassBase::Class(ClassType::NonGeneric(dataclass.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 SubsequentMroElements<'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::Stmt(stmt) => { + let mut full_mro_iter = match stmt.try_mro(self.db, self.specialization) { + Ok(mro) => mro.iter(), + Err(error) => error.fallback_mro().iter(), + }; + full_mro_iter.next(); + SubsequentMroElements::Borrowed(full_mro_iter) + } + ClassLiteral::Functional(functional) => { + let mro = functional.mro(self.db); + let elements: Vec<_> = mro.iter().skip(1).copied().collect(); + SubsequentMroElements::Owned(elements.into_iter()) + } + ClassLiteral::FunctionalNamedTuple(namedtuple) => { + // Functional namedtuples inherit from their tuple base type. + // For example, `NamedTuple("Point", [("x", int), ("y", str)])` has the MRO: + // `[Point, tuple[int, str], tuple, object]`. + let tuple_base = namedtuple.tuple_base_type(self.db); + let elements: Vec<_> = tuple_base.iter_mro(self.db).collect(); + SubsequentMroElements::Owned(elements.into_iter()) + } + ClassLiteral::FunctionalDataclass(dataclass) => { + // Dataclasses inherit from their base classes plus object. + // For simplicity, we just use object's MRO if no bases. + let bases = dataclass.bases(self.db); + if bases.is_empty() { + let object_class = KnownClass::Object + .to_class_literal(self.db) + .as_class_literal() + .expect("Object should be a class literal"); + let elements: Vec<_> = object_class.iter_mro(self.db).collect(); + SubsequentMroElements::Owned(elements.into_iter()) + } else { + // Use the first base's MRO. + let elements: Vec<_> = match bases[0] { + ClassBase::Class(class) => class.iter_mro(self.db).collect(), + _ => vec![ClassBase::Class(ClassType::object(self.db))], + }; + SubsequentMroElements::Owned(elements.into_iter()) + } + } }) - .copied() } } @@ -412,10 +571,7 @@ 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() } @@ -544,3 +700,15 @@ fn c3_merge(mut sequences: Vec>) -> Option { } } } + +/// Error kinds for functional class MRO computation. +/// +/// These mirror the relevant variants from `MroErrorKind` for regular classes. +#[derive(Debug, Clone)] +pub(crate) enum FunctionalMroError<'db> { + /// The class has duplicate bases in its bases tuple. + DuplicateBases(Box<[ClassBase<'db>]>), + + /// The MRO is unresolvable through the C3-merge algorithm. + UnresolvableMro, +} diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 19a0087509598a..0861ffd06be948 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, @@ -794,11 +796,16 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } // Treat enums as a union of their members. Type::NominalInstance(instance) - if enum_metadata(db, instance.class_literal(db)).is_some() => + if instance + .class_literal(db) + .is_some_and(|class| enum_metadata(db, class).is_some()) => { + let class_literal = instance + .class_literal(db) + .expect("Already checked that class_literal is Some"); UnionType::from_elements( db, - enum_member_literals(db, instance.class_literal(db), None) + enum_member_literals(db, class_literal, None) .expect("Calling `enum_member_literals` on an enum class") .map(|ty| filter_to_cannot_be_equal(db, ty, rhs_ty)), ) diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index 906999a9f2d600..4f54689e72f27e 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -19,7 +19,7 @@ use ruff_python_ast as ast; /// ``` /// /// The revealed types there are: -/// - `typing.NewType`: `Type::ClassLiteral(ClassLiteral)` with `KnownClass::NewType`. +/// - `typing.NewType`: `Type::ClassLiteral(StmtClassLiteral)` with `KnownClass::NewType`. /// - `Foo`: `Type::KnownInstance(KnownInstanceType::NewType(NewType { .. }))` /// - `x`: `Type::NewTypeInstance(NewType { .. })` /// diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index bf6f292e56878d..9839713f75362d 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, StmtClassLiteral, Type, class::CodeGeneratorKind, context::InferContext, diagnostic::{ @@ -45,7 +45,7 @@ const PROHIBITED_NAMEDTUPLE_ATTRS: &[&str] = &[ "_source", ]; -pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: ClassLiteral<'db>) { +pub(super) fn check_class<'db>(context: &InferContext<'db, '_>, class: StmtClassLiteral<'db>) { let db = context.db(); let configuration = OverrideRulesConfig::from(context); if configuration.no_rules_enabled() { @@ -116,7 +116,10 @@ fn check_class_declaration<'db>( return; }; - let (literal, specialization) = class.class_literal(db); + let Some((literal, specialization)) = class.stmt_class_literal(db) else { + // Functional classes don't have class literals. + return; + }; let class_kind = CodeGeneratorKind::from_class(db, literal, specialization); // Check for prohibited `NamedTuple` attribute overrides. @@ -164,7 +167,12 @@ 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.stmt_class_literal(db) + else { + // Functional classes in the MRO don't have class literals. + 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); diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 922ebcd5f361c9..acb6a79d6f8f80 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -159,7 +159,7 @@ impl Ty { .place .expect_type(); debug_assert!( - matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class_literal(db))) + matches!(ty, Type::NominalInstance(instance) if instance.class_literal(db).is_some_and(|class| is_single_member_enum(db, class))) ); ty } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 3c02a3aed898ec..81f3762dbc4035 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -12,11 +12,11 @@ use crate::{ place::{Definedness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations}, semantic_index::{definition::Definition, place::ScopedPlaceId, place_table, use_def_map}, types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, - ClassType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, - InstanceFallbackShadowsNonDataDescriptor, IsDisjointVisitor, KnownFunction, - MemberLookupPolicy, NormalizedVisitor, PropertyInstanceType, Signature, Type, TypeMapping, - TypeQualifiers, TypeRelation, TypeVarVariance, VarianceInferable, + ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassBase, ClassType, + FindLegacyTypeVarsVisitor, HasRelationToVisitor, InstanceFallbackShadowsNonDataDescriptor, + IsDisjointVisitor, KnownFunction, MemberLookupPolicy, NormalizedVisitor, + PropertyInstanceType, Signature, StmtClassLiteral, Type, TypeMapping, TypeQualifiers, + TypeRelation, TypeVarVariance, VarianceInferable, constraints::{ConstraintSet, IteratorConstraintsExtension, OptionConstraintsExtension}, context::InferContext, diagnostic::report_undeclared_protocol_member, @@ -26,11 +26,11 @@ use crate::{ }, }; -impl<'db> ClassLiteral<'db> { +impl<'db> StmtClassLiteral<'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()))) } } @@ -73,10 +73,17 @@ 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) + // Check if this class or any ancestor protocol is decorated with @runtime_checkable. + // Per PEP 544, @runtime_checkable propagates to subclasses. + self.0.iter_mro(db).any(|base| { + base.into_class() + .and_then(|class| class.stmt_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 @@ -85,7 +92,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.stmt_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 @@ -101,7 +111,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.stmt_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 { @@ -866,7 +880,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, @@ -874,15 +888,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.stmt_class_literal(db)?; + let protocol_class = class_literal.into_protocol_class(db)?; + let parent_scope = protocol_class.stmt_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/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index d78f110018a843..aea64dc4bd8efb 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -130,6 +130,21 @@ pub enum SpecialFormType { /// Typeshed defines this symbol as a class, but this isn't accurate: it's actually a factory function /// at runtime. We therefore represent it as a special form internally. NamedTuple, + + /// Internal annotation for `typing.NamedTuple` fields parameter. + TypingNamedTupleFieldsSchema, + + /// Internal annotation for `collections.namedtuple` field names parameter. + CollectionsNamedTupleFieldsSchema, + + /// Internal annotation for `collections.namedtuple` defaults parameter. + CollectionsNamedTupleDefaultsSchema, + + /// An internal type representing the fields argument to `dataclasses.make_dataclass`. + /// When a list or tuple literal is passed as the `fields` argument, it gets inferred as a + /// `KnownInstanceType::MakeDataclassFieldsSchema` instead of a regular list or tuple type. + /// This allows bind.rs to extract field information without AST access. + MakeDataclassFieldsSchema, } impl SpecialFormType { @@ -182,7 +197,13 @@ impl SpecialFormType { | Self::ChainMap | Self::OrderedDict => KnownClass::StdlibAlias, - Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy => KnownClass::Object, + Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::TypingNamedTupleFieldsSchema + | Self::CollectionsNamedTupleFieldsSchema + | Self::CollectionsNamedTupleDefaultsSchema + | Self::MakeDataclassFieldsSchema => KnownClass::Object, Self::NamedTuple => KnownClass::FunctionType, } @@ -266,7 +287,11 @@ impl SpecialFormType { | Self::Bottom | Self::Intersection | Self::TypeOf - | Self::CallableTypeOf => module.is_ty_extensions(), + | Self::CallableTypeOf + | Self::TypingNamedTupleFieldsSchema + | Self::CollectionsNamedTupleFieldsSchema + | Self::CollectionsNamedTupleDefaultsSchema + | Self::MakeDataclassFieldsSchema => module.is_ty_extensions(), } } @@ -327,7 +352,11 @@ impl SpecialFormType { | Self::ReadOnly | Self::Protocol | Self::Any - | Self::Generic => false, + | Self::Generic + | Self::TypingNamedTupleFieldsSchema + | Self::CollectionsNamedTupleFieldsSchema + | Self::CollectionsNamedTupleDefaultsSchema + | Self::MakeDataclassFieldsSchema => false, } } @@ -382,7 +411,11 @@ impl SpecialFormType { | Self::Callable | Self::Protocol | Self::Generic - | Self::Unpack => None, + | Self::Unpack + | Self::TypingNamedTupleFieldsSchema + | Self::CollectionsNamedTupleFieldsSchema + | Self::CollectionsNamedTupleDefaultsSchema + | Self::MakeDataclassFieldsSchema => None, } } @@ -434,7 +467,11 @@ impl SpecialFormType { | Self::Unknown | Self::TypeOf | Self::Any // can be used in `issubclass()` but not `isinstance()`. - | Self::Unpack => false, + | Self::Unpack + | Self::TypingNamedTupleFieldsSchema + | Self::CollectionsNamedTupleFieldsSchema + | Self::CollectionsNamedTupleDefaultsSchema + | Self::MakeDataclassFieldsSchema => false, } } @@ -485,6 +522,14 @@ impl SpecialFormType { SpecialFormType::Protocol => "Protocol", SpecialFormType::Generic => "Generic", SpecialFormType::NamedTuple => "NamedTuple", + SpecialFormType::TypingNamedTupleFieldsSchema => "_TypingNamedTupleFieldsSchema", + SpecialFormType::CollectionsNamedTupleFieldsSchema => { + "_CollectionsNamedTupleFieldsSchema" + } + SpecialFormType::CollectionsNamedTupleDefaultsSchema => { + "_CollectionsNamedTupleDefaultsSchema" + } + SpecialFormType::MakeDataclassFieldsSchema => "_MakeDataclassFieldsSchema", } } @@ -535,7 +580,11 @@ impl SpecialFormType { | SpecialFormType::TypeOf | SpecialFormType::CallableTypeOf | SpecialFormType::Top - | SpecialFormType::Bottom => &[KnownModule::TyExtensions], + | SpecialFormType::Bottom + | SpecialFormType::TypingNamedTupleFieldsSchema + | SpecialFormType::CollectionsNamedTupleFieldsSchema + | SpecialFormType::CollectionsNamedTupleDefaultsSchema + | SpecialFormType::MakeDataclassFieldsSchema => &[KnownModule::TyExtensions], } } diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index b27132d795a3db..3cf2a3df6ba456 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,14 +1,16 @@ use crate::place::PlaceAndQualifiers; use crate::semantic_index::definition::Definition; +use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSet; use crate::types::generics::InferableTypeVars; use crate::types::protocol_class::ProtocolClass; use crate::types::variance::VarianceInferable; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, DynamicType, - FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, KnownClass, - MaterializationKind, MemberLookupPolicy, NormalizedVisitor, SpecialFormType, Type, TypeContext, - TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypedDictType, UnionType, todo_type, + ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, ClassType, DynamicType, + FindLegacyTypeVarsVisitor, FunctionalClassLiteral, FunctionalNamedTupleLiteral, + HasRelationToVisitor, IsDisjointVisitor, KnownClass, MaterializationKind, MemberLookupPolicy, + NormalizedVisitor, SpecialFormType, Type, TypeContext, TypeMapping, TypeRelation, + TypeVarBoundOrConstraints, TypedDictType, UnionType, todo_type, }; use crate::{Db, FxOrderSet}; @@ -263,15 +265,15 @@ impl<'db> SubclassOfType<'db> { _visitor: &IsDisjointVisitor<'db>, ) -> ConstraintSet<'db> { match (self.subclass_of, other.subclass_of) { + // Dynamic types have unknown structure, so we can't prove disjointness. (SubclassOfInner::Dynamic(_), _) | (_, SubclassOfInner::Dynamic(_)) => { ConstraintSet::from(false) } (SubclassOfInner::Class(self_class), SubclassOfInner::Class(other_class)) => { ConstraintSet::from(!self_class.could_coexist_in_mro_with(db, other_class)) } - (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { - unreachable!() - } + // TypeVar should have been handled before calling this method. + (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => unreachable!(), } } @@ -332,9 +334,11 @@ 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)) + self.subclass_of.into_class(db).is_some_and(|class| { + class + .stmt_class_literal(db) + .is_some_and(|(lit, _)| lit.is_typed_dict(db)) + }) } } @@ -405,17 +409,21 @@ impl<'db> SubclassOfInner<'db> { } } - pub(crate) const fn into_dynamic(self) -> Option> { + pub(crate) const fn into_type_var(self) -> Option> { match self { - Self::Class(_) | Self::TypeVar(_) => None, - Self::Dynamic(dynamic) => Some(dynamic), + Self::Class(_) | Self::Dynamic(_) => None, + Self::TypeVar(bound_typevar) => Some(bound_typevar), } } - pub(crate) const fn into_type_var(self) -> Option> { + /// Convert to a `ClassBase` if this is a class-like type. + /// + /// Returns `None` for `TypeVar` since type variables require special handling. + pub(crate) const fn to_class_base(self) -> Option> { match self { - Self::Class(_) | Self::Dynamic(_) => None, - Self::TypeVar(bound_typevar) => Some(bound_typevar), + Self::Class(class) => Some(ClassBase::Class(class)), + Self::Dynamic(dynamic) => Some(ClassBase::Dynamic(dynamic)), + Self::TypeVar(_) => None, } } @@ -534,3 +542,17 @@ impl<'db> From> for Type<'db> { } } } + +impl<'db> From> for SubclassOfInner<'db> { + fn from(value: FunctionalClassLiteral<'db>) -> Self { + SubclassOfInner::Class(ClassType::NonGeneric(ClassLiteral::Functional(value))) + } +} + +impl<'db> From> for SubclassOfInner<'db> { + fn from(value: FunctionalNamedTupleLiteral<'db>) -> Self { + SubclassOfInner::Class(ClassType::NonGeneric(ClassLiteral::FunctionalNamedTuple( + value, + ))) + } +} diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 1b98da3db8d21c..4ed0329239e883 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -74,7 +74,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.stmt_class_literal(db) else { + return TypedDictSchema::default(); + }; class_literal .fields(db, specialization, CodeGeneratorKind::TypedDict) .into_iter() @@ -294,7 +296,7 @@ impl<'db> TypedDictType<'db> { pub fn definition(self, db: &'db dyn Db) -> Option> { match self { - TypedDictType::Class(defining_class) => Some(defining_class.definition(db)), + TypedDictType::Class(defining_class) => defining_class.definition(db), TypedDictType::Synthesized(_) => None, } } diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.pyi index feb22aae007320..a8b61c94e06b34 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/_type_checker_internals.pyi @@ -55,18 +55,16 @@ class TypedDictFallback(Mapping[str, object], metaclass=ABCMeta): class NamedTupleFallback(tuple[Any, ...]): _field_defaults: ClassVar[dict[str, Any]] _fields: ClassVar[tuple[str, ...]] + # Allow any attribute access since we don't know the actual fields. + def __getattr__(self, name: str, /) -> Any: ... # __orig_bases__ sometimes exists on <3.12, but not consistently # So we only add it to the stub on 3.12+. if sys.version_info >= (3, 12): __orig_bases__: ClassVar[tuple[Any, ...]] - @overload - def __init__(self, typename: str, fields: Iterable[tuple[str, Any]], /) -> None: ... - @overload - @typing_extensions.deprecated( - "Creating a typing.NamedTuple using keyword arguments is deprecated and support will be removed in Python 3.15" - ) - def __init__(self, typename: str, fields: None = None, /, **kwargs: Any) -> None: ... + # For instance construction when field names are unknown: Point(1, 2). + def __new__(cls, *args: Any, **kwargs: Any) -> typing_extensions.Self: ... + def __init__(self, *args: Any, **kwargs: Any) -> None: ... @classmethod def _make(cls, iterable: Iterable[Any]) -> typing_extensions.Self: ... def _asdict(self) -> dict[str, Any]: ...