Compare commits

...

6 Commits

Author SHA1 Message Date
Charlie Marsh
742c1b65d3 [ty] Add support for functional dataclasses 2026-01-02 15:09:11 -05:00
Charlie Marsh
5220cf736f Use special form for args 2026-01-02 15:08:26 -05:00
Charlie Marsh
2c5e618f89 Deduplicate synthesis 2026-01-01 16:34:46 -05:00
Charlie Marsh
5bb91fa527 [ty] Add functional namedtuple 2025-12-31 19:21:07 -05:00
Charlie Marsh
40a117cc43 Use an enum 2025-12-31 19:14:10 -05:00
Charlie Marsh
0c810fe869 Add support for dynamic type() classes 2025-12-31 16:13:02 -05:00
34 changed files with 4684 additions and 827 deletions

View File

@@ -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(

View File

@@ -3769,6 +3769,7 @@ quux.<CURSOR>
__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.<CURSOR>
__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, ...]

View File

@@ -23,14 +23,259 @@ tests for the `__class__` attribute.)
reveal_type(type(1)) # revealed: <class 'int'>
```
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: ...
# We synthesize a class type using the name argument
reveal_type(type("Foo", (), {})) # revealed: <class 'Foo'>
# With a single base class
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: <class 'Foo'>
# With multiple base classes
reveal_type(type("Foo", (Base, Mixin), {})) # revealed: <class 'Foo'>
# 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: ...
reveal_type(type("Foo", (), {})) # revealed: type
Foo = type("Foo", (Base,), {"custom_attr": 42})
foo = Foo()
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
# 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: <super: FunctionalChild, FunctionalChild>
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: <super: <class 'RegularChild'>, 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: <super: FunctionalChild, RegularChild>
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: <class 'Parent'>
# Create a functional class that inherits from another functional class.
ChildCls = type("ChildCls", (Parent,), {})
reveal_type(ChildCls) # revealed: <class 'ChildCls'>
# 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: <class 'IntContainer'>
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 <class 'A'> 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 `[<class 'X'>, <class 'Y'>]`"
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 `<class 'A'>`) and `Meta2` (metaclass of base class `<class 'B'>`) have no subclass relationship"
Bad = type("Bad", (A, B), {})
```
## Calls to `str()`
### Valid calls

View File

