[ty] Support 'dangling' type(...) constructors
This commit is contained in:
@@ -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<ExpressionNodeKey, ScopedUseId>,
|
||||
/// Maps potential synthesized-type call expressions to a call id for stable identity.
|
||||
tracked_calls_map: FxHashMap<ExpressionNodeKey, ScopedCallId>,
|
||||
/// Stores the ranges of tracked calls, indexed by their [`ScopedCallId`].
|
||||
/// Used for diagnostics (e.g., `header_range`).
|
||||
tracked_call_ranges: IndexVec<ScopedCallId, TextRange>,
|
||||
}
|
||||
|
||||
impl AstIds {
|
||||
fn use_id(&self, key: impl Into<ExpressionNodeKey>) -> 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<ExpressionNodeKey>) -> Option<ScopedCallId> {
|
||||
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<ExpressionNodeKey, ScopedUseId>,
|
||||
tracked_calls_map: FxHashMap<ExpressionNodeKey, ScopedCallId>,
|
||||
tracked_call_ranges: IndexVec<ScopedCallId, TextRange>,
|
||||
}
|
||||
|
||||
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<ExpressionNodeKey>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
|
||||
@@ -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<Definition<'db>> {
|
||||
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<TypeDefinition<'db>> {
|
||||
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<Definition<'db>> {
|
||||
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<TypeDefinition<'db>> {
|
||||
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<DataclassParams<'db>>,
|
||||
}
|
||||
|
||||
/// 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<Definition<'db>> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<Definition<'db>>,
|
||||
) -> Option<Type<'db>> {
|
||||
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.
|
||||
|
||||
@@ -303,14 +303,14 @@ impl<'db> TypedDictType<'db> {
|
||||
|
||||
pub fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
match self {
|
||||
TypedDictType::Class(defining_class) => Some(defining_class.definition(db)),
|
||||
TypedDictType::Class(defining_class) => defining_class.definition(db),
|
||||
TypedDictType::Synthesized(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
match self {
|
||||
TypedDictType::Class(defining_class) => Some(defining_class.type_definition(db)),
|
||||
TypedDictType::Class(defining_class) => defining_class.type_definition(db),
|
||||
TypedDictType::Synthesized(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user