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..ad65d605d9cc1 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/call/new_class.md @@ -0,0 +1,250 @@ +# 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] +``` + +## Assignment definition + +When assigned to a variable, the dynamic class is identified by the definition. + +```py +import types + +class Base: ... + +MyClass = types.new_class("MyClass", (Base,)) +reveal_type(MyClass) # revealed: + +instance = MyClass() +reveal_type(instance) # revealed: MyClass +``` + +## 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-tuple bases + +```py +import types + +class Base: ... + +# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `types.new_class()`: Expected `tuple[type, ...]`, 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() +``` + +### 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. + +### 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 + +`type()` doesn't support `__mro_entries__`, so `Generic[T]` and `TypedDict` fail as bases for +`type()`. `types.new_class()` handles `__mro_entries__` properly, so these are valid: + +```py +import types +from typing import Generic, TypeVar +from typing_extensions import TypedDict + +T = TypeVar("T") + +GenericClass = types.new_class("GenericClass", (Generic[T],)) +reveal_type(GenericClass) # revealed: + +TypedDictClass = types.new_class("TypedDictClass", (TypedDict,)) +reveal_type(TypedDictClass) # revealed: +``` + +### `type[X]` bases + +`type[X]` represents "some subclass of X". This is a valid base class, but ty cannot determine the +exact class, so it cannot solve the MRO. `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 — the MRO +being unknowable is inherent to `Any`/`Unknown`, not a ty limitation: + +```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 + +# 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.anything) # 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 67ed1935e06fe..ead5d17701830 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -533,9 +533,9 @@ type("Foo", (Base,), {b"attr": 1}) ## `type[...]` as base class -`type[...]` (SubclassOf) types cannot be used as base classes. When a `type[...]` is used in the -bases tuple, we emit a diagnostic and insert `Unknown` into the MRO. This gives exactly one -diagnostic about the unsupported base, rather than cascading errors: +`type[...]` (SubclassOf) types are valid class bases, but ty cannot determine the exact class, so it +cannot solve the MRO. `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 @@ -557,6 +557,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 — the MRO +being unknowable is inherent to `Any`/`Unknown`, not a ty limitation: + +```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/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a6adf4e375f45..f29566fd0baaf 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -5029,8 +5029,9 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { /// /// # 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,), {}) @@ -5039,33 +5040,33 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { /// ``` /// /// 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)] #[derive(PartialOrd, Ord)] 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 @@ -5080,13 +5081,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). @@ -5122,12 +5123,12 @@ 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. @@ -5135,7 +5136,7 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). 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, @@ -5151,8 +5152,16 @@ 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 { + // The `bases` argument is the second positional argument, or the `bases=` keyword. + let bases_arg = call_expr.arguments.args.get(1).or_else(|| { + call_expr + .arguments + .keywords + .iter() + .find(|kw| kw.arg.as_deref() == Some("bases")) + .map(|kw| &kw.value) + }); + let Some(bases_arg) = bases_arg else { return Box::default(); }; diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 308ddee6cc599..46f9719102ee1 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -1,6 +1,7 @@ use crate::types::class::CodeGeneratorKind; use crate::types::generics::{ApplySpecialization, Specialization}; use crate::types::mro::MroIterator; +use crate::types::subclass_of::SubclassOfInner; use crate::{Db, DisplaySettings}; use crate::types::tuple::TupleType; @@ -105,10 +106,15 @@ 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) => match subclass_of.subclass_of() { + // Given `type[Any]` or `type[Unknown]`, preserve the dynamic kind. + SubclassOfInner::Dynamic(dynamic) => Some(ClassBase::Dynamic(dynamic)), + // Given `type[X]` for a concrete class or TypeVar, we know it's a valid class, but + // not which one; treat it as unknown. + SubclassOfInner::Class(_) | SubclassOfInner::TypeVar(_) => { + Some(ClassBase::unknown()) + } + }, Type::Intersection(inter) => { let valid_element = inter .positive(db) diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 7362de61c6a51..d49338e0eaf42 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1602,6 +1602,8 @@ pub enum KnownFunction { RevealMro, /// `struct.unpack` Unpack, + /// `types.new_class` + NewClass, } impl KnownFunction { @@ -1686,6 +1688,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), @@ -2297,6 +2302,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 b9def0a60a1b9..3e4b8b86900d8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -160,6 +160,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> { @@ -6698,6 +6719,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(definition), namedtuple_kind, ) + } 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() @@ -7294,6 +7319,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } _ => {} } + 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); @@ -7433,24 +7465,79 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.typevar_binding_context = previous_context; // Extract and validate bases. - let Some(bases) = self.extract_explicit_bases(bases_arg, bases_type) else { + 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); + self.validate_dynamic_type_bases(bases_arg, &bases, name, DynamicClassKind::TypeCall); } - /// Iterate over all dynamic class definitions (created using `type()` calls) to check that - /// the definition will not cause an exception to be raised at runtime. This needs to be done - /// after deferred inference completes, since bases may contain forward references. + /// 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. + 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; + }; + + // Find the bases argument: second positional, or `bases=` keyword. + let bases_arg = call.arguments.args.get(1).or_else(|| { + call.arguments + .keywords + .iter() + .find(|kw| kw.arg.as_deref() == Some("bases")) + .map(|kw| &kw.value) + }); + + let Some(bases_arg) = bases_arg 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); + } + + /// Iterate over all dynamic class definitions (created using `type()` or `types.new_class()`) + /// to check that the definition will not cause an exception to be raised at runtime. This + /// needs to be done after deferred inference completes, since bases may contain forward + /// references. fn check_dynamic_class_definitions(&mut self, deferred_definitions: &[Definition<'db>]) { let db = self.db(); let module = self.module(); for definition in deferred_definitions { - // Only check assignment definitions (`type()` calls). + // Only check assignment definitions. let DefinitionKind::Assignment(assignment) = definition.kind(db) else { continue; }; @@ -7588,8 +7675,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) { let db = self.db(); - // A valid 3-argument type() call must have a `bases` argument. - let Some(bases) = call_expr.arguments.args.get(1) else { + // Find the bases argument: second positional, or `bases=` keyword. + let Some(bases) = call_expr.arguments.args.get(1).or_else(|| { + call_expr + .arguments + .keywords + .iter() + .find(|kw| kw.arg.as_deref() == Some("bases")) + .map(|kw| &kw.value) + }) else { return; }; @@ -7842,7 +7936,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // (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) + self.extract_explicit_bases(bases_arg, bases_type, DynamicClassKind::TypeCall) } else { None }; @@ -7891,8 +7985,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // 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); + let mut disjoint_bases = self.validate_dynamic_type_bases( + bases_arg, + explicit_bases, + &name, + DynamicClassKind::TypeCall, + ); // Check for MRO errors. if self.report_dynamic_mro_errors(dynamic_class, call_expr, bases_arg) { @@ -7931,6 +8029,203 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) } + /// 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. + 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(); + } + } + + // Extract name argument (first positional, or `name=` keyword). + let (name_node, name_type) = if let Some(first_arg) = args.first() { + let ty = self.infer_expression(first_arg, TypeContext::default()); + (Some(first_arg), ty) + } else { + // Infer and retrieve the `name=` keyword value. + let found = keywords + .iter() + .find(|kw| kw.arg.as_deref() == Some("name")) + .map(|kw| { + let ty = self.infer_expression(&kw.value, TypeContext::default()); + (&kw.value, ty) + }); + match found { + Some((node, ty)) => (Some(node), ty), + None => (None, Type::unknown()), + } + }; + + let name = if let Type::StringLiteral(literal) = name_type { + 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("") + }; + + // Infer remaining positional args and keywords (excluding `bases`, which may be + // deferred, and `name`, which was already inferred above when passed as a keyword). + for arg in args.iter().skip(2) { + self.infer_expression(arg, TypeContext::default()); + } + for keyword in keywords { + let is_bases = keyword.arg.as_deref() == Some("bases"); + let is_name_keyword = no_positional_args && keyword.arg.as_deref() == Some("name"); + if !is_bases && !is_name_keyword { + self.infer_expression(&keyword.value, TypeContext::default()); + } + } + + // Find the bases argument: second positional, or `bases=` keyword. + let bases_arg: Option<&ast::Expr> = args.get(1).or_else(|| { + keywords + .iter() + .find(|kw| kw.arg.as_deref() == Some("bases")) + .map(|kw| &kw.value) + }); + + // 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.infer_expression(bases_arg, TypeContext::default()); + 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, self.multi_inference_state); + 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, it can populate the namespace dynamically, so + // we mark it as dynamic. Without `exec_body`, no members can be added. + let has_exec_body = args.get(3).is_some() + || keywords + .iter() + .any(|kw| kw.arg.as_deref() == Some("exec_body")); + 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 self.report_dynamic_mro_errors(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)) + } + /// Infer a `typing.NamedTuple(typename, fields)` or `collections.namedtuple(typename, field_names)` call. /// /// This method *does not* call `infer_expression` on the object being called; @@ -8651,7 +8946,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - /// Extract explicit base types from a bases tuple type. + /// Extract base classes from the bases argument of a `type()` or `types.new_class()` call. /// /// Emits a diagnostic if `bases_type` is not a valid tuple type. /// @@ -8660,8 +8955,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, bases_node: &ast::Expr, bases_type: Type<'db>, + kind: DynamicClassKind, ) -> Option]>> { let db = self.db(); + let fn_name = kind.function_name(); // 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( @@ -8670,8 +8967,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) && 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 `{}`", bases_type.display(db) @@ -8683,11 +8981,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(Into::into) } - /// 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( @@ -8695,6 +8993,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bases_node: &ast::Expr, bases: &[Type<'db>], name: &Name, + kind: DynamicClassKind, ) -> IncompatibleBases<'db> { let db = self.db(); @@ -8703,6 +9002,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 @@ -8716,11 +9017,18 @@ 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. + // + // `type()` doesn't support `__mro_entries__`, so Generic and TypedDict bases + // are invalid. `types.new_class()` handles `__mro_entries__` properly, so + // these are allowed. + // + // 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 => { + ClassBase::Generic | ClassBase::TypedDict if kind == DynamicClassKind::TypeCall => { if let Some(builder) = self.context.report_lint(&INVALID_BASE, diagnostic_node) { let mut diagnostic = @@ -8745,16 +9053,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } + ClassBase::Generic | ClassBase::TypedDict => { + // types.new_class() handles __mro_entries__, so these are valid. + } ClassBase::Protocol => { if let Some(builder) = self .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" )); @@ -8782,34 +9096,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; } } @@ -8819,7 +9139,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } ClassBase::Dynamic(_) => { - // Dynamic bases are allowed. + // `type[X]` where X is a concrete class is a valid base, but we + // can't determine the exact class, so we emit + // `unsupported-dynamic-base`. `type[Any]`/`type[Unknown]` are fine + // as-is since the dynamic kind propagates. + if let Type::SubclassOf(s) = base + && !s.subclass_of().is_dynamic() + && let Some(builder) = self + .context + .report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_node) + { + let mut diagnostic = builder.into_diagnostic("Unsupported class base"); + diagnostic + .set_primary_message(format_args!("Has type `{}`", base.display(db))); + diagnostic.info(format_args!( + "ty cannot determine a MRO for class `{name}` due to this base" + )); + diagnostic.info("Only class objects or `Any` are supported as class bases"); + } } } } @@ -12214,6 +12551,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); diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index a54f9bd1f44e2..0983d27a97489 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -400,13 +400,6 @@ impl<'db> SubclassOfInner<'db> { } } - pub(crate) const fn into_dynamic(self) -> Option> { - match self { - Self::Class(_) | Self::TypeVar(_) => None, - Self::Dynamic(dynamic) => Some(dynamic), - } - } - pub(crate) const fn into_type_var(self) -> Option> { match self { Self::Class(_) | Self::Dynamic(_) => None,