[ty] Support 'dangling' type(...) constructors (#22537)

## Summary

This PR adds support for 'dangling' `type(...)` constructors, e.g.:

```python
class Foo(type("Bar", ...)):
   ...
```

As opposed to:

```python
Bar = type("Bar", ...)
```

The former doesn't have a `Definition` since it doesn't get bound to a
place, so we instead need to store the `NodeIndex`. Per @MichaReiser's
suggestion, we can use a Salsa tracked struct for this.
This commit is contained in:
Charlie Marsh
2026-01-14 08:48:53 -05:00
committed by GitHub
parent 853bb00626
commit b24afb643c
12 changed files with 310 additions and 86 deletions

View File

@@ -1649,6 +1649,65 @@ Traceb<CURSOR>ackType
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
/// goto-definition on a dynamic class literal (created via `type()`)
#[test]
fn goto_definition_dynamic_class_literal() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
DynClass = type("DynClass", (), {})
x = DynCla<CURSOR>ss()
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Go to definition
--> main.py:4:5
|
2 | DynClass = type("DynClass", (), {})
3 |
4 | x = DynClass()
| ^^^^^^^^ Clicking here
|
info: Found 2 definitions
--> main.py:2:1
|
2 | DynClass = type("DynClass", (), {})
| --------
3 |
4 | x = DynClass()
|
::: stdlib/builtins.pyi:137:9
|
135 | def __class__(self, type: type[Self], /) -> None: ...
136 | def __init__(self) -> None: ...
137 | def __new__(cls) -> Self: ...
| -------
138 | # N.B. `object.__setattr__` and `object.__delattr__` are heavily special-cased by type checkers.
139 | # Overriding them in subclasses has different semantics, even if the override has an identical signature.
|
"#);
}
/// goto-definition on a dangling dynamic class literal (not assigned to a variable)
#[test]
fn goto_definition_dangling_dynamic_class_literal() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
class Foo(type("Ba<CURSOR>r", (), {})):
pass
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
// TODO: Should only list `a: int`
#[test]
fn redeclarations() {

View File

@@ -20,16 +20,13 @@ class Base: ...
class Mixin: ...
# We synthesize a class type using the name argument
Foo = type("Foo", (), {})
reveal_type(Foo) # revealed: <class 'Foo'>
reveal_type(type("Foo", (), {})) # revealed: <class 'Foo'>
# With a single base class
Foo2 = type("Foo", (Base,), {"attr": 1})
reveal_type(Foo2) # revealed: <class 'Foo'>
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: <class 'Foo'>
# With multiple base classes
Foo3 = type("Foo", (Base, Mixin), {})
reveal_type(Foo3) # revealed: <class 'Foo'>
reveal_type(type("Foo", (Base, Mixin), {})) # revealed: <class 'Foo'>
# The inferred type is assignable to type[Base] since Foo inherits from Base
tests: list[type[Base]] = []
@@ -514,24 +511,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 `<class 'Base'>`"
# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found `<class 'Base'>`"
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})
```
@@ -598,6 +593,19 @@ class Y(C, B): ...
Conflict = type("Conflict", (X, Y), {})
```
## MRO error highlighting (snapshot)
<!-- snapshot-diagnostics -->
This snapshot test documents the diagnostic highlighting range for dynamic class literals.
Currently, the entire `type()` call expression is highlighted:
```py
class A: ...
Dup = type("Dup", (A, A), {}) # error: [duplicate-base]
```
## Metaclass conflicts
Metaclass conflicts are detected and reported:
@@ -877,20 +885,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: <class 'T'>
# The annotation `type[Base]` is compatible with the inferred type
class Base: ...
# TODO: Should infer `<class 'T'>` instead of `type`
T: type = type("T", (), {})
reveal_type(T) # revealed: type
# TODO: Should infer `<class 'Derived'>` 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: <class 'Derived'>
# Incompatible annotation produces an error
class Unrelated: ...
# error: [invalid-assignment]
Bad: type[Unrelated] = type("Bad", (Base,), {})
```
## Special base classes

View File

@@ -210,12 +210,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
```

View File

@@ -0,0 +1,34 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: type.md - Calls to `type()` - MRO error highlighting (snapshot)
mdtest path: crates/ty_python_semantic/resources/mdtest/call/type.md
---
# Python source files
## mdtest_snippet.py
```
1 | class A: ...
2 |
3 | Dup = type("Dup", (A, A), {}) # error: [duplicate-base]
```
# Diagnostics
```
error[duplicate-base]: Duplicate base class <class 'A'> in class `Dup`
--> src/mdtest_snippet.py:3:7
|
1 | class A: ...
2 |
3 | Dup = type("Dup", (A, A), {}) # error: [duplicate-base]
| ^^^^^^^^^^^^^^^^^^^^^^^
|
info: rule `duplicate-base` is enabled by default
```

