diff --git a/crates/ty_python_semantic/resources/mdtest/call/new_class.md b/crates/ty_python_semantic/resources/mdtest/call/new_class.md new file mode 100644 index 0000000000000..2a1b9327adc3c --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/new_class.md @@ -0,0 +1,300 @@ +# Calls to `types.new_class()` + +## Basic dynamic class creation + +`types.new_class()` creates a new class dynamically. We infer a dynamic class type using the name +from the first argument and bases from the second argument. + +```py +import types + +class Base: ... +class Mixin: ... + +# Basic call with no bases +reveal_type(types.new_class("Foo")) # revealed: + +# With a single base class +reveal_type(types.new_class("Bar", (Base,))) # revealed: + +# With multiple base classes +reveal_type(types.new_class("Baz", (Base, Mixin))) # revealed: +``` + +## Keyword arguments + +Arguments can be passed as keyword arguments. + +```py +import types + +class Base: ... + +reveal_type(types.new_class("Foo", bases=(Base,))) # revealed: +reveal_type(types.new_class(name="Bar")) # revealed: +reveal_type(types.new_class(name="Baz", bases=(Base,))) # revealed: +``` + +## Assignability to base type + +The inferred type should be assignable to `type[Base]` when the class inherits from `Base`. + +```py +import types + +class Base: ... + +tests: list[type[Base]] = [] +NewFoo = types.new_class("NewFoo", (Base,)) +tests.append(NewFoo) # No error - type[NewFoo] is assignable to type[Base] +``` + +## Invalid calls + +### Non-string name + +```py +import types + +class Base: ... + +# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `types.new_class()`: Expected `str`, found `Literal[123]`" +types.new_class(123, (Base,)) +``` + +### Non-iterable bases + +```py +import types + +class Base: ... + +# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `types.new_class()`: Expected `Iterable[object]`, found ``" +types.new_class("Foo", Base) +``` + +### Invalid base types + +```py +import types + +# error: [invalid-base] "Invalid class base with type `Literal[1]`" +# error: [invalid-base] "Invalid class base with type `Literal[2]`" +types.new_class("Foo", (1, 2)) +``` + +### No arguments + +```py +import types + +# error: [no-matching-overload] "No overload of `types.new_class` matches arguments" +types.new_class() +``` + +### Invalid `kwds` + +```py +import types + +# error: [invalid-argument-type] +types.new_class("Foo", (), 1) +``` + +### Invalid `exec_body` + +```py +import types + +# error: [invalid-argument-type] +types.new_class("Foo", (), None, 1) +``` + +### Too many positional arguments + +```py +import types + +# error: [too-many-positional-arguments] +types.new_class("Foo", (), None, None, 1) +``` + +### Duplicate bases + +```py +import types + +class Base: ... + +# error: [duplicate-base] "Duplicate base class in class `Dup`" +types.new_class("Dup", (Base, Base)) +``` + +## Special bases + +`types.new_class()` properly handles `__mro_entries__` and metaclasses, so it supports bases that +`type()` does not. + +These cases are mostly about showing that class creation is valid and that ty preserves the base +information it can see. `types.new_class()` still doesn't let ty observe explicit class members +unless `exec_body` populates the namespace dynamically, and then attribute types become `Unknown`. + +### Iterable bases + +Any iterable of bases is accepted. When the iterable is a list literal, we should still preserve the +real base-class information: + +```py +import types + +class Base: + base_attr: int = 1 + +FromList = types.new_class("FromList", [Base]) +reveal_type(FromList().base_attr) # revealed: int + +FromKeywordList = types.new_class("FromKeywordList", bases=[Base]) +reveal_type(FromKeywordList().base_attr) # revealed: int + +bases = (Base,) +FromStarredList = types.new_class("FromStarredList", [*bases]) +reveal_type(FromStarredList().base_attr) # revealed: int +``` + +### Enum bases + +Unlike `type()`, `types.new_class()` properly handles metaclasses, so inheriting from `enum.Enum` or +an empty enum subclass is valid: + +```py +import types +from enum import Enum + +class Color(Enum): + RED = 1 + GREEN = 2 + +# Enums with members are still final and cannot be subclassed, +# regardless of whether we use type() or types.new_class() +# error: [subclass-of-final-class] +ExtendedColor = types.new_class("ExtendedColor", (Color,)) + +class EmptyEnum(Enum): + pass + +# Empty enum subclasses are fine with types.new_class() because it +# properly resolves and uses the EnumMeta metaclass +EmptyEnumSub = types.new_class("EmptyEnumSub", (EmptyEnum,)) +reveal_type(EmptyEnumSub) # revealed: + +# Directly inheriting from Enum is also fine +MyEnum = types.new_class("MyEnum", (Enum,)) +reveal_type(MyEnum) # revealed: +``` + +### Generic and TypedDict bases + +Even though `types.new_class()` handles `__mro_entries__` at runtime, ty does not yet model the full +typing semantics of dynamically-created generic classes or TypedDicts, so these bases are rejected: + +```py +import types +from typing import Generic, TypeVar +from typing_extensions import TypedDict + +T = TypeVar("T") + +# error: [invalid-base] "Invalid base for class created via `types.new_class()`" +GenericClass = types.new_class("GenericClass", (Generic[T],)) + +# error: [invalid-base] "Invalid base for class created via `types.new_class()`" +TypedDictClass = types.new_class("TypedDictClass", (TypedDict,)) +``` + +### `type[X]` bases + +`type[X]` represents "some subclass of X". This is a valid base class, but the exact class is not +known, so the MRO cannot be resolved. `Unknown` is inserted and `unsupported-dynamic-base` is +emitted: + +```py +import types +from ty_extensions import reveal_mro + +class Base: + base_attr: int = 1 + +def f(x: type[Base]): + # error: [unsupported-dynamic-base] "Unsupported class base" + Child = types.new_class("Child", (x,)) + + reveal_type(Child) # revealed: + reveal_mro(Child) # revealed: (, Unknown, ) + child = Child() + reveal_type(child.base_attr) # revealed: Unknown +``` + +`type[Any]` and `type[Unknown]` already carry the dynamic kind, so no diagnostic is needed. An +unknowable MRO is already inherent to `Any`/`Unknown`: + +```py +import types +from typing import Any + +def g(x: type[Any]): + # No diagnostic: `Any` base is fine as-is + Child = types.new_class("Child", (x,)) + reveal_type(Child) # revealed: +``` + +## Dynamic namespace via `exec_body` + +When `exec_body` is provided, it can populate the class namespace dynamically, so attribute access +returns `Unknown`. Without `exec_body`, the namespace is empty and attribute access is an error: + +```py +import types + +class Base: + base_attr: int = 1 + +# Without exec_body: no dynamic namespace, so only base attributes are available +NoBody = types.new_class("NoBody", (Base,)) +instance = NoBody() +reveal_type(instance.base_attr) # revealed: int +instance.missing_attr # error: [unresolved-attribute] + +# With exec_body=None: same as no exec_body +NoBodyExplicit = types.new_class("NoBodyExplicit", (Base,), exec_body=None) +instance_explicit = NoBodyExplicit() +reveal_type(instance_explicit.base_attr) # revealed: int +instance_explicit.missing_attr # error: [unresolved-attribute] + +# With exec_body=None passed positionally: same as no exec_body +NoBodyPositional = types.new_class("NoBodyPositional", (Base,), None, None) +instance_positional = NoBodyPositional() +reveal_type(instance_positional.base_attr) # revealed: int +instance_positional.missing_attr # error: [unresolved-attribute] + +# With exec_body: namespace is dynamic, so any attribute access returns Unknown +def body(ns): + ns["x"] = 1 + +WithBody = types.new_class("WithBody", (Base,), exec_body=body) +instance2 = WithBody() +reveal_type(instance2.x) # revealed: Unknown +reveal_type(instance2.base_attr) # revealed: Unknown +``` + +## Forward references via string annotations + +Forward references via subscript annotations on generic bases are supported: + +```py +import types + +# Forward reference to X via subscript annotation in tuple base +# (This fails at runtime, but we should handle it without panicking) +X = types.new_class("X", (tuple["X | None"],)) +reveal_type(X) # revealed: +``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index e76974bf04128..c700205fc098b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -534,7 +534,7 @@ class Base: ... # error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `Literal[b"Foo"]`" type(b"Foo", (), {}) -# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found ``" +# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[object, ...]`, found ``" type("Foo", Base, {}) # error: 14 [invalid-base] "Invalid class base with type `Literal[1]`" @@ -545,11 +545,29 @@ type("Foo", (1, 2), {}) type("Foo", (Base,), {b"attr": 1}) ``` +Assigned calls still preserve list-literal base information after reporting the invalid `bases` +argument: + +```py +class Base: + attr: int = 1 + +# error: [invalid-argument-type] +FromList = type("FromList", [Base], {}) +reveal_type(FromList().attr) # revealed: int + +bases = (Base,) + +# error: [invalid-argument-type] +FromStarredList = type("FromStarredList", [*bases], {}) +reveal_type(FromStarredList().attr) # revealed: int +``` + ## `type[...]` as base class -`type[...]` (SubclassOf) types cannot be used as base classes. When a `type[...]` is used in the -bases tuple, we emit a diagnostic and insert `Unknown` into the MRO. This gives exactly one -diagnostic about the unsupported base, rather than cascading errors: +`type[...]` (SubclassOf) types are valid class bases, but the exact class is not known, so the MRO +cannot be resolved. `Unknown` is inserted into the MRO and `unsupported-dynamic-base` is emitted. +This gives exactly one diagnostic rather than cascading errors: ```py from ty_extensions import reveal_mro @@ -571,6 +589,18 @@ def f(x: type[Base]): reveal_type(child.base_attr) # revealed: Unknown ``` +`type[Any]` and `type[Unknown]` already carry the dynamic kind, so no diagnostic is needed. An +unknowable MRO is already inherent to `Any`/`Unknown`: + +```py +from typing import Any + +def g(x: type[Any]): + # No diagnostic: `Any` base is fine as-is + Child = type("Child", (x,), {}) + reveal_type(Child) # revealed: +``` + ## MRO errors MRO errors are detected and reported: diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md index dddf1e4c88f77..e89334cd526ed 100644 --- a/crates/ty_python_semantic/resources/mdtest/mro.md +++ b/crates/ty_python_semantic/resources/mdtest/mro.md @@ -208,6 +208,20 @@ if not isinstance(DoesNotExist, type): ## Inheritance from `type[Any]` and `type[Unknown]` +Using `type[T]` for a non-dynamic `T` as a base keeps the class analyzable, even though the exact +MRO cannot be determined: + +```py +from ty_extensions import reveal_mro + +class Base: + base_attr: int = 1 + +def f(x: type[Base]): + class Foo(x): ... # error: [unsupported-base] + reveal_mro(Foo) # revealed: (, Unknown, ) +``` + Inheritance from `type[Any]` and `type[Unknown]` is also permitted, in keeping with the gradual guarantee: diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index d10ac36829d57..6753322ea3bb6 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -618,6 +618,13 @@ reveal_type(nt2.a) # revealed: Any reveal_type(nt2.b) # revealed: Any reveal_type(nt2.c) # revealed: Any +field_names = ("left", "right") +NT2Starred = collections.namedtuple("NT2Starred", field_names=[*field_names]) +reveal_type(NT2Starred) # revealed: +nt2_starred = NT2Starred(1, 2) +reveal_type(nt2_starred.left) # revealed: Any +reveal_type(nt2_starred.right) # revealed: Any + # Keyword arguments can be combined with other kwargs like `defaults` NT3 = collections.namedtuple(typename="NT3", field_names="x y z", defaults=[None]) reveal_type(NT3) # revealed: @@ -685,6 +692,14 @@ Person = collections.namedtuple("Person", ["name", "age", "city"], defaults=["Un reveal_type(Person) # revealed: reveal_type(Person.__new__) # revealed: [Self](_cls: type[Self], name: Any, age: Any, city: Any = "Unknown") -> Self +defaults = (0, "Unknown") +PersonStarred = collections.namedtuple( + "PersonStarred", + ["name", "age", "city"], + defaults=[*defaults], +) +reveal_type(PersonStarred.__new__) # revealed: [Self](_cls: type[Self], name: Any, age: Any = 0, city: Any = "Unknown") -> Self + # revealed: (, , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(Person) # Can create with all fields diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 949ef76afca2f..353bb4e592ce1 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -28,6 +28,7 @@ pub(crate) use self::infer::{ TypeContext, infer_complete_scope_types, infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types, infer_scope_types, }; +pub(crate) use self::iteration::extract_fixed_length_iterable_element_types; pub use self::known_instance::KnownInstanceType; use self::set_theoretic::KnownUnion; pub(crate) use self::set_theoretic::builder::{IntersectionBuilder, UnionBuilder}; @@ -1194,23 +1195,6 @@ impl<'db> Type<'db> { .and_then(|instance| instance.own_tuple_spec(db)) } - /// If this type is a fixed-length tuple instance, returns a slice of its element types. - /// - /// Returns `None` if this is not a tuple instance, or if it's a variable-length tuple. - fn fixed_tuple_elements(&self, db: &'db dyn Db) -> Option]>> { - let tuple_spec = self.tuple_instance_spec(db)?; - match tuple_spec { - Cow::Borrowed(spec) => { - let elements = spec.as_fixed_length()?.elements_slice(); - Some(Cow::Borrowed(elements)) - } - Cow::Owned(spec) => { - let elements = spec.as_fixed_length()?.elements_slice(); - Some(Cow::Owned(elements.to_vec())) - } - } - } - /// Returns the materialization of this type depending on the given `variance`. /// /// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a71f1bfd1b697..278f812ec8edc 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1,7 +1,7 @@ use std::fmt::Write; pub(crate) use self::dynamic_literal::{ - DynamicClassAnchor, DynamicClassLiteral, DynamicMetaclassConflict, + DynamicClassAnchor, DynamicClassLiteral, DynamicMetaclassConflict, dynamic_class_bases_argument, }; pub use self::known::KnownClass; use self::named_tuple::synthesize_namedtuple_class_member; diff --git a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs index 6db6793f7e37d..fa9758b061920 100644 --- a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs +++ b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use ruff_db::{diagnostic::Span, parsed::parsed_module}; use ruff_python_ast::{self as ast, NodeIndex, name::Name}; use ruff_text_size::{Ranged, TextRange}; @@ -14,13 +12,13 @@ use crate::{ class::{ ClassMemberResult, CodeGeneratorKind, DisjointBase, InstanceMemberResult, MroLookup, }, - definition_expression_type, + definition_expression_type, extract_fixed_length_iterable_element_types, member::Member, mro::{DynamicMroError, Mro, MroIterator}, }, }; -/// A class created dynamically via a three-argument `type()` call. +/// A class created dynamically via a three-argument `type()` or `types.new_class()` call. /// /// For example: /// ```python @@ -36,8 +34,9 @@ use crate::{ /// /// # Salsa interning /// -/// This is a Salsa-interned struct. Two different `type()` calls always produce -/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases: +/// This is a Salsa-interned struct. Two different `type()` / `types.new_class()` calls +/// always produce distinct `DynamicClassLiteral` instances, even if they have the same +/// name and bases: /// /// ```python /// Foo1 = type("Foo", (Base,), {}) @@ -46,32 +45,32 @@ use crate::{ /// ``` /// /// The `anchor` field provides stable identity: -/// - For assigned `type()` calls, the `Definition` uniquely identifies the class. -/// - For dangling `type()` calls, a relative node offset anchored to the enclosing scope +/// - For assigned calls, the `Definition` uniquely identifies the class. +/// - For dangling calls, a relative node offset anchored to the enclosing scope /// provides stable identity that only changes when the scope itself changes. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct DynamicClassLiteral<'db> { - /// The name of the class (from the first argument to `type()`). + /// The name of the class (from the first argument). #[returns(ref)] pub name: Name, /// The anchor for this dynamic class, providing stable identity. /// - /// - `Definition`: The `type()` call is assigned to a variable. The definition - /// uniquely identifies this class and can be used to find the `type()` call. - /// - `ScopeOffset`: The `type()` call is "dangling" (not assigned). The offset + /// - `Definition`: The call is assigned to a variable. The definition + /// uniquely identifies this class and can be used to find the call expression. + /// - `ScopeOffset`: The call is "dangling" (not assigned). The offset /// is relative to the enclosing scope's anchor node index. #[returns(ref)] pub anchor: DynamicClassAnchor<'db>, - /// The class members from the namespace dict (third argument to `type()`). + /// The class members extracted from the namespace argument. /// Each entry is a (name, type) pair extracted from the dict literal. #[returns(deref)] pub members: Box<[(Name, Type<'db>)]>, - /// Whether the namespace dict (third argument) is dynamic (not a literal dict, - /// or contains non-string-literal keys). When true, attribute lookups on this - /// class and its instances return `Unknown` instead of failing. + /// Whether the namespace is dynamic (not a literal dict, or contains + /// non-string-literal keys). When true, attribute lookups on this class + /// and its instances return `Unknown` instead of failing. pub has_dynamic_namespace: bool, /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator @@ -86,13 +85,13 @@ pub struct DynamicClassLiteral<'db> { /// - For dangling calls, a relative offset provides stable identity. #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub enum DynamicClassAnchor<'db> { - /// The `type()` call is assigned to a variable. + /// The call is assigned to a variable. /// - /// The `Definition` uniquely identifies this class. The `type()` call expression + /// The `Definition` uniquely identifies this class. The call expression /// is the `value` of the assignment, so we can get its range from the definition. Definition(Definition<'db>), - /// The `type()` call is "dangling" (not assigned to a variable). + /// The call is "dangling" (not assigned to a variable). /// /// The offset is relative to the enclosing scope's anchor node index. /// For module scope, this is equivalent to an absolute index (anchor is 0). @@ -108,6 +107,20 @@ pub enum DynamicClassAnchor<'db> { impl get_size2::GetSize for DynamicClassLiteral<'_> {} +/// Returns the `bases` argument for a dynamic class constructor call. +/// +/// Dynamic class constructors accept `bases` either as the second positional argument or as a +/// `bases=` keyword argument. +pub(crate) fn dynamic_class_bases_argument(arguments: &ast::Arguments) -> Option<&ast::Expr> { + arguments.args.get(1).or_else(|| { + arguments + .keywords + .iter() + .find(|kw| kw.arg.as_deref() == Some("bases")) + .map(|kw| &kw.value) + }) +} + #[salsa::tracked] impl<'db> DynamicClassLiteral<'db> { /// Returns the definition where this class is created, if it was assigned to a variable. @@ -128,20 +141,20 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns the explicit base classes of this dynamic class. /// - /// For assigned `type()` calls, bases are computed lazily using deferred inference - /// to handle forward references (e.g., `X = type("X", (tuple["X | None"],), {})`). + /// For assigned calls, bases are computed lazily using deferred inference to handle + /// forward references (e.g., `X = type("X", (tuple["X | None"],), {})`). /// - /// For dangling `type()` calls, bases are computed eagerly at creation time and - /// stored directly on the anchor, since dangling calls cannot recursively reference - /// the class being defined. + /// For dangling calls, bases are computed eagerly at creation time and stored + /// directly on the anchor, since dangling calls cannot recursively reference the + /// class being defined. /// /// Returns an empty slice if the bases cannot be computed (e.g., due to a cycle) - /// or if the bases argument is not a tuple. + /// or if the bases argument cannot be extracted precisely. /// - /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). + /// Returns `[Unknown]` if the bases iterable is variable-length. pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> &'db [Type<'db>] { /// Inner cached function for deferred inference of bases. - /// Only called for assigned `type()` calls where inference was deferred. + /// Only called for assigned calls where inference was deferred. #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] fn deferred_explicit_bases<'db>( db: &'db dyn Db, @@ -157,21 +170,15 @@ impl<'db> DynamicClassLiteral<'db> { .as_call_expr() .expect("Definition value should be a call expression"); - // The `bases` argument is the second positional argument. - let Some(bases_arg) = call_expr.arguments.args.get(1) else { + let Some(bases_arg) = dynamic_class_bases_argument(&call_expr.arguments) else { return Box::default(); }; // Use `definition_expression_type` for deferred inference support. - let bases_type = definition_expression_type(db, definition, bases_arg); - - // For variable-length tuples (like `tuple[type, ...]`), we can't statically - // determine the bases, so return Unknown. - bases_type - .fixed_tuple_elements(db) - .map(Cow::into_owned) - .map(Into::into) - .unwrap_or_else(|| Box::from([Type::unknown()])) + extract_fixed_length_iterable_element_types(db, bases_arg, |expr| { + definition_expression_type(db, definition, expr) + }) + .unwrap_or_else(|| Box::from([Type::unknown()])) } match self.anchor(db) { diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 1ea74f77cf7d7..b73018374f336 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1770,6 +1770,8 @@ pub enum KnownFunction { RevealMro, /// `struct.unpack` Unpack, + /// `types.new_class` + NewClass, } impl KnownFunction { @@ -1855,6 +1857,9 @@ impl KnownFunction { Self::Unpack => { matches!(module, KnownModule::Struct) } + Self::NewClass => { + matches!(module, KnownModule::Types) + } Self::TypeCheckOnly => matches!(module, KnownModule::Typing), Self::NamedTuple => matches!(module, KnownModule::Collections), @@ -2359,6 +2364,7 @@ pub(crate) mod tests { KnownFunction::NamedTuple => KnownModule::Collections, KnownFunction::TotalOrdering => KnownModule::Functools, KnownFunction::Unpack => KnownModule::Struct, + KnownFunction::NewClass => KnownModule::Types, }; let function_definition = known_module_symbol(&db, module, function_name) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 903d3bcc31f50..35a2cf09d41ea 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::cell::RefCell; use std::rc::Rc; @@ -56,10 +55,7 @@ use crate::semantic_index::{ use crate::types::call::bind::MatchingOverloadIndex; use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::callable::CallableTypeKind; -use crate::types::class::{ - ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral, - DynamicMetaclassConflict, MethodDecorator, -}; +use crate::types::class::{ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, MethodDecorator}; use crate::types::constraints::{ConstraintSetBuilder, PathBounds, Solutions}; use crate::types::context::InNoTypeCheck; use crate::types::context::InferContext; @@ -70,19 +66,17 @@ use crate::types::diagnostic::{ INVALID_BASE, INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_BOUND, - INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NO_MATCHING_OVERLOAD, - POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE, SUBCLASS_OF_FINAL_CLASS, - UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, - UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, - hint_if_stdlib_attribute_exists_on_other_versions, report_attempted_protocol_instantiation, - report_bad_dunder_set_call, report_call_to_abstract_method, - report_cannot_pop_required_field_on_typed_dict, report_conflicting_metaclass_from_bases, - report_instance_layout_conflict, report_invalid_assignment, - report_invalid_attribute_assignment, report_invalid_class_match_pattern, - report_invalid_exception_caught, report_invalid_exception_cause, - report_invalid_exception_raised, report_invalid_exception_tuple_caught, - report_invalid_generator_yield_type, report_invalid_key_on_typed_dict, - report_invalid_type_checking_constant, + INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_MISSING_IMPLICIT_CALL, + POSSIBLY_MISSING_SUBMODULE, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, + UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, + UNUSED_AWAITABLE, hint_if_stdlib_attribute_exists_on_other_versions, + report_attempted_protocol_instantiation, report_bad_dunder_set_call, + report_call_to_abstract_method, report_cannot_pop_required_field_on_typed_dict, + report_invalid_assignment, report_invalid_attribute_assignment, + report_invalid_class_match_pattern, report_invalid_exception_caught, + report_invalid_exception_cause, report_invalid_exception_raised, + report_invalid_exception_tuple_caught, report_invalid_generator_yield_type, + report_invalid_key_on_typed_dict, report_invalid_type_checking_constant, report_match_pattern_against_non_runtime_checkable_protocol, report_match_pattern_against_typed_dict, report_possibly_missing_attribute, report_possibly_unresolved_reference, report_unsupported_augmented_assignment, @@ -113,8 +107,9 @@ use crate::types::{ MemberLookupPolicy, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, Signature, SpecialFormType, SubclassOfType, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, - TypedDictType, UnionBuilder, UnionType, binding_type, definition_expression_type, - infer_complete_scope_types, infer_scope_types, todo_type, + TypedDictType, UnionBuilder, UnionType, binding_type, + extract_fixed_length_iterable_element_types, infer_complete_scope_types, infer_scope_types, + todo_type, }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::UnpackPosition; @@ -128,9 +123,11 @@ mod final_attribute; mod function; mod imports; mod named_tuple; +mod new_class; mod paramspec_validation; mod post_inference; mod subscript; +mod type_call; mod type_expression; mod typed_dict; mod typevar; @@ -143,6 +140,27 @@ struct TypeAndRange<'db> { range: TextRange, } +/// Whether a dynamic class is being created via `type()` or `types.new_class()`. +/// +/// This is used to adjust validation rules and diagnostic messages for dynamic class +/// creation. For example, `types.new_class()` properly handles metaclasses and +/// `__mro_entries__`, so enum, `Generic`, and `TypedDict` bases are allowed +/// (unlike `type()`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DynamicClassKind { + TypeCall, + NewClass, +} + +impl DynamicClassKind { + const fn function_name(self) -> &'static str { + match self { + Self::TypeCall => "type()", + Self::NewClass => "types.new_class()", + } + } +} + /// A helper to track if we already know that declared and inferred types are the same. #[derive(Debug, Clone, PartialEq, Eq)] enum DeclaredAndInferredType<'db> { @@ -2924,6 +2942,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) } else if callable_type == Type::SpecialForm(SpecialFormType::TypedDict) { self.infer_typeddict_call_expression(call_expr, Some(definition)) + } else if let Some(function) = callable_type.as_function_literal() + && function.is_known(self.db(), KnownFunction::NewClass) + { + self.infer_new_class_call(call_expr, Some(definition)) } else { match callable_type .as_class_literal() @@ -3131,6 +3153,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_functional_typeddict_deferred(arguments); return; } + if let InferenceRegion::Deferred(definition) = self.region + && let Some(function) = func_ty.as_function_literal() + && function.is_known(self.db(), KnownFunction::NewClass) + { + self.infer_new_class_deferred(definition, value); + return; + } let mut constraint_tys = Vec::new(); for arg in arguments.args.iter().skip(1) { let constraint = self.infer_type_expression(arg); @@ -3352,374 +3381,49 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.typevar_binding_context = previous_context; } - /// Deferred inference for assigned `type()` calls. + /// Extract base classes from the bases argument of a `type()` or `types.new_class()` call. /// - /// Infers the bases argument that was skipped during initial inference to handle - /// forward references and recursive definitions. - fn infer_builtins_type_deferred(&mut self, definition: Definition<'db>, call_expr: &ast::Expr) { - let db = self.db(); - - let ast::Expr::Call(call) = call_expr else { - return; - }; - - // Get the already-inferred class type from the initial pass. - let inferred_type = definition_expression_type(db, definition, call_expr); - let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = inferred_type else { - return; - }; - - let [_name_arg, bases_arg, _namespace_arg] = &*call.arguments.args else { - return; - }; - - // Set the typevar binding context to allow legacy typevar binding in expressions - // like `Generic[T]`. This matches the context used during initial inference. - let previous_context = self.typevar_binding_context.replace(definition); - - // Infer the bases argument (this was skipped during initial inference). - let bases_type = self.infer_expression(bases_arg, TypeContext::default()); - - // Restore the previous context. - self.typevar_binding_context = previous_context; - - // Extract and validate bases. - let Some(bases) = self.extract_explicit_bases(bases_arg, bases_type) else { - return; - }; - - // Validate individual bases for special types that aren't allowed in dynamic classes. - let name = dynamic_class.name(db); - self.validate_dynamic_type_bases(bases_arg, &bases, name); - } - - /// Infer a call to `builtins.type()`. - /// - /// `builtins.type` has two overloads: a single-argument overload (e.g. `type("foo")`, - /// and a 3-argument `type(name, bases, dict)` overload. Both are handled here. - /// The `definition` parameter should be `Some()` if this call to `builtins.type()` - /// occurs on the right-hand side of an assignment statement that has a [`Definition`] - /// associated with it in the semantic index. - /// - /// If it's unclear which overload we should pick, we return `type[Unknown]`, - /// to avoid cascading errors later on. - fn infer_builtins_type_call( - &mut self, - call_expr: &ast::ExprCall, - definition: Option>, - ) -> Type<'db> { - let db = self.db(); - - let ast::Arguments { - args, - keywords, - range: _, - node_index: _, - } = &call_expr.arguments; - - for keyword in keywords { - self.infer_expression(&keyword.value, TypeContext::default()); - } - - let [name_arg, bases_arg, namespace_arg] = match &**args { - [single] => { - let arg_type = self.infer_expression(single, TypeContext::default()); - - return if keywords.is_empty() { - arg_type.dunder_class(db) - } else { - if keywords.iter().any(|keyword| keyword.arg.is_some()) - && let Some(builder) = - self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) - { - let mut diagnostic = builder - .into_diagnostic("No overload of class `type` matches arguments"); - diagnostic.help(format_args!( - "`builtins.type()` expects no keyword arguments", - )); - } - SubclassOfType::subclass_of_unknown() - }; - } - - [first, second] if second.is_starred_expr() => { - self.infer_expression(first, TypeContext::default()); - self.infer_expression(second, TypeContext::default()); - - match &**keywords { - [single] if single.arg.is_none() => { - return SubclassOfType::subclass_of_unknown(); - } - _ => { - if let Some(builder) = - self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) - { - let mut diagnostic = builder - .into_diagnostic("No overload of class `type` matches arguments"); - diagnostic.help(format_args!( - "`builtins.type()` expects no keyword arguments", - )); - } - - return SubclassOfType::subclass_of_unknown(); - } - } - } - - [name, bases, namespace] => [name, bases, namespace], - - _ => { - for arg in args { - self.infer_expression(arg, TypeContext::default()); - } - - if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { - let mut diagnostic = - builder.into_diagnostic("No overload of class `type` matches arguments"); - diagnostic.help(format_args!( - "`builtins.type()` can either be called with one or three \ - positional arguments (got {})", - args.len() - )); - } - - return SubclassOfType::subclass_of_unknown(); - } - }; - - let name_type = self.infer_expression(name_arg, TypeContext::default()); - - let namespace_type = self.infer_expression(namespace_arg, TypeContext::default()); - - // TODO: validate other keywords against `__init_subclass__` methods of superclasses - if keywords - .iter() - .filter_map(|keyword| keyword.arg.as_deref()) - .contains("metaclass") - { - if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { - let mut diagnostic = - builder.into_diagnostic("No overload of class `type` matches arguments"); - diagnostic - .help("The `metaclass` keyword argument is not supported in `type()` calls"); - } - } - - // If any argument is a starred expression, we can't know how many positional arguments - // we're receiving, so fall back to `type[Unknown]` to avoid false-positive errors. - if args.iter().any(ast::Expr::is_starred_expr) { - return SubclassOfType::subclass_of_unknown(); - } - - // Extract members from the namespace dict (third argument). - let (members, has_dynamic_namespace): (Box<[(ast::name::Name, Type<'db>)]>, bool) = - if let ast::Expr::Dict(dict) = namespace_arg { - // Check if all keys are string literal types. If any key is not a string literal - // type or is missing (spread), the namespace is considered dynamic. - let all_keys_are_string_literals = dict.items.iter().all(|item| { - item.key - .as_ref() - .is_some_and(|k| self.expression_type(k).is_string_literal()) - }); - let members = dict - .items - .iter() - .filter_map(|item| { - // Only extract items with string literal keys. - let key_expr = item.key.as_ref()?; - let key_name = self.expression_type(key_expr).as_string_literal()?; - let key_name = ast::name::Name::new(key_name.value(db)); - // Get the already-inferred type from when we inferred the dict above. - let value_ty = self.expression_type(&item.value); - Some((key_name, value_ty)) - }) - .collect(); - (members, !all_keys_are_string_literals) - } else if let Type::TypedDict(typed_dict) = namespace_type { - // `namespace` is a TypedDict instance. Extract known keys as members. - // TypedDicts are "open" (can have additional string keys), so this - // is still a dynamic namespace for unknown attributes. - let members: Box<[(ast::name::Name, Type<'db>)]> = typed_dict - .items(db) - .iter() - .map(|(name, field)| (name.clone(), field.declared_ty)) - .collect(); - (members, true) - } else { - // `namespace` is not a dict literal, so it's dynamic. - (Box::new([]), true) - }; - - if !matches!(namespace_type, Type::TypedDict(_)) - && !namespace_type.is_assignable_to( - db, - KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]), - ) - && let Some(builder) = self - .context - .report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg) - { - let mut diagnostic = builder - .into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`"); - diagnostic.set_primary_message(format_args!( - "Expected `dict[str, Any]`, found `{}`", - namespace_type.display(db) - )); - } - - // Extract name and base classes. - let name = if let Some(literal) = name_type.as_string_literal() { - Name::new(literal.value(db)) - } else { - if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) - && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) - { - let mut diagnostic = - builder.into_diagnostic("Invalid argument to parameter 1 (`name`) of `type()`"); - diagnostic.set_primary_message(format_args!( - "Expected `str`, found `{}`", - name_type.display(db) - )); - } - Name::new_static("") - }; - - let scope = self.scope(); - - // For assigned `type()` calls, bases inference is deferred to handle forward references - // and recursive references (e.g., `X = type("X", (tuple["X | None"],), {})`). - // This avoids expensive Salsa fixpoint iteration by deferring inference until the - // class type is already bound. For dangling calls, infer and extract bases eagerly - // (they'll be stored in the anchor and used for validation). - let explicit_bases = if definition.is_none() { - let bases_type = self.infer_expression(bases_arg, TypeContext::default()); - self.extract_explicit_bases(bases_arg, bases_type) - } else { - None - }; - - // Create the anchor for identifying this dynamic class. - // - For assigned `type()` calls, the Definition uniquely identifies the class, - // and bases inference is deferred. - // - For dangling calls, compute a relative offset from the scope's node index, - // and store the explicit bases directly (since they were inferred eagerly). - let anchor = if let Some(def) = definition { - // Register for deferred inference to infer bases and validate later. - self.deferred.insert(def); - DynamicClassAnchor::Definition(def) - } else { - let call_node_index = call_expr.node_index().load(); - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("scope anchor should not be NodeIndex::NONE"); - let call_u32 = call_node_index - .as_u32() - .expect("call node should not be NodeIndex::NONE"); - - // Use [Unknown] as fallback if bases extraction failed (e.g., not a tuple). - let anchor_bases = explicit_bases - .clone() - .unwrap_or_else(|| Box::from([Type::unknown()])); - - DynamicClassAnchor::ScopeOffset { - scope, - offset: call_u32 - anchor_u32, - explicit_bases: anchor_bases, - } - }; - - let dynamic_class = DynamicClassLiteral::new( - db, - name.clone(), - anchor, - members, - has_dynamic_namespace, - None, - ); - - // For dangling calls, validate bases eagerly. For assigned calls, validation is - // deferred along with bases inference. - if let Some(explicit_bases) = &explicit_bases { - // Validate bases and collect disjoint bases for diagnostics. - let mut disjoint_bases = - self.validate_dynamic_type_bases(bases_arg, explicit_bases, &name); - - // Check for MRO errors. - if report_dynamic_mro_errors(&self.context, dynamic_class, call_expr, bases_arg) { - // MRO succeeded, check for instance-layout-conflict. - disjoint_bases.remove_redundant_entries(db); - if disjoint_bases.len() > 1 { - report_instance_layout_conflict( - &self.context, - dynamic_class.header_range(db), - bases_arg.as_tuple_expr().map(|tuple| tuple.elts.as_slice()), - &disjoint_bases, - ); - } - } - - // Check for metaclass conflicts. - if let Err(DynamicMetaclassConflict { - metaclass1, - base1, - metaclass2, - base2, - }) = dynamic_class.try_metaclass(db) - { - report_conflicting_metaclass_from_bases( - &self.context, - call_expr.into(), - dynamic_class.name(db), - metaclass1, - base1.display(db), - metaclass2, - base2.display(db), - ); - } - } - - Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) - } - - /// Extract explicit base types from a bases tuple type. - /// - /// Emits a diagnostic if `bases_type` is not a valid tuple type. + /// Emits a diagnostic if `bases_type` is not a valid bases iterable for the given kind. /// /// Returns `None` if the bases cannot be extracted. fn extract_explicit_bases( &mut self, bases_node: &ast::Expr, bases_type: Type<'db>, + kind: DynamicClassKind, ) -> Option]>> { let db = self.db(); - // Check if bases_type is a tuple; emit diagnostic if not. - if bases_type.tuple_instance_spec(db).is_none() - && !bases_type.is_assignable_to( - db, - Type::homogeneous_tuple(db, KnownClass::Type.to_instance(db)), - ) + let fn_name = kind.function_name(); + let formal_parameter_type = match kind { + DynamicClassKind::TypeCall => Type::homogeneous_tuple(db, Type::object()), + DynamicClassKind::NewClass => { + KnownClass::Iterable.to_specialized_instance(db, &[Type::object()]) + } + }; + + if !bases_type.is_assignable_to(db, formal_parameter_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, bases_node) { - let mut diagnostic = - builder.into_diagnostic("Invalid argument to parameter 2 (`bases`) of `type()`"); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid argument to parameter 2 (`bases`) of `{fn_name}`" + )); diagnostic.set_primary_message(format_args!( - "Expected `tuple[type, ...]`, found `{}`", + "Expected `{}`, found `{}`", + formal_parameter_type.display(db), bases_type.display(db) )); } - bases_type - .fixed_tuple_elements(db) - .map(Cow::into_owned) - .map(Into::into) + + extract_fixed_length_iterable_element_types(db, bases_node, |expr| { + self.expression_type(expr) + }) } - /// Validate base classes from the second argument of a `type()` call. + /// Validate base classes from the second argument of a `type()` or `types.new_class()` call. /// /// This validates bases that are valid `ClassBase` variants but aren't allowed - /// for dynamic classes created via `type()`. Invalid bases that can't be converted - /// to `ClassBase` at all are handled by `DynamicMroErrorKind::InvalidBases`. + /// for dynamic classes. Invalid bases that can't be converted to `ClassBase` at all + /// are handled by `DynamicMroErrorKind::InvalidBases`. /// /// Returns disjoint bases found (for instance-layout-conflict checking). fn validate_dynamic_type_bases( @@ -3727,6 +3431,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bases_node: &ast::Expr, bases: &[Type<'db>], name: &Name, + kind: DynamicClassKind, ) -> IncompatibleBases<'db> { let db = self.db(); @@ -3735,6 +3440,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut disjoint_bases = IncompatibleBases::default(); + let fn_name = kind.function_name(); + // Check each base for special cases that are not allowed for dynamic classes. for (idx, base) in bases.iter().enumerate() { let diagnostic_node = bases_tuple_elts @@ -3748,27 +3455,38 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; // Check for special bases that are not allowed for dynamic classes. - // Dynamic classes can't be generic, protocols, TypedDicts, or enums. + // + // Generic and TypedDict bases rely on special typing semantics that ty cannot yet + // model for dynamically-created classes, so we reject them for both `type()` and + // `types.new_class()`. + // + // Protocol works with both, but ty can't yet represent a dynamically-created + // protocol class, so we emit a warning. + // // (`NamedTuple` is rejected earlier: `try_from_type` returns `None` // without a concrete subclass, so it's reported as an `InvalidBases` MRO error.) match class_base { ClassBase::Generic | ClassBase::TypedDict => { if let Some(builder) = self.context.report_lint(&INVALID_BASE, diagnostic_node) { - let mut diagnostic = - builder.into_diagnostic("Invalid base for class created via `type()`"); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid base for class created via `{fn_name}`" + )); diagnostic .set_primary_message(format_args!("Has type `{}`", base.display(db))); match class_base { ClassBase::Generic => { - diagnostic.info("Classes created via `type()` cannot be generic"); + diagnostic.info(format_args!( + "Classes created via `{fn_name}` cannot be generic" + )); diagnostic.info(format_args!( "Consider using `class {name}(Generic[...]): ...` instead" )); } ClassBase::TypedDict => { - diagnostic - .info("Classes created via `type()` cannot be TypedDicts"); + diagnostic.info(format_args!( + "Classes created via `{fn_name}` cannot be TypedDicts" + )); diagnostic.info(format_args!( "Consider using `TypedDict(\"{name}\", {{}})` instead" )); @@ -3782,11 +3500,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .context .report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_node) { - let mut diagnostic = builder - .into_diagnostic("Unsupported base for class created via `type()`"); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Unsupported base for class created via `{fn_name}`" + )); diagnostic .set_primary_message(format_args!("Has type `{}`", base.display(db))); - diagnostic.info("Classes created via `type()` cannot be protocols"); + diagnostic.info(format_args!( + "Classes created via `{fn_name}` cannot be protocols", + )); diagnostic.info(format_args!( "Consider using `class {name}(Protocol): ...` instead" )); @@ -3814,34 +3535,40 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } - // Enum subclasses require the EnumMeta metaclass, which - // expects special dict attributes that `type()` doesn't provide. - if let Some((static_class, _)) = class_type.static_class_literal(db) { - if is_enum_class_by_inheritance(db, static_class) { - if let Some(builder) = - self.context.report_lint(&INVALID_BASE, diagnostic_node) - { - let mut diagnostic = builder - .into_diagnostic("Invalid base for class created via `type()`"); - diagnostic.set_primary_message(format_args!( - "Has type `{}`", - base.display(db) - )); - diagnostic - .info("Creating an enum class via `type()` is not supported"); - diagnostic.info(format_args!( - "Consider using `Enum(\"{name}\", [])` instead" - )); - } - // Still collect disjoint bases even for invalid bases. - if let Some(disjoint_base) = class_type.nearest_disjoint_base(db) { - disjoint_bases.insert( - disjoint_base, - idx, - class_type.class_literal(db), - ); + // Enum subclasses require the EnumMeta metaclass, which expects special + // dict attributes that `type()` doesn't provide. `types.new_class()` + // handles metaclasses properly, so this restriction only applies to + // `type()` calls. + if kind == DynamicClassKind::TypeCall { + if let Some((static_class, _)) = class_type.static_class_literal(db) { + if is_enum_class_by_inheritance(db, static_class) { + if let Some(builder) = + self.context.report_lint(&INVALID_BASE, diagnostic_node) + { + let mut diagnostic = builder.into_diagnostic( + "Invalid base for class created via `type()`", + ); + diagnostic.set_primary_message(format_args!( + "Has type `{}`", + base.display(db) + )); + diagnostic.info( + "Creating an enum class via `type()` is not supported", + ); + diagnostic.info(format_args!( + "Consider using `Enum(\"{name}\", [])` instead" + )); + } + // Still collect disjoint bases even for invalid bases. + if let Some(disjoint_base) = class_type.nearest_disjoint_base(db) { + disjoint_bases.insert( + disjoint_base, + idx, + class_type.class_literal(db), + ); + } + continue; } - continue; } } @@ -7010,6 +6737,63 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )) } + /// Infer the variadic argument types needed for call binding and emit the shared diagnostics + /// for invalid `*args` and `**kwargs` inputs. + fn prepare_call_arguments<'a>( + &mut self, + arguments: &'a ast::Arguments, + ) -> CallArguments<'a, 'db> { + let call_arguments = + CallArguments::from_arguments(arguments, |arg_or_keyword, splatted_value| { + let ty = self.infer_expression(splatted_value, TypeContext::default()); + if let ast::ArgOrKeyword::Arg(argument) = arg_or_keyword + && argument.is_starred_expr() + { + self.store_expression_type(argument, ty); + } else if let Some(ty) = self.try_narrow_dict_kwargs(ty, arg_or_keyword) { + return ty; + } + + ty + }); + + for arg in &arguments.args { + if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = arg { + let iterable_type = self.expression_type(value); + if let Err(err) = iterable_type.try_iterate(self.db()) { + err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); + } + } + } + + for keyword in arguments + .keywords + .iter() + .filter(|keyword| keyword.arg.is_none()) + { + let mapping_type = self.expression_type(&keyword.value); + + if mapping_type.as_paramspec_typevar(self.db()).is_some() + || mapping_type.unpack_keys_and_items(self.db()).is_some() + { + continue; + } + + let Some(builder) = self + .context + .report_lint(&INVALID_ARGUMENT_TYPE, &keyword.value) + else { + continue; + }; + + builder + .into_diagnostic("Argument expression after ** must be a mapping type") + .set_primary_message(format_args!("Found `{}`", mapping_type.display(self.db()))); + } + + call_arguments + } + fn infer_call_expression( &mut self, call_expression: &ast::ExprCall, @@ -7080,6 +6864,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return self.infer_builtins_type_call(call_expression, None); } + // Handle `types.new_class(name, bases, ...)`. + if let Some(function) = callable_type.as_function_literal() + && function.is_known(self.db(), KnownFunction::NewClass) + { + return self.infer_new_class_call(call_expression, None); + } + // Handle `typing.NamedTuple(typename, fields)` and `collections.namedtuple(typename, field_names)`. if let Some(namedtuple_kind) = NamedTupleKind::from_type(self.db(), callable_type) { return self.infer_namedtuple_call_expression(call_expression, None, namedtuple_kind); @@ -7092,51 +6883,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // We don't call `Type::try_call`, because we want to perform type inference on the // arguments after matching them to parameters, but before checking that the argument types // are assignable to any parameter annotations. - let mut call_arguments = - CallArguments::from_arguments(arguments, |arg_or_keyword, splatted_value| { - let ty = self.infer_expression(splatted_value, TypeContext::default()); - if let ast::ArgOrKeyword::Arg(argument) = arg_or_keyword - && argument.is_starred_expr() - { - self.store_expression_type(argument, ty); - } else if let Some(ty) = self.try_narrow_dict_kwargs(ty, arg_or_keyword) { - return ty; - } - - ty - }); - - // Validate that starred arguments are iterable. - for arg in &arguments.args { - if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = arg { - let iterable_type = self.expression_type(value); - if let Err(err) = iterable_type.try_iterate(self.db()) { - err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); - } - } - } - - // Validate that double-starred keyword arguments are mappings. - for keyword in arguments.keywords.iter().filter(|k| k.arg.is_none()) { - let mapping_type = self.expression_type(&keyword.value); - - if mapping_type.as_paramspec_typevar(self.db()).is_some() - || mapping_type.unpack_keys_and_items(self.db()).is_some() - { - continue; - } - - let Some(builder) = self - .context - .report_lint(&INVALID_ARGUMENT_TYPE, &keyword.value) - else { - continue; - }; - - builder - .into_diagnostic("Argument expression after ** must be a mapping type") - .set_primary_message(format_args!("Found `{}`", mapping_type.display(self.db()))); - } + let mut call_arguments = self.prepare_call_arguments(arguments); if callable_type.is_notimplemented(self.db()) { if let Some(builder) = self diff --git a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs index 350b1ce65b8e5..9abdff2a75691 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs @@ -11,6 +11,7 @@ use crate::{ INVALID_ARGUMENT_TYPE, INVALID_NAMED_TUPLE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, }, + extract_fixed_length_iterable_element_types, function::KnownFunction, infer::TypeInferenceBuilder, }, @@ -205,41 +206,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> { match arg.id.as_str() { "defaults" if kind.is_collections() => { defaults_kw = Some(kw); - // Extract element types from AST literals (using already-inferred types) - // or fall back to the inferred tuple spec. - match &kw.value { - ast::Expr::List(list) => { - // Elements were already inferred when we inferred kw.value above. - default_types = list - .elts - .iter() - .map(|elt| self.expression_type(elt)) - .collect(); - } - ast::Expr::Tuple(tuple) => { - // Elements were already inferred when we inferred kw.value above. - default_types = tuple - .elts - .iter() - .map(|elt| self.expression_type(elt)) - .collect(); - } - _ => { - // Fall back to using the already-inferred type. - // Try to extract element types from tuple. - if let Some(spec) = kw_type.exact_tuple_instance_spec(db) - && let Some(fixed) = spec.as_fixed_length() - { - default_types = fixed.all_elements().to_vec(); - } else { - // Can't determine individual types; use Any for each element. - let count = kw_type - .exact_tuple_instance_spec(db) - .and_then(|spec| spec.len().maximum()) - .unwrap_or(0); - default_types = vec![Type::any(); count]; - } - } + if let Some(element_types) = + extract_fixed_length_iterable_element_types(db, &kw.value, |expr| { + self.expression_type(expr) + }) + { + default_types = element_types.into_vec(); + } else { + // Can't determine individual types; use Any for each element. + let count = kw_type + .exact_tuple_instance_spec(db) + .and_then(|spec| spec.len().maximum()) + .unwrap_or(0); + default_types = vec![Type::any(); count]; } // Emit diagnostic for invalid types (not Iterable[Any] | None). let iterable_any = @@ -436,32 +415,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(Name::new) .collect(), ) - } else if let Some(tuple_spec) = fields_type.tuple_instance_spec(db) - && let Some(fixed_tuple) = tuple_spec.as_fixed_length() - { - // Handle list/tuple of strings (must be fixed-length). - fixed_tuple - .all_elements() - .iter() - .map(|elt| elt.as_string_literal().map(|s| Name::new(s.value(db)))) - .collect() } else { - // Get the elements from the list or tuple literal. - let elements = match fields_arg { - ast::Expr::List(list) => Some(&list.elts), - ast::Expr::Tuple(tuple) => Some(&tuple.elts), - _ => None, - }; - - elements.and_then(|elts| { - elts.iter() - .map(|elt| { - // Each element should be a string literal. - let field_ty = self.expression_type(elt); - let field_lit = field_ty.as_string_literal()?; - Some(Name::new(field_lit.value(db))) - }) - .collect::>() + extract_fixed_length_iterable_element_types(db, fields_arg, |expr| { + self.expression_type(expr) + }) + .and_then(|field_types| { + field_types + .iter() + .map(|elt| elt.as_string_literal().map(|s| Name::new(s.value(db)))) + .collect() }) }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/new_class.rs b/crates/ty_python_semantic/src/types/infer/builder/new_class.rs new file mode 100644 index 0000000000000..47d1e1ba0bc60 --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/new_class.rs @@ -0,0 +1,284 @@ +use super::{ArgumentsIter, DynamicClassKind, TypeInferenceBuilder}; +use crate::semantic_index::definition::Definition; +use crate::types::class::{ + ClassLiteral, DynamicClassAnchor, DynamicClassLiteral, DynamicMetaclassConflict, + dynamic_class_bases_argument, +}; +use crate::types::diagnostic::{ + INVALID_ARGUMENT_TYPE, NO_MATCHING_OVERLOAD, report_conflicting_metaclass_from_bases, + report_instance_layout_conflict, +}; +use crate::types::{KnownClass, SubclassOfType, Type, TypeContext, definition_expression_type}; +use ruff_python_ast::{self as ast, HasNodeIndex, NodeIndex}; + +impl<'db> TypeInferenceBuilder<'db, '_> { + /// Infer a `types.new_class(name, bases, kwds, exec_body)` call. + /// + /// This method *does not* call `infer_expression` on the object being called; + /// it is assumed that the type for this AST node has already been inferred before this method + /// is called. + pub(super) fn infer_new_class_call( + &mut self, + call_expr: &ast::ExprCall, + definition: Option>, + ) -> Type<'db> { + let db = self.db(); + + let ast::Arguments { + args, + keywords, + range: _, + node_index: _, + } = &call_expr.arguments; + + // `new_class(name, bases=(), kwds=None, exec_body=None)` + // We need at least the `name` argument. + let no_positional_args = args.is_empty(); + if no_positional_args { + // Check if `name` is provided as a keyword argument. + let name_keyword = keywords.iter().find(|kw| kw.arg.as_deref() == Some("name")); + + if name_keyword.is_none() { + // Infer all keyword values for side effects. + for keyword in keywords { + self.infer_expression(&keyword.value, TypeContext::default()); + } + if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { + builder.into_diagnostic("No overload of `types.new_class` matches arguments"); + } + return SubclassOfType::subclass_of_unknown(); + } + } + + // Find the arguments we treat specially while preserving normal call-binding diagnostics. + let name_node = args.first().or_else(|| { + keywords + .iter() + .find(|kw| kw.arg.as_deref() == Some("name")) + .map(|kw| &kw.value) + }); + let bases_arg = dynamic_class_bases_argument(&call_expr.arguments); + + self.validate_new_class_call_arguments(call_expr, name_node, bases_arg, definition); + + let name_type = name_node + .map(|node| self.expression_type(node)) + .unwrap_or_else(Type::unknown); + + let name = if let Some(literal) = name_type.as_string_literal() { + ast::name::Name::new(literal.value(db)) + } else { + if let Some(name_node) = name_node + && !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_node) + { + let mut diagnostic = builder.into_diagnostic( + "Invalid argument to parameter 1 (`name`) of `types.new_class()`", + ); + diagnostic.set_primary_message(format_args!( + "Expected `str`, found `{}`", + name_type.display(db) + )); + } + ast::name::Name::new_static("") + }; + + // For assigned `new_class()` calls, bases inference is deferred to handle forward + // references and recursive references, matching the `type()` pattern. For dangling + // calls, infer and extract bases eagerly (they'll be stored in the anchor). + let explicit_bases: Option]>> = if definition.is_none() { + if let Some(bases_arg) = bases_arg { + let bases_type = self.expression_type(bases_arg); + self.extract_explicit_bases(bases_arg, bases_type, DynamicClassKind::NewClass) + } else { + Some(Box::from([])) + } + } else { + None + }; + + let scope = self.scope(); + + // Create the anchor for identifying this dynamic class. + let anchor = if let Some(def) = definition { + // Register for deferred inference to infer bases and validate later. + self.deferred.insert(def); + DynamicClassAnchor::Definition(def) + } else { + let call_node_index = call_expr.node_index().load(); + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("scope anchor should not be NodeIndex::NONE"); + let call_u32 = call_node_index + .as_u32() + .expect("call node should not be NodeIndex::NONE"); + + // Use [Unknown] as fallback if bases extraction failed (e.g., not a tuple). + let anchor_bases = explicit_bases + .clone() + .unwrap_or_else(|| Box::from([Type::unknown()])); + + DynamicClassAnchor::ScopeOffset { + scope, + offset: call_u32 - anchor_u32, + explicit_bases: anchor_bases, + } + }; + + // `new_class()` doesn't accept a namespace dict, so members are always empty. + // If `exec_body` is provided (and is not `None`), it can populate the namespace + // dynamically, so we mark it as dynamic. Without `exec_body`, no members can be added. + // + // TODO: Model `kwds`, especially `{"metaclass": Meta}`. `types.new_class()` uses the + // third argument for explicit metaclass overrides, but we currently only account for + // metaclass behavior that follows from the resolved bases. + let exec_body_arg = args.get(3).or_else(|| { + keywords + .iter() + .find(|kw| kw.arg.as_deref() == Some("exec_body")) + .map(|kw| &kw.value) + }); + let has_exec_body = exec_body_arg.is_some_and(|arg| !arg.is_none_literal_expr()); + let members: Box<[(ast::name::Name, Type<'db>)]> = Box::new([]); + let dynamic_class = + DynamicClassLiteral::new(db, name.clone(), anchor, members, has_exec_body, None); + + // For dangling calls, validate bases eagerly. For assigned calls, validation is + // deferred along with bases inference. + if let Some(explicit_bases) = &explicit_bases + && let Some(bases_arg) = bases_arg + { + let mut disjoint_bases = self.validate_dynamic_type_bases( + bases_arg, + explicit_bases, + &name, + DynamicClassKind::NewClass, + ); + + if super::report_dynamic_mro_errors(&self.context, dynamic_class, call_expr, bases_arg) + { + // MRO succeeded, check for instance-layout-conflict. + disjoint_bases.remove_redundant_entries(db); + if disjoint_bases.len() > 1 { + report_instance_layout_conflict( + &self.context, + dynamic_class.header_range(db), + bases_arg.as_tuple_expr().map(|tuple| tuple.elts.as_slice()), + &disjoint_bases, + ); + } + } + + // Check for metaclass conflicts. + if let Err(DynamicMetaclassConflict { + metaclass1, + base1, + metaclass2, + base2, + }) = dynamic_class.try_metaclass(db) + { + report_conflicting_metaclass_from_bases( + &self.context, + call_expr.into(), + dynamic_class.name(db), + metaclass1, + base1.display(db), + metaclass2, + base2.display(db), + ); + } + } + + Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) + } + + /// Deferred inference for assigned `types.new_class()` calls. + /// + /// Infers the bases argument that was skipped during initial inference to handle + /// forward references and recursive definitions. + pub(super) fn infer_new_class_deferred( + &mut self, + definition: Definition<'db>, + call_expr: &ast::Expr, + ) { + let db = self.db(); + + let ast::Expr::Call(call) = call_expr else { + return; + }; + + // Get the already-inferred class type from the initial pass. + let inferred_type = definition_expression_type(db, definition, call_expr); + let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = inferred_type else { + return; + }; + + let Some(bases_arg) = dynamic_class_bases_argument(&call.arguments) else { + return; + }; + + // Set the typevar binding context to allow legacy typevar binding in expressions + // like `Generic[T]`. This matches the context used during initial inference. + let previous_context = self.typevar_binding_context.replace(definition); + + // Infer the bases argument (this was skipped during initial inference). + let bases_type = self.infer_expression(bases_arg, TypeContext::default()); + + // Restore the previous context. + self.typevar_binding_context = previous_context; + + // Extract and validate bases. + let Some(bases) = + self.extract_explicit_bases(bases_arg, bases_type, DynamicClassKind::NewClass) + else { + return; + }; + + // Validate individual bases for special types that aren't allowed in dynamic classes. + let name = dynamic_class.name(db); + self.validate_dynamic_type_bases(bases_arg, &bases, name, DynamicClassKind::NewClass); + } + + /// Preserve normal call-binding diagnostics for `types.new_class()` while still allowing + /// special inference of the name and bases arguments. + fn validate_new_class_call_arguments( + &mut self, + call_expr: &ast::ExprCall, + name_node: Option<&ast::Expr>, + bases_arg: Option<&ast::Expr>, + definition: Option>, + ) { + let db = self.db(); + let callable_type = self.expression_type(call_expr.func.as_ref()); + let iterable_object = KnownClass::Iterable.to_specialized_instance(db, &[Type::object()]); + let mut call_arguments = self.prepare_call_arguments(&call_expr.arguments); + + let mut bindings = callable_type + .bindings(db) + .match_parameters(db, &call_arguments); + let bindings_result = self.infer_and_check_argument_types( + ArgumentsIter::from_ast(&call_expr.arguments), + &mut call_arguments, + &mut |builder, (_, expr, tcx)| { + if name_node.is_some_and(|name| std::ptr::eq(expr, name)) { + let _ = builder.infer_expression(expr, tcx); + KnownClass::Str.to_instance(builder.db()) + } else if bases_arg.is_some_and(|bases| std::ptr::eq(expr, bases)) { + if definition.is_none() { + let _ = builder.infer_expression(expr, tcx); + } + iterable_object + } else { + builder.infer_expression(expr, tcx) + } + }, + &mut bindings, + TypeContext::default(), + ); + + if bindings_result.is_err() { + bindings.report_diagnostics(&self.context, call_expr.into()); + } + } +} diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs index 7c1eb3dddfa92..dd510f2422215 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs @@ -2,7 +2,7 @@ use crate::{ semantic_index::definition::{Definition, DefinitionKind}, types::{ ClassLiteral, Type, binding_type, - class::{DynamicClassAnchor, DynamicMetaclassConflict}, + class::{DynamicClassAnchor, DynamicMetaclassConflict, dynamic_class_bases_argument}, context::InferContext, diagnostic::{ IncompatibleBases, report_conflicting_metaclass_from_bases, @@ -43,8 +43,7 @@ pub(crate) fn check_dynamic_class_definition<'db>( return; }; - // A valid 3-argument type() call must have a `bases` argument. - let Some(bases) = call_expr.arguments.args.get(1) else { + let Some(bases) = dynamic_class_bases_argument(&call_expr.arguments) else { return; }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_call.rs b/crates/ty_python_semantic/src/types/infer/builder/type_call.rs new file mode 100644 index 0000000000000..e9a1a6ba3e20e --- /dev/null +++ b/crates/ty_python_semantic/src/types/infer/builder/type_call.rs @@ -0,0 +1,354 @@ +use super::{DynamicClassKind, TypeInferenceBuilder, report_dynamic_mro_errors}; +use crate::semantic_index::definition::Definition; +use crate::types::class::{ + ClassLiteral, DynamicClassAnchor, DynamicClassLiteral, DynamicMetaclassConflict, +}; +use crate::types::diagnostic::{ + INVALID_ARGUMENT_TYPE, NO_MATCHING_OVERLOAD, report_conflicting_metaclass_from_bases, + report_instance_layout_conflict, +}; +use crate::types::{KnownClass, SubclassOfType, Type, TypeContext, definition_expression_type}; +use ruff_python_ast::name::Name; +use ruff_python_ast::{self as ast, HasNodeIndex, NodeIndex}; + +impl<'db> TypeInferenceBuilder<'db, '_> { + /// Infer a call to `builtins.type()`. + /// + /// `builtins.type` has two overloads: a single-argument overload (e.g. `type("foo")`, + /// and a 3-argument `type(name, bases, dict)` overload. Both are handled here. + /// The `definition` parameter should be `Some()` if this call to `builtins.type()` + /// occurs on the right-hand side of an assignment statement that has a [`Definition`] + /// associated with it in the semantic index. + /// + /// If it's unclear which overload we should pick, we return `type[Unknown]`, + /// to avoid cascading errors later on. + pub(super) fn infer_builtins_type_call( + &mut self, + call_expr: &ast::ExprCall, + definition: Option>, + ) -> Type<'db> { + let db = self.db(); + + let ast::Arguments { + args, + keywords, + range: _, + node_index: _, + } = &call_expr.arguments; + + for keyword in keywords { + self.infer_expression(&keyword.value, TypeContext::default()); + } + + let [name_arg, bases_arg, namespace_arg] = match &**args { + [single] => { + let arg_type = self.infer_expression(single, TypeContext::default()); + + return if keywords.is_empty() { + arg_type.dunder_class(db) + } else { + if keywords.iter().any(|keyword| keyword.arg.is_some()) + && let Some(builder) = + self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) + { + let mut diagnostic = builder + .into_diagnostic("No overload of class `type` matches arguments"); + diagnostic.help(format_args!( + "`builtins.type()` expects no keyword arguments", + )); + } + SubclassOfType::subclass_of_unknown() + }; + } + + [first, second] if second.is_starred_expr() => { + self.infer_expression(first, TypeContext::default()); + self.infer_expression(second, TypeContext::default()); + + match &**keywords { + [single] if single.arg.is_none() => { + return SubclassOfType::subclass_of_unknown(); + } + _ => { + if let Some(builder) = + self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) + { + let mut diagnostic = builder + .into_diagnostic("No overload of class `type` matches arguments"); + diagnostic.help(format_args!( + "`builtins.type()` expects no keyword arguments", + )); + } + + return SubclassOfType::subclass_of_unknown(); + } + } + } + + [name, bases, namespace] => [name, bases, namespace], + + _ => { + for arg in args { + self.infer_expression(arg, TypeContext::default()); + } + + if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { + let mut diagnostic = + builder.into_diagnostic("No overload of class `type` matches arguments"); + diagnostic.help(format_args!( + "`builtins.type()` can either be called with one or three \ + positional arguments (got {})", + args.len() + )); + } + + return SubclassOfType::subclass_of_unknown(); + } + }; + + let name_type = self.infer_expression(name_arg, TypeContext::default()); + + let namespace_type = self.infer_expression(namespace_arg, TypeContext::default()); + + // TODO: validate other keywords against `__init_subclass__` methods of superclasses + if keywords + .iter() + .any(|keyword| keyword.arg.as_deref() == Some("metaclass")) + { + if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { + let mut diagnostic = + builder.into_diagnostic("No overload of class `type` matches arguments"); + diagnostic + .help("The `metaclass` keyword argument is not supported in `type()` calls"); + } + } + + // If any argument is a starred expression, we can't know how many positional arguments + // we're receiving, so fall back to `type[Unknown]` to avoid false-positive errors. + if args.iter().any(ast::Expr::is_starred_expr) { + return SubclassOfType::subclass_of_unknown(); + } + + // Extract members from the namespace dict (third argument). + let (members, has_dynamic_namespace): (Box<[(ast::name::Name, Type<'db>)]>, bool) = + if let ast::Expr::Dict(dict) = namespace_arg { + // Check if all keys are string literal types. If any key is not a string literal + // type or is missing (spread), the namespace is considered dynamic. + let all_keys_are_string_literals = dict.items.iter().all(|item| { + item.key + .as_ref() + .is_some_and(|k| self.expression_type(k).is_string_literal()) + }); + let members = dict + .items + .iter() + .filter_map(|item| { + // Only extract items with string literal keys. + let key_expr = item.key.as_ref()?; + let key_name = self.expression_type(key_expr).as_string_literal()?; + let key_name = ast::name::Name::new(key_name.value(db)); + // Get the already-inferred type from when we inferred the dict above. + let value_ty = self.expression_type(&item.value); + Some((key_name, value_ty)) + }) + .collect(); + (members, !all_keys_are_string_literals) + } else if let Type::TypedDict(typed_dict) = namespace_type { + // `namespace` is a TypedDict instance. Extract known keys as members. + // TypedDicts are "open" (can have additional string keys), so this + // is still a dynamic namespace for unknown attributes. + let members: Box<[(ast::name::Name, Type<'db>)]> = typed_dict + .items(db) + .iter() + .map(|(name, field)| (name.clone(), field.declared_ty)) + .collect(); + (members, true) + } else { + // `namespace` is not a dict literal, so it's dynamic. + (Box::new([]), true) + }; + + if !matches!(namespace_type, Type::TypedDict(_)) + && !namespace_type.is_assignable_to( + db, + KnownClass::Dict + .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]), + ) + && let Some(builder) = self + .context + .report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg) + { + let mut diagnostic = builder + .into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`"); + diagnostic.set_primary_message(format_args!( + "Expected `dict[str, Any]`, found `{}`", + namespace_type.display(db) + )); + } + + // Extract name and base classes. + let name = if let Some(literal) = name_type.as_string_literal() { + Name::new(literal.value(db)) + } else { + if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) + { + let mut diagnostic = + builder.into_diagnostic("Invalid argument to parameter 1 (`name`) of `type()`"); + diagnostic.set_primary_message(format_args!( + "Expected `str`, found `{}`", + name_type.display(db) + )); + } + Name::new_static("") + }; + + let scope = self.scope(); + + // For assigned `type()` calls, bases inference is deferred to handle forward references + // and recursive references (e.g., `X = type("X", (tuple["X | None"],), {})`). + // This avoids expensive Salsa fixpoint iteration by deferring inference until the + // class type is already bound. For dangling calls, infer and extract bases eagerly + // (they'll be stored in the anchor and used for validation). + let explicit_bases = if definition.is_none() { + let bases_type = self.infer_expression(bases_arg, TypeContext::default()); + self.extract_explicit_bases(bases_arg, bases_type, DynamicClassKind::TypeCall) + } else { + None + }; + + // Create the anchor for identifying this dynamic class. + // - For assigned `type()` calls, the Definition uniquely identifies the class, + // and bases inference is deferred. + // - For dangling calls, compute a relative offset from the scope's node index, + // and store the explicit bases directly (since they were inferred eagerly). + let anchor = if let Some(def) = definition { + // Register for deferred inference to infer bases and validate later. + self.deferred.insert(def); + DynamicClassAnchor::Definition(def) + } else { + let call_node_index = call_expr.node_index().load(); + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("scope anchor should not be NodeIndex::NONE"); + let call_u32 = call_node_index + .as_u32() + .expect("call node should not be NodeIndex::NONE"); + + // Use [Unknown] as fallback if bases extraction failed (e.g., not a tuple). + let anchor_bases = explicit_bases + .clone() + .unwrap_or_else(|| Box::from([Type::unknown()])); + + DynamicClassAnchor::ScopeOffset { + scope, + offset: call_u32 - anchor_u32, + explicit_bases: anchor_bases, + } + }; + + let dynamic_class = DynamicClassLiteral::new( + db, + name.clone(), + anchor, + members, + has_dynamic_namespace, + None, + ); + + // For dangling calls, validate bases eagerly. For assigned calls, validation is + // deferred along with bases inference. + if let Some(explicit_bases) = &explicit_bases { + // Validate bases and collect disjoint bases for diagnostics. + let mut disjoint_bases = self.validate_dynamic_type_bases( + bases_arg, + explicit_bases, + &name, + DynamicClassKind::TypeCall, + ); + + // Check for MRO errors. + if report_dynamic_mro_errors(&self.context, dynamic_class, call_expr, bases_arg) { + // MRO succeeded, check for instance-layout-conflict. + disjoint_bases.remove_redundant_entries(db); + if disjoint_bases.len() > 1 { + report_instance_layout_conflict( + &self.context, + dynamic_class.header_range(db), + bases_arg.as_tuple_expr().map(|tuple| tuple.elts.as_slice()), + &disjoint_bases, + ); + } + } + + // Check for metaclass conflicts. + if let Err(DynamicMetaclassConflict { + metaclass1, + base1, + metaclass2, + base2, + }) = dynamic_class.try_metaclass(db) + { + report_conflicting_metaclass_from_bases( + &self.context, + call_expr.into(), + dynamic_class.name(db), + metaclass1, + base1.display(db), + metaclass2, + base2.display(db), + ); + } + } + + Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) + } + + /// Deferred inference for assigned `type()` calls. + /// + /// Infers the bases argument that was skipped during initial inference to handle + /// forward references and recursive definitions. + pub(super) fn infer_builtins_type_deferred( + &mut self, + definition: Definition<'db>, + call_expr: &ast::Expr, + ) { + let db = self.db(); + + let ast::Expr::Call(call) = call_expr else { + return; + }; + + // Get the already-inferred class type from the initial pass. + let inferred_type = definition_expression_type(db, definition, call_expr); + let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = inferred_type else { + return; + }; + + let [_name_arg, bases_arg, _namespace_arg] = &*call.arguments.args else { + return; + }; + + // Set the typevar binding context to allow legacy typevar binding in expressions + // like `Generic[T]`. This matches the context used during initial inference. + let previous_context = self.typevar_binding_context.replace(definition); + + // Infer the bases argument (this was skipped during initial inference). + let bases_type = self.infer_expression(bases_arg, TypeContext::default()); + + // Restore the previous context. + self.typevar_binding_context = previous_context; + + // Extract and validate bases. + let Some(bases) = + self.extract_explicit_bases(bases_arg, bases_type, DynamicClassKind::TypeCall) + else { + return; + }; + + // Validate individual bases for special types that aren't allowed in dynamic classes. + let name = dynamic_class.name(db); + self.validate_dynamic_type_bases(bases_arg, &bases, name, DynamicClassKind::TypeCall); + } +} diff --git a/crates/ty_python_semantic/src/types/iteration.rs b/crates/ty_python_semantic/src/types/iteration.rs index 2ba39abfa07ed..2f6ce9684adfa 100644 --- a/crates/ty_python_semantic/src/types/iteration.rs +++ b/crates/ty_python_semantic/src/types/iteration.rs @@ -14,6 +14,55 @@ use crate::{ use ruff_python_ast as ast; use std::borrow::Cow; +/// Extract the element types from an expression with a statically known fixed-length iteration. +/// +/// List and tuple literals are expanded directly so we preserve precise element types, including +/// recursively unpacking starred elements whose iterables are also fixed-length. +pub(crate) fn extract_fixed_length_iterable_element_types<'db>( + db: &'db dyn Db, + iterable: &ast::Expr, + mut expression_type: impl FnMut(&ast::Expr) -> Type<'db>, +) -> Option]>> { + fn extend_fixed_length_iterable<'db>( + db: &'db dyn Db, + iterable: &ast::Expr, + expression_type: &mut impl FnMut(&ast::Expr) -> Type<'db>, + element_types: &mut Vec>, + ) -> Option<()> { + let elements = match iterable { + ast::Expr::List(list) => Some(&list.elts), + ast::Expr::Tuple(tuple) => Some(&tuple.elts), + _ => None, + }; + + if let Some(elements) = elements { + for element in elements { + if let ast::Expr::Starred(starred) = element { + extend_fixed_length_iterable( + db, + starred.value.as_ref(), + expression_type, + element_types, + )?; + } else { + element_types.push(expression_type(element)); + } + } + return Some(()); + } + + let iterable_type = expression_type(iterable); + let spec = iterable_type.try_iterate(db).ok()?; + let tuple = spec.as_fixed_length()?; + element_types.extend(tuple.all_elements().iter().copied()); + Some(()) + } + + let mut element_types = Vec::new(); + extend_fixed_length_iterable(db, iterable, &mut expression_type, &mut element_types)?; + Some(element_types.into_boxed_slice()) +} + impl<'db> Type<'db> { /// Returns a tuple spec describing the elements that are produced when iterating over `self`. ///