diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 8769f15b8c..df5a21081c 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -20,16 +20,13 @@ class Base: ... class Mixin: ... # We synthesize a class type using the name argument -Foo = type("Foo", (), {}) -reveal_type(Foo) # revealed: +reveal_type(type("Foo", (), {})) # revealed: # With a single base class -Foo2 = type("Foo", (Base,), {"attr": 1}) -reveal_type(Foo2) # revealed: +reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: # With multiple base classes -Foo3 = type("Foo", (Base, Mixin), {}) -reveal_type(Foo3) # revealed: +reveal_type(type("Foo", (Base, Mixin), {})) # revealed: # The inferred type is assignable to type[Base] since Foo inherits from Base tests: list[type[Base]] = [] @@ -415,24 +412,22 @@ reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown ``` -The following calls are also invalid, due to incorrect argument types. - -Inline calls (not assigned to a variable) fall back to regular `type` overload matching, which -produces slightly different error messages than assigned dynamic class creation: +The following calls are also invalid, due to incorrect argument types: ```py class Base: ... -# error: 6 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`" +# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `Literal[b"Foo"]`" type(b"Foo", (), {}) -# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found ``" +# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found ``" type("Foo", Base, {}) -# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`" +# error: 14 [invalid-base] "Invalid class base with type `Literal[1]`" +# error: 17 [invalid-base] "Invalid class base with type `Literal[2]`" type("Foo", (1, 2), {}) -# error: 22 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`" +# error: [invalid-argument-type] "Invalid argument to parameter 3 (`namespace`) of `type()`: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`" type("Foo", (Base,), {b"attr": 1}) ``` @@ -656,20 +651,24 @@ def f(*args, **kwargs): ## Explicit type annotations -TODO: Annotated assignments with `type()` calls don't currently synthesize the specific class type. -This will be fixed when we support all `type()` calls (including inline) via generic handling. +When an explicit type annotation is provided, the inferred type is checked against it: ```py +# The annotation `type` is compatible with the inferred class literal type +T: type = type("T", (), {}) +reveal_type(T) # revealed: + +# The annotation `type[Base]` is compatible with the inferred type class Base: ... -# TODO: Should infer `` instead of `type` -T: type = type("T", (), {}) -reveal_type(T) # revealed: type - -# TODO: Should infer `` instead of `type[Base]} -# error: [invalid-assignment] "Object of type `type` is not assignable to `type[Base]`" Derived: type[Base] = type("Derived", (Base,), {}) -reveal_type(Derived) # revealed: type[Base] +reveal_type(Derived) # revealed: + +# Incompatible annotation produces an error +class Unrelated: ... + +# error: [invalid-assignment] +Bad: type[Unrelated] = type("Bad", (Base,), {}) ``` ## Special base classes diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md index c4c6332ebb..7053a74f35 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md @@ -169,12 +169,15 @@ Narrowing does not occur in the same way if `type` is used to dynamically create ```py def _(x: str | int): - # Inline type() calls fall back to regular type overload matching. - # TODO: Once inline type() calls synthesize class types, this should narrow x to Never. + # The following diagnostic is valid, since the three-argument form of `type` + # can only be called with `str` as the first argument. # - # error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`" + # error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `str | int`" if type(x, (), {}) is str: - reveal_type(x) # revealed: str | int + # But we synthesize a new class object as the result of a three-argument call to `type`, + # and we know that this synthesized class object is not the same object as the `str` class object, + # so here the type is narrowed to `Never`! + reveal_type(x) # revealed: Never else: reveal_type(x) # revealed: str | int ``` diff --git a/crates/ty_python_semantic/src/semantic_index/ast_ids.rs b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs index cc2c65526e..514984674f 100644 --- a/crates/ty_python_semantic/src/semantic_index/ast_ids.rs +++ b/crates/ty_python_semantic/src/semantic_index/ast_ids.rs @@ -1,8 +1,8 @@ -use rustc_hash::FxHashMap; - -use ruff_index::newtype_index; +use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast as ast; use ruff_python_ast::ExprRef; +use ruff_text_size::TextRange; +use rustc_hash::FxHashMap; use crate::Db; use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; @@ -28,12 +28,27 @@ use crate::semantic_index::semantic_index; pub(crate) struct AstIds { /// Maps expressions which "use" a place (that is, [`ast::ExprName`], [`ast::ExprAttribute`] or [`ast::ExprSubscript`]) to a use id. uses_map: FxHashMap, + /// Maps potential synthesized-type call expressions to a call id for stable identity. + tracked_calls_map: FxHashMap, + /// Stores the ranges of tracked calls, indexed by their [`ScopedCallId`]. + /// Used for diagnostics (e.g., `header_range`). + tracked_call_ranges: IndexVec, } impl AstIds { fn use_id(&self, key: impl Into) -> ScopedUseId { self.uses_map[&key.into()] } + + /// Returns the call ID for a potential synthesized-type call, if it was tracked during semantic indexing. + pub(crate) fn try_call_id(&self, key: impl Into) -> Option { + self.tracked_calls_map.get(&key.into()).copied() + } + + /// Returns the range of a tracked call by its ID. + pub(crate) fn call_range(&self, id: ScopedCallId) -> TextRange { + self.tracked_call_ranges[id] + } } fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds { @@ -45,6 +60,15 @@ fn ast_ids<'db>(db: &'db dyn Db, scope: ScopeId) -> &'db AstIds { #[derive(get_size2::GetSize)] pub struct ScopedUseId; +/// Uniquely identifies a potential synthesized-type call in a [`crate::semantic_index::FileScopeId`]. +/// +/// This is used to provide stable identity for inline calls that create synthesized types, +/// such as `type()`, `NamedTuple()`, `TypedDict()`, etc. The ID is assigned during semantic +/// indexing for calls that match known patterns for these synthesizers. +#[newtype_index] +#[derive(get_size2::GetSize)] +pub struct ScopedCallId; + pub trait HasScopedUseId { /// Returns the ID that uniquely identifies the use in `scope`. fn scoped_use_id(&self, db: &dyn Db, scope: ScopeId) -> ScopedUseId; @@ -88,6 +112,8 @@ impl HasScopedUseId for ast::ExprRef<'_> { #[derive(Debug, Default)] pub(super) struct AstIdsBuilder { uses_map: FxHashMap, + tracked_calls_map: FxHashMap, + tracked_call_ranges: IndexVec, } impl AstIdsBuilder { @@ -100,11 +126,25 @@ impl AstIdsBuilder { use_id } + /// Records a potential synthesized-type call for stable identity tracking. + pub(super) fn record_call( + &mut self, + expr: impl Into, + range: TextRange, + ) -> ScopedCallId { + let call_id = self.tracked_call_ranges.push(range); + self.tracked_calls_map.insert(expr.into(), call_id); + call_id + } + pub(super) fn finish(mut self) -> AstIds { self.uses_map.shrink_to_fit(); + self.tracked_calls_map.shrink_to_fit(); AstIds { uses_map: self.uses_map, + tracked_calls_map: self.tracked_calls_map, + tracked_call_ranges: self.tracked_call_ranges, } } } diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 3ef83e97d2..61945b5681 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -14,7 +14,7 @@ use ruff_python_ast::{self as ast, NodeIndex, PySourceType, PythonVersion}; use ruff_python_parser::semantic_errors::{ SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind, }; -use ruff_text_size::TextRange; +use ruff_text_size::{Ranged, TextRange}; use ty_module_resolver::{ModuleName, resolve_module}; use crate::ast_node_ref::AstNodeRef; @@ -2741,6 +2741,17 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { } walk_expr(self, expr); } + ast::Expr::Call(call_expr) => { + // Track potential synthesized-type calls for stable identity. + // Assigned calls use `Definition` for identity; inline calls need a `ScopedCallId`. + if self.current_assignment().is_none() + && is_potential_synthesized_type_call(call_expr) + { + self.current_ast_ids() + .record_call(call_expr, call_expr.range()); + } + walk_expr(self, expr); + } _ => { walk_expr(self, expr); } @@ -3195,3 +3206,31 @@ fn is_if_not_type_checking(expr: &ast::Expr) -> bool { }) if is_if_type_checking(operand) ) } + +/// Returns whether a call expression might create a synthesized type. +/// +/// This is a heuristic used during semantic indexing to assign stable IDs +/// to calls that may produce `NamedTuple`, `TypedDict`, `type()` classes, etc. +/// False positives are acceptable (the ID just won't be used during inference). +fn is_potential_synthesized_type_call(call: &ast::ExprCall) -> bool { + // Check for `type(...)` or `builtins.type(...)` + let is_type_call = match call.func.as_ref() { + ast::Expr::Name(name) => name.id.as_str() == "type", + ast::Expr::Attribute(attr) => { + attr.attr.as_str() == "type" + && matches!(attr.value.as_ref(), ast::Expr::Name(name) if name.id.as_str() == "builtins") + } + _ => false, + }; + + if is_type_call { + // type("Name", bases, dict) + return call.arguments.keywords.is_empty() && call.arguments.args.len() == 3; + } + + // TODO: Add more patterns as we support them: + // - NamedTuple("Name", [...]) or NamedTuple("Name", field1=type1, ...) + // - TypedDict("Name", {...}) or TypedDict("Name", field1=type1, ...) + + false +} diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 76cc4684b1..4c9df1bf3c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6529,9 +6529,9 @@ impl<'db> Type<'db> { Some(TypeDefinition::Function(function.definition(db))) } Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))), - Self::ClassLiteral(class_literal) => Some(class_literal.type_definition(db)), + Self::ClassLiteral(class_literal) => class_literal.type_definition(db), Self::GenericAlias(alias) => Some(TypeDefinition::StaticClass(alias.definition(db))), - Self::NominalInstance(instance) => Some(instance.class(db).type_definition(db)), + Self::NominalInstance(instance) => instance.class(db).type_definition(db), Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { Some(TypeDefinition::TypeVar(var.definition(db)?)) @@ -6545,7 +6545,7 @@ impl<'db> Type<'db> { Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { SubclassOfInner::Dynamic(_) => None, - SubclassOfInner::Class(class) => Some(class.type_definition(db)), + SubclassOfInner::Class(class) => class.type_definition(db), SubclassOfInner::TypeVar(bound_typevar) => { Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)) } @@ -6575,7 +6575,7 @@ impl<'db> Type<'db> { Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)), Self::ProtocolInstance(protocol) => match protocol.inner { - Protocol::FromClass(class) => Some(class.type_definition(db)), + Protocol::FromClass(class) => class.type_definition(db), Protocol::Synthesized(_) => None, }, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index ba4f3b61fc..0d4ed7360a 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -10,11 +10,13 @@ use super::{ function::FunctionType, }; use crate::place::{DefinedPlace, TypeOrigin}; -use crate::semantic_index::definition::{Definition, DefinitionState}; -use crate::semantic_index::scope::{NodeWithScopeKind, Scope, ScopeKind}; +use crate::semantic_index::ast_ids::ScopedCallId; +use crate::semantic_index::definition::{Definition, DefinitionKind, DefinitionState}; +use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind, Scope, ScopeKind}; use crate::semantic_index::symbol::Symbol; use crate::semantic_index::{ DeclarationWithConstraint, SemanticIndex, attribute_declarations, attribute_scopes, + semantic_index, }; use crate::types::bound_super::BoundSuperError; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; @@ -57,11 +59,7 @@ use crate::{ known_module_symbol, place_from_bindings, place_from_declarations, }, semantic_index::{ - attribute_assignments, - definition::{DefinitionKind, TargetKind}, - place_table, - scope::{FileScopeId, ScopeId}, - semantic_index, use_def_map, + attribute_assignments, definition::TargetKind, place_table, scope::ScopeId, use_def_map, }, types::{ CallArguments, CallError, CallErrorKind, MetaclassCandidate, TypeDefinition, UnionType, @@ -668,10 +666,10 @@ impl<'db> ClassLiteral<'db> { } } - /// Returns the definition of this class. - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + /// Returns the definition of this class, if available. + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { match self { - Self::Static(class) => class.definition(db), + Self::Static(class) => Some(class.definition(db)), Self::Dynamic(class) => class.definition(db), } } @@ -679,11 +677,11 @@ impl<'db> ClassLiteral<'db> { /// Returns the type definition for this class. /// /// For static classes, returns `TypeDefinition::StaticClass`. - /// For dynamic classes, returns `TypeDefinition::DynamicClass`. - pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> { + /// For dynamic classes, returns `TypeDefinition::DynamicClass` if a definition is available. + pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option> { match self { - Self::Static(class) => TypeDefinition::StaticClass(class.definition(db)), - Self::Dynamic(class) => TypeDefinition::DynamicClass(class.definition(db)), + Self::Static(class) => Some(TypeDefinition::StaticClass(class.definition(db))), + Self::Dynamic(class) => class.definition(db).map(TypeDefinition::DynamicClass), } } @@ -941,13 +939,13 @@ impl<'db> ClassType<'db> { self.class_literal(db).known(db) } - /// Returns the definition for this class. - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + /// Returns the definition for this class, if available. + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { self.class_literal(db).definition(db) } /// Returns the type definition for this class. - pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> { + pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option> { self.class_literal(db).type_definition(db) } @@ -4680,21 +4678,18 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { /// /// # Salsa interning /// -/// Each `type()` call is uniquely identified by its [`Definition`], which provides -/// stable identity without depending on AST node indices that can change when code -/// is inserted above the call site. +/// Each `type()` call is uniquely identified by its [`DynamicClassOrigin`], which provides +/// stable identity without depending on AST node indices that can change when code is +/// inserted above the call site. /// -/// Two different `type()` calls always produce distinct `DynamicClassLiteral` -/// instances, even if they have the same name and bases: +/// Two different `type()` calls always produce distinct `DynamicClassLiteral` instances, +/// even if they have the same name and bases: /// /// ```python /// Foo1 = type("Foo", (Base,), {}) /// Foo2 = type("Foo", (Base,), {}) /// # Foo1 and Foo2 are distinct types /// ``` -/// -/// Note: Only assigned `type()` calls are currently supported (e.g., `Foo = type(...)`). -/// Inline calls like `process(type(...))` fall back to normal call handling. #[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] #[derive(PartialOrd, Ord)] pub struct DynamicClassLiteral<'db> { @@ -4706,14 +4701,38 @@ pub struct DynamicClassLiteral<'db> { #[returns(deref)] pub bases: Box<[ClassBase<'db>]>, - /// The definition where this class is created. - pub definition: Definition<'db>, + /// The origin of this dynamic class, providing stable identity. + pub origin: DynamicClassOrigin<'db>, /// Dataclass parameters if this class has been wrapped with `@dataclass` decorator /// or passed to `dataclass()` as a function. pub dataclass_params: Option>, } +/// The origin of a dynamically created class, used for stable identity in Salsa. +/// +/// This enum provides stable identification for `type()` calls without relying on +/// AST node indices that change when code is inserted above the call site. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum DynamicClassOrigin<'db> { + /// A `type()` call that is assigned to a variable (e.g., `Foo = type("Foo", (), {})`). + /// + /// The Definition provides stable identity via its `ScopedPlaceId`. + Assigned(Definition<'db>), + + /// An inline `type()` call not assigned to a variable (e.g., `process(type("Foo", (), {}))`). + /// + /// Uses file, scope, and a scoped call ID for stable identity. + /// The ID is assigned sequentially during semantic indexing. + Inline { + file: File, + file_scope: FileScopeId, + call_id: ScopedCallId, + }, +} + +impl get_size2::GetSize for DynamicClassOrigin<'_> {} + impl get_size2::GetSize for DynamicClassLiteral<'_> {} #[salsa::tracked] @@ -4727,25 +4746,52 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns the range of the `type()` call expression that created this class. pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange { - let definition = self.definition(db); - let file = definition.file(db); - let module = parsed_module(db, file).load(db); - - // Dynamic classes are only created from regular assignments (e.g., `Foo = type(...)`). - let DefinitionKind::Assignment(assignment) = definition.kind(db) else { - unreachable!("DynamicClassLiteral should only be created from Assignment definitions"); - }; - assignment.value(&module).range() + match self.origin(db) { + DynamicClassOrigin::Assigned(definition) => { + // For assigned calls, get the range from the assignment value. + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + unreachable!( + "DynamicClassOrigin::Assigned should only be created from Assignment definitions" + ); + }; + assignment.value(&module).range() + } + DynamicClassOrigin::Inline { + file, + file_scope, + call_id, + } => { + // For inline calls, look up the range from the semantic index. + let index = semantic_index(db, file); + index.ast_ids(file_scope).call_range(call_id) + } + } } /// Returns the file containing the `type()` call. pub(crate) fn file(self, db: &'db dyn Db) -> File { - self.definition(db).file(db) + match self.origin(db) { + DynamicClassOrigin::Assigned(definition) => definition.file(db), + DynamicClassOrigin::Inline { file, .. } => file, + } } /// Returns the scope containing the `type()` call. pub(crate) fn file_scope(self, db: &'db dyn Db) -> FileScopeId { - self.definition(db).file_scope(db) + match self.origin(db) { + DynamicClassOrigin::Assigned(definition) => definition.file_scope(db), + DynamicClassOrigin::Inline { file_scope, .. } => file_scope, + } + } + + /// Returns the definition where this class is created, if in an assignment context. + pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { + match self.origin(db) { + DynamicClassOrigin::Assigned(definition) => Some(definition), + DynamicClassOrigin::Inline { .. } => None, + } } /// Get the metaclass of this dynamic class. @@ -4919,7 +4965,7 @@ impl<'db> DynamicClassLiteral<'db> { db, self.name(db).clone(), self.bases(db), - self.definition(db), + self.origin(db), dataclass_params, ) } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index b467f08bbe..290daac22a 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -96,7 +96,7 @@ pub(crate) fn typing_self<'db>( let identity = TypeVarIdentity::new( db, ast::name::Name::new_static("Self"), - Some(class.definition(db)), + class.definition(db), TypeVarKind::TypingSelf, ); let bounds = TypeVarBoundOrConstraints::UpperBound(Type::instance( diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 70fa611c77..73497948b1 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -169,9 +169,9 @@ pub fn definitions_for_name<'db>( // instead of `int` (hover only shows the docstring of the first definition). .rev() .filter_map(|ty| ty.as_nominal_instance()) - .map(|instance| { - let definition = instance.class_literal(db).definition(db); - ResolvedDefinition::Definition(definition) + .filter_map(|instance| { + let definition = instance.class_literal(db).definition(db)?; + Some(ResolvedDefinition::Definition(definition)) }) .collect(); } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9abed64628..e022a0c50d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -48,14 +48,14 @@ use crate::semantic_index::scope::{ }; use crate::semantic_index::symbol::{ScopedSymbolId, Symbol}; use crate::semantic_index::{ - ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, + ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, place_table, semantic_index, }; use crate::subscript::{PyIndex, PySlice}; use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex}; use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::class::{ - ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, DynamicMetaclassConflict, FieldKind, - MetaclassErrorKind, MethodDecorator, + ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, DynamicClassOrigin, + DynamicMetaclassConflict, FieldKind, MetaclassErrorKind, MethodDecorator, }; use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::cyclic::CycleDetector; @@ -5412,7 +5412,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Try to extract the dynamic class with definition. // This returns `None` if it's not a three-arg call to `type()`, // signalling that we must fall back to normal call inference. - self.infer_dynamic_type_expression(call_expr, definition) + self.infer_dynamic_type_expression(call_expr, Some(definition)) .unwrap_or_else(|| { self.infer_call_expression_impl(call_expr, callable_type, tcx) }) @@ -6031,7 +6031,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_dynamic_type_expression( &mut self, call_expr: &ast::ExprCall, - definition: Definition<'db>, + definition: Option>, ) -> Option> { let db = self.db(); @@ -6096,7 +6096,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bases = self.extract_dynamic_type_bases(bases_arg, bases_type, &name); - let dynamic_class = DynamicClassLiteral::new(db, name, bases, definition, None); + // Get the origin for this dynamic class. + let origin = if let Some(def) = definition { + // Assigned call: use the definition for stable identity. + DynamicClassOrigin::Assigned(def) + } else { + // Inline call: look up the ScopedCallId from semantic indexing. + let file = self.file(); + let file_scope = self.scope().file_scope_id(db); + let index = semantic_index(db, file); + let Some(call_id) = index.ast_ids(file_scope).try_call_id(call_expr) else { + // If no call ID was tracked for this call during semantic indexing, + // we can't create a stable DynamicClassLiteral. Fall back to regular + // type inference. + return None; + }; + DynamicClassOrigin::Inline { + file, + file_scope, + call_id, + } + }; + + let dynamic_class = DynamicClassLiteral::new(db, name, bases, origin, None); // Check for MRO errors. if let Err(error) = dynamic_class.try_mro(db) { @@ -9079,6 +9101,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Type::TypedDict(typed_dict); } + // Handle 3-argument `type(name, bases, dict)`. + if let Type::ClassLiteral(class) = callable_type + && class.is_known(self.db(), KnownClass::Type) + && let Some(dynamic_type) = self.infer_dynamic_type_expression(call_expression, None) + { + return dynamic_type; + } + // We don't call `Type::try_call`, because we want to perform type inference on the // arguments after matching them to parameters, but before checking that the argument types // are assignable to any parameter annotations. diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index d7a93d8bcf..66543694ad 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -303,14 +303,14 @@ impl<'db> TypedDictType<'db> { pub fn definition(self, db: &'db dyn Db) -> Option> { match self { - TypedDictType::Class(defining_class) => Some(defining_class.definition(db)), + TypedDictType::Class(defining_class) => defining_class.definition(db), TypedDictType::Synthesized(_) => None, } } pub fn type_definition(self, db: &'db dyn Db) -> Option> { match self { - TypedDictType::Class(defining_class) => Some(defining_class.type_definition(db)), + TypedDictType::Class(defining_class) => defining_class.type_definition(db), TypedDictType::Synthesized(_) => None, } }