View File

@@ -938,6 +938,18 @@ impl DefinitionKind<'_> {
| DefinitionKind::ExceptHandler(_) => DefinitionCategory::Binding,
}
}
/// Returns the value expression for assignment-based definitions.
///
/// Returns `Some` for `Assignment` and `AnnotatedAssignment` (if it has a value),
/// `None` for all other definition kinds.
pub(crate) fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> Option<&'ast ast::Expr> {
match self {
DefinitionKind::Assignment(assignment) => Some(assignment.value(module)),
DefinitionKind::AnnotatedAssignment(assignment) => assignment.value(module),
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Hash, get_size2::GetSize)]

View File

@@ -2,7 +2,7 @@ use std::ops::Range;
use ruff_db::{files::File, parsed::ParsedModuleRef};
use ruff_index::newtype_index;
use ruff_python_ast as ast;
use ruff_python_ast::{self as ast, NodeIndex};
use crate::{
Db,
@@ -463,6 +463,27 @@ impl NodeWithScopeKind {
_ => None,
}
}
/// Returns the anchor node index for this scope, or `None` for the module scope.
///
/// This is used to compute relative node indices for expressions within the scope,
/// providing a stable anchor that only changes when the scope-introducing node changes.
pub(crate) fn node_index(&self) -> Option<NodeIndex> {
match self {
Self::Module => None,
Self::Class(class) => Some(class.index()),
Self::ClassTypeParameters(class) => Some(class.index()),
Self::Function(function) => Some(function.index()),
Self::FunctionTypeParameters(function) => Some(function.index()),
Self::TypeAlias(type_alias) => Some(type_alias.index()),
Self::TypeAliasTypeParameters(type_alias) => Some(type_alias.index()),
Self::Lambda(lambda) => Some(lambda.index()),
Self::ListComprehension(comp) => Some(comp.index()),
Self::SetComprehension(comp) => Some(comp.index()),
Self::DictComprehension(comp) => Some(comp.index()),
Self::GeneratorExpression(generator) => Some(generator.index()),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]

View File

@@ -6559,9 +6559,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)?))
@@ -6575,7 +6575,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)?))
}
@@ -6605,7 +6605,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,
},

View File