@@ -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: <class 'Point'>
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: <class 'Point'>
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 `<class 'NoInit'>`. 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)
```

View File

@@ -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 <class 'Person'>._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

View File

@@ -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

View File

@@ -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<Specialization<'db>> {
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<Specialization<'db>> {
self.specialization_of_optional(db, None)
}
fn class_and_specialization_of_optional(
fn specialization_of_optional(
self,
db: &'db dyn Db,
expected_class: Option<ClassLiteral<'_>>,
) -> Option<(ClassLiteral<'db>, Specialization<'db>)> {
expected_class: Option<StmtClassLiteral<'_>>,
) -> Option<Specialization<'db>> {
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<ClassLiteral<'db>> {
pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option<StmtClassLiteral<'db>> {
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<Box<[_]>> = 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<Box<[_]>> = 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::<Box<[_]>>(),
)
}
}
/// 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<Type<'db>>)]>,
}
// 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::<Box<[_]>>(),
)
}
}
/// 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<impl Iterator<Item = Type<'db>> + '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<Specialization<'db>>,
is_known_base: impl Fn(ClassBase<'db>) -> bool,
) -> Type<'db> {

View File

@@ -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<Item = ClassBase<'db>>,
) -> impl Iterator<Item = ClassBase<'db>> {
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:

View File

@@ -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::<Box<[_]>>(),
RecursivelyDefined::No,

View File

@@ -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<Vec<Type<'db>>> {
};
}
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

View File

@@ -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<Box<[Name]>> = 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<Type<'db>>)]> =
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<Vec<(Name, Type<'db>, Option<Type<'db>>)>> =
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<Box<[ClassBase<'db>]>> =
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::<Option<Box<[_]>>>()
});
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<Box<[(Name, Type<'db>, Option<Type<'db>>)]>> =
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<Type<'db>>,
)> {
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::<Option<Box<[_]>>>()
},
)
};
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
_ => {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<Self> {
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)

View File

@@ -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<LintDiagnosticGuard<'ctx, 'db>> {
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<Definition<'db>>),
(field_with_default, field_with_default_def): &(Name, Option<Definition<'db>>),
) {
@@ -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<Definition<'db>>,
) {
@@ -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,
) {

View File

@@ -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<String>,
},
/// 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("<class '")?;
class
.display_with(self.db, self.settings.clone())
.fmt_detailed(&mut f)?;
match class {
ClassLiteral::Stmt(stmt_class) => {
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("<super: ")?;
Type::from(bound_super.pivot_class(self.db))
.display_with(self.db, self.settings.singleline())
.fmt_detailed(f)?;
// Display functional class pivots as just their name (not type[...]).
match bound_super.pivot_class(self.db) {
ClassBase::Class(ClassType::NonGeneric(ClassLiteral::Functional(fc))) => {
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("<namedtuple-fields-schema [")?;
let mut first = true;
for (name, field_ty) in schema.fields(self.db) {
if first {
first = false;
} else {
f.write_str(", ")?;
}
write!(
f.with_type(*field_ty),
"({name}, {})",
field_ty.display(self.db)
)?;
}
f.write_str("]>")
}
KnownInstanceType::CollectionsNamedTupleFieldsSchema(schema) => {
f.set_invalid_type_annotation();
f.write_str("<namedtuple-field-names-schema [")?;
let mut first = true;
for name in schema.field_names(self.db) {
if first {
first = false;
} else {
f.write_str(", ")?;
}
write!(f, "{name}")?;
}
f.write_str("]>")
}
KnownInstanceType::CollectionsNamedTupleDefaultsSchema(schema) => {
f.set_invalid_type_annotation();
write!(
f,
"<namedtuple-defaults-schema count={}>",
schema.count(self.db)
)
}
KnownInstanceType::MakeDataclassFieldsSchema(schema) => {
f.set_invalid_type_annotation();
f.write_str("<make-dataclass-fields-schema [")?;
let mut first = true;
for (name, field_ty, _) in schema.fields(self.db) {
if first {
first = false;
} else {
f.write_str(", ")?;
}
write!(
f.with_type(*field_ty),
"({name}, {})",
field_ty.display(self.db)
)?;
}
f.write_str("]>")
}
}
}
}

View File

@@ -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<EnumMetadata<'db>> {
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<EnumMetadata<'db>> {
// 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<impl Iterator<Item = Type<'a>> + '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))

View File

@@ -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,

View File

@@ -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<Definition<'db>>,
class: ClassLiteral<'db>,
class: StmtClassLiteral<'db>,
) -> Option<BoundTypeVarInstance<'db>> {
let index = semantic_index(db, function_scope_id.file(db));

View File

@@ -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);

View File

@@ -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<ClassLiteral<'db>> {
) -> Option<StmtClassLiteral<'db>> {
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)
})
}

View File

@@ -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_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 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<TypingNamedTupleFieldsSchema<'db>> {
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<CollectionsNamedTupleFieldsSchema<'db>> {
let db = self.db();
let mut field_names: Vec<ast::name::Name> = 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<MakeDataclassFieldsSchema<'db>> {
let db = self.db();
let mut fields: Vec<(ast::name::Name, Type<'db>, Option<Type<'db>>)> =
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__");
}

View File

@@ -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) {

View File

@@ -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<StmtClassLiteral<'db>> {
let class = match self.0 {
NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db),
NominalInstanceInner::NonTuple(class) => class,
NominalInstanceInner::Object => {
return KnownClass::Object
.try_to_class_literal(db)
.expect("Typeshed should always have a `object` class in `builtins.pyi`");
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);
}
}

View File

@@ -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<Specialization<'db>>,
) {
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));

View File

@@ -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<Specialization<'db>>,
) -> Result<Self, MroError<'db>> {
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<Self, FunctionalMroError<'db>> {
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<VecDeque<ClassBase<'db>>> = 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<ClassBase<'db>> 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<std::slice::Iter<'db, ClassBase<'db>>>,
subsequent_elements: Option<SubsequentMroElements<'db>>,
}
/// 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<ClassBase<'db>>),
}
impl<'db> Iterator for SubsequentMroElements<'db> {
type Item = ClassBase<'db>;
fn next(&mut self) -> Option<Self::Item> {
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<Item = ClassBase<'db>> + '_ {
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<Self::Item> {
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<VecDeque<ClassBase>>) -> Option<Mro> {
}
}
}
/// 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,
}

View File

@@ -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<Type<'db>> {
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)),
)

View File

@@ -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 { .. })`
///

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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<ProtocolClass<'db>> {
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();

View File

@@ -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],
}
}

View File

@@ -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,13 +409,6 @@ impl<'db> SubclassOfInner<'db> {
}
}
pub(crate) const fn into_dynamic(self) -> Option<DynamicType<'db>> {
match self {
Self::Class(_) | Self::TypeVar(_) => None,
Self::Dynamic(dynamic) => Some(dynamic),
}
}
pub(crate) const fn into_type_var(self) -> Option<BoundTypeVarInstance<'db>> {
match self {
Self::Class(_) | Self::Dynamic(_) => None,
@@ -419,6 +416,17 @@ impl<'db> SubclassOfInner<'db> {
}
}
/// 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<ClassBase<'db>> {
match self {
Self::Class(class) => Some(ClassBase::Class(class)),
Self::Dynamic(dynamic) => Some(ClassBase::Dynamic(dynamic)),
Self::TypeVar(_) => None,
}
}
pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
Some(match ty {
Type::NominalInstance(instance) => SubclassOfInner::Class(instance.class(db)),
@@ -534,3 +542,17 @@ impl<'db> From<SubclassOfType<'db>> for Type<'db> {
}
}
}
impl<'db> From<FunctionalClassLiteral<'db>> for SubclassOfInner<'db> {
fn from(value: FunctionalClassLiteral<'db>) -> Self {
SubclassOfInner::Class(ClassType::NonGeneric(ClassLiteral::Functional(value)))
}
}
impl<'db> From<FunctionalNamedTupleLiteral<'db>> for SubclassOfInner<'db> {
fn from(value: FunctionalNamedTupleLiteral<'db>) -> Self {
SubclassOfInner::Class(ClassType::NonGeneric(ClassLiteral::FunctionalNamedTuple(
value,
)))
}
}

View File

@@ -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<Definition<'db>> {
match self {
TypedDictType::Class(defining_class) => Some(defining_class.definition(db)),
TypedDictType::Class(defining_class) => defining_class.definition(db),
TypedDictType::Synthesized(_) => None,
}
}

View File

@@ -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]: ...