From 8fc0c7de1c2e07ee3012361911d71f9a503edd2a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Jan 2026 12:44:38 -0500 Subject: [PATCH 01/18] [ty] Defer base inference for functional type(...) classes --- .../resources/mdtest/call/type.md | 29 +++ crates/ty_python_semantic/src/types/class.rs | 176 ++++++++++++++++-- .../src/types/infer/builder.rs | 46 ++++- crates/ty_python_semantic/src/types/mro.rs | 52 ++++-- 4 files changed, 270 insertions(+), 33 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 1f34702713be7..673d59e7fb3ae 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -793,6 +793,35 @@ in the bases tuple before it's available: X = type("X", (X,), {}) ``` +String literals in the bases tuple are not valid class bases: + +```py +# error: [invalid-base] "Invalid class base with type `Literal["X"]`" +X = type("X", ("X",), {}) +``` + +However, forward references via string annotations are supported, similar to regular class +definitions. This works with `NamedTuple` where field annotations can be forward references: + +```py +from typing import NamedTuple + +# Forward reference in NamedTuple field annotation +X = type("X", (NamedTuple("NT", [("field", "X | int")]),), {}) +reveal_type(X) # revealed: +``` + +Forward references also work when a static class inherits from a dynamic class that references it: + +```py +from typing import NamedTuple + +# Static class inheriting from dynamic class with forward ref back to static class +class Y(type("X", (NamedTuple("NT", [("field", "Y | int")]),), {})): ... + +reveal_type(Y) # revealed: +``` + ## Dynamic class names (non-literal strings) When the class name is not a string literal, we still create a class literal type but with a diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index c26e7575f8850..a769d005a4ff5 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -20,8 +20,9 @@ use crate::types::bound_super::BoundSuperError; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::context::InferContext; use crate::types::diagnostic::{ - DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_DATACLASS_OVERRIDE, INVALID_TYPE_ALIAS_TYPE, - SUPER_CALL_IN_NAMED_TUPLE_METHOD, report_conflicting_metaclass_from_bases, + DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_BASE, INVALID_DATACLASS_OVERRIDE, + INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD, + report_conflicting_metaclass_from_bases, }; use crate::types::enums::{ enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value, @@ -33,7 +34,10 @@ use crate::types::function::{ use crate::types::generics::{ GenericContext, InferableTypeVars, Specialization, walk_specialization, }; -use crate::types::infer::{infer_expression_type, infer_unpack_types, nearest_enclosing_class}; +use crate::types::infer::{ + infer_complete_scope_types, infer_expression_type, infer_unpack_types, nearest_enclosing_class, +}; +use crate::types::list_members::all_end_of_scope_members; use crate::types::member::{Member, class_member}; use crate::types::mro::{DynamicMroError, DynamicMroErrorKind}; use crate::types::relation::{ @@ -80,6 +84,22 @@ use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use ty_module_resolver::{KnownModule, file_to_module}; +fn static_class_explicit_bases_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: StaticClassLiteral<'db>, +) -> Box<[Type<'db>]> { + Box::default() +} + +fn inheritance_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: StaticClassLiteral<'db>, +) -> Option { + None +} + fn implicit_attribute_initial<'db>( _db: &'db dyn Db, id: salsa::Id, @@ -108,7 +128,7 @@ fn implicit_attribute_cycle_recover<'db>( Member { inner } } -fn try_mro_cycle_initial<'db>( +fn static_class_try_mro_cycle_initial<'db>( db: &'db dyn Db, _id: salsa::Id, self_: StaticClassLiteral<'db>, @@ -131,6 +151,46 @@ fn try_metaclass_cycle_initial<'db>( }) } +fn decorators_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: StaticClassLiteral<'db>, +) -> Box<[Type<'db>]> { + Box::default() +} + +fn fields_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: StaticClassLiteral<'db>, + _specialization: Option>, + _field_policy: CodeGeneratorKind<'db>, +) -> FxIndexMap> { + FxIndexMap::default() +} + +fn dynamic_class_explicit_bases_cycle_initial<'db>( + _db: &'db dyn Db, + _id: salsa::Id, + _self: DynamicClassLiteral<'db>, +) -> Box<[Type<'db>]> { + Box::default() +} + +#[expect(clippy::unnecessary_wraps)] +fn dynamic_class_try_mro_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + self_: DynamicClassLiteral<'db>, +) -> Result, DynamicMroError<'db>> { + // When there's a cycle, return a minimal MRO with just the class itself and object. + // This breaks the cycle and allows type checking to continue. + Ok(Mro::from([ + ClassBase::Class(ClassType::NonGeneric(self_.into())), + ClassBase::object(db), + ])) +} + /// A category of classes with code generation capabilities (with synthesized methods). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(crate) enum CodeGeneratorKind<'db> { @@ -2461,7 +2521,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// Were this not a salsa query, then the calling query /// would depend on the class's AST and rerun for every change in that file. - #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(deref), cycle_initial=static_class_explicit_bases_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(super) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { tracing::trace!( "StaticClassLiteral::explicit_bases_query: {}", @@ -2642,7 +2702,7 @@ impl<'db> StaticClassLiteral<'db> { /// attribute on a class at runtime. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order - #[salsa::tracked(returns(as_ref), cycle_initial=try_mro_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(as_ref), cycle_initial=static_class_try_mro_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(super) fn try_mro( self, db: &'db dyn Db, @@ -5036,10 +5096,6 @@ pub struct DynamicClassLiteral<'db> { #[returns(ref)] pub name: Name, - /// The base classes (from the second argument to `type()`). - #[returns(deref)] - pub bases: Box<[ClassBase<'db>]>, - /// The anchor for this dynamic class, providing stable identity. /// /// - `Definition`: The `type()` call is assigned to a variable. The definition @@ -5105,6 +5161,73 @@ impl<'db> DynamicClassLiteral<'db> { } } + /// Returns the explicit base classes of this dynamic class. + /// + /// The bases are computed lazily from the `type()` call expression. For assigned + /// `type()` calls, this uses deferred inference to handle forward references + /// (e.g., `X = type("X", (tuple["X | None"],), {})`). + /// + /// 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. + #[salsa::tracked(returns(deref), cycle_initial=dynamic_class_explicit_bases_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + let scope = self.scope(db); + let file = scope.file(db); + let module = parsed_module(db, file).load(db); + + // Get the `type()` call expression and extract the `bases` argument. + let call_expr = match self.anchor(db) { + DynamicClassAnchor::Definition(definition) => { + let value = definition + .kind(db) + .value(&module) + .expect("DynamicClassAnchor::Definition should only be used for assignments"); + value + .as_call_expr() + .expect("Definition value should be a call expression") + } + DynamicClassAnchor::ScopeOffset { offset, .. } => { + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("anchor should not be NodeIndex::NONE"); + let absolute_index = NodeIndex::from(anchor_u32 + offset); + module + .get_by_index(absolute_index) + .try_into() + .expect("scope offset should point to ExprCall") + } + }; + + // The `bases` argument is the second positional argument. + let Some(bases_arg) = call_expr.arguments.args.get(1) else { + return Box::default(); + }; + + // Infer the `bases` type. + let bases_type = match self.anchor(db) { + DynamicClassAnchor::Definition(definition) => { + // Use `definition_expression_type` for deferred inference support. + definition_expression_type(db, definition, bases_arg) + } + DynamicClassAnchor::ScopeOffset { .. } => { + // For dangling calls, the bases were already inferred as part of the scope. + infer_complete_scope_types(db, scope).expression_type(bases_arg) + } + }; + + // For variable-length tuples (like `tuple[type, ...]`), we can't statically + // determine the bases, so return a single Unknown base. + let Some(tuple_spec) = bases_type.tuple_instance_spec(db) else { + return Box::from([Type::unknown()]); + }; + let Some(elements) = tuple_spec.as_fixed_length() else { + return Box::from([Type::unknown()]); + }; + + elements.elements_slice().iter().copied().collect() + } + /// Returns a [`Span`] with the range of the `type()` call expression. /// /// See [`Self::header_range`] for more details. @@ -5167,12 +5290,12 @@ impl<'db> DynamicClassLiteral<'db> { self, db: &'db dyn Db, ) -> Result, DynamicMetaclassConflict<'db>> { - let bases = self.bases(db); + let base_types = self.explicit_bases(db); // If no bases, metaclass is `type`. // To dynamically create a class with no bases that has a custom metaclass, // you have to invoke that metaclass rather than `type()`. - if bases.is_empty() { + if base_types.is_empty() { return Ok(KnownClass::Type.to_class_literal(db)); } @@ -5181,6 +5304,17 @@ impl<'db> DynamicClassLiteral<'db> { return Ok(SubclassOfType::subclass_of_unknown()); } + // Convert types to ClassBases. Use a placeholder class for conversion. + let placeholder_class: ClassLiteral<'db> = + KnownClass::Object.try_to_class_literal(db).unwrap().into(); + let bases: Vec> = base_types + .iter() + .map(|ty| { + ClassBase::try_from_type(db, *ty, placeholder_class) + .unwrap_or_else(ClassBase::unknown) + }) + .collect(); + // Start with the first base's metaclass as the candidate. let mut candidate = bases[0].metaclass(db); @@ -5325,7 +5459,7 @@ impl<'db> DynamicClassLiteral<'db> { /// /// Returns `Ok(Mro)` if successful, or `Err(DynamicMroError)` if there's /// an error (duplicate bases or C3 linearization failure). - #[salsa::tracked(returns(ref), heap_size = ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(ref), cycle_initial=dynamic_class_try_mro_cycle_initial, heap_size = ruff_memory_usage::heap_size)] pub(crate) fn try_mro(self, db: &'db dyn Db) -> Result, DynamicMroError<'db>> { Mro::of_dynamic_class(db, self) } @@ -5383,7 +5517,6 @@ impl<'db> DynamicClassLiteral<'db> { Self::new( db, self.name(db).clone(), - self.bases(db), self.anchor(db), self.members(db), self.has_dynamic_namespace(db), @@ -8114,6 +8247,19 @@ impl KnownClass { // Check for MRO errors if let Err(error) = dynamic_class.try_mro(db) { match error.reason() { + DynamicMroErrorKind::InvalidBases(invalid_bases) => { + for (_, invalid_type) in invalid_bases { + if let Some(builder) = + context.report_lint(&INVALID_BASE, call_expression) + { + builder.into_diagnostic(format_args!( + "Invalid class base with type `{}` (all bases must be a class, \ + `Any`, `Unknown` or `Todo`)", + invalid_type.display(db), + )); + } + } + } DynamicMroErrorKind::DuplicateBases(duplicates) => { if let Some(builder) = context.report_lint(&DUPLICATE_BASE, call_expression) @@ -8138,7 +8284,7 @@ impl KnownClass { for class `{}` with bases `[{}]`", dynamic_class.name(db), dynamic_class - .bases(db) + .explicit_bases(db) .iter() .map(|base| base.display(db)) .join(", ") diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3925f12845d04..77cdf0066c585 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7283,6 +7283,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_newtype_assignment_deferred(arguments); return; } + if known_class == Some(KnownClass::Type) { + // Infer the `bases` argument for three-argument `type()` calls. + // This is deferred to break cycles for self-referential definitions + // like `X = type("X", (tuple["X | None"],), {})`. + if arguments.args.len() >= 2 { + self.infer_expression(&arguments.args[1], TypeContext::default()); + } + return; + } let mut constraint_tys = Vec::new(); for arg in arguments.args.iter().skip(1) { let constraint = self.infer_type_expression(arg); @@ -7485,7 +7494,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let name_type = self.infer_expression(name_arg, TypeContext::default()); - let bases_type = self.infer_expression(bases_arg, TypeContext::default()); + // For assigned `type()` calls, defer bases inference to break cycles for self-referential + // definitions like `X = type("X", (tuple["X | None"],), {})`. The bases will be inferred + // later via `DynamicClassLiteral::explicit_bases()`. + let bases_type = if definition.is_some() { + None + } else { + Some(self.infer_expression(bases_arg, TypeContext::default())) + }; let namespace_type = self.infer_expression(namespace_arg, TypeContext::default()); // TODO: validate other keywords against `__init_subclass__` methods of superclasses @@ -7584,9 +7600,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::name::Name::new_static("") }; - let (bases, mut disjoint_bases) = - self.extract_dynamic_type_bases(bases_arg, bases_type, &name); - let scope = self.scope(); // Create the anchor for identifying this dynamic class. @@ -7611,17 +7624,36 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let dynamic_class = DynamicClassLiteral::new( db, - name, - bases, + name.clone(), anchor, members, has_dynamic_namespace, None, ); + // For assigned `type()` calls, the bases are inferred via deferred inference after the + // class is created. For dangling calls, `bases_type` is already available from immediate + // inference. + let bases_type = + bases_type.unwrap_or_else(|| self.infer_expression(bases_arg, TypeContext::default())); + + // Extract and validate bases for diagnostics. + let (_, mut disjoint_bases) = self.extract_dynamic_type_bases(bases_arg, bases_type, &name); + // Check for MRO errors. match dynamic_class.try_mro(db) { Err(error) => match error.reason() { + DynamicMroErrorKind::InvalidBases(invalid_bases) => { + for (_, invalid_type) in invalid_bases { + if let Some(builder) = self.context.report_lint(&INVALID_BASE, call_expr) { + builder.into_diagnostic(format_args!( + "Invalid class base with type `{}` (all bases must be a class, \ + `Any`, `Unknown` or `Todo`)", + invalid_type.display(db), + )); + } + } + } DynamicMroErrorKind::DuplicateBases(duplicates) => { if let Some(builder) = self.context.report_lint(&DUPLICATE_BASE, call_expr) { builder.into_diagnostic(format_args!( @@ -7642,7 +7674,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for class `{}` with bases `[{}]`", dynamic_class.name(db), dynamic_class - .bases(db) + .explicit_bases(db) .iter() .map(|base| base.display(db)) .join(", ") diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 41085fdaa4c0b..4f603f528fca4 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -8,7 +8,7 @@ use crate::Db; use crate::types::class_base::ClassBase; use crate::types::generics::Specialization; use crate::types::{ - ClassLiteral, ClassType, DynamicClassLiteral, KnownInstanceType, SpecialFormType, + ClassLiteral, ClassType, DynamicClassLiteral, KnownClass, KnownInstanceType, SpecialFormType, StaticClassLiteral, Type, }; @@ -334,17 +334,40 @@ impl<'db> Mro<'db> { db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>, ) -> Result> { - let bases = dynamic.bases(db); + let base_types = dynamic.explicit_bases(db); - // Check for duplicate bases first, but skip dynamic bases like `Unknown` or `Any`. + // Convert types to ClassBases and track invalid ones. + // Use a placeholder class for conversion (the actual class doesn't matter for validity). + let placeholder_class: ClassLiteral<'db> = + KnownClass::Object.try_to_class_literal(db).unwrap().into(); + + let mut bases = Vec::with_capacity(base_types.len()); + let mut invalid_bases = Vec::new(); + + for (index, &base_type) in base_types.iter().enumerate() { + match ClassBase::try_from_type(db, base_type, placeholder_class) { + Some(base) => bases.push(base), + None => invalid_bases.push((index, base_type)), + } + } + + // Report invalid bases as an error. + if !invalid_bases.is_empty() { + return Err( + DynamicMroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()) + .into_error(db, dynamic), + ); + } + + // Check for duplicate bases, but skip dynamic bases like `Unknown` or `Any`. let mut seen = FxHashSet::default(); let mut duplicates = Vec::new(); - for base in bases { + for &base in &bases { if matches!(base, ClassBase::Dynamic(_)) { continue; } - if !seen.insert(*base) { - duplicates.push(*base); + if !seen.insert(base) { + duplicates.push(base); } } if !duplicates.is_empty() { @@ -355,9 +378,7 @@ impl<'db> Mro<'db> { } // Check if any bases are dynamic, like `Unknown` or `Any`. - let has_dynamic_bases = bases - .iter() - .any(|base| matches!(base, ClassBase::Dynamic(_))); + let has_dynamic_bases = bases.iter().any(|base| matches!(base, ClassBase::Dynamic(_))); // Compute MRO using C3 linearization. let mro_bases = if bases.is_empty() { @@ -371,7 +392,7 @@ impl<'db> Mro<'db> { let mut seqs: Vec>> = Vec::with_capacity(bases.len() + 1); // Add each base's MRO. - for base in bases { + for base in &bases { seqs.push(base.mro(db, None).collect()); } @@ -408,7 +429,13 @@ impl<'db> Mro<'db> { let mut seen = FxHashSet::default(); seen.insert(self_base); - for base in dynamic.bases(db) { + // Convert types to ClassBases for fallback MRO computation. + let placeholder_class: ClassLiteral<'db> = + KnownClass::Object.try_to_class_literal(db).unwrap().into(); + + for &base_type in &*dynamic.explicit_bases(db) { + let base = ClassBase::try_from_type(db, base_type, placeholder_class) + .unwrap_or_else(ClassBase::unknown); for item in base.mro(db, None) { if seen.insert(item) { result.push(item); @@ -777,6 +804,9 @@ impl<'db> DynamicMroError<'db> { /// These mirror the relevant variants from `MroErrorKind` for static classes. #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)] pub(crate) enum DynamicMroErrorKind<'db> { + /// One or more base types are not valid bases for a class. + InvalidBases(Box<[(usize, Type<'db>)]>), + /// The class has duplicate bases in its bases tuple. DuplicateBases(Box<[ClassBase<'db>]>), From 265c37f5c081edcdea2c66248e6e41fad5f1576e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 23 Jan 2026 09:58:52 -0500 Subject: [PATCH 02/18] Make it more similar --- crates/ty_python_semantic/src/types/class.rs | 71 ++-- .../src/types/infer/builder.rs | 376 +++++++++--------- crates/ty_python_semantic/src/types/mro.rs | 120 +++--- 3 files changed, 277 insertions(+), 290 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a769d005a4ff5..1c577d90cc1b7 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -20,8 +20,8 @@ use crate::types::bound_super::BoundSuperError; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::context::InferContext; use crate::types::diagnostic::{ - DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_BASE, INVALID_DATACLASS_OVERRIDE, - INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD, + CYCLIC_CLASS_DEFINITION, DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_BASE, + INVALID_DATACLASS_OVERRIDE, INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD, report_conflicting_metaclass_from_bases, }; use crate::types::enums::{ @@ -37,7 +37,6 @@ use crate::types::generics::{ use crate::types::infer::{ infer_complete_scope_types, infer_expression_type, infer_unpack_types, nearest_enclosing_class, }; -use crate::types::list_members::all_end_of_scope_members; use crate::types::member::{Member, class_member}; use crate::types::mro::{DynamicMroError, DynamicMroErrorKind}; use crate::types::relation::{ @@ -92,14 +91,6 @@ fn static_class_explicit_bases_cycle_initial<'db>( Box::default() } -fn inheritance_cycle_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _self: StaticClassLiteral<'db>, -) -> Option { - None -} - fn implicit_attribute_initial<'db>( _db: &'db dyn Db, id: salsa::Id, @@ -151,24 +142,6 @@ fn try_metaclass_cycle_initial<'db>( }) } -fn decorators_cycle_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _self: StaticClassLiteral<'db>, -) -> Box<[Type<'db>]> { - Box::default() -} - -fn fields_cycle_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _self: StaticClassLiteral<'db>, - _specialization: Option>, - _field_policy: CodeGeneratorKind<'db>, -) -> FxIndexMap> { - FxIndexMap::default() -} - fn dynamic_class_explicit_bases_cycle_initial<'db>( _db: &'db dyn Db, _id: salsa::Id, @@ -5169,6 +5142,8 @@ impl<'db> DynamicClassLiteral<'db> { /// /// 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. + /// + /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). #[salsa::tracked(returns(deref), cycle_initial=dynamic_class_explicit_bases_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { let scope = self.scope(db); @@ -5217,7 +5192,7 @@ impl<'db> DynamicClassLiteral<'db> { }; // For variable-length tuples (like `tuple[type, ...]`), we can't statically - // determine the bases, so return a single Unknown base. + // determine the bases, so return Unknown. let Some(tuple_spec) = bases_type.tuple_instance_spec(db) else { return Box::from([Type::unknown()]); }; @@ -5290,12 +5265,12 @@ impl<'db> DynamicClassLiteral<'db> { self, db: &'db dyn Db, ) -> Result, DynamicMetaclassConflict<'db>> { - let base_types = self.explicit_bases(db); + let original_bases = self.explicit_bases(db); // If no bases, metaclass is `type`. // To dynamically create a class with no bases that has a custom metaclass, // you have to invoke that metaclass rather than `type()`. - if base_types.is_empty() { + if original_bases.is_empty() { return Ok(KnownClass::Type.to_class_literal(db)); } @@ -5304,17 +5279,20 @@ impl<'db> DynamicClassLiteral<'db> { return Ok(SubclassOfType::subclass_of_unknown()); } - // Convert types to ClassBases. Use a placeholder class for conversion. + // Convert Types to ClassBases for metaclass computation. let placeholder_class: ClassLiteral<'db> = KnownClass::Object.try_to_class_literal(db).unwrap().into(); - let bases: Vec> = base_types + + let bases: Vec> = original_bases .iter() - .map(|ty| { - ClassBase::try_from_type(db, *ty, placeholder_class) - .unwrap_or_else(ClassBase::unknown) - }) + .filter_map(|base_type| ClassBase::try_from_type(db, *base_type, placeholder_class)) .collect(); + // If all bases failed to convert, return type as the metaclass. + if bases.is_empty() { + return Ok(KnownClass::Type.to_class_literal(db)); + } + // Start with the first base's metaclass as the candidate. let mut candidate = bases[0].metaclass(db); @@ -8248,18 +8226,27 @@ impl KnownClass { if let Err(error) = dynamic_class.try_mro(db) { match error.reason() { DynamicMroErrorKind::InvalidBases(invalid_bases) => { - for (_, invalid_type) in invalid_bases { + for (_, base_type) in invalid_bases { if let Some(builder) = context.report_lint(&INVALID_BASE, call_expression) { builder.into_diagnostic(format_args!( - "Invalid class base with type `{}` (all bases must be a class, \ - `Any`, `Unknown` or `Todo`)", - invalid_type.display(db), + "Invalid class base with type `{}`", + base_type.display(db) )); } } } + DynamicMroErrorKind::InheritanceCycle => { + if let Some(builder) = + context.report_lint(&CYCLIC_CLASS_DEFINITION, call_expression) + { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}`", + dynamic_class.name(db) + )); + } + } DynamicMroErrorKind::DuplicateBases(duplicates) => { if let Some(builder) = context.report_lint(&DUPLICATE_BASE, call_expression) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 77cdf0066c585..f833eaf9193d3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7637,23 +7637,68 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bases_type = bases_type.unwrap_or_else(|| self.infer_expression(bases_arg, TypeContext::default())); - // Extract and validate bases for diagnostics. - let (_, mut disjoint_bases) = self.extract_dynamic_type_bases(bases_arg, bases_type, &name); + // Validate bases and collect disjoint bases for diagnostics. + let mut disjoint_bases = self.validate_dynamic_type_bases(bases_arg, bases_type, &name); // Check for MRO errors. match dynamic_class.try_mro(db) { Err(error) => match error.reason() { DynamicMroErrorKind::InvalidBases(invalid_bases) => { - for (_, invalid_type) in invalid_bases { - if let Some(builder) = self.context.report_lint(&INVALID_BASE, call_expr) { - builder.into_diagnostic(format_args!( - "Invalid class base with type `{}` (all bases must be a class, \ - `Any`, `Unknown` or `Todo`)", - invalid_type.display(db), + // Get the AST nodes for base expressions (for diagnostics). + let bases_tuple_elts = bases_arg.as_tuple_expr().map(|t| t.elts.as_slice()); + + for (idx, base_type) in invalid_bases { + let diagnostic_node = bases_tuple_elts + .and_then(|elts| elts.get(*idx)) + .unwrap_or(bases_arg); + + // Check if the type is "type-like" (e.g., `type[Base]`). + let instance_of_type = KnownClass::Type.to_instance(db); + if base_type.is_assignable_to(db, instance_of_type) { + if let Some(builder) = self + .context + .report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_node) + { + let mut diagnostic = + builder.into_diagnostic("Unsupported class base"); + diagnostic.set_primary_message(format_args!( + "Has type `{}`", + base_type.display(db) + )); + diagnostic.info(format_args!( + "ty cannot determine a MRO for class `{name}` due to this base" + )); + diagnostic.info( + "Only class objects or `Any` are supported as class bases", + ); + } + } else if let Some(builder) = + self.context.report_lint(&INVALID_BASE, diagnostic_node) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid class base with type `{}`", + base_type.display(db) )); + if bases_tuple_elts.is_none() { + diagnostic.info(format_args!( + "Element {} of the tuple is invalid", + idx + 1 + )); + } } } } + DynamicMroErrorKind::InheritanceCycle => { + if let Some(builder) = self + .context + .report_lint(&CYCLIC_CLASS_DEFINITION, call_expr) + { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}`", + dynamic_class.name(db) + )); + } + } DynamicMroErrorKind::DuplicateBases(duplicates) => { if let Some(builder) = self.context.report_lint(&DUPLICATE_BASE, call_expr) { builder.into_diagnostic(format_args!( @@ -8438,17 +8483,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - /// Extract base classes from the second argument of a `type()` call. + /// Validate base classes from the second argument of a `type()` call. /// - /// Returns the extracted bases and any disjoint bases found (for instance-layout-conflict - /// checking). If any bases were invalid, diagnostics are emitted and the dynamic class is - /// inferred as inheriting from `Unknown`. - fn extract_dynamic_type_bases( + /// 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`. + /// + /// Returns disjoint bases found (for instance-layout-conflict checking). + fn validate_dynamic_type_bases( &mut self, bases_node: &ast::Expr, bases_type: Type<'db>, name: &ast::name::Name, - ) -> (Box<[ClassBase<'db>]>, IncompatibleBases<'db>) { + ) -> IncompatibleBases<'db> { let db = self.db(); // Get AST nodes for base expressions (for diagnostics). @@ -8461,203 +8508,148 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut disjoint_bases = IncompatibleBases::default(); - let bases = bases_type - .tuple_instance_spec(db) - .as_deref() - .and_then(|spec| spec.as_fixed_length()) - .map(|tuple| { - // Fixed-length tuple: extract each base class - tuple - .elements_slice() - .iter() - .enumerate() - .map(|(idx, base)| { - let diagnostic_node = bases_tuple_elts - .and_then(|elts| elts.get(idx)) - .unwrap_or(bases_node); + let Some(tuple_spec) = bases_type.tuple_instance_spec(db) else { + // The `bases` argument is not a tuple. Emit a diagnostic. + if !bases_type.is_assignable_to( + db, + Type::homogeneous_tuple(db, KnownClass::Type.to_instance(db)), + ) { + if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, bases_node) + { + let mut diagnostic = builder + .into_diagnostic("Invalid argument to parameter 2 (`bases`) of `type()`"); + diagnostic.set_primary_message(format_args!( + "Expected `tuple[type, ...]`, found `{}`", + bases_type.display(db) + )); + } + } + return disjoint_bases; + }; + let Some(tuple) = tuple_spec.as_fixed_length() else { + return disjoint_bases; + }; - // First try the standard conversion. - if let Some(class_base) = - ClassBase::try_from_type(db, *base, placeholder_class) - { - // Check for special bases that are not allowed for dynamic classes. - // Dynamic classes can't be generic, protocols, TypedDicts, or enums. - 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()`", - ); - 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!( - "Consider using `class {name}(Generic[...]): ...` instead" - )); - } - ClassBase::TypedDict => { - diagnostic.info( - "Classes created via `type()` cannot be TypedDicts", - ); - diagnostic.info(format_args!( - "Consider using `TypedDict(\"{name}\", {{}})` instead" - )); - } - _ => unreachable!(), - } - } - return ClassBase::unknown(); - } - 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()`", - ); - diagnostic.set_primary_message(format_args!( - "Has type `{}`", - base.display(db) - )); - diagnostic.info( - "Classes created via `type()` cannot be protocols", - ); - diagnostic.info(format_args!( - "Consider using `class {name}(Protocol): ...` instead" - )); - } - return ClassBase::unknown(); - } - ClassBase::Class(class_type) => { - // Check if base is @final (includes enums with members). - if class_type.is_final(db) { - if let Some(builder) = self - .context - .report_lint(&SUBCLASS_OF_FINAL_CLASS, diagnostic_node) - { - builder.into_diagnostic(format_args!( - "Class `{name}` cannot inherit from final class `{}`", - class_type.name(db) - )); - } - return ClassBase::unknown(); - } + // Check each base for special cases that are not allowed for dynamic classes. + for (idx, base) in tuple.elements_slice().iter().enumerate() { + let diagnostic_node = bases_tuple_elts + .and_then(|elts| elts.get(idx)) + .unwrap_or(bases_node); - // 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" - )); - } - return ClassBase::unknown(); - } - } - - // Collect disjoint bases for instance-layout-conflict checking. - if let Some(disjoint_base) = class_type.nearest_disjoint_base(db) - { - disjoint_bases.insert( - disjoint_base, - idx, - class_type.class_literal(db), - ); - } + // Try to convert to ClassBase to check for special cases. + let Some(class_base) = ClassBase::try_from_type(db, *base, placeholder_class) else { + // Can't convert - will be handled by InvalidBases error from try_mro. + continue; + }; - return class_base; - } - ClassBase::Dynamic(_) => return class_base, + // Check for special bases that are not allowed for dynamic classes. + // Dynamic classes can't be generic, protocols, TypedDicts, or enums. + 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()`"); + 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!( + "Consider using `class {name}(Generic[...]): ...` instead" + )); + } + ClassBase::TypedDict => { + diagnostic + .info("Classes created via `type()` cannot be TypedDicts"); + diagnostic.info(format_args!( + "Consider using `TypedDict(\"{name}\", {{}})` instead" + )); } + _ => unreachable!(), } + } + } + 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()`"); + diagnostic + .set_primary_message(format_args!("Has type `{}`", base.display(db))); + diagnostic.info("Classes created via `type()` cannot be protocols"); + diagnostic.info(format_args!( + "Consider using `class {name}(Protocol): ...` instead" + )); + } + } + ClassBase::Class(class_type) => { + // Check if base is @final (includes enums with members). + // If it's @final, we emit a diagnostic and skip other checks + // to avoid duplicate errors (e.g., enums with members are both + // @final and would trigger the enum-specific diagnostic). + if class_type.is_final(db) { + if let Some(builder) = self + .context + .report_lint(&SUBCLASS_OF_FINAL_CLASS, diagnostic_node) + { + builder.into_diagnostic(format_args!( + "Class `{name}` cannot inherit from final class `{}`", + class_type.name(db) + )); + } + // 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; + } - // If that fails, check if the type is "type-like" (e.g., `type[Base]`). - // For type-like bases we emit `unsupported-dynamic-base` and use - // `Unknown` to avoid cascading errors. For non-type-like bases (like - // integers), we return `None` to fall through to regular call binding - // which will emit `invalid-argument-type`. - let instance_of_type = KnownClass::Type.to_instance(db); - - if base.is_assignable_to(db, instance_of_type) { - if let Some(builder) = self - .context - .report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_node) + // 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("Unsupported class base"); + 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!( - "ty cannot determine a MRO for class `{name}` due to this base" + "Consider using `Enum(\"{name}\", [])` instead" )); - diagnostic.info( - "Only class objects or `Any` are supported as class bases", - ); } - } else if let Some(builder) = - self.context.report_lint(&INVALID_BASE, diagnostic_node) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid class base with type `{}`", - base.display(db) - )); - if bases_tuple_elts.is_none() { - diagnostic.info(format_args!( - "Element {} of the tuple is invalid", - idx + 1 - )); + // 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; } + } - ClassBase::unknown() - }) - .collect() - }) - .unwrap_or_else(|| { - if !bases_type.is_assignable_to( - db, - Type::homogeneous_tuple(db, KnownClass::Type.to_instance(db)), - ) && let Some(builder) = - self.context.report_lint(&INVALID_ARGUMENT_TYPE, bases_node) - { - let mut diagnostic = builder - .into_diagnostic("Invalid argument to parameter 2 (`bases`) of `type()`"); - diagnostic.set_primary_message(format_args!( - "Expected `tuple[type, ...]`, found `{}`", - bases_type.display(db) - )); + // Collect disjoint bases for instance-layout-conflict checking. + if let Some(disjoint_base) = class_type.nearest_disjoint_base(db) { + disjoint_bases.insert(disjoint_base, idx, class_type.class_literal(db)); + } } - Box::from([ClassBase::unknown()]) - }); + ClassBase::Dynamic(_) => { + // Dynamic bases are allowed. + } + } + } - (bases, disjoint_bases) + disjoint_bases } fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) { diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 4f603f528fca4..2a740bef887b7 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -334,24 +334,25 @@ impl<'db> Mro<'db> { db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>, ) -> Result> { - let base_types = dynamic.explicit_bases(db); + let original_bases = dynamic.explicit_bases(db); - // Convert types to ClassBases and track invalid ones. - // Use a placeholder class for conversion (the actual class doesn't matter for validity). + // Use a placeholder class literal for try_from_type (the subclass parameter is only + // used for Protocol/TypedDict detection which doesn't apply here). let placeholder_class: ClassLiteral<'db> = KnownClass::Object.try_to_class_literal(db).unwrap().into(); - let mut bases = Vec::with_capacity(base_types.len()); + // Convert Types to ClassBases, tracking any that fail conversion. + let mut resolved_bases = Vec::with_capacity(original_bases.len()); let mut invalid_bases = Vec::new(); - for (index, &base_type) in base_types.iter().enumerate() { - match ClassBase::try_from_type(db, base_type, placeholder_class) { - Some(base) => bases.push(base), - None => invalid_bases.push((index, base_type)), + for (i, base_type) in original_bases.iter().enumerate() { + match ClassBase::try_from_type(db, *base_type, placeholder_class) { + Some(class_base) => resolved_bases.push(class_base), + None => invalid_bases.push((i, *base_type)), } } - // Report invalid bases as an error. + // If there are any invalid bases, return an error. if !invalid_bases.is_empty() { return Err( DynamicMroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()) @@ -359,17 +360,51 @@ impl<'db> Mro<'db> { ); } - // Check for duplicate bases, but skip dynamic bases like `Unknown` or `Any`. + // Check if any bases are dynamic, like `Unknown` or `Any`. + let has_dynamic_bases = resolved_bases + .iter() + .any(|base| matches!(base, ClassBase::Dynamic(_))); + + let self_base = ClassBase::Class(ClassType::NonGeneric(dynamic.into())); + + // Handle empty bases case: MRO is just [self, object]. + if resolved_bases.is_empty() { + return Ok(Self::from([self_base, ClassBase::object(db)])); + } + + // Build MRO sequences and check for inheritance cycles. + let mut seqs = vec![VecDeque::from([self_base])]; + for base in &resolved_bases { + if base.has_cyclic_mro(db) { + return Err(DynamicMroErrorKind::InheritanceCycle.into_error(db, dynamic)); + } + seqs.push(base.mro(db, None).collect()); + } + seqs.push(resolved_bases.iter().copied().collect()); + + // Try C3 merge. + if let Some(mro) = c3_merge(seqs) { + return Ok(mro); + } + + // C3 merge failed. Figure out why and report the most specific error. + + // Check for duplicate bases (skip dynamic bases like `Unknown` or `Any`). let mut seen = FxHashSet::default(); let mut duplicates = Vec::new(); - for &base in &bases { + let mut has_duplicate_dynamic_bases = false; + for base in &resolved_bases { if matches!(base, ClassBase::Dynamic(_)) { + if !seen.insert(*base) { + has_duplicate_dynamic_bases = true; + } continue; } - if !seen.insert(base) { - duplicates.push(base); + if !seen.insert(*base) { + duplicates.push(*base); } } + if !duplicates.is_empty() { return Err( DynamicMroErrorKind::DuplicateBases(duplicates.into_boxed_slice()) @@ -377,46 +412,11 @@ impl<'db> Mro<'db> { ); } - // Check if any bases are dynamic, like `Unknown` or `Any`. - let has_dynamic_bases = bases.iter().any(|base| matches!(base, ClassBase::Dynamic(_))); - - // Compute MRO using C3 linearization. - let mro_bases = if bases.is_empty() { - // Empty bases: MRO is just `object`. - Some(vec![ClassBase::object(db)]) - } else if bases.len() == 1 { - // Single base: MRO is just that base's MRO. - Some(bases[0].mro(db, None).collect()) + // No duplicate concrete bases. If there are dynamic bases, use fallback MRO. + if has_dynamic_bases || has_duplicate_dynamic_bases { + Ok(Self::dynamic_fallback(db, dynamic)) } else { - // Multiple bases: use C3 merge algorithm. - let mut seqs: Vec>> = Vec::with_capacity(bases.len() + 1); - - // Add each base's MRO. - for base in &bases { - seqs.push(base.mro(db, None).collect()); - } - - // Add the list of bases in order. - seqs.push(bases.iter().copied().collect()); - - c3_merge(seqs).map(|mro| mro.iter().copied().collect()) - }; - - match mro_bases { - Some(mro) => { - let mut result = vec![ClassBase::Class(ClassType::NonGeneric(dynamic.into()))]; - result.extend(mro); - Ok(Self::from(result)) - } - None => { - // C3 merge failed. If there are dynamic bases, use the fallback MRO. - // Otherwise, report an error. - if has_dynamic_bases { - Ok(Self::dynamic_fallback(db, dynamic)) - } else { - Err(DynamicMroErrorKind::UnresolvableMro.into_error(db, dynamic)) - } - } + Err(DynamicMroErrorKind::UnresolvableMro.into_error(db, dynamic)) } } @@ -429,13 +429,15 @@ impl<'db> Mro<'db> { let mut seen = FxHashSet::default(); seen.insert(self_base); - // Convert types to ClassBases for fallback MRO computation. + // Use a placeholder class literal for try_from_type. let placeholder_class: ClassLiteral<'db> = KnownClass::Object.try_to_class_literal(db).unwrap().into(); - for &base_type in &*dynamic.explicit_bases(db) { - let base = ClassBase::try_from_type(db, base_type, placeholder_class) + for base_type in dynamic.explicit_bases(db) { + // Convert Type to ClassBase, falling back to Unknown if conversion fails. + let base = ClassBase::try_from_type(db, *base_type, placeholder_class) .unwrap_or_else(ClassBase::unknown); + for item in base.mro(db, None) { if seen.insert(item) { result.push(item); @@ -804,9 +806,15 @@ impl<'db> DynamicMroError<'db> { /// These mirror the relevant variants from `MroErrorKind` for static classes. #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)] pub(crate) enum DynamicMroErrorKind<'db> { - /// One or more base types are not valid bases for a class. + /// The class inherits from one or more invalid bases. + /// + /// Similar to `StaticMroErrorKind::InvalidBases`, this records the indices + /// and types of bases that could not be converted to valid class bases. InvalidBases(Box<[(usize, Type<'db>)]>), + /// A cycle was encountered resolving the class' bases. + InheritanceCycle, + /// The class has duplicate bases in its bases tuple. DuplicateBases(Box<[ClassBase<'db>]>), From 3df4a85160d0fd097918eccf338422f169f5a943 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Jan 2026 12:11:37 -0500 Subject: [PATCH 03/18] Add recursive test --- crates/ty_python_semantic/resources/mdtest/call/type.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 673d59e7fb3ae..40b50e8602baf 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -822,6 +822,15 @@ class Y(type("X", (NamedTuple("NT", [("field", "Y | int")]),), {})): ... reveal_type(Y) # revealed: ``` +Forward references via subscript annotations on generic bases are supported: + +```py +# Forward reference to X via subscript annotation in tuple base +# (This fails at runtime, but we should handle it without panicking) +X = type("X", (tuple["X | None"],), {}) +reveal_type(X) # revealed: +``` + ## Dynamic class names (non-literal strings) When the class name is not a string literal, we still create a class literal type but with a From bbc864b35926e826ccda62ebe91a4bc454942884 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Jan 2026 12:13:12 -0500 Subject: [PATCH 04/18] Inline cycle --- crates/ty_python_semantic/src/types/class.rs | 20 ++------------------ crates/ty_python_semantic/src/types/mro.rs | 2 +- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 1c577d90cc1b7..eed6cf1a68f2b 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -83,14 +83,6 @@ use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use ty_module_resolver::{KnownModule, file_to_module}; -fn static_class_explicit_bases_cycle_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _self: StaticClassLiteral<'db>, -) -> Box<[Type<'db>]> { - Box::default() -} - fn implicit_attribute_initial<'db>( _db: &'db dyn Db, id: salsa::Id, @@ -142,14 +134,6 @@ fn try_metaclass_cycle_initial<'db>( }) } -fn dynamic_class_explicit_bases_cycle_initial<'db>( - _db: &'db dyn Db, - _id: salsa::Id, - _self: DynamicClassLiteral<'db>, -) -> Box<[Type<'db>]> { - Box::default() -} - #[expect(clippy::unnecessary_wraps)] fn dynamic_class_try_mro_cycle_initial<'db>( db: &'db dyn Db, @@ -2494,7 +2478,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// Were this not a salsa query, then the calling query /// would depend on the class's AST and rerun for every change in that file. - #[salsa::tracked(returns(deref), cycle_initial=static_class_explicit_bases_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] pub(super) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { tracing::trace!( "StaticClassLiteral::explicit_bases_query: {}", @@ -5144,7 +5128,7 @@ impl<'db> DynamicClassLiteral<'db> { /// or if the bases argument is not a tuple. /// /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). - #[salsa::tracked(returns(deref), cycle_initial=dynamic_class_explicit_bases_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { let scope = self.scope(db); let file = scope.file(db); diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 2a740bef887b7..dde2969a02107 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -337,7 +337,7 @@ impl<'db> Mro<'db> { let original_bases = dynamic.explicit_bases(db); // Use a placeholder class literal for try_from_type (the subclass parameter is only - // used for Protocol/TypedDict detection which doesn't apply here). + // used for NamedTuple subclasses, which doesn't apply here). let placeholder_class: ClassLiteral<'db> = KnownClass::Object.try_to_class_literal(db).unwrap().into(); From 0c64f5150e57e8b3ecbb5562ac81720a2b2cae34 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Jan 2026 12:18:43 -0500 Subject: [PATCH 05/18] store bases --- crates/ty_python_semantic/src/types/class.rs | 77 +++++++++---------- .../src/types/infer/builder.rs | 34 ++++++-- 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index eed6cf1a68f2b..02f507e79335c 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -35,7 +35,7 @@ use crate::types::generics::{ GenericContext, InferableTypeVars, Specialization, walk_specialization, }; use crate::types::infer::{ - infer_complete_scope_types, infer_expression_type, infer_unpack_types, nearest_enclosing_class, + infer_expression_type, infer_unpack_types, nearest_enclosing_class, }; use crate::types::member::{Member, class_member}; use crate::types::mro::{DynamicMroError, DynamicMroErrorKind}; @@ -5081,9 +5081,7 @@ pub struct DynamicClassLiteral<'db> { /// This enum provides stable identity for `DynamicClassLiteral`: /// - For assigned calls, the `Definition` uniquely identifies the class. /// - For dangling calls, a relative offset provides stable identity. -#[derive( - Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, salsa::Update, get_size2::GetSize, -)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub enum DynamicClassAnchor<'db> { /// The `type()` call is assigned to a variable. /// @@ -5095,7 +5093,14 @@ pub enum DynamicClassAnchor<'db> { /// /// 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). - ScopeOffset { scope: ScopeId<'db>, offset: u32 }, + /// + /// The `explicit_bases` are computed eagerly at creation time since dangling + /// calls cannot recursively reference the class being defined. + ScopeOffset { + scope: ScopeId<'db>, + offset: u32, + explicit_bases: Box<[Type<'db>]>, + }, } impl get_size2::GetSize for DynamicClassLiteral<'_> {} @@ -5120,9 +5125,12 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns the explicit base classes of this dynamic class. /// - /// The bases are computed lazily from the `type()` call expression. For assigned - /// `type()` calls, this uses deferred inference to handle forward references - /// (e.g., `X = type("X", (tuple["X | None"],), {})`). + /// For assigned `type()` 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. /// /// 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. @@ -5130,50 +5138,35 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { + // For dangling calls, bases are stored directly on the anchor. + if let DynamicClassAnchor::ScopeOffset { explicit_bases, .. } = self.anchor(db) { + return explicit_bases; + } + + // For assigned calls, we need to use deferred inference. + let DynamicClassAnchor::Definition(definition) = self.anchor(db) else { + unreachable!("handled above") + }; + let scope = self.scope(db); let file = scope.file(db); let module = parsed_module(db, file).load(db); - // Get the `type()` call expression and extract the `bases` argument. - let call_expr = match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => { - let value = definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments"); - value - .as_call_expr() - .expect("Definition value should be a call expression") - } - DynamicClassAnchor::ScopeOffset { offset, .. } => { - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); - module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall") - } - }; + let value = definition + .kind(db) + .value(&module) + .expect("DynamicClassAnchor::Definition should only be used for assignments"); + let call_expr = value + .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 { return Box::default(); }; - // Infer the `bases` type. - let bases_type = match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => { - // Use `definition_expression_type` for deferred inference support. - definition_expression_type(db, definition, bases_arg) - } - DynamicClassAnchor::ScopeOffset { .. } => { - // For dangling calls, the bases were already inferred as part of the scope. - infer_complete_scope_types(db, scope).expression_type(bases_arg) - } - }; + // 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. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f833eaf9193d3..686a3693b8593 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7602,9 +7602,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let scope = self.scope(); + // For assigned `type()` calls, the bases are inferred via deferred inference after the + // class is created. For dangling calls, we infer bases eagerly since they cannot + // recursively reference the class being defined. + let bases_type = + bases_type.unwrap_or_else(|| self.infer_expression(bases_arg, TypeContext::default())); + // Create the anchor for identifying this dynamic class. // - For assigned `type()` calls, the Definition uniquely identifies the class. - // - For dangling calls, compute a relative offset from the scope's node index. + // - For dangling calls, compute a relative offset from the scope's node index, + // and store the explicit bases directly (computed eagerly above). let anchor = if let Some(def) = definition { DynamicClassAnchor::Definition(def) } else { @@ -7616,9 +7623,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let call_u32 = call_node_index .as_u32() .expect("call node should not be NodeIndex::NONE"); + + // Extract explicit bases from the bases tuple type. + let explicit_bases = Self::extract_explicit_bases(db, bases_type); + DynamicClassAnchor::ScopeOffset { scope, offset: call_u32 - anchor_u32, + explicit_bases, } }; @@ -7631,12 +7643,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { None, ); - // For assigned `type()` calls, the bases are inferred via deferred inference after the - // class is created. For dangling calls, `bases_type` is already available from immediate - // inference. - let bases_type = - bases_type.unwrap_or_else(|| self.infer_expression(bases_arg, TypeContext::default())); - // Validate bases and collect disjoint bases for diagnostics. let mut disjoint_bases = self.validate_dynamic_type_bases(bases_arg, bases_type, &name); @@ -8483,6 +8489,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Extract explicit base types from a bases tuple type. + /// + /// This is used for dangling `type()` calls where bases are computed eagerly + /// and stored directly on the `DynamicClassAnchor::ScopeOffset` variant. + fn extract_explicit_bases(db: &'db dyn Db, bases_type: Type<'db>) -> Box<[Type<'db>]> { + let Some(tuple_spec) = bases_type.tuple_instance_spec(db) else { + return Box::from([Type::unknown()]); + }; + let Some(elements) = tuple_spec.as_fixed_length() else { + return Box::from([Type::unknown()]); + }; + elements.elements_slice().iter().copied().collect() + } + /// Validate base classes from the second argument of a `type()` call. /// /// This validates bases that are valid `ClassBase` variants but aren't allowed From 5173285bf8ed1b70b0b0c8ec7275009333f47783 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Jan 2026 12:26:51 -0500 Subject: [PATCH 06/18] Remove deferred --- .../ty_python_semantic/resources/mdtest/call/type.md | 10 +++++++++- crates/ty_python_semantic/src/types/class.rs | 4 +--- crates/ty_python_semantic/src/types/infer/builder.rs | 9 --------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 40b50e8602baf..e01540d93a185 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -785,6 +785,8 @@ Y = type("Y", bases, {}) ## Cyclic functional class definitions +### Self-referential + Self-referential class definitions using `type()` are detected. The name being defined is referenced in the bases tuple before it's available: @@ -793,13 +795,17 @@ in the bases tuple before it's available: X = type("X", (X,), {}) ``` -String literals in the bases tuple are not valid class bases: +### No string literal bases + +String literals directly in the bases tuple are not valid class bases: ```py # error: [invalid-base] "Invalid class base with type `Literal["X"]`" X = type("X", ("X",), {}) ``` +### Forward references via string annotations + However, forward references via string annotations are supported, similar to regular class definitions. This works with `NamedTuple` where field annotations can be forward references: @@ -811,6 +817,8 @@ X = type("X", (NamedTuple("NT", [("field", "X | int")]),), {}) reveal_type(X) # revealed: ``` +### Static class inheriting from dynamic class with forward ref + Forward references also work when a static class inherits from a dynamic class that references it: ```py diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 02f507e79335c..7d0fb9a5d9a4d 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -34,9 +34,7 @@ use crate::types::function::{ use crate::types::generics::{ GenericContext, InferableTypeVars, Specialization, walk_specialization, }; -use crate::types::infer::{ - infer_expression_type, infer_unpack_types, nearest_enclosing_class, -}; +use crate::types::infer::{infer_expression_type, infer_unpack_types, nearest_enclosing_class}; use crate::types::member::{Member, class_member}; use crate::types::mro::{DynamicMroError, DynamicMroErrorKind}; use crate::types::relation::{ diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 686a3693b8593..cd32ec82bfe07 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7283,15 +7283,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_newtype_assignment_deferred(arguments); return; } - if known_class == Some(KnownClass::Type) { - // Infer the `bases` argument for three-argument `type()` calls. - // This is deferred to break cycles for self-referential definitions - // like `X = type("X", (tuple["X | None"],), {})`. - if arguments.args.len() >= 2 { - self.infer_expression(&arguments.args[1], TypeContext::default()); - } - return; - } let mut constraint_tys = Vec::new(); for arg in arguments.args.iter().skip(1) { let constraint = self.infer_type_expression(arg); From 241083b9770e64d6bee93659352867550d3a6632 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 26 Jan 2026 14:26:40 -0500 Subject: [PATCH 07/18] Use deferred --- crates/ty_python_semantic/src/types/class.rs | 83 ++-- .../src/types/infer/builder.rs | 415 +++++++++++++----- 2 files changed, 337 insertions(+), 161 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 7d0fb9a5d9a4d..d87452acd6c6c 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -5057,6 +5057,7 @@ pub struct DynamicClassLiteral<'db> { /// uniquely identifies this class and can be used to find the `type()` call. /// - `ScopeOffset`: The `type()` 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()`). @@ -5108,7 +5109,7 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns the definition where this class is created, if it was assigned to a variable. pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { match self.anchor(db) { - DynamicClassAnchor::Definition(definition) => Some(definition), + DynamicClassAnchor::Definition(definition) => Some(*definition), DynamicClassAnchor::ScopeOffset { .. } => None, } } @@ -5117,7 +5118,7 @@ impl<'db> DynamicClassLiteral<'db> { pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { match self.anchor(db) { DynamicClassAnchor::Definition(definition) => definition.scope(db), - DynamicClassAnchor::ScopeOffset { scope, .. } => scope, + DynamicClassAnchor::ScopeOffset { scope, .. } => *scope, } } @@ -5134,48 +5135,50 @@ impl<'db> DynamicClassLiteral<'db> { /// or if the bases argument is not a tuple. /// /// Returns `[Unknown]` if the bases tuple is variable-length (like `tuple[type, ...]`). - #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn explicit_bases(self, db: &'db dyn Db) -> Box<[Type<'db>]> { - // For dangling calls, bases are stored directly on the anchor. - if let DynamicClassAnchor::ScopeOffset { explicit_bases, .. } = self.anchor(db) { - return explicit_bases; - } + 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. + #[salsa::tracked(returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size)] + fn deferred_explicit_bases<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + ) -> Box<[Type<'db>]> { + let module = parsed_module(db, definition.file(db)).load(db); - // For assigned calls, we need to use deferred inference. - let DynamicClassAnchor::Definition(definition) = self.anchor(db) else { - unreachable!("handled above") - }; + let value = definition + .kind(db) + .value(&module) + .expect("DynamicClassAnchor::Definition should only be used for assignments"); + let call_expr = value + .as_call_expr() + .expect("Definition value should be a call expression"); - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); + // The `bases` argument is the second positional argument. + let Some(bases_arg) = call_expr.arguments.args.get(1) else { + return Box::default(); + }; - let value = definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments"); - let call_expr = value - .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 { - return Box::default(); - }; + // Use `definition_expression_type` for deferred inference support. + let bases_type = definition_expression_type(db, definition, bases_arg); - // 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. + let Some(tuple_spec) = bases_type.tuple_instance_spec(db) else { + return Box::from([Type::unknown()]); + }; + let Some(elements) = tuple_spec.as_fixed_length() else { + return Box::from([Type::unknown()]); + }; - // For variable-length tuples (like `tuple[type, ...]`), we can't statically - // determine the bases, so return Unknown. - let Some(tuple_spec) = bases_type.tuple_instance_spec(db) else { - return Box::from([Type::unknown()]); - }; - let Some(elements) = tuple_spec.as_fixed_length() else { - return Box::from([Type::unknown()]); - }; + elements.elements_slice().iter().copied().collect() + } - elements.elements_slice().iter().copied().collect() + match self.anchor(db) { + // For dangling calls, bases are stored directly on the anchor. + DynamicClassAnchor::ScopeOffset { explicit_bases, .. } => explicit_bases.as_ref(), + // For assigned calls, use deferred inference. + DynamicClassAnchor::Definition(definition) => deferred_explicit_bases(db, *definition), + } } /// Returns a [`Span`] with the range of the `type()` call expression. @@ -5207,7 +5210,7 @@ impl<'db> DynamicClassLiteral<'db> { let anchor_u32 = scope_anchor .as_u32() .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); + let absolute_index = NodeIndex::from(anchor_u32 + *offset); // Get the node and return its range. let node: &ast::ExprCall = module @@ -5470,7 +5473,7 @@ impl<'db> DynamicClassLiteral<'db> { Self::new( db, self.name(db).clone(), - self.anchor(db), + self.anchor(db).clone(), self.members(db), self.has_dynamic_namespace(db), dataclass_params, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index cd32ec82bfe07..6f4005ec7616d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -651,8 +651,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Infer deferred types for all definitions. - for definition in std::mem::take(&mut self.deferred) { - self.extend_definition(infer_deferred_types(self.db(), definition)); + // Save the definitions for later validation (some may be dynamic classes). + let deferred_definitions: Vec<_> = std::mem::take(&mut self.deferred).into_iter().collect(); + for definition in &deferred_definitions { + self.extend_definition(infer_deferred_types(self.db(), *definition)); } assert!( @@ -662,6 +664,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if self.db().should_check_file(self.file()) { self.check_static_class_definitions(); + self.check_dynamic_class_definitions(&deferred_definitions); self.check_overloaded_functions(node); self.check_type_guard_definitions(); self.check_legacy_positional_only_convention(); @@ -7283,6 +7286,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_newtype_assignment_deferred(arguments); return; } + if let Some(KnownClass::Type) = known_class + && let InferenceRegion::Deferred(definition) = self.region + { + self.infer_builtins_type_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); @@ -7390,6 +7399,248 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Deferred inference for assigned `type()` calls. + /// + /// Infers the bases argument that was skipped during initial inference to handle + /// forward references and recursive definitions. This matches the pattern used + /// by `typing.NamedTuple`. + 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; + + // Validate individual bases for special types that aren't allowed in dynamic classes. + // This is local validation that doesn't require calling explicit_bases(). + let name = dynamic_class.name(db); + self.validate_dynamic_type_bases(bases_arg, bases_type, name); + } + + /// 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. + fn check_dynamic_class_definitions(&mut self, deferred_definitions: &[Definition<'db>]) { + let db = self.db(); + let module = self.module(); + + // Find all deferred definitions that are dynamic classes. + // These are the `type()` calls that had deferred bases inference. + let dynamic_class_definitions: Vec<_> = deferred_definitions + .iter() + .filter_map(|definition| { + // Only check assignment definitions (type() calls). + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + return None; + }; + + // Get the binding type for this definition. + let ty = binding_type(db, *definition); + + // Check if it's a dynamic class with a Definition anchor. + let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = ty else { + return None; + }; + + // Only check classes with Definition anchors (assigned type() calls). + // Dangling type() calls are validated eagerly during inference. + let DynamicClassAnchor::Definition(_) = dynamic_class.anchor(db) else { + return None; + }; + + let value = assignment.value(module); + let call_expr = value.as_call_expr()?; + Some((dynamic_class, call_expr)) + }) + .collect(); + + for (dynamic_class, call_expr) in dynamic_class_definitions { + self.check_dynamic_class_definition(dynamic_class, call_expr); + } + } + + /// Report MRO errors for a dynamic class. + /// + /// Returns `true` if the MRO is valid, `false` if there were errors. + fn report_dynamic_mro_errors( + &mut self, + dynamic_class: DynamicClassLiteral<'db>, + call_expr: &ast::ExprCall, + bases: &ast::Expr, + ) -> bool { + let db = self.db(); + let Err(error) = dynamic_class.try_mro(db) else { + return true; + }; + + let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); + + match error.reason() { + DynamicMroErrorKind::InvalidBases(invalid_bases) => { + for (idx, base_type) in invalid_bases { + // Check if the type is "type-like" (e.g., `type[Base]`). + let instance_of_type = KnownClass::Type.to_instance(db); + + // Determine the diagnostic node; prefer specific base expr, fall back to bases. + let specific_base = bases_tuple_elts.and_then(|elts| elts.get(*idx)); + let diagnostic_range = specific_base + .map(ast::Expr::range) + .unwrap_or_else(|| bases.range()); + + if base_type.is_assignable_to(db, instance_of_type) { + if let Some(builder) = self + .context + .report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_range) + { + let mut diagnostic = builder.into_diagnostic("Unsupported class base"); + diagnostic.set_primary_message(format_args!( + "Has type `{}`", + base_type.display(db) + )); + diagnostic.info(format_args!( + "ty cannot determine a MRO for class `{}` due to this base", + dynamic_class.name(db) + )); + diagnostic + .info("Only class objects or `Any` are supported as class bases"); + } + } else if let Some(builder) = + self.context.report_lint(&INVALID_BASE, diagnostic_range) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid class base with type `{}`", + base_type.display(db) + )); + if specific_base.is_none() { + diagnostic + .info(format_args!("Element {} of the tuple is invalid", idx + 1)); + } + } + } + } + DynamicMroErrorKind::InheritanceCycle => { + if let Some(builder) = self + .context + .report_lint(&CYCLIC_CLASS_DEFINITION, call_expr) + { + builder.into_diagnostic(format_args!( + "Cyclic definition of `{}`", + dynamic_class.name(db) + )); + } + } + DynamicMroErrorKind::DuplicateBases(duplicates) => { + if let Some(builder) = self.context.report_lint(&DUPLICATE_BASE, call_expr) { + builder.into_diagnostic(format_args!( + "Duplicate base class{maybe_s} {dupes} in class `{class}`", + maybe_s = if duplicates.len() == 1 { "" } else { "es" }, + dupes = duplicates + .iter() + .map(|base: &ClassBase<'_>| base.display(db)) + .join(", "), + class = dynamic_class.name(db), + )); + } + } + DynamicMroErrorKind::UnresolvableMro => { + if let Some(builder) = self.context.report_lint(&INCONSISTENT_MRO, call_expr) { + builder.into_diagnostic(format_args!( + "Cannot create a consistent method resolution order (MRO) \ + for class `{}` with bases `[{}]`", + dynamic_class.name(db), + dynamic_class + .explicit_bases(db) + .iter() + .map(|base| base.display(db)) + .join(", ") + )); + } + } + } + + false + } + + /// Check a single dynamic class definition for MRO and metaclass errors. + fn check_dynamic_class_definition( + &mut self, + dynamic_class: DynamicClassLiteral<'db>, + call_expr: &ast::ExprCall, + ) { + 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 { + return; + }; + + // Check for MRO errors. + if self.report_dynamic_mro_errors(dynamic_class, call_expr, bases) { + // MRO succeeded, check for instance-layout-conflict. + // Compute disjoint bases from explicit_bases(). + let mut disjoint_bases = IncompatibleBases::default(); + let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); + + for (idx, base_type) in dynamic_class.explicit_bases(db).iter().enumerate() { + // Convert to ClassType to access nearest_disjoint_base. + if let Some(class_type) = base_type.to_class_type(db) { + if let Some(disjoint_base) = class_type.nearest_disjoint_base(db) { + disjoint_bases.insert(disjoint_base, idx, class_type.class_literal(db)); + } + } + } + + disjoint_bases.remove_redundant_entries(db); + if disjoint_bases.len() > 1 { + report_instance_layout_conflict( + &self.context, + dynamic_class.header_range(db), + bases_tuple_elts, + &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), + ); + } + } + /// Infer a call to `builtins.type()`. /// /// `builtins.type` has two overloads: a single-argument overload (e.g. `type("foo")`, @@ -7485,14 +7736,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let name_type = self.infer_expression(name_arg, TypeContext::default()); - // For assigned `type()` calls, defer bases inference to break cycles for self-referential - // definitions like `X = type("X", (tuple["X | None"],), {})`. The bases will be inferred - // later via `DynamicClassLiteral::explicit_bases()`. + + // 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. let bases_type = if definition.is_some() { + // Don't infer bases eagerly for assigned calls; defer to later. None } else { + // For dangling calls, infer bases eagerly since they can't reference the class. Some(self.infer_expression(bases_arg, TypeContext::default())) }; + let namespace_type = self.infer_expression(namespace_arg, TypeContext::default()); // TODO: validate other keywords against `__init_subclass__` methods of superclasses @@ -7593,17 +7849,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let scope = self.scope(); - // For assigned `type()` calls, the bases are inferred via deferred inference after the - // class is created. For dangling calls, we infer bases eagerly since they cannot - // recursively reference the class being defined. - let bases_type = - bases_type.unwrap_or_else(|| self.infer_expression(bases_arg, TypeContext::default())); - // Create the anchor for identifying this dynamic class. - // - For assigned `type()` calls, the Definition uniquely identifies the 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 (computed eagerly above). + // 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, self.multi_inference_state); DynamicClassAnchor::Definition(def) } else { let call_node_index = call_expr.node_index().load(); @@ -7615,8 +7868,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_u32() .expect("call node should not be NodeIndex::NONE"); - // Extract explicit bases from the bases tuple type. - let explicit_bases = Self::extract_explicit_bases(db, bases_type); + // Extract explicit bases from the bases tuple type (which was inferred eagerly). + let explicit_bases = Self::extract_explicit_bases( + db, + bases_type.expect("bases_type should be Some for dangling calls"), + ); DynamicClassAnchor::ScopeOffset { scope, @@ -7634,97 +7890,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { None, ); - // Validate bases and collect disjoint bases for diagnostics. - let mut disjoint_bases = self.validate_dynamic_type_bases(bases_arg, bases_type, &name); + // For dangling calls, validate bases eagerly. For assigned calls, validation is + // deferred along with bases inference. + if let Some(bases_type) = bases_type { + // Validate bases and collect disjoint bases for diagnostics. + let mut disjoint_bases = self.validate_dynamic_type_bases(bases_arg, bases_type, &name); - // Check for MRO errors. - match dynamic_class.try_mro(db) { - Err(error) => match error.reason() { - DynamicMroErrorKind::InvalidBases(invalid_bases) => { - // Get the AST nodes for base expressions (for diagnostics). - let bases_tuple_elts = bases_arg.as_tuple_expr().map(|t| t.elts.as_slice()); - - for (idx, base_type) in invalid_bases { - let diagnostic_node = bases_tuple_elts - .and_then(|elts| elts.get(*idx)) - .unwrap_or(bases_arg); - - // Check if the type is "type-like" (e.g., `type[Base]`). - let instance_of_type = KnownClass::Type.to_instance(db); - if base_type.is_assignable_to(db, instance_of_type) { - if let Some(builder) = self - .context - .report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_node) - { - let mut diagnostic = - builder.into_diagnostic("Unsupported class base"); - diagnostic.set_primary_message(format_args!( - "Has type `{}`", - base_type.display(db) - )); - diagnostic.info(format_args!( - "ty cannot determine a MRO for class `{name}` due to this base" - )); - diagnostic.info( - "Only class objects or `Any` are supported as class bases", - ); - } - } else if let Some(builder) = - self.context.report_lint(&INVALID_BASE, diagnostic_node) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid class base with type `{}`", - base_type.display(db) - )); - if bases_tuple_elts.is_none() { - diagnostic.info(format_args!( - "Element {} of the tuple is invalid", - idx + 1 - )); - } - } - } - } - DynamicMroErrorKind::InheritanceCycle => { - if let Some(builder) = self - .context - .report_lint(&CYCLIC_CLASS_DEFINITION, call_expr) - { - builder.into_diagnostic(format_args!( - "Cyclic definition of `{}`", - dynamic_class.name(db) - )); - } - } - DynamicMroErrorKind::DuplicateBases(duplicates) => { - if let Some(builder) = self.context.report_lint(&DUPLICATE_BASE, call_expr) { - builder.into_diagnostic(format_args!( - "Duplicate base class{maybe_s} {dupes} in class `{class}`", - maybe_s = if duplicates.len() == 1 { "" } else { "es" }, - dupes = duplicates - .iter() - .map(|base: &ClassBase<'_>| base.display(db)) - .join(", "), - class = dynamic_class.name(db), - )); - } - } - DynamicMroErrorKind::UnresolvableMro => { - if let Some(builder) = self.context.report_lint(&INCONSISTENT_MRO, call_expr) { - builder.into_diagnostic(format_args!( - "Cannot create a consistent method resolution order (MRO) \ - for class `{}` with bases `[{}]`", - dynamic_class.name(db), - dynamic_class - .explicit_bases(db) - .iter() - .map(|base| base.display(db)) - .join(", ") - )); - } - } - }, - Ok(_) => { + // Check for MRO errors. + 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 { @@ -7736,25 +7909,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } } - } - // 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), + // Check for metaclass conflicts. + if let Err(DynamicMetaclassConflict { metaclass1, - base1.display(db), + base1, metaclass2, - base2.display(db), - ); + 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)) From d16901b016ebda2ebc7dc779eeac3a338534027e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Jan 2026 21:15:22 -0500 Subject: [PATCH 08/18] Touch-ups --- crates/ty_python_semantic/src/types.rs | 9 ++ crates/ty_python_semantic/src/types/class.rs | 11 +- .../src/types/infer/builder.rs | 152 +++++++++--------- crates/ty_python_semantic/src/types/mro.rs | 4 +- 4 files changed, 86 insertions(+), 90 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 47068e15df746..2d9a4f8935ef6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1179,6 +1179,15 @@ impl<'db> Type<'db> { .and_then(|instance| instance.own_tuple_spec(db)) } + /// If this type is a fixed-length tuple instance, returns a boxed 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)?; + let fixed = tuple_spec.as_fixed_length()?; + Some(fixed.elements_slice().iter().copied().collect()) + } + /// 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 d87452acd6c6c..62339344c7726 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -5163,14 +5163,9 @@ impl<'db> DynamicClassLiteral<'db> { // For variable-length tuples (like `tuple[type, ...]`), we can't statically // determine the bases, so return Unknown. - let Some(tuple_spec) = bases_type.tuple_instance_spec(db) else { - return Box::from([Type::unknown()]); - }; - let Some(elements) = tuple_spec.as_fixed_length() else { - return Box::from([Type::unknown()]); - }; - - elements.elements_slice().iter().copied().collect() + bases_type + .fixed_tuple_elements(db) + .unwrap_or_else(|| Box::from([Type::unknown()])) } match self.anchor(db) { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6f4005ec7616d..ad3281f3d470b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -651,7 +651,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } // Infer deferred types for all definitions. - // Save the definitions for later validation (some may be dynamic classes). let deferred_definitions: Vec<_> = std::mem::take(&mut self.deferred).into_iter().collect(); for definition in &deferred_definitions { self.extend_definition(infer_deferred_types(self.db(), *definition)); @@ -7402,8 +7401,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Deferred inference for assigned `type()` calls. /// /// Infers the bases argument that was skipped during initial inference to handle - /// forward references and recursive definitions. This matches the pattern used - /// by `typing.NamedTuple`. + /// forward references and recursive definitions. fn infer_builtins_type_deferred(&mut self, definition: Definition<'db>, call_expr: &ast::Expr) { let db = self.db(); @@ -7431,10 +7429,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // 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. - // This is local validation that doesn't require calling explicit_bases(). let name = dynamic_class.name(db); - self.validate_dynamic_type_bases(bases_arg, bases_type, name); + self.validate_dynamic_type_bases(bases_arg, &bases, name); } /// Iterate over all dynamic class definitions (created using `type()` calls) to check that @@ -7444,37 +7446,31 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let module = self.module(); - // Find all deferred definitions that are dynamic classes. - // These are the `type()` calls that had deferred bases inference. - let dynamic_class_definitions: Vec<_> = deferred_definitions - .iter() - .filter_map(|definition| { - // Only check assignment definitions (type() calls). - let DefinitionKind::Assignment(assignment) = definition.kind(db) else { - return None; - }; + for definition in deferred_definitions { + // Only check assignment definitions (`type()` calls). + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + continue; + }; - // Get the binding type for this definition. - let ty = binding_type(db, *definition); + // Get the binding type for this definition. + let ty = binding_type(db, *definition); - // Check if it's a dynamic class with a Definition anchor. - let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = ty else { - return None; - }; + // Check if it's a dynamic class with a Definition anchor. + let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = ty else { + continue; + }; - // Only check classes with Definition anchors (assigned type() calls). - // Dangling type() calls are validated eagerly during inference. - let DynamicClassAnchor::Definition(_) = dynamic_class.anchor(db) else { - return None; - }; + // Only check classes with Definition anchors (i.e., assigned `type()` calls). + // Dangling `type()` calls are validated eagerly during inference. + let DynamicClassAnchor::Definition(_) = dynamic_class.anchor(db) else { + continue; + }; - let value = assignment.value(module); - let call_expr = value.as_call_expr()?; - Some((dynamic_class, call_expr)) - }) - .collect(); + let value = assignment.value(module); + let Some(call_expr) = value.as_call_expr() else { + continue; + }; - for (dynamic_class, call_expr) in dynamic_class_definitions { self.check_dynamic_class_definition(dynamic_class, call_expr); } } @@ -7589,7 +7585,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) { let db = self.db(); - // A valid 3-argument type() call must have a bases argument. + // A valid 3-argument type() call must have a `bases` argument. let Some(bases) = call_expr.arguments.args.get(1) else { return; }; @@ -7597,7 +7593,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Check for MRO errors. if self.report_dynamic_mro_errors(dynamic_class, call_expr, bases) { // MRO succeeded, check for instance-layout-conflict. - // Compute disjoint bases from explicit_bases(). let mut disjoint_bases = IncompatibleBases::default(); let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); @@ -7832,7 +7827,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Extract name and base classes. let name = if let Type::StringLiteral(literal) = name_type { - ast::name::Name::new(literal.value(db)) + 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) @@ -7844,11 +7839,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { name_type.display(db) )); } - ast::name::Name::new_static("") + Name::new_static("") }; let scope = self.scope(); + // For dangling calls, extract bases eagerly (they'll be stored in the anchor + // and used for validation). Also validate that bases is a tuple type. + let explicit_bases = bases_type.and_then(|ty| self.extract_explicit_bases(bases_arg, ty)); + // Create the anchor for identifying this dynamic class. // - For assigned `type()` calls, the Definition uniquely identifies the class, // and bases inference is deferred. @@ -7868,16 +7867,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_u32() .expect("call node should not be NodeIndex::NONE"); - // Extract explicit bases from the bases tuple type (which was inferred eagerly). - let explicit_bases = Self::extract_explicit_bases( - db, - bases_type.expect("bases_type should be Some for dangling calls"), - ); + // 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, + explicit_bases: anchor_bases, } }; @@ -7892,9 +7890,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // For dangling calls, validate bases eagerly. For assigned calls, validation is // deferred along with bases inference. - if let Some(bases_type) = bases_type { + 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, bases_type, &name); + let mut disjoint_bases = + self.validate_dynamic_type_bases(bases_arg, explicit_bases, &name); // Check for MRO errors. if self.report_dynamic_mro_errors(dynamic_class, call_expr, bases_arg) { @@ -8655,16 +8654,31 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Extract explicit base types from a bases tuple type. /// - /// This is used for dangling `type()` calls where bases are computed eagerly - /// and stored directly on the `DynamicClassAnchor::ScopeOffset` variant. - fn extract_explicit_bases(db: &'db dyn Db, bases_type: Type<'db>) -> Box<[Type<'db>]> { - let Some(tuple_spec) = bases_type.tuple_instance_spec(db) else { - return Box::from([Type::unknown()]); - }; - let Some(elements) = tuple_spec.as_fixed_length() else { - return Box::from([Type::unknown()]); - }; - elements.elements_slice().iter().copied().collect() + /// Emits a diagnostic if `bases_type` is not a valid tuple type. + /// + /// Returns `None` if the bases cannot be extracted. + fn extract_explicit_bases( + &mut self, + bases_node: &ast::Expr, + bases_type: Type<'db>, + ) -> 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 Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, bases_node) + { + let mut diagnostic = + builder.into_diagnostic("Invalid argument to parameter 2 (`bases`) of `type()`"); + diagnostic.set_primary_message(format_args!( + "Expected `tuple[type, ...]`, found `{}`", + bases_type.display(db) + )); + } + bases_type.fixed_tuple_elements(db) } /// Validate base classes from the second argument of a `type()` call. @@ -8677,52 +8691,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn validate_dynamic_type_bases( &mut self, bases_node: &ast::Expr, - bases_type: Type<'db>, - name: &ast::name::Name, + bases: &[Type<'db>], + name: &Name, ) -> IncompatibleBases<'db> { let db = self.db(); // Get AST nodes for base expressions (for diagnostics). let bases_tuple_elts = bases_node.as_tuple_expr().map(|t| t.elts.as_slice()); - // We use a placeholder class literal for try_from_type (the subclass parameter is only + // We use a placeholder class literal for `try_from_type` (the subclass parameter is only // used for Protocol/TypedDict detection which doesn't apply here). let placeholder_class: ClassLiteral<'db> = KnownClass::Object.try_to_class_literal(db).unwrap().into(); let mut disjoint_bases = IncompatibleBases::default(); - let Some(tuple_spec) = bases_type.tuple_instance_spec(db) else { - // The `bases` argument is not a tuple. Emit a diagnostic. - if !bases_type.is_assignable_to( - db, - Type::homogeneous_tuple(db, KnownClass::Type.to_instance(db)), - ) { - if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, bases_node) - { - let mut diagnostic = builder - .into_diagnostic("Invalid argument to parameter 2 (`bases`) of `type()`"); - diagnostic.set_primary_message(format_args!( - "Expected `tuple[type, ...]`, found `{}`", - bases_type.display(db) - )); - } - } - return disjoint_bases; - }; - let Some(tuple) = tuple_spec.as_fixed_length() else { - return disjoint_bases; - }; - // Check each base for special cases that are not allowed for dynamic classes. - for (idx, base) in tuple.elements_slice().iter().enumerate() { + for (idx, base) in bases.iter().enumerate() { let diagnostic_node = bases_tuple_elts .and_then(|elts| elts.get(idx)) .unwrap_or(bases_node); // Try to convert to ClassBase to check for special cases. let Some(class_base) = ClassBase::try_from_type(db, *base, placeholder_class) else { - // Can't convert - will be handled by InvalidBases error from try_mro. + // Can't convert; will be handled by `InvalidBases` error from `try_mro`. continue; }; diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index dde2969a02107..183caa859e25f 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -429,12 +429,12 @@ impl<'db> Mro<'db> { let mut seen = FxHashSet::default(); seen.insert(self_base); - // Use a placeholder class literal for try_from_type. + // Use a placeholder class literal for `try_from_type`. let placeholder_class: ClassLiteral<'db> = KnownClass::Object.try_to_class_literal(db).unwrap().into(); for base_type in dynamic.explicit_bases(db) { - // Convert Type to ClassBase, falling back to Unknown if conversion fails. + // Convert `Type` to `ClassBase`, falling back to `Unknown` if conversion fails. let base = ClassBase::try_from_type(db, *base_type, placeholder_class) .unwrap_or_else(ClassBase::unknown); From 0892115dbdcbd8f40c3e78b8debdaaa3b717a7ab Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 13 Feb 2026 19:58:37 -0500 Subject: [PATCH 09/18] Use match --- .../src/types/infer/builder.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ad3281f3d470b..b0d8f3bc9be04 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7281,15 +7281,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let known_class = func_ty .as_class_literal() .and_then(|cls| cls.known(self.db())); - if known_class == Some(KnownClass::NewType) { - self.infer_newtype_assignment_deferred(arguments); - return; - } - if let Some(KnownClass::Type) = known_class - && let InferenceRegion::Deferred(definition) = self.region - { - self.infer_builtins_type_deferred(definition, value); - return; + match (known_class, self.region) { + (Some(KnownClass::NewType), _) => { + self.infer_newtype_assignment_deferred(arguments); + return; + } + (Some(KnownClass::Type), InferenceRegion::Deferred(definition)) => { + self.infer_builtins_type_deferred(definition, value); + return; + } + _ => {} } let mut constraint_tys = Vec::new(); for arg in arguments.args.iter().skip(1) { From 344ea08ccbe3698d53f0d760cf25846db5a5c91f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 13 Feb 2026 20:02:22 -0500 Subject: [PATCH 10/18] Consolidate bases_type --- .../src/types/infer/builder.rs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b0d8f3bc9be04..9278673b1f414 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7733,18 +7733,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let name_type = self.infer_expression(name_arg, TypeContext::default()); - // 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. - let bases_type = if definition.is_some() { - // Don't infer bases eagerly for assigned calls; defer to later. - None - } else { - // For dangling calls, infer bases eagerly since they can't reference the class. - Some(self.infer_expression(bases_arg, TypeContext::default())) - }; - let namespace_type = self.infer_expression(namespace_arg, TypeContext::default()); // TODO: validate other keywords against `__init_subclass__` methods of superclasses @@ -7845,9 +7833,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let scope = self.scope(); - // For dangling calls, extract bases eagerly (they'll be stored in the anchor - // and used for validation). Also validate that bases is a tuple type. - let explicit_bases = bases_type.and_then(|ty| self.extract_explicit_bases(bases_arg, ty)); + // 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, From e01c5a1438ec2ee031d7445868323ba6f2449180 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 13 Feb 2026 20:10:38 -0500 Subject: [PATCH 11/18] Add cyclic MRO test --- .../ty_python_semantic/resources/mdtest/call/type.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index e01540d93a185..f99e502d0e6a4 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -594,6 +594,17 @@ class Y(C, B): ... Conflict = type("Conflict", (X, Y), {}) ``` +## Cyclic base class MRO + +A dynamic class inheriting from a static class with a cyclic MRO also produces an error: + +```pyi +class Cyclic(Cyclic): ... # error: [cyclic-class-definition] + +# error: [cyclic-class-definition] +CyclicChild = type("CyclicChild", (Cyclic,), {}) +``` + ## `inconsistent-mro` errors with autofixes A common cause of "inconsistent MRO" errors is where a class inherits from `Generic[]`, but From d9e2514fd62c93446e8db90d4903e98593f07432 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 13 Feb 2026 20:14:16 -0500 Subject: [PATCH 12/18] Remove unused arm --- crates/ty_python_semantic/src/types/class.rs | 89 +------------------- 1 file changed, 4 insertions(+), 85 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 62339344c7726..a8a1b8e02e505 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -20,9 +20,7 @@ use crate::types::bound_super::BoundSuperError; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::context::InferContext; use crate::types::diagnostic::{ - CYCLIC_CLASS_DEFINITION, DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_BASE, INVALID_DATACLASS_OVERRIDE, INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD, - report_conflicting_metaclass_from_bases, }; use crate::types::enums::{ enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value, @@ -36,7 +34,7 @@ use crate::types::generics::{ }; use crate::types::infer::{infer_expression_type, infer_unpack_types, nearest_enclosing_class}; use crate::types::member::{Member, class_member}; -use crate::types::mro::{DynamicMroError, DynamicMroErrorKind}; +use crate::types::mro::DynamicMroError; use crate::types::relation::{ HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation, }; @@ -8190,89 +8188,10 @@ impl KnownClass { ))); } + // `type()` calls are handled by `infer_builtins_type_call` and never + // go through normal call inference, so this arm should be unreachable. KnownClass::Type => { - // Check for MRO and metaclass errors in three-argument type() calls. - if let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = - overload.return_type() - { - // Check for MRO errors - if let Err(error) = dynamic_class.try_mro(db) { - match error.reason() { - DynamicMroErrorKind::InvalidBases(invalid_bases) => { - for (_, base_type) in invalid_bases { - if let Some(builder) = - context.report_lint(&INVALID_BASE, call_expression) - { - builder.into_diagnostic(format_args!( - "Invalid class base with type `{}`", - base_type.display(db) - )); - } - } - } - DynamicMroErrorKind::InheritanceCycle => { - if let Some(builder) = - context.report_lint(&CYCLIC_CLASS_DEFINITION, call_expression) - { - builder.into_diagnostic(format_args!( - "Cyclic definition of `{}`", - dynamic_class.name(db) - )); - } - } - DynamicMroErrorKind::DuplicateBases(duplicates) => { - if let Some(builder) = - context.report_lint(&DUPLICATE_BASE, call_expression) - { - builder.into_diagnostic(format_args!( - "Duplicate base class{maybe_s} {dupes} in class `{class}`", - maybe_s = if duplicates.len() == 1 { "" } else { "es" }, - dupes = duplicates - .iter() - .map(|base: &ClassBase<'_>| base.display(db)) - .join(", "), - class = dynamic_class.name(db), - )); - } - } - DynamicMroErrorKind::UnresolvableMro => { - if let Some(builder) = - context.report_lint(&INCONSISTENT_MRO, call_expression) - { - builder.into_diagnostic(format_args!( - "Cannot create a consistent method resolution order (MRO) \ - for class `{}` with bases `[{}]`", - dynamic_class.name(db), - dynamic_class - .explicit_bases(db) - .iter() - .map(|base| base.display(db)) - .join(", ") - )); - } - } - } - } - - // Check for metaclass conflicts - if let Err(DynamicMetaclassConflict { - metaclass1, - base1, - metaclass2, - base2, - }) = dynamic_class.try_metaclass(db) - { - report_conflicting_metaclass_from_bases( - context, - call_expression.into(), - dynamic_class.name(db), - metaclass1, - base1.display(db), - metaclass2, - base2.display(db), - ); - } - } + unreachable!("three-argument `type()` calls are handled before `check_call`") } _ => {} From 0b2e311c74162a88337be396b177c88266cf9d4c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 13 Feb 2026 20:14:49 -0500 Subject: [PATCH 13/18] Remove bases --- crates/ty_python_semantic/src/types/class.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a8a1b8e02e505..f27cce70236bd 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -5022,7 +5022,7 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { /// /// The type of `Foo` would be `` where `Foo` is a `DynamicClassLiteral` with: /// - name: "Foo" -/// - bases: [Base] +/// - members: [("attr", int)] /// /// This is called "dynamic" because the class is created dynamically at runtime /// via a function call rather than a class statement. From 1f0b93e45dbc34eb17c59e181478e6bfca5566e1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 13 Feb 2026 20:22:10 -0500 Subject: [PATCH 14/18] Return cow --- crates/ty_python_semantic/src/types.rs | 16 ++++++++++++---- crates/ty_python_semantic/src/types/class.rs | 2 ++ .../src/types/infer/builder.rs | 7 ++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 2d9a4f8935ef6..c925e1167944b 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1179,13 +1179,21 @@ impl<'db> Type<'db> { .and_then(|instance| instance.own_tuple_spec(db)) } - /// If this type is a fixed-length tuple instance, returns a boxed slice of its element types. + /// 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]>> { + fn fixed_tuple_elements(&self, db: &'db dyn Db) -> Option]>> { let tuple_spec = self.tuple_instance_spec(db)?; - let fixed = tuple_spec.as_fixed_length()?; - Some(fixed.elements_slice().iter().copied().collect()) + 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`. diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index f27cce70236bd..81d2d05ee4ef3 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -5163,6 +5163,8 @@ impl<'db> DynamicClassLiteral<'db> { // 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()])) } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9278673b1f414..bd72dbf3a42df 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use itertools::{Either, EitherOrBoth, Itertools}; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, @@ -8675,7 +8677,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bases_type.display(db) )); } - bases_type.fixed_tuple_elements(db) + bases_type + .fixed_tuple_elements(db) + .map(Cow::into_owned) + .map(Into::into) } /// Validate base classes from the second argument of a `type()` call. From edc587d145848ae0df3e7ebf919bd03d8bc9f075 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 13 Feb 2026 20:23:30 -0500 Subject: [PATCH 15/18] Clarify filter --- crates/ty_python_semantic/src/types/class.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 81d2d05ee4ef3..ecfa31a015380 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -5253,6 +5253,8 @@ impl<'db> DynamicClassLiteral<'db> { } // Convert Types to ClassBases for metaclass computation. + // All bases should convert successfully here: `try_mro()` above would have + // returned `Err(InvalidBases)` if any failed, causing us to return early. let placeholder_class: ClassLiteral<'db> = KnownClass::Object.try_to_class_literal(db).unwrap().into(); From 2a7ee114a31abde844551643f8c7be8a8ff0967a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 13 Feb 2026 20:32:55 -0500 Subject: [PATCH 16/18] Make base optional --- .../resources/mdtest/call/type.md | 15 ++++- crates/ty_python_semantic/src/types/class.rs | 5 +- .../src/types/class_base.rs | 4 +- .../src/types/infer/builder.rs | 9 +-- crates/ty_python_semantic/src/types/mro.rs | 63 +++++++++---------- 5 files changed, 49 insertions(+), 47 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index f99e502d0e6a4..5b0a7b32a8a76 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -1010,7 +1010,7 @@ Dynamic classes cannot directly inherit from `Generic`, `Protocol`, or `TypedDic forms require class syntax for their semantics to be properly applied: ```py -from typing import Generic, Protocol, TypeVar +from typing import Generic, NamedTuple, Protocol, TypeVar from typing_extensions import TypedDict T = TypeVar("T") @@ -1023,6 +1023,19 @@ ProtocolClass = type("ProtocolClass", (Protocol,), {}) # error: [invalid-base] "Invalid base for class created via `type()`" TypedDictClass = type("TypedDictClass", (TypedDict,), {}) + +# error: [invalid-base] "Invalid class base with type ``" +NamedTupleClass = type("NamedTupleClass", (NamedTuple,), {"x": int}) +``` + +`NamedTuple` is also not allowed as a base for dynamic classes, since creating a NamedTuple requires +class syntax for the field declarations to be properly processed: + +```py +from typing import NamedTuple + +# error: [invalid-base] "Invalid class base with type ``" +NT = type("NT", (NamedTuple,), {}) ``` ### Protocol bases diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index ecfa31a015380..6602926bb3a6c 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -5255,12 +5255,9 @@ impl<'db> DynamicClassLiteral<'db> { // Convert Types to ClassBases for metaclass computation. // All bases should convert successfully here: `try_mro()` above would have // returned `Err(InvalidBases)` if any failed, causing us to return early. - let placeholder_class: ClassLiteral<'db> = - KnownClass::Object.try_to_class_literal(db).unwrap().into(); - let bases: Vec> = original_bases .iter() - .filter_map(|base_type| ClassBase::try_from_type(db, *base_type, placeholder_class)) + .filter_map(|base_type| ClassBase::try_from_type(db, *base_type, None)) .collect(); // If all bases failed to convert, return type as the metaclass. diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 5a6d990b787fb..308ddee6cc599 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -94,7 +94,7 @@ impl<'db> ClassBase<'db> { pub(super) fn try_from_type( db: &'db dyn Db, ty: Type<'db>, - subclass: ClassLiteral<'db>, + subclass: Option>, ) -> Option { match ty { Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), @@ -252,7 +252,7 @@ impl<'db> ClassBase<'db> { SpecialFormType::Generic => Some(Self::Generic), SpecialFormType::NamedTuple => { - let class = subclass.as_static()?; + let class = subclass?.as_static()?; let fields = class.own_fields(db, None, CodeGeneratorKind::NamedTuple); Self::try_from_type( db, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index bd72dbf3a42df..b9def0a60a1b9 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8701,11 +8701,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Get AST nodes for base expressions (for diagnostics). let bases_tuple_elts = bases_node.as_tuple_expr().map(|t| t.elts.as_slice()); - // We use a placeholder class literal for `try_from_type` (the subclass parameter is only - // used for Protocol/TypedDict detection which doesn't apply here). - let placeholder_class: ClassLiteral<'db> = - KnownClass::Object.try_to_class_literal(db).unwrap().into(); - let mut disjoint_bases = IncompatibleBases::default(); // Check each base for special cases that are not allowed for dynamic classes. @@ -8715,13 +8710,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .unwrap_or(bases_node); // Try to convert to ClassBase to check for special cases. - let Some(class_base) = ClassBase::try_from_type(db, *base, placeholder_class) else { + let Some(class_base) = ClassBase::try_from_type(db, *base, None) else { // Can't convert; will be handled by `InvalidBases` error from `try_mro`. continue; }; // Check for special bases that are not allowed for dynamic classes. // Dynamic classes can't be generic, protocols, TypedDicts, or enums. + // (`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) diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 183caa859e25f..c8ee82e4a411e 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -8,7 +8,7 @@ use crate::Db; use crate::types::class_base::ClassBase; use crate::types::generics::Specialization; use crate::types::{ - ClassLiteral, ClassType, DynamicClassLiteral, KnownClass, KnownInstanceType, SpecialFormType, + ClassLiteral, ClassType, DynamicClassLiteral, KnownInstanceType, SpecialFormType, StaticClassLiteral, Type, }; @@ -141,25 +141,29 @@ impl<'db> Mro<'db> { ) ) => { - ClassBase::try_from_type(db, *single_base, ClassLiteral::Static(class_literal)) - .map_or_else( - || { - Err(StaticMroErrorKind::InvalidBases(Box::from([( - 0, - *single_base, - )]))) - }, - |single_base| { - if single_base.has_cyclic_mro(db) { - Err(StaticMroErrorKind::InheritanceCycle) - } else { - Ok(std::iter::once(ClassBase::Class(class)) - .chain(single_base.mro(db, specialization)) - .collect()) - } - }, - ) - .map_err(|err| err.into_mro_error(db, class)) + ClassBase::try_from_type( + db, + *single_base, + Some(ClassLiteral::Static(class_literal)), + ) + .map_or_else( + || { + Err(StaticMroErrorKind::InvalidBases(Box::from([( + 0, + *single_base, + )]))) + }, + |single_base| { + if single_base.has_cyclic_mro(db) { + Err(StaticMroErrorKind::InheritanceCycle) + } else { + Ok(std::iter::once(ClassBase::Class(class)) + .chain(single_base.mro(db, specialization)) + .collect()) + } + }, + ) + .map_err(|err| err.into_mro_error(db, class)) } // The class has multiple explicit bases. @@ -186,7 +190,7 @@ impl<'db> Mro<'db> { match ClassBase::try_from_type( db, *base, - ClassLiteral::Static(class_literal), + Some(ClassLiteral::Static(class_literal)), ) { Some(valid_base) => resolved_bases.push(valid_base), None => invalid_bases.push((i, *base)), @@ -261,7 +265,7 @@ impl<'db> Mro<'db> { let Some(base) = ClassBase::try_from_type( db, *base, - ClassLiteral::Static(class_literal), + Some(ClassLiteral::Static(class_literal)), ) else { continue; }; @@ -336,17 +340,12 @@ impl<'db> Mro<'db> { ) -> Result> { let original_bases = dynamic.explicit_bases(db); - // Use a placeholder class literal for try_from_type (the subclass parameter is only - // used for NamedTuple subclasses, which doesn't apply here). - let placeholder_class: ClassLiteral<'db> = - KnownClass::Object.try_to_class_literal(db).unwrap().into(); - // Convert Types to ClassBases, tracking any that fail conversion. let mut resolved_bases = Vec::with_capacity(original_bases.len()); let mut invalid_bases = Vec::new(); for (i, base_type) in original_bases.iter().enumerate() { - match ClassBase::try_from_type(db, *base_type, placeholder_class) { + match ClassBase::try_from_type(db, *base_type, None) { Some(class_base) => resolved_bases.push(class_base), None => invalid_bases.push((i, *base_type)), } @@ -429,14 +428,10 @@ impl<'db> Mro<'db> { let mut seen = FxHashSet::default(); seen.insert(self_base); - // Use a placeholder class literal for `try_from_type`. - let placeholder_class: ClassLiteral<'db> = - KnownClass::Object.try_to_class_literal(db).unwrap().into(); - for base_type in dynamic.explicit_bases(db) { // Convert `Type` to `ClassBase`, falling back to `Unknown` if conversion fails. - let base = ClassBase::try_from_type(db, *base_type, placeholder_class) - .unwrap_or_else(ClassBase::unknown); + let base = + ClassBase::try_from_type(db, *base_type, None).unwrap_or_else(ClassBase::unknown); for item in base.mro(db, None) { if seen.insert(item) { From 7c757225fc43ba933b092d0d7e3d2df8eabc5939 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 14 Feb 2026 12:45:26 -0500 Subject: [PATCH 17/18] Remove unreachable --- .../resources/mdtest/call/type.md | 14 ++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 6 ------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 5b0a7b32a8a76..a5476fac72935 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -1197,3 +1197,17 @@ FinalDerived = final(type("FinalDerived", (Base,), {})) class Child2(FinalDerived): ... ``` + +## Calling `type` via unannotated parameter + +When `type` is captured as an unannotated parameter default (a common Python optimization +pattern), the parameter type is inferred as `Unknown | type[type]`. This union bypasses the +early-return guard for `type()` calls, but should not panic. + +```py +def _check_type_strict(obj, t, type=type, tuple=tuple): + if type(t) is tuple: + return type(obj) in t + else: + return type(obj) is t +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 6602926bb3a6c..a6adf4e375f45 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -8189,12 +8189,6 @@ impl KnownClass { ))); } - // `type()` calls are handled by `infer_builtins_type_call` and never - // go through normal call inference, so this arm should be unreachable. - KnownClass::Type => { - unreachable!("three-argument `type()` calls are handled before `check_call`") - } - _ => {} } } From f68b713aa118bbe2b782b0801dc6bbc249fd8045 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 14 Feb 2026 12:52:42 -0500 Subject: [PATCH 18/18] Add TODO around why we need match arm --- .../resources/mdtest/call/type.md | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index a5476fac72935..67ed1935e06fe 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -1200,9 +1200,9 @@ class Child2(FinalDerived): ... ## Calling `type` via unannotated parameter -When `type` is captured as an unannotated parameter default (a common Python optimization -pattern), the parameter type is inferred as `Unknown | type[type]`. This union bypasses the -early-return guard for `type()` calls, but should not panic. +When `type` is captured as an unannotated parameter default (a common Python optimization pattern), +the parameter type is inferred as `Unknown | type[type]`. This union bypasses the early-return guard +for `type()` calls, but should not panic. ```py def _check_type_strict(obj, t, type=type, tuple=tuple): @@ -1211,3 +1211,21 @@ def _check_type_strict(obj, t, type=type, tuple=tuple): else: return type(obj) is t ``` + +## Three-argument `type()` in a union + +When `type` is one member of a union, three-argument `type()` calls go through normal call binding +instead of the early-return path, so dynamic class creation is missed. + +```py +def f(flag: bool): + if flag: + x = type + else: + x = int + + # TODO: should be `type[MyClass] | int`, but the `type` arm misses dynamic class creation + # because the early-return guard only matches `ClassLiteral`, not union members. + MyClass = x("MyClass", (), {}) # error: [no-matching-overload] + reveal_type(MyClass) # revealed: type | Unknown +```