@@ -60,7 +60,7 @@ use crate::{
attribute_assignments,
definition::{DefinitionKind, TargetKind},
place_table,
scope::{FileScopeId, ScopeId},
scope::ScopeId,
semantic_index, use_def_map,
},
types::{
@@ -74,7 +74,7 @@ use ruff_db::diagnostic::Span;
use ruff_db::files::File;
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, PythonVersion};
use ruff_python_ast::{self as ast, NodeIndex, PythonVersion};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use ty_module_resolver::{KnownModule, file_to_module};
@@ -613,7 +613,7 @@ impl<'db> ClassLiteral<'db> {
pub(crate) fn file(self, db: &dyn Db) -> File {
match self {
Self::Static(class) => class.file(db),
Self::Dynamic(class) => class.file(db),
Self::Dynamic(class) => class.scope(db).file(db),
}
}
@@ -664,10 +664,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),
}
}
@@ -675,11 +675,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),
}
}
@@ -944,13 +944,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)
}
@@ -4704,12 +4704,8 @@ 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.
///
/// Two different `type()` calls always produce distinct `DynamicClassLiteral`
/// instances, even if they have the same name and bases:
/// This is a Salsa-interned struct. Two different `type()` calls always produce
/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases:
///
/// ```python
/// Foo1 = type("Foo", (Base,), {})
@@ -4717,9 +4713,11 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
/// # 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)]
/// The `anchor` field provides stable identity:
/// - For assigned `type()` calls, the `Definition` uniquely identifies the class.
/// - For dangling `type()` calls, a relative node offset anchored to the enclosing scope
/// provides stable identity that only changes when the scope itself changes.
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
#[derive(PartialOrd, Ord)]
pub struct DynamicClassLiteral<'db> {
/// The name of the class (from the first argument to `type()`).
@@ -4730,8 +4728,13 @@ pub struct DynamicClassLiteral<'db> {
#[returns(deref)]
pub bases: Box<[ClassBase<'db>]>,
/// The definition where this class is created.
pub definition: Definition<'db>,
/// The anchor for this dynamic class, providing stable identity.
///
/// - `Definition`: The `type()` call is assigned to a variable. The definition
/// uniquely identifies this class and can be used to find the `type()` call.
/// - `ScopeOffset`: The `type()` call is "dangling" (not assigned). The offset
/// is relative to the enclosing scope's anchor node index.
pub anchor: DynamicClassAnchor<'db>,
/// The class members from the namespace dict (third argument to `type()`).
/// Each entry is a (name, type) pair extracted from the dict literal.
@@ -4748,38 +4751,87 @@ pub struct DynamicClassLiteral<'db> {
pub dataclass_params: Option<DataclassParams<'db>>,
}
/// Anchor for identifying a dynamic class literal.
///
/// 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,
)]
pub enum DynamicClassAnchor<'db> {
/// The `type()` call is assigned to a variable.
///
/// The `Definition` uniquely identifies this class. The `type()` call expression
/// is the `value` of the assignment, so we can get its range from the definition.
Definition(Definition<'db>),
/// The `type()` call is "dangling" (not assigned to a variable).
///
/// The 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 },
}
impl get_size2::GetSize for DynamicClassLiteral<'_> {}
#[salsa::tracked]
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<Definition<'db>> {
match self.anchor(db) {
DynamicClassAnchor::Definition(definition) => Some(definition),
DynamicClassAnchor::ScopeOffset { .. } => None,
}
}
/// Returns the scope in which this dynamic class was created.
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,
}
}
/// Returns a [`Span`] with the range of the `type()` call expression.
///
/// See [`Self::header_range`] for more details.
pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
Span::from(self.file(db)).with_range(self.header_range(db))
Span::from(self.scope(db).file(db)).with_range(self.header_range(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 scope = self.scope(db);
let file = scope.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.anchor(db) {
DynamicClassAnchor::Definition(definition) => {
// For definitions, get the range from the definition's value.
// The `type()` call is the value of the assignment.
definition
.kind(db)
.value(&module)
.expect("DynamicClassAnchor::Definition should only be used for assignments")
.range()
}
DynamicClassAnchor::ScopeOffset { offset, .. } => {
// For dangling `type()` calls, compute the absolute index from the 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);
/// Returns the file containing the `type()` call.
pub(crate) fn file(self, db: &'db dyn Db) -> File {
self.definition(db).file(db)
}
/// Returns the scope containing the `type()` call.
pub(crate) fn file_scope(self, db: &'db dyn Db) -> FileScopeId {
self.definition(db).file_scope(db)
// Get the node and return its range.
let node: &ast::ExprCall = module
.get_by_index(absolute_index)
.try_into()
.expect("scope offset should point to ExprCall");
node.range()
}
}
}
/// Get the metaclass of this dynamic class.
@@ -5020,7 +5072,7 @@ impl<'db> DynamicClassLiteral<'db> {
db,
self.name(db).clone(),
self.bases(db),
self.definition(db),
self.anchor(db),
self.members(db),
self.has_dynamic_namespace(db),
dataclass_params,
@@ -5314,7 +5366,8 @@ impl<'db> QualifiedClassName<'db> {
}
ClassLiteral::Dynamic(class) => {
// Dynamic classes don't have a body scope; start from the enclosing scope.
(class.file(self.db), class.file_scope(self.db), 0)
let scope = class.scope(self.db);
(scope.file(self.db), scope.file_scope_id(self.db), 0)
}
};

View File

@@ -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(

View File

@@ -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();
}

View File

@@ -55,8 +55,8 @@ use crate::subscript::{PyIndex, PySlice};
use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex};
use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind};
use crate::types::class::{
ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, DynamicMetaclassConflict, FieldKind,
MetaclassErrorKind, MethodDecorator,
ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral,
DynamicMetaclassConflict, FieldKind, MetaclassErrorKind, MethodDecorator,
};
use crate::types::context::{InNoTypeCheck, InferContext};
use crate::types::cyclic::CycleDetector;
@@ -5581,7 +5581,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)
})
@@ -6200,7 +6200,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();
@@ -6310,11 +6310,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
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.
// - For assigned `type()` calls, the Definition uniquely identifies the class.
// - For dangling calls, compute a relative offset from the scope's node index.
let anchor = if let Some(def) = definition {
DynamicClassAnchor::Definition(def)
} else {
let call_node_index = call_expr.node_index().load();
let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0));
let anchor_u32 = scope_anchor
.as_u32()
.expect("scope anchor should not be NodeIndex::NONE");
let call_u32 = call_node_index
.as_u32()
.expect("call node should not be NodeIndex::NONE");
DynamicClassAnchor::ScopeOffset {
scope,
offset: call_u32 - anchor_u32,
}
};
let dynamic_class = DynamicClassLiteral::new(
db,
name,
bases,
definition,
anchor,
members,
has_dynamic_namespace,
None,
@@ -9514,6 +9536,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.

View File

@@ -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,
}
}