Compare commits

...

3 Commits

Author SHA1 Message Date
Charlie Marsh
597c35458e Use a tracked struct 2026-01-12 15:21:50 -05:00
Charlie Marsh
b88db87755 Revert "[ty] Support 'dangling' type(...) constructors"
This reverts commit 60d4dcbd9d41279aaa693fa6d3a385f2c5caa2bf.
2026-01-12 15:21:50 -05:00
Charlie Marsh
3932ef7c46 [ty] Support 'dangling' type(...) constructors 2026-01-12 15:21:50 -05:00
9 changed files with 102 additions and 83 deletions

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]] = []
@@ -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 `<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})
```
@@ -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: <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

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

View File

@@ -67,7 +67,7 @@ where
///
/// This method may panic or produce unspecified results if the provided module is from a
/// different file or Salsa revision than the module to which the node belongs.
pub(super) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self {
pub(crate) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self {
let index = node.node_index().load();
debug_assert_eq!(module_ref.get_by_index(index).try_into().ok(), Some(node));

View File

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

View File

@@ -52,6 +52,7 @@ use crate::types::{
};
use crate::{
Db, FxIndexMap, FxIndexSet, FxOrderSet, Program,
ast_node_ref::AstNodeRef,
place::{
Definedness, LookupError, LookupResult, Place, PlaceAndQualifiers, Widening,
known_module_symbol, place_from_bindings, place_from_declarations,
@@ -668,10 +669,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 +680,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 +942,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)
}
@@ -4678,14 +4679,10 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
/// Supporting namespace dict attributes would require parsing dict literals and tracking
/// the attribute types, similar to how TypedDict handles its fields.
///
/// # Salsa interning
/// # Salsa tracking
///
/// 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 tracked struct. Two different `type()` calls always produce
/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases:
///
/// ```python
/// Foo1 = type("Foo", (Base,), {})
@@ -4693,9 +4690,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 `_node_ref` field is marked with `#[tracked]` and `#[no_eq]`, which means
/// it doesn't participate in struct equality comparisons. Instead, accesses to
/// that field are tracked as separate dependencies, providing stable identity
/// across AST changes within the same scope.
#[salsa::tracked(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()`).
@@ -4706,8 +4705,26 @@ pub struct DynamicClassLiteral<'db> {
#[returns(deref)]
pub bases: Box<[ClassBase<'db>]>,
/// The definition where this class is created.
pub definition: Definition<'db>,
/// The file containing the `type()` call.
pub file: File,
/// The scope containing the `type()` call.
pub file_scope: FileScopeId,
/// The AST node reference for the `type()` call expression.
///
/// WARNING: Only access this field when doing type inference for the same
/// file as where `DynamicClassLiteral` is defined to avoid cross-file query dependencies.
#[no_eq]
#[tracked]
#[returns(ref)]
pub(crate) _node_ref: AstNodeRef<ast::ExprCall>,
/// The definition where this class is created (if in an assignment context).
///
/// This is `Some` when the `type()` call is part of an assignment,
/// allowing go-to-definition to navigate to the creation site.
pub definition: Option<Definition<'db>>,
/// Dataclass parameters if this class has been wrapped with `@dataclass` decorator
/// or passed to `dataclass()` as a function.
@@ -4727,25 +4744,8 @@ 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()
}
/// 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)
let module = parsed_module(db, self.file(db)).load(db);
self._node_ref(db).node(&module).range()
}
/// Get the metaclass of this dynamic class.
@@ -4918,7 +4918,10 @@ impl<'db> DynamicClassLiteral<'db> {
Self::new(
db,
self.name(db).clone(),
self.bases(db),
self.bases(db).into(),
self.file(db),
self.file_scope(db),
self._node_ref(db).clone(),
self.definition(db),
dataclass_params,
)

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

@@ -24,6 +24,7 @@ use super::{
infer_definition_types, infer_expression_types, infer_same_file_expression_type,
infer_unpack_types,
};
use crate::ast_node_ref::AstNodeRef;
use crate::diagnostic::format_enumeration;
use crate::node_key::NodeKey;
use crate::place::{
@@ -5412,7 +5413,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 +6032,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 +6097,12 @@ 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);
let file = self.file();
let file_scope = self.scope().file_scope_id(db);
let node_ref = AstNodeRef::new(self.module(), call_expr);
let dynamic_class = DynamicClassLiteral::new(
db, name, bases, file, file_scope, node_ref, definition, None,
);
// Check for MRO errors.
if let Err(error) = dynamic_class.try_mro(db) {
@@ -9079,6 +9085,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,
}
}